Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Java Reflectionから見たvalue class

Java Reflectionから見たvalue class

Java ReflectionベースのライブラリからKotlin value classを取り扱う上での辛さの話。

スライドのソース: https://gist.github.com/k163377/f58caed5a2c756a417ae28603ecfd6fb

0802fdda25fd4a67fd3f2a6189755763?s=128

wrongwrong

July 21, 2021
Tweet

Transcript

  1. Java Reflection から見た value class value class 対応の辛さの一端 @wrongwrong 1

  2. 自己紹介 wrongwrong Qiita: @wrongwrong GitHub: k163377 最近やっていること jackson-module-kotlin へのコントリビュート 2

  3. 今日話すこと Java Reflection から value class を処理するのが辛い話 あるいは、対応するのが結構大変だけど頑張ってる話 3

  4. value class とは Kotlin 1.5 で正式化された機能 幾つか制約の有る、高パフォーマンスなラッパークラスを定義できる inline class ->

    value class と名前が変更された // JVM向けの場合JvmInlineアノテーションを付ける必要が有る @JvmInline value class FooId(val value: Int) Unsigned Integers も value class として定義されている @JvmInline public value class UInt @PublishedApi internal constructor(@PublishedApi internal val data: Int) : Comparable<UInt> { /* 略 */ 4
  5. value class とは 従来の課題 // 素直に書くと、f1(barId, fooId, bazId)というように書けてしまう fun f1(fooId:

    Int, barId: Int, bazId: Int) // IDごとに値をラップする型を付けると、引数の設定ミスは発生しない fun f2(fooId: FooId, barId: BarId, bazId: BazId) // ただし、値をラップする分パフォーマンスは低下する data class FooId(val value: Int) value class を使う嬉しさ パフォーマンスの低下を抑えつつ値をラップできる 5
  6. value class は何故高パフォーマンスなのか @JvmInline value class FooId(val value: Int) {

    fun asString() = value.toString() } data class BarId(val value: Int) { fun asString() = value.toString() } fun f1(fooId: FooId, barId: BarId) { f2(fooId, barId) } fun f2(fooId: FooId, barId: BarId) { println("${fooId.asString()}, ${barId.asString()}") } /* f1, f2のデコンパイル結果(抜粋・整形済み) */ public static final void f1-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); f2-Zzqckw8(fooId, barId); } public static final void f2-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); String var2 = FooId.asString-impl(fooId) + ", " + barId.asString(); boolean var3 = false; System.out.println(var2); } 6
  7. value class は何故高パフォーマンスなのか fooId は引数上 unbox された型(= int )になっている f1

    内では完全に unbox された型として振る舞っている primitive 型になる場合、 null チェックが消える他、 JVM による最適化も効く /* f1, f2のデコンパイル結果(抜粋・整形済み) */ public static final void f1-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); f2-Zzqckw8(fooId, barId); } public static final void f2-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); String var2 = FooId.asString-impl(fooId) + ", " + barId.asString(); boolean var3 = false; System.out.println(var2); } ハイパフォーマンス!ハッピー! 7
  8. value class は何故高パフォーマンスなのか fooId は引数上 unbox された型(= int )になっている f1

    内では完全に unbox された型として振る舞っている primitive 型になる場合、 null チェックが消える他、 JVM による最適化も効く /* f1, f2のデコンパイル結果(抜粋・整形済み) */ public static final void f1-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); f2-Zzqckw8(fooId, barId); } public static final void f2-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); String var2 = FooId.asString-impl(fooId) + ", " + barId.asString(); boolean var3 = false; System.out.println(var2); } ハイパフォーマンス!ハッピー!……じゃないこともある 8
  9. 注目して頂きたい点 メソッド名に何やら接尾辞が付いている Kotlin 上の定義とデコンパイル結果の引数の型が違う インスタンス関数の呼び出しが static 関数の呼び出しに置き換わっている /* デコンパイル結果(抜粋・整形済み) */

    public static final void f2-Zzqckw8(int fooId, @NotNull BarId barId) { Intrinsics.checkNotNullParameter(barId, "barId"); String var2 = FooId.asString-impl(fooId) + ", " + barId.asString(); boolean var3 = false; System.out.println(var2); } Java Reflection から扱うのが大変! 9
  10. value class を Java Reflection で扱う時の辛さ 例えば JSON へのシリアライズ時…… @JvmInline

    value class FooId(val value: Int) data class Dto(val fooId: FooId) // expected { "fooId" : 1 } // jackson-2.12.0でのシリアライズ結果 // プロパティ名が何かおかしい!(※現在は解決済み) { "fooId-gdWu5YM" : 1 } 10
  11. value class を Java Reflection で扱う時の辛さ 何故こうなるのか getter の名前が変化しているから /*

    デコンパイル結果(抜粋・整形済み) */ public final class Dto { // getterの名前が書き変わっている! public final int getFooId-gdWu5YM() { return this.fooId; } } -> 無理やり整形するか Kotlin 上のプロパティ情報を探しに行くことになる! 11
  12. value class を Java Reflection で扱う時の辛さ getter から Kotlin 上の情報を捕捉するのが難しい

    /* デコンパイル結果(抜粋・整形済み) */ public final int getFooId-gdWu5YM() { return this.fooId; } getterの戻り値は型が変わっているため、元の型の情報が捕捉し難い -> unbox されるパターンでは、型情報に基づく処理を適用できない場合が有る! 12
  13. value class を Java Reflection で扱う時の辛さ getter で unbox されるパターンとされないパターンが有る

    data class Dto(val fooIds: List<FooId>) // expected { "fooIds" : [ 2 ] } // jackson-2.12.0でのシリアライズ結果 { "fooIds" : [ {"value":2} ] } // 値がunboxされていない!(※2.13で解決予定) /* デコンパイル結果(抜粋・整形済み) */ // getterがList<Integer>にはなっていない! @NotNull public final List<FooId> getFooIds() { return this.fooIds; } ※実際には Collection 以外にもいくつかパターンが有る パターンを全て把握してテストが必要! 13
  14. value class を Java Reflection で扱う時の辛さ 引数が変化し、コンストラクタも特殊な形になる data class Dto(val

    fooId: FooId) /* デコンパイル結果(抜粋・整形済み) */ // コンストラクタが2つ生成されており、引数もprimitive型化している private Dto(int fooId) { this.fooId = fooId; } public Dto(int fooId, DefaultConstructorMarker $constructor_marker) { this(fooId); } Java Reflection でのコンストラクタ/メソッド呼び出し時に困る! 14
  15. value class を Java Reflection で扱う時の辛さ インスタンスメソッドが static メソッドにコンパイルされるため、従来の Java

    インスタンスメソッド向けのリフレクション処理が機能しない @JvmInline value class FooId(val value: Int) { @JsonValue fun getJsonValue() = "FooId$value" } /* デコンパイル結果(抜粋・整形済み) */ public final class FooId { // JsonValueは付与されているが、実際にはstaticメソッドになっているため機能しない! @JsonValue @NotNull public static final String getJsonValue-impl(int $this) { return "FooId" + $this; } } 15
  16. Q. どうやって対処していくの? A. スマートな方法はあまり有りません。頑張って地道に一つ一つ 対処していくしかないです。 ここで紹介してない辛さもまだ有るよ! 16

  17. まとめ Kotlin value class はスマートで便利な機能 一方、 Java Reflection から value

    class を処理する時には多くの辛さが有る 発生する問題への対処は、 Java/Kotlin 両方のコンパイル結果とリフレクション を理解しながら地道にやっていくしかない 自分も jackson-module-kotlin の value class 対応頑張ります ので、応援してください 間違ってもRedditなんかでボロクソ言ったりしちゃいけないよ! 17
  18. 参考資料 Inline classes | Kotlin KEEP/value-classes.md at master · Kotlin/KEEP

    18
  19. おまけ: 何故 Kotlin value class はこの仕組みになったか Java Project Valhalla にて、

    Kotlin value class のようなものが Java 側に導入 される予定なため 現状では primitive classes という名称になっている これが導入された際に互換性が崩れないよう、独自のマングリングロジックなど が導入された @JvmInline アノテーションはそれを示すための目印でもある 詳しくはKEEPにまとめられている 19