JavaとKotlinにおけるnull許容性
_Null許容性_とは、変数がnull
値を保持できる能力のことです。 変数がnull
を含んでいる場合、その変数を参照解除しようとするとNullPointerException
が発生します。 nullポインタ例外が発生する可能性を最小限に抑えるためにコードを書く方法はたくさんあります。
このガイドでは、JavaとKotlinのnull許容変数へのアプローチの違いについて説明します。 JavaからKotlinへの移行を助け、Kotlinらしいコードを書くのに役立つでしょう。
このガイドの最初のパートでは、最も重要な違いであるKotlinにおけるnull許容型のサポートと、KotlinがJavaコードからの型をどのように処理するかについて説明します。2番目のパートでは、関数呼び出しの結果のチェックから始めて、いくつかの具体的なケースを検証し、特定の違いを解説します。
null許容型のサポート
Kotlinの型システムとJavaの型システムとの最も重要な違いは、Kotlinがnull許容型を明示的にサポートしている点です。 これは、どの変数がnull
値を保持する可能性があるかを示す方法です。 変数がnull
である可能性がある場合、その変数に対してメソッドを呼び出すのは安全ではありません。NullPointerException
を引き起こす可能性があるためです。 Kotlinはコンパイル時にこのような呼び出しを禁止することで、多くの潜在的な例外を防ぎます。 実行時には、null許容型のオブジェクトと非null許容型のオブジェクトは同じように扱われます。 null許容型は非null許容型のラッパーではありません。すべてのチェックはコンパイル時に実行されます。 これは、Kotlinでnull許容型を扱う際の実行時オーバーヘッドがほとんどないことを意味します。
「ほとんど」と述べるのは、組み込み関数のチェックは生成されるものの、 そのオーバーヘッドはごくわずかだからです。
Javaでは、nullチェックを書かないと、メソッドがNullPointerException
をスローすることがあります。
// Java
int stringLength(String a) {
return a.length();
}
void main() {
stringLength(null); // Throws a `NullPointerException`
}
この呼び出しは、以下の出力になります。
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許容型です。 a
がnull
ではないと想定する場合、stringLength()
関数を次のように宣言します。
// Kotlin
fun stringLength(a: String) = a.length
パラメータa
はString
型であり、Kotlinでは常にString
インスタンスを含み、null
を含むことはできないことを意味します。 Kotlinのnull許容型は、String?
のように疑問符?
でマークされます。 コンパイラがstringLength()
のすべての引数がnull
であってはならないというルールを強制するため、a
がString
である場合、実行時にNullPointerException
が発生する状況は不可能です。
null
値をstringLength(a: String)
関数に渡そうとすると、「Null can not be a value of a non-null type String (nullは非null型のStringの値にはなりえません)」というコンパイル時エラーが発生します。
null
を含む任意の引数でこの関数を使用したい場合は、引数型String?
の後に疑問符を付け、関数本体内で引数の値がnull
ではないことを確認します。
// 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
fun stringLength(a: String?): Int = a?.length ?: 0
プラットフォーム型
Javaでは、変数がnull
である可能性があるか、またはnull
ではないかを示すアノテーションを使用できます。 このようなアノテーションは標準ライブラリの一部ではありませんが、別途追加できます。 たとえば、JetBrainsのアノテーションである@Nullable
と@NotNull
(org.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()
メソッドを考えてみましょう。
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
)として宣言する必要があります。
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
// T1は厳密な非null許容型です
override fun load(x: T1 & Any): T1 & Any
}
厳密な非null許容型であるジェネリック型について詳しく学びましょう。
関数呼び出しの結果のチェック
null
チェックが必要となる最も一般的な状況の1つは、関数呼び出しから結果を取得する場合です。
次の例では、Order
とCustomer
の2つのクラスがあります。Order
はCustomer
のインスタンスへの参照を持っています。 findOrder()
関数はOrder
クラスのインスタンスを返すか、注文が見つからない場合はnull
を返します。 目的は、取得した注文の顧客インスタンスを処理することです。
Javaでのクラスは次のとおりです。
//Java
record Order (Customer customer) {}
record Customer (String name) {}
Javaでは、関数を呼び出し、結果に対してif-not-nullチェックを行い、必要なプロパティの参照解除を進めます。
// Java
Order order = findOrder();
if (order != null) {
processCustomer(order.getCustomer());
}
上記のJavaコードを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
val order = findOrder()
order?.let {
processCustomer(it.customer)
}
同じものの短いバージョンは次のとおりです。
// Kotlin
findOrder()?.customer?.let(::processCustomer)
nullの代わりにデフォルト値
null
のチェックは、nullチェックが成功した場合のデフォルト値の設定と組み合わせてよく使用されます。
nullチェックを含むJavaコード:
// Java
Order order = findOrder();
if (order == null) {
order = new Order(new Customer("Antonio"))
}
Kotlinで同じことを表現するには、エルビス演算子 (If-not-null-else shorthand)を使用します。
// Kotlin
val order = findOrder() ?: Order(Customer("Antonio"))
値またはnullを返す関数
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で一般的です。
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
var numbers = new ArrayList<Integer>();
var max = numbers.stream().max(Comparator.naturalOrder()).orElse(null);
System.out.println("Max: " + max);
Kotlinでは、集計演算を使用します。
// Kotlin
val numbers = listOf<Int>()
println("Max: ${numbers.maxOrNull()}")
JavaとKotlinのコレクションについて詳しく学びましょう。
型を安全にキャストする
型を安全にキャストする必要がある場合、Javaではinstanceof
演算子を使用し、その後それがどれくらいうまく機能したかをチェックします。
// 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
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
を使用できます。 しかし、そのような関数に負の値を返させ、その値をチェックする方がリソース効率が良いです。 いずれにせよチェックは行いますが、この方法では追加のボックス化は実行されません。
次のステップ
- 他のKotlinイディオムを参照してください。
- Java-to-Kotlin (J2K) コンバーターを使用して、既存のJavaコードをKotlinに変換する方法を学びましょう。
- その他の移行ガイドを確認してください。
お気に入りのイディオムがあれば、プルリクエストを送ってぜひ共有してください!