VoicyでAndroidエンジニアをやっております、おはぎです。
先日案内させていただいた2025GWブログリレーの先鋒を務めます。
みなさんゴールデンウィークはいかに過ごされましたか?
KotlinがAndroidアプリ開発のデファクトスタンダードな開発言語となって久しい昨今です。 あえて新規機能Javaで開発しなくてはならないという場面はそうないと思いますが、Javaで書かれている既存機能をメンテナンスしたり、ライブラリという形でJavaで書かれた資産を使わなくてはならない機会には時々出くわすことがあると思います。
この記事では、KotlinとJavaの言語仕様の差異の一つであるNull安全を取り上げ、実験してみた結果を共有します。
はじめに
私の個人的嗜好としてJavaよりもKotlinを好みます。 その理由の大きな部分を占めるものがKotlinのNull安全性です。 Kotlinでは、変数をnullable型(nullableな文字列はString?)とnon-null型(non-nullな文字列はString)に明確に区別することで、実行時にNullPointerExceptionが発生するリスクを大幅に減らすことができます。
しかし、KotlinをJavaと相互運用する場合においては、このNull安全性の境界線があいまいになることがあります。 今回の記事では、KotlinとJavaそれぞれにおけるNullable、つまりNullになり得る型の取り扱いかたについて取り上げます。
KotlinでのNull安全性
まずKotlin単体でのNullの取り扱いについてみていきましょう。 例として、単純なValue Object的なクラスを考えます(Kotlinではdata classが用いられることが多いですが今回は簡単のために通常のclassを用いています)。
class KotlinClass( val nonNullText: String, val nullableText: String? ) // String?型のフィールドnullableTextに // nullでない文字列を持たせることができる val kotlinObject1 = KotlinClass( "nonNullText", "nullableText" ) // String?型のフィールドnullableTextに // nullを持たせることもできる val kotlinObject2 = KotlinClass( "nonNullText", null ) // String型のフィールドnullableTextに // nullを持たせることができない // (コンパイルが通らない) val kotlinObject3 = KotlinClass( null "nullableText" )
このように、nullableでない型として宣言されている変数にはnullが代入できないようになっています。 これによってコンパイルの段階で型の不正を検出できるため、エンドユーザーにプロダクトを何かしらの形でお届けする遥か前の段階で開発者が不正なコーディングをしていることに気づくことができるという点で非常に役立つ言語仕様といえます。
また、nullableである型を持つ値を安全に参照する言語仕様も備わっています。
val nonNullLength = kotlinObject1.nonNullText.length // kotlinObject1.nullableTextがnullである場合に // nullableLengthにnullが代入される val nullableLength = kotlinObject1.nullableText?.length // kotlinObject1.nullableTextがnullである場合に // NullPointerExceptionがthrowされる val nullableLength = kotlinObject1.nullableText!!.length // この書き方ではコンパイルが通らない val nullableLength = kotlinObject1.nullableText.length
JavaでのNull安全性(?)
Javaの言語仕様ではintやcharなどのprimitive型を除けば全てがNullであり得るので、「全てはNullableである」とあえて言い表すこともできます。
Null可能性に対してのプラクティスの一つとしてアノテーションを付与して表現することがあります。
これによりNullPointerExceptionが発生しうる使い方をしている場合にはIDEから警告を出してくれます。
今回は比較のために@NonNull、@Nullableそれぞれのアノテーションを付与したフィールドと、アノテーションをしないフィールドの3つを用意します。
先述のKotlinのコードとほぼ同等のものをJavaで書く場合は次のようになります。
今回はKotlinとの比較を簡単にするためにあえてプレーンな書き方をしていますが、nullが入り得る値を扱う際にはOptional
public class JavaClass { @NonNull final String nonNullText; @Nullable final String nullableText; final String nonAnnotatedText; JavaClass( @NonNull String nonNullText, @Nullable String nullableText, String nonAnnotatedText ) { this.nonNullText = nonNullText; this.nullableText = nullableText; this.nonAnnotatedText = nonAnnotatedText; } } // (これは完全に余談ですが、最近のJavaでは以下のような書き方もできるそうです) // @Nullableと注釈されたString型のフィールドnullableTextに // nullでない文字列を持たせることができる final JavaClass javaObject1 = new JavaClass("nonNullText", "nullableText", "nonAnnotatedText"); // @NullableとannotateされたString型のフィールドnullableTextに // nullを持たせることもできる final JavaClass javaObject2 = new JavaClass("nonNullText", null, "nonAnnotatedText"); // annotateされていないString型のフィールドnonAnnotatedTextに // nullを持たせることもできる final JavaClass javaObject3 = new JavaClass("nonNullText", "nullableText", null); // @NonNullとannotateされたString型のフィールドnonNullTextに // nullを持たせようとすると警告される // (コンパイルはできる) final JavaClass javaObject3 = new JavaClass(null, "nullableText", "nonAnnotatedText");
int nonNullLength = javaObject1.nonNullText.length(); // NullPointerExceptionの可能性があることを警告される int nullableLength = javaObject1.nullableLength.length(); // 警告されない // (実際にはNullPointerExceptionの可能性がある) int nullableLength = javaObject1.nonAnnotatedText.length()
KotlinからJavaを呼び出す
// @Nullableと注釈されたString型のフィールドnullableTextに // nullでない文字列を持たせることができる val javaObject1 = JavaClass( "nonNullText", "nullableText", "nonAnnotatedText" ) // @NullableとannotateされたString型のフィールドnullableTextに // nullを持たせることもできる val javaObject2 = JavaClass( "nonNullText", null, "nonAnnotatedText" ) // @NonNullとannotateされたString型のフィールドnonNullTextに // nullを持たせることができない // (コンパイルができない) val javaObject3 = JavaClass( null, "nullableText", "nonAnnotatedText", ) // annotateされてないString型のフィールドnonAnnotatedTextに // nullを持たせることもできる val javaObject4 = JavaClass( "nonNullText", "nullableText", null )
警告はされるものののJavaでは@NonNullとannotateされた変数にnullを代入することができた一方で、 KotlinからはJavaでは@NonNullとannotateされた変数にnullを代入することがコンパイルの段階で許されません。
値を参照する場合も同様でした。
val nonNullLength = javaObject1.nonNullText.length // javaObject1.nullableTextがnullである場合に // nullableLength1にnullが代入される val nullableLength1 = javaObject1.nullableText?.length // javaObject1.nullableTextがnullである場合に // NullPointerExceptionがthrowされる val nullableLength2 = javaObject1.nullableText!!.length // コンパイルが通らない // (Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String) val nullableLength3 = javaObject1.nullableText.length // javaObject1.nonAnnotatedTextがnullである場合に // nonAnnotatedLength1にnullが代入される val nonAnnotatedLength1 = javaObject1.nonAnnotatedText?.length // javaObject1.nonAnnotatedTextがnullである場合に // NullPointerExceptionがThrowされる val nonAnnotatedLength2 = javaObject1.nonAnnotatedText!!.length // javaObject1.nonAnnotatedTextがnullである場合に // NullPointerExceptionがThrowされる val nonAnnotatedLength3 = javaObject1.nonAnnotatedText.length
この場合、KotlinではnonAnnotatedTextはPlatform型として解釈されます。
JavaからKotlinを呼び出す
// @Nullableと注釈されたString型のフィールドnullableTextに // nullでない文字列を持たせることができる final KotlinClass kotlinObject1 = new KotlinClass("nonNullText", "nullableText"); // @NullableとannotateされたString型のフィールドnullableTextに // nullを持たせることもできる final KotlinClass kotlinObject2 = new KotlinClass("nonNullText", null); // @NonNullとannotateされたString型のフィールドnonNullTextに // nullを持たせようとすると警告される // (コンパイルはできる) final KotlinClass kotlinObject3 = new KotlinClass(null, "nullableText");
int nonNullLength = javaObject1.nonNullText.length(); // NullPointerExceptionの可能性があることを警告される int nullableLength1 = javaObject1.nullableLength.length(); // 警告されない // (実際にはNullPointerExceptionの可能性がある) int nullableLength2 = javaObject1.nonAnnotatedText.length()
JavaからJavaのコードを扱うときと同じように扱えることがわかります。
終わりに
今回はNull安全に関する言語仕様の差分について取り扱いましたが、この他にもKotlinはJavaとは異なる言語仕様をいくつも備えており、Javaとの相互運用に関して注意しておくべき点は他にも多数あります。
また機会があれば実験してみて、本テックブログでも取り上げてみようと思います。
詳しくはAndroidやKotlinの公式ドキュメントをご覧ください。
Kotlin-Java interop guide | Android Developers