Androidのアプリを作成しているとSAM変換が頻繁に登場します。
プログラムが簡素に記述できることから、プログラマーに好評なようです。
Kotlinが関数を第一級オブジェクトとして扱える恩恵です。
しかし、私はSAM変換に出合うと、いつもプログラムの論理が把握できなくて戸惑います。なので、苦手です。あまり好きではありません。
苦手を克服するために、もっとSAM変換の理解を深めたいと思い、まとめてみました。
目次
SAM変換とは
PlayerクラスにplayCD( )関数を委譲して、CDPlayerオブジェクトを作るプログラムをKotlinで記述すると、次のようになります。
class Player { var playCtrl: ((String)->Unit)? = null ... fun play(title: String) { playCtrl?.invoke(title) // 委譲された関数の実行 } ... fun setPlayCtrl(ctrl: (String)->Unit) { // 関数オブジェクトを引数で受ける playCtrl = ctrl } ... }
fun playCD(title: String) { println("CDの「${title}」を再生する!") } ... val _CDPlayer = Player().apply { setPlayCtrl(::playCD) // 関数オブジェクトを引数で渡す ... } _CDPlayer.play("SampleMusic") ...
「関数オブジェクトを渡し、関数オブジェクトを引数に持つ高階関数で受ける形」になります。
関数オブジェクトは関数の記述そのものを格納しています。「関数がどのクラスに属しているか」といった所在の情報は失われています。
JVM(Java Virtual Machine)はクラスに属さないメソッド(関数)を処理できません。所在の情報を持たない関数オブジェクトのままでは、委譲ができません。
従って、関数オブジェクト内の関数は、内部的に「SAM型のインターフェースを実装したクラスに属するメソッド(関数)」へ変換されます。ここで定義されたクラスが関数オブジェクトを表しています。
※SAM(Single Absolute Method):メソッド(関数)が一つしかない抽象クラス
この変換をSAM変換といいます。
また、「SAM型のインターフェース」のことを関数型インターフェースと言います。
同様に、関数オブジェクトを引数に持つ高階関数も、関数型インターフェースを受けるメソッドに変換されます。
ソースコード上は渡し側も受け側も、引数が同じ関数オブジェクトになっています。なので、プログラム文法上の違和感がなく、内部で行われているSAM変換が認識できないケースです。
多用されるSAM変換
「SAM変換とは」で示したCDPlayerオブジェクトを作るプログラムを、PlayerクラスがJavaで記述された場合で考えてみると、次のようになります。
class Player { private PlayCtrl mPlayCtrl = null; ... public void play(String title) { if(mPlayCtrl != null) mPlayCtrl.play(title); // 委譲された関数の実行 } ... public void setPlayCtrl(PlayCtrl ctrl) { // 関数型インターフェースを引数で受ける mPlayCtrl = ctrl; } interface PlayCtrl { void play(String title); } ... }
fun playCD(title: String) { println("CDで「${title}」を再生する!") } ... val _CDPlayer = Player().apply { setPlayCtrl(::playCD) // 関数オブジェクトを引数で渡す ... } _CDPlayer.play("SampleMusic") ...
「関数オブジェクトを渡し、関数型インターフェースを引数に持つメソッドで受ける形」になります。
ソースコード上は渡し側と受け側の引数が異なっています。なので、プログラム文法上の違和感がありますが、内部で行われるSAM変換の効果で受け渡しができてしまいます。SAM変換が認識できるケースです。
Androidアプリのプログラムは、このケースが多用されます。次のようなインターフェースが使われる場面です。
OnClickListener#onClick(v: View)
OnLongClickListener#onLongClick(v: View)
Runnable#run( )
…
Java APIやAndroid APIがJavaで記述されており、それらライブラリをKotlinから呼出すためです。
SAM変換の例
Kotlinは「プログラムが簡素に書ける」という特徴を持っています。不要な部分を省略できるからです。
特にSAM変換が行われる部分はラムダ式を利用します。ラムダ式は省略の方法が色々あります。知っていないとプログラムを読む時に苦労します。
以下にSAM変換の例を省略の方法と共に示します。
OnClickListener#onClick(v: View)
// ★関数オブジェクトの参照値を受け渡す // 省略できる箇所なし fun onClickB(v: View) { println("${v.id}ボタンが押されました!") } btn.setOnClickListener(::onClickB) // ★無名関数を受け渡す // 省略できる箇所なし btn.setOnClickListener(fun(v: View){ println("${v.id}ボタンが押されました!") }) // ★ラムダ式(無名関数)を受け渡す // 原形 btn.setOnClickListener({ v: View -> println("${v.id}ボタンが押されました!") }) // 引数リストの最後がラムダ式ならカッコの外に出せる btn.setOnClickListener(){ v: View -> println("${v.id}ボタンが押されました!") } // 引数のカッコが空なので削除可能 btn.setOnClickListener{ v: View -> println("${v.id}ボタンが押されました!") } // ラムダ式の引数の型が明確な場合(ここではView)は削除可能 btn.setOnClickListener{ v-> println("${v.id}ボタンが押されました!") } // ラムダ式の引数が一つの場合は削除可能、引数はitで参照 btn.setOnClickListener{ println("${it.id}ボタンが押されました!") }
OnLongClickListener#onLongClick(v: View)
// ★関数オブジェクトの参照値を受け渡す // 省略できる箇所なし fun onLongClickB(v: View): Boolean { println("${v.id}ボタンが押されました!") return true } btn.setOnLongClickListener(::onLongClickB) // ★無名関数を受け渡す // 省略できる箇所なし btn.setOnLongClickListener( fun(v: View): Boolean{ println("${v.id}ボタンが押されました!") return true } ) // ★ラムダ式(無名関数)を受け渡す // 原形 btn.setOnLongClickListener({ v: View -> println("${v.id}ボタンが押されました!");true }) // 引数リストの最後がラムダ式ならカッコの外に出せる btn.setOnLongClickListener() { v: View -> println("${v.id}ボタンが押されました!");true } // 引数のカッコが空なので削除可能 btn.setOnLongClickListener{ v: View -> println("${v.id}ボタンが押されました!");true } // ラムダ式の引数の型が明確な場合(ここではView)は削除可能 btn.setOnLongClickListener{ v -> println("${v.id}ボタンが押されました!");true } // ラムダ式の引数が一つの場合は削除可能、引数はitで参照 btn.setOnLongClickListener{ println("${it.id}ボタンが押されました!");true }
関連記事: