おでーぶでおでーぶ

いろいろ書く。いろいろ。

Javaからの利用を視野に入れたKotlinコードで何をするべきか

(2015年に書いたものをコピペ)

Kotlin Advent Calendar 2015 10日目.

TL; DR

Javaからの見た目を考慮して,アノテーションと修飾子を使って整形しましょう

  • @file:JvmName@JvmStatic@JvmOverloadsをつけよう
  • constopen 修飾子は適切に
  • 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 objectnamed objectPackage-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を知っている人からすると xxxKtCompanion といったクラスが補完で出てきたら色々と察するところではあると思うんですが,そうじゃない人たちにとってこの部分はなかなかの障害になるかと思います.

拡張関数について

拡張関数は定義した位置の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 AJava側で継承し,自動生成してみましょう.

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

  1. 個人の感想です

  2. 現状はAnt,Maven,Gradleの3種類を公式がサポート.SBTプラグインを書いてる有志も.

  3. 他にあったらぜひ教えてください

  4. 何かしらの方法をご存知の方は教えてください

  5. propertyなので

  6. 著者 @RyotaMurohoshi さん,Qiita Kotlinのクラスの継承とメソッドのオーバーライド、あとポエム

  7. 筆者 @ngsw_taro さん,算譜王におれはなる!!!! Kotlin M13で追加されたsealed class