Руководство по Java 8 Необязательно

Руководство по Java 8 Необязательно

1. обзор

В этом руководстве мы собираемся показать классOptional, который был представлен в Java 8.

Цель класса - предоставить решение на уровне типа для представления дополнительных значений вместо ссылокnull.

Чтобы лучше понять, почему нам следует заботиться о классеOptional, взгляните наthe official Oracle’s article.

Дальнейшее чтение:

Java Необязательный тип возврата

Изучите лучшие практики и когда возвращать необязательный тип в Java.

Read more

Java 9 Необязательные дополнения API

Быстрые и практические примеры новых методов в Optional API в Java.

Read more

2. Создание объектовOptional

Есть несколько способов создания объектовOptional. Чтобы создать пустой объектOptional, нам просто нужно использовать его статический методempty:

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

Обратите внимание, что мы использовали методisPresent(), чтобы проверить, есть ли значение внутри объектаOptional. Значение присутствует, только если мы создалиOptional со значением, отличным отnull. Мы рассмотрим методisPresent в следующем разделе.

Мы также можем создать объектOptional с помощью статического методаof:

@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());
}

Этот метод возвращаетtrue, если обернутое значение неnull.

Кроме того, начиная с 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());
}

Этот код проверяет, является ли переменная имени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()));
}

В приведенном выше примере мы используем только две строки кода, чтобы заменить пять, которые работали в первом примере. Одна строка для обертывания объекта в объектOptional, а следующая для выполнения неявной проверки, а также выполнения кода.

5. Значение по умолчанию сorElse()

МетодorElse() используется для извлечения значения, заключенного в экземплярOptional. Он принимает один параметр, который действует как значение по умолчанию. Метод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. Разница междуorElse иorElseGet()

Для многих программистов, которые плохо знакомы сOptional или Java 8, разница междуorElse() иorElseGet() не очевидна. На самом деле, эти два метода создают впечатление, что они перекрывают друг друга по функциональности.

Однако между ними есть тонкое, но очень важное различие, которое может существенно повлиять на производительность нашего кода, если его не понять.

Давайте создадим в тестовом классе метод с именемgetMyDefault(), который не принимает аргументов и возвращает значение по умолчанию:

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

Давайте посмотрим на два теста и понаблюдаем за их побочными эффектами, чтобы установить, где перекрываются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 и пытаемся получить обернутое значение, используя каждый из двух подходов. Побочный эффект, как показано ниже:

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(), независимо от того, присутствует ли обернутое значение или нет, создается объект по умолчанию. Так что в этом случае мы только что создали один избыточный объект, который никогда не используется.

В этом простом примере создание объекта по умолчанию не требует значительных затрат, поскольку 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);
}

Однако, в отличие от трех вышеуказанных подходов,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 обычно используется таким образом для отклонения упакованных значений на основе предопределенного правила. Мы можем использовать его, чтобы отклонить неверный формат электронной почты или пароль, который недостаточно надежен.

Давайте посмотрим на еще один показательный пример. Допустим, мы хотим купить модем и заботимся только о его цене. Мы получаем push-уведомления о ценах на модемы с определенного сайта и храним их в объектах:

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. Единственной частью условия 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));
}

Кроме того, можно забыть о нулевых проверках в течение долгого дня без каких-либо ошибок времени компиляции.

Теперь рассмотрим вариант с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()

В предыдущем разделе мы рассмотрели, как отклонить или принять значение на основе фильтра. Мы можем использовать аналогичный синтаксис для преобразования значенияOptional с помощью методаmap():

@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);
}

Мы можем связатьmap иfilter вместе, чтобы сделать что-то более мощное.

Предположим, мы хотим проверить правильность ввода пароля пользователем; мы можем очистить пароль с помощью преобразования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 принимает значение в оболочке и разворачивает его перед преобразованием.

Ранее мы создавали простые объектыString иInteger для упаковки в экземплярOptional. Однако часто мы получаем эти объекты от средства доступа к сложному объекту.

Чтобы получить более четкое представление о разнице, давайте взглянем на объект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
}

Обычно мы создаем такой объект и оборачиваем его в объектOptional, как мы это делали со String. Кроме того, он может быть возвращен нам другим вызовом метода:

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);
}

Здесь мы пытаемся получить атрибут name объектаPerson для выполнения утверждения.

Обратите внимание, как мы достигаем этого с помощью методаmap() в третьем операторе, а затем обратите внимание, как мы делаем то же самое с методомflatMap() впоследствии.

Ссылка на методPerson::getName аналогична вызовуString::trim, который мы использовали в предыдущем разделе для очистки пароля.

Единственное отличие состоит в том, чтоgetName() возвращаетOptional, а не строку, как это делала операцияtrim(). Это, в сочетании с тем фактом, что преобразованиеmap оборачивает результат в объектOptional, приводит к вложенномуOptional.

Следовательно, при использовании методаmap() нам необходимо добавить дополнительный вызов для извлечения значения перед использованием преобразованного значения. Таким образом, оберткаOptional будет удалена. Эта операция выполняется неявно при использованииflatMap.

13. ЦепочкаOptionals в Java 8

Иногда нам может потребоваться получить первый непустой объектOptional из числаOptionals. В таких случаях было бы очень удобно использовать такой метод, как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, мы можем использовать APIStream:

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

    assertEquals(getHello(), found);
}

Обратной стороной этого подхода является то, что все наши методыget всегда выполняются, независимо от того, где вStream появляется непустойOptional.

Если мы хотим лениво оценить методы, переданные в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. API JDK 9Optional

Выпуск Java 9 добавил еще больше новых методов в APIOptional:

  • методor() для предоставления поставщика, который создает альтернативуOptional

  • методifPresentOrElse(), который позволяет выполнить действие, еслиOptional присутствует, или другое действие, если нет

  • stream() метод преобразованияOptional в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());
}

Здесь параметр по-прежнему необязательный, но мы обрабатываем его только за одну проверку. Другой возможностью было бы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());
}

Таким образом, мы предлагаем понятный 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.

Мы также вкратце изучили некоторые причины, по которым мы решили использоватьOptional вместо явной проверки на null и проверки ввода.

Мы также узнали, как получить значениеOptional или значение по умолчанию, если оно пусто, с помощью методовget(),orElse() иorElseGet() (и увиделиthe important difference between the two last ).

Затем мы увидели, как преобразовать или отфильтровать нашOptionals с помощьюmap(), flatMap() иfilter().

Мы увидели, что предлагает свободныйAPIOptional, поскольку он позволяет нам легко связывать различные методы.

Наконец, мы увидели, что использованиеOptionals в качестве параметров метода - плохая идея и как этого избежать.

Исходный код для всех примеров в статье доступенover on GitHub.