{{announcement.body}}
{{announcement.title}}

A First Look at Records in Java 14

DZone 's Guide to

A First Look at Records in Java 14

Here's a first look at records in Java 14.

· Java Zone ·
Free Resource

Here's a first look at records in Java 14!

The 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:

Java




x


 
1
package examples;
2
 
3
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:

Shell




xxxxxxxxxx
1


 
1
> javac --enable-preview --release 14 Person.java
2
Note: Person.java uses preview language features.
3
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:

Shell




xxxxxxxxxx
1


 
1
>javap -v -p Person.class


Plain Text




xxxxxxxxxx
1
172


 
1
Classfile examples/Person.class
2
  Last modified Dec 22, 2019; size 1273 bytes
3
  SHA-256 checksum 6f1b325121ca32a0b6127180eff29dcac4834f9c138c9613c526a4202fef972f
4
  Compiled from "Person.java"
5
final class examples.Person extends java.lang.Record
6
  minor version: 65535
7
  major version: 58
8
  flags: (0x0030) ACC_FINAL, ACC_SUPER
9
  this_class: #8                          // examples/Person
10
  super_class: #2                         // java/lang/Record
11
  interfaces: 0, fields: 2, methods: 6, attributes: 4
12
Constant pool:
13
   #1 = Methodref          #2.#3          // java/lang/Record."":()V
14
   #2 = Class              #4             // java/lang/Record
15
   #3 = NameAndType        #5:#6          // "":()V
16
   #4 = Utf8               java/lang/Record
17
   #5 = Utf8               
18
   #6 = Utf8               ()V
19
   #7 = Fieldref           #8.#9          // examples/Person.firstName:Ljava/lang/String;
20
   #8 = Class              #10            // examples/Person
21
   #9 = NameAndType        #11:#12        // firstName:Ljava/lang/String;
22
  #10 = Utf8               examples/Person
23
  #11 = Utf8               firstName
24
  #12 = Utf8               Ljava/lang/String;
25
  #13 = Fieldref           #8.#14         // examples/Person.lastName:Ljava/lang/String;
26
  #14 = NameAndType        #15:#12        // lastName:Ljava/lang/String;
27
  #15 = Utf8               lastName
28
  #16 = Fieldref           #8.#9          // examples/Person.firstName:Ljava/lang/String;
29
  #17 = Fieldref           #8.#14         // examples/Person.lastName:Ljava/lang/String;
30
  #18 = InvokeDynamic      #0:#19         // #0:toString:(Lexamples/Person;)Ljava/lang/String;
31
  #19 = NameAndType        #20:#21        // toString:(Lexamples/Person;)Ljava/lang/String;
32
  #20 = Utf8               toString
33
  #21 = Utf8               (Lexamples/Person;)Ljava/lang/String;
34
  #22 = InvokeDynamic      #0:#23         // #0:hashCode:(Lexamples/Person;)I
35
  #23 = NameAndType        #24:#25        // hashCode:(Lexamples/Person;)I
36
  #24 = Utf8               hashCode
37
  #25 = Utf8               (Lexamples/Person;)I
38
  #26 = InvokeDynamic      #0:#27         // #0:equals:(Lexamples/Person;Ljava/lang/Object;)Z
39
  #27 = NameAndType        #28:#29        // equals:(Lexamples/Person;Ljava/lang/Object;)Z
40
  #28 = Utf8               equals
41
  #29 = Utf8               (Lexamples/Person;Ljava/lang/Object;)Z
42
  #30 = Utf8               (Ljava/lang/String;Ljava/lang/String;)V
43
  #31 = Utf8               Code
44
  #32 = Utf8               LineNumberTable
45
  #33 = Utf8               MethodParameters
46
  #34 = Utf8               ()Ljava/lang/String;
47
  #35 = Utf8               ()I
48
  #36 = Utf8               (Ljava/lang/Object;)Z
49
  #37 = Utf8               SourceFile
50
  #38 = Utf8               Person.java
51
  #39 = Utf8               Record
52
  #40 = Utf8               BootstrapMethods
53
  #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;
54
  #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;
55
  #43 = Class              #45            // java/lang/runtime/ObjectMethods
56
  #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;
57
  #45 = Utf8               java/lang/runtime/ObjectMethods
58
  #46 = Utf8               bootstrap
59
  #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;
60
  #48 = String             #49            // firstName;lastName
61
  #49 = Utf8               firstName;lastName
62
  #50 = MethodHandle       1:#7           // REF_getField examples/Person.firstName:Ljava/lang/String;
63
  #51 = MethodHandle       1:#13          // REF_getField examples/Person.lastName:Ljava/lang/String;
64
  #52 = Utf8               InnerClasses
65
  #53 = Class              #54            // java/lang/invoke/MethodHandles$Lookup
66
  #54 = Utf8               java/lang/invoke/MethodHandles$Lookup
67
  #55 = Class              #56            // java/lang/invoke/MethodHandles
68
  #56 = Utf8               java/lang/invoke/MethodHandles
69
  #57 = Utf8               Lookup
