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

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

Классу Optional посвящено немало статей и tutorials, в том числе этот и этот на Хабре. Большинство из них рассказывают как вызываются методы этого класса. Я в этом tutorial делаю упор на то зачем, почему, в каких случаях можно (а скорее даже нужно) применять тот или иной метод класса. Я думаю, это очень важно, ибо как показал опрос после первой статьи этого tutorial, далеко не все Java — программисты вошли во вкус использования всей мощи методов этого класса. Для лучшего объяснения методов класса я буду использовать более сложные и наглядные примеры, чем в большинстве других tutotials — кофеварку, фильтрационную установку, миксер и т.д. Это вторая статья серии, посвящённая использованию класса Optional при обработке объектов с динамической структурой. В первой статье было рассказано о способах избежания NullPointerException в ситуациях, когда вы не можете или не хотите использовать Optional. В этой статье мы рассмотрим все методы класса в том виде, как их предоставляет Java 8. Расширения класса в Java 9 рассмотрены в третьей статье этой серии. Четвертая статья посвящена необходимому (с точки зрения автора) дополнению к этому классу. Ну а в пятой статье я рассказываю о том, где внутри класса следует применять Optional, подвожу итоги и дарю каждому читателю, дочитавшему серию до конца, ценный подарок. В этом tutorial будет много исходного кода, в том числе и Junit тестов. Я разделяю мнение некоторых моих коллег по перу, что чтение тестового кода помогает лучшему усвоению материала. Все исходные тексты вы найдете в моём проекте на GitHub. Итак, в первой статье этой серии я пытался рассмотреть подходы, которые вы можете использовать при реализации объектов с динамической структурой и пообещал обосновать, почему почти всегда Optional в этой ситуации работает лучше других подходов. Приступим к исполнению обещаний. Начнём с определения.

Что такое Optonal?

Перед тем как мы перейдем к рассмотрению конкретных пример, попытаемся ответить на вопрос, а что такое Optional?

Я рискну дать собственное наглядное определение. В первом приближении Optional — это программный аналог футляра физического объекта, например – очков. Лежит ли объект внутри футляра, вы можете узнать с помощью метода isPresent(). Если он там лежит, вы можете взять его с помощью метода get(). В общем, примерно так, как это показано на заглавной картинке серии.

Использование по-минимуму

В нашем первом примере мы пытаемся с использованием Java 8 Optional симулировать функционирование прибора, соединяющего в себе кран питьевой воды и кипятильник. Его схема представлена на картинке внизу:

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

Таким образом вход этого прибора можно описать таким вот интерфейсом:

А выход вот таким:

Поскольку (в зависимости от входных данных) прибор может выдавать или не выдавать сырую и кипяченую воду, мы представляем результат вызова get… с помощью Optional.

Поведение прибора в целом описывает интерфейс, объединяющий методы входа и выхода.

Классы, представляющие разновидности воды для нас мало интересны, поэтому мы реализуем их минимальным образом. Вот это класс для порции сырой воды:

Мы будем считать, что порция кипяченной воды представляет собой новый, отличный от сырой воды объект. Поэтому мы представим его самостоятельным классом:

Итак, задача поставлена. В лучших традициях TDD (Test-Driven Development) пишем сначала тест для проверки, правильно ли мы симулировали поведение нашего простого прибора:

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

Перед тем, как перейти к реализации, остановитесь на минутку и продумайте в голове или даже за клавиатурой, как бы вы запрограммировали решение этой задачи в рамках подходов, рассмотренных в первой статье серии: С помощью пары has… get… C помощью возврата массива или листа значений С помощью признака активности выдаваемого наружу продукта. Если вы действительно попробовали себе это представить, а еще лучше -попытаться запрограммировать решение задачи в рамках этих подходов, вы несомненно оцените простоту и элегантность, которую привносят в нашу программистскую жизнь Java 8 Optional.

Посмотрите мое, наверняка не оптимальное решение:

Обратите внимание на последнюю строчку листинга, где используется метод map() из класса Optional. Таким образом вы можете строить цепочки обработки. Если на одном из звеньев цепочки выяснится, что дальнейшая обработка невозможна, вся цепочка вернет пустой ответ.

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

Рассмотрим, как можно применить Optional, если поведение определяется не булевыми переменными, а “старорежимными” объектами, допускающими нулевые значения. Пусть вход нашего кипятильника несколько иной модели, чем в первом примере определяется следующим образом:

Нулевое значение объекта water означает, что вода в прибор из водопровода не поступает. Тогда поведение прибора в целом задается следующим интерфейсом:

Как в предыдущем примере определяем тесты, проверяющие корректность нашей реализации:

