Running on Java 22-ea+27-2262 (Preview)
Home of The JavaSpecialists' Newsletter

276Serializing Records

Author: Dr Heinz M. KabutzDate: 2020-02-29Java Version: 14-previewCategory: Tips and Tricks
 

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.

Serializing Records

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.

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...