Класс Optional появился в Java 8. Задачей этого класса является предоставление решений на уровне типа-обертки для обеспечения удобства обработки возможных null-значений.

Звучит не слишком просто. Наглядно это можно представить так:

Внутри объект может быть, а может и не быть
Внутри объект может быть, а может и не быть

Хорошее представление класса Optional есть в статье на сайте Oracle.

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

Существует несколько способов. Чтобы создать пустой объект можно использовать статический метод empty():

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

Метод isPresent() проверяет наличие значения внутри объекта Optional. Чуть позже мы вернемся к этому методу.

Также объект Optional можно создать с помощью статического метода of():

    @Test
    public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
        String name = "Wine";
        Optional<String> 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 = "One more wine";
        Optional<String> opt = Optional.ofNullable(name);
        assertTrue(opt.isPresent());
    }

Если мы передадим в этот метод null, то вместо NPE получим просто пустой объект Optional:

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

Проверка наличия значений: isPresent() и isEmpty()

Когда у нас имеется объект Optional, который мы получаем из метода или создаем сами, с помощью метода isPresent() можно проверить есть ли что-то внутри:

    @Test
    public void givenOptional_whenIsPresentWorks_thenCorrect() {
        Optional<String> opt = Optional.of("Rioja wine region");
        assertTrue(opt.isPresent());

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

Метод возвращает true, если оборачиваемое значение не null. С Java 11 появился метод isEmpty(), который делает ровно противоположное:

    @Test
    public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
        Optional<String> opt = Optional.of("Rioja 11");
        assertFalse(opt.isEmpty());

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

Метод ifPresent()

Метод ifPresent() позволяет нам выполнить код над объектом внутри Optional, если он не null. Раньше код был такой:

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

Перед тем как выполнить метод, нам необходимо проверить значение на null. Это долго и может привести к ошибкам в будущем. Где гарантия, что далее в коде мы не будем использовать эту переменную и не забудем сделать проверку на null потом?

Тогда, во время выполнения, программа рано или поздно упадет с NullPointerException. Класс Optional позволяет нам лучше и качественнее работать с null-значениями. Посмотрим как можно улучшить этот кусочек кода и перепишем его в функциональном стиле:

    @Test
    public void givenOptional_whenIfPresentWorks_thenCorrect() {
        Optional<String> opt = Optional.ofNullable("Wine again");
        opt.ifPresent(name -> System.out.println(name.length()));
    }
  • 1 строчка - оборачиваем объект в Optional

  • 2 строчка - валидация значения и исполнение кода

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

Метод orElse() используется для получения значения, которое находится внутри Optional. Он принимает один параметр - значение по умолчанию. Таким образом orElse() возвращает или само значение из Optional или переданный аргумент:

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

Значение по умолчанию с orElseGet()

Метод orElseGet() очень похож на метод orElse(). Однако, вместо значения по умолчанию, которое передается в качестве аргумента, метод принимает функциональный интерфейс Supplier, который вызывает определенное действие. Посмотрим на пример:

    @Test
    public void whenOrElseGetWorks_thenCorrect() {
        String nullName = null;
        String name = Optional.ofNullable(nullName).orElseGet(new Supplier<String>() {
            @Override
            public String get() {
                return "Boris";
            }
        });
        assertEquals("Boris", name);
    }

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

String name = Optional.ofNullable(nullName).orElseGet(() -> "Boris");

В чем отличие orElseGet() от orElse()

На первый взгляд, разницы нет. По крайней мере, создается такое впечатление, не так ли? Тем не менее, методы отличаются. Давайте попробуем это понять на примерах. Создадим метод, который ничего не принимает и возвращает значение по умолчанию:

public String getMyDefault() {
    System.out.println("А не выпить ли бокальчик?");
    return "Вера, надежда, вино";
}

Напишем теперь два теста с использованием методов orElseGet() и orElse():

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

        String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
        assertEquals("Вера, надежда, вино", defaultText);

        defaultText = Optional.ofNullable(text).orElse(getMyDefault());
        assertEquals("Вера, надежда, вино", defaultText);
    }

В этом примере мы оборачиваем null-объект в Optional и пытаемся получить его значение с orElseGet() и orElse(). В обоих случаях "побочный" эффект одинаков:

А не выпить ли бокальчик?
А не выпить ли бокальчик?

Метод getMyDefault() вызывается и для orElseGet(), и для orElse(). Когда значение внутри Optional отсутствует, методы работают идентично.

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

    @Test
    public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
        String text = "Вино не может быть null!";

        System.out.print("Метод orElseGet:");
        String defaultText
                = Optional.ofNullable(text).orElseGet(this::getMyDefault);
        assertEquals("Вино не может быть null!", defaultText);

        System.out.print("\nМетод orElse:");
        defaultText = Optional.ofNullable(text).orElse(getMyDefault());
        assertEquals("Вино не может быть null!", defaultText);
    }

А теперь заглянем в консоль:

Метод orElseGet:
Метод orElse:А не выпить ли бокальчик?

Обратите внимание, что в случае метода orElseGet() метод getMyDefault() даже не вызывается. Но в случае использования метода orElse() объект по умолчанию создается и он ни разу даже не используется.

В этом простом примере создание объекта по умолчанию не является для нас чем-то "дорогим", но что если метод getMyDefault() должен выполнить какой-то тяжелый и "дорогостоящий" запрос к БД или другому сервису?

Исключения с orElseThrow()

Метод orElseThrow() вместо предоставления дефолтного метода выбрасывает исключение:

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

С Java 10 orElseThrow() поддерживает пустой конструктор. В случае, если Optional содержит null, выбрасывается NoSuchElementException.

    @Test(expected = NoSuchElementException.class)
    public void whenNoArgOrElseThrowWorks_thenCorrect() {
        String nullName = null;
        String name = Optional.ofNullable(nullName).orElseThrow();
    }

Получение значения с помощью get()

Еще один способ получить значение из Optional - вызвать метод get():

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

В отличие от предыдущих способов get() возвращает значение только если оно не null. В противном случае выбрасывается NoSuchElementException.

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

Условия: метод filter()

Метод filter() принимает предикат в качестве аргумента и возвращает объект Optional. Если значение проходит условие, заданное предикатом, то Optional возвращается без изменений. Если предикат возвращает false, то на выходе мы получим пустой Optional:

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

Метод filter() используется для отбраковки обернутых значений по заранее определенным правилам.

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

class Headphones {
    private Double price;

    public Headphones(Double price) {
        this.price = price;
    }
    // геттеры и сеттеры
}

Теперь мы передаем этот объект какому-то коду, единственная цель которого проверить, подходит ли он нам по цене. Как это будет без Optional:

    public boolean priceIsInRange1(Headphones headphones) {
        boolean isInRange = false;

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

            isInRange = true;
        }
        return isInRange;
    }

Посмотрите сколько кода... Значимых строчек только две - 5 и 6, остальные проверочные

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

Теперь попробуем про все это забыть и сделать с помощью Optional и его метода filter():

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

Метод map() используется для преобразования одного значения в другое. Он не меняет оригинального значения. В итоге мы получаем optional-объект цены.

Во-первых, нас теперь больше не волнует возможное появление null-значений.

Во-вторых, нам пришлось писать только указанную в названии метода логику - проверять цену. Все остальное за нас делает Optional.

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

Согласитесь, решение с Optional в данном примере намного более компактное и избавляет нас от лишних проверок, не связанных с основной логикой.

Изменение значения: метод map()

В прошлом разделе мы увидели как проверить значение на соответствие некоторым условиям. С помощью метода map() можно изменить и само значение, и его тип.

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

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

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

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

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

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

Можно комбинировать методы map() и filter() в цепочки, чтобы сделать какую-то более сложную операцию.

В следующем примере мы сначала очищаем пароль от лишних символов с помощью метода map() и только потом делаем проверку filter():

    @Test
    public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
        String password = " password ";
        Optional<String> 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);
    }

