はじめに
この記事は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非許容型について見ていきます。
プラットフォーム型が出来た理由
(引用元 : 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
などと定義することは出来ず、以下のようにエラーメッセージなどに出てくるのみです。
なぜわざざわこのような特別な型を用意したのか?
その理由を知るために、仮に「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の場合は以下のソースから確認することが出来ます。
また、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.List
もkotlin.MutableList
も、Javaにおいてはjava.util.List
として扱われ、さらに内部的にはjava.util.ArrayList
が使われています。
継承関係は以下のようになっています。
(引用元 : 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!