Skip to content

Instantly share code, notes, and snippets.

@k163377
Last active July 22, 2021 04:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save k163377/f58caed5a2c756a417ae28603ecfd6fb to your computer and use it in GitHub Desktop.
Save k163377/f58caed5a2c756a417ae28603ecfd6fb to your computer and use it in GitHub Desktop.
Java Reflectionから見たvalue class
marp

Java Reflectionから見たvalue class

value class対応の辛さの一端

@wrongwrong


自己紹介

  • wrongwrong
  • 最近やっていること
    • jackson-module-kotlinへのコントリビュート

今日話すこと

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

value classとは

  • Kotlin 1.5で正式化された機能
    • 幾つか制約の有る、高パフォーマンスなラッパークラスを定義できる
    • inline class -> value classと名前が変更された
// JVM向けの場合JvmInlineアノテーションを付ける必要が有る
@JvmInline value class FooId(val value: Int)
  • Unsigned Integersvalue classとして定義されている
@JvmInline
public value class UInt @PublishedApi internal constructor(@PublishedApi internal val data: Int) : Comparable<UInt> {
  /**/

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を使う嬉しさ

  • パフォーマンスの低下を抑えつつ値をラップできる

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);
}

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);
}

ハイパフォーマンス!ハッピー!


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);
}

ハイパフォーマンス!ハッピー!……じゃないこともある


注目して頂きたい点

  • メソッド名に何やら接尾辞が付いている
  • 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から扱うのが大変!


value classJava 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 }

value classJava Reflectionで扱う時の辛さ

何故こうなるのか

  • getterの名前が変化しているから
/* デコンパイル結果(抜粋・整形済み) */
public final class Dto {
  // getterの名前が書き変わっている!
  public final int getFooId-gdWu5YM() { return this.fooId; }
}

-> 無理やり整形するかKotlin上のプロパティ情報を探しに行くことになる!


value classJava Reflectionで扱う時の辛さ

getterからKotlin上の情報を捕捉するのが難しい

/* デコンパイル結果(抜粋・整形済み) */
public final int getFooId-gdWu5YM() { return this.fooId; }

getterの戻り値は型が変わっているため、元の型の情報が捕捉し難い -> unboxされるパターンでは、型情報に基づく処理を適用できない場合が有る!


value classJava Reflectionで扱う時の辛さ

getterunboxされるパターンとされないパターンが有る

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以外にもいくつかパターンが有る パターンを全て把握してテストが必要!


value classJava 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でのコンストラクタ/メソッド呼び出し時に困る!


value classJava 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;
   }
}

Q. どうやって対処していくの?

A. スマートな方法はあまり有りません。頑張って地道に一つ一つ対処していくしかないです。

ここで紹介してない辛さもまだ有るよ!


まとめ

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

参考資料


おまけ: 何故Kotlin value classはこの仕組みになったか

  • Java Project Valhallaにて、Kotlin value classのようなものがJava側に導入される予定なため
    • 現状ではprimitive classesという名称になっている
  • これが導入された際に互換性が崩れないよう、独自のマングリングロジックなどが導入された
    • @JvmInlineアノテーションはそれを示すための目印でもある
  • 詳しくはKEEPにまとめられている
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment