Java 20 で java.lang.Void をインスタンス化する
はじめに
次の記事を見て自分でもやってみようと思い立ちました。ありがとうございます。
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.
インスタンス化する
インスタンス化は前述のとおりできないようになっているのですが、リフレクションを使えば可能です。
java --add-opens java.base/java.lang=ALL-UNNAMED 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
以外持たずインスタンス化もできないシンプルなクラスです。
コンストラクタは private
なので、インスタンス化するにはコンストラクタを取得、アクセス可能にして、インスタンスを作成する必要があります。
var constructor = Void.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
Class<T>::getDeclaredConstructor
Class<T>::getDeclaredConstructor
は Class<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 isConstructor<?>[]
and notConstructor<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 ofConstructor<T>[]
.
このメソッドは
Constructor<T>
オブジェクトの配列(このクラスのコンストラクタの配列)を返しますが、このメソッドの戻り値はConstructor<?>[]
であり、期待されるようなConstructor<T>[]
ではありません。このメソッドから返された後、配列は異なるクラスのコンストラクタオブジェクトを保持するように変更される可能性があり、Constructor<T>[]
の型保証に違反するため、この情報量の少ない戻り値の型が必要です。
とある通り、返された配列に他のコンストラクタの入る可能性があり、入ったときに Constructor<T>[]
だと型が合わなくなるのでダメらしいです… ちょっと厳しすぎでは。
おわりに
元の記事を少し簡略化しただけですが、モジュールシステムによって内部APIへのアクセスが厳しくなっていて大変でした。
いや普通にコード書く分には影響ないはずなのでむしろ良いことなはずなんですが。
Discussion