JavaのOptional、もう「nullPointerException」で悩まない!

docs

以前、オブジェクト指向プログラミングの基本を学んで、Javaの奥深さに触れてもらいましたね。
クラスとオブジェクトの概念 – オブジェクト指向の入り口 | ToolDocs
今回は、Javaプログラミングで多くの人が一度は経験する、あの厄介な「NullPointerException」をスマートに回避するための強力な味方、「Optional」について、とことん深掘りしていきます!

NullPointerExceptionって、なんで起きるの?

まず、「NullPointerException」(以下、NPEと略します)がどうして起きるのか、軽くおさらいしましょう。

NPEは、簡単に言うと「存在しないものを使おうとしたとき」に発生します。例えば、あなたが友達に「あの本取って」とお願いしたとします。でも、そこに本がなかったらどうでしょう?「本がないじゃん!」ってなりますよね。これと同じで、Javaでは「値がない(null)」変数に対して、何か操作を行おうとするとNPEが発生してしまうんです。

Java

public class Book {
    String title;

    public Book(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

public class Library {
    public Book findBook(String bookTitle) {
        // 例として、"存在しない本"の場合はnullを返すとする
        if ("存在しない本".equals(bookTitle)) {
            return null;
        }
        return new Book(bookTitle);
    }

    public static void main(String[] args) {
        Library library = new Library();
        Book book = library.findBook("存在しない本");
        // ここでNPEが発生する可能性がある!
        System.out.println(book.getTitle());
    }
}

この例だと、findBookメソッドがnullを返す可能性があるのに、呼び出し側でそのnullチェックをしていないため、book.getTitle()の実行時にNPEが発生します。

従来のJavaでは、このNPEを防ぐために、常にnullチェックを行う必要がありました。

Java

// 従来のnullチェック
Book book = library.findBook("存在しない本");
if (book != null) {
    System.out.println(book.getTitle());
} else {
    System.out.println("本が見つかりませんでした。");
}

これでも動きますが、もしnullになる可能性がある箇所がたくさんあったら、コードのあちこちにif (xxx != null)が散りばめられて、読みにくくなっちゃいますよね。

Optionalって何?

そこで登場するのが、Java 8から加わった「Optional」です! Optionalは、「もしかしたら値があるかもしれないし、ないかもしれない」ということを明示的に表現するためのコンテナ(入れ物)クラスなんです。

Optionalを使うと、nullの可能性がある値をOptionalの箱に入れて管理することで、コンパイル時にnullの可能性を意識させ、NPEを未然に防ぐ手助けをしてくれます。

例えるなら、宅配便の「不在票」みたいなものです。荷物(値)がある場合はそれが届きますが、不在の場合は「不在票」という「何も入ってないよ」という情報が入った紙が届きますよね。Optionalもそんな感じで、値がある場合はその値が入ったOptionalが、値がない場合は「空っぽ」のOptionalが返ってくるんです。

Optionalを使ってみよう!

じゃあ、早速Optionalを使ってみましょう。

Optionalの生成方法

Optionalを生成する方法はいくつかあります。

1. Optional.of(value): 値が絶対にnullではないと分かっている場合に使います。もしvaluenullだったら、ここでNPEが発生します。

Java

String name = "Taro"; 
Optional<String> optionalName = Optional.of(name); 
System.out.println(optionalName); // Optional[Taro]

2. Optional.ofNullable(value): 値が**nullの可能性がある**場合に使います。valuenullでもNPEは発生しません。nullの場合は空のOptionalが生成されます。

Java

String firstName = "Hanako"; 
Optional<String> optionalFirstName = Optional.ofNullable(firstName);
System.out.println(optionalFirstName); // Optional[Hanako] 

String lastName = null; 
Optional<String> optionalLastName = Optional.ofNullable(lastName);
System.out.println(optionalLastName); // Optional.empty

Optional.empty(): 明示的に空のOptionalを生成したい場合に使います。

Java

Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional); // Optional.empty

Optionalから値を取り出す方法

Optionalに入っている値を取り出すには、いくつか安全な方法があります。

1.isPresent()get(): isPresent()で値が存在するかを確認し、存在する場合のみget()で値を取り出します。get()は値が存在しない場合にNPEを投げるので、必ずisPresent()とセットで使いましょう。

Java

Optional<String> optName = Optional.ofNullable("Jiro"); 
if (optName.isPresent()) { 
    String name = optName.get(); 
    System.out.println("名前は " + name + " です。"); 
} else { 
    System.out.println("名前はありません。"); 
} 

Optional<String> optNullName = Optional.ofNullable(null); 
if (optNullName.isPresent()) { 
    // ここは実行されない 
} else { 
    System.out.println("名前はありません。"); 
}

2.orElse(defaultValue): もし値が存在すればその値を返し、存在しなければ引数で指定したデフォルト値を返します。これが一番よく使うパターンかもしれません。

Java

Optional<String> optCity = Optional.ofNullable("Tokyo"); 
String city = optCity.orElse("Unknown"); 
System.out.println("都市は " + city + " です。"); // 都市は Tokyo です。 

Optional<String> optNullCity = Optional.ofNullable(null); 
String defaultCity = optNullCity.orElse("Osaka"); 
System.out.println("都市は " + defaultCity + " です。"); // 都市は Osaka です。

3.orElseGet(supplier): orElse()と似ていますが、デフォルト値を生成する処理をSupplier(関数型インターフェース)で渡します。デフォルト値の生成にコストがかかる場合など、値が存在しない場合のみ生成処理を実行したいときに便利です。

Java

Optional<String> optCountry = Optional.ofNullable("Japan"); 
String country = optCountry.orElseGet(() -> "USA"); // "Japan"が存在するのでSupplierは実行されない 
System.out.println("国は " + country + " です。"); // 国は Japan です。 

Optional<String> optNullCountry = Optional.ofNullable(null); 
String defaultCountry = optNullCountry.orElseGet(() -> { 
    System.out.println("デフォルト国を生成します..."); // 値がないのでSupplierが実行される 
    return "Germany"; 
}); 
System.out.println("国は " + defaultCountry + " です。"); // 国は Germany です。

4.orElseThrow(exceptionSupplier): 値が存在しない場合に、指定した例外を投げたいときに使います。

Java

Optional<String> optFood = Optional.ofNullable(null); 
try { 
    String food = optFood.orElseThrow(() -> new IllegalArgumentException("食べ物が見つかりません!")); 
    System.out.println("食べ物は " + food + " です。"); 
} catch (IllegalArgumentException e) { 
    System.err.println(e.getMessage()); // 食べ物が見つかりません! 
}

Optionalを使った高度な操作

Optionalには、値を加工したり、条件によって処理を分岐させたりするための便利なメソッドがたくさんあります。

1.map(Function): Optionalの中に値があれば、その値を別の型に変換して、新しいOptionalとして返します。値がなければ空のOptionalを返します。

Java

Optional<String> optSentence = Optional.of("hello world"); 
Optional<Integer> length = optSentence.map(s -> s.length()); 
System.out.println(length); // Optional[11] 

Optional<String> optNullSentence = Optional.empty(); 
Optional<Integer> nullLength = optNullSentence.map(s -> s.length()); 
System.out.println(nullLength); // Optional.empty

2.flatMap(Function): mapと似ていますが、引数に渡す関数がOptionalを返す場合に使います。mapだとOptional<Optional<T>>のようになってしまいますが、flatMapだとOptional<T>のようにフラットな構造になります。

Java

Optional<String> userText = Optional.of("user@example.com"); // メールアドレスからドメイン部分だけを取り出すメソッドがあると仮定 

// このメソッド自体がOptionalを返す可能性がある 
public Optional<String> extractDomain(String email) { 
    if (email != null && email.contains("@")) { 
        return Optional.of(email.substring(email.indexOf("@") + 1)); 
    } 
    return Optional.empty(); 
} // mapを使うとOptional<Optional<String>>になる 

Optional<Optional<String>> domainWithMap = userText.map(this::extractDomain); 
System.out.println(domainWithMap); // Optional[Optional[example.com]] // flatMapを使うとOptional<String>になる 

Optional<String> domainWithFlatMap = userText.flatMap(this::extractDomain); 
System.out.println(domainWithFlatMap); // Optional[example.com]

3.filter(Predicate): Optionalの中に値があり、その値が指定した条件を満たす場合のみ、その値を保持したOptionalを返します。条件を満たさない場合や値がない場合は空のOptionalを返します。

Java

Optional<Integer> age = Optional.of(25); 
Optional<Integer> adultAge = age.filter(a -> a >= 20); // 25 >= 20 なのでOptional[25] 
System.out.println(adultAge); 

Optional<Integer> youngAge = Optional.of(15); 
Optional<Integer> adultYoungAge = youngAge.filter(a -> a >= 20); // 15 < 20 なのでOptional.empty 
System.out.println(adultYoungAge);

4.ifPresent(Consumer): Optionalの中に値が存在する場合のみ、その値を使って特定の処理を実行します。値がない場合は何も実行しません。

Java

Optional<String> message = Optional.of("こんにちは!"); 
message.ifPresent(m -> System.out.println(m + " 良い一日を!")); // こんにちは! 良い一日を! 

Optional<String> emptyMessage = Optional.empty(); 
emptyMessage.ifPresent(m -> System.out.println(m + " 良い一日を!")); // 何も表示されない

Optionalを使うべき場面、使わないべき場面

Optionalは便利ですが、いつでもどこでも使えばいいというわけではありません。

Optionalを使うべき場面

メソッドの戻り値: 戻り値がnullになる可能性がある場合にOptionalを使うと、呼び出し側は戻り値がnullかもしれないことを意識してコードを書くようになります。これにより、NPEを未然に防ぎやすくなります。

Java

// 従来の良くない例 public Book findBookByTitle(String title) {     // 本が見つからない場合、nullを返す     return null; } 
// Optionalを使った良い例 public Optional<Book> findBookByTitle(String title) {     // 本が見つからない場合、Optional.empty()を返す     return Optional.empty(); }

ストリームAPIとの連携: mapfilterなどのメソッドは、ストリームAPIと非常に相性が良く、より簡潔で読みやすいコードを書くことができます。

メソッドの引数には使わない: メソッドの引数にOptionalを使うのは推奨されません。引数がOptionalだと、呼び出し側が毎回Optional.of()Optional.ofNullable()でラップする必要があり、コードが煩雑になります。引数でnullを許可しない場合は@NonNullなどのアノテーションを使うか、nullチェックをメソッドの先頭で行いましょう。

フィールド変数には使わない: クラスのフィールド変数にOptionalを使うのも避けるべきです。Optionalはあくまで「値の有無」を表現するためのもので、オブジェクトのライフサイクルを通じてその状態が変化する可能性のあるフィールドには不向きです。

Optionalを使った具体例

では、最初に登場したLibraryの例をOptionalを使って書き直してみましょう。

Java

public class Book {
    String title;

