Pull to refresh

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

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

Что такое Optonal?


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

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

Итак:
В первом приближении Optional — это футляр для некоторого объекта.

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


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



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

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

public interface IBoilerInput {   
   void setAvailability(boolean waterAvailable, boolean powerAvailable);
}

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

public interface IBoilerOutput {   
   Optional<CupOfWater> getCupOfWater();   
   Optional<CupOfBoiledWater> getCupOfBoiledWater();
}

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

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

public interface IBoiler extends IBoilerInput, IBoilerOutput {}

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

public class CupOfWater {
   public CupOfBoiledWater boil() {return new CupOfBoiledWater();}
}

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

public class CupOfBoiledWater {}

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

JUnit test Boiler1Test
public class Boiler1Test {
   
   private IBoiler boiler;
   
   @Before
   public void setUp() throws Exception {
      boiler = new Boiler1();
   }

   @Test
   public void testBothNotAvailable() {            
      boiler.setAvailability(false, false);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   
   @Test
   public void testPowerAvailable() {                
      boiler.setAvailability(false, true);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testWaterAvailable() {    
      boiler.setAvailability(true, false);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testBothAvailable() {     
      boiler.setAvailability(true, true);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertTrue(boiler.getCupOfBoiledWater().isPresent());
   }
}

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

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

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

public class Boiler1 implements IBoiler {
   
   private boolean waterAvailable;
   private boolean powerAvailable;
   
   @Override
   public void setAvailability(boolean waterAvailable, boolean powerAvailable) {
      this.waterAvailable = waterAvailable;
      this.powerAvailable = powerAvailable;
   }

   @Override
   public Optional<CupOfWater> getCupOfWater() {
      return waterAvailable ? Optional.of(new CupOfWater()) : Optional.empty();
   }

   @Override
   public Optional<CupOfBoiledWater> getCupOfBoiledWater() {
      if(!powerAvailable)return Optional.empty();
      return getCupOfWater().map(cupOfWater->cupOfWater.boil());
   }
}

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

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

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

public interface IBoilerInput2 {
   
   void setAvailability(@Nullable CupOfWater water, boolean powerAvailable);
}

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

public interface IBoiler2 extends IBoilerInput2, IBoilerOutput {}

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

JUnit test Boiler2Test
public class Boiler2Test {
   
   private IBoiler2 boiler;
   
   @Before
   public void setUp() throws Exception {
      boiler = new Boiler2();
   }

   @Test
   public void testBothNotAvailable() {            
      boiler.setAvailability(null, false);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   
   @Test
   public void testPowerAvailable() {                
      boiler.setAvailability(null, true);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testWaterAvailable() {    
      boiler.setAvailability(new CupOfWater(), false);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testBothAvailable() {     
      boiler.setAvailability(new CupOfWater(), true);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertTrue(boiler.getCupOfBoiledWater().isPresent());
   }
}

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

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

public class Boiler2 implements IBoiler2 {
   
   @Nullable
   private CupOfWater water;
   private boolean powerAvailable;
   

   @Override
   public void setAvailability(@Nullable CupOfWater water, boolean powerAvailable) {
      this.water = water;
      this.powerAvailable = powerAvailable;
   }

   @Override
   public Optional<CupOfWater> getCupOfWater() {
      return Optional.ofNullable(water);
   }

   @Override
   public Optional<CupOfBoiledWater> getCupOfBoiledWater() {
      if(!powerAvailable)return Optional.empty();
      return getCupOfWater().map(cupOfWater->cupOfWater.boil());
   }
}

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

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

Находится ли объект в футляре, вы определяете с помощью метода isPresent(). И если проверка дала положительный результат, вы извлекаете объект из футляра с помощью get()

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

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

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

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


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

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

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

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

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

public interface IWaterDispenserInput {   
   void setAvailability(@Nullable CupOfWater firstPortion);
}

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

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

public interface IWaterDispenserOutput {   
   CupOfWater getCupOfWater();
}

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

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

public interface IWaterDispenser extends IWaterDispenserInput, IWaterDispenserOutput {}

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

JUnit test WaterDispenser1Test

public class WaterDispenser1Test {
    private IWaterDispenser waterDispenser;

    @Before
    public void setUp() throws Exception {
        waterDispenser = new WaterDispenser1();
    }

    @Test
    public void testMainAvailable() {
        waterDispenser.setAvailability(new CupOfWater());
        assertNotNull(waterDispenser.getCupOfWater());
    }


    @Test
    public void testMainNotAvailable() {
        waterDispenser.setAvailability(null);
        assertNotNull(waterDispenser.getCupOfWater());
    }
}

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

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

public class WaterDispenser1  implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElse(new CupOfWater());
    }
}

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

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


public class WaterDispenser2 implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElseGet(()->new CupOfWater());
    }
}

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


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

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

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


@Test  (expected = IllegalStateException.class)
public void testMainNotAvailable() {
    waterDispenser.setAvailability(null);
    waterDispenser.getCupOfWater();
    fail("This code line must be not reached");
}

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


public class WaterDispenser3 implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElseThrow(()->new IllegalStateException("Resource not available"));
    }
}

