KotlinでのSAM変換
1. 概要
ラムダ式を使用すると、Kotlinでより簡潔でエレガントな方法で動作を表現できます。
このチュートリアルでは、Kotlinラムダ式がSAM変換を使用してJava関数インターフェースと相互運用できる方法を確認します。 その過程で、バイトコードレベルでのこの相互運用性の内部表現をさらに深く掘り下げていきます。
2. 機能インターフェイス
Javaでは、ラムダ式は機能インターフェースの観点から実装されています。 機能インターフェイスには、実装する抽象メソッドが1つだけあります。
一方、Kotlinでは、コンパイル時に適切な関数タイプがあります。 たとえば、(String)-> Int は、 String を入力として受け取り、Intを出力として返す関数です。
実装の違いにもかかわらず、KotlinラムダはJavaの機能インターフェースと完全に相互運用可能です。
さらに具体的に言うと、java.lang.Threadクラスについて考えてみましょう。 このクラスには、Runnableインターフェイスインスタンスを入力として受け取るコンストラクターがあります。
public Thread(Runnable target) {
// omitted
}
Java 8より前は、 Runnable から匿名内部クラスを作成して、スレッドインスタンスを作成する必要がありました。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// the logic
}
});
ここでは、サブクラスとインスタンスの両方を同時に作成しています。
Kotlinでも同様に、オブジェクト式を使用して同じことを実現できます。
val thread = Thread(object : Runnable {
override fun run() {
// the logic
}
})
しかし、それは完全に正しく感じられず、エレガントで簡潔にはほど遠いです。
幸いなことに、Kotlinのラムダ式はJavaの機能インターフェイスと相互運用可能であるため、ラムダをスレッドコンストラクターに渡すことができます:
val thread = Thread({
// the logic
})
これは明らかにはるかに快適なAPIです。 最後のパラメーターはラムダであるため、括弧を省略してラムダブロックをその外に移動できます。
val thread = Thread {
// the logic
}
簡単に言えば、Java APIは機能的なインターフェイスを期待していますが、対応するラムダを渡すことができます。 内部的には、Kotlinコンパイラはラムダを機能的なインターフェイスに変換します。
この可能性についてわかったので、さらに深く掘り下げて、内部で物事がどのように機能しているかを見てみましょう。
3. SAM変換
機能インターフェイスには抽象メソッドが1つしかないため、ラムダから機能インターフェイスへの変換が機能します。 このようなインターフェースは、シングル抽象メソッドまたはSAMインターフェースと呼ばれます。 また、この自動変換はSAM変換とも呼ばれます。
ただし、内部的には、KotlinコンパイラはSAM変換用の匿名内部クラスを作成します。 たとえば、同じスレッドの例を考えてみましょう。
val thread = Thread {
// the logic
}
kotlinc を使用してコードをコンパイルする場合:
$ kotlinc SamConversions.kt
次に、javapを使用してバイトコードを検査できます。
$ javap -v -p -c SamConversionsKt
// truncated
0: new #11 // class java/lang/Thread
3: dup
4: getstatic #17 // Field SamConversionsKt$main$thread$1.INSTANCE:LSamConversionsKt$main$thread$1;
7: checkcast #19 // class java/lang/Runnable
10: invokespecial #23 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
13: astore_0
// truncated
InnerClasses:
static final #13; // class SamConversionsKt$main$thread$1
Kotlinコンパイラが生成したものは次のとおりです。
- 匿名の内部クラスを定義します— SamConversionsKt $ main $ thread $ 1 part
- そのクラスのシングルトンインスタンスを取得します— SamConversionsKt $ main $ thread $ 1.INSTANCE part
- 次に、それが実行可能インスタンスであることを確認し、その後
- そのインスタンスをスレッドコンストラクターに渡します
3.1. オブジェクト式
前述したように、オブジェクト式を使用して同じことを実現できます。
Thread(object : Runnable {
override fun run() {
// the logic
}
})
ただし、バイトコードが以下に示すように、これにより、スレッドインスタンスを作成するたびに匿名の内部クラスが作成されます。
0: new #11 // class java/lang/Thread
3: dup
4: new #13 // class SamConversionsKt$main$thread$1
7: dup
8: invokespecial #16 // Method SamConversionsKt$main$thread$1."<init>":()V
11: checkcast #18 // class java/lang/Runnable
14: invokespecial #21 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
17: astore_0
インデックス8で、JVMは匿名内部クラスのインスタンスを作成し、そのインスタンス初期化メソッド、つまりコンストラクターを呼び出しています。
一方、ラムダはシングルトンインスタンスを作成し、必要になるたびにそのインスタンスを再利用します。
4: getstatic #17 // Field SamConversionsKt$main$thread$1.INSTANCE:LSamConversionsKt$main$thread$1;
したがって、より簡潔であることに加えて、ラムダ式とSAM変換により、作成されるオブジェクトが少なくなるため、メモリフットプリントが向上します。
3.2. クロージャ
ただし、外部スコープから変数をキャプチャすると、このパフォーマンスの向上は脆弱になる可能性があります。
var answer = 42
val thread = Thread {
println(answer)
}
その場合、重要な変更があります。
23: invokespecial #25 // Method SamConversionsKt$main$thread$1."<init>":(Lkotlin/jvm/internal/Ref$IntRef;)V
26: checkcast #27 // class java/lang/Runnable
上に示したように、匿名内部クラスはコンストラクターで1つの引数を受け入れるようになりました。 実際のところ、Kotlinコンパイラは、キャプチャされた変数を IntRef インスタンス内にラップし、それを内部クラスコンストラクタに渡します。
したがって、ラムダ内の変数をキャプチャすると、JVMは呼び出しごとに1つのインスタンスを作成します。 したがって、そのパフォーマンスの向上は失われます。 これを回避するために、Kotlinでインライン関数を使用できます。
4. SAMコンストラクター
通常、ラムダ式から対応する機能インターフェースへの変換は、コンパイラーによって自動的に行われます。 ただし、この変換を手動で実行する必要がある場合があります。
たとえば、 ExecutorService インターフェイスは、 submit()の2つのオーバーロードされたバージョンを提供します。
Future<T> submit(Callable task);
Future<?> submit(Runnable task);
呼び出し可能と実行可能はどちらも機能インターフェースです。 したがって、次のように記述します。
val result = executor.submit {
return@submit 42
}
その場合、Kotlinコンパイラは、使用しているオーバーロードされたバージョンを推測できません。 この混乱を解決するために、SAMコンストラクターを使用できます:
val submit = executor.submit(Callable {
return@Callable 42
})
上に示したように、SAMコンストラクターの名前は、基礎となる機能インターフェースの名前と同じです。 また、コンストラクター自体は、コンパイラーによって生成された特別な関数であり、ラムダを機能インターフェイスのインスタンスに明示的に変換できます。
さらに、SAMコンストラクターは、機能インターフェイスを返すときにも役立ちます。
fun doSomething(): Runnable = Runnable {
// doing something
}
または、ラムダを変数に格納することもできます。
val runnable = Runnable {
// doing something
}
SAM変換は、抽象クラスに抽象メソッドが1つしかない場合でも、機能インターフェイスでのみ機能し、抽象クラスでは機能しないことに注意してください。
4.1. KotlinインターフェースのSAM変換
Kotlin 1.4 以降、KotlinインターフェースにもSAM変換を使用できます。 Kotlinインターフェースをfun修飾子でマークするだけです。
fun interface Predicate<T> {
fun accept(element: T): Boolean
}
これで、このインターフェイスのインスタンスにSAM変換を適用できます。
val isAnswer = Predicate<Int> { i -> i == 42 }
fun 修飾子をKotlinインターフェースに適用する場合、インターフェースに単一の抽象メソッドが含まれていることを確認する必要があることに注意してください。 そうしないと、コードはコンパイルされません。
fun interface NotSam {
// no abstract methods
}
上記のコードはコンパイル時に失敗し、エラーメッセージが表示されます。
Fun interfaces must have exactly one abstract method
5. 結論
この記事では、SAMインターフェイスについて説明しました。 また、Kotlinがラムダ式をJavaの機能インターフェイスに変換する方法も確認しました。 これにより、関数インターフェイスが期待される場所にラムダ式を渡すことができました。
最後に、SAMコンストラクターを介してこの変換を明示的に行うようにコンパイラーに指示する必要がある場合があることを学びました。
いつものように、すべての例はGitHubでから入手できます。