    public Book(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

public class Library {
    // 戻り値にOptionalを使うことで、nullの可能性を明示する
    public Optional<Book> findBook(String bookTitle) {
        if ("存在しない本".equals(bookTitle)) {
            return Optional.empty(); // 本がない場合は空のOptionalを返す
        }
        return Optional.of(new Book(bookTitle)); // 本がある場合はOptionalに包んで返す
    }

    public static void main(String[] args) {
        Library library = new Library();

        // 存在する本を検索
        Optional<Book> foundBook = library.findBook("Java入門");
        String bookTitle1 = foundBook.map(Book::getTitle) // Bookオブジェクトがあればタイトルを取り出す
                                     .orElse("見つかりませんでした。"); // なければデフォルトメッセージ
        System.out.println("検索結果1: " + bookTitle1); // 検索結果1: Java入門

        // 存在しない本を検索
        Optional<Book> notFoundBook = library.findBook("存在しない本");
        String bookTitle2 = notFoundBook.map(Book::getTitle)
                                        .orElse("見つかりませんでした。");
        System.out.println("検索結果2: " + bookTitle2); // 検索結果2: 見つかりませんでした。

        // さらに、見つかった本に対して何か処理をしたい場合
        notFoundBook.ifPresent(book -> System.out.println("見つかった本: " + book.getTitle()));
        // 上記は何も表示されない(値がないため)

        foundBook.ifPresent(book -> System.out.println("見つかった本: " + book.getTitle()));
        // 見つかった本: Java入門
    }
}

どうでしょう? nullチェックが一切なくなり、コードがスッキリして、何よりNPEの心配がなくなりましたよね!

まとめ

今回はJavaの「Optional」について、その使い方やメリット、そしてどんな場面で使うと効果的なのかを解説しました。

  • Optionalは「値が存在するかもしれないし、しないかもしれない」という状態を表現するためのクラス。
  • NPENullPointerException)を防ぐ強力な味方。
  • Optional.of(), Optional.ofNullable(), Optional.empty()で生成。
  • orElse(), orElseGet(), orElseThrow(), map(), flatMap(), filter(), ifPresent()などのメソッドを使って安全に値を操作できる。
  • メソッドの戻り値として使うのが特に効果的。メソッドの引数やクラスのフィールドには基本的に使わない。

Optionalを使いこなせば、あなたのJavaコードはもっと堅牢で読みやすくなること間違いなしです! 最初は少しとっつきにくいかもしれませんが、実際にコードを書いてみて、その便利さを実感してくださいね。

他の記事も読んでみてくださいね! ToolDocs | 様々なツールや技術情報を紹介します

コメント

タイトルとURLをコピーしました