TL;DR
-
value class
はインスタンス化のために幾つかのstatic
メソッドを生成する - 生成されたメソッドを
constructor-impl
->box-impl
の順で呼び出すことで、value class
をJava Reflection
のみでインスタンス化できる - 付与したアノテーションは
impl
メソッドにも付与される
やること
以下のように、init
ブロックに入力チェックが有るようなvalue class
について、Class<Value>
を取得できている状況から、Java Reflection
のみで、入力チェックを機能させた上でインスタンス化する方法を紹介します1。
@JvmInline
value class Value(val value: Int) {
init { if (value == 1) throw TestErr }
}
object TestErr : RuntimeException("試験用エラー")
やり方
デコンパイルしてみる
先ほど紹介したvalue class
をデコンパイルし、インスタンス化に関わる部分を抽出・整形したものが以下です。
少なくとも、通常のクラスのように「単にJava Constructor
を呼び出せば入力チェック・インスタンス化が行える」ようなものでないことが分かります。
public final class Value {
private final int value;
private Value(int value) { this.value = value; }
public static int constructor-impl(int value) {
if (value == 1) {
throw (Throwable)TestErr.INSTANCE;
} else {
return value;
}
}
public static final Value box-impl(int v) { return new Value(v); }
}
これらの処理を整理すると、以下のことが読み取れます。
- コンストラクタは公開されておらず、また入力チェックの実体もその中には実装されていない
- 入力チェックの実体は
constructor-impl
に実装されている -
Value
のインスタンスを得られる公開されたメソッドはbox-impl
インスタンス化してみる
先ほど整理した内容から、constructor-impl
-> box-impl
の順で呼び出すことで、入力チェックとインスタンス化の両方を行えることを読み取れます。
これを実際にやってみたコードが以下です2。
少し面倒なのは、try
した際にはTestErr
ではなくInvocationTargetException
がcatch
される(TestErr
はcause
に入っている)点でしょうか。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
Class<Value> clazz = Value.class;
// constructor-impl/box-implの取得
Method constructorImpl = Arrays.stream(clazz.getMethods())
.filter(it -> it.getName().equals("constructor-impl"))
.findFirst()
.orElseThrow(RuntimeException::new);
Method boxImpl =Arrays.stream(clazz.getMethods())
.filter(it -> it.getName().equals("box-impl"))
.findFirst()
.orElseThrow(RuntimeException::new);
// 呼び出し
int checked = (int) constructorImpl.invoke(null, 0);
Value value = (Value) boxImpl.invoke(null, checked);
System.out.println(value.toString());
// constructor-implの入力チェックのテスト、入力チェックが機能していればChecked!と出力される
try {
constructorImpl.invoke(null, 1);
System.out.println("Unchecked");
} catch (InvocationTargetException e) {
if (e.getCause() instanceof TestErr) {
System.out.println("Checked!");
} else {
System.out.println("Unchecked");
}
}
}
}
実行結果は以下のようになります。
問題無くインスタンス化・チェック共に行えていることが分かります。
Value(value=0)
Checked!
コンストラクタが複数/アノテーションが付与されている場合
もう少し複雑な状況として、複数定義されているコンストラクタの内、TargetMarker
アノテーションが付与されたものを呼び出し対象とする場面を仮定します。
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CONSTRUCTOR)
annotation class TargetMarker
@JvmInline
value class Value(val value: Int) {
@TargetMarker constructor(value: String) : this(value.toInt())
init { if (value == 1) throw TestErr }
}
デコンパイルしてみる
コンストラクタを追加したValue
クラスをデコンパイルし、constructor-impl
関数について抽出・整形を行った結果が以下です。
public static int constructor-impl(int value) {
if (value == 1) {
throw (Throwable)TestErr.INSTANCE;
} else {
return value;
}
}
@TargetMarker
public static int constructor-impl(@NotNull String value) {
Intrinsics.checkNotNullParameter(value, "value");
boolean var2 = false;
return constructor-impl(Integer.parseInt(value));
}
TargetMarker
アノテーションはKotlin
上のコンストラクタに対応するconstructor-impl
に付与されているため、これを目印にメソッドの抽出が可能なことが分かります。
抽出・呼び出しをしてみる
実際に抽出と呼び出しを行ったコードは以下の通りです。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
Class<Value> clazz = Value.class;
// TargetMarkerが付与されたconstructor-implの抽出
Method constructorImpl = Arrays.stream(clazz.getMethods())
.filter(it ->
it.getName().equals("constructor-impl") && it.getAnnotation(TargetMarker.class) != null
).findFirst()
.orElseThrow(RuntimeException::new);
// constructor-implの入力チェックのテスト、入力チェックが機能していればChecked!と出力される
try {
constructorImpl.invoke(null, "1");
System.out.println("Unchecked");
} catch (InvocationTargetException e) {
if (e.getCause() instanceof TestErr) {
System.out.println("Checked!");
} else {
System.out.println("Unchecked");
}
}
}
}
実行結果は以下の通りです。
String
を引数に呼び出せており、かつ入力チェックが機能していることが読み取れます。
Checked!