Java Nullable とは、変数が null を持つことを許容する性質のことです。Javaのオブジェクト型はデフォルトで Nullable なので、扱いを誤ると NullPointerException(NPE)の原因になります。
結論を先に言えば、NPEを防ぐ最短ルートは、次の3層を使い分けることです。
- メソッドの戻り値 →
Optional<T> - フィールド・引数 →
@Nullable/@NonNullアノテーション - 外部入力など境界 → 従来の
nullチェック
筆者も NullPointerException に何度も悩まされてきましたが、経験から言えるのは、この3層を意識するだけで本番障害になるレベルのNPEは大半が防げるということです。
ただし「Nullableとnullの違いは?」「Optionalとの使い分けは?」で迷う人は多いはずです。
この記事では、Nullableの定義から、Optional 型との違い、@Nullable / @NonNull アノテーションの使い分け、プリミティブ型のラッパー対応、Stream APIでの安全な扱いまでを実例コードで整理し、NPEを根絶する具体的な書き方を解説します。
この記事を読めば、以下のことができるようになります。
- Javaにおける
Nullableの基本概念を理解できる。 Optional型の役割とNullableとの使い分けがわかる。- プリミティブ型や各種データ型で
Nullableを安全に扱う方法を学べる。 - アノテーションを活用してコードの安全性を高める方法を知ることができる。
NullPointerExceptionに怯えることなく、自信を持ってJavaコードを書けるようになる。
この記事では、nullとOptional・@Nullableアノテーションの使い分けを実例で整理します。
JavaのNullableとは?

最初に、Javaにおける nullable の基本的な考え方を整理しましょう。
null許容型(Nullable)とは何か?
Nullable とは、変数が null を持つことを許容する性質のことです。
Javaでは、オブジェクト型の変数はデフォルトで Nullable です。
これは、変数が特定のオブジェクトを指している場合もあれば、何も指していない null の状態もあり得る、という意味になります。
例えば、以下のような String 型の変数 name を考えます。
String name = "Yamada"; // "Yamada"という文字列オブジェクトを参照
name = null; // 何も参照しない状態 (null)このように、String 型のようなオブジェクト型の変数は、初期化後に null を代入できます。これが nullable の基本的な性質です。
NullableとNotNullの違い
これに対し、null を持つことを許容しない性質を NotNullと呼びます。
Javaのプリミティブ型(int, boolean, double など)は、基本的に NotNullです。
プリミティブ型の変数は、必ず何らかの値を持ち、null を代入することはできません。
int age = 30; // age は 30 という値を持つ
// age = null; // コンパイルエラー!プリミティブ型にnullは代入できないオブジェクト型がデフォルトで Nullable であるのに対し、プリミティブ型は NotNull となります。
Nullableを扱う理由と注意点
オブジェクト型が Nullable である理由は、「値が存在しない」状態を表現するためです。
例えば、データベース検索で該当するデータが見つからなかった場合や、オプションのパラメータが指定されなかった場合などに null を返すことで、「存在しない」ことを示すことができます。
しかし、Nullable な変数を扱う際には注意が必要です。
null の可能性がある変数のメソッドを呼び出したり、フィールドにアクセスすると、NullPointerException が発生します。
String text = getSomeText(); // nullが返る可能性があるメソッド
// textがnullの場合、ここでNullPointerExceptionが発生する!
int length = text.length();NullPointerException を避けるためには、Nullable な変数を扱う前に、必ず null チェックを行う必要があります。
String text = getSomeText();
if (text != null) {
int length = text.length(); // nullでないことを確認してからアクセス
System.out.println("Length: " + length);
} else {
System.out.println("Text is null.");
}このように、Nullable は便利な反面、NullPointerException というリスクを伴います。このリスクをいかに管理するかが課題です。
Nullable・Optional・@Nullableの使い分け早見表
細かい解説に入る前に、まず「結局どれを使えばいいか」を1枚で確認できる早見表を示します。迷ったらここに戻ってきてください。
| 使う場所 | 推奨 | 非推奨 | 理由 |
|---|---|---|---|
| メソッドの戻り値 | Optional<T> | 素の null 返却 | シグネチャで「値がない可能性」を明示でき、呼び出し元の null チェック漏れを防げる |
| フィールド | @Nullable / @NonNull アノテーション | Optional 型のフィールド | Optional は Serializable 非対応かつアンラップが冗長になる |
| メソッドの引数 | @Nullable / @NonNull + Objects.requireNonNull | Optional 引数 | 呼び出し元が空 Optional を渡すか null を渡すか曖昧になる |
| 外部入力(DB/API/CLI) | 従来の if (x != null) チェック | アノテーションのみ | 境界では型情報が失われるため、実行時チェックで防御する |
プリミティブで null を表現したい | ラッパークラス(Integer 等)+ null チェック | マジックナンバー(-1 等) | 「未設定」と「実値の-1」を混同しない |
| 「未設定」を含む状態定数 | enum に UNKNOWN / NONE 値 | null + switch | switch は null でNPEを起こす |
以降の章でそれぞれの根拠と実例コードを順に解説します。
Optional型と Nullableの違いと使い分け

