Problem: Some of the fields are not serializable and you are getting java.io.NotSerializableException exceptions such as this:
Apr 10, 2023 9:40:35 AM TransientFinalExample main
INFO: Checking Serialization ...
Exception in thread "main" java.io.NotSerializableException: TransientFinalExample$NonSerializableField
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1196)
at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1581)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1538)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1447)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1190)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:355)
at com.flowlogix.util.SerializeTester.serializeAndDeserialize(SerializeTester.java:40)
at TransientFinalExample.main(TransientFinalExample.java:33)Furthermore, these kinds of issues can be buried deep inside the code that you do not control, or can’t modify easily. What to do?
transient keyword to the rescue!
However, transient comes with it’s own challenges, not easily solved, but here I offer a simple, generic solution.
Not only does this method solve the serialization problem, but also aids immutability via the ability to add the final keyword in addition to transient.
Here is an example of a non-serializable class with one integer field:
import java.io.*;public class TransientFinalExample implements Serializable {
static class NonSerializableField {
final Integer intValue;
public NonSerializableField(Integer intValue) {
this.intValue = intValue;
}
}
final Integer intValue = 5;
// Step One: Add transient keyword to avoid java.io.NotSerializableException
final transient NonSerializableField nonSerializableField
= new NonSerializableField(6);
}
Here, we added the transient keyword in addition to final to avoid java.io.NotSerializableException, however, we are now facing an additional problem:
Apr 10, 2023 9:41:12 AM TransientFinalExample main
INFO: Checking Serialization ...
Exception in thread "main" java.lang.NullPointerException: Cannot read field "intValue" because "deserialized.nonSerializableField" is null
at TransientFinalExample.main(TransientFinalExample.java:35)Apparently, even when initializing a transient field inline, it remains null upon deserialization. Below is the code that causes the above exception:
log.info("Checking Serialization ...");
var original = new TransientFinalExample();
TransientFinalExample deserialized = SerializeTester.serializeAndDeserialize(original);assert original.nonSerializableField.intValue.equals(deserialized.nonSerializableField.intValue);
How to fix this? Java’s magic method readResolve() to the rescue:
public class TransientFinalExample implements Serializable {
static class NonSerializableField {
final Integer intValue; public NonSerializableField(Integer intValue) {
this.intValue = intValue;
}
}
final Integer intValue = 5;
// Step One: Add transient keyword to avoid java.io.NotSerializableException
final transient NonSerializableField nonSerializableField
= new NonSerializableField(6);
// Step Two: Add the readResolve() method to force transient field initialization
Object readResolve() {
return new TransientFinalExample();
}
}
Bravo! This runs correctly!
Apr 10, 2023 9:43:28 AM TransientFinalExample main
INFO: Checking Serialization ...
Apr 10, 2023 9:43:29 AM TransientFinalExample main
INFO: DoneWhat happened? readResolve() method replaces the deserialized class with a new copy that is returned from it. This is how the final transient fields got initialized.
However, the readResolve() implementation leaves something to be desired. It’s not generic, and does not account for the state of the deserialized class. Project Lombok to the rescue! Below is a generic implementation:
Object readResolve() {
return toBuilder().build();
}That’s it! Now, the class will be generically constructed, taking into account any state that deserialized object had.
Putting it all together (code available on GitHub) / (How to Run):
///usr/bin/env jbang "$0" "$@" ; exit $?
//RUNTIME_OPTIONS -ea
//DEPS org.projectlombok:lombok:LATEST
//DEPS com.flowlogix:flowlogix-jee:LATEST// remove the below line when using JDK < 21
//COMPILE_OPTIONS -proc:full
import java.io.*;
import com.flowlogix.util.*;
import lombok.*;
import lombok.extern.java.*;
@Log
@Builder(toBuilder = true)
public class TransientFinalExample implements Serializable {
static class NonSerializableField {
final Integer intValue;
public NonSerializableField(Integer intValue) {
this.intValue = intValue;
}
}
final Integer intValue = 5;
final transient NonSerializableField nonSerializableField = new NonSerializableField(6);
Object readResolve() {
// return new TransientFinalExample();
return toBuilder().build();
}
public static void main(String... args) throws IOException, ClassNotFoundException {
log.info("Checking Serialization ...");
var original = new TransientFinalExample();
TransientFinalExample deserialized = SerializeTester.serializeAndDeserialize(original);
assert original.nonSerializableField.intValue.equals(deserialized.nonSerializableField.intValue);
log.info("Done");
}
}