A First Look at Records in Java 14
Here's a first look at records in Java 14.
Join the DZone community and get the full member experience.
Join For FreeThe upcoming release of Java will be version 14 scheduled to be in general availability in March 2020. Similar to the already released versions under the new 6-month release cycle, JDK 14 is expected to have several new features at both the language and JVM levels.
If we look at the feature list, however, we notice quite a few language features that are highly anticipated by developers: records, switch expressions (which exist in JDK 13 but in preview mode), and pattern matching. Let’s have a look at records that seems to be an interesting addition to the language.
You may also like: Introducing Java Record
Prerequisites
All that we’re going to need is the JDK 14 Early-Access binary from the OpenJDK website: https://jdk.java.net/14/.
What Is a Record?
A record is basically a “data class,” a special kind of class that is intended to hold pure data in it. The semantics of records already exist in similar constructs in other languages such as data classes in Kotlin. By declaring a type as a record, the developer is clearly expressing their intention that the type represents only data. The syntax for declaring a record is much simpler and concise, compared to using a normal class where you typically need to implement core Object
methods like equals()
and hashCode()
(often referred to as “boilerplate” code). Records seem to be an interesting choice when modeling things like domain model classes (potentially to be persisted via ORM), or data transfer objects (DTOs).
A good way to think of how records are implemented in the language is to remember enums. An enum is also a class that has special semantics with a nicer syntax. Since both are still classes, many of the features available in classes are preserved, so there is a balance between simplicity and flexibility in their design.
Records are a preview language feature, which means that, although it is fully implemented, it is not yet standardized in the JDK and can only be used by activating a flag. Preview language features can be updated or even removed in future versions. Similar to switch expressions, it may become final and permanent in a future version.
A Record Example
Here’s an example of how a basic record looks like:
package examples;
record Person (String firstName, String lastName) {}
We have a Person
record defined in a package with two components: firstName
and lastName
, and an empty body.
Let’s try to compile it — notice the --enable-preview
option:
xxxxxxxxxx
> javac --enable-preview --release 14 Person.java
Note: Person.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
How Does it Look Under the Hood?
As mentioned previously, a record is just a class with the purpose of holding and exposing data. Let’s have a look at the generated bytecode with the javap
tool:
xxxxxxxxxx
>javap -v -p Person.class
xxxxxxxxxx
Classfile examples/Person.class
Last modified Dec 22, 2019; size 1273 bytes
SHA-256 checksum 6f1b325121ca32a0b6127180eff29dcac4834f9c138c9613c526a4202fef972f
Compiled from "Person.java"
final class examples.Person extends java.lang.Record
minor version: 65535
major version: 58
flags: (0x0030) ACC_FINAL, ACC_SUPER
this_class: #8 // examples/Person
super_class: #2 // java/lang/Record
interfaces: 0, fields: 2, methods: 6, attributes: 4
Constant pool:
#1 = Methodref #2.#3 // java/lang/Record."":()V
#2 = Class #4 // java/lang/Record
#3 = NameAndType #5:#6 // "":()V
#4 = Utf8 java/lang/Record
#5 = Utf8
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // examples/Person.firstName:Ljava/lang/String;
#8 = Class #10 // examples/Person
#9 = NameAndType #11:#12 // firstName:Ljava/lang/String;
#10 = Utf8 examples/Person
#11 = Utf8 firstName
#12 = Utf8 Ljava/lang/String;
#13 = Fieldref #8.#14 // examples/Person.lastName:Ljava/lang/String;
#14 = NameAndType #15:#12 // lastName:Ljava/lang/String;
#15 = Utf8 lastName
#16 = Fieldref #8.#9 // examples/Person.firstName:Ljava/lang/String;
#17 = Fieldref #8.#14 // examples/Person.lastName:Ljava/lang/String;
#18 = InvokeDynamic #0:#19 // #0:toString:(Lexamples/Person;)Ljava/lang/String;
#19 = NameAndType #20:#21 // toString:(Lexamples/Person;)Ljava/lang/String;
#20 = Utf8 toString
#21 = Utf8 (Lexamples/Person;)Ljava/lang/String;
#22 = InvokeDynamic #0:#23 // #0:hashCode:(Lexamples/Person;)I
#23 = NameAndType #24:#25 // hashCode:(Lexamples/Person;)I
#24 = Utf8 hashCode
#25 = Utf8 (Lexamples/Person;)I
#26 = InvokeDynamic #0:#27 // #0:equals:(Lexamples/Person;Ljava/lang/Object;)Z
#27 = NameAndType #28:#29 // equals:(Lexamples/Person;Ljava/lang/Object;)Z
#28 = Utf8 equals
#29 = Utf8 (Lexamples/Person;Ljava/lang/Object;)Z
#30 = Utf8 (Ljava/lang/String;Ljava/lang/String;)V
#31 = Utf8 Code
#32 = Utf8 LineNumberTable
#33 = Utf8 MethodParameters
#34 = Utf8 ()Ljava/lang/String;
#35 = Utf8 ()I
#36 = Utf8 (Ljava/lang/Object;)Z
#37 = Utf8 SourceFile
#38 = Utf8 Person.java
#39 = Utf8 Record
#40 = Utf8 BootstrapMethods
#41 = MethodHandle 6:#42 // REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
#42 = Methodref #43.#44 // java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
#43 = Class #45 // java/lang/runtime/ObjectMethods
#44 = NameAndType #46:#47 // bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
#45 = Utf8 java/lang/runtime/ObjectMethods
#46 = Utf8 bootstrap
#47 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
#48 = String #49 // firstName;lastName
#49 = Utf8 firstName;lastName
#50 = MethodHandle 1:#7 // REF_getField examples/Person.firstName:Ljava/lang/String;
#51 = MethodHandle 1:#13 // REF_getField examples/Person.lastName:Ljava/lang/String;
#52 = Utf8 InnerClasses
#53 = Class #54 // java/lang/invoke/MethodHandles$Lookup
#54 = Utf8 java/lang/invoke/MethodHandles$Lookup
#55 = Class #56 // java/lang/invoke/MethodHandles
#56 = Utf8 java/lang/invoke/MethodHandles
#57 = Utf8 Lookup
{
private final java.lang.String firstName;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
private final java.lang.String lastName;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
public examples.Person(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Record."":()V
4: aload_0
5: aload_1
6: putfield #7 // Field firstName:Ljava/lang/String;
9: aload_0
10: aload_2
11: putfield #13 // Field lastName:Ljava/lang/String;
14: return
LineNumberTable:
line 3: 0
MethodParameters:
Name Flags
firstName
lastName
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokedynamic #18, 0 // InvokeDynamic #0:toString:(Lexamples/Person;)Ljava/lang/String;
6: areturn
LineNumberTable:
line 3: 0
public final int hashCode();
descriptor: ()I
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokedynamic #22, 0 // InvokeDynamic #0:hashCode:(Lexamples/Person;)I
6: ireturn
LineNumberTable:
line 3: 0
public final boolean equals(java.lang.Object);
descriptor: (Ljava/lang/Object;)Z
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokedynamic #26, 0 // InvokeDynamic #0:equals:(Lexamples/Person;Ljava/lang/Object;)Z
7: ireturn
LineNumberTable:
line 3: 0
public java.lang.String firstName();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #16 // Field firstName:Ljava/lang/String;
4: areturn
LineNumberTable:
line 3: 0
public java.lang.String lastName();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #17 // Field lastName:Ljava/lang/String;
4: areturn
LineNumberTable:
line 3: 0
}
SourceFile: "Person.java"
Record:
java.lang.String firstName;
descriptor: Ljava/lang/String;
java.lang.String lastName;
descriptor: Ljava/lang/String;
BootstrapMethods:
0: #41 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
Method arguments:
#8 examples/Person
#48 firstName;lastName
#50 REF_getField examples/Person.firstName:Ljava/lang/String;
#51 REF_getField examples/Person.lastName:Ljava/lang/String;
InnerClasses:
public static final #57= #53 of #55; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
Interesting… Several things we can notice:
- The class is marked
final
, which means we cannot create a subclass of it. - The class extends
java.lang.Record
, which is the base class for all records, much likejava.lang.Enum
is the base class for all enums. - There are two private final fields named after the two components of the record:
firstName
andlastName
. - There is a public constructor that is generated for us:
public examples.Person(java.lang.String, java.lang.String)
. By looking at its body, it’s easy to see that it just assigns the two arguments to the two fields. The constructor is equivalent to: -
Java
xxxxxxxxxx
1
1public Person(String firstName, String lastName) {
2this.firstName = firstName;
3this.lastName = lastName;
4}
- There are two getter methods named
firstName()
andlastName()
. - Three other methods are generated:
toString()
,hashCode()
andequals()
. They all rely oninvokedynamic
to dynamically invoke the appropriate method containing the implicit implementation. There is a bootstrap methodObjectMethods.bootstrap
that takes the component names of the record and its getter methods, and generates the methods. Their behaviors is consistent with what we would expect to have: -
Java
xxxxxxxxxx
113
1Person john = new Person("John", "Doe");
2System.out.println(john.firstName()); // John
3System.out.println(john.lastName()); // Doe
4System.out.println(john); // Person[firstName=John, lastName=Doe]
56Person jane = new Person("Jane", "Dae");
7Person johnCopy = new Person("John", "Doe");
89System.out.println(john.hashCode()); // 71819599
10System.out.println(jane.hashCode()); // 71407578
11System.out.println(johnCopy.hashCode()); // 71819599
12System.out.println(john.equals(jane)); // false
13System.out.println(john.equals(johnCopy)); // true
Adding Member Declarations in Records
We cannot add instance fields to records, which is expected, given that such state should be part of the components. We can, however, add static fields:
xxxxxxxxxx
record Person (String firstName, String lastName) {
static int x;
}
We can define static methods and instance methods that can operate on the state of the object:
xxxxxxxxxx
record Person (String firstName, String lastName) {
static int x;
public static void doX() {
x++;
}
public String getFullName() {
return firstName + " " + lastName;
}
}
We can also add constructors, and modify the canonical constructor (the one that takes the two String
parameters). If we want to override the canonical constructor, we can omit the parameters and the assignments to the fields:
xxxxxxxxxx
record Person (String firstName, String lastName) {
public Person {
if(firstName == null || lastName == null) {
throw new IllegalArgumentException("firstName and lastName must not be null");
// We can also omit assigning fields, the compiler will auto-add them
}
}
public Person(String fullName) {
this(fullName.split(" ")[0], fullName.split(" ")[1]);
}
}
Conclusion
Records introduce the capability of properly implementing data classes, without the need to write verbose code. Plain data classes are reduced from several lines of code to a one-liner. There are other language features in progress that work well with records, such as pattern matching. For a much deeper dive into records and background information, see Brian Goetz’s exploratory document on OpenJDK.
Further Reading
Published at DZone with permission of Mahmoud Anouti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments