JavaとKotlinの相互運用 Null安全篇

VoicyでAndroidエンジニアをやっております、おはぎです。

先日案内させていただいた2025GWブログリレーの先鋒を務めます。

tech-blog.voicy.jp

みなさんゴールデンウィークはいかに過ごされましたか?

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というWrapperクラスを使うこともできます。

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

Calling Java from Kotlin | Kotlin Documentation

Calling Kotlin from Java | Kotlin Documentation