Guide to Java 8オプション

Java 8オプションガイド

1. 概要

このチュートリアルでは、Java 8で導入されたOptionalクラスを紹介します。

このクラスの目的は、nullの参照ではなく、オプションの値を表すためのタイプレベルのソリューションを提供することです。

Optionalクラスを気にする必要がある理由をより深く理解するには、the official Oracle’s articleを見てください。

参考文献:

Java戻り型としてオプション

ベストプラクティスと、JavaでOptional型を返すタイミングについて説明します。

Java 9のオプションのAPIの追加

JavaのオプションAPIの新しいメソッドの迅速で実用的な例。

2. Optionalオブジェクトの作成

Optionalオブジェクトを作成する方法はいくつかあります。 空のOptionalオブジェクトを作成するには、そのempty静的メソッドを使用する必要があります。

@Test
public void whenCreatesEmptyOptional_thenCorrect() {
    Optional empty = Optional.empty();
    assertFalse(empty.isPresent());
}

isPresent()メソッドを使用して、Optionalオブジェクト内に値があるかどうかを確認したことに注意してください。 値は、null以外の値でOptionalを作成した場合にのみ存在します。 次のセクションでは、isPresentメソッドについて説明します。

静的メソッドofを使用してOptionalオブジェクトを作成することもできます。

@Test
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
    String name = "example";
    Optional opt = Optional.of(name);
    assertTrue(opt.isPresent());
}

ただし、of()メソッドに渡される引数をnull.にすることはできません。そうしないと、NullPointerExceptionが取得されます。

@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
    String name = null;
    Optional.of(name);
}

ただし、いくつかのnull値が予想される場合は、ofNullable()メソッドを使用できます。

@Test
public void givenNonNull_whenCreatesNullable_thenCorrect() {
    String name = "example";
    Optional opt = Optional.ofNullable(name);
    assertTrue(optionalName.isPresent());
}

これを行うことにより、null参照を渡すと、例外はスローされず、空のOptionalオブジェクトが返されます。

@Test
public void givenNull_whenCreatesNullable_thenCorrect() {
    String name = null;
    Optional opt = Optional.ofNullable(name);
    assertFalse(optionalName.isPresent());
}

3. 値の存在の確認:isPresent()およびisEmpty()

メソッドから返された、または作成されたOptionalオブジェクトがある場合、isPresent()メソッドを使用してそのオブジェクトに値があるかどうかを確認できます。

@Test
public void givenOptional_whenIsPresentWorks_thenCorrect() {
    Optional opt = Optional.of("example");
    assertTrue(opt.isPresent());

    opt = Optional.ofNullable(null);
    assertFalse(opt.isPresent());
}

ラップされた値がnull.でない場合、このメソッドはtrueを返します

また、Java 11の時点では、isEmpty メソッドを使用して反対のことを行うことができます。

@Test
public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
    Optional opt = Optional.of("example");
    assertFalse(opt.isEmpty());

    opt = Optional.ofNullable(null);
    assertTrue(opt.isEmpty());
}

4. ifPresent()を使用した条件付きアクション

ifPresent()メソッドを使用すると、ラップされた値がnull以外であることが判明した場合に、その値に対してコードを実行できます。 Optionalの前に、次のことを行います。

if(name != null) {
    System.out.println(name.length());
}

このコードは、name変数がnullであるかどうかをチェックしてから、コードを実行します。 このアプローチは時間がかかり、それだけが問題ではなく、エラーが発生しやすくなります。

実際、その変数を出力した後、それを再度使用してからforget to perform the null checkを使用しないことを保証します。

This can result in a NullPointerException at runtime if a null value finds its way into that code.入力の問題が原因でプログラムが失敗した場合、それは多くの場合、不十分なプログラミング手法の結果です。

Optionalを使用すると、優れたプログラミング手法を適用する方法として、null許容値を明示的に処理できます。 ここで、上記のコードをJava 8でリファクタリングする方法を見てみましょう。

典型的な関数型プログラミングスタイルでは、実際に存在するオブジェクトに対してアクションを実行できます。

@Test
public void givenOptional_whenIfPresentWorks_thenCorrect() {
    Optional opt = Optional.of("example");
    opt.ifPresent(name -> System.out.println(name.length()));
}

上記の例では、2行のコードのみを使用して、最初の例で機能した5行を置き換えています。 1行でオブジェクトをOptionalオブジェクトにラップし、次の行で暗黙的な検証を実行し、コードを実行します。

5. orElse()のデフォルト値

orElse()メソッドは、Optionalインスタンス内にラップされた値を取得するために使用されます。 デフォルト値として機能する1つのパラメーターを取ります。 orElse()メソッドは、ラップされた値が存在する場合はそれを返し、存在しない場合はその引数を返します。

@Test
public void whenOrElseWorks_thenCorrect() {
    String nullName = null;
    String name = Optional.ofNullable(nullName).orElse("john");
    assertEquals("john", name);
}

6. orElseGet()のデフォルト値

orElseGet()メソッドはorElse()に似ています。 ただし、Optional値が存在しない場合に返す値を取得する代わりに、呼び出されて呼び出しの値を返すサプライヤ機能インターフェイスを取得します。

@Test
public void whenOrElseGetWorks_thenCorrect() {
    String nullName = null;
    String name = Optional.ofNullable(nullName).orElseGet(() -> "john");
    assertEquals("john", name);
}

7. orElseorElseGet()の違い

OptionalまたはJava 8を初めて使用する多くのプログラマーにとって、orElse()orElseGet()の違いは明確ではありません。 実際のところ、これらの2つの方法は、機能が互いに重複しているという印象を与えます。

ただし、2つの間には微妙ですが非常に重要な違いがあり、十分に理解されていない場合、コードのパフォーマンスに大幅な影響を与える可能性があります。

テストクラスにgetMyDefault()というメソッドを作成してみましょう。このメソッドは引数をとらず、デフォルト値を返します。

public String getMyDefault() {
    System.out.println("Getting Default Value");
    return "Default Value";
}

2つのテストを見て、それらの副作用を観察して、orElse()orElseGet()が重複する場所と異なる場所の両方を確認してみましょう。

@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
    String text = null;

    String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Default Value", defaultText);

    defaultText = Optional.ofNullable(text).orElse(getMyDefault());
    assertEquals("Default Value", defaultText);
}

上記の例では、Optionalオブジェクト内にnullテキストをラップし、2つのアプローチのそれぞれを使用してラップされた値を取得しようとします。 副作用は次のとおりです。

Getting default value...
Getting default value...

getMyDefault()メソッドがそれぞれの場合に呼び出されます。 when the wrapped value is not present, then both orElse() and orElseGet() work exactly the same wayが発生することがあります。

次に、値が存在する別のテストを実行してみましょう。理想的には、デフォルト値を作成しないでください。

@Test
public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
    String text = "Text present";

    System.out.println("Using orElseGet:");
    String defaultText
      = Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Text present", defaultText);

    System.out.println("Using orElse:");
    defaultText = Optional.ofNullable(text).orElse(getMyDefault());
    assertEquals("Text present", defaultText);
}

上記の例では、null値をラップしなくなり、残りのコードは同じままです。 次に、このコードを実行した場合の副作用を見てみましょう。

Using orElseGet:
Using orElse:
Getting default value...

ラップされた値を取得するためにorElseGet()を使用する場合、含まれている値が存在するため、getMyDefault()メソッドは呼び出されないことに注意してください。

ただし、orElse()を使用する場合、ラップされた値が存在するかどうかに関係なく、デフォルトのオブジェクトが作成されます。 そのため、この場合、使用されない冗長オブジェクトを1つ作成しました。

この単純な例では、JVMはデフォルトオブジェクトの処理方法を知っているため、デフォルトオブジェクトの作成に大きなコストはかかりません。 However, when a method such as getMyDefault() has to make a web service call or even query a database, then the cost becomes very obvious.

8. orElseThrow()の例外

orElseThrow()メソッドはorElse()orElseGet()に続き、存在しない値を処理するための新しいアプローチを追加します。 ラップされた値が存在しないときにデフォルト値を返す代わりに、例外をスローします。

@Test(expected = IllegalArgumentException.class)
public void whenOrElseThrowWorks_thenCorrect() {
    String nullName = null;
    String name = Optional.ofNullable(nullName).orElseThrow(
      IllegalArgumentException::new);
}

ここでは、例外コンストラクターを渡すために、Java 8のメソッド参照が役立ちます。

9. get()の戻り値

ラップされた値を取得するための最後のアプローチは、get()メソッドです。

@Test
public void givenOptional_whenGetsValue_thenCorrect() {
    Optional opt = Optional.of("example");
    String name = opt.get();
    assertEquals("example", name);
}

ただし、上記の3つのアプローチとは異なり、get()は、ラップされたオブジェクトがnullでない場合にのみ値を返すことができます。それ以外の場合は、そのような要素の例外はスローされません。

@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
    Optional opt = Optional.ofNullable(null);
    String name = opt.get();
}

これは、get()メソッドの主な欠陥です。 理想的には、Optionalは、このような予期しない例外を回避するのに役立つはずです。 したがって、このアプローチはOptionalの目的に反して機能し、将来のリリースで非推奨になる可能性があります。

したがって、nullの場合に備えて明示的に処理できるようにする、他のバリアントを使用することをお勧めします。

10. filter()による条件付きリターン

filterメソッドを使用して、ラップされた値に対してインラインテストを実行できます。 述語を引数として取り、Optionalオブジェクトを返します。 ラップされた値が述部によるテストに合格した場合、Optionalはそのまま返されます。

ただし、述語がfalseを返す場合、空のOptionalを返します。

@Test
public void whenOptionalFilterWorks_thenCorrect() {
    Integer year = 2016;
    Optional yearOptional = Optional.of(year);
    boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
    assertTrue(is2016);
    boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
    assertFalse(is2017);
}

filterメソッドは通常、事前定義されたルールに基づいてラップされた値を拒否するためにこの方法で使用されます。 これを使用して、間違った電子メール形式または十分に強力でないパスワードを拒否できます。

別の意味のある例を見てみましょう。 モデムを購入したいとし、その価格だけを気にしているとしましょう。 特定のサイトからモデムの価格に関するプッシュ通知を受け取り、これらをオブジェクトに保存します。

public class Modem {
    private Double price;

    public Modem(Double price) {
        this.price = price;
    }
    // standard getters and setters
}

次に、これらのオブジェクトを、モデムの価格が予算内に収まっているかどうかを確認することを唯一の目的とするコードに送ります。

Optionalのないコードを見てみましょう。

public boolean priceIsInRange1(Modem modem) {
    boolean isInRange = false;

    if (modem != null && modem.getPrice() != null
      && (modem.getPrice() >= 10
        && modem.getPrice() <= 15)) {

        isInRange = true;
    }
    return isInRange;
}

特にif条件で、これを実現するために作成する必要のあるコードの量に注意してください。 アプリケーションにとって重要な条件が最後の価格帯チェックである場合の唯一の部分。残りのチェックは防御的です:

@Test
public void whenFiltersWithoutOptional_thenCorrect() {
    assertTrue(priceIsInRange1(new Modem(10.0)));
    assertFalse(priceIsInRange1(new Modem(9.9)));
    assertFalse(priceIsInRange1(new Modem(null)));
    assertFalse(priceIsInRange1(new Modem(15.5)));
    assertFalse(priceIsInRange1(null));
}

それとは別に、コンパイル時のエラーが発生することなく、長い日のnullチェックを忘れることができます。

ここで、Optional#filterのバリアントを見てみましょう。

public boolean priceIsInRange2(Modem modem2) {
     return Optional.ofNullable(modem2)
       .map(Modem::getPrice)
       .filter(p -> p >= 10)
       .filter(p -> p <= 15)
       .isPresent();
 }