Если мы сравним эти тесты с тестами для кипятильника первой модели, то увидим их очень большое сходство. Проверка результатов у одноименных тестов из разных наборов одинакова. Ну а вход отличается тем, что вместо значения true для источника воды мы подаем объект, а вместо false -null.

А вот и сама реализация:

Как мы видим, метод Optional.ofNullable() позволяет элегантно “положить” в футляр опасный объект с потенциально нулевым значением. Если объект имеет нулевое значение, футляр будет пустой. В противном случае в нем лежит необходимый нам объект.

Пора подвести первые итоги и сформулировать первые правила, для минимального использования Optional:

Если ваш метод выдает объект, который может присутствовать, а может отсутствовать, вы «укладываете» его в Optional. При укладке вы используете следующие правила: Условие Используемый метод класса Объект отсутствует Optional.empty() Объект присутствует и точно не null Optional.of(. ) Объект присутствует, но может быть null Optional.ofNullable(. ) Находится ли объект в футляре, вы определяете с помощью метода isPresent(). И если проверка дала положительный результат, вы извлекаете объект из футляра с помощью get()

Вот мы и освоили использование Optional так, чтобы больше не использовать null в качестве возвращаемого результата.

Но не будем останавливаться на достигнутом.

Рассмотрим теперь другую, не такую уж редкую ситуацию, когда некий ресурс представлен основным и резервным элементом.

Хорошо, когда есть заначка.

Заначка -это простонародное определение для резервного ресурса. Отвлечемся от эмоциональной стороны этого термина и рассмотрим техническую сторону вопроса.

В технических системах нередко ресурсы одного и того же вида могут быть доступны более чем одним способом.

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

Мы не будем усложнять задачу излишними деталями о неполной заполненности дождевого бака и его размере и просто используем снова знакомый уже класс CupOfWater.

Вход такого приспособления описывается таким образом:

Если дождевая вода не собрана, то на входе мы имеем нулевой объект, иначе – нормальный объект.

Выход прибора описывается следующим интерфейсом:

Отметим, что на выходе мы имеем объект CupOfWater а не Optional. Мы делаем так, чтобы яснее показать интересующий нас механизм. После того как вы, уважаемые читатели, его поймете, вы легко сможете перепрограммировать пример и получать на выходе Optional.

Поведение прибора в целом определяется совокупностью этих интерфейсов:

Как и в предыдущих примерах, подготовим вначале тесты для проверки поведения нашей реализации:

Наши ожидания таковы: прибор выдает воду независимо от того, заполнен бак с дождевой водой или нет, поскольку в последнем случае вода возьмется из “резерва” (водопровода).

Рассмотрим теперь реализацию:

Как мы видим, в сцепку к методу ofNullable() добавился метод orElse. Если первый элемент выдаст пустой Optional (дождевой воды не накоплено) второй метод добавит от себя объект. Если же первый метод выдаст непустой Optional, второй метод просто пропустит его через себя и водопроводная вода останется нетронутой.

Эта реализация предполагала наличие резервного объекта. Если же объект перед этим необходимо создать (в нашем случае – накачать воду) можно использовать метод orElseGet() с параметром типа Supplier:

Не выпускаем джина из бутылки

В некоторых случаях ограничения на ваш API не позволяет использовать Optional в качестве возвращаемого значения.

Предположим, что наш интерфейс определен таким образом, что клиент всегда ожидает на выходе нашей функции некоторый объект. Если запрашиваемого ресурса на момент запроса нет, и мы не хотим возвращать null, нам остается одно средство – выбросить Exception. Тем самым мы не выпускаем джина из бутылки – не даем возможности выпущенному нулевому объекту обернутся уже в коде клиента NullPoiner Exception.

Может ли нам помочь в этом случае Java 8 Optional? Да, может. Но перед тем, как рассмотреть решение, подготовим тест, проверяющий корректность его работы:

А вот и решение:

Думаю, многих читателей это решение не очень убедит. В самом деле, чем это лучше проверки на null с помощью if?

Основным аргументом в пользу этого решения является возможность строить таким образом цепочки функциональных вызовов. Однако цепочка может прерваться выбросом Exception. В четвертой статье этого цикла я рискну предложить своё решение проблемы обработки Exception в подобных цепочках функциональных вызовов.

Пришла пора сформулировать новую группу правил по использованию Optional для случая, когда у нас есть несколько альтернатив для создания динамического объекта:

Если у ваш есть две и больше альтернатив для создания динамического объекта, используйте следующие правила:: Условие Используемый метод класса Альтернативный объект присутствует orElse(. ) Альтернативный объект надо сначала создать (например, достать из repository) orElseGet(()->. ) Альтернативный ресурс иссяк (бросаем exception) orElseThrow(()->new IllegalStateException(. ))

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