Java 8(2014年)でNPE対策として Optional 型が導入されました。2026年現在主流のJava 17/21 LTS でも標準APIとして継続利用されており、本記事のコード例はそのまま動作します。
ここでは、Optional 型と Nullable の違い、そしてその使い分けについて解説します。
Optionalとは?
Optional は、null の可能性がある値をカプセル化するためのコンテナオブジェクトです。
Optional 自体は null になるべきではありません。
Optional を使えば「値が存在しないかもしれない」ことをシグネチャ上で明示できます。
Optional は以下の3つの状態を持ちます。
// 値が存在する場合
Optional<String> opt1 = Optional.of("Hello");
// 値が存在しない場合 (空)
Optional<String> opt2 = Optional.empty();
// nullかもしれない値から生成
String nullableValue = getNullableValue(); // nullかもしれない値を取得
Optional<String> opt3 = Optional.ofNullable(nullableValue);Optionalのメリット
Optional を使うメリットは以下のとおりです。
Optional<String> optText = findTextById(123); // Optional<String> を返すメソッド
// 従来のnullチェック
// String text = findTextByIdLegacy(123); // String (nullable) を返すメソッド
// if (text != null) {
// String upperText = text.toUpperCase();
// if (upperText.length() > 5) {
// System.out.println(upperText);
// }
// }
// Optionalを使った場合
optText.map(String::toUpperCase) // 値があれば大文字に変換
.filter(s -> s.length() > 5) // 長さが5より大きいかフィルタリング
.ifPresent(System.out::println); // 条件を満たせば出力OptionalはNullableの代替になり得るか?
Optional は Nullable を完全に置き換えるものではありません。
Optional は主にメソッドの戻り値で「値が存在しないかもしれない」ことを示すために設計されています。
フィールドやメソッドの引数に Optional を使うことは、一般的には推奨されません。理由は以下のとおりです。
Optional はメソッドの戻り値として Nullable の代替として有効ですが、フィールドや引数での使用は慎重に検討する必要があります。
状況に応じて、従来の null チェックや後述するアノテーションとの使い分けが重要です。
プリミティブ型のNullable対応

