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

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


  1. koldyr
    28.01.2018 00:11
    -1

    Это такое сложное объяснение монады Maybe?


    1. visirok Автор
      28.01.2018 00:39

      Нет. Это описание методов класса Optional из Java 8, что следует из названия статьи.


    1. aamonster
      28.01.2018 09:25

      Ну вот, вы сломали автора, подав на вход незнакомое слово.


      Тоже разочарован, увидев многабукфф вместо чего-нибудь полезного (ожидал рассказа об устройстве Optional, но не увидел даже сравнения эффективности Optional и Nullable).


      1. visirok Автор
        28.01.2018 12:48
        +1

        Да Вы задира, однако. Какое из 6 слов меня сломало, по Вашему мнению? (Шутка)
        Я планировал в последней статье серии порекомендовать читателям посмотреть исходый код класса Optional. Это того стоит. Кстати, он поразительно компактен. Однако, ни изучение его внутреннего строения, ни сравнение эффективности не входит в задачи этого Tutorial. В моём понимании Tutorial, как жанр технических публикаций, нацелен на обучение использовать тот или иной инструмент.


  1. Sap_ru
    28.01.2018 00:41

    Какая ужасная аналогия… Вроде и знаешь, а осознать в термина чистой/грязной воды, кранов и кружек невозможно абсолютно. Ибо, бессмысленно.


    1. lany
      29.01.2018 04:16

      Да, на мой взгляд проще понять ? Optional без всяких аналогий, чем вникнуть в эту уйму объектов и взаимодействий между ними…


      1. visirok Автор
        29.01.2018 08:46

        Допускаю, что для определённой (возможно, весьма большой) категории Java-разработчиков проще познакомиться с Optional на других примерах.
        Цель, которую я преследую в этой серии — показать использование Optional при работе с объектами, которые могут менять свою структуру. Для этого и были привлечены простые с точки зрения понимания физические модели.


  1. Sultansoy
    28.01.2018 00:50

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


  1. xdenser
    28.01.2018 03:03

    Ох что то мне пованивает ваша реализация класса Mixer.
    Зачем там этот result в виде поля?
    Зачем вообще метод mix? Чем MixedWater::new и Optional.map не устраивает?
    А чтобы продемонстрировать ifPresent?
    Так это плохой пример. Так делать не надо.
    ifPresent/isPresent нужны в момент, когда вы хотите перейти от упакованного значения к самому значению. Т.е. когда вы хотите его куда то передать, где не принимают Optional.
    А так вышло, что плохому учите.


    1. visirok Автор
      28.01.2018 12:39
      +1

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


      1. xdenser
        29.01.2018 15:50

        Причем тут физические модели? Я же не к физической модели претензии предъявляю. Можно было придумать уместный пример. Например, для вызова логгера ifPresent замечательно подходит.


        1. visirok Автор
          29.01.2018 16:16

          Моя цель состояла не столько в том, чтобы рассказать о методах класса Optional. На эту тему есть другие статьи, в том числе упомянутые мной в начале статьи. Я хотел показать, как методы этого класса можно применять к объектам, чья структура в каком-то смысле меняется. Поэтому и «материальные» примеры.
          Понятно, что это отнюдь не вся область применения Optional.
          Засучите рукава и как Moriline приведите свои примеры. Или по совету Danik-ik приведите ссылку. Это поможет коллегам по сайту.


          1. xdenser
            29.01.2018 16:30
            +1

            Да опять же претензия не к материальности. Туториал должен учить «хорошему». И прививать хороший стиль программирования. А по факту учит «плохому». Я вам привел правильный пример — вызов API, которое не знает об Optional. Аргументы из серии «сделай лучше» оставим за скобками.


  1. Danik-ik
    28.01.2018 09:47
    +4

    Дорогие недовольные! Большая просьба написать лучше или дать ссылку. При этом непременно именно "на то, с чем я работаю каждый день", ибо программисты наверняка все настолько одинаковые, что имеют рост 191 и размер обуви 46.
    Всё, перестал ёрничать.
    Может быть, примеры и в вакууме, но лично мне они показали удобство использования Optional вместо null, стоило только представить себе обратный случай. А обратный случай у меня в практике есть, пришлось даже писать собственный optional класс. А язык-то у меня без женериков, да…
    P.S. даже если Вы дадите ссылку на идеальную статью по теме, я её увижу в комментариях именно к ЭТОЙ статье. Как-то так...


    1. koldyr
      28.01.2018 10:07

      Бартоз Малевски «Теория категорий для программистов».


      1. Danik-ik
        28.01.2018 11:06
        +3

        Она раскрывает в простейших примерах прикладные аспекты использования Optional в Java в формате "прочитай, пока в маршрутке"?
        Нет, я не умаляю достоинств упомянутого материала, мне просто претит хайп о ненужности на Хабре "неидеальных" статей. В Интернете кто-то неправ, да.


        1. koldyr
          28.01.2018 11:33

          От уж мне эти «простейшие примеры». Первым комментарием я не в коей мере не хотел упрекнуть автора в ненужности статьи. Ч хотел обратить внимание на то, что описанная им концепция является частым случаем очень мощной и очень важной конструкции. И я не заметил в статье оператора join. Может плохо читал, джаву я почти не знаю.


          1. visirok Автор
            28.01.2018 12:21
            +1

            Эта серия статей из категории Tutorial. Её цель — показать, как можно использовать класс Optional в тех или иных случаях. Возник замысел из попыток автора рассказать коллегам в разных углах нашей планеты об этом.
            Я польщён, что Вы прочитали эту статью, не зная Java. Но всё же — целевая группа этой серии — практикующие Java — программисты.
            Эта стья не последняя из серии. О некоторых более общих аспектах, выступающих за рамки Tutorial, я планирую поговорить в последней статье этой серии. Но и там я не планирую писать о более общих концепциях. Это другая тема.


  1. Moriline
    28.01.2018 12:10
    +1

    Спасибо автору за подробную статью с примерами. Есть несколько замечаний:
    1. Введены лишние абстракции(ИМХО — класс CupOfBoiledWater). Как вариант, можно использовать этот код в примере для бойлера:

    final class CupOfWater {
    	private final boolean powerAvailable;
    	public CupOfWater(boolean powerAvailable) {
    		this.powerAvailable = powerAvailable;
    	}
    	/** Return cups of water.
    	 * @return Iterator<Water>
    	 */
    	public Iterator<Water> get(Integer numberOfCups) {
    		return (this.powerAvailable)? fill(numberOfCups, 90).iterator(): fill(numberOfCups, 10).iterator();
    	}
    	private List<Water> fill(Integer number, Integer temperature) {
    		List<Water>list = new ArrayList<Water>();
    		for(int i=0;i<number;i++) {
    			list.add(new Water(10L, temperature));
    		}
    		return list;
    	}
    }
    final class Water{
    	private final Long volume;
    	private final Integer temperature;
    	public Water(Long volume, Integer temperature) {
    		this.volume = volume;
    		this.temperature = temperature;
    	}
    	/** Return volume in millilitres of water.
    	 * @return Long 
    	 */
    	public Long volume() {
    		return this.volume;
    	}
    	/** Return temperature of water in Celsius scale.
    	 * @return Integer
    	 */
    	public Integer temperature() {
    		return this.temperature;
    	}
    	@Override
    	public String toString() {
    		return "Water [volume=" + volume + ", temperature=" + temperature + "]";
    	}
    }
    final class Boiler {
    	private final CupOfWater water;
    	public Boiler(CupOfWater water) {
    		this.water = water;
    	}
    	public Iterator<Water> getCupOfWater() {
    		return water.get(2);
    	}
    }
    public class Main {
    	public static void main(String[] args) {
    		boilerTest();
    		
    	}
    	public static void boilerTest() {
    		Iterator<Water>iterator = new Boiler(new CupOfWater(true)).getCupOfWater();
    		if(iterator.hasNext()) {
    			while (iterator.hasNext()) {
    				Water water = (Water) iterator.next();
    				System.out.println(water.toString());
    				if(water.temperature() > 10) {
    					// water is hot!
    				}else {
    					//water is cold!
    				}
    			}
    		}else {
    			System.out.println("water is not exists.");
    		}
    	}
    }

    2.
    Но в большинстве практически интересных задач на вход подаются не простые переменные, а объекты. В том числе такие, которые могут принимать значение null.
    Автор предполагает существование null в качестве параметра при вызове его методов. Я предлагаю его избегать/отказаться всеми способами и на уровне кода и на уровне архитектуры. Как это делать — другая тема.
    3.
    Если дождевая вода не собрана, то на входе мы имеем нулевой объект, иначе – нормальный объект.
    И вот тут абстракции начинают течь. Бак имеет ёмкость с какой-то шкалой или объемом. Объем не может отсутствовать вообще у данного объекта так как это один из главных его параметров! Он может быть равным 0 и уже тем более не отрицательным. Он также может быть 1,2,3,4 и так далее. Если автор имел ввиду не бак, а подачу воды в водопроводе — там та же картина — вода хоть там, хоть там имеет свой объем в каких-то единицах. И он может быть только от 0 и выше. Но трактовку понятия «отсутствия воды» каждый видит по своему и в этом кроется ошибка. Я предлагаю вариант как «объект с параметром 0».
    4.
    Если запрашиваемого ресурса на момент запроса нет, и мы не хотим возвращать null, нам остается одно средство – выбросить Exception.

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


    1. visirok Автор
      28.01.2018 12:34
      +1

      Спасибо и Вам за очередной расширенный комментарий. Мои ответы в порядке поступления.
      Про лишние абстракции. Я заметил, что совсем абстрактные примеры со списками из строк или чисел часто «не зацепляют» и быстро забываются. Поэтому я предпочитаю использовать «материальные» примеры. Но они опасны тем, что моделируемые обьекты реального мира имеют важные с физической точки зрения, но несущественные с дидактической точки зрения свойства. Например — количество воды в ёмкости. В примерах я пытался от этих второстепенных аспектов по возможности абстрагироваться.
      Мы часто не можем избежать возможный null в качестве параметра, если вынуждены использовать чужие API или работаем в многоязыковых средах.


    1. koldyr
      28.01.2018 15:17

      Очевидны естественные преобразования между Maybe a и List a. Если в списке не более одного элемента это преобразование будет ещё и изоморфизмом.


      1. visirok Автор
        28.01.2018 20:25
        +1

        Эта серия статей из категории Tutorial. Её цель — показать, как можно использовать класс Optional в тех или иных случаях. Ваш пример любопытен и познавателен. Однако, в этой статье речь не о Maybe, а об Optional.


        1. koldyr
          28.01.2018 20:30

          Вам предложили использовать списки, я написал, что в некотором смысле, это одно и тоже. И буду признателен, если вы мне объясните разницу между Optional<a*> и Maybe a. С категориальной точки зрения.


          1. visirok Автор
            28.01.2018 23:18

            Увольте. Дайте мне сначала мою задумку с Optional в Java 8, Java 9 до конца довести. Я не считаю себя специалистом в области теории функционального программирования.
            Может Вы сами или кто-то из читателей попробует?


          1. mayorovp
            29.01.2018 10:56

            Разница между ними в том, что Optional — это класс-контейнер в Java, а Maybe — функтор из теории категорий. Могли бы и сами догадаться...


            1. koldyr
              29.01.2018 11:57

              Судя по тому что написано в статье Optional реализует Maybe в джаве. А если что-то крякает как утка, ходит как утка. плавает как утка, летает как утка, то я называю это уткой.


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

                Вы не одиноки в таком мнении. Автор книги:
                Functional Programming in Java
                How functional techniques improve your Java programs
                Pierre-Yves Saumont
                Manning Pubn
                ISBN 9781617292736
                Говорит примерно тоже самое, правда не столь образно.(Шутка)


            1. visirok Автор
              29.01.2018 14:06
              -1

              Я противник сравнения вещей из разных миров. Да, класс в языке программирования и абстрактное понятие некой теории по определению разные. Как слон и автомобиль. Но интересно сравнивать африкансого слона с индийским, а BMW с Toyota. Либо мы должны договориться, какие сравнительные характеристики вещей нам интересны. Например у слона и автомобиля это могут быть вес и скорость.


              1. mayorovp
                29.01.2018 14:07

                Вы сейчас вообще о чем?


                1. visirok Автор
                  29.01.2018 16:02

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


                  1. mayorovp
                    29.01.2018 16:18

                    Почему вы рассказываете это именно мне?


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

                      Мне казалось, мы дискутируем это начиная с вот этого Вашего комментария. Так или иначе, позиции ясны, тема исчерпана, имхо.


                      1. mayorovp
                        29.01.2018 20:12

                        А мне казалось что я написал то же самое что пытаетесь сказать вы…


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

                          А может Вам и правда попробовать написать на тему соответствия реализации Optional положениям теории? Это было бы наверное многим интересно.


    1. Dzhyrma
      28.01.2018 18:09

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

      Аналогичная проблема в оригинальном решении. Чашка не кипятит воду которая внутри чашки, потому иметь функцию boil в CupOfWater классе довольно не логично.


      1. visirok Автор
        28.01.2018 20:20
        +1

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

        В модели чашки нет понятия энергии. Про энергию знает кипятильник.
        А с функцией boil Вы правы. С реальной физикой процесса кипения это связано мало. Это допущение сделано, чтобы не отвлекаться на детали моделирования.


    1. Danik-ik
      28.01.2018 18:20
      +1

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


      1. Moriline
        28.01.2018 23:03
        +1

        Мы либо получили порцию воды из приоритетного источника, либо нет.
        А в чём КОНКРЕТНО Вы эту воду получили? В каких единицах?
        Просто неправильно использовать аргументы из анекдота про блондинку, динозавров и теорию вероятностей. И не зря же есть возраст у каждого человека в каких-то единицах, а не просто — или человек есть или его нет? И температура на Земле тоже в каких-то единицах, а не просто — или есть или нет! А теперь главное. Если стоять на позиции что тут мы играем, тут не играем, а тут мы рыбу заворачиваем — то так мы из первобытно-общинного строя с применением принципа «лично мое мнение» в ИТ не выйдем очень долго, не говоря уже про науку и научный метод.


  1. visirok Автор
    28.01.2018 18:17
    +1

    Про соответствие предложенных моделей физической реальности я уже ответил здесь. Да, Вы правы. Разумеется воду кипятит прибор. Но для обьяснения действия Optional я решил сделать такое допущение.


  1. CyberSoft
    28.01.2018 21:40

    Я конечно вижу профит от Optional и использую его. Но всё же и теперь вы не смогли убедить меня.
    1. Примеры странные и неубедительные. Складывается ощущение, что от Optional всё стало громоздко.
    2. И всё-таки это since java 8


    1. visirok Автор
      28.01.2018 23:09

      Я специально выделил как отдельный параграф минимальное использование Optional. Это всего 5 методов. Остальные методы себя показывают, если Вы интенсивно будете использовать нововведения Java 8, в первую осередь streams.
      Некоторые вещи надо начать пробовать применять и тогда становится понятной их прелесть. В этом Вам могут помочь примеры из других параграфов статьи.


  1. Moriline
    29.01.2018 07:09

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

    public class WaterDispenser implements IWaterDispenser{
    	private final List<Water> water = new ArrayList<>();
    	@Override
    	public void setAvailability(Iterator<Water> input) {
    		water.clear();
    		while (input.hasNext()) {
    			water.add(input.next());
    		}
    	}
    	@Override
    	public Iterator<Water> getCupOfWater() {
    		return !this.water.isEmpty()?this.water.iterator():new CupOfWater(false).get(5);
    	}
    	public static void main(String[] args) {
    		IWaterDispenser waterDispenser = new WaterDispenser();
    		waterDispenser.setAvailability(Collections.EMPTY_LIST.iterator());
    		Iterator<Water>one = waterDispenser.getCupOfWater();
    		//expected 5 items of water
    		while (one.hasNext()) {
    			System.out.println(one.next());			
    		}
    		waterDispenser.setAvailability(Arrays.asList(new Water(20L, 10)).iterator());
    		Iterator<Water>two = waterDispenser.getCupOfWater();
    		//expected 1 item of water
    		while (two.hasNext()) {
    			System.out.println(two.next());			
    		}
    	}
    }
    interface IWaterDispenser{
    	void setAvailability(Iterator<Water> input);
    	Iterator<Water> getCupOfWater();
    }


    1. visirok Автор
      29.01.2018 08:33

      Признаться, меня Вы не убедили. К тому же, Ваша исходная физическая модель несколько шире моей.
      Однако, большое спасибо за пример. Заинтересованные читатели могут теперь сравнить два решения и сами решить, что использовать в подобных случаях — Optional или Iterator.