はじめに
Kotlin文法 - アノテーション、リフレクション、型安全なビルダー、動的型の続き。
Kotlin ReferenceのInterop章の大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。
KotlinからJavaのコードを呼ぶ
KotlinはJavaとの相互運用を念頭において設計されている。既存のJavaコードはKotlinから自然な方法で呼び出すことができる。そしてKotlinのコードも同様にかなりスムーズにJavaから利用することができる。この節ではKotlinからJavaのコードを呼ぶ場合の詳細を記述する。
ほとんど全てのJavaコードは何の問題もなく利用できる。
import java.util.*
fun demo(source: List<Int>) {
val list = ArrayList<Int>()
// Javaコレクションでのforループが動作する
for (item in source)
list.add(item)
// 演算子置換ルールも同様に機能する
for (i in 0..source.size() - 1)
list[i] = source[i] // get と set が呼ばれる
}
GetterとSetter
Javaの慣習に従ったgetterとsetter(get で始まる名前の引数なしメソッドと、set で始まる1引数のメソッド)はKotlinではプロパティとして解釈される。例えば以下のように。
import java.util.Calendar
fun calendarDemo() {
val calendar = Calendar.getInstance()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // getFirstDayOfWeek()を呼ぶ
calendar.firstDayOfWeek = Calendar.MONDAY // setFirstDayOfWeek()を呼ぶ
}
}
もしJavaクラスがsetterしか持たなかったら、Kotlinではプロパティとして見えないことに注意。今のところKotlinではsetしかできないプロパティはサポートしていないので。
voidを返すメソッド
もしJavaメソッドがvoidを返す場合、Kotlinから呼び出されるときは Unit を返す。ひょっとして誰かがその値を使うなら、Kotlinコンパイラによって呼び出し箇所で代入される。値そのものは事前に(Unit になると)わかっているので。
KotlinでキーワードになっているJava識別子のエスケープ
幾つかのKotlinキーワードはJavaでは有効な識別子。つまり in, object, is など。もしJavaライブラリがKotlinキーワードをメソッドに使っていたら、バッククォートでエスケープして呼び出すことができる。
foo.`is`(bar)
Null安全とプラットフォーム型
Javaでの全ての参照は null になりうる。なのでJavaから来るオブジェクトに対しては、Kotlinの厳格なnull安全要求は実現困難になる。Java宣言の型はKotlinでは特別扱いされており、プラットフォーム型 と呼ばれている。この型に対してのNullチェックは緩和され、安全保証はJavaの場合と同じになる(下記参照)。
次の例を考えてみよう。
val list = ArrayList<String>() // nullでない (コンストラクタの結果)
list.add("Item")
val size = list.size() // nullでない (プリミティブ型のint)
val item = list[0] // プラットフォーム型と推論される(普通のJavaオブジェクト)
プラットフォーム型の変数に対してメソッドを呼ぶ時、Kotlinはコンパイル時にnull可能性エラーを報告しない。しかしnullポインタ例外やnullの伝搬を防ぐためにKotlinが生成するアサーションによって、実行時に呼び出しが失敗するかもしれない。
item.substring(1) // 許可される。them == null で例外が投げられるかも。
プラットフォーム型は言語で明示的に書き下すことができないという意味で 表記できない 。プラットフォーム型の値がKotlinの変数に代入されるとき、型推論(変数は上記例の item のようにプラットフォーム型と推論される)に頼るか、期待する型(nulllableもnullでない型も許される)を選ぶことができる1。
val nullable: String? = item // 許可されるし、常に動作する
val notNull: String = item // 許可されるけど、実行時に失敗するかもしれない
nullでない型を選べば、コンパイラが代入前にアサーションを挿入する。これによりKotlinのnullでない型がnullを持つことを防ぐ。アサーションは、プラットフォーム型をnullでない型を期待する関数に渡すときなどにも挿入される。全体として、コンパイラはプログラムを通してnullが遠くに伝搬するのを防ぐようベストを尽くす(ジェネリクスがあるので完全に排除できないことがあるけれど)。
プラットフォーム型の表記
上で言及した通り、プラットフォーム型はプログラム中で明示的に示すことはできない。つまり言語にそのための表記法はない。それでもコンパイラやIDEは時々それらを表示する必要がある(エラーメッセージやパラメータ情報など)。そのためこれらのための簡略表示法を用意している。
- T! は "TまたはT? " を表す
- (Mutable)Collection<T>! は「 T のJavaコレクションはmutableまたはそうでないかもしれず、nullableまたはそうでないかもしれない」を意味する。
- Array<(out) T>! は「 T (または T のサブ型)のJava配列はnullableまたはそうでない」を意味する。
Null可能性アノテーション
Null可能性アノテーションを持ったJava型は、プラットフォーム型ではなく実際のKotlinのnullableまたはnullでない型として表現される。今のところ、コンパイラはJetBrains風のNull可能性アノテーション(org.jetbrains.annotations パッケージにある @Nullable と @NotNull)をサポートしている。
マッピングされる型
Kotlinは幾つかのJava型を特別に扱う。これらの型はJavaから "そのまま" はロードされず、Kotlinの対応する型にマッピングされる。このマッピングはコンパイル時にだけ重要であり、実行時の表現は変化しないままである。Javaのプリミティブ型は対応するKotlin型にマッピングされる(プラットフォーム型のことは心に留めておいて)。
Java型 | Kotlin型 |
---|---|
byte | kotlin.Byte |
short | kotlin.Short |
int | kotlin.Int |
long | kotlin.Long |
char | kotlin.Char |
float | kotlin.Float |
double | kotlin.Double |
boolean | kotlin.Boolean |
幾つかのプリミティブでないビルトインクラスもマッピングされる。
Java型 | Kotlin型 |
---|---|
java.lang.Object | kotlin.Any! |
java.lang.Cloneable | kotlin.Cloneable! |
java.lang.Comparable | kotlin.Comparable! |
java.lang.Enum | kotlin.Enum! |
java.lang.Annotation | kotlin.Annotation! |
java.lang.Deprecated | kotlin.Deprecated! |
java.lang.Void | kotlin.Nothing! |
java.lang.CharSequence | kotlin.CharSequence! |
java.lang.String | kotlin.String! |
java.lang.Number | kotlin.Number! |
java.lang.Throwable | kotlin.Throwable! |
コレクション型はKotlinでは読み取り専用か可変のどちらかになる。なのでJavaコレクションは次のようにマッピングされる(この表のKotlin型は全て kotlin パッケージにある)。
Java型 | Kotlin読取専用型 | Kotlin可変型 | ロードされるプラットフォーム型 |
---|---|---|---|
Iterator<T> | Iterator<T> | MutableIterator<T> | (Mutable)Iterator<T>! |
Iterable<T> | Iterable<T> | MutableIterable<T> | (Mutable)Iterable<T>! |
Collection<T> | Collection<T> | MutableCollection<T> | (Mutable)Collection<T>! |
Set<T> | Set<T> | MutableSet<T> | (Mutable)Set<T>! |
List<T> | List<T> | MutableList<T> | (Mutable)List<T>! |
ListIterator<T> | ListIterator<T> | MutableListIterator<T> | (Mutable)ListIterator<T>! |
Map<K, V> | Map<K, V> | MutableMap<K, V> | (Mutable)Map<K, V>! |
Map.Entry<K, V> | Map.Entry<K, V> | MutableMap.MutableEntry<K,V> | (Mutable)Map.(Mutable)Entry<K, V>! |
Java配列のマッピングについては後述する。
| Java型 | Kotlin型 |
|:-- |:-- | |
| int[] | kotlin.IntArray! |
| String[] | kotlin.Array<(out) String>! |
KotlinでのJavaジェネリクス
KotlinのジェネリクスはJavaとは少し違っている(ジェネリクス参照)。Java型をKotlinにインポートするとき、幾つかの変換が行われる。
- Javaのワイルドカードは型投影に変換される
- Foo<? extends Bar> は Foo<out Bar!>! になる
- Foo<? super Bar> は Foo<in Bar!>! になる
- Javaの原型(raw type)は星投影(star projection)に変換される
- List は List<*>! つまり List<out Any?>! になる
Javaと同様にKotlinのジェネリクスは実行時には保持されない。つまりオブジェクトはコンストラクタに渡された引数の実際の型情報を持ち運ばない。つまり ArrayList<Integer>() は ArrayList<Character>() と区別がつかない。これはジェネリクスを考慮した is チェックを不可能にする。Kotlinは星投影(star projection)されたジェネリック型に対してのみ is チェックを許している。
if (a is List<Int>) // Error: 本当にIntのリストかどうかチェックできない
// しかし
if (a is List<*>) // OK: リストの内容について何も保証しない
Java配列
Kotlinでの配列はJavaと違って不変(invariant)である。これは Array<String> を Array<Any> に代入できないことを意味している。これにより実行時エラーを防ぐことができる。サブクラスの配列をスーパークラスの配列としてKotlinのメソッドに渡すことも禁止されている。しかしJavaのメソッドはこれを許可する(Array<(out) String>! の形のプラットフォーム型を通して)。
Javaプラットフォームではボクシングのコストを避けるため、プリミティブ型の配列が使われる。Kotlinはそれらの実装の詳細を隠すので、Javaコードのインターフェースに対しては回避方法が必要になる。こういう場合のためにプリミティブ型の配列にはそれぞれ特別なクラスが用意されている(IntArray, DoubleArray, CharArrayなどなど)。これらは Array クラスとの関係はなく、最大のパフォーマンスを発揮するJavaのプリミティブ配列にコンパイルされる。
intの配列を取るJavaメソッドがあるとしよう。
public class JavaArrayExample {
public void removeIndices(int[] indices) {
// code here...
}
}
プリミティブ型の配列を渡すために、Kotlinでは次のようにできる。
val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3) // IntArrayを返す
javaObj.removeIndices(array) // int[] をメソッドに渡す
JVMバイトコードにコンパイルされるとき、コンパイラは配列へのアクセスを最適化するので、オーバーヘッドは生じない。
val array = arrayOf(1, 2, 3, 4)
array[x] = array[x] * 2 // 実際にはget()やset()の呼び出しは生成されない
for (x in array) // イテレータは生成されない
print(x)
インデックスをループで回すときでさえ、オーバーヘッドは生じない。
for (i in array.indices) // イテレータは生成されない
array[i] += 2
最後に、in チェックもオーバーヘッドを生じない。
if (i in array.indices) { // (i >= 0 && i < array.size) と同じ
print(array[i])
}
Java可変長引数
Javaクラスはしばしば可変長引数(varargs)を使ってメソッドの引数を宣言している。
public class JavaArrayExample {
public void removeIndices(int... indices) {
// code here...
}
}
こういう場合、IntArray を渡すのに展開演算子 * を使う必要がある。
val javaObj = JavaArray()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)
可変長引数として宣言されているメソッドにnullを渡すのは、今のところ不可能。
演算子
Kotlinの operator 修飾子のような、演算子として使うと分かるようにメソッドに印を付ける方法はJavaにはない。なのでKotlinは、Javaメソッドが正しい名前とシグネチャを持っていれば、演算子オーバーロードや他の変換ルール(*invoke()*など)に使うことを許す。接中辞記法でJavaメソッドを呼び出すことはできない。
検査例外
Kotlinでは全ての例外は検査されない。つまりコンパイラはどの例外もプログラマにキャッチすることを強制しない。なのでJavaで検査例外として宣言されているメソッドを呼び出す場合、Kotlinは何も強制しない。
fun render(list: List<*>, to: Appendable) {
for (item in list)
to.append(item.toString()) // JavaならIOExceptionをキャッチすることを要求するだろう
}
オブジェクトメソッド
Java型がKotlinにインポートされるとき、全ての java.lang.Object 型の参照は Any になる。Any はプラットフォーム依存でないので、そのメンバとして toString(), hashCode() と equals() しか宣言していない。他の java.lang.Object のメンバを有効にするのに、Kotlinは拡張関数を使う。
wait()/notify()
Effective Javaの69項は、wait()とnotify()よりもコンカレンシーユーティリティーを使うように提案している。それでこれらのメソッドは Any では有効になっていない。これらを本当に呼ぶ必要があるなら、java.lang.Object にキャストする。
(foo as java.lang.Object).wait()
getClass()
オブジェクトから型情報を引き出すには、javaClass 拡張プロパティを利用する。
val fooClass = foo.javaClass
Javaの Foo.class の代わりに Foo::class.java を使う。
val fooClass = Foo::class.java
clone()
clone() をオーバーライドするには、kotlin.Cloneable を実装する必要がある。
class Example : Cloneable {
override fun clone(): Any { ... }
}
Effective Javaの第11項「cloneを注意してオーバーライドする」を忘れないこと。
finalize()
finalize() をオーバーライドするには、override キーワードなしで単に宣言するだけでいい。
class C {
protected fun finalize() {
// finalization logic
}
}
Javaのルールに従い、finalize() は private であってはいけない。
Javaクラスからの継承
最大1つのJavaクラス(と好きなだけの数のJavaインターフェース)がKotlinクラスのスーパー型になれる。
staticメンバへのアクセス
Javaクラスのstaticメンバは "コンパニオンオブジェクト" の形になる。こういう "コンパニオンブジェクト" は値として渡すことはできない。しかしメンバに明示的にアクセスすることはできる。例えば以下のように。
if (Character.isLetter(a)) {
// ...
}
Javaリフレクション
JavaリフレクションはKotlinでも動作するし、その逆も同様。java.lang.Class を通してJavaリフレクションに入り込むのに、上述したように instance.javaClass や ClassName::class.java を利用できる。
他にもJavaのgetter/setterメソッドやKotlinプロパティのバッキングフィールドを獲得することもできる。Javaフィールドには KProperty 、Javaメソッドやコンストラクタには KFunction を使う。
SAM変換
Java 8のように、KotlinはSAM2変換をサポートする。つまりKotlinの関数リテラルは、デフォルトでない1つだけのメソッドを持つJavaインターフェースの実装へと、自動的に変換される。インターフェースメソッドの引数の型とKotlin関数の引数の型が一致していれば。
SAMインターフェースのインスタンスをこんな風に作成できる。
val runnable = Runnable { println("This runs in a runnable") }
...で、メソッド呼び出しではこんな感じ。
val executor = ThreadPoolExecutor()
// Javaでのシグネチャ: void execute(Runnable command)
executor.execute { println("This runs in a thread pool") }
もしJavaクラスが別のSAMインターフェースを取る複数のメソッドを持つなら、ラムダを指定SAM型に変換するアダプタ関数を使ってどれを使うか選択する。これらのアダプタ関数も必要に応じてコンパイラが生成する。
// Runnable以外のSAMインターフェースを取るexecuteがあるので、
// アダプタ関数 Runnable を使って、Rnnnableを取るexecuteを指定。
executor.execute(Runnable { println("This runs in a thread pool") })
SAM変換はインターフェースに対してしか働かないことに注意。たとえ1つの抽象メソッドしか持たなかったとしても、抽象クラスには働かない。
またこの機能はJavaとの相互運用でしか働かないことにも注意。Kotlinはちゃんとした関数型を持ってるので、関数からKotlinインターフェースへの自動変換は必要ないし、だからサポートされていない。
JavaからKotlinのコードを呼ぶ
KotlinのコードはJavaから簡単に呼び出すことができる。
プロパティ
プロパティのgetterはget-メソッドに、setterはset-メソッドになる。
パッケージレベルの関数
パッケージ org.foo.bar 内の example.kt ファイルで宣言された全ての関数とプロパティは、org.foo.bar.ExampleKt という名前のJavaクラスに所属する。
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.ExampleKt.bar();
生成されるJavaクラスの名前は @JvmName アノテーションで変更することができる。
@file:JvmName("DemoUtils")
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.DemoUtils.bar();
同じJavaクラス名(同じパッケージで同じ名前または同じ@JvmNameアノテーション)で生成されるファイルが複数ある場合、通常はエラーになる。しかしコンパイラにはそれらを1つにまとめたファサードクラスを生成する機能がある。これを有効にするには全てのファイルに @JvmMultifileClass アノテーションを付ける。
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun foo() {
}
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun bar() {
}
// Java
demo.Utils.foo();
demo.Utils.bar();
フィールド
もしKotlinのプロパティをJavaのフィールドとして外に出す必要があるなら、@JvmField アノテーションを付ける。そのフィールドのアクセス制限はプロパティと同じになる。このアノテーションを付けられるのは、バッキングフィールドを持ち、privateでなく、open でも override でも const でもなく、デリテートされたプロパティでもない場合。
class C(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(C c) {
return c.ID;
}
}
staticなメソッドとフィールド
上述したように、Kotlinはパッケージレベルの関数をstaticメソッドとして生成する。またコンパニオンオブジェクトや名前付きオブジェクトに定義された関数に @JvmStatic アノテーションが付けられた場合も、staticメソッドとして生成する。例を挙げよう。
class C {
companion object {
@JvmStatic fun foo() {} // コンパニオンオブジェクトの関数に@JvmStatic指定
fun bar() {}
}
}
これで foo() はJavaでstaticになる。*bar()*はならない。
C.foo(); // うまく動作する
C.bar(); // error: staticメソッドでない
名前付きオブジェクトも同じ。
object Obj {
@JvmStatic fun foo() {}
fun bar() {}
}
Javaからこう呼び出せる。
Obj.foo(); // うまくいく
Obj.bar(); // error
Obj.INSTANCE.bar(); // 動作する。シングルトンインスタンスを通して呼び出す。
Obj.INSTANCE.foo(); // 同様に動作する。
またオブジェクトやコンパニオンオブジェクトに定義されたpublicプロパティも、const 修飾されたトップレベルプロパティと同様に、Javaではstaticフィールドになる。
object Obj {
val CONST = 1
}
const val MAX = 239
Javaからこう呼び出せる。
int c = Obj.CONST;
int d = ExampleKt.MAX;
@JvmNameによるシグネチャ衝突の回避
ときどきKotlinで、バイトコードでは異なるJVM名が必要な名前付き関数を持つことがある。典型的な例は 型消去 によって起こる。
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
これら2つの関数は一緒には定義できない。なぜならこれらのJVMシグネチャは同じ(filterValid(Ljava/util/List;)Ljava/util/List;)だから。もしKotlinで本当にこれらを同じ名前にしたいなら、片方(または両方)に @JvmName アノテーションで異なるJVM名を付けてやる付けてやる。
fun List<String>.filterValid(): List<String>
// こっちはKotlinでは同じ名前で使うけど、JVMバイトコードではfilterValidIntって名前にする
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
これでKotlinからはどちらも同じ名前 filterValid でアクセスできる。けどJavaからはfilterValid と filterValidInt になる。
同じトリックは getX() があるのにプロパティ x が必要な時にも使える。
// getX()があるので、何も指定しないとxのgetterと名前が衝突する
val x: Int
@JvmName("getX_prop") // getterのJVM名に別の名前を指定する
get() = 15
fun getX() = 10
オーバーロードの生成
デフォルト値を持ったKotlinのメソッドを書く場合、通常Javaからは全てのパラメータを持った1つのメソッドとして見える。もしJavaから複数のオーバーロードとして見えるようにしたいなら、@JvmOverloads アノテーションが使える。
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
...
}
デフォルト値を持つそれぞれのパラメータに対して、そのパラメータの右側にあるパラメータを除いたオーバーロードを1つずつ生成する。この例では、次のメソッドが生成される。
// Java
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }
このアノテーションはコンストラクタやstaticメソッドなどに対しても使える。インターフェースに定義されたものも含めて抽象メソッドに対しては使えない。
コンストラクタで記述したように、全てのコンストラクタ引数がデフォルト値を持つクラスでは、publicな引数なしコンストラクタが生成されることに注意。これは @JvmOverloads を指定しなくてもそうなる。
検査例外
前述した通り、Kotlinに検査例外はない。なので通常Kotlin関数のJavaシグネチャは例外を投げるとは宣言されない。こんなKotlinの関数があるとする。
package demo
fun foo() {
throw IOException()
}
それをJavaから呼んで例外をキャッチしたいとする。
try {
demo.Example.foo();
}
catch (IOException e) { // error: foo()は検査例外IOExceptionをthrowsリストの中に宣言していない
// ...
}
foo() は IOException を宣言していないので、Javaコンパイラからエラーメッセージを受け取る。この問題に対処するには、Kotlin側で @Throws アノテーションを使う。
@Throws(IOException::class)
fun foo() {
throw IOException()
}
Null安全
JavaからKotlinの関数を呼ぶとき、nullでないパラメータにnullを渡せてしまう。なのでKotlinの方で、nullでないことを期待する全てのpublic関数に実行時チェックを挿入している。これによってJavaコードは即座に NullPointerException を受け取ることになる。
ジェネリクスの変性
Kotlinのクラスが宣言側変性を使うとき、Javaコードから見える利用方法には2つのオプションがある。次のクラスとそれを使う2つの関数を見てみよう。
// T型の値を格納する箱。Tを返すメソッドしか持たないので <out T> にできる。
// コンストラクタ引数にvalを付けているので、読み取り専用プロパティvalueが自動生成される。
class Box<out T>(val value: T)
// Boxに入れる型として、Baseインターフェースを実装したDerivedクラスを用意
interface Base
class Derived : Base
// Defived型の値をBoxに入れて返す
fun boxDerived(value: Derived): Box<Derived> = Box(value)
// BoxからBase型(Derivedじゃないよ)の値を取り出す
fun unboxBase(box: Box<Base>): Base = box.value
これらをJavaに翻訳する自然な方法はこうなるだろう。
Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }
問題はKotlinでは unboxBase(boxDerived("s")) と書けても3、Javaでは書けないということ。なぜならJavaではBoxは型Tに対して 不変(invariant) だから。つまり Box<Derived> は Box<Base> のサブ型ではない。これをJavaで動作させるには unboxBase を次のように定義しなければならない。
Base unboxBase(Box<? extends Base> box) { ... }
ここでは宣言側変性を利用側変性でエミュレートするために、Javaのワイルドカード( ? extends Base )を使っている。だってJavaにはこれしかないから。
KotlinのAPIをJavaで動作させるために、パラメータとして現れる場所で、共変として定義された Box に対して Box<Super> を Box<? extends Super> として生成している。戻り値に対してワイルドカードは生成しない。そうしないとJavaクライアントがそれらを扱わないといけなくなるから(そしてJavaのコーディングスタイルに反する)。そんなわけで、先ほどの例は実際は次のように翻訳される。
// 戻り値の型 - ワイルドカードは使わない
Box<Derived> boxDerived(Derived value) { ... }
// パラメータ - ワイルドカードを使う
Base unboxBase(Box<? extends Base> box) { ... }
注:パラメータの型がfinalの場合4、通常はワイルドカードを生成する箇所はない。なのでそれを取る場所に関係なく Box<String> は常に Box<String> になる。
デフォルトでは生成されない場所にワイルドカードが必要なら、@JvmWildcard アノテーションが使える。
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// これは次のように翻訳される
// Box<? extends Derived> boxDerived(Derived value) { ... }
逆に生成される場所でワイルドカードがいらない場合は、@JvmSuppressWirldcard が使える。
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// これは次のように翻訳される
// Base unboxBase(Box<Base> box) { ... }
Nothing型の翻訳
Nothing 型5は特別で、Javaには自然に対応するものがない。実際、java.lang.Void を含む全てのJava型の参照は値としてnullを受け入れるが、Nothing はそれさえも受け入れない。なのでこの型はJavaの世界では正確に表現することはできない。これが引数の型に Nothing を使う場所でKotlinが原型(raw type)を生成する理由である。
fun emptyList(): List<Nothing> = listOf()
// これは次のように翻訳される
// List emptyList() { ... }