Перейти к содержимому
Теория · Лямбды и Stream API · урок 11 из 12

Optional

Вернёмся к концу конвейера. Иногда сборщику нужен не ящик со всем, что доехало, а одна коробка — первая, которая прошла все станции. Но что, если до конца не доехало ничего?

Лента могла бы отдать null. С null вы уже встречались: он выглядит как обычная ссылка, пока на нём не вызовут метод, и тогда программа падает с NullPointerException.

Поэтому лента отдаёт находку в прозрачном футляре. В футляре либо лежит коробка, либо он пустой, и это видно сразу. Прежде чем взять коробку, вы смотрите в футляр. В Java такой футляр называется Optional:

JAVA
import java.util.List;
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("cat", "zebra", "ox");
        Optional<String> found =
            words.stream().filter(w -> w.length() > 4).findFirst();
        Optional<String> missing =
            words.stream().filter(w -> w.length() > 9).findFirst();
        System.out.println(found.orElse("none"));   // zebra
        System.out.println(missing.orElse("none")); // none
    }
}

Давайте разберём программу по строкам:

  1. В первом стриме filter пропускает только слова длиннее четырёх букв, и до конца доезжает zebra.
  2. findFirst() — терминальная операция: она берёт первый элемент, который доехал до конца, и кладёт его в футляр Optional<String>.
  3. Во втором стриме слов длиннее девяти букв нет, и findFirst() возвращает пустой футляр.
  4. orElse("none") отдаёт содержимое футляра, а если футляр пуст — запасное значение "none". Поэтому программа печатает zebra и none.

Футляр вместо null

Optional — объект-контейнер, в котором либо лежит одно значение, либо нет ничего. Метод, который может ничего не найти, возвращает Optional, и по его типу сразу видно: результата может не быть. Обычная ссылка такого предупреждения не даёт.

Если напечатать футляр целиком, println покажет Optional[zebra] для полного и Optional.empty для пустого. Так же, как findFirst, футляр возвращают max и min стрима: у пустого списка нет ни самого длинного, ни самого короткого слова.

Зачем нужен Optional, часто спрашивают на собеседовании. Ответ: чтобы отсутствие результата было видно по типу, а не пряталось в null.

Как достать значение

У футляра есть несколько способов отдать содержимое, и каждый по-своему поступает с пустым футляром.

orElse

orElse(x) возвращает значение из футляра, а для пустого — запасное значение x. Результат есть всегда, поэтому это самый спокойный способ.

isPresent и get

isPresent() отвечает, есть ли что-то в футляре, а get() достаёт значение. Вместе их пишут так: сначала проверка, потом get().

ifPresent

ifPresent принимает Consumer и выполняет его, только если значение есть. Для пустого футляра он не делает ничего.

JAVA
if (found.isPresent()) {
    System.out.println(found.get().length()); // 5
}
found.ifPresent(w -> System.out.println("found " + w)); // found zebra
missing.ifPresent(w -> System.out.println("found " + w));

Разберём по строкам:

  1. found.isPresent() даёт true, поэтому get() безопасен и возвращает "zebra", а печать даёт 5.
  2. found.ifPresent(...) печатает found zebra.
  3. missing.ifPresent(...) ничего не печатает: футляр пуст.

Здесь картинка с футляром неточна. В настоящий футляр можно положить другую коробку, а Optional не меняется: в нём навсегда то, что положил туда метод.

Частая ошибка

get() вызвали, не заглянув в футляр:

JAVA
System.out.println(missing.get()); // ✗
TEXT
Exception in thread "main" java.util.NoSuchElementException: No value present

Программа падает: футляр пуст, и get() бросает исключение. Это та же беда, что с null, только под другим именем: футляр не помог, потому что в него не заглянули. Берите orElse или ifPresent, а get() вызывайте только после проверки isPresent().

Главное

  • Optional — футляр, в котором либо одно значение, либо ничего; findFirst возвращает именно его.
  • orElse(x) отдаёт значение или запасное x, а ifPresent выполняет действие, только если значение есть.
  • get() на пустом Optional бросает NoSuchElementException, поэтому без проверки isPresent() его не вызывают.
  • Пустой результат в Optional виден по типу, а null прячется до первого вызова метода.