I’m hoping that Java 14 records actually use less memory than a similar data class.
Do they or is the memory using the same?
Like enum , record is also a special class type in Java. It is intended to be used in places where a class is created only to act as plain data carrier.
In Java, a record is a special type of class declaration aimed at reducing the boilerplate code.
Records are immutable data classes that require only the type and name of fields. The equals, hashCode, and toString methods, as well as the private, final fields and public constructor, are generated by the Java compiler.
Record classes are final so they cannot be extended.
Every object in java has 64 bit of metadata, so an array of objects will consume more memory than an array of records, since the metadata will be attached only into the array reference, not in each record / struct . Moreover the advantage should be in the way the memory of records can be managed from Garbage Collector since it is fixed and contiguous. This is what I understand, if somebody could confirm or add extra information will be very useful. Thanks
To add to the basic analysis performed by @lugiorgi and a similar noticeable difference that I could come up with analyzing the byte code, is in the implementation of toString
, equals
and hashcode
.
On one hand, the existing class with overridden Object
class APIs looking like
public class City {
private final Integer id;
private final String name;
// all-args, toString, getters, equals, and hashcode
}
produces the byte code as following
public java.lang.String toString();
Code:
0: aload_0
1: getfield #7 // Field id:Ljava/lang/Integer;
4: aload_0
5: getfield #13 // Field name:Ljava/lang/String;
8: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Integer;Ljava/lang/String;)Ljava/lang/String;
13: areturn
public boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: if_acmpne 7
5: iconst_1
6: ireturn
7: aload_1
8: ifnull 22
11: aload_0
12: invokevirtual #21 // Method java/lang/Object.getClass:()Ljava/lang/Class;
15: aload_1
16: invokevirtual #21 // Method java/lang/Object.getClass:()Ljava/lang/Class;
19: if_acmpeq 24
22: iconst_0
23: ireturn
24: aload_1
25: checkcast #8 // class edu/forty/bits/records/equals/City
28: astore_2
29: aload_0
30: getfield #7 // Field id:Ljava/lang/Integer;
33: aload_2
34: getfield #7 // Field id:Ljava/lang/Integer;
37: invokevirtual #25 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
40: ifne 45
43: iconst_0
44: ireturn
45: aload_0
46: getfield #13 // Field name:Ljava/lang/String;
49: aload_2
50: getfield #13 // Field name:Ljava/lang/String;
53: invokevirtual #31 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ireturn
public int hashCode();
Code:
0: aload_0
1: getfield #7 // Field id:Ljava/lang/Integer;
4: invokevirtual #34 // Method java/lang/Integer.hashCode:()I
7: istore_1
8: bipush 31
10: iload_1
11: imul
12: aload_0
13: getfield #13 // Field name:Ljava/lang/String;
16: invokevirtual #38 // Method java/lang/String.hashCode:()I
19: iadd
20: istore_1
21: iload_1
22: ireturn
On the other hand the record representation for the same
record CityRecord(Integer id, String name) {}
produces the bytecode as less as
public java.lang.String toString();
Code:
0: aload_0
1: invokedynamic #19, 0 // InvokeDynamic #0:toString:(Ledu/forty/bits/records/equals/CityRecord;)Ljava/lang/String;
6: areturn
public final int hashCode();
Code:
0: aload_0
1: invokedynamic #23, 0 // InvokeDynamic #0:hashCode:(Ledu/forty/bits/records/equals/CityRecord;)I
6: ireturn
public final boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: invokedynamic #27, 0 // InvokeDynamic #0:equals:(Ledu/forty/bits/records/equals/CityRecord;Ljava/lang/Object;)Z
7: ireturn
Note: To what I could observe on the accessors and constructor byte code generated, they are alike for both the representation and hence excluded from the data here as well.
I did some quick and dirty testing with following
public record PersonRecord(String firstName, String lastName) {}
vs.
import java.util.Objects;
public final class PersonClass {
private final String firstName;
private final String lastName;
public PersonClass(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() {
return firstName;
}
public String lastName() {
return lastName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonClass that = (PersonClass) o;
return firstName.equals(that.firstName) &&
lastName.equals(that.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
@Override
public String toString() {
return "PersonRecord[" +
"firstName=" + firstName +
", lastName=" + lastName +
"]";
}
}
The compiled record file amounts to 1.475 bytes, the class to 1.643 bytes. The size difference probably comes from different equals/toString/hashCode implementations.
Maybe someone can do some bytecode digging...
correct, I agree with [@lugiorgi] and [@Naman], the only difference in the generated bytecode between a record and the equivalent class is in the implementation of methods: toString
, equals
and hashCode
. Which in the case of a record class are implemented using an invoke dynamic (indy) instruction to the same bootstrap method at class: java.lang.runtime.ObjectMethods
(freshly added in the records project). The fact that these three methods, toString
, equals
and hashCode
, invoke the same bootstrap method saves more space in the class file than invoking 3 different bootstraps methods. And of course as already shown in the other answers, saves more space than generating the obvious bytecode
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With