Follow @vlad_mihalcea Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

Introduction

In this article, I’m going to present to you the Records feature that was introduced in Java 14, and which aims to simplify the way we create a POJO (Plain Old Java Objects), DTO, or Value Object.

Domain Model

Let’s assume we have the following PostInfo and AuditInfo POJO classes:

Both classes define several properties and provide specific implementations for the equals , hashCode , and toString Java Object methods.

The AuditInfo class is implemented like this:

public class AuditInfo { private final LocalDateTime createdOn; private final String createdBy; private final LocalDateTime updatedOn; private final String updatedBy; public AuditInfo( LocalDateTime createdOn, String createdBy, LocalDateTime updatedOn, String updatedBy) { this.createdOn = createdOn; this.createdBy = createdBy; this.updatedOn = updatedOn; this.updatedBy = updatedBy; } public LocalDateTime getCreatedOn() { return createdOn; } public String getCreatedBy() { return createdBy; } public LocalDateTime getUpdatedOn() { return updatedOn; } public String getUpdatedBy() { return updatedBy; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AuditInfo)) return false; AuditInfo auditInfo = (AuditInfo) o; return createdOn.equals(auditInfo.createdOn) && createdBy.equals(auditInfo.createdBy) && Objects.equals(updatedOn, auditInfo.updatedOn) && Objects.equals(updatedBy, auditInfo.updatedBy); } @Override public int hashCode() { return Objects.hash( createdOn, createdBy, updatedOn, updatedBy ); } @Override public String toString() { return String.format(""" AuditInfo { createdOn : '%s', createdBy : '%s', updatedOn : '%s', updatedBy : '%s' } """, createdOn, createdBy, updatedOn, updatedBy ); } }

And the PostInfo class looks as follows:

public class PostInfo { private final Long id; private final String title; private final AuditInfo auditInfo; public PostInfo( Long id, String title, AuditInfo auditInfo) { this.id = id; this.title = title; this.auditInfo = auditInfo; } public Long getId() { return id; } public String getTitle() { return title; } public AuditInfo getAuditInfo() { return auditInfo; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PostInfo)) return false; PostInfo postInfo = (PostInfo) o; return id.equals(postInfo.id) && title.equals(postInfo.title) && auditInfo.equals(postInfo.auditInfo); } @Override public int hashCode() { return Objects.hash( id, title, auditInfo ); } @Override public String toString() { return String.format(""" PostInfo { id : '%s', title : '%s', auditInfo : { createdOn : '%s', createdBy : '%s', updatedOn : '%s', updatedBy : '%s' } } """, id, title, auditInfo.createdOn, auditInfo.createdBy, auditInfo.updatedOn, auditInfo.updatedBy ); } }

Frankly, that’s a lot of code for such a simple data object.

Java Records

Java 14 introduces a new way of defining such data objects, as Records, that take the burden of defining the fields, getters, equals , hashCode , and toString method implementations.

So, let’s see how the AuditInfo and PostInfo classes look when we define them as Records, instead of Plain Old Java Objects:

public record AuditInfo( LocalDateTime createdOn, String createdBy, LocalDateTime updatedOn, String updatedBy ) {} public record PostInfo( Long id, String title, AuditInfo auditInfo ) {}

That’s it!

Behind the scenes, Java Records are defined as any other Java class. In our case, the decompiled classes look as follows:

public final class PostInfo extends java.lang.Record { private final java.lang.Long id; private final java.lang.String title; private final AuditInfo auditInfo; public PostInfo( java.lang.Long id, java.lang.String title, AuditInfo auditInfo) { /* compiled code */ } public java.lang.String toString() { /* compiled code */ } public final int hashCode() { /* compiled code */ } public final boolean equals(java.lang.Object o) { /* compiled code */ } public java.lang.Long id() { /* compiled code */ } public java.lang.String title() { /* compiled code */ } public AuditInfo auditInfo() { /* compiled code */ } } public final class AuditInfo extends java.lang.Record { private final java.time.LocalDateTime createdOn; private final java.lang.String createdBy; private final java.time.LocalDateTime updatedOn; private final java.lang.String updatedBy; public AuditInfo( java.time.LocalDateTime createdOn, java.lang.String createdBy, java.time.LocalDateTime updatedOn, java.lang.String updatedBy) { /* compiled code */ } public java.lang.String toString() { /* compiled code */ } public final int hashCode() { /* compiled code */ } public final boolean equals(java.lang.Object o) { /* compiled code */ } public java.time.LocalDateTime createdOn() { /* compiled code */ } public java.lang.String createdBy() { /* compiled code */ } public java.time.LocalDateTime updatedOn() { /* compiled code */ } public java.lang.String updatedBy() { /* compiled code */ } }

The generated class is final and extends the Record base class that was introduced by Java 14.

Since Java Records define a single constructor that takes the same arguments we used when defining the Record type, this is how we can create a PostInfo with an AuditInfo object:

PostInfo postInfo = new PostInfo( 1L, "High-Performance Java Persistence", new AuditInfo( LocalDateTime.of(2016, 11, 2, 12, 0, 0), "Vlad Mihalcea", LocalDateTime.now(), "Vlad Mihalcea" ) );

Note that, unlike the POJO specification, Java Records getters don’t follow the Java Bean standard, and the method names match the encapsulated field names:

assertEquals( 1L, postInfo.id().longValue() ); assertEquals( "High-Performance Java Persistence", postInfo.title() ); assertEquals( LocalDateTime.of(2016, 11, 2, 12, 0, 0), postInfo.auditInfo().createdOn() ); assertEquals( "Vlad Mihalcea", postInfo.auditInfo().createdBy() );

We can see that a toString method is also generated, and the implementation is based on the Record properties. So, when calling the toString methods of the AuditInfo and PostInfo Records:

LOGGER.info("Audit info:

{}", postInfo.auditInfo()); LOGGER.info("Post info:

{}", postInfo);

We get the following log entries:

Audit info: AuditInfo[createdOn=2016-11-02T12:00, createdBy=Vlad Mihalcea, updatedOn=2020-04-14T12:29:29.534875700, updatedBy=Vlad Mihalcea] Post info: PostInfo[id=1, title=High-Performance Java Persistence, auditInfo=AuditInfo[createdOn=2016-11-02T12:00, createdBy=Vlad Mihalcea, updatedOn=2020-04-14T12:29:29.534875700, updatedBy=Vlad Mihalcea]]

Customizing Java Records

Even if the generated classes are final , we can still override the default methods. For instance, let’s say we want to provide a custom toString implementation that matches the one we defined previously in our POJO classes.

To override the toString method, we just have to provide the new method definition when declaring Java Records:

public record AuditInfo( LocalDateTime createdOn, String createdBy, LocalDateTime updatedOn, String updatedBy ) { @Override public String toString() { return String.format(""" AuditInfo { createdOn : '%s', createdBy : '%s', updatedOn : '%s', updatedBy : '%s' } """, createdOn, createdBy, updatedOn, updatedBy ); } } public record PostInfo( Long id, String title, AuditInfo auditInfo ) { @Override public String toString() { return String.format(""" PostInfo { id : '%s', title : '%s', auditInfo : { createdOn : '%s', createdBy : '%s', updatedOn : '%s', updatedBy : '%s' } } """, id, title, auditInfo.createdOn, auditInfo.createdBy, auditInfo.updatedOn, auditInfo.updatedBy ); } }

Now, when the Logger framework calls the toString method, this is what we get in the application log:

Audit info: AuditInfo { createdOn : '2016-11-02T12:00', createdBy : 'Vlad Mihalcea', updatedOn : '2020-04-14T12:45:09.569632400', updatedBy : 'Vlad Mihalcea' } Post info: PostInfo { id : '1', title : 'High-Performance Java Persistence', auditInfo : { createdOn : '2016-11-02T12:00', createdBy : 'Vlad Mihalcea', updatedOn : '2020-04-14T12:45:09.569632400', updatedBy : 'Vlad Mihalcea' } }

Cool, right?

Conclusion

The new Java Records feature is very handy, as it simplifies the way we build value objects. Just like Multiline String Text Blocks, this is a preview language feature in Java 14.

So, if you want to try it out, not that you need to use the enable-preview to both the Java compiler and the JVM when running the program.

Insert details about how the information is going to be processed DOWNLOAD NOW