Androidのアプリを作成しているとSAM変換が頻繁に登場します。
プログラムが簡素に記述できることから、プログラマーに好評なようです。
Kotlinが関数を第一級オブジェクトとして扱える恩恵です。
しかし、私はSAM変換に出合うと、いつもプログラムの論理が把握できなくて戸惑います。なので、苦手です。あまり好きではありません。
苦手を克服するために、もっとSAM変換の理解を深めたいと思い、まとめてみました。
SAM変換とは
PlayerクラスにplayCD( )関数を委譲して、CDPlayerオブジェクトを作るプログラムをKotlinで記述すると、次のようになります。
CDPlayerクラスを作るプログラムのプロローグ
総称して「プレーヤー(Player)」と呼ばれる物の中には、アプリケーションならばAudioプレーヤー、Videoプレーヤー、機器ならばCDプレーヤー、DVDプレーヤー、などが考えられます。
これらの共通点は、再生(Play)・一時停止(Pause)・停止(Stop)が出来ることです。ただし、Playを行うための「具体的な処理」はプレーヤー毎に異なります。PauseとStopも同様です。
CDプレーヤーをプログラムで実現する方法を考えてみると…
play()・pause()・stop()関数(またはメソッド)を持つPlayerクラスを作成して、CDプレーヤ用に「具体的な処理」を実装した関数playCD()・pauseCD()・stopCD()を委譲してあげれば、CDプレーヤーが作れそうです。
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 }
関連記事:
Kotlinは関数を変数に代入したり、引数で受け渡したりできます。 関数を第一級オブジェクトで扱えるためです。 これにより、関数を使った処理の委譲が容易になりました。ArrayOf#forEach( )がその典型的な例です。 以上のような操作で必要となる「関数オブジェクト」について、まとめます。 ...
クロージャ(Closure)は新しい概念ではなく、関数型プログラミングに古くから存在していました。 手続き型やオブジェクト指向プログラミングにも、だいぶ前から採用が進んでいます。 Kotlinもクロージャが使える言語の1つです。 ...
近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
Android Studio Giraffe(2023.07)になって、ビルドスクリプトの推奨がKotlin DSLになりました。 この機会に、ビルドスクリプトと記述言語についてまとめます。 ※環境:Android Studio Giraffe | 2022.3.1 :Android Gradle Plugin 8.1.0 :Gradle 8.0 ...
Kotlinにおいて、各クラスのインスタンスに自動生成されるequals()関数は、classとdata classで比較対象に違いがあります。 紛らさしいので、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1 Kotlin 1.8.10 ...
KotlinはJavaのプリミティブ型に当たるデータ型を持ちません。しかし、同等な記述が基本データ型を用いて出来ます。 Kotlinにおいて、基本データ型がどのように扱われているのか、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1 Kotlin 1.8.10 ...
継承を行うことなく新しい機能(関数やプロパティ)をクラスへ追加する「拡張機能」についてまとめます。 何処にでも手軽に定義できるため、とても重宝する仕組みですが、乱用するとプログラムが無法地帯になるので、管理は重要です。そのために、十分な理解が必要になります。 ちなみに、Android Jetpackは、この「拡張機能」を活用して作られた部分が多くあります。「拡張機能」無くして、成り立たないシステムです。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 1 Kotlin 1.8.10 ...
スコープ関数はKotlinの標準ライブラリで提供されています。 そのことから、Kotlinを特徴付ける重要な構文であることが分かります。 ただし、「プログラムの動作を定義する構文」ではなく、「プログラムのコードの品質を上げる構文」です。 同様な動作はスコープ関数を使わなくても記述できます。しかし、スコープ関数を使えばスマートな(洗練された)記述になります。 積極的に使いたいと思いますが、applyとlet以外はあまり使う機会がありません。また、稀にwithなどが登場すると、コードの流れが理解できずに戸惑います。 ですので、スコープ関数について、まとめました。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2 Kotlin 1.9.0 Compose Compiler 1.5.1 ...
Kotlinで利用可能なJSONライブラリーには「GSON, Jackson, Moshi, Kotlin serializationなど」があります。 始めの3つはJavaがベースです。その中のMoshiは、拡張機能によりKotlinとの親和性が高められています。 Kotlin serializationはJSON以外(Protobuf, CBOR, Hocon, Properties )のフォーマットも扱えます。フォーマットを扱うというよりも、シリアル化の機能を重視したライブラリーのようです。 プログラム間でデータを受け渡す際に用いるのであれば、Kotlin serializationが最も適しているかも知れません。 Kotlin serializationに興味を引かれますが、後の機会に置いといて... 今回は、Moshiについて、まとめます。 ※環境:Android Studio Ladybug Feature Drop | 2024.2.2 Kotlin 2.0.0 Moshi 1.15.2 ...
MoshiでJSONをパース(JSONの記述⇔データクラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス以外の、ユーザ定義のクラスはパース出来ません。 対応させるためには、そのクラスのカスタムJsonAdapterを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムJsonAdapterの作成方法をまとめます。 ※環境:Android Studio Ladybug Feature Drop | 2024.2.2 Kotlin 2.0.0 Moshi 1.15.2 ...
Kotlinで利用可能なJSONライブラリーには「GSON, Jackson, Moshi, Kotlin serializationなど」があります。 Kotlin Serializationは、Kotlinがベース(前の3つはJavaがベース)です。 2020.10にVer 1.0.0がリリースされており、比較的新しいライブラリです。 今回は、Kotlin serializationについて、まとめます。 ※環境:Android Studio Ladybug Feature Drop | 2024.2.2 Patch 1 Kotlin 2.0.0 Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ユーザ定義クラス、Primitive版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1 Kotlin 2.0.0 Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ライブラリ提供クラス、Primitive版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1 Kotlin 2.0.0 Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ユーザ定義クラス、Delegating版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1 Kotlin 2.0.0 Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ライブラリ提供クラス、Surrogate版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1 Kotlin 2.0.0 Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ユーザ定義クラス、Handwritten版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1 Kotlin 2.0.0 Kotlin serialization json 1.7.1 ...