Kotlinでデフォルト引数を持つ関数をinterfaceで定義して使うことってよくあると思うのですが、これをJavaから使おうとしたときにちょっと嫌な感じになってしまいました。ベストな方法が知りたいんですがたどり着けなかったので、誰か教えてくれる人が現れることを期待しつつ残しておきます。
なお、インターフェースの実装内容などは検証コードなので適当です。
- インターフェースの定義(Kotlin)
データベースへの接続を管理するconnect関数
をローンパターンで定義。
interface ConnectionManager {
fun createConnection(host: String): Connection {
return Connection(host)
}
fun<A> connect(c: Connection = createConnection("localhost"), f: (Connection) -> A): A {
try {
c.begin()
return f(c)
} catch (e: Exception) {
c.rollback()
throw e
} finally {
c.commit()
}
}
}
- インターフェースを利用(Kotlin)
データベースに接続し、select文を発行して、Userインスタンスを返すイメージ
class UserDao : ConnectionManager {
fun findById(id: Int): User {
return connect { c -> c.select("select * from user where id = ${id}", User()) }
}
}
ここまでは特に何の問題もないです。で、このインターフェースをJavaからも使いたいと思った時に問題が発生しました。
- インターフェースを利用(Java) (コンパイルエラー)
public class UserDaoJava implements ConnectionManager {
public User findById(int id) {
return connect(c -> c.select("select * from user where id = " + id, new User()) );
}
}
これはコンパイルエラーになります。Javaからはデフォルト引数を省略した引数が一つのconnect関数として呼ぶことができません。
仕方がないので、引数が一つの関数をインターフェースに追加しました。
interface ConnectionManager {
・・・省略・・・
// この関数を仕方なく追加
fun<A> connect(f: (Connection) -> A): A {
return connect(createConnection("localhost"), f)
}
}
これで大丈夫かと思いきや、これでもコンパイルが通りません。JavaからはKotlinのインターフェースのデフォルト実装がそのままでは呼べません。そのため、利用するJavaクラス側でオーバーライドしないといけません。
public class UserDaoJava implements ConnectionManager {
・・・省略・・・
// 下の3つが必要
@NotNull
@Override
public Connection createConnection(@NotNull String host) {
// 内部実装はConnectionManagerのものを利用したいのでそのまま呼ぶ。
return ConnectionManager.DefaultImpls.createConnection(this, host);
}
@Override
public <A> A connect(@NotNull Connection c, @NotNull Function1<? super Connection, ? extends A> f) {
return ConnectionManager.DefaultImpls.connect(this, c, f);
}
@Override
public <A> A connect(@NotNull Function1<? super Connection, ? extends A> f) {
return ConnectionManager.DefaultImpls.connect(this, f);
}
}
これでとりあえずコンパイルも通り動くようになりました。ただ、Kotlin側でConnectionManagerを使うときにconnect関数が2つ見えてしまうのも嫌だし、何よりJava側で利用するたびにオーバーライドするなんてありえない。
なので、Java用のConnectionManagerを作ってしまおう!!
まず、ConnectionManagerからは先ほど追加した引数が一つのconnect関数を削除。そして、新しく作成するJava用のConnectionManagerの方で引数が一つのconnect関数を定義します。
public interface ConnectionManagerJava extends ConnectionManager {
@NotNull
@Override
default Connection createConnection(@NotNull String host) {
return ConnectionManager.DefaultImpls.createConnection(this, host);
}
@Override
default <A> A connect(@NotNull Connection c, @NotNull Function1<? super Connection, ? extends A> f) {
return ConnectionManager.DefaultImpls.connect(this, c, f);
}
default <A> A connect(@NotNull Function1<? super Connection, ? extends A> f) {
return connect(createConnection("localhost"), f);
}
}
そして、UserDaoJavaはConnectionManagerJavaを使うように変更
public class UserDaoJava implements ConnectionManagerJava {
public User findById(int id) {
return connect(c -> c.select("select * from user where id = " + id, new User()) );
}
}
これで、KotlinのConnectionManagerに追加した引数1つのconnect関数は削除でき、Java側もすっきりしました。ただし、interfaceでdefault実装を使っているので、Java8じゃないといけないという問題がありますが。
こんな感じで、Kotlinのデフォルト引数を持つinterfaceをJavaから使うために、Java側にもinterfaceを作るという荒技?にたどり着いちゃったんですが、何か間違ってる気もする。。。
これで正解なのか、Kotlinのinterfaceで実装しているところから違うのか、それとももっと別のやり方が用意されているのか、気になるところです。