Kotlin が Android の公式言語になることが Goole I/O 2017 で発表されました。これから Kotlin を始める Java プログラマが多くなると思うので、本投稿では Java プログラマが Kotlin でつまづきがちなところについて説明します。
本投稿は単独で理解できるように書いていますが、↓の連載の第二弾です。 Kotlin の基礎的な構文は理解していることを前提としているので、 Kotlin の基礎については "Javaとほぼ同じところ" を御覧下さい。
- Javaとほぼ同じところ
- 新しい考え方が必要でつまづきがちなところ ←この投稿で扱う内容
- Kotlinならではの便利なこと
新しい考え方が必要でつまづきがちなところ
新しい概念を学ぶときには、何ができるのかだけでなく、どうしてそうなっているのかがわからないとそれをうまく使いこなすことができません。 本節では、 Kotlin が Java と違っており新しい考え方が必要になることの中で僕が特に重要だと考える点について、 「どうしてそうなっているのか」をベースに「何ができるのか」を説明します。
Smart Cast
一般的に広く浸透している考えかわかりませんが、僕が感じているところでは Java において instanceof
は良くないものという風潮があると思います。 instanceof
で分岐してからキャストするのは最後の手段で、ポリモーフィズムで何とかするのが望ましいという考えです。しかし、 instanceof
が良くないことに対して、その解決策がポリモーフィズムだというのは飛躍があります。他の解決策があっても良いはずです。
Java の instanceof
の問題は、型のチェックとキャストを同時にできないことです。
// Java
Animal animal = new Cat();
if (animal instanceof Cat) {
Cat cat = (Cat)animal; // ダウンキャスト
cat.catMethod(); // `Cat` のメソッドを使う処理
}
一度 instanceof
で animal
が Cat
であると確認したにも関わらず、その後 Cat
へのダウンキャストが必要になってしまいました。ダウンキャストは安全でない処理なので、できれば使いたくありません。間違えてここで (Dog)animal
としてしまうと実行時に ClassCastException
が発生してしまいます。
そもそもおかしいのは、直前の if
文で animal
が Cat
であることを確認しているのに、明示的にキャストしなければならないことです。その if
のスコープでは animal
は Cat
であることが保証されているのだから、コンパイラが animal
を Cat
として扱ってくれれば良い話です。それをしてくれるのが Kotlin の Smart Cast です。
//Kotlin
val animal = Cat()
if (animal is Cat) { // Smart Cast
animal.catMethod() // `Cat` のメソッドを使う処理
}
Smart Cast 自体はちょっとした機能のように見えますが、 Smart Cast があることで Kotlin では( Smart Cast による)キャストを普通に使います。特に後で説明する Null Safety には欠かせませんし、次回説明する Sealed Class は Smart Cast とも相性が良いです。型を調べて分岐するのは良くないことという考え方からの脱却が必要です。
Null Safety
Java プログラマが一番困惑しそうで、かつ、僕が Kotlin で最も素晴らしいと思う言語仕様が Null Safety です。
Java では↓のようなコードで NullPointerException
が発生する可能性があります。
// Java
String s = foo(); // null かもしれない
int l = s.length(); // null だったら実行時に NullPointerException
しかし、 Kotlin で同様のコードを書くとコンパイルエラーになります。
// Kotlin
val s = foo() // null かもしれない
val l = s.length // コンパイルエラー
実行時にエラーになるかもしれないコードを Kotlin コンパイラが検出し、コンパイル時に教えてくれるのです。一般的に、 エラーの検出は遅れれば遅れるほど対処が困難になります。実行時エラーはテストで検出できなければ潜在バグとしてプロダクトに埋めこまれてリリースされてしまうかもしれません。コンパイルエラーであれば解消されないままリリースされるということはありません。
このコンパイルエラーは null
チェックをすることで消せます。
// Kotlin
val s = foo() // null かもしれない
if (s != null) {
val l = s.length // OK
}
ここで Smart Cast が利いてきます。 null
チェックで s
が null
でないことが保証され、この if
のスコープの中では s
を null
でない String
として扱えるのです。
では、この場合、 Smart Cast は何型を何型にキャストしているのでしょうか?
Kotlin では null
の可能性のある値の型と、 null
でないことが保証されている値の型を区別して扱います。前者を Nullable Type 、後者を Non-null Type と呼びます。
Java と同じように String
などと書いた場合には Non-null Type になります。この場合、 null
を代入することはできません。
// Kotlin
val s: Sting = null // コンパイルエラー
元の型に ?
を付けて、 String?
などと書くと Nullable Type になります。 Nullable Type には null
を代入できます。
// Kotlin
val s: String? = null // OK
注意が必要なのは、 String
と String?
はまったく別の型だということです。 String?
型の値に対して String
のメソッドを呼び出すことはできません。 最初の例で s.length
がコンパイルエラーになるのは s
が null
かもしれないからではなく、 String?
型には length
などというプロパティは存在していない からです。 length
を持っているのは String
であって String?
ではありません。
そして、もう一つ重要なのは、 String
は String?
のサブタイプであるということです。 そのため、 String
型から String?
型への代入はできますが、 String?
型から String
型への代入はできません。
// Kotlin
val a: String = "abc"
val b: String? = a // OK
val c: String? = "xyz"
val d: String = c // コンパイルエラー
これは、 Cat
から Animal
に代入できるけど逆はできないのと同じです。
// Kotlin
val a: Cat = Cat()
val b: Animal = a // OK
val c: Animal = Animal()
val d: Cat = c // コンパイルエラー
では、元の話に戻って、 null
チェックに伴う Smart Cast は何型を何型にキャストしたのでしょうか。答えは、 String?
を String
にキャストした、です。
// Kotlin
val s: String? = foo() // ここでは `String?`
if (s != null) { // このスコープ内では `s` は `String`
val l = s.length
}
つまり、 s != null
は s is String
のように働くわけです。実際、↓にように書いても同じ結果が得られます。
// Kotlin
val s: String? = "abc"
if (s is String) { // null チェックではなく is で Smart Cast
val l = s.length
}
このような仕組みで、 Kotlin は NullPointerException
を防止しています。 Java を書いていれば NullPointerException
に遭遇するのは日常茶飯事だと思います。それが起こらない Null Safety は素晴らしいと思いませんか?
!!
Null Safety は素晴らしいですが、型は万能ではありません。
たとえば、 Kotlin の List
(正確には Iterable
)には max
というメソッドがあります。これは、 List
の中の最大の要素を返してくれるというものです。
// Kotlin
listOf(2, 7, 5, 3).max() // 7
しかし、 List<Int>
の max
メソッドの戻り値は Int
ではありません。 Int?
です。なぜなら、空の List
には最大の要素は存在しないからです。
// Kotlin
listOf<Int>().max() // null
もし今、必ず一つ以上の要素を含む List<Int>
があり、その最大の要素を得て二乗したいとします。 Java ならば何も考えずに max
の戻り値を二乗すれば OK です。
// Java
List<Integer> list = ...; // 必ず一つ以上の要素を含む
int maxValue = Collections.max(list);
int square = maxValue * maxValue; // OK
しかし、 Kotlin ではうまくいきません。
// Kotlin
val list: List<Int> = ...; // 必ず一つ以上の要素を含む
val maxValue = list.max()
val square = maxValue * maxValue // コンパイルエラー
なぜなら、↑の maxValue
の型は Int
ではなく Int?
だからです。 Int?
同士を計算する *
という演算子は存在しません。
これを解決するために null
チェックをするとひどいコードになります。
// Kotlin
val list: List<Int> = ...; // 必ず一つ以上の要素を含む
val maxValue = list.max() // maxValue は `Int?` 型
if (maxValue != null) { // このスコープ内では `maxValue` は `Int` 型
val square = maxValue * maxValue // OK
} else {
throw RuntimeException("Never reaches here.") // 絶対にここには来ない
}
else
節はいるのかと思うかもしれませんが、これを書いておかないと、何らかのバグで万が一 list
が空だった場合にそのエラーを握りつぶすことになり危険です。 エラーを握りつぶすのは実行時エラーよりもバグの発見を遅らせる最悪の行為です。
しかし、↑のような無駄な null
チェックを毎回書くのは面倒です。これは Java では不要だった処理です。もし Null Safety のために毎回こんなコードを書かないといけないのだったら、とても Null Safety が良いものだとは言えません。それに、面倒な処理は省略されがちです。 else
節を書かない人も出てくるでしょう。そうするとエラーが握りつぶされて最悪です。
この問題がやっかいなのは、これを型で解決することが困難なことです。 Kotlin には「必ず一つ以上を要素を含むリスト」のような型はないですし、たとえあったとしてもそれでは解決できないケースもあります。
// Kotlin
val list: MutableList<Int> = mutableListOf() // この時点では空
...
list.add(42)
val maxValue = list.max() // ここでは絶対に空でないが `maxValue` は `Int?`
↑の最後の行では list
は絶対に空でないですが、 list
の実体はあくまで MutableList
のインスタンスです。たとえ NonEmptyMutableList
(空でないリスト)のような型があったとしても、 MutableList
のインスタンスが空でないときに NonEmptyMutableList
のインスタンスになってくれるわけではありません。
このような問題に対処するのが !!
という後置演算子です。 !!
は T?
を T
に変換してくれます。ただし、値が null
だった場合は例外を投げます。
// Kotlin
val list: List<Int> = ...; // 必ず一つ以上の要素を含む
val maxValue = list.max()!! // `maxValue` は `Int` 型
val square = maxValue * maxValue // OK
!!
は Non-null 化に失敗すると NullPointerException
を投げ、 Null Safety の安全性に穴を開けます。決して乱用してはいけません。↑の例のように null
でないことがわかっていて、絶対に失敗しない場合にだけ !!
を使います1。型の上では Nullable だけど、処理の流れ上、絶対に null
でないと言い切れる場合にだけ使うべきです。
- The !! Operator: https://kotlinlang.org/docs/reference/null-safety.html#the--operator
?.
?.
を使えば T?
型の値に対して、 null
でなかった場合だけ T
のメソッドを呼び出すことができます。
// Kotlin
val s: String? = ...
val l: Int? = s?.length
↑の l
が Int?
なことに注意して下さい。 s
が null
の場合、 s?.length
の結果は null
となります。そのため、 length
の戻り値は Int
ですが、 s?.length
では Nullable Type である Int?
となります。
?.
は、 null
になるかもしれない処理を複数連ねて書く場合に、 foo?.bar?.baz
のように書けるので便利です。しかし、 ?.
を乱用するとあまり良い結果を招きません。特に、 Nullable のまま値を取り回すのはオススメしません。 いざ値を使おうとして null
だった場合に null
の発生源がわからないからです。 null
になったのがバグによるものだった場合に、 null
の発生箇所がわからないのでバグフィックスが大変です。そのような事態を防ぐために、 Nullable な値は早めに null
チェックして Non-null 化してしまうのがオススメです。
?:
値がなければ初期値を埋めたいというのはよくあるケースです。そんなときは ?:
演算子が便利です。 ?:
は foo ?: bar
の形で書き、 foo
が null
だった場合は代わりに bar
となります。
たとえば、ユーザーの名前が設定されていなかった場合に No Name と表示したければ↓のように書けます。
val name: String? = ...
val displayName: String = name ?: "No Name"
ここで重要なのは、 displayName
の型が String?
ではなく String
なことです。 ?:
は name
が null
だった場合に代わりの文字列 "No Name"
を使うので、必ず null
でなくなります。そのため、結果の型は String?
ではなく String
となります。
更に便利なのは、 ?:
の右辺に return
などの文を書けることです。 if
による null
チェックでは書きづらい、代入と同時に早期脱出を実現できるので重宝します。
fun foo(list: List<String>) {
val length: Int = list.min()?.length ?: return // `list` が空なら早期脱出
// length を使う処理
}
?.let
↓のコメントをいただいたので、そのような場合に使える方法を簡単に説明します。
kitakokomo Nullableについては「listOf(1,2)のlist[0]+5は問題なく動くのにmapOf(1 to 11, 2 to 22)のmap[1]+5がNullableで動かんのは悲しい」という話が2chで出てたな
?.
は foo?.bar
のような形でプロパティやメソッドをコールするのには使えますが、 Nullable な値を引数や演算子のオペランドとして渡すことはできません。そんなときに使えるのが let
メソッドです( let
メソッドについて詳しくは次回説明予定なので、ここでは、こんな方法があるという紹介に留めます)。
// Kotlin
map[1]?.let { it + 5 }
このようにすれば、 map[1]
の戻り値である Int?
と 5
を足した結果を Int?
として得ることができます。しかし、可読性がよくないのと、乱用すると Nullable のまま値を取り回すことにもつながりかねないので、利用は最小限にとどめて、計算の結果はすぐに null
チェックするか ?:
するかして Non-null 化することをオススメします。
余談ですが、(これも次回説明予定ですが) Kotlin の演算子はメソッドのシンタックスシュガーなので、たし算したいだけであれば +
の本体である plus
メソッドを使って↓のようにも書けます。
map[1]?.plus(5)
inline
と Non-local return
Null Safety と並んで Java プログラマが面食らうのではないかと思うのがインライン展開による Non-local return です。
Kotlin では inline
修飾子が付けられたメソッドはインライン展開されます。インライン展開とは、コンパイラがメソッドの実装を呼び出し側に展開することでメソッドコールのオーバーヘッドを削減する仕組みです。
↓はインライン展開のイメージです。
// Kotlin
// インライン展開前
inline fun square(value: Int): Int {
return value * value
}
for (i in 1..100) {
println(square(i))
}
// インライン展開後
for (i in 1..100) {
println(i * i)
}
しかし、 Kotlin で重要なのは、 inline
メソッドの引数に渡されたラムダ式も inline
展開されることです。
// Kotlin
// インライン展開前
inline fun forEach(list: List<Int>, operation: (Int) -> Unit) {
for (element in list) {
operation(element)
}
}
val list: List<Int> = ...
forEach(list) { element ->
println(element)
}
// インライン展開後
val list: List<Int> = ...
for (element in list) {
println(element)
}
これによって、 forEach
, map
, filter
, fold
( fold
は Java の初期値を与える reduce
に相当)などを使ってもループで書いたのと同様のパフォーマンスを実現できます。↑の forEach
は自作しましたが、標準ライブラリでも List
等の持つ forEach
, map
, filter
, fold
などのメソッドはすべて inline
になっています。
Kotlin の inline
はそのようにラムダ式と併せて使用されることが想定されており、↑の square
のような普通の関数やメソッドを inline
化しようとするとコンパイラが↓のような警告を発します。
Expected performance impact of inlining 'public inline fun square(value: Int): Int defined in root package' can be insignificant. Inlining works best for functions with lambda parameters
ここまでは特別な話ではありません。 Java プログラマが一番戸惑うと思うのは、 Non-local return と呼ばれる仕組みです。 Non-local return とは、ラムダ式の中からラムダ式の外側のメソッドを return
させたり、ラムダ式の外側のループを break
したりできる仕組みです。
これも例で説明します。 List<Boolean>
のすべての要素の論理積( &&
)をとるメソッド and
を考えてみましょう。たとえば、 [true, true, true]
なら and
の結果は true
ですが、 [true, false, true]
のように一つでも false
が混ざっていたら結果は false
となります。
拡張 for
文で and
メソッドは簡単に実装できます。
// Java
static boolean and(List<Boolean> list) {
for (boolean x : list) {
if (!x) {
return false;
}
}
return true;
}
しかし、 for
とほぼ等価の forEach
を使っても Java では同じようなことはできません。ラムダ式の中からラムダ式の外のメソッドの return
をすることはできないからです。
// Java
static boolean and(List<Boolean> list) {
list.forEach(x -> {
if (!x) {
return false; // コンパイルエラー
}
});
return true;
}
Kotlin ではこれが可能です。
// Kotlin
fun and(list: List<Boolean>): Boolean {
list.forEach { x ->
if (!x) {
return false // OK
}
}
return true
}
これが Non-local return です。
ラムダ式の中からラムダ式の外側を return
させられるなんて気持ち悪いですね。どうしてこんなことができるのでしょうか。これは、 forEach
がインライン化されているからです。↑のコードのインライン展開後のイメージは↓です。
// Kotlin
fun and(list: List<Boolean>): Boolean {
for (x in list) {
if (!x) {
return false
}
}
return true
}
これなら return
できるのも不思議ではないですね。元々ラムダ式の中にあった if
の中の return
が、ラムダ式の外側にあった and
の return
になっています。
Kotlin ではラムダ式の内部で書いた return
は必ずラムダ式の外側に対して作用します。ラムダ式自体の戻り値を返すために return
を使うことはできません。ラムダ式の戻り値はラムダ式の内部で最後に評価された式の値となります。
では、 inline
でないメソッドに渡されたラムダ式の内部で return
を書くとどうなるでしょうか?インライン展開されないので、ラムダ式の外側に対して作用することはできません。その場合、コンパイルエラーになります。
// Kotlin
// `inline` でない場合
fun foo(f: () -> Int): Int {
...
}
fun bar(): Int {
return foo {
if (flag) {
return 0 // コンパイルエラー
}
42
}
}
↑の例は、↓のように foo
が inline
なら問題ありません。
// Kotlin
// `inline` の場合
inline fun foo(f: () -> Int): Int {
...
}
fun bar(): Int {
return foo {
if (flag) {
return 0 // OK: `bar` の `return`
}
42
}
}
ラムダ式の中の return
は常にラムダ式の外側に作用する というのは Java を含む他の言語から考えるとなかなか思い切った仕様ですが、 Kotlin のラムダ式と付き合う上ではここを押さえておかないとハマります。
Non-local return は最初は気持ち悪いですが使い方によっては便利です。たとえば、 Java では { }
で簡単に無名スコープを作れますが Kotlin では { }
はラムダ式に割り当てられており無名スコープが作れません。無名スコープがないと、変数名の重複を防ぐためにスコープを細かく切りたいときなどに困ってしまいます。 Kotlin ではそんなときに run
を使います。
// Java
for (int x : list) {
{ // 無名スコープ
int a = 2;
...
if (x < a) {
break;
}
}
{
int a = 3; // スコープが異なるので `a` を作れる
...
}
}
// Kotlin
for (x in list) {
run { // 無名スコープの代わり
val a = 2
...
if (x < a) {
break // ラムダ式の中だけど外のループを `break` できる
}
}
run {
val a = 3 // スコープが異なるので `a` を作れる
...
}
}
このように、 Non-local return を使えばカスタム制御構文のようなものも作れます。乱用すると可読性を損なうかもしれないですが、新しい制御構文がほしくなったような場合に言語仕様を肥大化させるのではなく、言語の仕組みで解決できるというのは好ましい特性です。 Non-local return と上手に付き合っていきましょう。
- Inline Functions: https://kotlinlang.org/docs/reference/inline-functions.html
匿名関数
ラムダ式の中で return
できないなら、早期脱出したいようなケースはどうすればよいでしょうか?
// Kotlin
numbers.forEach { number ->
if (number % 2 == 1) {
// ここで早期脱出したいけどどうすればいい?
}
...
}
こういう場合にはラムダ式ではなく 匿名関数( Anonymous Function ) を使うと良いです。
// Kotlin
numbers.forEach(fun(number: Int) {
if (number % 2 == 1) {
return // 早期脱出
}
...
})
ラムダ式と違って、匿名関数の中の return
は匿名関数自体の return
を意味します。
- Anonymous Functions: https://kotlinlang.org/docs/reference/lambdas.html#anonymous-functions
Any
と Any?
Java ではルートクラスは Object
でした。 Kotlin では Any
がそれに当たります。 Kotlin から Java のメソッドを使う場合、 Java で引数や戻り値に Object
が使われている箇所は Kotlin ではすべて Any
に見えるようになっています。しかし、これは単純に名前が変わったという話ではありません。 Kotlin でも Java の Object
を使うことは可能です2。 Kotlin では Object
はルートクラスではなく、 Any
は Object
のスーパークラスになります。
何のために Any
が必要なのでしょうか。 Java ではプリミティブ型から Object
に代入するには一度 Integer
などのラッパークラスにボクシングする必要がありました。 int
と Object
の間に型の上での派生関係は存在しません。しかし、 Kotlin の Int
などはクラスとして振る舞うため、 Int
は Any
のサブタイプです。そのため、 Int
クラスのインスタンスをそのまま Any
型の変数に代入するようなことができます。
// Kotlin
val a: Int = 42
val b: Any = a
if (b is Int) { // 直接 `Int` のインスタンスかチェック
// このスコープでは `b` を `Int` として使える
println(b + 1) // 43
}
ただし、このことは実行時に内部的にボクシングが行われないという意味ではありません。直接的にドキュメントで言及されている箇所は見つけられませんでしたが、 Int
などの Any
への代入は、原理的にも、 Nullable Type への代入に関する挙動から類推しても、内部的にはボクシングされるものと思われます。 Java と違って構文上はボクシングを意識せずに使えますが3、 Any
への Int
などの代入はパフォーマンス上のオーバーヘッドを伴うことには注意して下さい。
Java ではクラス宣言時に extends
を使ってスーパークラスを明示的に指定しなければ、そのクラスは Object
を継承していることになりました。同様に、 Kotlin ではスーパークラスを指定しなければ Any
を継承します。
しかし、ややこしいのは Kotlin には Any
にも代入できない値がある ことです。それは null
です。つまり Any
は Int?
など、 Nullable Type のスーパータイプではないということです。 Kotlin では、型ヒエラルキーのルートに存在するトップタイプは Any?
です。 Any?
に代入できないものはありません。 Any
といういかにも何でも代入できそうな名前なのにややこしいですが、型ヒエラルキーを図で表すと次のようになります4。
[Any?]
|
+---------------+
| |
[Any] [その他のすべてのNullable]
|
[その他のすべてのNon-null]
これを理解しておかないと辛いのは、ジェネリクスを使う場合です。 ?
のことをただの null
を入れられるようにするための記号だと理解していると、↓のような実装をしてしまいがちです。
// Kotlin
// Nullable Type も渡せるようにしたい
fun <T> id(x: T?): T? {
return x
}
しかし、 ↑の ?
は必要ありません。 T
が Nullable Type を表すことができるからです。型パラメータは制約を付けなければ Nullable Type を含めてすべての型のプレースホルダーとして働きます(デフォルトで upper bound が Any?
)。
// Kotlin
// これでも Nullable Type の値を渡せる
fun <T> id(x: T): T {
return x
}
そして、もし Non-null Type の値しか渡せないようにしたいなら↓のようになります。
// Kotlin
// Nullable Type は渡せない
fun <T: Any> id(x: T): T {
return x
}
これは、先程の型ヒエラルキーの図の左の Any
の枝の先にある型しか T
としてとれないようにするという制約です( upper bound が Any
)。 <T: Animal>
などと制約をかけるのと何の違いもありません。もし Nullable Type と Non-null Type のサブタイピングを理解せずに ?
が null
を入れられるようにするための記号と理解していると、なぜ <T: Any>
で T
が Non-null Type を表すようになるのか理解できないでしょう。
- Inheritance: https://kotlinlang.org/docs/reference/classes.html#inheritance
-
Any
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/
Nothing
と Nothing?
Any
や Any?
と対になる存在が Nothing
です。 Nothing
はボトムタイプと呼ばれるもので、全ての型のサブタイプになります。つまり、すべてのクラスを多重継承しており、どんなメソッドでも呼び出せるということです。当然そんな型は実現不可能なので、 Nothing
のインスタンスは存在しません。そんな空想上の型のようなものを考えて何の意味があるのでしょうか?
Kotlin ではメソッドの戻り値の型として Nothing
を指定することができます。しかし、前述の通り Nothing
のインスタンスは存在しません。戻り値の型として Nothing
型を指定しているのに、 Nothing
型の値を return
できなければコンパイルエラーになってしまいます。コンパイルを通すにはメソッドの中で例外を throw
するしかありません。そのため、 Kotlin では Nothing
を返すメソッドは常に失敗し例外を throw
することを表します。
これが便利なのは、コンパイラの網羅性チェックに対応するときです。たとえば、必ず失敗して例外を throws
する alwaysFail
というメソッドがあったとします。次のようなコードを考えてみましょう。
// Java
static void alwaysFail() {
throw new RuntimeException();
}
static String foo(int bar) {
switch (bar) {
case 0:
return "A";
case 1:
return "B";
case 2:
return "C";
default:
alwaysFail();
}
} // コンパイルエラー
alwaysFail
は必ず例外を throw
するのでこのメソッドの末尾に到達することはありません。しかし、構文上は default
のケースでメソッドの末尾に到達してしまうので、 foo
は戻り値を return
していないフローがあるということでコンパイルエラーとなります。
Kotlin では alwaysFail
の戻り値を Nothing
型にすることで、コンパイラがその後の処理が実行されることはないと判断し、↑のようなケースでもコンパイルが通るようになります。
// Kotlin
fun alwaysFail(): Nothing {
throw RuntimeException()
}
fun foo(bar: Int): String {
when (bar) {
0 -> return "A"
1 -> return "B"
2 -> return "C"
else -> alwaysFail()
}
}
便利ですね!
ところで、これは余談気味ですが、 Any?
があるなら Nothing?
もあるはずです。 Nothing?
は何を意味のするでしょうか?
Nothing?
は Nothing
または null
であることを表します。 Nothing
のインスタンスは存在しないので Nothing?
の唯一のインスタンスは null
です。つまり、 Nothing?
は null
のみを入れられる型です。ある意味、 null
は Nothing?
クラスのインスタンスのようなものです(もちろん実際には Nothing?
というクラスは存在しません)。
また、 Nothing?
はすべての Nullable Type のサブタイプでもあります。図で表すと↓のようになります。
[Any?]
|
+---------------+
| |
[Any] [その他のすべてのNullable]
| |
[その他のすべてのNon-null] |
| |
| [Nothing?]
| |
+-------+-------+
|
[Nothing]
Nothing?
を使う機会はあまりないと思いますが、 null
のみを入れられる型を使いたくなったら Nothing?
を使うと覚えておくと役に立つことがあるかもしれません。
ジェネリクスと変性
Java のワイルドカードや変性について知識がないと↓の内容は理解できませんので、その場合はまずこちらを読むことをオススメします。
Java では List<? extends Animal>
のようにワイルドカードを使って、ジェネリックな型を使う側で変性をコントロールします。これは Use-site variance と呼ばれる方法です。一方で、 C# や Scala は Java の反省を活かして Declaration-site variance と呼ばれる方法を採用しました。 Declaration-site variance では、名前の通りその型を使う側ではなく、宣言する側で変性をコントロールします。 Kotlin はどちらもできるようになっていますが、基本的には Declaration-site variance だけでなんとかできるように設計するのが良いと思います。 Kotlin の標準ライブラリでも、至るところで Declaration-site variance が使われています。
Declaration-site variance の具体例を見てみましょう。
たとえば、 Iterable<T>
は型パラメータ T
を戻り値でしか使いません。それなら使う側で Iterable<? extends Animal>
(Java) / Iterable<out Animal>
(Kotlin) のようにしなくても最初から T
について共変になっていてくれれば便利です。
一方で、 Comparable<T>
は型パラメータ T
を引数でしか使いません。それなら使う側で Comparable<? super Cat>
(Java) / Comparable<in Cat>
(Kotlin) のようにしなくても最初から T
について反変になっていてくれれば便利です。
これを実現するために、 Kotlin の Iterable
や Comparable
では型パラメータを宣言するときに out
や in
を付けて共変性・反変性を指定しています。これが Declaration-site variance です。
// Kotlin
interface Iterable<out T> {
...
}
interface Comparable<in T> {
...
}
このように型パラメータの宣言時に out
や in
を付けることで、利用時には何も付けなくても常に ? extends
/ out
や ? super
/ in
が付いているような挙動になります。また、たとえば Iterable<out T>
で T
をメソッドの引数に使おうとするとおかしいので5、コンパイラがコンパイルエラーとして教えてくれます。
クラスやインタフェースの設計者は、利用者よりもその型についてよく考えているので適切に変性を設定できるはずです。逆に、宣言時に変性について考えれば、変性をないがしろにした変な型を設計することも防げます。また、利用時に ? extends
/ out
など毎回を付けるのも面倒です。 Declaration-site variance はそのような Use-site variance の抱えていた問題に対するアプローチです。
- Declaration-site variance: https://kotlinlang.org/docs/reference/generics.html#declaration-site-variance
プリミティブ型の明示的変換
Java では↓は問題のないコードです。
// Java
int x = 42;
double y = x; // OK
しかし、 Kotlin ではコンパイルエラーになります。
// Kotlin
val x: Int = 42
val y: Double = x // コンパイルエラー
通常、 A
型の値を B
型の変数に代入できるためには、 A
が B
のサブタイプである必要があります。しかし、 Kotlin の Int
は Double
のサブクラスではありません。型の派生関係がないのに代入できる場合には、暗黙の型変換が行われているということになります。 Kotlin はたとえ Int
から Double
であっても暗黙の型変換は行われません。これは、 Java に慣れていると最初は面倒に感じられるかもしれませんが、型の扱いが適当になってしまわないので僕は良い仕様だと思います。
たとえば、 Java で↓を実行すると short
がオーバーフローして -32768
と表示されます。
// Java
short a = Short.MAX_VALUE;
a++;
System.out.println(a); // -32768
では、↓では何と表示されるでしょうか?
// Java
short a = Short.MAX_VALUE;
short b = 1;
System.out.println(a + b);
なんとこのケースでは 32768
となります。これは、 a + b
のたし算が実行される前に a
と b
が暗黙的に int
に変換されるからです。暗黙的型変換は仕様をよく理解していないとこのような想定外の挙動を生みがちです。僕は、暗黙的型変換よりも簡潔な明示的型変換の方法を提供する方が良いアプローチだと考えています。
Kotlin では、 Int
から Double
に明示的に型変換するには toDouble
メソッドを使います。
// Kotlin
val x: Int = 42
val y: Double = x.toDouble() // OK
また、 Double
から Int
のように Java で明示的なキャストが必要なケースでも、 Kotlin ではキャストではなくメソッドで型変換するので注意して下さい。これは、 Int
と Double
には型の派生関係がないためです。
// Java
double x = 42.0;
int y = (int)x; // OK
// Kotlin
val x: Double = 42.0
val y: Int = x as Int // ClassCastException
// Kotlin
val x: Double = 42.0
val y: Int = x.toInt() // OK
まとめ
本投稿では、 Kotlin を始める Java プログラマにとって、新しい考え方が必要になるのでつまづきがちだと思われる点について説明しました。
- Smart Cast
- Null Safety
-
inline
と Non-local return -
Any
とAny?
-
Nothing
とNothing?
次の投稿: JavaプログラマがKotlinで便利だと感じること
-
絶対に
null
でないことを担保するのはコードだとは限りません。たとえば、必ず一つ以上の引数を渡さなければならないスクリプトにユーザーが引数を渡さなければ、それが失敗したのはユーザーの責任です。クリティカルなシステムではnull
でないことをコードが保証すべきですが、使い捨てのスクリプトではユーザーが保証すべきというケースも多いでしょう。そのような場合null
を!!
してしまったら、その責任はユーザーにあります。 ↩ -
ただし、
Object
を使おうとすると使うべきでないというコンパイラ警告が出ます。 ↩ -
Java でもオートボクシングがありますが、オートボクシングはあくまで
int
とInteger
間などの型の変換を自動でやってくれるだけです。Kotlin
のInt
は、内部的にボクシングされているかどうかにかかわらず同一の型ですし、コードの上で違いはありません。 ↩ -
ただし、
Int
はInt?
のサブタイプなので、正確には「その他すべてのNullable」の中の個々の型から「その他すべてのNon-null」の中の個々の型へ伸びる線があります。 ↩ -
厳密には、引数の型の反変な型パラメータに
T
をとることで、負と負のかけ算が正になるようにひっくり返って使えるので、この表現は正確ではありません。正確なルールについては( C# についての記事ですが)こちらを御覧下さい。 ↩