こんにちは、GxPのチョウキです。
こちらはグロースエクスパートナーズ Advent Calendar 2023の3日目の記事です。
現在、私はアプリ開発に携わっており、その過程でKotlinを使用しています。別のプロジェクトではJavaを用いていましたが、JavaからKotlinへの移行にはいくつか困難が伴います。JavaとKotlinは100%互換性があるとされていますが、一方で似ている部分と大きく異なる部分が存在します。特に、よく使われているLambda関数の観点から、Kotlinの書き方を紹介したいと思います。Lambda関数を使用する際、初見では意味が分かりにくいことがしばしばあり、それを難しく感じることがあります。そのため、この知識をまとめることにしました。
JavaとKotlinにおけるLambda関数と高階関数の使用
Lambda関数に入る前に、高階関数から説明します。
高階関数とは、簡単に言うと、関数型をパラメータや戻り値に持つ関数を指します。
Javaでのメソッドの扱い
Javaでは、一つのメソッド内で別のメソッドを呼び出すことは一般的です。
int a() {
return b(1) + 1;
}
int b(int num) {
return 1;
}
a();
さらに、メソッドaで動的にメソッドbのパラメータを設定したい場合、そのパラメータをaに渡し、内部でbに渡します。
int a(int num){
return b(num) + 1;
}
int b(int num){
return 1;
}
a(1);
もし動的に設定したいのがメソッドのパラメータではなく、メソッド自体だったらどうでしょうか?例えば、私のあるメソッド「a」の内部に他のメソッドを呼び出す部分があり、そのメソッドは「b」である可能性も「c」である可能性もあります。つまり、メソッド自体をパラメータとして別のメソッドに渡したいのです。
Javaではメソッドを直接パラメータとして渡すことはできませんが、インターフェースを介してメソッドを包み込むことで、間接的にメソッドを渡すことができます。
public interface A {
int method();
}
int a(A a) {
return a.method() + 1;
}
Kotlinにおける高階関数と関数参照
Kotlinでは、関数のパラメータや戻り値として関数型を使用することができます。
fun a(funParam: (Int) -> String): String {
return funParam(1)
}
fun b(param: Int): (Int) -> Unit {
return { x ->
val result = x + param
println("計算結果: $result")
}
}
このような関数型をパラメータや戻り値に持つ関数は、Kotlinで「高階関数」と呼ばれています。
関数参照
関数を変数に代入したり、他の関数に渡す場合、関数名の前に::を付けます。これを「関数参照」と呼びます。
fun a(funParam: (Int) -> String): String {
return funParam(1)
}
fun b(param: Int): (Int) -> Unit {
return { x ->
val result = x + param
println("計算結果: $result")
}
}
a(::b)
val d = ::b
kotlinで関数をパラメーターとして渡すことができる理由は、::によって関数はオブジェクトに変換されるということです(このオブジェクトは、関数の機能を持ちます)。
a(::b)
val d = ::b
b(1) // 関数の呼び出し
d(1) // 関数オブジェクトの呼び出し
(::b)(1) // 関数オブジェクトの呼び出し
オブジェクトは、()で呼び出すことができません。でも関数型のオブジェクトは()で呼び出すことできます。これは、実際にオブジェクトのinvokeメソッドを使用して呼び出しています(kotlinのシンタックスシュガー)。
a(::b)
val d = ::b
val e = d // オブジェクトの代入
b(1) // 関数の呼び出し、b.invoke(1)は不可
d.invoke(1) // 関数オブジェクトの呼び出し
(::b).invoke(1) // 関数オブジェクトの呼び出し
Kotlinの匿名関数とLambda式
関数型のパラメータと変数への代入
Kotlinでは、関数型のパラメータを渡すか、関数型のオブジェクトを変数に代入する場合、2つの方法があります。一つ目は関数参照(::を使用する方法)、もう一つは匿名関数を使用する方法です。
fun b(param: Int): String {
return param.toString()
}
a(::b)
// Kotlinではこのような形式は許可されていません
a(fun b(param: Int): String {
return param.toString()
})
// 以下のように書くべき
a(fun(param: Int): String {
return param.toString()
})
val d = ::b
// この形式もKotlinでは許可されていません
val d = fun b(param: Int): String {
return param.toString()
}
// 以下のように書くべき
val d = fun(param: Int): String {
return param.toString()
}
Javaとの比較
Javaでは、以下のようにインターフェースを使用してイベントリスナーを実装します。
public class MyViewClass {
public interface OnTypeListener {
void onType(View v);
}
private OnTypeListener listener;
public void setOnTypeListener(OnTypeListener listener) {
this.listener = listener;
}
public void triggerOnType(View view) {
if (listener != null) {
listener.onType(view);
}
}
public static void main(String[] args) {
MyViewClass view = new MyViewClass();
view.setOnTypeListener(new OnTypeListener() {
@Override
public void onType(View v) {
sendCommand();
}
});
view.triggerOnType(null);
}
private static void sendCommand() {
System.out.println("sendCommand called");
}
}
Kotlinでは、以下のように柔軟に書き換えることができます。
class MyKotlinViewClass {
private var onTypeListener: ((View) -> Unit)? = null
fun setOnTypeListener(onType: (View) -> Unit) {
this.onTypeListener = onType
}
fun triggerOnType(view: View) {
onTypeListener?.invoke(view)
}
companion object {
fun sendCommand() {
println("sendCommand called")
}
}
}
fun main() {
val myView = MyKotlinViewClass()
myView.setOnTypeListener { v ->
MyKotlinViewClass.sendCommand()
}
myView.triggerOnType(View(null))
}
Lambda式
lambdaと匿名関数はどちらも関数リテラルの形です。lambdaはより簡潔で、多くの場合には適しています。匿名関数は、多くの場合Lambda式に置き換えることができます。
コード例:
//funを略
view.setOnTypeListener({ v: View ->
sendCommand();
})
// Lambdaが関数の最後のパラメータの場合、外に書くことができます
view.setOnTypeListener() { v: View ->
sendCommand();
}
// Lambdaが唯一のパラメータの場合、括弧を省略できます
view.setOnTypeListener { v: View ->
sendCommand();
}
// Lambdaが一つのパラメータを持つ場合、そのパラメータは省略可能です(省略しない場合は、デフォルトit使っています)
view.setOnTypeListener {
sendCommand();
it.closeWindow();
}
関数を定義する時、パラメーター情報があれば、Lambda式の呼び出しとパラメータの省略できます。
fun setOnTypeListener(onType: (View) -> Unit) {
this.onType = onType
}
view.setOnTypeListener {
sendCommand()
it.closeWindow()
}
Lambda式を変数に代入する際に、型を省略することはできないため、指定する必要があります。
//匿名関数
val d = fun(param: Int): String {
return param.toString()
}
//Lambda式
val d = { param: Int ->
param.toString()
}
//Lambda式
val d: (Int) -> String = {
it.toString()
}
戻り値:
Lambda式でreturnを使用すると、外側の関数からの戻り値として扱われます。Lambda内での戻り値には、return@ラベルの形式を使用します。
fun main() {
val multiply = { a: Int, b: Int ->
return a * b // これは main 関数からの戻り値になります
}
val result = multiply(3, 4)
// この println は実行されません
println(result)
}
// Lambda式内からの戻り値の例
val multiply = { a: Int, b: Int ->
return@multiply a * b
}
lambdaと匿名関数はどちらも関数リテラルの形ですが、違いはいくつがあります:
- シンタックス:ラムダ式は {} で囲まれ、パラメータリスト(定義する場合)と本体を含みます。パラメータは -> の前に記述されます。匿名関数は fun キーワードを使用して定義されます。これは通常の関数定義に似ていますが、関数名がありません。
- 省略可能な構文: ラムダ式では、コンテキストから推論可能な場合、パラメータの型を省略することができます。また、ラムダ式が単一のパラメータのみを受け取る場合、そのパラメータを明示的に宣言せずに it キーワードで参照できます。匿名関数では、パラメータの型を省略することはできません。ラムダ式のように it を使用することもできません。
- 戻り値: ラムダ式の本体が式である場合(単一の式で構成されている場合)、その式の評価結果が自動的に戻り値となります。ブロック本体を使用する場合は、return キーワードは必要ありません。匿名関数では、戻り値の型を指定できます。ラムダ式とは異なり、ブロック本体を持つ匿名関数では、return キーワードを使用して明示的に戻り値を指定する必要があります。
lambdaはより簡潔で、よく使われています。一方で、匿名関数はより明示的で、戻り値の型を指定したい場合や、複数の return 文を持つ複雑なロジックを記述する場合に有用です。
*JavaのLambda式:
Kotlinにおける匿名関数とLambda式は、関数型のオブジェクトです。これらは、Java 8で導入されたLambda式とは異なります。
Java 8のLambda式
Java 8では、Lambda式が導入され、SAM(Single Abstract Method)インターフェースの実装を簡潔に記述できるようになりました。SAMインターフェースは、一つの抽象メソッドを持つインターフェースのことです。
interface A {
void showA();
}
// Java 8以前の実装
A a = new A() {
@Override
public void showA() {
System.out.println("this is A");
}
};
// Java 8のLambda式による実装
A a = () -> System.out.println("this is A");
Kotlin のラムダ式はより柔軟で、文法的に簡潔であり、単一のパラメータを持つ場合の特別な扱い(it)や、ラムダ式を変数として扱うことができるなど、使いやすい特性があります。一方、Java のラムダ式は型がより厳格で、関数型インターフェースと密接に関連しています。Kotlin は関数型プログラミングの要素をより深く統合しているのに対し、Java のラムダ式は既存の言語構造に追加された機能という位置付けです。
Kotlinでは、JavaのSAMインターフェースに対応し、Lambda式を使ってこれらを実装することが可能です。これにより、KotlinのコードはJavaの関数型プログラミングの特性を活用しやすくなります。