Enum クラスの getDeclaringClass()
以下の Enum があったとします。
public enum Season { SPRING, SUMMER, AUTUMN, WINTER, ; }
getClass()
と getDeclaringClass()
は以下のような結果となります。
Season.SPRING.getClass() // foo.Season Season.SPRING.getDeclaringClass() // foo.Season Season.SPRING.getClass() == Season.SPRING.getDeclaringClass() // true
getClass()
も getDeclaringClass()
も Season
となり、これはあたりまえの結果ですね。
現在の季節の、次の季節を得る next()
を以下のように定義したとします。
public enum Season { SPRING { Season next() { return SUMMER; } }, SUMMER { Season next() { return AUTUMN; } }, AUTUMN { Season next() { return WINTER; } }, WINTER { Season next() { return SPRING; } }, ; abstract Season next(); }
TimeUnit などでもおなじみの書き方です(ただし現在の TimeUnit は匿名内部クラスを使わない実装に変更されています)。
この場合、 getClass()
と getDeclaringClass()
は以下のような結果となります。
Season.SPRING.getClass() // foo.Season$1 Season.SPRING.getDeclaringClass() // foo.Season Season.SPRING.getClass() == Season.SPRING.getDeclaringClass() // false
Enum 値は匿名内部クラス(anonymous inner classes)としてコンパイルされているため、Season$1
となります。まぁ、当然と言えば当然ですが。
これの何が問題かというと、例えば、以下のように Season.SPRING
の文字列を得て、リソースバンドルからローカライズ名を取得するというのは良くあるケースでしょう。
season.getClass().getSimpleName() + "." + season.name()
しかし、上記は、.SPRING
という文字列しか得られません。匿名内部クラスの getSimpleName()
は空文字を返却するためです。匿名と言うからにはそうなのかもしれませんが、Enum における上記実装では、匿名内部クラスのようには見えにくいので、割と混乱するポイントです。
Season.SPRING
というキーでリソースバンドル定義していたとすると、Season の内部実装の変更により、図らずリソースバンドルから値を取得できなくなってしまいます。
匿名クラスの getSimpleName()
が空文字を返すことは意外と知られておらず、たまに問題になったりしますね。
念のため getSimpleName()
の JavaDoc は以下のようになっています。
Returns the simple name of the underlying class as given in the source code. Returns an empty string if the underlying class is anonymous.
ソース・コード内で指定されたとおり、基本となるクラスの単純名を返します。基本となるクラスが匿名の場合、空の文字列を返します。
話を元に戻しましょう。Enum の getClass()
と getDeclaringClass()
の違いについての話でした。
Enum.getDeclaringClass()
の JavaDoc を見てみましょう。
この enum 定数の enum 型に対応するClassオブジェクトを返します。 e1.getDeclaringClass() == e2.getDeclaringClass() の場合だけ、2つの enum 定数 e1 と e2 は同じ enum 型の enum 定数です。 このメソッドにより返される値は、定数固有のクラス本文を持つ enum 定数について Object.getClass() メソッドで返される値とは異なる可能性があります。
実装を見た方が早いかもしれません。
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { public final Class<E> getDeclaringClass() { Class<?> clazz = getClass(); Class<?> zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper; } }
Enum 定数の親クラスが Enum.class
の場合は、getClass()
と同じ。そうでなければ親クラスを返しています。
Enum 定数値のクラスを取得するという、多くの場合の(一般的な)意図に即するには、Enum のクラスは getDeclaringClass()
で取得するのが望ましいでしょう。
逆に言えば、Enum については getClass()
を使うべきではない と覚えておいた方が良いです。
Enum 定数の前方参照定義
先の例では、次の季節を得るために、以下の実装を考えました。
public enum Season { SPRING { Season next() { return SUMMER; } }, SUMMER { Season next() { return AUTUMN; } }, AUTUMN { Season next() { return WINTER; } }, WINTER { Season next() { return SPRING; } }, ; abstract Season next(); }
匿名内部クラスを利用しなければ、前述の getClass()
の問題は発生しません。
ですので、以下のように変更すればどうでしょうか?
public enum Season { SPRING(SUMMER), SUMMER(AUTUMN), AUTUMN(WINTER), WINTER(SPRING), ; private final Season next; Season(Season next) { this.next = next; } }
しかし、これは前方参照エラーとなりコンパイルできません。 SPRING の定義には(この時点で未定義の)SUMMER が必要となるためです。
ですので匿名内部クラス定義に流れる場合が多いのですが、以下の実装の方が望ましいでしょう。
public enum Season { SPRING, SUMMER, AUTUMN, WINTER, ; public Season next() { return switch(this) { case SPRING -> SUMMER; case SUMMER -> AUTUMN; case AUTUMN -> WINTER; case WINTER -> SPRING; }; } }
Java14 からの switch 式を使っています。Enum 値を追加した場合でも、コンパイラが switch式の網羅性を検証し、漏れがあった場合にはコンパイルエラーになるため安心です。
Enum を匿名内部クラスで定義するのは一見スマートに見えますが、意図の集約という観点からも、上記実装の方がより良いと言えるのではないでしょうか。
クラスの同一性について
ここからは全くのおまけです。
先に示した例では、以下のようにクラスを ==
で比較しました。
Season.SPRING.getClass() == Season.SPRING.getDeclaringClass()
equals()
を使えば以下のようになります。
Season.SPRING.getClass().equals(Season.SPRING.getDeclaringClass())
どちらを使うべきでしょうか?
Class は 以下のように final 宣言されており、 equals() の実装は Object.equals()
で行われます。
public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement { }
public class Object { public boolean equals(Object obj) { return (this == obj); } }
Object.equals()
は、==
で同一性の比較をしているため、クラスの比較は、==
でも equals()
でも同じになります。
クラスオブジェクトは、同一のクラスローダー上では同じ唯一のインスタンスとなりますが、異なるクラスローダー上では異なるインスタンスとなります。
クラスローダーが分かれた場合には、それらは区別され、同一とはみなされません。 同じクラスの異なるバージョンかもしれませんし、それらがたまたま同じものであっても互換性はありません。
ですので、Class インスタンスは、==
で同一性を判定しておけば通常は問題になりません。