【!ってなんだ】KotlinとJava、nullとPlatformType【NullableにNotNull】

  • 31
    いいね
  • 0
    コメント

 この投降は、Kotlin Advent Calendar 2016の5日目の投稿です。

はじめに

 Kotlinではnullをより扱いやすくするように、Nullable、「?.」そして「?:」など様々な言語機能・構文が提供されています。

 Kotlinのみで書かれたコードでは、NullPointerExceptionに遭遇することはそうそうないでしょう。

 ところが、実際の開発ではではKotlinだけで製品を完成させることはまずできません。Javaで書かれたSDKや外部ライブラリを利用して製品を作っていくことがほとんどです。

 その場合、

  • Javaで定義されたメソッドの返り値を変数に代入する
  • Javaで定義された抽象クラスを継承する
  • Javaで定義されたインターフェースをSAM変換を用いて処理

 を行った場合、nullはどう扱われるのでしょうか?

 「Kotlinはnull安全だから」といって扱い方を間違えると、NullPointerExceptionIllegalStateExceptionを発生させてしまいます。

 本投稿では、『【!ってなんだ】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から呼び出したらどうなるでしょうか。

 ここでは、次のような順番で確認していきましょう。

  1. String?型と型を明示した変数に代入した場合
  2. String型と型を明示した変数に代入した場合
  3. 型を明示しない変数に代入した場合

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 TypeString!に推論されているのです。

 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での扱い方を誤ると、NullPointerExceptionIllegalStateExceptionが発生します。

 どのような時に発生するか、確認しましょう。

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で呼び出した場合、使い方を誤るとNullPointerExceptionIllegalStateExceptionがスローされる場合があります。

 これを避けることはできないでしょうか?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アノテーションの大事さに気づいてなかった

 という方の、理解の助けになれば幸いです。

関連

この投稿は Kotlin Advent Calendar 20165日目の記事です。