Quiz yourself: Deserialization of Java enum types and records

March 13, 2023 | 7 minute read
Text Size 100%:

Whenever code running in a JVM refers to a particular enum value, execution cannot proceed unless that enum value has been initialized.

More quiz questions available here

Imagine you are given the following enum and record:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
enum Gender {
  MALE("M"), FEMALE("F"), OTHER;
  Gender() {
    System.out.print("Gender ");
  }
  Gender(String s) {
    System.out.print("GenderS ");
  }
}
record Person(int age, String name, Gender g) implements Serializable {
  Person() {
    this(0, "", Gender.OTHER);
    System.out.print("Person ");
  }
  Person {
    System.out.print("PersonC ");
  }
}

Previously an instance of Person was constructed like the following and serialized to a file:

var v = new Person(60, "John Doe", Gender.MALE);

What might be printed to the console when a program deserializes the contents of the file mentioned above? Choose two.

A. Gender PersonC Person
B. Gender PersonC
C. PersonC Person
D. PersonC
E. Gender Gender Gender Person PersonC
F. GenderS GenderS Gender Person PersonC
G. GenderS GenderS Gender PersonC

Answer. This question investigates the mechanisms of deserialization for both enum types and records.

When a record is deserialized, each of the attributes—in this case the age, name, and gender attributes—must be deserialized to their respective objects or values first. (Note that the age primitive will not create an object.) These attributes are then passed to the canonical constructor of the record. Note that this behavior with records is quite different from deserializing instances of normal classes. No output is printed in handling the int age and the String name attributes, but it’s possible that output might be printed for Gender. You should look at this first.

Whenever code running in a JVM refers to a particular enum value, execution cannot proceed unless that enum value has been initialized. In most cases, the very first reference to one of these values will require several steps to be performed: class loading, class linking, and class preparation. Those three steps must happen in the order listed.

It’s fairly unusual for developers to notice the fact that these steps are separate, but that really doesn’t matter. Class preparation is the step that performs static initialization, and therefore it’s the step that creates the values of the enum itself. This quiz question is unaffected by the earlier steps, so the following discussion will consider the enum to be fully prepared, ignoring the fact that that’s actually the last of several steps. (If you’re interested in this, you can find more in section 12.2 and section 12.3 of the Java Language Specification, which in turn refer to the Java Virtual Machine Specification.)

For this question, you need to know three things.

  • An attempt to deserialize an object that contains a field of an enum type will make a reference to an enum value.
  • Any reference to an enum value requires the enum class to be fully prepared.
  • Therefore, the enum must have been fully prepared—complete with the initialization of all the enum’s constants—before deserialization can proceed.

In view of the above, there are now two distinct possibilities.

First, suppose that at the moment when an instance of Person is deserialized, the enum has not been fully prepared. In this case, it will be necessary to perform that preparation before Person can proceed with deserialization. That initialization includes instantiating all three of the enum’s values. In this situation, expect the three instances to be initialized using their appropriate constructors. (Remember this is the initialization process, not deserialization itself, that is causing this behavior.) So, in this situation, you will see GenderS GenderS Gender as the output, preceding any output referring to Person.

By contrast, if the Gender type has been fully prepared before the moment when an instance of Person is deserialized, the three instances already exist, and no further constructor behavior will be invoked for the Gender type. In such a situation, no messages related to Gender will be output by the deserialization process.

Once the enum is fully prepared, deserialization of the record type can proceed; note that record types differ from other classes in how this happens. A particular difference is that the canonical constructor for a record type is invoked to perform the initialization of the newly allocated object. By contrast, regular serializable classes do not have any constructor invoked, nor is any of the normal instance initialization of the serializable class itself run. (Note, however, that nonserializable parent types will have their default constructors executed during deserialization.)

Given that the canonical constructor is invoked, you will see the message PersonC printed as output. Note that in this case, the canonical constructor is presented in the compact form whereas the argument list is omitted, as is the code that initializes the fields of the record.

Because you might or might not see GenderS GenderS Gender, and you will see PersonC in the output, the two valid answers are

PersonC
GenderS GenderS Gender PersonC

These match options D and G, which tells you that those are the correct options and, by elimination, the other options are incorrect.

If you want to dive more deeply into the serialization process, it is, of course, in the Java specification. See the section that describes the process as it relates to records. In particular, the documentation notes the following:

During deserialization, if the local class equivalent of the specified stream class descriptor is a record class, then first the stream fields are read and reconstructed to serve as the record’s component values; and second, a record object is created by invoking the record’s canonical constructor with the component values as arguments…

The process related to enums is documented in this section of the Java specification, which notes

To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments.

Notice that the enum deserialization process does not instantiate the values of the enum unless, by necessity, the static initialization process must be completed to complete the deserialization.

As a side note, the descriptions above touched on the idea that there are several steps on the way to class preparation. Most JVMs will perform these steps lazily, simply ensuring that a class is prepared before it’s actually needed. The exact details can vary from one JVM to the next even if the behavior of the developer’s code conforms to the expectations listed in the Java specification.

This lazy approach can be quite beneficial. For example, consider a program with a help system that is built with many classes that are unique to it. If the help system is never used during an execution of that program, those classes might not be needed, and they might never be loaded at all. Further, even if a class is loaded, it might not need to be prepared. Either of these situations can save memory.

To see how this works, try building the following code and observing at what point in the process the MyLazySingleton class is prepared. (Remember that preparation relates to the static initialization.)

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
class MyLazySingleton {
  static {
    System.out.println("preparing MyLazySingleton");
  }
  private static final MyLazySingleton singleton =
    new MyLazySingleton();
  private MyLazySingleton() {
    System.out.println("instantiating MyLazySingleton");
  }
  public static MyLazySingleton get() {
    return singleton;
  }
}

public class TryStuff {
  public static void main(String[] args) {
    System.out.println("Starting");
    MyLazySingleton mls = null;
    System.out.println("mls variable initialized to null");
    Class<?> cl = MyLazySingleton.class;
    System.out.println("MyLazySingleton.class evaluated.");
    mls = MyLazySingleton.get();
    System.out.println("singleton retrieved");
  }
}

If you run this code under a tracing tool, you’ll probably notice that the file myclasspath/packagename/MyLazySingleton.class is read during the reference to the java.lang.Class object MyLazySingleton.class. However, you’ll also see that the static initialization and, with it, the instantiation of the single instance of MyLazySingleton, does not occur until the code invokes MyLazySingleton.get().

Conclusion. The correct answers are options D and G.

Dig deeper

Mikalai Zaikin

Mikalai Zaikin is a lead Java developer at IBA Lithuania (part of worldwide IBA Group) and currently located in Vilnius. During his career, Zaikin has helped Oracle with development of Java certification exams, and he has been a technical reviewer of several Java certification books, including three editions of the famous Sun Certified Programmer for Java study guides by Kathy Sierra and Bert Bates.

Simon Roberts

Simon Roberts joined Sun Microsystems in time to teach Sun’s first Java classes in the UK. He created the Sun Certified Java Programmer and Sun Certified Java Developer exams. He wrote several Java certification guides and is currently a freelance educator who publishes recorded and live video training through Pearson InformIT (available direct and through the O’Reilly Safari Books Online service). He remains involved with Oracle’s Java certification projects.


Previous Post

Quiz yourself: Verifying the operation of stinky Java code

Simon Roberts | 6 min read

Next Post


Quiz yourself: The strange case of the Java developer’s birthday

Simon Roberts | 4 min read