The map call is simply used to transform a value to some other value.この操作は元の値を変更しないことに注意してください。

この例では、Modelクラスから価格オブジェクトを取得しています。 次のセクションでは、map()メソッドについて詳しく説明します。

まず、nullオブジェクトがこのメソッドに渡された場合、問題は発生しません。

第二に、その本体内に記述する唯一のロジックは、メソッド名が正確に記述するもの、価格帯のチェックです。 Optionalが残りを処理します:

@Test
public void whenFiltersWithOptional_thenCorrect() {
    assertTrue(priceIsInRange2(new Modem(10.0)));
    assertFalse(priceIsInRange2(new Modem(9.9)));
    assertFalse(priceIsInRange2(new Modem(null)));
    assertFalse(priceIsInRange2(new Modem(15.5)));
    assertFalse(priceIsInRange2(null));
}

以前のアプローチは価格帯をチェックすることを約束しますが、その固有の脆弱性から守るためにそれ以上のことをしなければなりません。 したがって、filterメソッドを使用して、不要なifステートメントを置き換え、不要な値を拒否できます。

11. map()による値の変換

前のセクションでは、フィルターに基づいて値を拒否または受け入れる方法について説明しました。 同様の構文を使用して、map()メソッドでOptional値を変換できます。

@Test
public void givenOptional_whenMapWorks_thenCorrect() {
    List companyNames = Arrays.asList(
      "paypal", "oracle", "", "microsoft", "", "apple");
    Optional> listOptional = Optional.of(companyNames);

    int size = listOptional
      .map(List::size)
      .orElse(0);
    assertEquals(6, size);
}

この例では、文字列のリストをOptionalオブジェクト内にラップし、そのmapメソッドを使用して、含まれているリストに対してアクションを実行します。 実行するアクションは、リストのサイズを取得することです。

mapメソッドは、Optional内にラップされた計算の結果を返します。 次に、返されたOptionalに対して適切なメソッドを呼び出して、その値を取得する必要があります。

filterメソッドは単に値のチェックを実行し、booleanを返すことに注意してください。 一方、mapメソッドは既存の値を取得し、この値を使用して計算を実行し、Optionalオブジェクトにラップされた計算の結果を返します。

@Test
public void givenOptional_whenMapWorks_thenCorrect2() {
    String name = "example";
    Optional nameOptional = Optional.of(name);

    int len = nameOptional
     .map(String::length)
     .orElse(0);
    assertEquals(8, len);
}

mapfilterをチェーンして、より強力なことを行うことができます。

ユーザーが入力したパスワードの正確さを確認したいとします。 map変換を使用してパスワードをクリーンアップし、filterを使用してパスワードが正しいかどうかを確認できます。

@Test
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
    String password = " password ";
    Optional passOpt = Optional.of(password);
    boolean correctPassword = passOpt.filter(
      pass -> pass.equals("password")).isPresent();
    assertFalse(correctPassword);

    correctPassword = passOpt
      .map(String::trim)
      .filter(pass -> pass.equals("password"))
      .isPresent();
    assertTrue(correctPassword);
}

ご覧のように、最初に入力をクリーンにせずにフィルターで除外しますが、ユーザーは先頭と末尾のスペースがすべて入力を構成することを当然のことと考えるかもしれません。 そのため、不正なパスワードを除外する前に、ダーティなパスワードをmapのクリーンなパスワードに変換します。

12. flatMap()による値の変換

map()メソッドと同様に、値を変換する代わりにflatMap()メソッドもあります。 違いは、mapはラップされていない場合にのみ値を変換するのに対し、flatMapはラップされた値を取得し、変換する前にアンラップすることです。

以前は、Optionalインスタンスでラップするための単純なStringおよびIntegerオブジェクトを作成しました。 ただし、頻繁に、これらのオブジェクトを複雑なオブジェクトのアクセサーから受け取ります。

違いをより明確に把握するために、名前、年齢、パスワードなどの個人の詳細を取得するPersonオブジェクトを見てみましょう。

