🕳️

Java 20 で java.lang.Void をインスタンス化する

2023/05/14に公開

はじめに

次の記事を見て自分でもやってみようと思い立ちました。ありがとうございます。

https://qiita.com/momosetkn/items/57e814088026dbd4480c

Void クラスとは

void を表す Class オブジェクト、Void.TYPE を持った一般にはインスタンス化できないクラスです。
Integer.TYPE と似た感じですね。

The Void class is an uninstantiable placeholder class to hold a reference to the Class object representing the Java keyword void.

https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/Void.html

インスタンス化する

インスタンス化は前述のとおりできないようになっているのですが、リフレクションを使えば可能です。

java --add-opens java.base/java.lang=ALL-UNNAMED Main.java
Main.java
package io.github.risu729.test;

import java.lang.reflect.InvocationTargetException;

class Main {

    public static void main(String[] args) throws Exception {
        System.out.println(getVoid());
        // java.lang.Void@23a5fd2
    }

    private static Void getVoid() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        var constructor = Void.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        return constructor.newInstance();
    }
}

この取得したインスタンスは何か特別なわけではなく、Object のインスタンスとほとんど変わりません。
使われないので当たり前ですが、一切オーバーライドがなされていないので。

java コマンドの解説

java Main.java について

Java 11の JEP 330: Launch Single-File Source-Code Programs によって、単一ファイルの実行は javac でコンパイルする必要がなくなりました。
java コマンドでjavaファイルを指定すればそのまま実行できます。

JEP draft: Launch Multi-File Source-Code Programs にこれを複数ファイルでも使えるようにする提案も上がっています。

--add-opens について

Java 17の JEP 403: Strongly Encapsulate JDK Internals によって、JDK内部APIへのアクセス制限が強化されました。

上のコードでは module-info.java を用意していないので、Main クラスは無名モジュールとなっています。
--add-opens java.base/java.lang=ALL-UNNAMED とすることで、java.base/java.lang モジュールへのアクセスを ALL-UNNAMED (無名モジュール) に許可します。

java コマンドのドキュメント--add-opens について記載されています。
--add-exports というオプションもありますが、こちらは public なメンバーにのみアクセスできるようにするもので、リフレクションは使うことができません。

これらのオプションはJava 9の JEP 261: Module System から導入されています。

ちなみにこのオプションがないと次のように InaccessibleObjectException が発生します。

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make private java.lang.Void() accessible: module java.base does not "opens java.lang" to unnamed module @78a2da20
        at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:387)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:363)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:311)
        at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:192)
        at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:185)
        at io.github.risu729.test.Main.getVoid(Main.java:13)
        at io.github.risu729.test.Main.main(Main.java:7)

--illegal-access について

Java 9の JEP 261: Module System--illegal-access も追加されていました。
JEP 260: Encapsulate Most Internal APIs でカプセル化された内部APIへアクセスできるようにするオプションです。

permit, warn, debug, deny のいずれかを指定でき、Java 9~15では permit がデフォルトでした。

Java 16では JEP 396: Strongly Encapsulate JDK Internals by Default によって deny がデフォルトになりました。

そして、Java 17の JEP 403: Strongly Encapsulate JDK Internals によって、--illegal-access 自体がサポートされなくなりました。
ちなみに、使用すると次のような警告が表示されるようになります。

Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option --illegal-access=permit; support was removed in 17.0

Main の解説

Void クラスは次のように定義されています。Void.TYPE 以外持たずインスタンス化もできないシンプルなクラスです。

https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Void.java#L26-L49

コンストラクタは private なので、インスタンス化するにはコンストラクタを取得、アクセス可能にして、インスタンスを作成する必要があります。

var constructor = Void.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();

Class<T>::getDeclaredConstructor

Class<T>::getDeclaredConstructorClass<T>::getConstructor と異なり、public でないコンストラクタも取得できます。
このメソッドの引数は、Class<?>... parameterTypes とコンストラクタの引数の配列になっていますが、可変長引数なので何も渡さなければ自動的に空の配列が渡されます。

また、Class<T>::getDeclaredConstructors はすべてのコンストラクタを返すメソッドです。
Class<T>::getConstructors は、 public なコンストラクタのみを返します。
ただし、この2つのメソッドはConstructor<T>[] を返すべきですが、Constructor<?>[] を返します。

While this method returns an array of Constructor<T> objects (that is an array of constructors from this class), the return type of this method is Constructor<?>[] and not Constructor<T>[] as might be expected. This less informative return type is necessary since after being returned from this method, the array could be modified to hold Constructor objects for different classes, which would violate the type guarantees of Constructor<T>[].

このメソッドは Constructor<T> オブジェクトの配列(このクラスのコンストラクタの配列)を返しますが、このメソッドの戻り値は Constructor<?>[] であり、期待されるような Constructor<T>[] ではありません。このメソッドから返された後、配列は異なるクラスのコンストラクタオブジェクトを保持するように変更される可能性があり、Constructor<T>[]の型保証に違反するため、この情報量の少ない戻り値の型が必要です。

とある通り、返された配列に他のコンストラクタの入る可能性があり、入ったときに Constructor<T>[] だと型が合わなくなるのでダメらしいです… ちょっと厳しすぎでは。

おわりに

元の記事を少し簡略化しただけですが、モジュールシステムによって内部APIへのアクセスが厳しくなっていて大変でした。
いや普通にコード書く分には影響ないはずなのでむしろ良いことなはずなんですが。

GitHubで編集を提案

Discussion