Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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 { /* 略 */ 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

注目して頂きたい点 メソッド名に何やら接尾辞が付いている 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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Q. どうやって対処していくの? A. スマートな方法はあまり有りません。頑張って地道に一つ一つ 対処していくしかないです。 ここで紹介してない辛さもまだ有るよ! 16

Slide 17

Slide 17 text

まとめ Kotlin value class はスマートで便利な機能 一方、 Java Reflection から value class を処理する時には多くの辛さが有る 発生する問題への対処は、 Java/Kotlin 両方のコンパイル結果とリフレクション を理解しながら地道にやっていくしかない 自分も jackson-module-kotlin の value class 対応頑張ります ので、応援してください 間違ってもRedditなんかでボロクソ言ったりしちゃいけないよ! 17

Slide 18

Slide 18 text

参考資料 Inline classes | Kotlin KEEP/value-classes.md at master · Kotlin/KEEP 18

Slide 19

Slide 19 text

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