しらとりのブログ

社会人ひよこプログラマのtil

匿名クラスをシリアライズするときの奇妙な挙動について

TL;DR

  • 非staticのコンテキストで宣言された匿名クラスはエンクロージングインスタンスへの非transientな参照を持つ

匿名クラスでオブジェクトの初期化

Javaでは匿名クラスと初期化ブロックを組み合わせて以下のような書き方ができます。

User user = new User(){{
    setName("hoge");
}};

C#のオブジェクト初期化子のように書けるので一見便利ですが、シリアライズが絡む場合に直感的でない挙動をするので注意が必要です。
下記にオブジェクト初期化のパターンを4つ挙げてみます。

class User implements Serializable{
    private String name;
    public void setName(String name){
        this.name = name;
    }
}

class Main{
    public static Main(String[] args){
        User user1 = new User();
        user1.setName("hoge");

        User user2 = new User(){{
            setName("hoge");
        }};
        new Main.exec();
    }

    private void exec(){
        User user3 = new User();
        user3.setName("hoge");

        User user4 = new User(){{
            setName("hoge");
        }};
    }
}

このうち、user4のインスタンスをシリアライズしようとするとNotSerializableExceptionの実行時エラーが発生します。 これは非staticなコンテキストで宣言された匿名クラスが暗黙的にエンクロージングインスタンス(ここではMainクラスのインスタンス)を保持していることに起因します。 これによりMainクラスまでがシリアライズの対象となり本来の意図とは異なる動作をします。

エンクロージングインスタンスへの参照は明示的にtransientを付与することはできず、仮にMainクラスにSerializableでマークしてもさらに外側にクラスがある場合にそちらに伝播するだけなので注意が必要です。

参考 docs.oracle.com