JavaプログラマがKotlinでつまづきがちなところ

  • 339
    いいね
  • 14
    コメント

Kotlin が Android の公式言語になることが Goole I/O 2017 で発表されました。これから Kotlin を始める Java プログラマが多くなると思うので、本投稿では Java プログラマが Kotlin でつまづきがちなところについて説明します。

本投稿は単独で理解できるように書いていますが、↓の連載の第二弾です。 Kotlin の基礎的な構文は理解していることを前提としているので、 Kotlin の基礎については "Javaとほぼ同じところ" を御覧下さい。

  1. Javaとほぼ同じところ
  2. 新しい考え方が必要でつまづきがちなところ ←この投稿で扱う内容
  3. 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` のメソッドを使う処理
}

一度 instanceofanimalCat であると確認したにも関わらず、その後 Cat へのダウンキャストが必要になってしまいました。ダウンキャストは安全でない処理なので、できれば使いたくありません。間違えてここで (Dog)animal としてしまうと実行時に ClassCastException が発生してしまいます。

そもそもおかしいのは、直前の if 文で animalCat であることを確認しているのに、明示的にキャストしなければならないことです。その if のスコープでは animalCat であることが保証されているのだから、コンパイラが animalCat として扱ってくれれば良い話です。それをしてくれるのが Kotlin の Smart Cast です。

//Kotlin
let 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 チェックで snull でないことが保証され、この if のスコープの中では snull でない 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

注意が必要なのは、 StringString? はまったく別の型だということです。 String? 型の値に対して String のメソッドを呼び出すことはできません。 最初の例で s.length がコンパイルエラーになるのは snull かもしれないからではなく、 String? 型には length などというプロパティは存在していない からです。 length を持っているのは String であって String? ではありません。

そして、もう一つ重要なのは、 StringString? のサブタイプであるということです。 そのため、 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 != nulls 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 でないと言い切れる場合にだけ使うべきです。

?.

?. を使えば T? 型の値に対して、 null でなかった場合だけ T のメソッドを呼び出すことができます。

// Kotlin
val s: String? = ...
val l: Int? = s?.length

↑の lInt? なことに注意して下さい。 snull の場合、 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 の形で書き、 foonull だった場合は代わりに bar となります。

たとえば、ユーザーの名前が設定されていなかった場合に No Name と表示したければ↓のように書けます。

val name: String? = ...
val displayName: String = name ?: "No Name"

ここで重要なのは、 displayName の型が String? ではなく String なことです。 ?:namenull だった場合に代わりの文字列 "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, foldfold は 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 が、ラムダ式の外側にあった andreturn になっています。

Kotlin ではラムダ式の内部で書いた return は必ずラムダ式の外側に対して作用します。ラムダ式自体の戻り値を返すために return を使うことはできません。ラムダ式の戻り値はラムダ式の内部で最後に評価された式の値となります。

では、 inline でないメソッドに渡されたラムダ式の内部で return を書くとどうなるでしょうか?インライン展開されないので、ラムダ式の外側に対して作用することはできません。その場合、コンパイルエラーになります。

// Kotlin
// `inline` でない場合
fun foo(f: () -> Int): Int {
  ...
}

fun bar(): Int {
  return foo {
    if (flag) {
      return 0 // コンパイルエラー
    }

    42
  }
}

↑の例は、↓のように fooinline なら問題ありません。

// 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 と上手に付き合っていきましょう。

匿名関数

ラムダ式の中で return できないなら、早期脱出したいようなケースはどうすればよいでしょうか?

// Kotlin
numbers.forEach { number ->
  if (number % 2 == 1) {
    // ここで早期脱出したいけどどうすればいい?
  }

  ...
}

こういう場合にはラムダ式ではなく 匿名関数( Anonymous Function ) を使うと良いです。

// Kotlin
numbers.forEach(fun(number: Int) {
  if (number % 2 == 1) {
    return // 早期脱出
  }

    ...
})

ラムダ式と違って、匿名関数の中の return は匿名関数自体の return を意味します。

AnyAny?

Java ではルートクラスは Object でした。 Kotlin では Any がそれに当たります。 Kotlin から Java のメソッドを使う場合、 Java で引数や戻り値に Object が使われている箇所は Kotlin ではすべて Any に見えるようになっています。しかし、これは単純に名前が変わったという話ではありません。 Kotlin でも Java の Object を使うことは可能です2。 Kotlin では Object はルートクラスではなく、 AnyObject のスーパークラスになります。

何のために Any が必要なのでしょうか。 Java ではプリミティブ型から Object に代入するには一度 Integer などのラッパークラスにボクシングする必要がありました。 intObject の間に型の上での派生関係は存在しません。しかし、 Kotlin の Int などはクラスとして振る舞うため、 IntAny のサブタイプです。そのため、 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 と違って構文上はボクシングを意識せずに使えますが3Any への Int などの代入はパフォーマンス上のオーバーヘッドを伴うことには注意して下さい。

Java ではクラス宣言時に extends を使ってスーパークラスを明示的に指定しなければ、そのクラスは Object を継承していることになりました。同様に、 Kotlin ではスーパークラスを指定しなければ Any を継承します。

しかし、ややこしいのは Kotlin には Any にも代入できない値がある ことです。それは null です。つまり AnyInt? など、 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 を表すようになるのか理解できないでしょう。

NothingNothing?

AnyAny? と対になる存在が 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 のみを入れられる型です。ある意味、 nullNothing? クラスのインスタンスのようなものです(もちろん実際には 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 の IterableComparable では型パラメータを宣言するときに outin を付けて共変性・反変性を指定しています。これが Declaration-site variance です。

// Kotlin
interface Iterable<out T> {
  ...
}

interface Comparable<in T> {
  ...
}

このように型パラメータの宣言時に outin を付けることで、利用時には何も付けなくても常に ? extends / out? super / in が付いているような挙動になります。また、たとえば Iterable<out T>T をメソッドの引数に使おうとするとおかしいので5、コンパイラがコンパイルエラーとして教えてくれます。

クラスやインタフェースの設計者は、利用者よりもその型についてよく考えているので適切に変性を設定できるはずです。逆に、宣言時に変性について考えれば、変性をないがしろにした変な型を設計することも防げます。また、利用時に ? extends / out など毎回を付けるのも面倒です。 Declaration-site variance はそのような Use-site variance の抱えていた問題に対するアプローチです。

プリミティブ型の明示的変換

Java では↓は問題のないコードです。

// Java
int x = 42;
double y = x; // OK

しかし、 Kotlin ではコンパイルエラーになります。

// Kotlin
val x: Int = 42
val y: Double = x // コンパイルエラー

通常、 A 型の値を B 型の変数に代入できるためには、 AB のサブタイプである必要があります。しかし、 Kotlin の IntDouble のサブクラスではありません。型の派生関係がないのに代入できる場合には、暗黙の型変換が行われているということになります。 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 のたし算が実行される前に ab が暗黙的に int に変換されるからです。暗黙的型変換は仕様をよく理解していないとこのような想定外の挙動を生みがちです。僕は、暗黙的型変換よりも簡潔な明示的型変換の方法を提供する方が良いアプローチだと考えています。

Kotlin では、 Int から Double に明示的に型変換するには toDouble メソッドを使います。

// Kotlin
val x: Int = 42
val y: Double = x.toDouble() // OK

また、 Double から Int のように Java で明示的なキャストが必要なケースでも、 Kotlin ではキャストではなくメソッドで型変換するので注意して下さい。これは、 IntDouble には型の派生関係がないためです。

// 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
  • AnyAny?
  • NothingNothing?

次の投稿: JavaプログラマがKotlinで便利だと感じること



  1. 絶対に null でないことを担保するのはコードだとは限りません。たとえば、必ず一つ以上の引数を渡さなければならないスクリプトにユーザーが引数を渡さなければ、それが失敗したのはユーザーの責任です。クリティカルなシステムでは null でないことをコードが保証すべきですが、使い捨てのスクリプトではユーザーが保証すべきというケースも多いでしょう。そのような場合 null!! してしまったら、その責任はユーザーにあります。 

  2. ただし、 Object を使おうとすると使うべきでないというコンパイラ警告が出ます。 

  3. Java でもオートボクシングがありますが、オートボクシングはあくまで intInteger 間などの型の変換を自動でやってくれるだけです。 KotlinInt は、内部的にボクシングされているかどうかにかかわらず同一の型ですし、コードの上で違いはありません。 

  4. ただし、 IntInt? のサブタイプなので、正確には「その他すべてのNullable」の中の個々の型から「その他すべてのNon-null」の中の個々の型へ伸びる線があります。 

  5. 厳密には、引数の型の反変な型パラメータに T をとることで、負と負のかけ算が正になるようにひっくり返って使えるので、この表現は正確ではありません。正確なルールについては( C# についての記事ですが)こちらを御覧下さい。