Сказка про Ивана-Программиста или Развитие Optional в Java 9

Image:Репка. https://botan.cc/

В серии статей:

Java 8 Optional и объекты с динамической структурой. Часть 1,

Java 8 Optional и объекты с динамической структурой. Часть 2,

Java 8 Optional и объекты с динамической структурой. Часть 3,

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

Закроем дыру в Java: Класс Result

я описал области применения и достоинства класса Optional появившегося в Java 8, а также некоторые недоработки в реализации этого класса.

В Java 9 некоторые недостатки были устранены путем расширения класса новыми методами. Именно их мы и рассмотрим в этой статье.

Исходные тексты этого всех моих статей на эту тему вы найдете в GitHub: https://github.com/vsirotin/Smartenesse-Java

Всего в класс Optional<T> в Java 9 добавлено три новых метода: stream(), ifPresentsOrElse() и or(). Начнем наше рассмотрение.

Метод stream(): берем все что можно

Этот метод полезен, если у вас имеется список List<Optional<T>>. Каждый элемент списка это чехол, или контейнер, который может содержать “настоящий” элемент либо быть пустым. Если вам необходимо наиболее простым способом получить из этого списка все “настоящие” элементы – вам поможет в этом метод stream().

Представим себе такую ситуацию. Программист Иван работает в проекте в режиме удаленного доступа (remote). Это дало ему возможность переехать жить в домик в деревне. В своем огороде он посадил несколько грядок овощей. Каждое утро Иван выходит в свой огород собирает с каждого куста созревшие плоды. На каждом кусте плод за ночь может созреть, а может и не созреть. Поэтому мы можем смоделировать урожай каждого куста с помощью Optional<T>. Для упрощения мы будем использовать в качестве T класс String.

Таким образом, урожай овощей, собираемый Иваном каждое утро мы можем смоделировать как List<Optional<String>> getTomatoBeds().

Предположим, мы хотим получить список плодов (разумеется созревших) в виде массива.

Без использования stream() нам пришлось бы для этой цели написать с помощью for … цикл, перебрать в нем все “чехлы”, записать “настоящие” элементы в список а оттуда переписать их в массив.

В Java 8 для массив созревших овощей можно получить так:

String[] result = tomatoGarden.getTomatoBeds()
        .stream()
        .filter(Optional::isPresent)
        .map(Optional::get)
        .toArray(String[]::new);

А в Java 9 это можно сделать ещё короче и элегантнее:

String[] result = tomatoGarden.getTomatoBeds()
        .stream()
        .flatMap(Optional::stream)
        .toArray(String[]::new);

Замена двух строчек на одну оказалась возможной благодаря вызову внутри flatMap нового метода stream(), который описан в документации Oralcle https://docs.oracle.com/javase/9/docs/api/java/util/Optional.html так:

Stream<T> stream​()
If a value is present, returns a sequential Stream containing only that value, otherwise returns an empty Stream.

В большинстве случаев мы используем метод stream() для обработки больших последовательностей данных. В случае с Optional<T> мы столкнулись со stream() для обработки последовательности максимум из одного элемента.

Для закрепления ещё один пример:

public void testOptionalStreamBase()  {
    Optional<String> opFilled = Optional.of("Filled");
    assertEquals(1, opFilled.stream().count());

    Optional<String> opEmpty  = Optional.empty();
    assertEquals(0, opEmpty.stream().count());
}

Metod ifPresentOrElse(): Если нет – добавим!

Эта новая функция закрыла дыру, оставшуюся в Java 8 и которую приходилось закрывать комбинацией вызовов методов ifPresent​(Consumer<? super T> action) и orElse​(T other).

Посмотрим снова документацию на новый метод:

void ifPresentOrElse​(Consumer<? superT> action, Runnable emptyAction)
If a value is present, performs the given action with the value, otherwise performs the given empty-based action.

Как видим, он позволяет внутри себя обработать и заполненный и пустой “чехол” (Optional<T>)

Итак продолжим историю с Иваном. Иван каждое утро выходит в свой огород, собирает созревшие овощи и складывает их в салат. Для моделирования этого факта мы будем использовать метод setValue(String s). А вот если на грядках ничего не выросло, ему приходится доставать консервированные овощи из банки. Для этого мы будем использовать метод setDefault().

Вот их реализация:

private void setValue(String s){
    veg = s;}
private void setDefault(){
    veg = CANNED_FOOD;}

Переменная класса veg это то, что окажется у Ивана в салатнице.

А теперь проверим, как ifPresentOrElse() работает с помощью теста:

@Test
public void testOptionalStreamIfPresentOrElse()  {
    Optional<String> optFilled = Optional.of(TOMATO);
    optFilled.ifPresentOrElse(this::setValue, this::setDefault);

    assertEquals(TOMATO, veg);

    Optional<String> optEmpty  = Optional.empty();
    optEmpty.ifPresentOrElse(this::setValue, this::setDefault);

    assertEquals(CANNED_FOOD, veg);
}

Как  мы видим, с помощью этой модели Иван хоть и не нашел на грядке свежего овоща, но заместил его овощем из консервной банки.

Метод or(): упорно ищем своё счастье!

Внимательный читатель наверное подметил, что в предыдущем примере Иван обследовал только первый куст. А как смоделировать ситуацию, если кустов много?

В этом случае нам поможет метод or(). Снова заглянем в документацию:

Optional<T> or​(Supplier<? extends Optional<? extends T>> supplier)
If a value is present, returns an Optional describing the value, otherwise returns an Optionalproduced by the supplying function.

Другими словами, с помощью or() можно строить цепочки обработки, которые будут анализировать “чехлы” (Optional<T>) до тех пор, пока не встретится первый непустой элемент.

В следующем примере optBed1, 2, 3 моделируют овощные грядки или отдельные кусты.

@Test
public void testOptionalOr1()  {
    Optional<String> optBed1 = Optional.empty();
    Optional<String> optBed2 = Optional.of(TOMATO);
    Optional<String> optBed3 = Optional.of(CUCUMBER);
    String res = optBed1
            .or(()->{return optBed2;})
            .or(()->optBed3)
            .or(this::getDefault)
            .get();

    assertEquals(res, TOMATO);
}

Поздравим Ивана, с помощью нового метода or() он в этот раз продвинулся дальше, нашел свежий помидор и ему не придется есть овощи из  консервной банки.