Skip to content

JavaとKotlinにおけるnull許容性

_Null許容性_とは、変数がnull値を保持できる能力のことです。 変数がnullを含んでいる場合、その変数を参照解除しようとするとNullPointerExceptionが発生します。 nullポインタ例外が発生する可能性を最小限に抑えるためにコードを書く方法はたくさんあります。

このガイドでは、JavaとKotlinのnull許容変数へのアプローチの違いについて説明します。 JavaからKotlinへの移行を助け、Kotlinらしいコードを書くのに役立つでしょう。

このガイドの最初のパートでは、最も重要な違いであるKotlinにおけるnull許容型のサポートと、KotlinがJavaコードからの型をどのように処理するかについて説明します。2番目のパートでは、関数呼び出しの結果のチェックから始めて、いくつかの具体的なケースを検証し、特定の違いを解説します。

Kotlinのnull安全性についてさらに詳しく学ぶ

null許容型のサポート

Kotlinの型システムとJavaの型システムとの最も重要な違いは、Kotlinがnull許容型を明示的にサポートしている点です。 これは、どの変数がnull値を保持する可能性があるかを示す方法です。 変数がnullである可能性がある場合、その変数に対してメソッドを呼び出すのは安全ではありません。NullPointerExceptionを引き起こす可能性があるためです。 Kotlinはコンパイル時にこのような呼び出しを禁止することで、多くの潜在的な例外を防ぎます。 実行時には、null許容型のオブジェクトと非null許容型のオブジェクトは同じように扱われます。 null許容型は非null許容型のラッパーではありません。すべてのチェックはコンパイル時に実行されます。 これは、Kotlinでnull許容型を扱う際の実行時オーバーヘッドがほとんどないことを意味します。

「ほとんど」と述べるのは、組み込み関数のチェックは生成されるものの、 そのオーバーヘッドはごくわずかだからです。

Javaでは、nullチェックを書かないと、メソッドがNullPointerExceptionをスローすることがあります。

java
// Java
int stringLength(String a) {
    return a.length();
}

void main() {
    stringLength(null); // Throws a `NullPointerException`
}

この呼び出しは、以下の出力になります。

java
java.lang.NullPointerException: Cannot invoke "String.length()" because "a" is null
    at test.java.Nullability.stringLength(Nullability.java:8)
    at test.java.Nullability.main(Nullability.java:12)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Kotlinでは、すべての通常の型は、明示的にnull許容型としてマークしない限り、デフォルトで非null許容型です。 anullではないと想定する場合、stringLength()関数を次のように宣言します。

kotlin
// Kotlin
fun stringLength(a: String) = a.length

パラメータaString型であり、Kotlinでは常にStringインスタンスを含み、nullを含むことはできないことを意味します。 Kotlinのnull許容型は、String?のように疑問符?でマークされます。 コンパイラがstringLength()のすべての引数がnullであってはならないというルールを強制するため、aStringである場合、実行時にNullPointerExceptionが発生する状況は不可能です。

null値をstringLength(a: String)関数に渡そうとすると、「Null can not be a value of a non-null type String (nullは非null型のStringの値にはなりえません)」というコンパイル時エラーが発生します。

Nullを非null許容関数に渡すエラー

nullを含む任意の引数でこの関数を使用したい場合は、引数型String?の後に疑問符を付け、関数本体内で引数の値がnullではないことを確認します。

kotlin
// Kotlin
fun stringLength(a: String?): Int = if (a != null) a.length else 0

チェックが正常にパスされた後、コンパイラは、チェックが実行されるスコープ内でその変数を非null許容型のStringであるかのように扱います。

このチェックを行わない場合、コードは「Only safe (?.) or non-nullable asserted (!!.) calls are allowed on a nullable receiver of type String? (String?型のnull許容レシーバーでは、セーフコール(?.)または非nullアサート(!!.)による呼び出しのみが許可されます)」というメッセージでコンパイルに失敗します。

同じことをより短く書くことができます。nullチェックとメソッド呼び出しを単一の操作に結合できるセーフコール演算子 ?. (If-not-null shorthand)を使用します。

kotlin
// Kotlin
fun stringLength(a: String?): Int = a?.length ?: 0

プラットフォーム型

Javaでは、変数がnullである可能性があるか、またはnullではないかを示すアノテーションを使用できます。 このようなアノテーションは標準ライブラリの一部ではありませんが、別途追加できます。 たとえば、JetBrainsのアノテーションである@Nullable@NotNullorg.jetbrains.annotationsパッケージから)や、Eclipse(org.eclipse.jdt.annotation)のアノテーションを使用できます。 Kotlinは、KotlinコードからJavaコードを呼び出す際にこれらのアノテーションを認識し、そのアノテーションに従って型を扱います。

Javaコードにこれらのアノテーションがない場合、KotlinはJavaの型を_プラットフォーム型_として扱います。 しかし、Kotlinにはこのような型のnull許容性情報がないため、そのコンパイラはそれらに対するすべての操作を許可します。 nullチェックを実行するかどうかは、自分で判断する必要があります。なぜなら、

  • Javaと同じように、nullに対して操作を実行しようとするとNullPointerExceptionが発生します。
  • コンパイラは、非null許容型の値に対してnull安全な操作を実行する際に通常行うような、冗長なnullチェックを強調表示しません。

null安全性とプラットフォーム型に関してKotlinからJavaを呼び出す方法について詳しく学んでください。

