Javaからの利用を視野に入れたKotlinコードで何をするべきか
(2015年に書いたものをコピペ)
Kotlin Advent Calendar 2015 10日目.
TL; DR
Javaからの見た目を考慮して,アノテーションと修飾子を使って整形しましょう
@file:JvmName
,@JvmStatic
,@JvmOverloads
をつけようconst
,open
修飾子は適切に- interfaceのdefault/static methodの扱いには注意しよう
Javaからの利用を視野に入れるということ
Null-safeの恩恵は強いし,拡張関数は便利だし,他にも色々機能はあるし,多分金髪美少女だし,Kotlinは非常に扱いやすい可愛い言語です1.
さらに,普段からJavaを用いて開発しているひとにとって馴染みの深いbuild toolを用いた開発が可能です2.
つまりはbuild toolさえ動けば良いので,jitpack.ioなどのpackage repositoryサービスでもKotlinの利用が可能になっています.となると,Kotlinライブラリの配布・利用を行う壁は非常に低いことが分かりますね.
Kotlinの特徴を用いたライブラリの利点は KotlinからでもJavaからでも非常に高いと思っています.Kotlinだけをターゲットとしたライブラリであれば必要ないのでしょうが,JVM上で動作する便利ライブラリとして配布していくとしたら,最低限Javaからの利用は考慮する必要があるでしょう.
それらの考慮のうち,ひとの手による調整が必要/適切だと思われるものをあげていきたいと思います.
Staticメソッド周り
Kotlinで NameSpace.method()
と呼び出せるメソッドを定義する際,companion object, named object,Package-level function が挙げられます3.
例えば以下のような定義をKotlinでしてみましょう.
package daruma fun debu() = true class RedDaruma { companion object { fun fat() = true } } object Jmatsu { fun yaseru() = throw NotImplementedError() }
Kotlinからであれば daruma.debu()
,daruma.RedDaruma.fat()
,daruma.Jmatsu.yaseru()
で呼び出せるこのメソッドたちですね.
しかし,Javaから呼び出す際はそれぞれ daruma.DebudarumaKt.debu();
,daruma.RedDaruma.Companion.fat();
,daruma.Jmatsu.INSTANCE.yaseru();
と書くことになります.
KotlinではPackage-levelでのfunction定義が可能.
特に指定がない場合, Packege-level functionは "${filename}Kt"クラスのstaticメソッドとして充てがわれる.
でも "${filename}Kt"
とか Companion
ってださいしあんまり良くないですよね.そこで fileに対する@JvmName
と methodに対する@JvmStatic
アノテーションを利用します.
@file:JvmName("Majide") // Use Majide instead of DebudarumaKt package daruma fun debu() = true class RedDaruma { companion object { @JvmStatic fun fat() = true // Don't need Companion } } object Jmatsu { @JvmStatic fun yaseru() = throw NotImplementedError() // Don't need INSTANCE }
とすると,Javaからでも daruma.Majide.debu();
,daruma.RedDaruma.fat();
,daruma.Jmatsu.yaseru();
で呼び出せるようになります.
※ object
の場合,インスタンスメソッドであって欲しいときには関係ありません
Kotlinを知っている人からすると xxxKt
や Companion
といったクラスが補完で出てきたら色々と察するところではあると思うんですが,そうじゃない人たちにとってこの部分はなかなかの障害になるかと思います.
拡張関数について
拡張関数は定義した位置のstaticメソッドとして生え,レシーバーとして第一引数に充てがわれます.つまり上記のstaticメソッドのケースが当てはまります.ということで同様の対策を行いましょう.
ついついPackage-levelの位置で宣言しがちですので,@JvmName
を充てましょう.
Interfaceに載せる系の実装
Interfaceのstaticメソッド
JavaだとJava8からinterfaceにstaticメソッドを定義できるようになりました.
Kotlinでは companion object
を使うことでinterface上にstaticメソッドが生えたように見せることが可能です.(Kotlinから見た場合)
しかしそのとき,Javaから見るとCompanionオブジェクトが生えることは上記で説明しました.
では @JvmStatic
をつければよいのでしょうか? 答えはNOです.(そもそもつけられません.)
つけられるとしたらそれはJava8相当の動きになることを表していますね.
結論として,前述したstaticメソッドと同等の挙動を実現することは不可能です.
そこで妥協策としては named object で実装することで Companionという名前を回避することになります.
interface Foo { fun foo(): String object Bar { @JvmStatic fun baz() = 10 } }
とすれば,Javaからは Foo.Bar.baz()
として呼び出すことが可能です.
Interfaceのdefaultメソッド
JavaだとJava8から,interfaceのメソッドにdefault修飾子を指定することで初期実装を与えることができるようになりました. 詳細は割愛しますが,主な特徴として,interfaceを継承したクラスにおいてデフォルト実装を持ったメソッドをOverrideをするかしないかは任意となっています.
Kotlinのinterfaceではdefault修飾子は必要なく,通常のメソッド定義と同様にメソッドボディを持たせることができます. これをKotlinはJVMに解釈させるためにどう変換しているのかが問題になります. 以下の例を見てみましょう.
interface A { fun foo(): String = "foo" } class B : A { // Overriding 'foo()' is not needed }
同様の実装をJava8で書くなら
public interface A { default String foo() { return "foo" } } class B implements A { // Overriding 'foo()' is not needed }
となりますね.
ではKotlinで定義したinterface A
をJava側で継承し,自動生成してみましょう.
public class B implements A { @Override public String foo() { return null; // 'foo()' must be implemented! } }
はい,残念ながらJava8のdefault methodと同様の動きとはいきません. では消えてしまったのでしょうか? JVM上で動いてる以上,そんなわけはないですね.
代わりに A.DefaultImpls.foo(A $receiver)
が定義されています.
interface内部に定義されたstatic finalなclassにおいて,staticメソッドとして移植されるわけですね.
これに関してはKotlinから(アノテーション等で)どうこうできる問題ではありません. Javadocに「デフォルト実装がある」と書いておくくらいしか出来ません4. DefaultImplsという名前も変えることは自分ではできませんでした.また変えてしまうとデフォルト実装がどこに置かれるかがわかりづらくなるため,これに関してはそのままで良いという感じもします.
このケースではJava側が注意する必要があります. つまりKotlinのinterfaceを継承したクラスではDefaultImplsがあるかどうか,あるなら中でdecorateするといった行為が必要になるでしょう.
public class B implements A { @Override public String foo() { return DefaultImpls.foo(this); } }
Staticでfinalなフィールド周り
Staticでfinalなフィールドはまた違う問題があります.Kotlinは優しいのでフィールドにはgetter/setter
をつけてくれます5.つまり何も指定をしなければカプセル化をしてくれるということですね.そう,それが例えstaticでfinalであっても・・・
package daruma val debu = true class RedDaruma { companion object { val fat = true } } object Jmatsu { val yaseru = null // :bow::bow: }
とすれば,
- daruma.DebudarumaKt.getDebu();
- daruma.RedDaruma.Companion.getFat();
- daruma.Jmatsu.INSTANCE.getYaseru();
が推奨されたアクセスになります.この表現から分かると思いますが,
- daruma.DebudarumaKt.debu;
- daruma.RedDaruma.fat;
- daruma.Jmatsu.yaseru;
は非推奨のアクセスになります.※ ちゃんとstatic&&finalで宣言されてます
こちらはアノテーションではなく,const
修飾子を使います.
package daruma const val debu = true class RedDaruma { companion object { const val fat = true } } object Jmatsu { const val yaseru = null // :bow::bow: }
ちなみにこちらだと先ほどの推奨と非推奨が逆転した形になります.
デフォルト値を用いたメソッド・コンストラクタ
Kotlinを使っていると結構使うのがデフォルト値.便利の一言に尽きます.
class GitApi { fun push(remote: String = "origin", branch: String = "master", force: Boolean = false) { // do something } }
実はこれ,Javaから見ると GitApi#push(String, String, boolean)
しか定義されていません.割と困ります.そこで @JvmOverloads
アノテーションを使って解決しましょう.
class GitApi { @JvmOverloads fun push(remote: String = "origin", branch: String = "master", force: Boolean = false) { // do something } }
- GitApi#push(String)
- GitApi#push(String, String)
- GitApi#push(String, String, boolean)
@JvmOverload
アノテーションにより自動で各メソッド(今回だと追加で2種類の計3種類)が定義されます.引数が全て揃ったメソッド以外は中でdispatchするだけの存在になります.
実はこれコンストラクタにも使えるんですよね,ということで
Androiderの方には下記のコンストラクタ定義が結構おすすめ
import android.content.Context as Ctx // 横に長過ぎたので import android.util.AttributeSet as AS // 同上 class CustomView : View { @JvmOverloads constructor(ctx: Ctx, attrs: AS? = null, style: Int = 0): super(ctx, attrs, style) { // init action attrs?.apply { // load the attributes } } }
クラス継承とOverride
9日目で @RyotaMurohoshi さんがちょうどこの周りの話をしてくださいました6.
Kotlinでは継承に関して未指定の定義 == Javaでいうfinalで修飾された扱い となります.
これに関しては存在そのものになど賛否両論あるかと思います.ただ賛否どちらでも変わらない事実として,「このクラスは継承させたら困るからfinal」と考えていた人は「このクラスは継承してもいい/されることもあるからopen」という逆の考え方をしなければなりません.
自分のKotlinコードであればエラーが出た際に修正する程度で構わないのかもしれませんが,他人のライブラリ,あるいは自分が他人へ提供するライブラリではそんなことも言えないので,気をつける必要があるでしょう.
まとめ
- Package-level declaration は
@file:JvmName
で空間名の指定 - 静的メソッドは
@JvmStatic
で生やす場所の指定 - 静的な不変値は
const
で - デフォルト値は
@JvmOverloads
で自動多重定義 open
の指定は適切に- interfaceのstaticメソッドは
named object
&@JvmStatic
- interfaceのdefaultメソッドはJava8とは違う挙動になり,DefaultImpls
Kotlinは非常に便利ですが,Java100%相互運用という利点を活かそうと思うと,やはりひとの手である程度の補正を行う必要が出てきますね.
おまけ
IntelliJ上でのsealed classの扱い
Kotlin M13よりsealed classというものが追加されました7.
sealed
という修飾子をつけると,そのclassを継承する際に制限をかけることができます.その制限とは,「sealed classを継承するクラスはそのsealed classの内部クラスであること」になります.
sealed class A { class B : A() // ok } class C : A() // error
Javaから使った場合もちゃんとコンストラクタ周りで怒られて継承はできません.正しい挙動ですね.
ただ現状,IntelliJだと以下のコードにエラーがエディタ上に表示されません.
package daruma sealed class A
public class D extends daruma.A { public D() { super(); // 本来なら private access ということで怒られるはず } }
勿論コンパイル時にはエラーとして検出されるので,Runtimeでは全く問題がないのですが.
Kotlinのコードをjarにし,Eclipseで書いてみるとちゃんとエラー扱いになります.ただこのケースでもIntelliJだとやはりエラーが見えないという・・・ Javaでprivateコンストラクタのみを宣言したクラスに対する継承の場合はちゃんとエラーが出るので,Kotlinプラグインの問題なんですかね.
Javaから見るドキュメント
KDocを書いたときのJavaからの見た目ですが,特に差異はないです(違いを見つけていないだけかもしれませんが).
下記の環境の元,書かれました.
- Kotlin 1.0.0-beta-3595
- Android Studio 1.5.1 #AI-141-2456560 w/ plugin IJ141-11
- IntelliJ IDEA 15.0.1 #IU-143.382 w/ plugin IJ143-26