Без предварительной "очистки" наш пароль пройдет сквозь метод фильтра (первый assert).

Изменение значения: метод flatMap()

Разница между map() и flatMap() в том, что map() изменяет только "распакованные" значения, а flatMap() перед изменением "распаковывает" значение самостоятельно. Чтобы это лучше понять, давайте сразу посмотрим на пример. Возьмем произвольный объект Person c полями имя, возраст и пароль:

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

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

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

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

    // конструкторы и сеттеры
}

Представим, что нам вернулся объект Person обернутый в Optional (или же мы сами его таким создали):

Person person = new Person("Владимир", 69);
Optional<Person> personOptional = Optional.of(person);

Теперь если мы попробуем получить поля этого объекта, они тоже будут будут обернуты в Optional:

@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
    Person person = new Person("Владимир", 69);
    Optional<Person> personOptional = Optional.of(person);

    Optional<Optional<String>> nameOptionalWrapper  
      = personOptional.map(Person::getName);
    Optional<String> nameOptional  
      = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
    String name1 = nameOptional.orElse("");
    assertEquals("Владимир", name1);

    String name = personOptional
      .flatMap(Person::getName)
      .orElse("");
    assertEquals("Владимир", name);
}

Обратите внимание, что в этом примере мы получаем доступ к полю объекта двумя путями: с помощью map() и flatMap().

Использование map() оборачивает в Optional то, что мы получаем вызовом getName. В итоге нам приходится делать дополнительную "распаковку". FlatMap() же делает это неявно за нас.

Цепочки Optional в Java 8

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

private Optional<String> getEmpty() {
    return Optional.empty();
}

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

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

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

Используем Stream API, чтобы сформировать цепочку Optional и вернуть первый непустой элемент:

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

Недостатком такого метода является то, что все наши get-методы всегда исполняются, независимо от того где мы встретим затем в потоке непустой объект.

Если мы хотим воспользоваться "ленивой" обработкой, нам нужно использовать ссылку на метод и интерфейс Supplier:

@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
    Optional<String> found =
      Stream.<Supplier<Optional<String>>>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<String> found = Stream.<Supplier<Optional<String>>>of(
      () -> createOptional("empty"),
      () -> createOptional("hello")
    )
      .map(Supplier::get)
      .filter(Optional::isPresent)
      .map(Optional::get)
      .findFirst();

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

Иногда нам необходимо вернуть какое-то значение по умолчанию, если все Optional пустые. Мы можем сделать это, просто добавив вызов orElse() или orElseGet():

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

    assertEquals("default", found);
}

JDK 9 и Optional API

Начиная с 9-ки в Optional добавилось несколько новых методов:

  • метод or() с помощью Supplier создает альтернативный Optional;

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

  • метод stream() преобразует Optional в поток.

Некорректное использование Optional

Представьте, что у нас есть лист Person и мы хотим использовать метод, который будет искать людей по имени. Кроме этого мы хотим, чтобы можно было искать и по возрасту, если он указан. Получится такой код:

public static List<Person> search(List<Person> people, String name, Optional<Integer> age) {
    // проверки на null для people и name
    return people.stream()
            .filter(p -> p.getName().equals(name))
            .filter(p -> p.getAge().get() >= age.orElse(0))
            .collect(Collectors.toList());
}

Вызовем метод:

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

И получим NullPointerException. В итоге мы вынуждены будем проверять наш параметр на null, что противоречит нашей первоначальной задаче избежать этих проверок.

Как можно это исправить? Давайте глянем:

public static List<Person> search(List<Person> people, String name, Integer age) {
    // проверки на null для people и name
    final Integer ageFilter = age != null ? age : 0;

    return people.stream()
            .filter(p -> p.getName().equals(name))
            .filter(p -> p.getAge().get() >= ageFilter)
            .collect(Collectors.toList());
}

Еще один вариант - создать перегруженные методы:

