0
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-10-20

TL;DR

  • value classはインスタンス化のために幾つかのstaticメソッドを生成する
  • 生成されたメソッドをconstructor-impl -> box-implの順で呼び出すことで、value classJava 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を呼び出せば入力チェック・インスタンス化が行える」ようなものでないことが分かります。

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の順で呼び出すことで、入力チェックとインスタンス化の両方を行えることを読み取れます。
これを実際にやってみたコードが以下です2

少し面倒なのは、tryした際にはTestErrではなくInvocationTargetExceptioncatchされる(TestErrcauseに入っている)点でしょうか。

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

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!

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

  2. Javaで記述しているのは、Kotlinで書いてしまうとvalue class周りの最適化が絡んでしまってややこしいためです。 

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1