Статьи

Работа с Java 8 необязательно

В этом посте мы расскажем о работе с классом Optional, представленным в Java 8. Введение Optionalбыло новым только для Java. У Guava была версия Optional, а у Scala в течение некоторого времени был тип Option . Вот описание Optional из документации по API Java:

Контейнерный объект, который может содержать или не содержать ненулевое значение.

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

Работа с опциями

Использование Optionalкласса помогает в значительной степени предотвратить исключения NullPointerException. Это достигается за счет того, что разработчики могут более четко определить, где следует ожидать пропущенных значений. Но в какой-то момент нам нужно будет работать с этими потенциальными ценностями. OptionalКласс предоставляет два метода , которые соответствуют всем требованиям сразу: то Optional.isPresent()и Optional.get()метода. Но наша цель сегодня — работать с Optionalтипами и избегать (или, по крайней мере, откладывать до абсолютно необходимого) методов, которые напрямую запрашивают или получают доступ к потенциальному значению, содержащемуся в нем.

Условные операции

Java-разработчики привыкли писать что-то вроде следующего:

Добавление элемента в список или коллекцию

1
2
3
4
5
List<String> values = new ArrayList<>;
String someString = getValue();
if(someString != null){
    values.add(someString)
}

Теперь вместо этого мы можем использовать Optional.ifPresent(Consumer<? super T> consumer)метод. Если значение присутствует, предоставленный потребитель будет вызываться со значением, содержащимся в качестве параметра. Вот пример, представленный в модульном тесте:

Добавление необязательного значения в список

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void optional_is_present_add_to_list_without_get_test() {
    List<String> words = Lists.newArrayList();

    Optional<String> month = Optional.of("October");
    Optional<String> nothing = Optional.ofNullable(null);

    month.ifPresent(words::add);
    nothing.ifPresent(words::add);

    assertThat(words.size(), is(1));
    assertThat(words.get(0), is("October"));
}

Сверху, если значение присутствует, оно будет добавлено в список посредством List.add()вызова, в противном случае операция пропускается.

Условное изменение необязательных значений

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

Substring

1
2
3
4
5
String longString = getValue();
String smallerWord = null;
if(longString != null){
    smallerWord = longString.subString(0,4)
}

Для преобразования значения Optionalмы можем использовать метод карты . Вот пример, опять же в контексте модульного теста:

Пример подстроки Optional.map

1
2
3
4
5
6
7
8
9
10
11
@Test
public void optional_map_substring_test() {
    Optional<String> number = Optional.of("longword");
    Optional<String> noNumber = Optional.empty();

    Optional<String> smallerWord = number.map(s -> s.substring(0,4));
    Optional<String> nothing = noNumber.map(s -> s.substring(0,4));

    assertThat(smallerWord.get(), is("long"));
    assertThat(nothing.isPresent(), is(false));
}

Хотя это тривиальный пример, мы выполняем операцию отображения без явной проверки наличия значения или извлечения значения для применения данной функции. Мы просто предоставляем функцию и позволяем API обрабатывать детали. Также есть функция Optional.flatMap . Мы используем Optional.flatMapметод, когда у нас есть существующее Optionalи хотим применить функцию, которая также возвращает тип Optional. Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void optional_flat_map_test() {
    Function<String, Optional<String>> upperCaseOptionalString = s -> (s == null) ? Optional.empty() : Optional.of(s.toUpperCase());

    Optional<String> word = Optional.of("apple");

    Optional<Optional<String>> optionalOfOptional = word.map(upperCaseOptionalString);

    Optional<String> upperCasedOptional = word.flatMap(upperCaseOptionalString);

    assertThat(optionalOfOptional.get().get(), is("APPLE"));

    assertThat(upperCasedOptional.get(), is("APPLE"));

}

Используя этот flatMapметод, мы можем избежать неудобного типа возврата Option<Option<String>>, «сгладив» результаты в одном Optionalконтейнере.

Опция фильтрации

Также есть возможность фильтровать Optionalобъекты. Если значение присутствует и соответствует данному предикату, Optionalвозвращается с неизменным значением, в противном случае Optionalвозвращается пустое значение . Вот пример Optional.filterметода:

Пример дополнительного фильтра

1
2
3
4
5
6
7
8
9
@Test
public void optional_filter_test() {
    Optional<Integer> numberOptional = Optional.of(10);
    Optional<Integer> filteredOut = numberOptional.filter(n -> n > 100);
    Optional<Integer> notFiltered = numberOptional.filter(n -> n < 100);

    assertThat(filteredOut.isPresent(), is(false));
    assertThat(notFiltered.isPresent(), is(true));
}

