X

A blog about Oracle Technology Network Japan

Optionalクラスを意図されたとおりに使うための12のレシピ

Guest Author

※本記事は、Mohamed Tamanによる"12 recipes for using the Optional class as it’s meant to be used"を翻訳したものです。


美しくないnullポインタ例外からアプリケーションを守る12のベスト・プラクティスに従い、コードを読みやすく簡潔にする

著者:Mohamed Taman
2020年6月22日

真剣なJava開発者やアーキテクトなら、誰でも迷惑なNullPointerException例外について聞いたことや、体験したことがあります。

これに対して、何ができるでしょうか。プログラマーがメソッドから値を返すとき、値がないことを示すためにnull参照を使うことはよく行われます。しかしこれは、さまざまな重大な問題の原因になります。

このnull参照問題について詳しく知りたい方は、Raoul-Gabriel Urma氏の記事「Tired of Null Pointer Exceptions?Consider Using Java SE 8's 'Optional'!」を読んでみるとよいでしょう。この問題について詳細に説明され、Optionalクラスが紹介されています。

本記事では、Urma氏の記事を踏まえて、Optionalの使い方と、これをどう使うべきかについて確認します。筆者は、開発者のコードをレビューしていたときの経験や実践を通じて、開発者が日々のコーディングでOptionalクラスを使っていると認識しました。そこから、本記事で紹介する、読者のスキル向上とアンチパターン回避に役立つ12のベスト・プラクティスが生まれました。

本記事と、まもなくJava Magazineに掲載される続編の記事では、Java 14までにリリースされている、Optionalクラスのすべてのメソッドを確認します。また、Java 15の完成も近づいているため、Java 15についても触れます。

 

Optionalクラスの由来

Javaエンジニアはnull問題に長い間取り組んできました。そして、Java 7でこの問題に対処するソリューションを導入しようとしましたが、そのリリースには追加されませんでした。ここでは、言語の設計者がStream APIについてどう考えたのかを振り返る、というよりも想像してみることにします。count()やsum()などのいくつかのメソッドには、値が存在しない場合の必然的な戻りタイプ(0など)が存在します。このゼロは合理的です。

しかし、findFirst()メソッドやfindAny()メソッドはどうでしょう。入力がない場合にnull値を返すのは、合理的ではありません。そういったメソッドへの入力が存在すること(または存在しないこと)を示す、値の型があるべきです。

そこでJava 8では、Optional<T>という新たな型が追加されました。この型は、T型の値が存在するどうかを表します。Optionalは、ストリーム(またはOptionalを返すメソッド)と組み合わせて、Fluent APIを構築する場合の戻りタイプとして使うことを意図したものでした。さらに、開発者がnull参照を適切に扱えるようにするという目的もありました。

ところで、Java SE 11ドキュメントには、Optionalについて次のような記述があります。「Optionalは主に、『結果がない』ことを示す明確な必要性があり、nullを使うことでエラーが起こりやすくなる場合にメソッドの戻りタイプとして使うことを意図しています。Optional型の変数は、決してnullにはならずに、常にOptionalインスタンスを指す必要があります」

まもなくコード・レシピで紹介しますが、筆者流の定義では、「Optionalクラスは、存在しない可能性のある値のコンテナ型である」となります。

また、いくつかの特殊ケースや誘惑は、わなに相当する場合があります。このわなに引っかかった場合、コードの質が低下することや、さらには予期しない動作の原因となることがあります。本シリーズ記事では、それらの点について説明していきます。

「コードはどこだろう」と思う方もいらっしゃるかもしれませんから、本題に入ります。筆者のアプローチは、Optionalクラスのすべての使用場面を分類し、それぞれの分類に関連して開発者がよく抱く質問に答えるというものです。本記事では、次の3つの大分類について、12のレシピを使って説明します。

  • Optionalを使っているのにnullになるのはなぜですか
  • 値がない場合、何を返せば(または、何を設定すれば)よいですか
  • どうすればOptional値を効率的に使用できますか

