Skip to content
Advertisement

Java record serialization and repeated calls to canonical constructor

In this post about serializable records it is stated that

Deserialization creates a new record object by invoking a record class’s canonical constructor, passing values deserialized from the stream as arguments to the canonical constructor. This is secure because it means the record class can validate the values before assigning them to fields, just like when an ordinary Java program creates a record object via new. “Impossible” objects are impossible.

This argues with a constructor that is used for validation only. However, when the constructor manipulates the arguments this results in rather strange behavior. Consider this very artificial simple example:

The following record manipulates a before saving it:

import java.io.Serializable;

public record TRecord (int a) implements Serializable {
    public TRecord {
        a = a-1;
    }
}

And the following program just saves the serialized record the first time and loads it the subsequent times:

import java.io.*;

public class TestRecords {

    public static void main(String args[]) {
        TRecord a1 = null;

        try {
            FileInputStream fileIn = new FileInputStream("tmp");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            a1 = (TRecord) in.readObject();
            in.close();
            fileIn.close();
        } catch (IOException | ClassNotFoundException i) {
            // ignore for now
        }
        if (a1 == null) {
            try {
                a1 = new TRecord(5);
                FileOutputStream fileOut = new FileOutputStream("tmp");
                ObjectOutputStream out = new ObjectOutputStream(fileOut);
                out.writeObject(a1);
                out.close();
                fileOut.close();
                System.out.printf("Serialized data is saved in /tmp/employee.ser");
            } catch (IOException i) {
                i.printStackTrace();
            }
        }

        System.out.println(a1);
    }
}

The output for the first run is TRecord[a=4], and TRecord[a=3] in subsequent runs, so the state that I get from deserialization differs from what I put in there. Using a comparable class like the following instead would have gotten me the same result TClass[a=4] every time.

import java.io.Serializable;

public class TClass implements Serializable {
    private int a;

    public TClass(final int a) {
        this.a = a-1;
    }

    public int getA() {return a;}

    public String toString() {
        return "Class[" + a + "]";
    }
}

So my question is: Is there any rule for records that forbids/discourages using the constructor for anything other than validations (I am thinking for example about hashing a password before storing the input)? Or is there another way to deserialize an object so that the initial state is restored?

Advertisement

Answer

If you look at the documentation for records it says the following:

For all record classes, the following invariant must hold: if a record R’s components are c1, c2, … cn, then if a record instance is copied as follows:

 R copy = new R(r.c1(), r.c2(), ..., r.cn());  

then it must be the case that r.equals(copy).

This is not the case for your record class however:

jshell> TRecord r1 = new TRecord(42);
r1 ==> TRecord[a=41]

jshell> TRecord copy = new TRecord(r1.a());
copy ==> TRecord[a=40]

jshell> r1.equals(copy)
$4 ==> false

In other words, your record type violates this invariant, and this is also the reason why you are seeing the inconsistent deserialization.

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement