Records, added in JDK 16 (JEP 395), are designed to address difficulties around defining data carriers in Java. In this we will look at how Records help when transforming data within a method.
Defining even a simple data carrier before records often required dozens of lines of code. A data carrier would need; fields, a constructor, accessors, equals()
, hashCode()
, and often toString()
, for printing of the contents. This problem can been seen in this below example:
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
class Person{
private String firstName;
private String lastName;
private String title;
private String twitterHandle;
public Person(String firstName, String lastName, String title, String twitterHandle) {
this.firstName = firstName;
this.lastName = lastName;
this.title = title;
this.twitterHandle = twitterHandle;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((twitterHandle == null) ? 0 : twitterHandle.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (firstName == null) {
if (other.firstName != null)
return false;
} else if (!firstName.equals(other.firstName))
return false;
if (lastName == null) {
if (other.lastName != null)
return false;
} else if (!lastName.equals(other.lastName))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
if (twitterHandle == null) {
if (other.twitterHandle != null)
return false;
} else if (!twitterHandle.equals(other.twitterHandle))
return false;
return true;
}
@Override
public String toString() {
return "Person [firstName=" + firstName + ", lastName=" + lastName + ", title=" + title
+ ", twitterHandle=" + twitterHandle + "]";
}
}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(System.out::println);
In the above example the meaning of the what the method is doing is lost because of all the cruft associated with defining a data carrier in Java. With a Records a data carrier can be defined within a single line:
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
record Person(String firstName, String lastName, String title, String twitterHandle) {}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(System.out::println);
The ability to define a data carrier within a single line does come with some tradeoffs. There is less freedom allowed in the defining of a data carrier, but in return a canonical constructor, accessors, equals()
, hashCode()
, and toString()
, are automatically generated by the Java compiler. There are a few other important restrictions, which you can read here.
An explicit declaration of a generated method is allowed, like in this example with toString()
:
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
record Person(String firstName, String lastName, String title, String twitterHandle) {
public String toString() {
return lastName + ", " + firstName + " twitter handle: " + twitterHandle + " job title: " + title;
}
}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(System.out::println);
Additional methods can also be added to a record class like here with toJSON()
:
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
record Person(String firstName, String lastName, String title, String twitterHandle) {
public String toJSON() {
return """
{
"firstName" : "%s",
"lastName" : "%s",
"title" : "%s",
"twitterHandle" : "%s"
}
""".formatted(firstName, lastName, title, twitterHandle);
}
}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(p -> System.out.println(p.toJSON()));
Happy Coding!