次回の記事では、次の2つの分類について説明します。

  • どうすればOptionalのアンチパターンを回避できますか
  • Optionalは好きですが、どうすればもっとプロらしくできますか

 

Optionalを使っているのにnullになるのはなぜですか

通常、この質問はOptionalクラスの作成と、データの取得方法に関連します。

レシピ1:オプショナル変数にnullを代入しない。開発者がデータベースを使用して従業員を検索する場合に、Optional<Employee>を返すメソッドを設計することがあります。しかし、データベースから結果が返されない場合に、依然としてnullを返している開発者を見かけたことがあります。次の例をご覧ください。

1 public Optional<Employee> getEmployee(int id) {
2    // 従業員を検索 
3    Optional<Employee> employee = null; // 従業員が存在しない場合
4    return employee; 
5 }


上記のコードは正しくないため、徹底的に避けるべきです。このコードを修正するためには、3行目を次の行で置き換えます。修正後の行では、空のOptionalでOptionalを初期化しています。

Optional<Employee> employee = Optional.empty();

Optionalは値を保持できるコンテナであるため、nullで初期化するのは意味がありません。

APIメモ:empty()メソッドは、Java 8以降に存在します。

レシピ2:直接get()を呼ばない。次のコードについて考えてみます。何がおかしいでしょうか。

Optional<Employee> employee = HRService.getEmployee();
Employee myEmployee = employee.get();

"employee" Optionalは空になる可能性があるため、get()を直接呼び出すとjava.util.NoSuchElementExceptionがスローされると思ったでしょうか。そう思ったなら、正解です。get()を呼び出しても大丈夫と思った方は、間違いです。必ず最初にisPresent()メソッドを使って値の存在をチェックする必要があります。たとえば、次のようにします。

if (employee.isPresent()) {
    Employee myEmployee = employee.get();
    ... // "myEmployee"を使って何かを行う
} else {
    ... // employee.get()を呼ばないで何かを行う
} 

上記のコードは定型挿入文であるため、望ましくないという点には注意してください。次は、isPresent()/get()ペアを呼び出す代案として、もっと美しいさまざまな方法を紹介します。

APIメモ:isPresent()メソッドとget()メソッドは、Java 8以降に存在します。

レシピ3:Optionalでnull参照を取得するときは、nullを直接使わない。場合によっては、null参照を保持しなければならないこともあります。しかし、Optionalでは直接nullを使わないでください。代わりに、orElse(null)を使います。

次の例について考えてみてください。Reflection APIのMethodクラスに対して、invoke()メソッドを呼び出しています。これは、実行時にメソッドを呼び出す方法です。最初の引数には、呼び出されるメソッドがstaticであればnullを、そうでない場合はメソッドを含むクラス・インスタンスを渡しています。

1 public void callDynamicMethod(MyClass clazz, String methodName) throws ... {
2    Optional<MyClass> myClass = clazz.getInstance();
3    Method = MyClass.class.getDeclaredMethod(methodName, String.class);
4    if (myClass.isPresent()) {
5        method.invoke(myClass.get(), "Test");
6    } else {
7        method.invoke(null, "Test");
8    }
9 }

通常はorElse(null)を使うべきではありません。ただし、このような場合は、上記のコードよりもorElse(null)を使う方が望ましいと言えます。4行目から8行目は、次に示す簡潔な1行のコードで置き換えることができます。

4    method.invoke(myClass.orElse(null), "Test");

APIメモ:orElse()メソッドは、Java 8以降に存在します。

 

値がない場合、何を返せば(または、何を設定すれば)よいですか

先ほどのセクションでは、Optionalを使っていても発生するnull参照問題を避ける方法について取り上げました。次は、Optionalを使ってデータを設定してターンする別の方法について見てみます。

レシピ4:値の設定とターンには、isPresent()とget()のペアを使わない。次のコードについて考えてみます。もっと美しく効果的にするために、どこを変更できるでしょうか。

