0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

89.インスタンス制御に対しては、readResolve より enum 型を選ぶべし

Item3で以下のような例でシングルトンパターンを紹介した。

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

    private Elvis() {
    ...
    }

    public void leaveTheBuilding() {
    ...
    }
}

このクラスはSerializableを実装すると、シングルトンではなくなってしまう。
デフォルトserialized formであるかカスタムserialized form であるかに関わらず、また、readObjectメソッドを提供するか(Item87)に関わらず、シングルトンではなくなってしまう。
readObjectメソッドはデフォルトのものであれ、そうでないものであれ、クラス初期化時に作られたものとは違う、新しく作成したインスタンスを返すようになっている。

readResolve

readResolveメソッドを使うことで、readObjectで作成されたオブジェクトとは別のインスタンスを返すことができる。

もし、デシリアライズされるクラスが適切にreadResolveメソッドを定義していたら、このメソッドは新しく作成されたオブジェクトに対して、デシリアライズされた後に呼び出される。
このメソッドによって返される参照は、新しく作成されたオブジェクトの代わりとなるものである。この機能を用いると、大体の場合新しく作成されたオブジェクトに対する参照はなくなり、新しく作成されたオブジェクトはすぐにガベージコレクションによって処理可能となる。

上記のElvisクラスがSerializableを実装した場合、以下のようにreadResolveメソッドを付ければシングルトンであることを保証できる。

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

    private Elvis() {
    ...
    }

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

このreadResolveメソッドでは、デシリアライズされたオブジェクトを無視し、クラス初期化時に作成されたインスタンスを返却する。
そのため、シリアライズされたElvisインスタンスは何らかのデータを持ってはならない。つまり、すべてのフィールドはtransientで宣言されていなければならない。

もしreadResolveでインスタンスのコントロールをしようとするならば、参照を持つすべてのフィールドにはtransientをつけねばならない
そうでなければ、Item88で見せた例のような攻撃が、readResolveが走る前に行われる可能性がある。

攻撃は少し複雑だが、そのアイデアはシンプルなものである。

まず、stealerクラスを書く。
stealerクラスには、readResolveメソッドと、stealerが隠ぺいするシングルトンのフィールドがある。
シリアライゼーションストリームの中で、シングルトンのtransientでないフィールドをstealerのインスタンスで置き換える。
この時、シングルトンのインスタンスはstealerを含み、stealerはシングルトンの参照を持っている。

シングルトンはstealerを含んでいるので、シングルトンがデシリアライズされる時、最初にstealerのreadResolveメソッドが流れる。
結果として、stealerのreadResolveメソッドが流れるときには、そのインスタンスフィールドには一部デシリアライズされたシングルトンが参照されることとなる。

stealerのreadResolveメソッドは、その参照をインスタンスフィールドからstaticフィールドにコピーし、該当の参照はreadResolveメソッドが流れた後でもアクセス可能となる。
そのあと、readResolveメソッドは、隠したシングルトンのフィールドの正しい型を返す。このようにしないと、シリアライゼーションシステムがシングルトンのフィールドの中にstealerの参照を格納するときに、VMがCastExceptionをスローする。

具体例を示す。以下のような壊れたシングルトンクラスについて考える。

package tryAny.effectiveJava;

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

//非 transient プロパティを含む不完全なシングルトン実装
public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    // 非 transient なプロパティ
    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

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

    private Object readResolve() {
        return INSTANCE;
    }
}

stealerクラスは以下のよう。

package tryAny.effectiveJava;

import java.io.Serializable;

//Elvis クラスを攻撃するクラス
public class ElvisStealer implements Serializable {
    // シングルトン以外のインスタンスを保持しておくための static フィールド
    static Elvis impersonator;

    // もう一つの Elvis インスタンス
    private Elvis payload;

    private Object readResolve() {
        // payload に保持されている Elvis インスタンスを impersonator に保持しておく
        impersonator = payload;

        // このプロパティを文字列配列として偽装するので文字列配列を返す
        return new String[] { "A Fool Such as I" };
    }

    private static final long serialVersionUID = 0;
}

欠陥シングルトンの2つのインスタンスを生成するお手製ストリームをデシリアライズするクラスが以下になる。(Java10だと動かない?)

package tryAny.effectiveJava;

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

public class ElvisImpersonator {
    // 改変した Elvis ストリーム
    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);
        }

    }
}

このプログラムを実行すると、

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

のように表示されるらしい。(Java10で実行したところ動かず。以下の感じになる)

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

想定通りの結果になったとすると、シングルトンであるにも関わらず、異なる2つのインスタンスが存在していることになるので問題である。

この問題はfavoriteSongsフィールドをtransientにすることで解消できる。

single-element enum

しかし、Elvisクラスを1つの要素をもつenumとして作るほうが筋が良い(Item3)。

もし、シリアライザブルでインスタンスコントロールが必要なクラスをenumで書いたとしたら、Javaによって、宣言されたもの以外のインスタンスはあり得ないということが保証される。
Elvisの例では以下のようになる。

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

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

enumによって、readResolveによるインスタンスコントロールが時代遅れになったわけではない。
コンパイル時には確定していない、シリアライザブルでインスタンスコントロールが必要なクラスに関しては、enumでは対処することができない。

readResolveのアクセシビリティ

readResolveをfinalクラスに置くのであれば、privateにすべき。

finalではないクラスに置くのであれば、考慮が必要となる。
privateであれば、サブクラスからはアクセスできない。
package-privateであれば、同じパッケージのサブクラスしかアクセスできない。
protected または publicであれば、全てのサブクラスで使える。

protected または publicの場合で、サブクラスがreadResolveをオーバーライドしない場合、サブクラスをデシリアライズするときにスーパークラスのインスタンスを返すことになるので、ClassCastExceptionを発生させる可能性が高い。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?