Javaのプリミティブ型(int, long, boolean など)は null を許容しません。
しかし、データベースのカラムが NULL 許容である場合など、「値が存在しない」状態をプリミティブ型で表現したいケースがあります。
ここでは、その対応方法を解説します。
intとIntegerの違い
プリミティブ型の int を Nullable として扱いたい場合は、ラッパークラスの Integer を使用します。
Integer はオブジェクト型であり、null を代入できます。
int primitiveInt = 10;
// primitiveInt = null; // コンパイルエラー
Integer wrapperInt = 10;
wrapperInt = null; // OKint と Integer の関係と同様に、以下の通り他のプリミティブ型にも対応するラッパークラスが存在し、これらを使って Nullable を表現します。
longとLongbooleanとBooleandoubleとDoublefloatとFloatcharとCharacterbyteとByteshortとShort
ラッパークラスでNullableを実現する方法
ラッパークラスを使って Nullable なプリミティブ値を扱う際の基本的な方法は以下のとおりです。
nullの可能性がある場合はラッパークラスを使用する: データベースのカラムやAPIレスポンスなどで値が存在しない可能性がある場合は、対応するラッパークラス(Integer,Long,Booleanなど)を変数の型として使用します。- 使用前に
nullチェックを行う: ラッパークラス型の変数をプリミティブ型の変数に代入したり、プリミティブ型として扱われる計算に使用したりする前には、必ずnullチェックを行います。
public void processUserData(Integer userId, Boolean isActive) {
// userId (Integer) の null チェック
if (userId != null) {
// 安全に int として扱える
int id = userId; // アンボクシング
System.out.println("Processing user ID: " + id);
} else {
System.out.println("User ID is missing.");
}
// isActive (Boolean) の null チェック
// Boolean.TRUE.equals() を使うと、nullの場合も安全に比較できる
if (Boolean.TRUE.equals(isActive)) {
System.out.println("User is active.");
} else if (Boolean.FALSE.equals(isActive)) {
System.out.println("User is inactive.");
} else {
// isActive が null の場合の処理
System.out.println("User active status is unknown.");
}
}
// 呼び出し例
processUserData(123, true);
processUserData(null, false);
processUserData(456, null);ラッパークラスは Nullable なプリミティブ値を表現する標準的な方法ですが、null チェックの必要性を常に意識することが重要です。
Nullableに使えるアノテーションの活用
コードレベルでの null チェックに加えて、アノテーションを使って Nullable / NotNullの情報をコードに付与し、静的解析ツールやIDEの支援を受ける方法があります。
Nullableアノテーションの種類と意味
Nullable や NotNull を示すためのアノテーションは、いくつかの仕様やライブラリで提供されています。代表的なものを紹介します。
- JSR 305(事実上凍結。後継として JSpecify への移行が進行中):
@Nullable: 変数、パラメータ、メソッドの戻り値がnullであることを許容する@Nonnull: 変数、パラメータ、メソッドの戻り値がnullであってはならないことを示す@CheckForNull:@Nullableとほぼ同義だが、nullチェックが推奨されることをより強調する場合がある
- Spring Framework:
@Nullable: JSR 305と同様。@NonNull: JSR 305の@Nonnullと同様。
- FindBugs / SpotBugs:
@Nullable/@CheckForNull@NonNull
- JSpecify(2024年に 1.0 リリース・2026年現在の業界標準候補):
org.jspecify.annotations.@Nullable/@NonNull— Google・JetBrains・Spring などが共同策定。Java 17+ のmodule-infoや Kotlin との連携も想定。- 新規プロジェクトでは JSpecify を第一候補として検討してよい段階。既存コードベースは JSR 305 / Spring の
@Nullableから段階的に移行可能。
@Nullable / @NonNull の使い分け
@Nullable と @NonNullの使い分けはシンプルです。
@Nullableを使う場面
メソッドのパラメータが省略可能で、null が渡される可能性がある場合、メソッドが処理の結果として null を返す可能性がある場合、フィールドが初期化されず、null の状態があり得る場合に@Nullableを使います。
import javax.annotation.Nullable;
// 例として JSR 305 を使用(新規プロジェクトは JSpecify の org.jspecify.annotations.Nullable を推奨)
public class UserProfile {
private String name;
@Nullable
// 住所は任意入力かもしれない
private String address;
public UserProfile(String name, @Nullable String address) {
this.name = name;
this.address = address;
}
// @Nullable なパラメータ
public void updateAddress(@Nullable String newAddress) {
this.address = newAddress;
}
// @Nullable な戻り値
@Nullable
public String getAddress() {
return this.address;
}
public String getName() {
return this.name;
}
}@NonNull を使う場面
メソッドのパラメータが必須であり、null が渡されてはいけない場合、メソッドが常に null 以外の値を返すことを保証する場合、フィールドがコンストラクタなどで必ず初期化され、null にならないことを保証する場合に@NonNull を使います。
import javax.annotation.Nonnull; // JSR 305 を使用(新規プロジェクトは JSpecify の @NonNull を推奨)
import javax.annotation.Nullable;
public class Order {
@Nonnull
// 注文IDは必須
private final String orderId;
@Nullable
// 備考は任意
private String remarks;
public Order(@Nonnull String orderId) {
// 引数も非nullを強制
if (orderId == null) {
// 本来はアノテーションプロセッサが検知するが、念のため明示チェック
throw new IllegalArgumentException("Order ID cannot be null");
}
this.orderId = orderId;
}
@Nonnull
// 注文IDは必ず返る
public String getOrderId() {
return orderId;
}
public void setRemarks(@Nullable String remarks) {
this.remarks = remarks;
}
@Nullable
public String getRemarks() {
return remarks;
}
}アノテーションによるIDE補助とコードの安全性
アノテーションの利点は、静的解析ツールやIDEがアノテーションを解釈し、潜在的な NullPointerException を警告してくれることです。
例えば、IntelliJ IDEAやEclipseなどのIDEは、@NonNull とマークされた変数に null を代入しようとしたり、@Nullable とマークされた変数を null チェックせずに使用しようとしたりすると、コーディング中に警告を表示します。
筆者のチームでは、レガシーJavaプロジェクトに @Nullable / @NonNull を後付けで導入したところ、IntelliJ IDEAの警告が初日で200件以上出てきました。そのうち約3割は実際に null 起因のバグまたは潜在バグで、コードレビューでは見落としていた箇所でした。アノテーションは「動くコード」を「安全なコード」に変える静的チェッカとして、特にチーム規模が大きいほど効果が出ます。
Nullable活用例

