🍇

21.2 Optionalクラスとnull値の取り扱い(null安全)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

21.2 Optionalクラスとnull値の取り扱い

チャプターの概要

このチャプターでは、null安全の考え方や、null安全を担保するためのOptionalという機能について学びます。

21.2.1 nullチェックとnull安全

nullチェック

参照型変数がnull値の場合、そのメンバー(メソッドなど)にアクセスしようとするとNullPointerExceptionが発生するため、null値の可能性がある変数に対しては、if文によってnullチェックを行う必要があります。一口にnullチェックと言っても、幾つかの典型的なパターンがあるので、このレッスンで紹介します。
まずnull値の可能性がある変数のメソッドを呼び出す場合は、メソッド呼び出し前に以下のようにしてnullチェックを行います。

snippet
if (person != null) {
    // personのメソッドを呼び出す
    ........
}

これが最もシンプルなnullチェックのケースです。if文により参照型変数がnull値ではない分岐を作り、その中でメソッドを呼び出します。
次に、String型変数strの値が"foo"かどうかを判定するif文を考えてみましょう。以下のコードを見てください。

snippet
if (str != null && str.equals("foo")) { .... }

このように変数strのequals()メソッド呼び出しの前に、まずstrのnullチェックを行います。ここでポイントとなるのが、論理演算子&&を使っている点です。&&は「左辺の式のみを評価するだけで論理演算が確定する場合は、右辺の評価は行わない」という論理演算子でした(これを短絡評価と呼ぶ)。従ってもし変数strがnull値の場合、右辺の評価は行われないため、NullPointerExceptionは発生しません。

複雑なnullチェック

もう少し複雑なnullチェックの仕様を考えてみましょう。String型変数str1とstr2があるものとします。条件判定により、str1とstr2がともにnullだった場合はtrueを返します。そしてどちらか片方がnull以外だった場合は、両者が一致していればtrue、一致していなければfalseを返すという仕様です。これは以下のようなコードになります。

snippet_1 (pro.kensait.java.basic.lsn_21_2_1.Main)
if (str1 == null) { //【1】
    if (str2 != null) {
        return false;
    }
} else if (! str1.equals(str2)) {
    return false;
}
return true;

このような判定処理は、equals()メソッドの典型的な実装パターンとして比較的よく登場します。ここで考えていただきたいのが、最初のifブロック【1】を以下のように書き換えられるか、という点です。

snippet_2 (pro.kensait.java.basic.lsn_21_2_1.Main)
if (str1 == null && str2 != null) {
    return false;
} else if (! str1.equals(str2)) { //【1】NullPointerException発生の可能性
    return false;
}
return true;

一見すると、if文のネストを論理演算子&&で書き換えただけのように感じられますが、このコードには問題があります。何故なら、仮にstr1とstr2がいずれもnull値の場合、最初のifブロックの中には入らず次のelse-if文【2】に進んでしまうため、ここでNullPointerExceptionが発生してしまうからです。
このようにnullチェックにも様々なパターンがあり、比較的複雑なコードが必要なケースもある、という点を理解しておきましょう。

nullチェックの必要性とnull安全

このレッスンでは、nullチェックについて取り上げました。繰り返しになりますが、変数がnull値になる可能性がある場合にはnullチェックが必要です。ただしそうだとすると、参照型変数が登場するたびに毎回nullチェックをしなければいけない、ということになり兼ねません。
実はnullチェックの必要性は、ケースバイケースです。業務仕様として、null値が「空の値」を表す場合はnullチェックが必須です。その一方で、引数や戻り値が初期化されていることが仕様として保証されている場合は、毎回nullチェックを行う必要はありません。NullPointerExceptionが発生するとしたら、それはプログラム不良しかありえないからです。このようにJavaアプリケーションを設計する上では、nullチェックの必要性について注意深く検討しなければなりません。
なおnullチェックを行うまでもなく、実行時にNullPointerExceptionを発生させない仕組みを、null安全と呼びます。Javaでは後述するOptionalという機能によって、ある程度null安全を担保することができます。

21.2.2 Optionalクラスの特徴

メソッドの戻り値とOptionalクラス

メソッドを呼び出したとき、戻り値としてnull値が返されるかどうかはメソッドの仕様次第です。
例えば、データベースを検索するメソッドでは「データが存在しない(空の値であった)」ことをnull値によって表す、というケースがあります。このように仕様としてnull値が返される可能性がある場合は、呼び出し元で戻り値に対するnullチェックが必要です。一方、仕様としてnull値が返される可能性がないメソッドでは、null値が返されるのはプログラム不良しかありえないためnullチェックは不要です。
ここで問題なのは、メソッドがnull値を返す仕様であるか、そうではないのかという点が、メソッドの宣言から読み解くことができないという点です。そこでjava.util.Optionalクラスによって、この問題を解決します。Optionalクラスをメソッドの戻り値に指定すると、呼び出し元ではnull安全を担保することができます。

