【Kotlin】value classをJava Reflectionのみでインスタンス化する

# TL;DR
- `value class`はインスタンス化のために幾つかの`static`メソッドを生成する
- 生成されたメソッドを`constructor-impl` -> `box-impl`の順で呼び出すことで、`value class`を`Java Reflection`のみでインスタンス化できる
- 付与したアノテーションは`impl`メソッドにも付与される

# やること
以下のように、`init`ブロックに入力チェックが有るような`value class`について、`Class<Value>`を取得できている状況から、`Java Reflection`のみで、入力チェックを機能させた上でインスタンス化する方法を紹介します[^with-reflect]。

```kotlin
@JvmInline
value class Value(val value: Int) {
    init { if (value == 1) throw TestErr }
}

object TestErr : RuntimeException("試験用エラー")
```


[^with-reflect]: なお、`kotlin-reflect`有りの場合、`KClass.constructors`から呼び出し対象となるコンストラクタを取得して呼び出せば同様のことができます。

# やり方
## デコンパイルしてみる
先ほど紹介した`value class`をデコンパイルし、インスタンス化に関わる部分を抽出・整形したものが以下です。
少なくとも、通常のクラスのように「単に`Java Constructor`を呼び出せば入力チェック・インスタンス化が行える」ようなものでないことが分かります。

```java:Valueのデコンパイル結果（抽出・整形済み）
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`の順で呼び出すことで、入力チェックとインスタンス化の両方を行えることを読み取れます。
これを実際にやってみたコードが以下です[^by-java]。

[^by-java]: `Java`で記述しているのは、`Kotlin`で書いてしまうと`value class`周りの最適化が絡んでしまってややこしいためです。

少し面倒なのは、`try`した際には`TestErr`ではなく`InvocationTargetException`が`catch`される（`TestErr`は`cause`に入っている）点でしょうか。

```java
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`アノテーションが付与されたものを呼び出し対象とする場面を仮定します。

```kotlin
@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`関数について抽出・整形を行った結果が以下です。

```java: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`に付与されているため、これを目印にメソッドの抽出が可能なことが分かります。

## 抽出・呼び出しをしてみる
実際に抽出と呼び出しを行ったコードは以下の通りです。

```java
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!
```