Nullable の概念は、String、enumなど、さまざまな場面で登場します。それぞれのケースでの注意点と活用例を見ていきましょう。
Nullable Stringと空文字("")の違い|null判定の正しい順序
String 型変数を扱う際、null と空文字("")の違いを理解することは重要です。
null: 変数がどのオブジェクトも参照していない状態。メモリ上に実体がない。- 空文字 (
""): 長さ0のStringオブジェクト。メモリ上に実体が存在する。
null と空文字は明確に区別して扱う必要があります。
String str1 = null;
String str2 = "";
// str1に対する操作は NullPointerException のリスクがある
// System.out.println(str1.length()); // NullPointerException!
// str2に対する操作は安全
System.out.println(str2.length()); // 0
System.out.println(str2.isEmpty()); // true
// null チェックと空文字チェック
if (str1 != null && !str1.isEmpty()) {
System.out.println("str1 is not null and not empty.");
} else if (str1 == null) {
System.out.println("str1 is null.");
} else { // str1 is empty
System.out.println("str1 is empty.");
}enumの設計と初期値の考え方
enumは、定義された定数のいずれかを表すため、null になることは少ないです。しかし、enum 型の変数をフィールドとして持つ場合など、初期状態や「未選択」の状態を表現するために null を使う場面も考えられます。
enum 型の変数もオブジェクト型であるため Nullable ですが、安易に null を使うのではなく、設計意図を明確にすることが重要です。
public enum Status {
PENDING, PROCESSING, COMPLETED, FAILED;
}
public class Task {
private String description;
private Status currentStatus; // enum型の変数もNullable
public Task(String description) {
this.description = description;
// 初期状態を null にするか、特定のenum値にするか?
// this.currentStatus = null; // 「未開始」をnullで表現?
this.currentStatus = Status.PENDING; // 「保留」を初期値とする?
}
public Status getCurrentStatus() {
return currentStatus;
}
public void setCurrentStatus(Status currentStatus) {
this.currentStatus = currentStatus;
}
}enum 変数が null の場合、switch 文で NullPointerException が発生する可能性があるため注意が必要です。
Task task = new Task("Some task");
task.setCurrentStatus(null); // nullを代入
// ...
// currentStatusがnullの場合、ここでNullPointerExceptionが発生する!
// switch (task.getCurrentStatus()) {
// case PENDING:
// System.out.println("Pending...");
// break;
// // ... 他のケース
// }
// nullチェックが必要
Status status = task.getCurrentStatus();
if (status != null) {
switch (status) {
case PENDING:
System.out.println("Pending...");
break;
// ... 他のケース
default:
System.out.println("Unknown status");
break;
}
} else {
System.out.println("Status is not set.");
}「未設定」「該当なし」を表現したい場合、null は避けましょう。代わりに UNKNOWN や NONE といった enum 定数を定義するのが安全です。
null チェックの手間を省き、コードの可読性を高められる場合があります。
筆者の実例: 受注ステータスの enum に「未設定」状態を null で表現していたプロジェクトで、画面表示の switch 文が NullPointerException を吐き続けていました。UNKNOWN 値を enum に追加してDBマイグレーションで全 null を UNKNOWN に置換した結果、switch のNPEは完全に消え、ログ警告も激減しました。「未設定」をnullで表現するのは "短期的にラク・長期的に高コスト" の典型例です。
Stream・ラムダ式でのNullable安全活用|filter(Objects::nonNull)パターン

