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

Java14: Records

DZone 's Guide to

Java14: Records

In this article, check out a new feature of Java 14, Records, and see how you can implement them as more effective "data carriers".

· Java Zone ·
Free Resource

Java 14 introduces a new interesting feature: records. Here, you can find the official proposal: JEP 359 Records. The goal is to have a "data carrier" class without the traditional 'Java ceremony' (boilerplate).

In other words, a record represents an immutable state. Record, like Enum, is a restricted form of class. The feature has still a preview status and it could change in future releases.

Use Cases

Typical use cases are: DTOs, compound keys, data structures used as tuples (e.g. Pair, Map.Entry), method's multiple return, tree nodes.

Records are not intended to replace (mutable) data objects or libraries like Lombok.

Benefits

  • equals(), hashCode(), toString(),  constructor(), and read accessors are generated for you.
  • Interfaces can be implemented.

Restrictions

  • A record cannot be extended — it's a final class.
  • A record cannot extend a class.
  • The value (reference) of a field is final and cannot be changed.

Record Definition

Record definition

The body is optional. The state description declares the components of the record. This simple line of code is translated by the compiler in a class similar to this one:

Java
 




x
21


1
public final class Person extends Record {
2
  public final String name;
3
  public final Integer yearOfBirth;
4
 
5
  public Person(String name, Integer yearOfBirth) {
6
    this.name = name;
7
    this.yearOfBirth = yearOfBirth;
8
  }
9
  
10
  public String name() {
11
      return name;
12
  };
13
  
14
  public Integer yearOfBirth() {
15
    return yearOfBirth;
16
  };
17
 
18
  public final int hashCode() { /* implementation according to spec */}
19
  public final boolean equals(Object o) { /* implementation according to spec */}
20
}
21
 
           



Beware

If the fields contain objects, only the reference is immutable. The referenced objects can change its value, compromising the state of the record. For this reason, you should use immutable objects in your record to avoid surprises.

You may also like: A Guide to Streams: In-Depth Tutorial With Examples.

Examples

How to Execute Them

To execute the examples, you can use JShell with the flag --enable-preview or compile your source using the flags javac --enable-preview --release 14 [source].java and execute it using java --enable-preview [mainclass].

If you are using a single file program, you need the source flag: java --enable-preview --source 14 [source].java

The code in this post has been tested with JShell and IntelliJ (EAP), using OpenJDK build 14-ea+32-1423.

minimalistic

Java
 




x


 
1
record Person(){};



This is a minimalistic valid record.

Java
 




xxxxxxxxxx
1


 
1
var marco = new Person();
2
var jon = new Person();
3
 
           
4
marco.hashCode(); // => 0
5
 
           
6
marco.equals(jon) // => true, the objects have the same field content
7
marco == jon // => false, the objects have different references
8
 
           
9
marco.toString() // => "Person[]", toString() is implemented by record. A default result for a standard class would have been something like: "Person2@573fd745"


Adding a Field

In this example, we add an argument to the new record.

Java
 




xxxxxxxxxx
1


 
1
record Person(String name){};



Java adds the private field ( final String name;) and the accessor ( public String name() {return this.name;}) to the class and implements toString(), equals(), and the constructor Person(String name) {this.name = name}.

Record workflow
Record workflow

Java
 




xxxxxxxxxx
1
17


1
// a new constructor is generated a mandatory parameter 'name' 
2
var marco = new Person("marco");
3
 
           
4
// the default no parameter constructor is not generated
5
var andy = new Person(); // => constructor Person in record Person cannot be applied to given types; required: java.lang.String
6
 
           
7
// to read the value of the name we access the field 
8
marco.name() // => "marco", no 'get' here!
9
marco.toString() // => "Person[name=marco]"
10
marco.hashCode() // => 103666250
11
 
           
12
marco.equals(new Person("andy")); // => false
13
 
           
14
// if we try to modify the state
15
marco.name = "andy"; // => Error: name has private access in Person
16
marco.setName("andy"); // => Error: cannot find symbol
17
 
           