public static final String DEFAULT_STATUS = "Unknown";
...
public String getEmployeeStatus(long id) {
    Optional<String> empStatus = ... ;
    if (empStatus.isPresent()) {
        return empStatus.get();
    } else {
        return DEFAULT_STATUS;
    }
}

レシピ3と同様に、次のようにしてisPresent()とget()のペアをorElse()に置き換えます。

public String getEmployeeStatus(long id) {
    Optional<String> empStatus = ... ;
    return empStatus.orElse(DEFAULT_STATUS); 
}

ここで考慮すべき非常に重要な点は、パフォーマンスのペナルティが発生する可能性があることです。orElse()が返す値は、オプショナル値が存在するかどうかによらず、必ず評価されます。そのため、ここでのルールは、すでに構築した値があり、高価な演算値を使っていない場合に、orElse()を使うということになります。

APIメモ:orElse()メソッドは、Java 8以降に存在します。

レシピ5:演算値を返す場合にorElse()を使わない。レシピ4で説明したように、パフォーマンスのペナルティがあるため、演算値を返す場合はorElse()を使わないようにしてください。次のコード・スニペットについて考えてみます。

Optional<Employee> getFromCache(int id) {
    System.out.println("search in cache with Id: " + id);
    // キャッシュから値を取得
}

Optional<Employee> getFromDB(int id) {
    System.out.println("search in Database with Id: " + id);    
    // データベースから値を取得
}

public Employee findEmployee(int id) {        
    return getFromCache(id)
            .orElse(getFromDB(id)
                    .orElseThrow(() -> new NotFoundException("Employee not found with id" + id)));}

まず、このコードでは、指定されたIDの従業員をキャッシュから取得しようとしています。その従業員がキャッシュに存在しない場合は、データベースから取得しようとします。次に、目的の従業員がキャッシュにもデータベースにも存在しない場合、NotFoundExceptionがスローされます。このコードを実行した場合、従業員がキャッシュに存在するときは、次のように出力されます。

Search in cache with Id: 1
Search in Database with Id: 1

従業員はキャッシュから返されるにもかかわらず、データベースの問合せが呼び出されています。これはとても高価です。そこで、orElseGet(Supplier<? extends T> supplier)を使います。このメソッドはorElse()に似ていますが、1つ違いがあります。Optionalが空だった場合、orElse()では直接デフォルト値が返されますが、orElseGet()にはOptionalが空だった場合にのみ呼び出されるSupplier関数を渡すことができます。これはパフォーマンス改善に役立ちます。

それでは、従業員がキャッシュに存在するという同じ前提のもと、orElseGet()を使って次のように変更したコードを再実行することを考えてみます。

public Employee findEmployee(int id) {        
    return getFromCache(id)
        .orElseGet(() -> getFromDB(id)
            .orElseThrow(() -> {
                return new NotFoundException("Employee not found with id" + id);
            }));
}

今度は、望みどおりの結果が得られ、パフォーマンスが改善されるでしょう。コードからは、次の内容のみが出力されます。

Search in cache with Id: 1

なお、isPresent()とget()のペアを使おうとは思わないでください。この方法は美しくないからです。

APIメモ:orElseGet()メソッドは、Java 8以降に存在します。

レシピ6:値が存在しないときは例外をスローする。値が存在しないことを示すために例外をスローしたい場合もあります。通常は、データベースなどのリソースと連携するサービスを開発する場合にこのようなことがあります。Optionalを使うことで、これが簡単に実現します。次の例について考えてみます。

public Employee findEmployee(int id) {        
    var employee = p.getFromDB(id);
    if(employee.isPresent())
        return employee.get();
    else
        throw new NoSuchElementException();
}

次のようにすれば、上記のようなコードは書かずに、タスクを美しく表現することができます。

public Employee findEmployee(int id) {        
    return getFromDB(id).orElseThrow();
}

上記のコードでは、コール元のメソッドにjava.util.NoSuchElementExceptionをスローします。

APIメモ:orElseThrow()メソッドは、Java 10以降に存在します。まだJava 8または9をお使いの方は、レシピ7を検討してください。

