Javaの列挙型(Enum)へ新しい要素を追加
Java の列挙型(Enum)へ新しい要素(識別子)を動的に追加する方法を探ってみました。
列挙型の場合、普通のリフレクションクラスではインスタンス化できませんので、下記のように sun パッケージのクラスを使用する必要があります。
- (1) sun.reflect.ConstructorAccessor で列挙型の新しい要素をインスタンス化
- (2) sun.misc.Unsafe で列挙型の $VALUES フィールドへ (1) のインスタンスを追加
そのため、下記の環境では動作確認できましたが、他の Java 実行環境では使えないかもしれません。
ソースは http://github.com/fits/try_samples/tree/master/blog/20140309/
はじめに
今回は下記のような列挙型へ Second と Third の要素を追加する事にします。
enum EType {
First
}
Constructor の newInstance では列挙型をインスタンス化できない
下記のように Constructor
の newInstance
メソッドで列挙型(今回の EType)の新しい要素をインスタンス化したいところなのですが、IllegalArgumentException
エラーとなります。
EnumAddValueError.java
import java.lang.reflect.Constructor; public class EnumAddValueError { public static void main(String... args) throws Exception { // EType のコンストラクタ(private)取得 Constructor<EType> cls = EType.class.getDeclaredConstructor(String.class, int.class); // private コンストラクタを実行するための設定 cls.setAccessible(true); // 列挙型は newInstance できないのでエラー EType t2 = cls.newInstance("Second", 1); } enum EType { First } }
実行例
> java EnumAddValueError Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:521) at EnumAddValueError.main(EnumAddValueError.java:11)
これは newInstance メソッドの実装内容が原因です。
java.lang.reflect.Constructor のソース(一部抜粋)
public final class Constructor<T> extends Executable { ・・・ @CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { ・・・ if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; } ・・・ }
ここで、sun.reflect.ConstructorAccessor
を直接使えば IllegalArgumentException を避けてインスタンス化できる事も分かります。
実装
それでは、列挙型へ新しい要素を追加する処理を実装してみます。
(1) sun.reflect.ConstructorAccessor で列挙型の新しい要素をインスタンス化
まずは、列挙型(下記の EType)の新しいインスタンスを作成する必要がありますが、前述したように sun.reflect.ConstructorAccessor
を使う必要があります。
ここで、どのようにして列挙型の ConstructorAccessor を入手するかが課題となりますが、今回は Constructor の acquireConstructorAccessor
メソッドを使って取得してみました。
acquireConstructorAccessor は private メソッドなのでリフレクションを使って実行します。
列挙型の ConstructorAccessor が手に入れば、newInstance メソッドを実行して列挙型の新しいインスタンスを得る事ができます。
EnumAddValue.java (列挙型のインスタンス化)
・・・ import sun.reflect.ConstructorAccessor; public class EnumAddValue { public static void main(String... args) throws Exception { EType t2 = addEnumValue(EType.class, "Second", 1); ・・・ } // (1) sun.reflect.ConstructorAccessor で列挙型の新しい要素をインスタンス化 private static <T extends Enum<?>> T addEnumValue(Class<T> enumClass, String name, int ordinal) throws Exception { // acquireConstructorAccessor メソッド Method m = Constructor.class.getDeclaredMethod("acquireConstructorAccessor"); m.setAccessible(true); // 列挙型のコンストラクタ取得 Constructor<T> cls = enumClass.getDeclaredConstructor(String.class, int.class); // acquireConstructorAccessor を実行し ConstructorAccessor を取得 ConstructorAccessor ca = (ConstructorAccessor)m.invoke(cls); // 列挙型の新しい要素をインスタンス化 @SuppressWarnings("unchecked") T result = (T)ca.newInstance(new Object[]{name, ordinal}); // (2) sun.misc.Unsafe で列挙型の $VALUES フィールドへ (1) のインスタンスを追加 addValueToEnum(result); return result; } ・・・ enum EType { First } }
(2) sun.misc.Unsafe で列挙型の $VALUES フィールドへ (1) のインスタンスを追加
(1) で列挙型の新しい要素をインスタンス化できるようになりましたが、これだけでは不十分です。
valueOf
メソッド等を使えるようにするには、列挙型の $VALUES
クラスフィールド(private static final)へ新しいインスタンスを追加する必要があります。
private final なクラスフィールドの内容を強引に変更するには sun.misc.Unsafe
クラスを使用する事になります。
ここで、Unsafe のインスタンスを Unsafe.getUnsafe()
で取得したいところですが、今回のやり方だと SecurityException
エラーとなってしまいます。
SecurityException を回避するのは面倒そうだったので、今回はリフレクションを使って Unsafe の theUnsafe
クラスフィールド(private static final)から Unsafe インスタンスを取得してみました。
Unsafe のインスタンスを得られれば putObjectVolatile
メソッド等で private final なクラスフィールドの内容を変更できます。
EnumAddValue.java ($VALUES への要素追加)
・・・ // (2) sun.misc.Unsafe で列挙型の $VALUES フィールドへ (1) のインスタンスを追加 private static <T extends Enum<?>> void addValueToEnum(T newValue) throws Exception { // $VALUES フィールド取得 Field f = newValue.getClass().getDeclaredField("$VALUES"); f.setAccessible(true); // $VALUES の値を取得 @SuppressWarnings("unchecked") T[] values = (T[])f.get(null); T[] newValues = Arrays.copyOf(values, values.length + 1); // 列挙型の新しい要素を追加 newValues[values.length] = newValue; // theUnsafe フィールド Field uf = Unsafe.class.getDeclaredField("theUnsafe"); uf.setAccessible(true); // theUnsafe フィールドから Unsafe インスタンスを取得 Unsafe unsafe = (Unsafe)uf.get(null); // $VALUES フィールドへ値を設定 unsafe.putObjectVolatile(unsafe.staticFieldBase(f), unsafe.staticFieldOffset(f), newValues); } ・・・
実行
今回作成したソースの全容は下記の通りです。
EnumAddValue.java
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; import sun.misc.Unsafe; import sun.reflect.ConstructorAccessor; public class EnumAddValue { public static void main(String... args) throws Exception { EType t2 = addEnumValue(EType.class, "Second", 1); System.out.println(t2); EType t3 = addEnumValue(EType.class, "Third", 2); System.out.println(EType.valueOf("Third")); System.out.println("Thrid == t3 : " + (EType.valueOf("Third") == t3)); System.out.println("-----"); for (EType type : EType.values()) { System.out.println(type); } } // (1) sun.reflect.ConstructorAccessor で列挙型の新しい要素をインスタンス化 private static <T extends Enum<?>> T addEnumValue(Class<T> enumClass, String name, int ordinal) throws Exception { Method m = Constructor.class.getDeclaredMethod("acquireConstructorAccessor"); m.setAccessible(true); Constructor<T> cls = enumClass.getDeclaredConstructor(String.class, int.class); ConstructorAccessor ca = (ConstructorAccessor)m.invoke(cls); @SuppressWarnings("unchecked") T result = (T)ca.newInstance(new Object[]{name, ordinal}); addValueToEnum(result); return result; } // (2) sun.misc.Unsafe で列挙型の $VALUES フィールドへ (1) のインスタンスを追加 private static <T extends Enum<?>> void addValueToEnum(T newValue) throws Exception { Field f = newValue.getClass().getDeclaredField("$VALUES"); f.setAccessible(true); @SuppressWarnings("unchecked") T[] values = (T[])f.get(null); T[] newValues = Arrays.copyOf(values, values.length + 1); newValues[values.length] = newValue; Field uf = Unsafe.class.getDeclaredField("theUnsafe"); uf.setAccessible(true); Unsafe unsafe = (Unsafe)uf.get(null); unsafe.putObjectVolatile(unsafe.staticFieldBase(f), unsafe.staticFieldOffset(f), newValues); } enum EType { First } }
ビルドすると ConstructorAccessor・Unsafe を使っている事に対する警告が出ます。
ビルド
> javac EnumAddValue.java EnumAddValue.java:7: 警告: Unsafeは内部所有のAPIであり、今後のリリースで削除される可能性があります import sun.misc.Unsafe; ・・・ 警告7個
実行してみると、一応 Second と Third を追加できている事を確認できました。
実行
> java EnumAddValue Second Third Thrid == t3 : true ----- First Second Third