Java records are a preview feature available in Java 14 which natively reduces boilerplate code when dealing with Data Classes. Learn how we can actually model data as data-only aggregates and close the gap in Java’s type system.
Java Records – What’s the Problem?
We all model data Classes like domain objects, DTO’s etc .. and we do it by encapsulating it in a Class. We don’t really have any other option since that is the Java way. The problem is that we encounter so much overhead like …
- Overriding equals ( ), toString ( ), hashCode ( )
- Accessor methods
- Constructor
- Possibly enforcing immutability (final)
Most of us resort to third party frameworks like Lombok which allow us to use the @Data annotation to auto-generate the boilerplate code for us. Sometimes we actually write the code ourselves or have the IDE do it for us. Either way, we do this this because Java just doesn’t have a native way to generate it for us.
Here is an example of a Java data class named “Location” which is just a data container for a geographic point (latitude and longitude) on the earth .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
public class Location { private float latitude; private float longitude; public Location(float latitude, float longitude) { setLatitude(latitude); setLongitude(longitude); } public float getLatitude() { return latitude; } public float getLongitude() { return longitude; } public void setLongitude(float longitude) { this.longitude = longitude; } public void setLatitude(float latitude) { this.latitude = latitude; } public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Location)) { return false; } final Location location = (Location) o; if (Float.compare(location.longitude, longitude) != 0) { return false; } return location.equals(longitude); } public int hashCode() { int result = 0; result = 29 * result + Float.floatToIntBits(latitude); result = 29 * result + Float.floatToIntBits(longitude); return result; } public String toString() { return "latitude =" + getLongitude() + " long = " + getLongitude(); } } |
That is a lot of noise … all we want is to model Location as a type which groups related fields together as a data item. Debugging can get tricky with this boilerplate code since refactoring can lead to hard to find bugs. There’s also the case where one may not even implement the code above at all .. of course I’m not talking about you 😉
Isn’t there a more concise way to model data in Java? There is now and that’s where Java records come in.
What is a Java Record?
A Java Record is a simply a data container for a group of related fields you want to aggregate together. We now have a native way to achieve this. Here is an example of using a Java Record to replace the Location Class which was shown earlier …
1 2 3 4 5 |
/* Declare a Java Record */ record Location (float latitude, float longitude){} /*Instantiate it */ Location india = new Location (28.6139F, 77.2090F); |
Where did all that code go? Well, I guess I have some explaining to do now. A record is “the state, the whole state, and nothing but the state” or put another way “the fields, just the fields, and nothing but the fields“.
This means that a Java record allows us to make a stronger semantic statement in which the type (Location) is just the state provided (latitude, longitude) and the instance (india) is just an aggregate of the field values (28.6139, 77.2090).
In order to support this, we have a new java.lang.Record Class which extends Object and declares equals( ) , toString( ) and hashCode( ) as abstract. Don’t try to extend the Record class and instantiate it yourself, that won’t work since it is marked as final. You can only get one as I declared above, then javac will create it for you … but with some goodies.
You can check out the Video version of this article on YouTube …
What is the Advantage of a Java Record ?
The beauty is that all that ceremony code is auto-generated for you. Here’s what is auto-generated ..
- A default public constructor which assigns your state to final instance variables
- equals( ) , toString( ) and hashCode( ) method
- accessor methods but no setters since records are by default immutable (fields are final)
A Java record declares its representation, and commits to an API that matches that representation. This means the field names become your API, thus you lose the flexibility to decouple API from representation. What does that mean?
It means you lose the flexibility to hide the representation behind names like getLat(), getX() or getWhateverYouCallit() etc ..
By default, .latitude() and .longitude() are the auto-generated accessor methods you get, therefore the names you select for your state description are very important since they become your API (what you interact with)!
Here’s an example that will drive it home using jshell (all code on GitHub here). Notice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
System.out.println("MVP Java - JDK 14 Records Demo") record Location (float latitude, float longitude){} Location santaMaria = new Location (36.947613F, -25.146546F); Location santaMariaCopy = new Location (36.947613F, -25.146546F); Location india = new Location (28.6139F , 77.2090F ); System.out.println ("india.toString() = " + india); System.out.println ("santaMaria.toString() = " + santaMaria); System.out.println ("santaMaria latititude = " + santaMaria.latitude()); //not getLatitude() !! System.out.println ("santaMaria longitude = " + santaMaria.longitude()); //not getLongitude() !! System.out.println (); System.out.println ("santaMaria.hashCode() = " + santaMaria.hashCode()); System.out.println ("santaMariaCopy.hashCode() = " + santaMariaCopy.hashCode()); System.out.println ("santaMaria.equals(santaMariaCopy) = " + santaMaria.equals(santaMariaCopy)); System.out.println ("santaMaria.equals(india) = " + santaMaria.equals(india)); |
And the following output will show that the equals( ) , toString( ) , hashCode( ) and accessor methods have been auto-generated.
1 2 3 4 5 6 7 8 9 10 |
MVP Java - JDK 14 Records Demo india.toString() = Location[latitude=28.6139, longitude=77.209] santaMaria.toString() = Location[latitude=36.947613, longitude=-25.146545] santaMaria latititude = 36.947613 santaMaria longitude = -25.146545 santaMaria.hashCode() = -1037128411 santaMariaCopy.hashCode() = -1037128411 santaMaria.equals(santaMariaCopy) = true santaMaria.equals(india) = false |
Can I Customize a Java Record?
Yes, you can customize a Java record. You may want to add validation in the constructor or add some static factory method. You can customize a Java record to the point that it largely resembles an actual Java Class which begs the question, should you?
Beside a Java Record not being able to extend a Class or being made abstract there is the limitation where you can only add additional fields which are static.
The following example introduces parameter validation in the canonical constructor (the one whose signature matches the record’s state description). Even if you leave out the args and variable assignments in the canonical constructor like I have below, javac will still do it for you. There is also a custom toString() and I have even brought back our good old getters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
System.out.println("MVP Java - JDK 14 Records Demo - Override Record") //customize/Override record record Location (float latitude, float longitude){ static float DEFAULT_LATITUDE = 0F; static float DEFAULT_LONGITUDE = 0F; public Location // canonical constructor (don't put empty parenthesis! { if(latitude < -90 || latitude > 90){ throw new IllegalArgumentException("Invalid latititude " + latitude); } // add longitude validation too ... // what is missing here? ... assignment done for us anyways! } public String toString() { return "lat = " + latitude + " long = " + longitude; } //can override hashCode .. etc .. public float getLatitude (){ return latitude; } public float getLongitude (){ return longitude; } public static Location origin() { return new Location(DEFAULT_LATITUDE, DEFAULT_LONGITUDE); } } Location santaMaria = new Location (36.947613F, -25.146546F); System.out.println ("santaMaria.toString() = " + santaMaria); System.out.println ("santaMaria latititude = " + santaMaria.getLatitude()); System.out.println ("santaMaria longitude = " + santaMaria.getLongitude()); System.out.println (); Location originLocation = Location.origin(); System.out.println ("originLocation.toString() = " + originLocation); |
Here is the output when the above is executed in JShell.
1 2 3 4 5 6 7 |
MVP Java - JDK 14 Records Demo - Override Record santaMaria.toString() = lat = 36.947613 long = -25.146545 santaMaria latititude = 36.947613 santaMaria longitude = -25.146545 originLocation.toString() = lat = 0.0 long = 0.0 |
My advise is to not customize too much like the example above. If you need to perform validation and/or add some static factory method than that’s ok but once you open the flood gates, it’s better to go with a standard Java Class.
Summary
Java records provide first class support for modeling data only aggregates and eliminates much of the boilerplate code in doing so but that’s not all. Aside from this, it provides a clearer way for APIs, compilers and frameworks to understand and process the high level data. It will be interesting to see what developments will be made in that area in the near future.
Will this close a possible gap in Java’s type system? Time will tell but I already like what I see and seeing as this is an experimental preview feature introduced in Java 14, your feedback is welcome here at the Amber mailing list.
Check out another preview feature article here (Switch expression).
All code on MVP Java’s GitHub account