public class Person {
    private String name;
    private int age;
    private String password;

    public Optional getName() {
        return Optional.ofNullable(name);
    }

    public Optional getAge() {
        return Optional.ofNullable(age);
    }

    public Optional getPassword() {
        return Optional.ofNullable(password);
    }

    // normal constructors and setters
}

通常、このようなオブジェクトを作成し、Stringの場合と同じように、Optionalオブジェクトでラップします。 または、別のメソッド呼び出しで返すことができます:

Person person = new Person("john", 26);
Optional personOptional = Optional.of(person);

Personオブジェクトをラップすると、ネストされたOptionalインスタンスが含まれることに注意してください。

@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
    Person person = new Person("john", 26);
    Optional personOptional = Optional.of(person);

    Optional> nameOptionalWrapper
      = personOptional.map(Person::getName);
    Optional nameOptional
      = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
    String name1 = nameOptional.orElse("");
    assertEquals("john", name1);

    String name = personOptional
      .flatMap(Person::getName)
      .orElse("");
    assertEquals("john", name);
}

ここでは、Personオブジェクトのname属性を取得して、アサーションを実行しようとしています。

3番目のステートメントのmap()メソッドでこれをどのように達成するかに注意してください。その後、flatMap()メソッドで同じことを行う方法に注目してください。

Person::getNameメソッド参照は、パスワードをクリーンアップするために前のセクションで行ったString::trim呼び出しに似ています。

唯一の違いは、getName()は、trim()操作のように文字列ではなくOptionalを返すことです。 これは、map変換が結果をOptionalオブジェクトにラップするという事実と相まって、ネストされたOptionalにつながります。

したがって、map()メソッドを使用している間は、変換された値を使用する前に、値を取得するための呼び出しを追加する必要があります。 このようにして、Optionalラッパーが削除されます。 この操作は、flatMapを使用するときに暗黙的に実行されます。

13. Java 8でOptionalsをチェーンする

場合によっては、いくつかのOptionalsから最初の空でないOptionalオブジェクトを取得する必要があります。 このような場合、orElseOptional()のようなメソッドを使用すると非常に便利です。 残念ながら、このような操作はJava 8では直接サポートされていません。

最初に、このセクション全体で使用するいくつかの方法を紹介しましょう。

private Optional getEmpty() {
    return Optional.empty();
}

private Optional getHello() {
    return Optional.of("hello");
}

private Optional getBye() {
    return Optional.of("bye");
}

private Optional createOptional(String input) {
    if (input == null || "".equals(input) || "empty".equals(input)) {
        return Optional.empty();
    }
    return Optional.of(input);
}

複数のOptionalオブジェクトをチェーンし、Java 8で最初の空でないオブジェクトを取得するには、StreamAPIを使用できます。

@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {
    Optional found = Stream.of(getEmpty(), getHello(), getBye())
      .filter(Optional::isPresent)
      .map(Optional::get)
      .findFirst();

    assertEquals(getHello(), found);
}

このアプローチの欠点は、空でないOptionalStreamのどこにあるかに関係なく、すべてのgetメソッドが常に実行されることです。

Stream.of()に渡されたメソッドを遅延評価する場合は、メソッド参照とSupplierインターフェイスを使用する必要があります。

@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
    Optional found =
      Stream.>>of(this::getEmpty, this::getHello, this::getBye)
        .map(Supplier::get)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .findFirst();

    assertEquals(getHello(), found);
}

引数を取るメソッドを使用する必要がある場合、ラムダ式に頼らなければなりません:

@Test
public void givenTwoOptionalsReturnedByOneArgMethod_whenChaining_thenFirstNonEmptyIsReturned() {
    Optional found = Stream.>>of(
      () -> createOptional("empty"),
      () -> createOptional("hello")
    )
      .map(Supplier::get)
      .filter(Optional::isPresent)
      .map(Optional::get)
      .findFirst();

    assertEquals(createOptional("hello"), found);
}