Думаю, многих читателей это решение не очень убедит. В самом деле, чем это лучше проверки на 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

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

public class RainWater {

   private final boolean clean;

   public RainWater(boolean clean) {
      this.clean = clean;
   }

   public boolean isClean() {
      return clean;
   }
}

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

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

public interface IRainWaterDispenserInput { void setAvailability(@Nullable RainWater rainWater); }

public interface IRainWaterDispenserOutput {   
   Optional<RainWater> getRainWater();
}

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

public interface IRainWaterDispenser extends IRainWaterDispenserInput, IRainWaterDispenserOutput {}

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

JUnit test RainWaterDispenser1Test
public class RainWaterDispenser1Test {
    private IRainWaterDispenser rainWaterDispenser;

    @Before
    public void setUp() throws Exception {
        rainWaterDispenser = new RainWaterDispenser1();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        rainWaterDispenser.setAvailability(new RainWater(true));
        assertTrue(rainWaterDispenser.getRainWater().isPresent());
        assertTrue(rainWaterDispenser.getRainWater().get().isClean());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        rainWaterDispenser.setAvailability(null);
        assertFalse(rainWaterDispenser.getRainWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        rainWaterDispenser.setAvailability(new RainWater(false));
        assertFalse(rainWaterDispenser.getRainWater().isPresent());
    }
}

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

public class RainWaterDispenser implements IRainWaterDispenser{
    @Nullable private RainWater rainWater;

    @Override
    public void setAvailability(@Nullable RainWater rainWater) {
        this.rainWater = rainWater;
    }

    @Override
    public Optional<RainWater> getRainWater() {
        return Optional.ofNullable(rainWater).filter(RainWater::isClean);
    } 
}

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

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

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


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

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



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



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

public interface IRainWaterCleanerInput {
   
   void setAvailability(@Nullable RainWater rainWater);
}

public interface IRainWaterCleanerOutput {
   
   Optional<CupOfWater> getCleanedWater();
}

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

JUnit test RainWaterCleanerTest

public class RainWaterCleanerTest {
    private IRainWaterCleaner rainWaterDispenser;

    @Before
    public void setUp() throws Exception {
        rainWaterDispenser = new RainWaterCleaner();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        rainWaterDispenser.setAvailability(new RainWater(true));
        assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        rainWaterDispenser.setAvailability(null);
        assertFalse(rainWaterDispenser.getCleanedWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        rainWaterDispenser.setAvailability(new RainWater(false));
        assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
    }
}

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

public class RainWaterCleaner implements IRainWaterCleaner {
    @Nullable private RainWater rainWater;

    @Override
    public void setAvailability(@Nullable RainWater rainWater) {
        this.rainWater = rainWater;
    }

    @Override
    public Optional<CupOfWater> getCleanedWater() {
        return Optional.ofNullable(rainWater).flatMap(w->Optional.of(new CupOfWater()));
    }    
}

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

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


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

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

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

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



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

public class MixedWater extends CupOfWater {
   public MixedWater(CupOfWater water) {}
}

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

public interface IMixerOutput extends IRainWaterCleanerOutput {   
   Optional<MixedWater> getMixedWater();
}

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

public interface IMixer extends IRainWaterCleanerInput, IMixerOutput {}

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

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

JUnit test MixerTest

public class MixerTest {
    private IMixer mixer;

    @Before
    public void setUp() throws Exception {
        mixer = new Mixer();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        mixer.setAvailability(new RainWater(true));
        assertTrue(mixer.getMixedWater().isPresent());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        mixer.setAvailability(null);
        assertFalse(mixer.getMixedWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        mixer.setAvailability(new RainWater(false));
        assertTrue(mixer.getMixedWater().isPresent());
    }
}

А вот и реализация основного класса:
public class Mixer extends RainWaterCleaner implements IMixer{
   
   private MixedWater result = null;

   @Override
   public Optional<MixedWater> getMixedWater() {
      super.getCleanedWater().ifPresent(this::mix);
      return Optional.ofNullable(result);

   }
   
   private void mix(CupOfWater water) {
      result = new MixedWater(water);
   }
}

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

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

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

Но наш разговор об этом классе еще не закончен. В следующих статьях я расскажу о нововведениях в этом классе в Java 9, а также о некоторых его недостатках и ограничениях.
Переход к третьей статье этой серии.
Иллюстрация: ThePixelman
Tags:
Hubs:
Total votes 25: ↑18 and ↓7+11
Comments43

Articles