Java と Kotlin における Null 許容性
「Null 許容性(Nullability)」とは、変数が null 値を保持できる能力のことです。 変数に null が含まれているときに、その変数をデリファレンスしようとすると NullPointerException が発生します。 Null ポインター例外が発生する確率を最小限に抑えるために、コードを記述する方法はたくさんあります。
このガイドでは、Null 許容である可能性のある変数の扱いに関する、Java と Kotlin のアプローチの違いについて説明します。 これにより、Java から Kotlin への移行を助け、Kotlin らしい(オーセンティックな)スタイルでコードを記述できるようになります。
このガイドの最初の部分では、最も重要な違いである Kotlin での Null 許容型のサポートと、Kotlin が Java コードからの型をどのように処理するかについて説明します。 「関数呼び出しの結果のチェック」から始まる第 2 部では、いくつかの具体的なケースを検討し、特定の違いについて解説します。
Null 許容型のサポート
Kotlin と Java の型システムの最も重要な違いは、Kotlin が Null 許容型(nullable types)を明示的にサポートしていることです。 これは、どの変数が null 値を保持する可能性があるかを示す方法です。 変数が null になる可能性がある場合、その変数に対してメソッドを呼び出すことは NullPointerException を引き起こす可能性があるため、安全ではありません。 Kotlin はコンパイル時にこのような呼び出しを禁止することで、多くの発生しうる例外を防ぎます。 実行時には、Null 許容型のオブジェクトと非 Null 型のオブジェクトは同じように扱われます。 Null 許容型は非 Null 型のラッパーではありません。すべてのチェックはコンパイル時に行われます。 つまり、Kotlin で Null 許容型を扱う際の実行時のオーバーヘッドはほとんどありません。
「ほとんど」と言ったのは、組み込みの(intrinsic)チェックは生成されますが、そのオーバーヘッドは最小限であるためです。
Java では、Null チェックを記述しない場合、メソッドが NullPointerException をスローすることがあります。
// Java
int stringLength(String a) {
return a.length();
}
void main() {
stringLength(null); // `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? のように疑問符 ? を付けてマークされます。 a が String であれば、コンパイラが stringLength() のすべての引数が null でないという規則を強制するため、実行時に NullPointerException が発生する状況はあり得ません。
stringLength(a: String) 関数に null 値を渡そうとすると、「Null can not be a value of a non-null type 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?」というメッセージが表示されます。
これと同じことをより短く記述できます。安全呼び出し演算子 ?. (If-not-null の短縮表記)を使用します。これにより、Null チェックとメソッド呼び出しを単一の操作に組み合わせることができます。
// Kotlin
fun stringLength(a: String?): Int = a?.length ?: 0プラットフォーム型
Java では、変数が null になるかどうかを示すアノテーションを使用できます。 このようなアノテーションは標準ライブラリの一部ではありませんが、別途追加することができます。 例えば、JetBrains のアノテーションである @Nullable や @NotNull(org.jetbrains.annotations パッケージから)、JSpecify のアノテーション(org.jspecify.annotations)、または Eclipse のアノテーション(org.eclipse.jdt.annotation)を使用できます。 Kotlin は、Kotlin コードから Java コードを呼び出す際にこのようなアノテーションを認識し、そのアノテーションに従って型を扱います。
Java コードにこれらのアノテーションがない場合、Kotlin は Java の型を プラットフォーム型(platform types) として扱います。 しかし、Kotlin はそのような型に対する Null 許容性の情報を持っていないため、コンパイラはそれらに対するすべての操作を許可します。 以下の理由から、Null チェックを行うかどうかを判断する必要があります。
- Java と同様に、
nullに対して操作を実行しようとするとNullPointerExceptionが発生します。 - コンパイラは、非 Null 型の値に対して Null 安全な操作を行ったときに通常表示される、冗長な Null チェックの警告を表示しません。
Null 安全とプラットフォーム型に関する、Kotlin からの Java 呼び出しについて詳しくはこちら。
確定的な非 Null 型のサポート
Kotlin で、引数として @NotNull を含む Java メソッドをオーバーライドしたい場合、Kotlin の確定的な非 Null 型(definitely non-nullable types)が必要になります。
例えば、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 型(definitely non-nullable types)であるジェネリック型について詳しくはこちら。
関数呼び出しの結果のチェック
null のチェックが必要になる最も一般的な状況の 1 つは、関数呼び出しから結果を取得するときです。
次の例では、Order と Customer という 2 つのクラスがあります。Order は Customer のインスタンスへの参照を持っています。 findOrder() 関数は Order クラスのインスタンスを返しますが、注文が見つからない場合は null を返します。 目的は、取得した注文の顧客(customer)インスタンスを処理することです。
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()
// 直接的な変換
if (order != null){
processCustomer(order.customer)
}安全呼び出し演算子 ?. (If-not-null の短縮表記)を標準ライブラリのスコープ関数のいずれかと組み合わせて使用します。これには通常、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 の短縮表記)を使用します。
// 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) // 例外!Kotlin 標準ライブラリは、null 値を返す可能性があるかどうかを名前で示す関数をしばしば提供しています。これは、特にコレクション API で一般的です。
fun main() {
// Kotlin
// Java と同じコード:
val numbers = listOf(1, 2)
println(numbers[0]) // コレクションが空の場合、IndexOutOfBoundsException をスローする可能性がある
//numbers.get(5) // 例外!
// その他の機能:
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)); // `-1` を出力
}Kotlin で例外を回避するには、失敗時に null を返す安全なキャスト演算子 as? を使用します。
// Kotlin
fun main() {
println(getStringLength(1)) // `-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を使用できます。 しかし、このような関数には負の値を返させ、その値をチェックさせる方がリソース効率が良いです。いずれにせよチェックは行いますが、この方法では追加のボクシングは行われません。
Java コードを Kotlin に移行する際、最初は元のコードのセマンティクスを維持するために、Null 許容型に対して通常のキャスト演算子 as を使用したくなるかもしれません。しかし、より安全で Kotlin らしいアプローチとして、安全なキャスト演算子 as? を使用するようにコードを適応させることをお勧めします。例えば、次のような Java コードがあるとします。
public class UserProfile {
Object data;
public static String getUsername(UserProfile profile) {
if (profile == null) {
return null;
}
return (String) profile.data;
}
}これを as 演算子で直接移行すると、次のようになります。
class UserProfile(var data: Any? = null)
fun getUsername(profile: UserProfile?): String? {
if (profile == null) {
return null
}
return profile.data as String?
}ここでは、profile.data は as String? を使用して Null 許容の文字列にキャストされています。
さらに一歩進んで、値を安全にキャストするために as? String を使用することをお勧めします。このアプローチでは、ClassCastException をスローする代わりに、失敗時に null を返します。
class UserProfile(var data: Any? = null)
fun getUsername(profile: UserProfile?): String? =
profile?.data as? Stringこのバージョンでは、if 式を安全呼び出し演算子 ?. に置き換えています。これにより、キャストを試みる前にデータプロパティに安全にアクセスできます。
次のステップ
- その他の Kotlin の慣用句(イディオム)をブラウズする。
- Java-to-Kotlin (J2K) コンバーターを使用して、既存の Java コードを Kotlin に変換する方法を学ぶ。
- その他の移行ガイドを確認する:
お気に入りのイディオムがあれば、プルリクエストを送ってぜひ共有してください!