レシピ7:値が存在しない場合、どうすれば明示的な例外をスローできるか。レシピ6の方法では、明示的でない例外の一種であるNoSuchElementExceptionをスローすることしかできませんでした。状況をよりよく説明する、さらに適切な情報とともに問題をクライアントに報告するためには、このような例外では不十分です。

レシピ5では、orElseThrow(Supplier<? extends X> exceptionSupplier)メソッドを使いました。このメソッドは、isPresent()とget()のペアに代わる美しい方法です。したがって、次のコードは避けるように努めてください。

@GetMapping("/employees/{id}")
public Employee getEmployee(@PathVariable("id") String id) {
    Optional<Employee> foundEmployee = HrRepository.findByEmployeeId(id);
    if(foundEmployee.isPresent())
        return foundEmployee.get();
    else
        throw new NotFoundException("Employee not found with id " + id);
}

orElseThrow()メソッドは、Optionalに値が存在しない場合に、渡された明示的な例外をスローするだけのものです。そこで、次のようにして先ほどのメソッドを美しく書き換えてみます。

@GetMapping("/employees/{id}")
public Employee getEmployee(@PathVariable("id") String id) {
    return HrRepository
    .findByEmployeeId(id)
    .orElseThrow(
        () -> new NotFoundException("Employee not found with id " + id));
}

さらに、空の例外をスローするだけでよければ、次のようにすることもできます。

return status.orElseThrow(NotFoundException::new);

 

APIメモ:orElseThrow()メソッドにnullを渡すと、値が存在しない場合にNullPointerExceptionがスローされます。orElseThrow(Supplier<? extends X> exceptionSupplier)メソッドは、Java 8以降に存在します。

 

どうすればOptional値を効率的に使用できますか

レシピ8:Optional値が存在する場合にのみアクションを実行したい場合は、isPresent()-get()を使わない。Optional値が存在する場合にのみアクションを実行し、そうでない場合は何もしたくないことがあります。そんなときは、ifPresent(Consumer<? super T> action)メソッドの出番です。このメソッドには、引数としてコンシューマ・アクションを渡します。ちなみに、次のコードは避けてください。

1 Optional<String> confName = Optional.of("CodeOne");
2 if(confName.isPresent())
3    System.out.println(confName.get().length());

ifPresent()を使っている点はまったく問題ありません。2行目と3行目を次の1行と置き換えます。

confName.ifPresent( s -> System.out.println(s.length()));

APIメモ:ifPresent()メソッドは何も返しません。このメソッドは、Java 8以降に存在します。

レシピ9:値が存在しない場合に引数なしのアクションを実行するために、isPresent()/get()は使わない。開発者は、Optional値が存在する場合に何かを行う一方、そうでない場合は引数のないアクションを実行するコードを書くことがあります。たとえば、次のようなものです。

1 Optional<Employee> employee = ... ;
2 if(employee.isPresent()) {
3    log.debug("Found Employee: {}" , employee.get().getName());
4 } else {
5    log.error("Employee not found");
6 }

ifPresentOrElse()はifPresent()に似ていますが、唯一の違いとして、else分岐にも対応していることに注意してください。そのため、2行目から6行目を以下で置き換えることができます。

employee.ifPresentOrElse(
emp -> log.debug("Found Employee: {}",emp.getName()), 
() -> log.error("Employee not found"));

APIメモ:ifPresentOrElse()メソッドは、Java 9以降に存在します。

レシピ10:値が存在しない場合は、別のOptionalを返す。Optionalの値が存在する場合は値を記述するOptionalを返し、存在しない場合はサプライヤ関数が生成するOptionalを返す場合もあります。次のコードは避けてください。

Optional<String> defaultJobStatus = Optional.of("Not started yet.");
public Optional<String> fetchJobStatus(int jobId) {
    Optional<String> foundStatus = ... ; // fetch declared job status by id
    if (foundStatus.isPresent())
        return foundStatus;
    else
        return defaultJobStatus; 
}