Указание значений по умолчанию

В какой-то момент мы захотим восстановить значение, содержащееся в Optional, но если оно не найдено, вместо этого укажите значение по умолчанию. Но вместо того, чтобы прибегать к шаблону «если нет, то получить», мы можем указать значения по умолчанию. Есть два метода, которые позволяют установить значения по умолчанию Oprional.orElseи Optional.orElseGet. С помощью Optional.orElseнашей директивы мы предоставляем значение по умолчанию, а с помощью Optional.orElseGetмы предоставляем поставщика, который используется для предоставления значения по умолчанию.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void optional_or_else_and_or_else_get_test() {
    String defaultValue = "DEFAULT";

    Supplier<TestObject> testObjectSupplier = () -> {
        //Mimics set up needed to create object
        //Pretend these values need to be retrieved from some datastore
        String name = "name";
        String category = "justCreated";
        return new TestObject(name, category, new Date());

    };

    Optional<String> emptyOptional = Optional.empty();
    Optional<TestObject> emptyTestObject = Optional.empty();

    assertThat(emptyOptional.orElse(defaultValue), is(defaultValue));

    TestObject testObject = emptyTestObject.orElseGet(testObjectSupplier);

    assertNotNull(testObject);
    assertThat(testObject.category, is("justCreated"));
}

Когда нулевое / отсутствующее значение не ожидается

Для тех случаев, когда отсутствующее значение представляет собой состояние ошибки, существует Optional.orElseThrowметод:

Пример Optional.orElseThrow

1
2
3
4
5
6
@Test(expected = IllegalStateException.class)
public void optional_or_else_throw_test() {
    Optional<String> shouldNotBeEmpty = Optional.empty();

    shouldNotBeEmpty.orElseThrow(() -> new IllegalStateException("This should not happen!!!"));
}

Работа с коллекциями опций

И, наконец, давайте рассмотрим , как мы можем использовать эти методы в сочетании с Collections из Optionalэкземпляров. Было бы хорошо, если бы мы могли указать Collector, который просто возвращал найденные значения или значения плюс значения по умолчанию. Просто для интереса давайте определим одно:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 public static <T> Collector<Optional<T>, List<T>, List<T>> optionalToList() {
    return optionalValuesList((l, v) -> v.ifPresent(l::add));
 }


public static <T> Collector<Optional<T>, List<T>, List<T>> optionalToList(T defaultValue) {
   return optionalValuesList((l, v) -> l.add(v.orElse(defaultValue)));
}

private static <T> Collector<Optional<T>, List<T>, List<T>> optionalValuesList(BiConsumer<List<T>, Optional<T>> accumulator) {
  Supplier<List<T>> supplier = ArrayList::new;
  BinaryOperator<List<T>> combiner = (l1, l2) -> {
            l1.addAll(l2);
            return l1;
        };
   Function<List<T>, List<T>> finisher = l1 -> l1;
return Collector.of(supplier, accumulator, combiner, finisher);

}

Здесь мы определили , Collectorчто возвращает , Listсодержащие значения найдены в Collectionиз Optionalтипов. Существует также перегруженная версия, в которой мы можем указать значение по умолчанию. Давайте завершим это модульным тестом, показывающим новое Collectorв действии:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//some details left out for clarity

private List<String> listWithNulls = Arrays.asList("foo", null, "bar", "baz", null);
private List<Optional<String>> optionals;

@Before
public void setUp() {
  optionals = listWithNulls.stream().map(Optional::ofNullable).collect(Collectors.toList());
}

@Test
public void collect_optional_values_test(){
   List<String> upperCasedWords = optionals.stream().map(o -> o.map(String::toUpperCase)).collect(CustomCollectors.optionalToList());
   List<String> expectedWords = Arrays.asList("FOO","BAR","BAZ");

   assertThat(upperCasedWords,is(expectedWords));
}

@Test
public void collect_optional_with_default_values_test(){
  String defaultValue = "MISSING";

  List<String> upperCasedWords = optionals.stream().map(o -> o.map(String::toUpperCase)).collect(CustomCollectors.optionalToList(defaultValue));
  List<String> expectedWords = Arrays.asList("FOO", defaultValue, "BAR", "BAZ", defaultValue);

   assertThat(upperCasedWords,is(expectedWords));
}

В первом тесте Listвозвращается содержимое , содержащее только текущие значения, а во втором тесте — значения по умолчанию, заданные данным параметром.

Вывод

Мы рассмотрели, как использовать методы, найденные в Optionalклассе. Используя методы, продемонстрированные здесь, работа с отсутствующими данными обязательно станет проще и менее подвержена ошибкам.