В этом посте мы расскажем о работе с классом 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
классе. Используя методы, продемонстрированные здесь, работа с отсутствующими данными обязательно станет проще и менее подвержена ошибкам.