これを実現するためにorElse()メソッドやorElseGet()メソッドを使い過ぎないでください。この2つのメソッドは、いずれもラップされていない値を返すからです。そのため、次のようなコードも避けてください。

public Optional<String> fetchJobStatus(int jobId) {
    Optional<String> foundStatus = ... ; // 宣言されたジョブのステータスをIDによりフェッチ
    return foundStatus.orElseGet(() -> Optional.<String>of("Not started yet."));
}

美しく完璧なソリューションは、or (Supplier<? extends Optional<? extends T>> supplier)メソッドを使うものです。次のコードをご覧ください。

 1 public Optional<String> fetchJobStatus(int jobId) {
2    Optional<String> foundStatus = ... ; // 宣言されたジョブのステータスをIDによりフェッチ
3    return foundStatus.or(() -> defaultJobStatus);
4 }

最初にオプショナル型defaultJobStatusを定義せず、3行目のコードを次のコードで置き換えることもできます。

return foundStatus.or(() -> Optional.of("Not started yet."));

APIメモ:サプライヤ関数がnullであるか、またはnullの結果を生成する場合、or()はNullPointerExceptionをスローします。このメソッドは、Java 9以降に存在します。

レシピ11:Optionalのステータスは、空かどうかにかかわらず取得する。Java 11以降では、isEmpty()メソッドを使って、Optionalが空かどうかを直接確認できます。このメソッドは、Optionalが空であればtrueを返します。そのため、次のコードは記述しないでください。

1 public boolean isMovieListEmpty(int id){
2    Optional<MovieList> movieList = ... ;
3    return !movieList.isPresent();
4 }

3行目を次の行で置き換えて、コードの可読性を高めることができます。

return movieList.isEmpty();

APIメモ:isEmpty()メソッドは、Java 11以降に存在します。

レシピ12:Optionalを使い過ぎない。開発者には、お気に入りの機能を使い過ぎる傾向が見られる場合もあります。Optionalクラスもその1つです。値を取得するという目的だけでメソッド・チェーンを作成し、至るところでOptionalを使おうとする開発者もいます。しかしこれは、わかりやすさ、メモリ・フットプリント、単純さを顧みない方法です。そのため、次のコードは避けてください。

1 public String fetchJobStatus(int jobId) {
2    String status = ... ; // fetch declared job status by id
3    return Optional.ofNullable(status).orElse("Not started yet.");
4 }

わかりやすい次のコードで3行目を置き換えて、単純にしてください。

return status == null ? "Not started yet." : status;

 

まとめ

他のJava言語機能と同じく、Optionalにも正しい使い方と誤った使い方があります。Optionalクラスの最適な使い方を知るためには、本記事で掘り下げた内容を理解し、紹介したレシピをすぐに使えるようにして、ツールキットを強化することが必要です。

筆者は、19世紀の米国の作家であるOliver Wendell Holmes, Sr.の言葉、「若者はルールを知る。しかし老人は例外を知る」がお気に入りです。

ここでは、Optionalクラスの表面をなぞっただけにすぎません。次回の記事では、筆者が開発者のコード(筆者自身のコードも含みます)で気づいた、Optionalのアンチパターンに迫ってみたいと思います。その後、Stream APIや変換などを扱うさらに高度なレシピを紹介し、Optionalクラスに含まれる他のメソッドの使い方について解説したいと思います。

詳しく学びたい方は、Java SE 15 APIのOptionalに関するドキュメントや、Raoul-Gabriel Urma氏、Mario Fusco氏、Alan Mycroft氏による『Java 8 in Action』(Manning、2014年)をご覧ください。


Mohamed Taman

Mohamed Taman(@_tamanm):Sirius-XI InnovationsのCEO/オーナー、およびDevTech Systemsのシニア・ソリューション・アーキテクト。セルビアのベオグラードを拠点とするJava Champion、Oracle Groundbreakerで、Jakarta EEのAdopt-a-SpecプログラムとOpenJDKのAdopt-a-JSRのメンバーを務める。

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.