Kotlin
KotlinDay 25

Kotlinの型を知る ~後編~ プラットフォーム型とCollections

はじめに

この記事はKotlin Advent Calendar 最終日の記事です。

今年はGoogle I/OでKotlinがAndroidの公式開発言語の1つになり、初めてのカンファレンスであるKotlin Confが開かれるなど、Kotlinの普及がますます進みそうなことを感じた1年になりました。
個人的にはKotlin In Actionの翻訳に携わり、Kotlinの普及へ微力ながら貢献出来たのではないかと思います
(翻訳についてまとめた記事もあります:技術書を翻訳する技術)。

本記事ではそのKotlin In Actionの著者の1人であるSvetlana Isakova氏のKotlin Confでのセッション「Kotlin Types : Exposed」の内容と、Kotlin In Actionの6章の内容を元に「Kotlinの型」についてまとめた内容になります。
アジェンダは以下のとおりです。

  • KotlinとJavaの型
  • Any, Unit, Nothing, void
  • Null許容型/Null非許容型とプラットフォーム型
  • Collection型

「KotlinとJavaの型」「Any, Unit, Nothing, void」については「Kotlinの型を知る ~前編~ 」をご覧ください。
本記事は後半の「Null許容型/Null非許容型とプラットフォーム型」、「Collection型」についてです。

Null許容型/Null非許容型とプラットフォーム型

ご存知のとおり、Kotlinの型システムではNull許容型(Nullable)と、Null非許容型(Non-Null)が区別されています。
これはもちろん「NullPointerException(NPE)」を防ぐためであり、Kotlinを導入する大きなメリットの1つでしょう。
一方で、Kotlinは「Javaとの相互運用性」も使命としている言語であるので、KotlinをJavaと混在させて使うことも多々あります。
まず、KotlinとJavaを混在させた場合のNull許容型/Null非許容型について見ていきます。

プラットフォーム型が出来た理由

nullable_annotaition.png
(引用元 : KotlinConf 2017 - Kotlin Types: Exposed by Svetlana Isakova)

Javaに@Nullable@NonNullなどのアノテーションがついていた場合、Kotlinにおいてはそれらはそれぞれ、Null許容型、Null非許容型として扱われます。この場合は問題ありません。
Javaにこれらのアノテーションがついていない場合にどうするか?を考える必要があります。

KotlinにおいてはアノテーションのないJavaの型は「プラットフォーム型(Platfrom Type)」として扱われます。
定義としては「Null許容についての情報がない型(Unknown Nullability)」のことです。
この型はJavaの型と全く同じで、この型をNull許容型として扱うか、Null非許容型として扱うかは、実装者の責務になっています。

プラットフォーム型はString!のように型の名前の後ろに!をつけて表現されますが、

val s : String! = session.description

などと定義することは出来ず、以下のようにエラーメッセージなどに出てくるのみです。

Screen Shot 2017-12-23 at 4.17.35 PM.png

なぜわざざわこのような特別な型を用意したのか?
その理由を知るために、仮に「Javaから来た型を全てNull許容型として扱う」ことを考えてみます。
この方法はNullPointerExceptionを防ぐことが出来るので、「一番安全な方法」のように思います。

val i : Int? = session.description?.length // Int?は扱いにくい
val i : Int = session.description?.length ?: 0 // 使うたびにNullチェック
val i : Int = session.description?.length!! // JavaからのdescriptionがNullでないことが分かっていればこれが楽

しかし、実際は上のように使う度にNullチェックするようなことが起こり、多くの場合は非Null表明(!!)を多用することになるでしょう。
Javaから来た型をどう扱うかは以下の3つの中から2つだけを選択する必要があって、

  • Null安全(NullPointerExceptionが発生しない)
  • 便利さ
  • Javaとの相互運用性

Kotlinの言語設計者は「Null安全」を諦め、下の2つを選択しました。

  • Null安全(NullPointerExceptionが発生しない)
  • 便利さ
  • Javaとの相互運用性

以下のような場合、NullPointerExceptionが発生します。

//Java
public class Session {
  public String getDescription() {
    return null;
  }
}

//Kotlin
val session = Session()
val description = session.description
println(description.length) // NullPointerException

「Null安全」を諦めたとはいえ、NPEを防ぐための工夫はされています。
それについては次節に書きます。

また、Kotlin In Actionには「Javaから来た型を全てNull許容型として扱う」ことの問題点として、ジェネリクスでの例も示されています。

ジェネリクスを使ったときは、さらに悪いことになります。例えば、Java からもたらされる全ての ArrayList は、Kotlin においては ArrayList? となり、 参照する際は必ずnullチェックをするかキャストを使うこととなります。これは安全であることのメリットを無力化してしまいます。そのようなチェックを書くことは非常に面倒なので、Kotlin の設計者は実用的な方式を選択し、Java からもたらされる値を正しく扱うのは開発者の責務であるということにしました。

(「Kotlin イン・アクション」P199 6.1.11節)

NullPointerExceptionを防ぐ方法

Javaから来た型はプラットフォーム型として扱われるため、Null許容についての情報がなく、NPEを完全に防ぐことは出来ません。
しかしながら以下のような方法でNPEの可能性を減らすことが出来ます。

  • Javaのコードにアノテーションをつける
  • Kotlinで扱うときに型を明示的にする

それぞれの方法を見てみましょう。

Javaのコードにアノテーションをつける

