読者です 読者をやめる 読者になる 読者になる

Javaの列挙型(Enum)へ新しい要素を追加

Java

Java の列挙型(Enum)へ新しい要素(識別子)を動的に追加する方法を探ってみました。

列挙型の場合、普通のリフレクションクラスではインスタンス化できませんので、下記のように sun パッケージのクラスを使用する必要があります。

  • (1) sun.reflect.ConstructorAccessor で列挙型の新しい要素をインスタンス
  • (2) sun.misc.Unsafe で列挙型の $VALUES フィールドへ (1) のインスタンスを追加

そのため、下記の環境では動作確認できましたが、他の Java 実行環境では使えないかもしれません。

  • Java SE 7 (1.7.0_51-b13)
  • Java SE 8 (1.8.0-b129)

ソースは http://github.com/fits/try_samples/tree/master/blog/20140309/

はじめに

今回は下記のような列挙型へ Second と Third の要素を追加する事にします。

enum EType {
    First
}

Constructor の newInstance では列挙型をインスタンス化できない

下記のように ConstructornewInstance メソッドで列挙型(今回の 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