OptionalクラスのAPI

java.util.Optionalクラスには様々なAPIがありますが、その中で主要なものを以下に示します。

API(メソッド) 説明
static Optional<T> empty() 空のOptionalオブジェクトを返す。
static Optional<T> of(T) 指定された値を持つOptionalオブジェクトを返す。
static Optional<T> ofNullable(T) 指定された値が非null値の場合はそれを持つOptionalオブジェクトを返し、null値の場合は空のOptionalオブジェクトを返す。
boolean isEmpty() この値が空の場合はtrueを、それ以外の場合はfalseを返す。
boolean isPresent() この値が存在する場合はtrueを、それ以外の場合はfalseを返す。
T get() この値が存在する場合はそれを返す。
※実質的にorElseThrow()メソッドとまったく同じ。
T orElse(T) この値が存在する場合はそれを返し、そうでない場合は指定された値を返す。
T orElseThrow() この値が存在する場合はそれを返し、そうでない場合はNoSuchElementExceptionを送出する。
T orElseThrow(Supplier<? extends X>) この値が存在する場合はそれを返し、そうでない場合はラムダ式に指定された例外を送出する。

次項からは、これらのAPIの使用方法について具体的に説明していきます。

Optionalの生成方法(呼び出されるメソッド側)

仕様としてnull値を返す可能性のあるメソッドは、戻り値にOptional型を指定することで、呼び出し元におけるnull安全を担保します。Optionalクラスは、どんな型のインスタンスでも格納できる、一種の汎用的な入れ物です。ここでは呼び出される側のメソッド本体において、戻り値としてOptional型を返すケースを見ていきましょう。
具体例として、データベースから特定の「人物」を検索するためのクラスを取り上げます。まずはOptionalを使わない場合は、以下のようなコードになります。

pro.kensait.java.basic.lsn_21_2_2.PersonDBSearcher
public class PersonDBSearcher {
    public Person find(String name) {
        Person person = personDB.get(name); // 【1】
        return person;
    }
    private static Map<String, Person> personDB = //【2】
            Map.of("Alice", new Person("Alice", 25, "female"),
                    "Bob", new Person("Bob", 35, "male"),
                    "Carol", new Person("Carol", 25, "female"));
}

検索対象のPersonクラスはこれまでに何度も登場していますが、ここではname(名前)、age(年齢)、gender(性別)という3つのフィールドを持ち、コンストラクタとアクセサメソッドも定義されているものとします。便宜上このクラスにはマップによって簡易的なデータベースが組み込まれており、"Alice"、"Bob"、"Carol"の3人が、名前をキーに登録されているものとします【2】。
find()メソッドはPersonを検索するためのもので、引数として渡された名前をキーに、マップから値を取り出して返します【1】。例えば"Alice"が渡されると、検索の結果ヒットしたAliceのPersonインスタンスを返します。逆に存在しない名前("Dave"など)が渡されると、このメソッドは「データベースには存在しない」という意味でnull値を返します。この挙動は「null値が返されるのはプログラム不良(初期化漏れ)ではなく一種の業務仕様である」ということを表すので、呼び出し元ではnullチェックが必要です。
それではこのような仕様のfind()メソッドをOptional型を返すように修正し、新たにfind2()メソッドを追加します。以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_21_2_2.PersonDBSearcher)
public Optional<Person> find2(String name) { //【3】
    Person person = personDB.get(name);
    if (person == null) {
        return Optional.empty(); //【4】
    }
    return Optional.of(person); //【5】
}

まず戻り値は「Personインスタンスを格納するためのOptional型」という意味でOptional<Person>にします【3】。マップから値を取り出した結果、Personがnull値だった場合は、Optionalクラスのempty()メソッド(スタティックメソッド)により、空の値を持つOptionalを生成して返します【4】。逆にPersonが取得できた場合は、Optionalクラスのof()メソッド(スタティックメソッド)により、取得したPersonインスタンスを持つOptionalを生成して返します【5】。
なおこのfind2()メソッドは、以下のコードのように書き換えることが可能です。

snippet_2 (pro.kensait.java.basic.lsn_21_2_2.PersonDBSearcher)
public Optional<Person> find3(String name) {
    Person person = personDB.get(name);
    return Optional.ofNullable(person);
}

このようにOptionalクラスのofNullable()メソッド(スタティックメソッド)を呼び出すと、引数がnull値だった場合は空の値を持つOptionalが、null値ではなかった場合はその値を持つOptionalが、それぞれ生成されます。Optionalを生成する場合は、基本的にはこのofNullable()メソッドを利用するケースが多いでしょう。

メソッド呼び出し元におけるハンドリング(1)

ここでは、Optional型を受け取った、メソッド呼び出し元における処理内容を説明します。呼び出し元では、返されたOptionalのAPIを呼び出すことで、値を受け取ったり、空だった場合に適切なハンドリングを行います。
以下は、前項で取り上げたfind2()メソッドの呼び出し元となるコードです。