Использование Optional в преобразователях

Преобразователь (Transformer) получает на вход некий объект и либо его изменяет либо преобразует в некий другой объект. В нашем случае, поскольку мы ограничиваемся использованием Optional, в качестве объекта на входе мы имеем всегда Optional. Напомним, что это можно себе представить как футляр или контейнер, в котором находится или не находится объект.

Преобразовать его можно либо в “настоящий” объект какого-либо типа, либо в новый футляр с каким-либо новым объектом.

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

T t = f1(Optional<T> opt)

U u = f2(Optional<T> opt)

Optional<U> = f3(Optional<T> opt)

Кандидаты на роли функций преобразований f1, f2 и f3 – методы из класса Optional представлены в этой таблице:

Кандидаты на роль f1 Кандидаты на роль f2 Кандидаты на роль f3 filter() map() flatMap() orElse() map() orElseGet()

В предыдущих постах этого цикла мы уже рассмотрели большинство из этих методов. Нерассмотренными остались только filter и flatMap.

Ниже мы рассмотрим примеры использования этих методов.

Фильтрование (использование метода filter)

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

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

Полностью коды этого примера вы можете найти в упомянутом в начале статьи проекте на GitHuB в package eu.sirotin.example.optional4

Вначале познакомимся с классом представляющим собранную дождевую воду:

Как вы можете видеть, с помощью метода isClean() можно узнать, является ли собранная вода чистой или нет.

Этот класс используется в качестве входного параметра в нашем приборе. Этот же объект но в “футляре” используется на выходе прибора.

А полностью поведение прибора описывается составным интерфейсом:

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

Ну а теперь приступим к рассмотрению реализации нашего класса с помощью Optional. Вот его полный текст:

В последней строчке показано использование метода filter(). В качестве критерия используется значение возвращаемое методом объекта isClean().

Обратите внимание также на использование методов ofNullable() и filter() в цепочке вызовов. Неправда ли, выглядит очень элегантно?

Трансформация – (использование метода flatMap)

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

Его максимально упрощенная схема показана внизу.

А поведение прибора описывается вот такой семантической таблицей:

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

Подготовим тест, проверяющий, реализует прибор ли ожидаемое от него поведение:

Ну а теперь рассмотрим и сам класс:

Использование метода flatMap() показано в последней строчке. В отличие от метода map() этот метод возвращает не сам объект а футляр (контейнер), который может быть и пустой.

Использование Optional в потребителях объектов (Consume)

В первом примере мы рассмотрели использование метода isPresent(), позволяющего определить, находится ли объект в футляре. В случае, если дальнейшая обработка предполагается только в случае его наличия, вместо isPresent(. ) целесообразнее использовать ifPresent(. )

Этот метод не возвращает какого-либо значения, но позволяет обработать объект в футляре, если он там присутствует. Если его там нет, ничего не происходит.

Рассмотрим его действие на примере еще одного прибора, который является усложнением предыдущего за счет дополнительной функции. Новый вариант прибора может не только очищать дождевую воду от загрязнений, но и смешивать её с некими добавками. Как и в предыдущих примерах, нас не будут интересовать детали об этих добавках.

Схема прибора показана на рисунке внизу:

Вначале определим новый класс, представляющий результат смешивания:

Выход прибора определяется вот этим интерфейсом:

В качестве входа мы используем интерфейс из предыдущего примера. Тогда полностью вход и выход прибора определяется таким совместным интерфейсом:

Поведение прибора сходно с поведением предыдущего прибора, только вместо очищенной дождевой воды мы получаем очищенную дождевую воду с желаемыми добавками.

Составим тест для проверки корректности поведения нашего прибора:

А вот и реализация основного класса:

Посмотрим внимательнее на использование метода ifPresent(). Как мы видим, в качестве входного параметра метода используется метод из нашего же класса mix(). Он в свою очередь ожидает в качестве входного параметра объект типа CupOfWater. Заметьте, что футляр с объектом именно этого типа возвращает метод getCleanedWater().

Сформулируем правила использования Optional в потребителях (клиентах).

Ну вот и все примеры, которые я хотел рассмотреть применительно к классу Optional в Java 8.

Но наш разговор об этом классе еще не закончен. В следующих статьях я расскажу о нововведениях в этом классе в Java 9, а также о некоторых его недостатках и ограничениях. Переход к третьей статье этой серии. Иллюстрация: ThePixelman

📎📎📎📎📎📎📎📎📎📎