[JAVA] Item 89: For instance control, prefer enum types to readResolve

89. For instance control, choose enum type from readResolve

Item 3 introduced the singleton pattern with the following example.

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    ...
    }

    public void leaveTheBuilding() {
    ...
    }
}

This class is no longer a singleton when it implements Serializable. It is no longer a singleton, regardless of whether it is a default serialized form or a custom serialized form, and whether it provides a readObject method (Item87). The readObject method, whether default or not, returns a newly created instance that is different from the one created when the class was initialized.

readResolve By using the readResolve method, it is possible to return an instance different from the object created by readObject.

If the deserialized class properly defines the readResolve method, this method will be called after deserialization for the newly created object. The reference returned by this method replaces the newly created object. With this feature, in most cases there will be no references to newly created objects, and newly created objects will soon be available for garbage collection.

If the above Elvis class implements Serializable, you can guarantee that it is a singleton by adding the readResolve method as follows.

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    ...
    }

    public void leaveTheBuilding() {
    ...
    }
    private Object readResolve() {
        return INSTANCE;
    }
}

This readResolve method ignores the deserialized object and returns the instance created during class initialization. Therefore, serialized Elvis instances must not have any data. That is, all fields must be declared as transient.

** If you want to control an instance with readResolve, you must add transient to all fields that have references **. Otherwise, an attack like the one shown in Item88 could be made before readResolve runs.

The attack is a bit complicated, but the idea is simple.

First, write the stealer class. The stealer class has a readResolve method and a singleton field hidden by the stealer. In the serialization stream, replace the singleton non-transient field with an instance of stealer. At this time, the singleton instance contains a stealer, which has a singleton reference.

Since the singleton contains a stealer, when the singleton is deserialized, the stealer's readResolve method flows first. As a result, when the stealer's readResolve method flows, the instance field will refer to a partially deserialized singleton.

stealer's readResolve method copies the reference from the instance field to the static field, and the reference is accessible even after the readResolve method has flowed. The readResolve method then returns the correct type of hidden singleton field. Otherwise, the VM will throw a CastException when the serialization system stores a stealer reference inside a singleton field.

A specific example is shown. Consider the following broken singleton class.

package tryAny.effectiveJava;

import java.io.Serializable;
import java.util.Arrays;

//Incomplete singleton implementation with non-transient properties
public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    //Non-transient properties
    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

The stealer class is as follows.

package tryAny.effectiveJava;

import java.io.Serializable;

//A class that attacks the Elvis class
public class ElvisStealer implements Serializable {
    //A static field for holding non-singleton instances
    static Elvis impersonator;

    //Another Elvis instance
    private Elvis payload;

    private Object readResolve() {
        //Keep the Elvis instance held in the payload in the impersonator
        impersonator = payload;

        //Returns a string array because it disguises this property as a string array
        return new String[] { "A Fool Such as I" };
    }

    private static final long serialVersionUID = 0;
}

Below is a class that deserializes a homemade stream that creates two instances of a defective singleton. (Doesn't it work with Java 10?)

package tryAny.effectiveJava;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ElvisImpersonator {
    //Modified Elvis stream
    private static final byte[] serializedForm = new byte[] { (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00,
            0x05, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6, (byte) 0x93, 0x33, (byte) 0xc3, (byte) 0xf4,
            (byte) 0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53,
            0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f,
            0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76, 0x69, 0x73,
            0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
            0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69,
            0x73, 0x3b, 0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02 };

    public static void main(String[] args) {
        Elvis elvis = (Elvis) deserialize(serializedForm);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }

    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }

    }
}

When you run this program

[Hound Dog,Heartbreak Hotel]
[A Fool Such as I]

It seems that it is displayed as. (When I ran it on Java 10, it didn't work. It looks like the following)

Exception in thread "main" java.lang.IllegalArgumentException: java.lang.ClassNotFoundException: Elvis
	at tryAny.effectiveJava.ElvisImpersonator.deserialize(ElvisImpersonator.java:29)
	at tryAny.effectiveJava.ElvisImpersonator.main(ElvisImpersonator.java:19)
Caused by: java.lang.ClassNotFoundException: Elvis
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:374)
	at java.base/java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:685)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1877)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1763)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2051)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at tryAny.effectiveJava.ElvisImpersonator.deserialize(ElvisImpersonator.java:27)
	... 1 more

If the result is as expected, it is a problem because there are two different instances even though it is a singleton.

This problem can be solved by making the favoriteSongs field transient.

single-element enum However, it makes more sense to create an Elvis class as an enum with one element (Item3).

If you write a serializable class that requires instance control in an enum, Java guarantees that there can be no instance other than the one declared. Here's an example from Elvis:

public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs =
            {"Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

Enums haven't made readResolve instance control obsolete. Enums cannot handle classes that are serializable and require instance control, which are not fixed at compile time.

readResolve accessibility

If you put readResolve in the final class, it should be private.

If you put it in a non-final class, you need to consider it. If it is private, it cannot be accessed from subclasses. If package-private, only subclasses of the same package can be accessed. Can be used in all subclasses if protected or public.

In the case of protected or public, if the subclass does not override readResolve, it will return an instance of the superclass when deserializing the subclass, which is likely to cause a ClassCastException.

Recommended Posts

Item 89: For instance control, prefer enum types to readResolve
Item 58: Prefer for-each loops to traditional for loops
Item 61: Prefer primitive types to boxed primitives
Item 28: Prefer lists to arrays
Item 65: Prefer interfaces to reflection
Item 43: Prefer method references to lambdas
Item 42: Prefer lambdas to anonymous classes
Item 39: Prefer annotations to naming patterns
Item 85: Prefer alternatives to Java serialization
Item 41: Use marker interfaces to define types
Item 23: Prefer class hierarchies to tagged classes
Item 81: Prefer concurrency utilities to wait and notify
Item 80: Prefer executors, tasks, and streams to threads