I recently had the opportunity to join a team of developers and we agreed to upgrade our Java version from 8 to 17. With this upgrade, we had a whole load of Java goodness including records that were added in preview for Java 14 & 15. Then released as part of Java 16.
As is the tradition in our posts, we will be looking at the Why, What, and How of Java records.
Why [Java] records?
In the days predating Java records, when we define POJOs which would be used as “data carriers”, we could either
- define these “data carriers” as full-blown POJOs, where we declare all fields and also override equals(), hashcode(), and toString() methods;
- Or we could go the Lombok way and annotate as necessary.
From the two approaches above, we will discover
- that with the first “full-blown-pojo”, approach, we have so much boilerplate code. Yes I know, most IDEs can help generate these when needed; and
- with the second approach, we have had to introduce a new dependency that helps us reduce the boilerplate codes and make life easier.
How about if we do not need to create so much boilerplate code or introduce a dependency and still be able to create a “data-carrier-pojo” classes that
- contains data not meant to be altered (think final fields);
- has the most fundamental methods like constructors and accessors;
This is where Java records come in handy.
Next, we will be looking at the What and Hows of Java records.
So what are Java records 🤔 🤔
According to JEP 395, Java records are
Classes that act as transparent carriers for immutable data
Essentially, records enable us to model plain and concise data classes with less ceremony — no need for us to create getters/setters or equals or hashcode methods ourselves.
Yes, I heard you ask 😄!
We don’t need to create getters/setters, equals, hashCode, and toString methods because the makeup of a record ensures that these methods are gotten automatically.
Constructing a record class
A record class is made up of
- The header lists the components (think fields 😃). Each field declared is automatically
private final
and apublic
accessor (think getter) method; - A canonical or compact constructor with the same name as the header. This constructor is optional but can be used in case we want to ensure that some or all our fields have values (or not
null
)
Next, let's look at examples of record classes using our above EmailData
object.
A few non-exhaustive notes on record classes
- A
record
class is implicitlyfinal
and cannot beabstract
- The
abstract
classjava.lang.Record
is the superclass for all records classes; - We can create our own accessor methods, but these should match the accessor which would be automatically created for us
- We can also have static fields and methods in our record class but not instance (non-static) fields
- A record class can implement one or more interfaces
- A record does not have a default constructor. Remember a record is created as a data carrier, which means it is declared with expected components (think fields)
Now let’s put our record class to use
Putting records to use 🎯
Having looked at the why and what of record classes, it's time to put records to use…
The basic use
EmailData emailData = new EmailData("test@tester.com", "Just testing", "A test message", Set.of());
assertThat(emailData, notNullValue());
assertThat(emailData.message(), equalTo("A test message"));
Surprised??! 😅 Yes, we use the new
keyword to create an instance of our record class.
JSON Use
To demonstrate this use, I will create a Person
class with a minimal annotation to allow the use of randonuser generator API
Next, we fetch a Person
from the randomuser API, then using our Person.from(..)
method deserialize to Person
So far in today's post, we have looked at Java records and answered the Why, What, and How questions.
The code used above is available on GitHub.