はじめに
Javaエンジニアなら一度は経験したことがあるはずです。
Exception in thread "main" java.lang.NullPointerException
NullPointerException(NPE)は、Javaの生みの親であるトニー・ホーアが「10億ドルの失敗」と呼んだほど、長年エンジニアを悩ませてきた問題です。
Java 8では Optional が導入されてnullを扱いやすくなりましたが、Kotlinはさらに一歩進んで言語レベルでnullを安全に扱う仕組みを提供しています。
この記事では、JavaのOptionalとKotlinのNull安全を比較しながら、Kotlinがnullをどのように解決しているかを解説します。
Javaのnull問題
まず、Javaでnullがどれほど厄介かを確認します。
public String getUserName(int userId) {
User user = userRepository.findById(userId);
return user.getName(); // userがnullだとNPE!
}
このコードは userRepository.findById() が null を返す可能性があります。しかしコンパイルは通るため、実行時になって初めてNPEが発生します。
nullチェックを追加すると…
public String getUserName(int userId) {
User user = userRepository.findById(userId);
if (user == null) {
return "不明";
}
return user.getName();
}
シンプルな処理なのにnullチェックが増えてコードが膨らみます。さらにネストが深くなると…
// nullチェックの地獄
public String getCityName(int userId) {
User user = userRepository.findById(userId);
if (user != null) {
Address address = user.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
return city.getName();
}
}
}
return "不明";
}
いわゆるネストの地獄です。
JavaのOptionalによる解決
Java 8で導入された Optional<T> は「値が存在するかもしれないし、しないかもしれない」を表すコンテナです。
// Optional を使った例
public Optional<User> findUser(int userId) {
return Optional.ofNullable(userRepository.findById(userId));
}
// 呼び出し側
String name = findUser(1)
.map(User::getName)
.orElse("不明");
Optionalの主なメソッド
Optional<String> opt = Optional.ofNullable(getValue());
// 値が存在するか確認
opt.isPresent(); // true / false
opt.isEmpty(); // isPresent()の逆(Java 11以降)
// 値を取り出す
opt.get(); // 値を取得(空だとNoSuchElementException)
opt.orElse("デフォルト"); // 空なら代替値を返す
opt.orElseGet(() -> "デフォルト"); // 空なら Supplier を実行
opt.orElseThrow(); // 空なら例外をスロー
// 変換・フィルタ
opt.map(String::toUpperCase); // 値を変換
opt.filter(s -> s.length() > 3); // 条件でフィルタ
opt.flatMap(s -> Optional.of(s)); // Optional を返す変換
Optionalのアンチパターン
Optional は便利ですが、誤った使い方も多く見られます。
// ❌ isPresent() + get() はnullチェックと変わらない
if (opt.isPresent()) {
String value = opt.get();
}
// ✅ orElse / map を使う
String value = opt.orElse("デフォルト");
// ❌ フィールドに Optional を使う
public class User {
private Optional<String> email; // NG(シリアライズなどで問題が起きる)
}
// ❌ メソッドの引数に Optional を使う
public void sendEmail(Optional<String> email) { } // NG(呼び出し元が面倒になる)
KotlinのNull安全
Kotlinはnullの問題を型システムに組み込むことで解決しています。
Nullable型と Non-Nullable型
Kotlinでは、型宣言で nullを許可するか を明示します。
var name: String = "田中" // Non-Nullable(nullを代入できない)
var name: String? = null // Nullable(nullを代入できる)
var name: String = "田中"
name = null // コンパイルエラー!
var nullableName: String? = "田中"
nullableName = null // OK
nullを代入できない型とできる型をコンパイル時に区別するため、うっかりnullを代入するミスを防げます。
Nullable型へのアクセスはコンパイルエラー
var name: String? = "田中"
println(name.length) // コンパイルエラー!nullかもしれないから
Kotlinは「この変数はnullかもしれないよ」とコンパイル時に教えてくれます。アクセスするには適切な処理が必要です。
Kotlinのnull安全演算子
?.(安全呼び出し演算子)
nullの場合はnullを返し、非nullの場合だけメソッドを呼び出します。
val name: String? = null
println(name?.length) // null(例外は発生しない)
val name2: String? = "田中"
println(name2?.length) // 2
Javaのnullチェックと比較すると…
// Java
String name = null;
int length = (name != null) ? name.length() : 0;
// Kotlin
val name: String? = null
val length = name?.length ?: 0
?:(エルビス演算子)
nullの場合に使うデフォルト値を指定します。
val name: String? = null
val displayName = name ?: "名無し"
println(displayName) // 名無し
val name2: String? = "田中"
val displayName2 = name2 ?: "名無し"
println(displayName2) // 田中
JavaのOptionalと比較すると…
// Java
String name = null;
String displayName = Optional.ofNullable(name).orElse("名無し");
// Kotlin(はるかに簡潔)
val name: String? = null
val displayName = name ?: "名無し"
ネストしたnullチェックもすっきり
先ほどのJavaのネスト地獄をKotlinで書き直すと…
// Kotlin
fun getCityName(userId: Int): String {
return userRepository.findById(userId)
?.address
?.city
?.name
?: "不明"
}
?. を連鎖させるだけで、どこかが null でも安全に処理できます。
!!(非null断言演算子)
「この変数は絶対nullでない」と断言する演算子です。nullだった場合は NullPointerException が発生します。
val name: String? = "田中"
println(name!!.length) // 2
val name2: String? = null
println(name2!!.length) // NullPointerException!
⚠️
!!は最終手段です。乱用するとKotlinのNull安全の恩恵がなくなります。本当にnullでないことが保証できる場合のみ使いましょう。
スマートキャスト
nullチェックをすると、Kotlinは自動的に型を Non-Nullable として扱います(スマートキャスト)。
val name: String? = getName()
if (name != null) {
// このブロック内では name は String(Non-Nullable)として扱われる
println(name.length) // ?.不要!
}
JavaのOptional vs KotlinのNull安全 比較
| 比較項目 | Java Optional | Kotlin Null安全 |
|---|---|---|
| 仕組み | ライブラリ(ラッパークラス) | 言語の型システム |
| 強制力 | 任意(使わなくてもコンパイル通る) | コンパイル時に強制される |
| 記述量 | やや多い | 少ない(?. ?: で簡潔) |
| nullを返すメソッド | Optional で明示できる |
? で型レベルで明示 |
| パフォーマンス | ラッパーオブジェクトのオーバーヘッドあり | 型情報のみなのでオーバーヘッドなし |
| null断言 |
opt.get()(空でも例外) |
!!(明示的) |
最大の違いは強制力です。Javaの Optional はあくまでコーディング規約で使うものであり、返り値を Optional にしないメソッドが混在してもコンパイルは通ります。KotlinのNull安全は型システムに組み込まれているため、コンパイラが強制します。
JavaのコードをKotlinに変換してみる
// Java
public String getDisplayName(User user) {
if (user == null) {
return "名無し";
}
String name = user.getName();
if (name == null || name.isEmpty()) {
return "名無し";
}
return name.toUpperCase();
}
// Kotlin
fun getDisplayName(user: User?): String {
return user?.name?.takeIf { it.isNotEmpty() }?.uppercase() ?: "名無し"
}
Javaと連携するときの注意点
KotlinはJavaと相互運用できますが、Javaのコードから来る値はプラットフォーム型と呼ばれ、nullかどうかをKotlinが判断できません。
// Javaの String(@Nullable/@NonNull がない場合)は
// Kotlin では String! という「プラットフォーム型」になる
val javaString = javaObject.getString() // String!(nullかもしれない)
// 安全に扱うには明示的にNullable型として受け取る
val safeString: String? = javaObject.getString()
Java側で @Nullable / @NonNull アノテーションをつけると、Kotlin側で適切な型として認識されます。
まとめ
| 演算子 | 意味 | 使いどころ |
|---|---|---|
?(型宣言) |
nullを許容する型 | nullが入る可能性がある変数 |
?. |
nullなら処理をスキップ | Nullable型へのアクセス |
?: |
nullならデフォルト値を使う | nullのときの代替値指定 |
!! |
nullでないと断言 | nullでないことが確実な場合のみ |
- KotlinのNull安全は型システムに組み込まれているため、コンパイル時にnullの危険を検出できる
- JavaのOptionalより記述量が少なく、強制力が強い
-
!!の乱用はNPEの温床になるため避ける - JavaコードとKotlinを連携させる際はプラットフォーム型に注意する
JavaのNPEに悩まされてきた方なら、KotlinのNull安全の快適さをきっと実感できます。