Хабрахабр

Объект в футляре или Optional в Java 8 и Java 9. Часть 3: «Что добавилось в Java 9»

Объект в футляре

Это третья статья серии, посвящённая использованию класса Optional при обработке объектов с динамической структурой. В первой статье было рассказано о способах избежания NullPointerException в ситуациях, когда вы не можете или не хотите использовать Optional.

Вторая статья посвящена описанию методов класса Optional в том виде, как он появился в Java 8.

Эта статья описывает методы класса, появившиеся в Java 9.

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

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

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

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

Показать больше

Похожие публикации

Кнопка «Наверх»