public static List<Person> search(List<Person> people, String name) {
    return doSearch(people, name, 0);
}

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

private static List<Person> doSearch(List<Person> people, String name, int age) {
    // проверки на null для people и name
    return people.stream()
            .filter(p -> p.getName().equals(name))
            .filter(p -> p.getAge().get().intValue() >= age)
            .collect(Collectors.toList());
}

Существуют решения, позволяющие избежать использования Optional в качестве параметров метода. Запомните, что Optional появилось в Java, чтобы его использовали в качестве возвращаемого типа, таким образом указывая, что метод может вернуть пустое значение.

Важно помнить

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

  1. Старайтесь избегать ситуаций с возвратом null из методов. Большинство клиентов вашего API ожидают получать безопасный ответ. Возврат null может привести к NullPointerException. Вместо этого можно вернуть Optional.empty.

  2. Старайтесь не использовать Optional в качестве параметра в методах. Если такое происходит, то придется делать лишнюю обертку для аргумента. Это ухудшит читаемость кода. Если параметра в методе может не быть, то лучше использовать перегруженную версию метода.

  3. В большинстве ситуаций удобнее использовать Optional.map и Optional.flatMap вместо Optional.ifPresent, чтобы избежать лишних проверок.

  4. Optional не имплементирует интерфейс Serializable, поэтому лучше не использовать его в качестве полей класса. Основная его цель - использоваться в качестве возвращаемого значения, если такое значение может отсутствовать.

  5. Не злоупотребляйте использованием Optional.


Послесловие

Я начинающий Java-разработчик. В рамках развития своей карьеры я решил сделать две вещи:

  • Завести канал в ТГ, где буду рассказывать о себе, своем пути и проблемах, с которыми сталкиваюсь - https://t.me/java_wine

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

Надеюсь, буду полезен сообществу и новичкам, которые пойдут по моим или чьим-то еще стопам.

Спасибо!

Комментарии (12)


  1. LeshaRB
    02.04.2022 20:03
    +6

    Вам задавали вопрос, уже раньше, я хочу опять его задать

    Зачем?

    Если вас забанили на Google можно воспользоваться Yandex

    Тут на днях вышла 18 Java, вы отстаете Селиков и Боярсикий уже выпустили книгу OCP Oracle Certified Professional Java SE 17 Developer Study Guide: Exam 1Z0-829


    1. sbv239 Автор
      02.04.2022 20:17
      -11

      Если Вам это не нужно, просто пройдите мимо ????


      1. DistortNeo
        02.04.2022 20:37
        +3

        В тексте ещё и ошибки имеются. Например:

        Также пустой объект Optional можно создать с помощью статического метода of():

        Очевидно, что здесь создаётся НЕ пустой объект.


      1. sshikov
        02.04.2022 21:55
        +10

        А я вот не хочу, чтобы Хабр замусоривали подобным вторичным старьем. С чего это мимо?


      1. Maccimo
        02.04.2022 22:54
        +4

        Любой комментарий становится лучше, если в нём нет emoji.


  1. gotoxy
    02.04.2022 20:43
    -2

    В более свежих версиях Java догадались ли скопировать null-операторы из шарпа?


    1. Maccimo
      02.04.2022 22:55
      -1

      Зачем?
      Для любителей синтаксического мусора есть Kotlin.


  1. mbait
    03.04.2022 03:56
    +6

    Напишите, пожалуйста, про арифметические операции в Java, очень не хватает такого поста.


    1. n00ker
      03.04.2022 12:45

      И битовые сдвиги :)


      1. mbait
        03.04.2022 13:39
        +2

        Про битовые сдвиги придётся разбивать статью на две части.


    1. vics001
      04.04.2022 01:00

      А помню этот геморрой ...

      int a = 0;

      byte b += a; // OK
      byte b = ((byte) 1) + a; // COMPILE ERROR


  1. jh7
    03.04.2022 12:00
    +3

    Такие статьи были бы полезны на javarush, только там подписка платная. Здесь народ выбирает место релокации.