Объект в футляре
Это третья статья серии, посвящённая использованию класса Optional при обработке объектов с динамической структурой. В первой статье было рассказано о способах избежания NullPointerException в ситуациях, когда вы не можете или не хотите использовать Optional.
Вторая статья посвящена описанию методов класса Optional в том виде, как он появился в Java 8.
Эта статья описывает методы класса, появившиеся в Java 9.
Четвертая статья посвящена необходимому (с точки зрения автора) дополнению к этому классу. Ну а пятая подведёт итоги.

Исходные тексты примеров для этой и остальных статей на эту тему вы найдете в проекте на GitHub.
В Java 9 в класс Optional добавлено три новых метода: stream(), ifPresentsOrElse() и or().
Начнем наше рассмотрение.

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


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

Представим себе такую ситуацию.

Программист Иван работает в проекте в режиме удаленного доступа (remote). Это дало ему возможность переехать жить в домик в деревне. В своем огороде он посадил несколько грядок овощей. (Если Вы, уважаемый читатель, практикующий дачник, будьте снисходительны к допущениям в моделировании предметной области).

Каждое утро Иван выходит в свой огород и собирает с кустов созревшие плоды. На каждом кусте плод за ночь может созреть, а может и не созреть. Поэтому мы можем смоделировать урожай каждого куста с помощью Optional. Для упрощения мы будем использовать в качестве 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(). Этот метод, согласно документации делает следующее: «Если объект присутствует, возвращает последовательный поток (stream), содержащий только этот объект, в противном случае возвращает пустой поток.»

В большинстве случаев мы используем метод stream() для обработки больших последовательностей данных. В случае с Optional мы столкнулись со 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?(...) и orElse?(...).

Посмотрим снова документацию на новый метод: «Если объект присутствует, выполняется заданное действие с ним, в противном случае выполняется действие с отсутствующим объектом.»
Другими словами, метод позволяет внутри себя обработать как заполненный так и пустой “футляр”.

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

Другими словами, с помощью 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(()->optBed2)
            .or(()->optBed3)
            .or(()->getDefault())
            .get();

    assertEquals(res, TOMATO);
}

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

Полный текст теста
public class VegetableGardenTest {

    private static final String CANNED_FOOD = "Canned food";
    private static final String MY_GARDEN = "My garden";
    public static final String TOMATO = "Tomato";
    public static final String CUCUMBER = "Cucumber";
    private  String[] EXPECTED_RESULT;
    private VegetableGarden tomatoGarden;
    private String veg;


    @Before
    public void setUp() throws Exception {
        EXPECTED_RESULT = new String[]{"A1", "A3", "A6"};
        tomatoGarden = new VegetableGarden("A1", null, "A3", null, null, "A6");
    }

    @Test
    public void testOptionalStreJava8() {

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

        assertArrayEquals(EXPECTED_RESULT, result);
    }

    @Test
    public void testOptionalStreamJava9()  {

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

        assertArrayEquals(EXPECTED_RESULT, result);
    }

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

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

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

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

        assertEquals(res1, TOMATO);
    }

    @Test
    public void testOptionalOr2()  {
        Optional<String> optBed1 = Optional.empty();
        Optional<String> optBed2 = Optional.empty();
        Optional<String> optBed3 =  Optional.empty();
        String res1 = optBed1
                .or(()->optBed2)
                .or(()->optBed3)
                .or(()->getDefault())
                .get();

        assertEquals(res1, CANNED_FOOD);
    }

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

    private Optional<? extends String> getDefault(){return Optional.of(CANNED_FOOD);};
}

Нововведения в классе Optional в Java 9 увеличили его мощь и привлекательность для повседневного использования в Java — проектах. И тем не менее, мои ожидания не сбылись.
Дело в том, что при обработке объектов с динамической структурой в случае, если объект не может быть создан или получен из ресурса, важно не только предохраниться от NullPointerException, но и узнать причину неудачи. А хорошего способа для решения подобной проблемы не предлагает и Java 9.

Поэтому я рискну предложить в следующей статье собственное решение этой важной практической задачи.
Иллюстрация: ThePixelman

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


  1. foal
    29.01.2018 12:46

    Ещё одно неочевидное использование метода stream() это более простое преобразование объектного Optional в примитивный, например:


    OptionalLong toLong(Optional<Long> o) {
        return  o.stream().mapToLong(Long::longValue).findAny();
    }


    1. visirok Автор
      29.01.2018 14:12

      Спасибо. Ценное дополнение к тесту статьи.


  1. koldyr
    29.01.2018 20:35

    С нетерпением жду Вашей реализации монады Error.


  1. visirok Автор
    29.01.2018 20:51

    Вынужден Вас огорчить. Класс будет называться Result. (Шутка)


    1. koldyr
      29.01.2018 21:15

      Давайте же взглянем и на эту утку.


  1. Guitariz
    29.01.2018 21:47

    По поводу последнего вашего замечания — не кажется ли вам, что это перебор?
    Сам класс опционал подразумевает, что он может содержать в себе пустые элементы. Тут возможны два варианта — либо опционалы вам не нужны и объекты должны жестко существовать, либо метод, возвращающий null, не должен быть вызван.
    В любом случае, отлов ошибок получения данных ужа на этапе использования — плохая затея, хотя бы потому, что защита от дурака не сработала ранее.
    Выходит, что опционал вам именно в этом случае должен быть не нужен, и даже противопоказан.


    1. visirok Автор
      29.01.2018 22:58

      Класс о котором я хочу рассказать в следующей статье возник при попытке найти решение реальной задачи. На абстрактном уровне постановка задачи звучит так: вы обращаетесь к некому сервису, который в случае успеха должен вернуть обьект. А причин для неудачи может быть несколько. И логика обработки ошибочной ситуации зависит от того, какова была причина неудачи.
      Если сервис возвращает Optional, о причине мы ничего не узнаем. Значит надо использовать что-то похожее на Optional, но содержащее информацию об ошибке в случае неуспеха.
      Optional в этом случае действительно недостаточен. О том и будет статья.


      1. Guitariz
        29.01.2018 23:28

        Жду. Вообще спасибо, не хватает хабру вот таких жизненных циклов, выстраданных определенной тематикой.


        1. visirok Автор
          29.01.2018 23:39

          Спасибо на добром слове. Потороплюсь.


      1. e_Hector
        30.01.2018 11:17

        1. visirok Автор
          30.01.2018 23:30

          Осень похоже. Я выбрал строгое закрепление ролей за первым и вторым типом. Первый отвечает за успех, второй за информацию о проблемах. Поэтому класс называется Result<SUCCESS, FAILURE> Но теоретически пользователь может их использовать с точностью до наоборот.