厳密な非null許容型のサポート

Kotlinでは、引数に@NotNullを含むJavaメソッドをオーバーライドしたい場合、Kotlinの厳密な非null許容型が必要になります。

たとえば、Javaのこのload()メソッドを考えてみましょう。

java
import org.jetbrains.annotations.*;

public interface Game<T> {
  public T save(T x) {}
  @NotNull
  public T load(@NotNull T x) {}
}

Kotlinでload()メソッドを正常にオーバーライドするには、T1を厳密な非null許容型(T1 & Any)として宣言する必要があります。

kotlin
interface ArcadeGame<T1> : Game<T1> {
  override fun save(x: T1): T1
  // T1は厳密な非null許容型です
  override fun load(x: T1 & Any): T1 & Any
}

厳密な非null許容型であるジェネリック型について詳しく学びましょう。

関数呼び出しの結果のチェック

nullチェックが必要となる最も一般的な状況の1つは、関数呼び出しから結果を取得する場合です。

次の例では、OrderCustomerの2つのクラスがあります。OrderCustomerのインスタンスへの参照を持っています。 findOrder()関数はOrderクラスのインスタンスを返すか、注文が見つからない場合はnullを返します。 目的は、取得した注文の顧客インスタンスを処理することです。

Javaでのクラスは次のとおりです。

java
//Java
record Order (Customer customer) {}

record Customer (String name) {}

Javaでは、関数を呼び出し、結果に対してif-not-nullチェックを行い、必要なプロパティの参照解除を進めます。

java
// Java
Order order = findOrder();

if (order != null) {
    processCustomer(order.getCustomer());
}

上記のJavaコードをKotlinコードに直接変換すると、次のようになります。

kotlin
// Kotlin
data class Order(val customer: Customer)

data class Customer(val name: String)

val order = findOrder()

// Direct conversion
if (order != null){
    processCustomer(order.customer)
}

セーフコール演算子 ?. (If-not-null shorthand)を標準ライブラリのいずれかのスコープ関数と組み合わせて使用します。 通常、これにはlet関数が使用されます。

kotlin
// Kotlin
val order = findOrder()

order?.let {
    processCustomer(it.customer)
}

同じものの短いバージョンは次のとおりです。

kotlin
// Kotlin
findOrder()?.customer?.let(::processCustomer)

nullの代わりにデフォルト値

nullのチェックは、nullチェックが成功した場合のデフォルト値の設定と組み合わせてよく使用されます。

nullチェックを含むJavaコード:

java
// Java
Order order = findOrder();
if (order == null) {
    order = new Order(new Customer("Antonio"))
}

Kotlinで同じことを表現するには、エルビス演算子 (If-not-null-else shorthand)を使用します。

kotlin
// Kotlin
val order = findOrder() ?: Order(Customer("Antonio"))

値またはnullを返す関数

Javaでは、リスト要素を扱う際に注意が必要です。要素を使用しようとする前に、常にインデックスに要素が存在するかどうかをチェックする必要があります。

java
// Java
var numbers = new ArrayList<Integer>();
numbers.add(1);
numbers.add(2);

System.out.println(numbers.get(0));
//numbers.get(5) // Exception!

Kotlinの標準ライブラリは、null値を返す可能性があるかどうかを示す関数名をよく提供しています。 これは特にコレクションAPIで一般的です。

kotlin
fun main() {
    // Kotlin
    // Javaと同じコード:
    val numbers = listOf(1, 2)
    
    println(numbers[0])  // コレクションが空の場合、IndexOutOfBoundsExceptionをスローする可能性がある
    //numbers.get(5)     // Exception!

    // その他の機能:
    println(numbers.firstOrNull())
    println(numbers.getOrNull(5)) // null
}

集計演算

要素がない場合に最大要素またはnullを取得する必要がある場合、JavaではStream APIを使用します。

java
// Java
var numbers = new ArrayList<Integer>();
var max = numbers.stream().max(Comparator.naturalOrder()).orElse(null);
System.out.println("Max: " + max);

Kotlinでは、集計演算を使用します。

kotlin
// Kotlin
val numbers = listOf<Int>()
println("Max: ${numbers.maxOrNull()}")

JavaとKotlinのコレクションについて詳しく学びましょう。

型を安全にキャストする

型を安全にキャストする必要がある場合、Javaではinstanceof演算子を使用し、その後それがどれくらいうまく機能したかをチェックします。

java
// Java
int getStringLength(Object y) {
    return y instanceof String x ? x.length() : -1;
}

void main() {
    System.out.println(getStringLength(1)); // Prints `-1`
}

Kotlinで例外を避けるには、失敗時にnullを返すセーフキャスト演算子as?を使用します。

kotlin
// Kotlin
fun main() {
    println(getStringLength(1)) // Prints `-1`
}

fun getStringLength(y: Any): Int {
    val x: String? = y as? String // null
    return x?.length ?: -1 // `x`がnullのため、-1を返す
}

上記のJavaの例では、getStringLength()関数はプリミティブ型intの結果を返します。 nullを返すようにするには、_ボックス化された_型Integerを使用できます。 しかし、そのような関数に負の値を返させ、その値をチェックする方がリソース効率が良いです。 いずれにせよチェックは行いますが、この方法では追加のボックス化は実行されません。

次のステップ

お気に入りのイディオムがあれば、プルリクエストを送ってぜひ共有してください!