Singleton にするために enum を使う是非

以下思考の変遷。

priority という値を持つ DTO と、その DTO の List を priority順で処理する必要があるロジックがありました。

■Dto
public class HogeDto {
    private short priority;
    public short getPriority() {return priority;}
    // その他の項目は省略
}

■Logic
public class FooLogic {
    private Comparator<HogeDto> priorityComparator = new Comparator<HogeDto>() {
        public int compare(HogeDto o1, HogeDto o2) {
            // 桁あふれを起こさないためにキャスト
            return ((int) o1.getPriority()) - o2.getPriority();
        }
    };
    // その他省略
}

そんな中、他のロジックでも priority順で処理したいという要望があがってきたので無名クラスで作成していた Comparator を public class にすることにしました。

public class HogeDto {
    private short priority;
    public short getPriority() {return priority;}
    // その他の項目は省略

    public static final class PriorityComparator implements Comparator<HogeDto>, Serializable {
        private static final long serialVersionUID = 699700470652630616L;

        @Override
        public int compare(HogeDto o1, HogeDto o2) {
            // 桁あふれを起こさないためにキャスト
            return ((int) o1.getPriority()) - o2.getPriority();
        }

        @Override
        public boolean equals(Object object) {
            // このクラスのインスタンスは全て等しいとみなす
            return object instanceof PriorityComparator;
        }

        @Override
        public int hashCode() {
            // equals を override したのでそれに合わせる
            return 1;
        }
    }
}

Comparator の実装は直列化可能であることを強く推奨されています。また性能要件から効率的な equals() をオーバーライドすることが推奨されています。

この PriorityComparator をはじめ、Comparator の多くはステートレスと思われるので、全てのインスタンスが同一であると見なせます。そうした場合、Object#equals() のデフォルト実装よりも、明示的にオーバーライドする方が効率のよい equals() を提供できます。したがって上記のようになりました。

ここで、はて?となります。

それならばいっそ Singleton にしてしまえば、equals() と hashCode() の明示的なオーバーライドは必要なくなるのでは?と考えました。

public class HogeDto {
    private short priority;
    public short getPriority() {return priority;}
    // その他の項目は省略

    public static final Comparator<HogeDto> priorityComparator = new PriorityComparator();

    public static final class PriorityComparator implements Comparator<HogeDto>, Serializable {
        private static final long serialVersionUID = 699700470652630616L;

        private PriorityComparator() {}

        @Override
        public int compare(HogeDto o1, HogeDto o2) {
            // 桁あふれを起こさないためにキャスト
            return ((int) o1.getPriority()) - o2.getPriority();
        }

        private Object readResolve() throws ObjectStreamException {
            return HogeDto.priorityComparator;
        }
    }
}

Singleton にしたコードです。最近ではその弊害が指摘され、安易に使用するのは憚られる Singleton ですが、PriorityComparator の場合、ステートレスなためそれほど気にせず Singleton にしても問題なさそうです。

これで equals() と hashCode() を実装しなくてすんで、コードがすっきり。と行きたい所ですがそう簡単にはいきません。

Serializable な Singleton クラスを作る場合、そのインスタンスが一意であることを保障するために readResolve() を実装しなければいけません。

そこで思い出すのが Joshua Bloch が Effective Java で書いていた内容です。彼曰く「Singleton を作りたければ enum を使え」

public class HogeDto {
    private short priority;
    public short getPriority() {return priority;}
    // その他の項目は省略

    public static enum PriorityComparator implements Comparator<HogeDto> {
        INSTANCE;
        
        @Override
        public int compare(HogeDto o1, HogeDto o2) {
            // 桁あふれを起こさないためにキャスト
            return ((int) o1.getPriority()) - o2.getPriority();
        }
    }
}

という訳で enum で書き直したコードです。恐ろしくすっきり!enum であれば直列化可能な事は保障されていますので、implements から Serializable も除去しました。readResolve の問題も自動で解決してくれます。

すっきりしたのは良いのですが、何やら釈然としないものを感じます。

というのは、enum の意図している目的から外れた利用方法なのでは?という印象が残るからです。

例えば、定数 interface の implements によるコード量の削減など、機能としては可能だけれども意図が不明になるのでやるべきではない記述(一部でテクニックとか言ったりする人が居る)というのが存在します。この enum の利用もそれに含まれるのではないか?という思いが強いのです。

この辺、Singleton に enum を使ってよい理由/使ってはNGな理由というのを識者の方々に聞いてみたいところです。