Noteworthy here:

  • the fields are private and final
  • an accessor is created for the fields without the traditional bean notation 'get'.

implementing an interface

records can implement an interface, here an example:

Java
 




xxxxxxxxxx
1
12


 
1
interface Person {
2
  String getFullName();
3
}
4
 
           
5
record Developer(String firstName, String lastName) implements Person {
6
  public String getFullName() {
7
    return firstName + " " + lastName;
8
  }
9
}
10
 
           
11
var marco = new Developer("Marco", "Molteni"); // => marco ==> Developer[firstName=Marco, lastName=Molteni]
12
marco.getFullName(); // =>  "Marco Molteni"



Record workflow

Implementing Multiple Constructors

Records implement a constructor with fields declared as parameters.

Java
 




xxxxxxxxxx
1


 
1
record Person(String name, Integer age){};




This code generates something like:

Java
 




xxxxxxxxxx
1
16


 
1
public final class Person extends Record {
2
  private final String name;
3
  private final Integer age;
4
  
5
  // constructor generated
6
  public Person(String name, Integer age) {
7
    this.name = name;
8
    this.age = age;
9
  }
10
  // accessors
11
  public String name() {return name;}
12
  public String age() {return age;}
13
  
14
  // ... other methods
15
 
           
16
}



If you try to instantiate the record without the two parameters an exception is thrown:

Java
 




xxxxxxxxxx
1


 
1
var marco = new Person();
2
 
           
3
constructor Person in record Person cannot be applied to given types;
4
|    required: java.lang.String,java.lang.Integer
5
|    found:    no arguments
6
|    reason: actual and formal argument lists differ in length
7
|  var marco = new Person();



You can add your own constructor if needed (e.g. not all the parameters are required).

Java
 




xxxxxxxxxx
1


 
1
record Person(String name, Integer age){
2
  public Person() {
3
    this("unknown", null);
4
  }
5
};



In this case, you can instantiate an object using the extra constructor:

Java
 




xxxxxxxxxx
1


 
1
var marco = new Person(); // => marco ==> Person[name=unknown, age=null]



Mutating the State

In this example, I show how it's possible to mutate the values inside a record. The types used in a record should be immutable to be sure that the state won't change.

Java
 




xxxxxxxxxx
1
20


 
1
record Developer (String name, List<String> languages){};
2
 
           
3
List<String> languages = new ArrayList<String>(Arrays.asList("Java"));
4
languages; // ==> [Java];
5
 
           
6
var marco = new Developer("marco", languages); // marco ==> Developer[name=marco, languages=[Java]]
7
marco.languages(); // => [Java]
8
marco.hashCode(); // => -1079012009
9
 
           
10
// we add one value to the array referenced in the record
11
languages.add("JavaScript");
12
 
           
13
// the value of the record changes
14
marco.languages(); // => [Java, JavaScript]
15
marco.hashCode(); () // => 256362082
16
 
           
17
// the Array list is mutable
18
marco.languages().remove(1);
19
marco.languages(); // => [Java]
20
marco.hashCode(); // => -1079012009



Redefining an Accessor

When you declare a record, you can redefine an accessor method. This is useful if you need to add annotations or modify standard behavior.

Java
 




xxxxxxxxxx
1
10


 
1
record Developer(String name){
2
    // we modify the default accessor
3
    public String name() {
4
    // this.name refers to the field generated by record  
5
        return "Hi " + this.name;
6
    }
7
 };
8
    
9
var bill = new Developer("William");
10
bill.name(); // => "Hi William"



Annotations

Records components support annotation. The annotation requires has to declare RECORD_COMPONENT as target.

Java
 




xxxxxxxxxx
1


 
1
@Retention(RetentionPolicy.RUNTIME)
2
@Target(ElementType.RECORD_COMPONENT)
3
public @interface RecordExample {
4
}
5
 
           
6
record Person(@RecordExample String name){}



Further Reading

Topics:
java ,java 14 ,jdk ,openjdk ,records ,java records

Published at DZone with permission of Marco Molteni . See the original article here.

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}