この投降は、Kotlin Advent Calendar 2016の5日目の投稿です。
はじめに
Kotlinではnullをより扱いやすくするように、Nullable、「?.
」そして「?:
」など様々な言語機能・構文が提供されています。
Kotlinのみで書かれたコードでは、NullPointerException
に遭遇することはそうそうないでしょう。
ところが、実際の開発ではではKotlinだけで製品を完成させることはまずできません。Javaで書かれたSDKや外部ライブラリを利用して製品を作っていくことがほとんどです。
その場合、
- Javaで定義されたメソッドの返り値を変数に代入する
- Javaで定義された抽象クラスを継承する
- Javaで定義されたインターフェースをSAM変換を用いて処理
を行った場合、nullはどう扱われるのでしょうか?
「Kotlinはnull安全だから」といって扱い方を間違えると、NullPointerException
やIllegalStateException
を発生させてしまいます。
本投稿では、『【!ってなんだ】KotlinとJava、nullとPlatformType【NullableにNotNull】』と題して
JavaとKotlinの相互運用において、大事なポイントとなるnullとPlatform Type
とその関連事項について紹介します。
KotlinのNullableの確認
まずは、KotlinのNullableについて確認しましょう。
Stringを返すメソッドの例
次のように、String型を返すメソッドを定義します。
fun returnString(): String = ""
このメソッドの返り値を変数に代入しましょう。
次のように変数の型を明示しない場合は変数str
はString型と推論されます。String型の変数str
で、?.
を使ってメンバにアクセスした場合、「?.
は必要ない」という旨の警告が出ます。
val str = returnString()
println(str.length)
// 次は?.は必要ないという旨の警告がでる
// println(str?.length)
次のように変数の型を明示することも可能です。
val str: String = returnString()
println(str.length)
// 次は?.は必要ないという旨の警告がでる
// println(str?.length)
String型を、String?型の変数に代入することも可能です。変数str
は、String?と明示的に型を宣言しました。str
はString?型なので、メンバにアクセスする際は、「.
」ではなくて「?.
」としないとコンパイルエラーになります。
// String?にStringを代入することもできる
val str: String? = returnString()
println(str?.length)
// println(str.length)は、コンパイルエラー
String?を返すメソッドの例
次は、String?型を返すメソッドを確認します。
fun returnNullableString(): String? = null
このメソッドの返り値を変数に代入します。
次のように変数の型を明示しない場合は変数str
はString?型と推論されます。メンバにアクセスする際は、「?.
」を使わないとコンパイルエラーになります。
val str = returnNullableString()
println(str?.length)
// println(str.length)は、コンパイルエラー
次のようにString?型の返り値をString型の変数に代入することはできません。Type Mismatchでコンパイルエラーとなります。
// Type Mismatchでコンパイルエラー
// val str: String = returnNullableString()
次のように変数の型を明示することも可能です。
val str: String? = returnNullableString()
println(str?.length)
// println(str.length)は、コンパイルエラー
Kotlinで定義したメソッドをKotlinで使う例を確認しました。
つぎは、Javaで作ったメソッドをKotlinで使う場合を示します。
JavaのメソッドをKotlinから呼ぼう
Javaで次のようなクラス・メソッドを定義します。
public class Utility {
public static String returnStringJava() {
return null;
}
}
この「Utility#returnStringJava
」をKotlinから呼び出したらどうなるでしょうか。
ここでは、次のような順番で確認していきましょう。
- String?型と型を明示した変数に代入した場合
- String型と型を明示した変数に代入した場合
- 型を明示しない変数に代入した場合
String?型と明示した変数に代入した場合
次のコードは、Javaで定義したUtility#returnStringJava
を、String?型の変数に代入したコードです。
String?型なので、メンバにアクセスするには.
ではなくて?.
でないとコンパイルエラーになります。
val str: String? = Utility.returnStringJava()
println(str?.length)
// println(str.length)は、コンパイルエラー
Kotlinの中でのString?はnullかもしれない型、Stringはnullではない型です。
一方で、JavaのStringはnullな可能性があります。
Javaで書いたStringを返すメソッドの返り値を、KotlinではString?の変数に代入すれば扱いやすくなり、NullPointerException
を回避できそうですね。
String型と明示した変数に代入した場合
次のコードは、Javaで定義したUtility#returnStringJava
を、String型の変数に代入したコードです。
val str: String = Utility.returnStringJava()
// ?.は必要ないという旨の警告がでる
println(str?.length)
println(str.length)
このコードは、コンパイルエラーには
なりません。(「?.は必要ないという」旨の警告は出ますが。)コンパイルエラーには
なりませんが、実行時エラーになります。どこで実行時エラーになるか想像してみてください。
ちなみに「Utility#returnStringJava
」は次のようにnullを返すメソッドでしたね。
public class Utility {
public static String returnStringJava() {
return null;
}
}
先ほどのコードは、
val str: String = Utility.returnStringJava()
で、nullをString型の変数に代入しようとしたタイミングで、IllegalStateException
が発生して実行時エラーになります。代入しようとしたタイミングでIllegalStateException
実行時エラーですよ!メンバにアクセスした時点でNullPointerException
ではありません
。
ところでもし、「Utility#returnStringJava
」が
public class Utility {
public static String returnStringJava() {
return "";
}
}
のようにnullを返さず、非nullを返す場合は実行時エラーにはなりません。
型を明示しない変数に代入した場合
JavaでStringを返すメソッドをKotlinで、
- 「String?型と明示した変数に代入した場合」
- 「String型と明示した変数に代入した場合」
をみてきました。それでは、型を明示しない変数に代入した場合をみてみましょう。
val str = Utility.returnStringJava()
println(str.length)
println(str?.length)
このコードは、コンパイルエラーにはなりません。警告も出ません。
今までと違う点に「おや」と思うことはありませんか?
strがString型の変数と明示的に宣言される、もしくは推論された場合、「?.
」でのメンバアクセスは警告が出ていましたね。
strがString?型の変数と明示的に宣言される、もしくは推論された場合、「.
」でのメンバアクセスはコンパイルエラーになっていましたね。
そのどちらでもありません。
それは「Javaで定義・宣言された参照型を、KotlinではPlatform Type
型という特別な型で扱われるため」です。
val str = Utility.returnStringJava()
上記のコードでstrは、StringともString?とも明示的に宣言されていません。
strは、String?でもStringでもない型、Platform Type
のString!
に推論されているのです。
Platform Type
について詳しいことは次の節で紹介するとして、先に実行結果を見てみましょう。
コードを再掲します。
public class Utility {
public static String returnStringJava() {
return null;
}
}
val str = Utility.returnStringJava()
println(str.length)
println(str?.length)
を実行すると、NullPointerException
が発生します。「.
」でメンバにアクセスしたタイミングでです。
val str:String = Utility.returnStringJava()
とString型であることを明示した変数では、変数への代入したタイミングでIllegalStateException
を発生していましたね。
例外が発生するタイミングと発生する例外が違うことに注目してください。
もし、Utility#returnStringJava
が非nullなStringを返す場合、
public class Utility {
public static String returnStringJava() {
return "";
}
}
次のようにNullPointerException
は発生せず、正常にプログラミングが終了します。
0
0
PlatformTypeって?
それでは、PlatformType
について説明します。
Javaの参照型はnullになりえます。Kotlinだけで閉じていれば、Nullableか非Nullableかは明確で扱いやすいのですが、Javaとの境界がやっかいです。どんな参照型もnullかもしれませんし、そうではないかもしれませんから。
Kotlinでは、(@Nullable
と@NotNull
が付いていない)Javaの来た参照型は、すべて特別にPlatform Typeとして扱われます。
Platform Typeは、T!と表現されます。例えば、String!とか、View!とかです。
public class Utility {
public static String returnStringJava() {
return null;
}
}
というメソッドをKotlinから使おうとした場合、IDEAの予測変換ではString!
型を返すよという情報が表示されます。
val str = Utility.returnStringJava()
また、IDEAなどでstrはString!
型だという情報が示されます。
Utility#returnStringJava
の返り値の型はString!
だし、strはString!
型ですが、明示的にString!
という型を変数の宣言等には使えません。次のコードはコンパイルエラーになります。
// コンパイルエラー!!!!
// val str: String! = Utility.returnStringJava()
Platform Typeである、String!型は、NullableなString?でもあり非NullableなStringでもある型です。次のように、String?にもStringにも代入できます。
val str = Utility.returnStringJava() // 推論され、strはString!型
val strNullable: String? = str // strNullableはString?型
val strNotNull: String = str // strNotNullはString型
ところで、Javaの参照型はnullになりえました。そのため、String!型をString?に代入するのは問題ないのですが、String型に代入する時は注意が必要です。
val str:String = Utility.returnStringJava() // IllegalStateExceptionが投げられる
(実際はnullな)String!をnullを許さないStringに代入しています。このコードは、代入しようとしたタイミングでIllegalStateExceptionが発生するのでしたね。
Platform Typeのまま扱う場合も注意が必要です。
val str = Utility.returnStringJava() // 推論され、strはString!型
println(str.length) // NullPointerException
このコードは、コンパイルエラーにはなりませんが、NullPointerExceptionが発生しましたね。
まとめると、
『Xyz!と書いている型は、Javaから来た参照型でPlatform Type。NullableなXyz?型としても扱えるし、非NullableなXyz型としても扱える。Xyzとして扱った場合、例外が発生することがある』
またここでは説明を省きましたが、「(Mutable)Collection!」というPlatform Type
もあります。この説明はまたの機会に。
KotlinだってNullPointerExceptionが発生する
「Kotlinはnull安全」といっても、Javaで定義したメソッドのKotlinでの扱い方を誤ると、NullPointerException
やIllegalStateException
が発生します。
どのような時に発生するか、確認しましょう。
IllegalStateExceptionが発生しうる非Nullable型への代入
非Nullable型で宣言した変数にnullを代入した場合、代入したタイミングでIllegalStateException
が発生します。
Javaで、String型を返すUtility#returnStringJava
、
public class Utility {
public static String returnStringJava() {
return null;
}
}
をKotlinで次のように使うと、
val str:String = Utility.returnStringJava()
代入したタイミングで時点でIllegalStateException
が発生します。
Exception in thread "main" java.lang.IllegalStateException: Utility.returnStringJava() must not be null
NullPointerExceptionが発生しうるPlatform Type型のメンバアクセス
PlatformTypeの型で、中身がnullだった場合のメンバアクセスにおいて、「.
」を使った場合、NullPointerException
が発生します。
Javaで、String型を返すUtility#returnStringJava
、
public class Utility {
public static String returnStringJava() {
return null;
}
}
をKotlinで次のように使うと、
val str = Utility.returnStringJava() // Platform Typeとして扱われる
println(str.length) // この時点でNullPointerException
メンバアクセスしたstr.length
で、NullPointerException
が発生します。
Exception in thread "main" java.lang.NullPointerException
Javaの型をNullable・NotNull指定
前節で示した通り、Javaで定義したメソッドをKotlinで呼び出した場合、使い方を誤るとNullPointerException
やIllegalStateException
がスローされる場合があります。
これを避けることはできないでしょうか?Javaで定義したメソッドを、Kotlin側に「これはnullを返すかもしれないから、Nullable型として扱ってね!」と伝えられればよさそうですね。
これは、Nullableアノテーション
を用いることで可能になります。
Nullableアノテーション
次のように、Javaのメソッドをorg.jetbrains.annotations.Nullable
をつけて定義します。
@Nullable
public static String returnStringJavaNullableAnnotation() {
return null;
}
これをKotlin側から使います。
// String?型と推論される
val str = Utility.returnStringJavaNullableAnnotation()
println(str?.length)
// コンパイルエラー
// println(str.length)
Javaで@Nullable
アノテーションがついたUtility#returnStringJavaNullableAnnotation
の返り値型は、KotlinにおいてString?型になります。(String!型ではありません。)そのためstrはString?と推論されます。
String?型なので、メンバアクセスする場合は「?.
」を用いないといけません。「.
」ではコンパイルエラーになります。
次のようにString?と型を明示的に書くこともできます。
// String?と型を明示的に書いてもOK
// val str : String?= Utility.returnStringJavaNullableAnnotation()
Utility#returnStringJavaNullableAnnotation
の返り値はString?型ですので、String型に代入はできません。
次のコードはコンパイルエラーになります。
// コンパイルエラーに Type Mismatch
// val str: String = Utility.returnStringJavaNullableAnnotation()
このように、Javaのnullを返すかもしれないJavaメソッドにNullableアノテーション
を付与しておくことで、Kotlin側でNullable型として扱ってもらうことが可能になります。
NotNullアノテーション
nullかもしれないNullableアノテーション
の次は、nullではないことを表すNotNullアノテーション
です。
次のように、Javaのメソッドをorg.jetbrains.annotations.NotNull
をつけて定義します。
@NotNull
public static String returnStringJavaNotNullAnnotation() {
return "";
}
これをKotlin側から使います。
// String型と推論される
val str = Utility.returnStringJavaNotNullAnnotation()
println(str.length)
// ?.も書けるけれど、必要ないよと警告がでる
// println(str?.length)
Javaで@NotNullアノテーションがついたUtility#returnStringJavaNotNullAnnotation
の返り値型は、KotlinにおいてString型になります。(String!型ではありません。)そのためstrはStringと推論されます。
String型なので、「.
」でメンバアクセスをすることができます。「?.
」ともかけますが、警告がでます。
次のようにStringと型を明示的に書くこともできます。
// Stringと型を明示的に書いてもOK
val str : String = Utility.returnStringJavaNotNullAnnotation()
KotlinでString型のインスタンスをSting?の変数に代入できたように、Utility#returnStringJavaNotNullAnnotation
の返り値をString?型の変数に代入することも可能ではあります。
// String型のインスタンスをString?の変数に代入できるので、次もOK
val str: String? = Utility.returnStringJavaNotNullAnnotation()
ところで、@NotNull
アノテーションがついたメソッドがnullを返した場合、IllegalStateException
がスローされます。
たくさんあるぞ、Nullable・NotNull
さきほど、
org.jetbrains.annotations.Nullable
org.jetbrains.annotations.NotNull
を紹介しました。
ところで、読者のあなたはNullableやNotNullと聞いて違うアノテーションを想像しませんか?
Androidや、Lombok、FindBugsなどさまざまなSDKやツール・ライブラリに、Nullable・NotNullというアノテーションが存在します。
先ほど紹介した
org.jetbrains.annotations.Nullable
org.jetbrains.annotations.NotNull
でなくても、後述するアノテーションを用いると、同様の効果を得ることができます。
org.jetbrains.annotations.Nullable
と同様の効果が得られるアノテーション一覧
- org.jetbrains.annotations.Nullable
- android.support.annotation.Nullable
- com.android.annotations.Nullable
- org.eclipse.jdt.annotation.Nullable
- org.checkerframework.checker.nullness.qual.Nullable
- javax.annotation.Nullable
- javax.annotation.CheckForNull
- edu.umd.cs.findbugs.annotations.CheckForNull
- edu.umd.cs.findbugs.annotations.Nullable
- edu.umd.cs.findbugs.annotations.PossiblyNull
org.jetbrains.annotations.NotNull
と同様の効果が得られるアノテーション一覧
- org.jetbrains.annotations.NotNull
- edu.umd.cs.findbugs.annotations.NonNull
- android.support.annotation.NonNull
- com.android.annotations.NonNull
- org.eclipse.jdt.annotation.NonNull
- org.checkerframework.checker.nullness.qual.NonNull
- lombok.NonNull
あなたが使っているSDKやツールの中のNullable、NotNullをぜひ使ってみてください。
これは今後増える可能性もあります。公式ドキュメントやソースコードを参照してください。
メソッドのオーバーライドは?
Javaで定義した引数なし返り値Stringのメソッドを、Kotlinで使う例を見てきました。しかし、実際の開発で多いのはSDK内に定義されているJavaのクラスを継承することでしょう。
ここでは、その場合を見ていきましょう。
Javaで次のようなクラスを定義します。
public class Bundle {}
public class MyActivity {
public void onCreate(Bundle savedInstanceState){
}
}
KotlinでこのMyActivityを継承して、onCreateメソッドをオーバーライドしましょう。
このような書き方もできますし、
class MyKActivity : MyActivity(){
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(bundle)
}
}
このような書き方も可能です。
class MyKActivity : MyActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(bundle)
}
}
違いは、onCreateの引数がBundle型がBundle?型という点ですね。
引数がBundle型で書いた場合、savedInstanceStateがnullだったならば、IllegalStateExceptionがスローされます。
JavaのMyActivityを次のように変えます。
public class MyActivity {
public void onCreate(@Nullable Bundle savedInstanceState){
}
}
このように@Nullable
を付与した場合、
class MyKActivity : MyActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(bundle)
}
}
としか書けなくなります。
実際、android.support.v7.app.AppCompatActivity#onCreate
の引数savedInstanceStateには@Nullable
が付与されています。そのため、Bundle?としか書けません。
さいごに
本投稿では、『【!ってなんだ】KotlinとJava、nullとPlatformType【NullableにNotNull】』と題して、JavaとKotlinの相互運用のポイントとなるPlatform Typeを紹介しました。
- なんとなく?をつけてた
- !って何か気になっていた
- なんで、Bundle!って書けないのかな
- NotNullやNullableアノテーションの大事さに気づいてなかった
という方の、理解の助けになれば幸いです。