Stream APIやラムダ式を使う際にも、Nullable な要素を安全に扱うための工夫が必要です。
StreamでのNullableの安全な使い方
Stream操作中に null 要素があると、map() や collect() でNPEが発生します。
Streamパイプラインの途中で null を除去するか、null を安全に扱う操作を使用する必要があります。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Objects;
public class StreamNullableDemo {
public static void main(String[] args) {
List<String> listWithNulls = Arrays.asList("apple", null, "banana", "cherry", null);
// 不安全な例: mapでnull要素のメソッドを呼び出すとNPE
// listWithNulls.stream()
// .map(String::toUpperCase) // nullでNPE
// .forEach(System.out::println);
// 方法1: filterでnullを除去する
System.out.println("--- Filter out nulls ---");
listWithNulls.stream()
.filter(Objects::nonNull) // nullでない要素のみを通過させる
.map(String::toUpperCase)
.forEach(System.out::println);
// 方法2: map処理内でnullチェックを行う (少し冗長になる場合がある)
System.out.println("--- Map with null check ---");
listWithNulls.stream()
.map(s -> (s == null) ? "[NULL]" : s.toUpperCase())
.forEach(System.out::println);
// 方法3: OptionalとflatMapを組み合わせる (より関数型らしい書き方)
System.out.println("--- Optional and flatMap ---");
listWithNulls.stream()
.map(Optional::ofNullable) // 各要素をOptionalにラップ
// .flatMap(opt -> opt.map(Stream::of).orElseGet(Stream::empty)) // Java 8
.flatMap(Optional::stream) // Java 9以降の標準(Java 17/21 LTSでも利用可): OptionalをStreamに変換 (空なら空Stream)
.map(String::toUpperCase)
.forEach(System.out::println);
// 収集 (Collectors) 時の注意:
// デフォルトではnull要素があると toMap などで NPE になることがある
List<String> safeList = listWithNulls.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
System.out.println("--- Collected non-null list ---");
System.out.println(safeList);
}
}
Objects.nonNull() や Optional を活用することで、Stream処理中に null を安全に扱うことができます。filter(Objects::nonNull) は null 要素を除去する最も一般的な方法です。
戻り値の設計|Optionalを使うべき場面とは
メソッドが値を返せない場合があることを示すために、戻り値の型をどう設計するかは重要です。
メソッドの戻り値が null になり得るなら、Optional<T> の使用を強く推奨します。
- 非推奨:
nullを直接返す。- 呼び出し元が
nullチェックを忘れるとNullPointerExceptionが発生する。メソッドのシグネチャだけではnullの可能性が伝わらない。
- 呼び出し元が
- 推奨:
Optional<T>を返す。- 型シグネチャで「値がない可能性」を明示できる。
Optionalが提供するメソッドにより、安全な値の取り扱いを強制または促進できる。
- 型シグネチャで「値がない可能性」を明示できる。
- 代替案:
- 空のコレクション: 結果がリストやマップの場合、
nullの代わりに空のCollections.emptyList()やCollections.emptyMap()を返す。これにより、呼び出し元はnullチェックなしにループ処理などを行える。 - Null Objectパターン:
nullの代わりに、何もしない(またはデフォルトの動作をする)特別なオブジェクトを返す。
- 空のコレクション: 結果がリストやマップの場合、
import java.util.Optional;
import java.util.Map;
import java.util.HashMap;
public class ReturnTypeDesign {
private static Map<Integer, String> dataStore = new HashMap<>();
static {
dataStore.put(1, "Apple");
dataStore.put(2, "Banana");
}
// 非推奨: nullを返す可能性
@Nullable // アノテーションで示すことはできるが...
public String findItemLegacy(int id) {
return dataStore.get(id); // Map.getはキーがなければnullを返す
}
// 推奨: Optionalを返す
public Optional<String> findItemOptional(int id) {
return Optional.ofNullable(dataStore.get(id));
}
public static void main(String[] args) {
ReturnTypeDesign design = new ReturnTypeDesign();
// Legacy呼び出し (nullチェックが必要)
String item1 = design.findItemLegacy(1);
if (item1 != null) System.out.println(item1.toUpperCase());
String item3Legacy = design.findItemLegacy(3);
// System.out.println(item3Legacy.toUpperCase()); // NullPointerException!
// Optional呼び出し (安全な処理)
design.findItemOptional(2)
.map(String::toUpperCase)
.ifPresent(System.out::println); // 値があれば大文字で出力
String item3 = design.findItemOptional(3)
.orElse("Not Found"); // 値がなければ"Not Found"
System.out.println(item3);
}
}
Optional を戻り値に使うことで、null 安全性を高め、APIの意図を明確に伝えられます。
引数の設計|@Nullable と @NonNull の選び方
メソッドの引数が null であることを許容するかどうか、そしてそれをどう扱うかも設計上のポイントです。
メソッドの引数が null であることを許容する場合は @Nullable アノテーションで明示し、メソッド内部で必ず null チェックを行うべきです。
- パラメータが必須の場合:
@NonNullアノテーションを付与する。- メソッドの冒頭で
Objects.requireNonNull(param, "param must not be null");のようなチェックを行う。
- パラメータが任意の場合:
@Nullableアノテーションを付与する。- メソッド内部で、そのパラメータを使用する前に
nullチェックを行う。 nullの場合のデフォルト動作を定義する。
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class ParameterDesign {
// 必須パラメータ (NonNull) と任意パラメータ (Nullable)
public void processData(@Nonnull String id, @Nullable String tag, int value) {
// 必須パラメータのnullチェック (アノテーションと合わせて二重チェックにもなる)
Objects.requireNonNull(id, "ID cannot be null");
System.out.println("Processing ID: " + id);
System.out.println("Value: " + value);
// 任意パラメータのnullチェック
if (tag != null) {
System.out.println("Tag: " + tag.toLowerCase());
} else {
System.out.println("Tag: [Not Provided]");
// tagがnullの場合のデフォルト処理など
}
}
// 引数にOptionalを使うのは一般的に非推奨
// public void processOptionalData(Optional<String> optionalTag) { ... }
public static void main(String[] args) {
ParameterDesign design = new ParameterDesign();
design.processData("user123", "important", 100);
design.processData("item456", null, 200);
// design.processData(null, "test", 300); // @Nonnull違反 (IDEやツールが警告) / Objects.requireNonNullで実行時エラー
}
}パラメータの nullable 性を明確にし、メソッド内部で適切に処理することで、予期せぬ NullPointerException を防ぎ、メソッドの利用者に正しい使い方を伝えることができます。
まとめ:Nullable設計で安全なJavaコードを実現
これまで見てきたように、Javaにおける Nullable の扱いは、NullPointerException を避け、安全で保守性の高いコードを書く上で非常に重要です。
最後に Nullable 設計のポイントを以下に整理します。
Optional・アノテーション
NullPointerException をなくすための特効薬は一つではありません。
Optional、null 安全性アノテーション (@Nullable, @NonNull)、そして従来の null チェックを、状況に応じて適切に使い分ける総合的なアプローチが求められます。
- メソッドの戻り値:
nullを返す代わりにOptional<T>を積極的に使用する。 - フィールドとパラメータ:
@Nullable/@NonNullアノテーションを活用し、変数がnullを許容するかどうかを明確にする。 - プリミティブ型:
nullを表現する必要がある場合は、対応するラッパークラス (Integer,Booleanなど) を使用し、アンボクシング時のNullPointerExceptionに注意する。 - 従来の
nullチェック: アノテーションやOptionalが使えないコードやライブラリとの連携部分では、if (variable != null)による基本的なnullチェックが依然として重要となる。
nullを「許容する」より「明示する」
Nullable を扱う上での基本的な考え方として、null を暗黙的に「許容する」のではなく、「null の可能性がある」ことをコード上で「明示する」という意識を持つことが重要です。
Optional やアノテーションは、まさにこの「明示」のためのツールです。null がどこで発生しうるのか、どこで null であってはならないのかがコードから読み取れるようになります。
nullを返す可能性があるメソッドはOptionalでラップする。nullを許容するパラメータには@Nullableを付ける。nullであってはならない変数には@NonNullを付ける。
このように null の存在を積極的に管理することで、NullPointerException のリスクを大幅に低減できます。
結論: Optional を戻り値に、@Nullable/@NonNull をフィールド・引数に、null チェックは境界処理に。この3層で使い分ければNPEはほぼ防げます。
2026年時点の推奨スタック(新規プロジェクト向け): Java 17 または 21 LTS + Optional<T>(戻り値)+ JSpecify @Nullable / @NonNull(フィールド・引数)+ Objects.requireNonNull(境界処理)。既存コードがJSR 305 / Spring の @Nullable を使っている場合、互換性は保たれるため急いで移行する必要はありません。
Javaの Nullable は確かに厄介な問題を引き起こします。しかし、その性質を正しく理解して Optional やアノテーションなどのモダンな機能を活用すれば、安全かつ効果的に扱えます。NPEは「正しく付き合えば防げる」ものです。