多くの場合、チェーンされたOptionalsがすべて空の場合に備えて、デフォルト値を返したいと思うでしょう。 次の例のように、orElse()またはorElseGet()に呼び出しを追加するだけでこれを行うことができます。

@Test
public void givenTwoEmptyOptionals_whenChaining_thenDefaultIsReturned() {
    String found = Stream.>>of(
      () -> createOptional("empty"),
      () -> createOptional("empty")
    )
      .map(Supplier::get)
      .filter(Optional::isPresent)
      .map(Optional::get)
      .findFirst()
      .orElseGet(() -> "default");

    assertEquals("default", found);
}

14. JDK 9Optional API

Java 9のリリースにより、OptionalAPIにさらに多くの新しいメソッドが追加されました。

  • 代替のOptionalを作成するサプライヤを提供するためのor()メソッド

  • Optionalが存在する場合はアクションを実行し、存在しない場合は別のアクションを実行できるifPresentOrElse()メソッド

  • OptionalStreamに変換するためのstream()メソッド

これがfurther readingの完全な記事です。

15. Optionalsの誤用

最後に、Optionalsを使用する魅力的で危険な方法を見てみましょう。Optionalパラメータをメソッドに渡します。

Personのリストがあり、そのリストから特定の名前の人を検索するメソッドが必要だとします。 また、指定されている場合は、そのメソッドが少なくとも特定の年齢のエントリと一致するようにします。 このパラメーターはオプションであり、次のメソッドが付属しています。

public List search(List people, String name, Optional age) {
    // Null checks for people and name
    people.stream()
      .filter(p -> p.getName().equals(name))
      .filter(p -> p.getAge() >= age.orElse(0))
      .collect(Collectors.toList());
}

次に、メソッドをリリースし、別の開発者がそれを使用しようとします。

someObject.search(people, "Peter", null);

これで、開発者はコードを実行し、NullPointerException.There we are, having to null check our optional parameter, which defeats our initial purpose in wanting to avoid this kind of situation.を取得します

これをうまく処理するためにできることはいくつかあります。

public List search(List people, String name, Integer age) {
    // Null checks for people and name

    age = age != null ? age : 0;
    people.stream()
      .filter(p -> p.getName().equals(name))
      .filter(p -> p.getAge() >= age)
      .collect(Collectors.toList());
}

ここでは、パラメータはまだオプションですが、1回のチェックで処理します。 別の可能性はcreate two overloaded methodsでした:

public List search(List people, String name) {
    return doSearch(people, name, 0);
}

public List search(List people, String name, int age) {
    return doSearch(people, name, age);
}

private List doSearch(List people, String name, int age) {
    // Null checks for people and name
    return people.stream()
      .filter(p -> p.getName().equals(name))
      .filter(p -> p.getAge() >= age)
      .collect(Collectors.toList());
}

このようにして、2つのメソッドが異なることを行う明確なAPIを提供します(ただし、実装は共有されます)。

そのため、メソッドパラメータとしてOptionalsを使用しないようにするソリューションがあります。 The intent of Java when releasing Optional was to use it as a return type、したがって、メソッドが空の値を返す可能性があることを示します。 実際のところ、メソッドパラメータとしてOptionalを使用する方法は、discouraged by some code inspectorsですらあります。

16. 結論

この記事では、Java 8Optionalクラスの重要な機能のほとんどについて説明しました。

また、明示的なnullチェックと入力検証の代わりにOptionalを使用することを選択するいくつかの理由についても簡単に説明しました。

また、get(),orElse()メソッドとorElseGet()メソッドを使用して、Optionalの値、または空の場合はデフォルトの値を取得する方法も学習しました(the important difference between the two lastを見ました)。

次に、Optionalsをmap(), flatMap()filter()で変換またはフィルタリングする方法を確認しました。

さまざまなメソッドを簡単にチェーンできるため、流暢なAPIOptionalが提供するものを確認しました。

最後に、メソッドパラメータとしてOptionalsを使用することは悪い考えであり、それを回避する方法を見てきました。

記事のすべての例のソースコードは利用可能ですover on GitHub.