Abstract: Java 14 Records (JEP359) reduce the boiler plate for simple POJOs. In this newsletter we look at what special magic is used to serialize and deserialize these new records.
Welcome to the 276th edition of The Java(tm) Specialists' Newsletter, which I am writing once again at 36.000 feet, flying from Madrid to Athens. Our captain just announced the speed of this Airbus A320 as Mach 0.79. Too funny. I've certainly not heard that before. Here's a shout-out to Captain Nick for keeping us entertained.
I had the honour of assisting fellow Java Champion Oleh Dokuka as he presented his Reactive Programming Course. The class started cautiously, but as the training progressed, they reacted more and more to his prompts for questions, so much so that we almost had to apply back pressure as we were running out of time. (OK, I admit that was a bad joke, even for me!)
I mentioned in a previous newsletter that my dynamic proxies in Java book is nearing completion. We are waiting for some artwork for the cover, an ISBN number, and then we should be ready to rock and roll. You can have a look at our code samples here. It's been far too much fun writing this book. Since the topic is a tad complicated, I'm also developing a companion course to make the content more accessible. It will be ready by the time the book is published. The course has a ton of exercises, plus detailed lectures of how everything works. Imagine - 279 slides on dynamic proxies in Java. BTW, we use Java modules for the book samples. Our plan is to upload our jar file onto Maven Central, so that you can use our code magic in your projects.
I didn't write this new book for the general Java population, or as I like to call them, the "Java proletariat". No, I wrote it for YOU, my Java Specialist fan. You will love the book. The code is challenging, with just the right amount of humour to make you chuckle. This book was a gigantic group effort. The acknowledgements page carries on for two full A4 pages. This isn't my book. This is our book.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
A few months ago, Java language architect Brian Goetz bemoaned the lack of feedback they were getting for preview features. Since no one was using them in anger, deficiencies in the design also did not flow back to the architects before the concrete was set.
I vowed to contribute in some small way towards making the Java world a better place. My website JavaSpecialists.EU is my bread and butter. When it's down, my children don't eat. It contains hundreds of articles and has a good chunk of visitors every day. It's why you know me (except for my Mutti, who knew me before anyone else on this list). It is thus super important that it is up 24/7. However, I decided that from this year, I will always run my website on the next OpenJDK preview version, using as many of the new features as reasonable. My website now proudly displays the version we are using. At the moment it is "Running on Java 14+36-1461 (Preview)". As soon as Java 14 is officially released, I'm switching to Java 15 preview. Yes, there are risks of using the latest preview features in production. I am hoping (and praying) that my tests discover the bugs before my customers do.
One of the features that I gobbled up was JEP359 - records. I now create all my course completion certificates as records instead of conventional Java classes. Records certainly have many very interesting features, and I'm sure that if you search on the internet, you will find at least half a dozen articles on how cool they are. However, in this newsletter, I want to only focus on one small aspect of records - serialization.
Serialization allows us to convert an existing class to bits and bytes by simply implementing Serializable.
Let's start with a small example of a Person interface, followed by a PersonClass that shows how we would traditionally code in Java.
public interface Person { String firstName(); String lastName(); }
import java.util.*; public class PersonClass implements Person, java.io.Serializable { private final String firstName; private final String lastName; public PersonClass(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String firstName() { return firstName; } public String lastName() { return lastName; } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersonClass that = (PersonClass) o; return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName); } public int hashCode() { return Objects.hash(firstName, lastName); } }
Wow, that's a lot of code to program something really simple.
Plus we forgot to implement toString(), so we will see the
dreaded PersonClass@324a093
output.
Note that to make this serializable, all we needed was to
implement the java.io.Serializable
interface.
Now let's forward to Java 14-preview. Here is how it looks with records:
public record PersonRecord(String firstName, String lastName) implements Person, java.io.Serializable { }
And, um, we're done.
Awesome, now how about writing some of these persons to a file? That's easy in both cases:
import java.io.*; public class PersonWriteTest { public static void main(String... args) throws IOException { try (var out = new ObjectOutputStream( new FileOutputStream("persons.bin"))) { out.writeObject(new PersonRecord("Heinz", "Kabutz")); out.writeObject(new PersonClass("Heinz", "Sommerfeld")); out.writeObject(null); } } }
And we can also read them back again and print them to
System.out
.
import java.io.*; public class PersonReadTest { public static void main(String... args) throws IOException, ClassNotFoundException { try (var in = new ObjectInputStream( new FileInputStream("persons.bin"))) { Person p; while ((p = (Person) in.readObject()) != null) { System.out.println(p); } } } }
Output is this:
PersonRecord[firstName=Heinz, lastName=Kabutz] PersonClass@864b6d9
Ah yes, of course. We forgot to add toString()
to our PersonClass
, but the record added it
automatically, together with equals()
and
hashCode()
.
However, with GDPR, we might want to ask for as little
personal information as possible. Let's change both
PersonClass
and PersonRecord
to
have an optional parameter for lastName
. This
time we also add the toString()
method.
import java.util.*; public class PersonClass implements Person, java.io.Serializable { private final String firstName; private final String lastName; public PersonClass(String firstName) { this(firstName, null); } public PersonClass(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String firstName() { return firstName; } public String lastName() { return lastName; } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersonClass that = (PersonClass) o; return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName); } public int hashCode() { return Objects.hash(firstName, lastName); } public String toString() { return "PersonClass{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; } }
Our PersonRecord is also changed to get a second constructor, besides its canonical one:
public record PersonRecord(String firstName, String lastName) implements Person, java.io.Serializable { public PersonRecord(String firstName) { this(firstName, null); } }
We now re-run the PersonReadTest
and this
happens:
PersonRecord[firstName=Kabutz, lastName=Heinz] Exception in thread "main" java.io.InvalidClassException: PersonClass; local class incompatible: stream classdesc serialVersionUID = 3606827991270232798, local class serialVersionUID = 370566374786230668
As we can see, the PersonRecord
was read back
without an issue, but the PersonClass
failed.
Can we make it work without re-running
PersonWriteTest
? Indeed we can, by adding
the following line to the PersonClass
:
private static final long serialVersionUID = 3606827991270232798L;
Run it again and we see this:
PersonRecord[firstName=Heinz, lastName=Kabutz] PersonClass{firstName='Heinz', lastName='Sommerfeld'}
Beautiful! However, we also received a stern letter from the USA that we are using a trademarked name. Not allowed! We change our constructors to explicitly disallow this:
public class PersonClass implements Person, java.io.Serializable { /* snip */ public PersonClass(String firstName, String lastName) { if ("Heinz".equals(firstName)) throw new IllegalArgumentException( "\"%s\" is trademarked".formatted(firstName)); this.firstName = firstName; this.lastName = lastName; } /* snip */ }
When we run our PersonReadTest
, we see that
the constructor of PersonClass
is not called,
thus we are still creating persons with names that might land
us in hot water:
PersonRecord[firstName=Heinz, lastName=Kabutz] PersonClass{firstName='Heinz', lastName='Sommerfeld'}
Let's also change PersonRecord
:
public record PersonRecord(String firstName, String lastName) implements Person, java.io.Serializable { public PersonRecord(String firstName) { this(firstName, null); } public PersonRecord{ if ("Heinz".equals(firstName)) throw new IllegalArgumentException( "\"%s\" is trademarked".formatted(firstName)); } }
When we now run the PersonReadTest
, we see
this:
Exception in thread "main" java.io.InvalidObjectException: "Heinz" is trademarked at java.base/java.io.ObjectInputStream.readRecord.... at PersonReadTest.main(PersonReadTest.java:9) Caused by: java.lang.IllegalArgumentException: "Heinz" is trademarked at PersonRecord(PersonRecord.java:9) at java.base/java.io.ObjectInputStream.readRecord
Stunning. The canonical constructor is called when we read
records from an ObjectInputStream
. This means
any checking code is also called. Great work, Java
architects!
You might be wondering why we were able to read the record
earlier, even after changing the structure of the class.
The explanation is simple. Records by default always have a
serialVersionUID
of 0.
Kind regards
Heinz
P.S. My newsletter was ready yesterday, but I could not resist the urge to delay it until today, the 29th of February. The previous leap day was about my MapClashInspector, still as relevant today as it was four year ago, or one leap year ago.
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.