【サーバーサイドKotlin】関数型インターフェース、SAM変換について

はじめに

今回はKotlinでの関数型インターフェースの扱い、SAM変換という機能についてご紹介します。

Javaを呼び出すことができるKotlinならではの問題で、最初は理解するのになかなか時間がかかりました。

問題点

関数型インターフェース

Javaでは関数型インタフェースを引数として渡す時、ラムダを渡すことができます。

@FunctionalInterface
public interface FunctionSample {
    public Integer calc(Integer num1, Integer num2);
}
public class CallFunctionSample {
    public Integer callFunction(FunctionSample function) {
        return function.calc(1, 2);
    }
}
public class Main {
    public static void main(String[] args) {
        CallFunctionSample callFunctionSample = new CallFunctionSample();
        Integer result = callFunctionSample.callFunction((Integer num1, Integer num2) -> {
            return num1 + num2;
        });
        System.out.println(result);
    }
}

SAMインターフェース、SAM変換

Javaで書かれたメソッドをKotlinから呼び出す際も、同じようにラムダ式を引数で渡すことができます。

fun main(args: Array<String>) {
    val callFunctionSample = CallFunctionSample()
    println(callFunctionSample.callFunction{ num1: Int, num2: Int -> num1 + num2 })
}

これはSAM変換という機能が働くためです。

関数型インターフェースのように抽象メソッドを一つだけ持つインターフェースのことをSAMインターフェースと言います(Single Abstract Methodの略)。

KotlinはSAMインターフェースを引数として取る関数にラムダ式を渡すと、自動的にSAMインターフェースに変換してくれます。

この機能を、SAM変換と呼びます。

Kotlinで実装した場合の問題

しかし、Kotlinで作られた単一の関数のインターフェースを呼ぶ際は言語仕様上SAM変換が効かないため、下記のように匿名オブジェクトを書いて渡す必要があります。

class CallFunctionSample {
    fun callFunction(function: FunctionSample): Int {
        return function.calc(1, 2)
    }
}
fun main(args: Array<String>) {
    val callFunctionSample = CallFunctionSample()
    println(callFunctionSample.callFunction(object : FunctionSample {
        override fun calc(num1: Int, num2: Int): Int {
            return num1 + num2
        }
    }))
}

見た目の煩わしさもありますが、ラムダで渡した場合は基本的に対応する匿名クラスのインスタンスが呼び出し間で再利用されるというメリットがあります。

匿名オブジェクトを渡した場合は呼び出し毎に新しいインスタンスが生成されてしまいます。

対応策

この問題に関しては、次のような対応方法があります。

Kotlinではこちらの書き方の方が推奨されています。

高階関数を使う

高階関数とは、関数を引数や戻り値に取る関数のことです。

class CallFunctionSample {
    fun callFunction(function: (Int, Int) -> Int): Int {
        return function(1, 2)
    }
}

このように引数に関数を書くことによって、呼び出す際もラムダを渡すことが可能になります。

fun main(args: Array<String>) {
    val callFunctionSample = CallFunctionSample()
    println(callFunctionSample.callFunction{ num1: Int, num2: Int -> num1 + num2 })
}

Kotlinでは関数型が存在しており、関数渡しにはそちらを使用すれば良いため、関数型インターフェースのような単一メソッドのインターフェースを作る必要がありません。

そのため、前述したようにKotlinは高階関数を使うことが推奨されています。

SAM変換はあくまで関数型が存在しないJavaのインターフェースを扱うための機能として用意されています。

最後に

JavaのコードをKotlinへ変換した際この問題はよく出ていて、冒頭に書いた通り最初はなかなか理解できませんでした。

ライブラリ等に入っているJavaで書かれた関数型インターフェースはラムダ式で渡せて、Kotlinで書いた同じようなコードではエラーになる、ということが起きていたためです。

もし最初からKotlinとして言語の思想に沿って作っていれば起きない問題のため、Javaから変換するという形だったからこそぶつかった問題かなと思います。

引数に関数を渡す処理でなぜかコンパイルエラーがでることがあった時、この内容を思い出してもらえると解決できるかもしれません。

関連記事

なぜサーバーサイドKotlinを導入するのか?

【サーバーサイドKotlin】データクラスの継承について