70
{
71
  private final java.lang.String firstName;
72
    descriptor: Ljava/lang/String;
73
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL
74
 
75
  private final java.lang.String lastName;
76
    descriptor: Ljava/lang/String;
77
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL
78
 
79
  public examples.Person(java.lang.String, java.lang.String);
80
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
81
    flags: (0x0001) ACC_PUBLIC
82
    Code:
83
      stack=2, locals=3, args_size=3
84
         0: aload_0
85
         1: invokespecial #1                  // Method java/lang/Record."":()V
86
         4: aload_0
87
         5: aload_1
88
         6: putfield      #7                  // Field firstName:Ljava/lang/String;
89
         9: aload_0
90
        10: aload_2
91
        11: putfield      #13                 // Field lastName:Ljava/lang/String;
92
        14: return
93
      LineNumberTable:
94
        line 3: 0
95
    MethodParameters:
96
      Name                           Flags
97
      firstName
98
      lastName
99
 
100
  public java.lang.String toString();
101
    descriptor: ()Ljava/lang/String;
102
    flags: (0x0001) ACC_PUBLIC
103
    Code:
104
      stack=1, locals=1, args_size=1
105
         0: aload_0
106
         1: invokedynamic #18,  0             // InvokeDynamic #0:toString:(Lexamples/Person;)Ljava/lang/String;
107
         6: areturn
108
      LineNumberTable:
109
        line 3: 0
110
 
111
  public final int hashCode();
112
    descriptor: ()I
113
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
114
    Code:
115
      stack=1, locals=1, args_size=1
116
         0: aload_0
117
         1: invokedynamic #22,  0             // InvokeDynamic #0:hashCode:(Lexamples/Person;)I
118
         6: ireturn
119
      LineNumberTable:
120
        line 3: 0
121
 
122
  public final boolean equals(java.lang.Object);
123
    descriptor: (Ljava/lang/Object;)Z
124
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
125
    Code:
126
      stack=2, locals=2, args_size=2
127
         0: aload_0
128
         1: aload_1
129
         2: invokedynamic #26,  0             // InvokeDynamic #0:equals:(Lexamples/Person;Ljava/lang/Object;)Z
130
         7: ireturn
131
      LineNumberTable:
132
        line 3: 0
133
 
134
  public java.lang.String firstName();
135
    descriptor: ()Ljava/lang/String;
136
    flags: (0x0001) ACC_PUBLIC
137
    Code:
138
      stack=1, locals=1, args_size=1
139
         0: aload_0
140
         1: getfield      #16                 // Field firstName:Ljava/lang/String;
141
         4: areturn
142
      LineNumberTable:
143
        line 3: 0
144
 
145
  public java.lang.String lastName();
146
    descriptor: ()Ljava/lang/String;
147
    flags: (0x0001) ACC_PUBLIC
148
    Code:
149
      stack=1, locals=1, args_size=1
150
         0: aload_0
151
         1: getfield      #17                 // Field lastName:Ljava/lang/String;
152
         4: areturn
153
      LineNumberTable:
154
        line 3: 0
155
}
156
SourceFile: "Person.java"
157
Record:
158
  java.lang.String firstName;
159
    descriptor: Ljava/lang/String;
160
 
161
  java.lang.String lastName;
162
    descriptor: Ljava/lang/String;
163
 
164
BootstrapMethods:
165
  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;
166
    Method arguments:
167
      #8 examples/Person
168
      #48 firstName;lastName
169
      #50 REF_getField examples/Person.firstName:Ljava/lang/String;
170
      #51 REF_getField examples/Person.lastName:Ljava/lang/String;
171
InnerClasses:
172
  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:

  1. The class is marked final, which means we cannot create a subclass of it.
  2. The class extends java.lang.Record, which is the base class for all records, much like java.lang.Enum is the base class for all enums.
  3. There are two private final fields named after the two components of the record: firstName and lastName.
  4. 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:
  5. Java




    xxxxxxxxxx
    1


     
    1
    public Person(String firstName, String lastName) {
    2
        this.firstName = firstName;
    3
        this.lastName = lastName;
    4
    }


  6. There are two getter methods named firstName() and lastName().
  7. Three other methods are generated: toString(), hashCode() and equals(). They all rely on invokedynamic to dynamically invoke the appropriate method containing the implicit implementation. There is a bootstrap method ObjectMethods.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:
  8. Java




    xxxxxxxxxx
    1
    13


     
    1
    Person john = new Person("John", "Doe");
    2
    System.out.println(john.firstName());         // John
    3
    System.out.println(john.lastName());          // Doe
    4
    System.out.println(john);                     // Person[firstName=John, lastName=Doe]
    5
     
    6
    Person jane = new Person("Jane", "Dae");
    7
    Person johnCopy = new Person("John", "Doe");
    8
     
    9
    System.out.println(john.hashCode());          // 71819599
    10
    System.out.println(jane.hashCode());          // 71407578
    11
    System.out.println(johnCopy.hashCode());      // 71819599
    12
    System.out.println(john.equals(jane));        // false
    13
    System.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:

Java




xxxxxxxxxx
1


 
1
record Person (String firstName, String lastName) {
2
    static int x;
3
}



We can define static methods and instance methods that can operate on the state of the object:

Java




xxxxxxxxxx
1
11


 
1
record Person (String firstName, String lastName) {
2
    static int x;
3
 
4
    public static void doX() {
5
        x++;
6
    }
7
 
8
    public String getFullName() {
9
        return firstName + " " + lastName;
10
    }
11
}



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:

Java




xxxxxxxxxx
1
12


 
1
record Person (String firstName, String lastName) {
2
    public Person {
3
        if(firstName == null || lastName == null) {
4
            throw new IllegalArgumentException("firstName and lastName must not be null");
5
        // We can also omit assigning fields, the compiler will auto-add them
6
        }
7
    }
8
 
9
    public Person(String fullName) {
10
        this(fullName.split(" ")[0], fullName.split(" ")[1]);
11
    }
12
}



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

Introducing Java Record

JDK 14: Records, Text Blocks, and More

Topics:
java ,jdk ,project amber ,openjdk

Published at DZone with permission of Mahmoud Anouti , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}