snippet_1 (pro.kensait.java.basic.lsn_21_2_2.Main)
PersonDBSearcher pds = new PersonDBSearcher();
Optional<Person> opt = pds.find2(....); //【1】"Alice" or "Dave"
if (opt.isEmpty()) { //【2】
    //【3】Optionalが空だった場合の処理
    ........
}
Person person = opt.get(); //【4】

まずfind2()メソッド呼び出しにより、Optional<Person>型が返されます【1】。次にif文の条件式として、OptionalクラスのisEmpty()メソッドを呼び出していますが【2】、このメソッドはOptionalが空の場合にtrueが返されます。この例では、もし"Alice"を引数に渡していたら検索にヒットするためifブロックには入りません。その場合はOptionalのget()メソッドを呼び出すして、AliceのPersonインスタンスを取得します【4】。逆にもし"Dave"を引数に渡していたら検索にはヒットしないため、空のOptionalが返され、その結果ifブロックに入ります。
ifブロックには、値が空の場合の処理を実装します【3】。空の場合の処理としては、何らかの代わりのインスタンスを生成して返す、例外を発生させる、他のデータベースを検索する、といったものが考えられます。

メソッド呼び出し元におけるハンドリング(2)

ここではメソッド呼び出し元においてOptionalクラスが返された後、isEmpty()メソッドによる判定を行わずに、値が空の場合の処理を効率的に実装する方法を説明します。以下のコードを見てください。

snippet_2 (pro.kensait.java.basic.lsn_21_2_2.Main)
Optional<Person> opt = pds.find2(....);
//【1】空の場合、何らかの代わりのインスタンスを代入する
Person p1 = opt.orElse(new Person());
//【2】空の場合、例外(java.util.NoSuchElementException)を発生させる
Person p2 = opt.orElseThrow();
//【3】空の場合、例外(任意の例外)を発生させる
Person p3 = opt.orElseThrow(() -> new RuntimeException("Daveは存在せず"));

まず「空の場合、何らかの代わりのインスタンスを代入する」方法です。orElse()メソッドを呼び出す【1】と、値が存在する場合はそれを受け取り、空の場合は指定された別の値を返すことができます。もし"Alice"を渡していたら検索にヒットするため、orElse()メソッドの引数は無視され、AliceのPersonインスタンスが返されます。逆にもし"Dave"を渡していたら、検索にはヒットせず空のOptionalが返されるため、代わりにorElse()メソッドで指定した値が返されます。ここでは、属性を持たないPersonインスタンスを返しています。
次に「空の場合、例外を発生させる」方法です。orElseThrow()メソッドを呼び出す【2】と、値が存在する場合はそれを受け取り、空の場合はNoSuchElementExceptionという例外が発生します。
最後に同じく「空の場合、例外を発生させる」方法です。orElseThrow()メソッドを呼び出し、ラムダ式に例外オブジェクトを指定する【3】と、任意の例外を送出することができます。なおラムダ式については、詳細は『Java Advanced編』を参照してください。

メソッドの引数とOptionalクラス

このレッスンではこれまで、メソッドから返される戻り値について、Optionalによってnull安全を担保する方法を説明してきました。
それでは、メソッドの引数についてはどうでしょうか。メソッドの引数にnull値を許容するケースは、一般的にあまり多くはありませんが、あくまでもメソッドの仕様次第です。ただし引数に関しては、Optionalによってnull安全を担保することができません。仮にnull値を許容する引数があったとして、その型をOptional型に変更したとしても、「Optional型のnull値」が渡されてしまうとnull安全ではなくなるからです。
これを踏まえると、メソッド引数におけるnull値の取り扱いはどのようにあるべきでしょうか。まずメソッド引数でnull値を許容する場合は、メソッドの設計者が『APIリファレンス』などのドキュメントでそれを明示する必要があります。逆にそのようにドキュメントで明示さない限りは、基本的にnull値は非許容と見なすべきでしょう。
ただしnull値が非許容だからといって、メソッド側で引数に対するnullチェックは一切行わないかというと、必ずしもそうとも限りません。これはメソッドの設計次第ですが、以下のような戦略が考えられるでしょう。

  1. 引数のnullチェックは行わない(メソッド内のどこかでNullPointerExceptionが発生するが、それを許容する)。
  2. メソッドの先頭で引数のnullチェックを行い、null値だった場合は、IllegalArgumentException例外を送出する。
  3. メソッドの先頭で引数のnullチェックを行い、null値だった場合は、何もせずにメソッドを直ちに終了する(returnする)。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. nullチェックの必要性とnull安全の考え方について。
  2. Optionalクラスによってnull安全を担保する方法について。
  3. Optionalクラスの特徴やAPIについて。
  4. メソッド引数に対してどのようにnull安全を担保するか。

Discussion