冒頭でも書いたようにJavaのコードに@NotNullというアノテーションがついていれば、KotlinのコンパイラはそれをNull非許容型と解釈します。
このようにKotlinのコンパイラが解釈するアノテーションには以下のようなものがあります。

定義 Null非許容型 Null許容型
JetBrains org.jetbrains.annotations.NotNull org.jetbrains.annotations.Nullable
Android com.android.annotations.NonNull android.support.annotation.NonNull com.android.annotations.Nullable android.support.annotation.Nullable
JSR-305 javax.annotation.CheckForNull javax.annotation.Nullable
FindBugs edu.umd.cs.findbugs.annotations.CheckForNull edu.umd.cs.findbugs.annotations.Nullable
Lombok lombok.NonNull

対応しているアノテーションはKotln1.1の場合は以下のソースから確認することが出来ます。

https://github.com/JetBrains/kotlin/blob/1.1.0/core/descriptor.loader.java/src/org/jetbrains/kotlin/load/java/JvmAnnotationNames.kt

また、JSR-305の@ParametersAreNonnullByDefaultというアノテーションを仕えば、@Nullableのついてない引数全てを@NonNull(Null非許容型)として扱うことができます。
こちらについては以下のブログの記事に詳しく書かれています。

Yukiの枝折 : Android: デフォルトで@NonNull扱いにする

Kotlinで扱うときに型を明示的にする

次の方法はKotlinがJavaからの型を扱う場合に、すぐにその型を明示する、という方法です。

//Java
public class Session {
  public String getDescription() {
    return null;
  }
}

//Kotlin
val session = Session()
val description = session.description
println(description.length) // NullPointerException

上の例の場合、descriptionの型は明示されていないので、その型はプラットフォーム型であるString!型となります。
この場合はランタイム時にdescription.lengthを呼び出すタイミングでNullPointerExceptionが発生します。

ではこのコードのKotlin側を次のように書き換えた場合はどうなるでしょう?
違いは2行目のStringという型を明記した部分だけです。

val session = Session()
val description : String = session.description
println(description.length) // NullPointerException?

この場合もランタイム時にエラーが発生しますが、そのエラーは「session.description must not be null」というメッセージのついたIllegalStateExceptionになります。
NullPointerExceptionではなく、IllegalStateExceptionになる理由はこのコードのバイトコードをJavaへとデコンパイルするとすぐに分かります。

Session session = new Session();
String var10000 = session.getDescription();
Intrinsics.checkExpressionValueIsNotNull(var10000, "session.description");
String description = var10000;

中間の変数(ここではvar10000)にJavaからの値を一度代入し、Intrinsics.checkExpressionValueIsNotNullというメソッドを使って、そのNullチェックを行っています。
今回の例ではJavaからの値をdescription.lengthのようにすぐに使っていましたが、そうではなく、関数呼び出しが連続する場合などにおいても、「JavaからKotlinへの境界線でNullチェックを行う」ことで、間違った箇所を特定しやすくしています。

Collection型

Kotlinには読み取り専用コレクション(Read-only Collection)ミュータブルコレクション(Mutable Collection)の2種類のコレクションが用意されています。
Kotlinのコレクションを扱う際に最も重要なのは、

読取り専用コレクションはイミュータブルではない

ということです。
イミュータブルコレクションは、以下のリポジトリにプロトタイプが公開されており、

イミュータブルコレクションは、Kotlin 標準ライブラリへの追加が予定されています。

(「Kotlin イン・アクション」P215 6.3.2節)

とのことです。

読み取り専用コレクションはイミュータブルではないことを知るために次の例を見てみましょう。

val mutableList = mutableListOf(1, 2, 3)
val list : List<Int> = mutableList

println(list) // [1, 2, 3]
list.add(4) // Error!

変数listは読み取り専用コレクションのためaddという関数を呼び出すことは出来ません。
しかし、同じインスタンスを共有する変数mutableListはミュータブルコレクションなので、以下のようにaddを呼び出せます。

mutableList.add(4)
println(list) // [1, 2, 3, 4]

この時、インスタンスを共有する変数listの中身を出力すると、add(4)の内容が反映されてしまっています。

読み取り専用コレクションkotlin.Listkotlin.MutableListも、Javaにおいてはjava.util.Listとして扱われ、さらに内部的にはjava.util.ArrayListが使われています。
継承関係は以下のようになっています。

Screen Shot 2017-12-23 at 5.59.09 PM.png
(引用元 : KotlinConf 2017 - Kotlin Types: Exposed by Svetlana Isakova)

さらにJavaの関数にKotlinのリストを渡す場合など、KotlinとJavaとの間でコレクションのクラスをやり取りする場合は、そのコレクションがJava内で変更されるのかどうか?を考える必要があります。

コレクションを引数にとって、それを Java へと渡す関数を書く場合、呼び出している Java コードがコレクションを変更するかどうかに応じて、引数の型に正しいコレクショ ンの型を使用することは実装者の責務となります。

(「Kotlin イン・アクション」P218 6.3.3節)

これはNull許容性に関するプラットフォーム型の考え方と似ています。

まとめ

  • KotlinのNull許容は、Null安全と便利さとの間でバランスのとれた妥協をしている
  • Kotlinの読み取り専用コレクションはイミュータブルコレクションではない

2017年はKotlinが非常に盛り上がった年でした。来年はさらに普及が進み、Kotlinが当たり前になることでしょう!

今年も一年お疲れさまでした。
Have a Nice Kotlin!

関連記事