marp |
---|
@wrongwrong
- wrongwrong
- Qiita: @wrongwrong
- GitHub: k163377
- 最近やっていること
jackson-module-kotlin
へのコントリビュート
Java Reflection
から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> {
/* 略 */
// 素直に書くと、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)
- パフォーマンスの低下を抑えつつ値をラップできる
@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);
}
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);
}
ハイパフォーマンス!ハッピー!
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);
}
ハイパフォーマンス!ハッピー!……じゃないこともある
- メソッド名に何やら接尾辞が付いている
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
から扱うのが大変!
@JvmInline value class FooId(val value: Int)
data class Dto(val fooId: FooId)
// expected
{ "fooId" : 1 }
// jackson-2.12.0でのシリアライズ結果
// プロパティ名が何かおかしい!(※現在は解決済み)
{ "fooId-gdWu5YM" : 1 }
getter
の名前が変化しているから
/* デコンパイル結果(抜粋・整形済み) */
public final class Dto {
// getterの名前が書き変わっている!
public final int getFooId-gdWu5YM() { return this.fooId; }
}
-> 無理やり整形するかKotlin
上のプロパティ情報を探しに行くことになる!
/* デコンパイル結果(抜粋・整形済み) */
public final int getFooId-gdWu5YM() { return this.fooId; }
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
以外にもいくつかパターンが有る
パターンを全て把握してテストが必要!
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
でのコンストラクタ/メソッド呼び出し時に困る!
@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;
}
}
ここで紹介してない辛さもまだ有るよ!
Kotlin value class
はスマートで便利な機能- 一方、
Java Reflection
からvalue class
を処理する時には多くの辛さが有る - 発生する問題への対処は、
Java/Kotlin
両方のコンパイル結果とリフレクションを理解しながら地道にやっていくしかない- 自分も
jackson-module-kotlin
のvalue class
対応頑張ります- ので、応援してください
間違ってもRedditなんかでボロクソ言ったりしちゃいけないよ!
- 自分も
Java Project Valhalla
にて、Kotlin value class
のようなものがJava
側に導入される予定なため- 現状では
primitive classes
という名称になっている
- 現状では
- これが導入された際に互換性が崩れないよう、独自のマングリングロジックなどが導入された
@JvmInline
アノテーションはそれを示すための目印でもある
- 詳しくはKEEPにまとめられている