java.util.Optional<T> в Java 8 — плохой родственник scala.Option[T] и Data.Maybe в Haskell . Но это не значит, что это бесполезно. Если эта концепция является новой для вас, представьте себе Optional как контейнер, который может содержать или не содержать какое-либо значение. Так же, как все ссылки в Java могут указывать на некоторый объект или иметь null , Option может содержать некоторую (не нулевую!) Ссылку или быть пустым.
Оказывается, что аналогия между Optional и обнуляемыми ссылками вполне разумна. Optional был введен в Java 8, поэтому очевидно, что он не используется во всей стандартной библиотеке Java — и никогда не будет по причинам обратной совместимости. Но я рекомендую вам хотя бы попробовать и использовать его, когда у вас есть недействительные ссылки. Optional вместо обычного null проверяется статически во время компиляции и является гораздо более информативным, поскольку он ясно указывает, что данная переменная может присутствовать или нет. Конечно, это требует некоторой дисциплины — вам никогда не следует присваивать значение null какой-либо переменной.
Использование опции (возможно) модель является довольно спорным , и я не собираюсь шагнуть в эту дискуссию. Вместо этого я представляю вам несколько вариантов использования null и как их можно переоборудовать в Optional<T> . В следующих примерах используются данные переменные и типы:
|
1
2
3
4
5
6
|
public void print(String s) { System.out.println(s);} String x = //...Optional<String> opt = //... |
x — это строка, которая может быть null , opt никогда не бывает null , но может содержать или не содержать некоторое значение ( настоящее или пустое ). Существует несколько способов создания Optional :
|
1
2
3
4
5
|
opt = Optional.of(notNull); opt = Optional.ofNullable(mayBeNull); opt = Optional.empty(); |
В первом случае Optional должен содержать не null значение и будет выдавать исключение, если передан null . ofNullable() либо вернет пустой, либо присутствует (установлено) Optional . empty( всегда возвращать empty Optional , соответствует null . Это одиночка, потому что Optional<T> неизменен.
ifPresent() — сделать что-нибудь, когда установлен Optional
Утомительное заявление:
|
1
2
3
|
if (x != null) { print(x);} |
можно заменить на функцию более высокого порядка ifPresent() :
|
1
2
|
opt.ifPresent(x -> print(x));opt.ifPresent(this::print); |
Последний синтаксис (ссылка на метод) может использоваться, когда лямбда-аргумент ( String x ) соответствует формальным параметрам функции.
filter() — отклонить (отфильтровать) определенные Optional значения.
Иногда вы хотите выполнить какое-то действие не только при установке ссылки, но и при выполнении определенного условия:
|
1
2
3
|
if (x != null && x.contains("ab")) { print(x);} |
Это может быть заменено на Optional.filter() который превращает настоящее (установленное) в Optional значение пустое. Optional если базовое значение не соответствует данному предикату. Если входное значение Optional было пустым, оно возвращается как есть:
|
1
2
3
|
opt. filter(x -> x.contains("ab")). ifPresent(this::print); |
Это эквивалентно более императиву:
|
1
2
3
|
if(opt.isPresent() && opt.get().contains("ab")) { print(opt.get());} |
map() — преобразовать значение, если оно присутствует
Очень часто вам нужно применить какое-либо преобразование к значению, но только если оно не равно null (исключая NullPointerException ):
|
1
2
3
4
5
6
|
if (x != null) { String t = x.trim(); if (t.length() > 1) { print(t); }} |
Это можно сделать гораздо более декларативным способом, используя map() :
|
1
2
3
4
|
opt. map(String::trim). filter(t -> t.length() > 1). ifPresent(this::print); |
Это становится сложно. Optional.map() применяет данную функцию к значению внутри Optional — но только если присутствует Optional . В противном случае ничего не происходит, и empty() возвращается. Помните, что преобразование является типобезопасным — посмотрите на дженерики здесь:
|
1
2
|
Optional<String> opt = //...Optional<Integer> len = opt.map(String::length); |
Если присутствует Optional<String> , также присутствует Optional<Integer> len , длина переноса String . Но если opt был пустым, map() поверх него ничего не делает, кроме изменения универсального типа.
orElse() / orElseGet() — становится пустым Optional<T> по умолчанию T
В какой-то момент вы можете развернуть Optional и получить реальную ценность внутри. Но вы не можете сделать это, если Optional пуст. Вот способ обработки такого сценария до Java 8:
|
1
|
int len = (x != null)? x.length() : -1; |
С помощью Optional мы можем сказать:
|
1
|
int len = opt.map(String::length).orElse(-1); |
Существует также версия, которая принимает Supplier<T> если вычисление значения по умолчанию является медленным, дорогим или имеет побочные эффекты:
|
1
2
3
|
int len = opt. map(String::length). orElseGet(() -> slowDefault()); //orElseGet(this::slowDefault) |
flatMap() — нам нужно идти глубже
Представьте, что у вас есть функция, которая не принимает значение null но может выдать ее:
|
1
|
public String findSimilar(@NotNull String s) //... |
Использовать его немного громоздко:
|
1
|
String similarOrNull = x != null? findSimilar(x) : null; |
С Optional это немного проще:
|
1
|
Optional<String> similar = opt.map(this::findSimilar); |
Если функция, которую мы map() возвращает null , результат map() является пустым Optional . В противном случае это результат указанной функции, обернутый (настоящей) Optional . Пока все хорошо, но почему мы возвращаем null значение, если у нас есть Optional ?
|
1
|
public Optional<String> tryFindSimilar(String s) //... |
Наши намерения понятны, но использование map() не дает правильного типа. Вместо этого мы должны использовать flatMap() :
|
1
2
|
Optional<Optional<String>> bad = opt.map(this::tryFindSimilar);Optional<String> similar = opt.flatMap(this::tryFindSimilar); |
Вы видите двойной Optional<Optional<...>> ? Определенно не то, что мы хотим. Если вы отображаете функцию, которая возвращает Optional , используйте вместо этого flatMap . Вот упрощенная реализация этой функции:
|
1
2
3
4
5
6
7
|
public <U> Optional<U> flatMap(Function<T, Optional<U>> mapper) { if (!isPresent()) return empty(); else { return mapper.apply(value); }} |
orElseThrow() — лениво выбрасывать исключения на пустую Optional
Часто мы хотели бы вызвать исключение, если значение не доступно:
|
1
2
3
4
5
6
|
public char firstChar(String s) { if (s != null && !s.isEmpty()) return s.charAt(0); else throw new IllegalArgumentException();} |
Весь этот метод может быть заменен следующей идиомой:
|
1
2
3
4
|
opt. filter(s -> !s.isEmpty()). map(s -> s.charAt(0)). orElseThrow(IllegalArgumentException::new); |
Мы не хотим создавать экземпляр исключения заранее, потому что создание исключения требует значительных затрат .
Большой пример
Представьте, что у нас есть Person с Address , у которого есть validFrom дата. Все они могут быть null . Мы хотели бы знать, установлен ли validFrom и в прошлом:
|
01
02
03
04
05
06
07
08
09
10
|
private boolean validAddress(NullPerson person) { if (person != null) { if (person.getAddress() != null) { final Instant validFrom = person.getAddress().getValidFrom(); return validFrom != null && validFrom.isBefore(now()); } else return false; } else return false;} |
Довольно некрасиво и оборонительно. В качестве альтернативы, но все еще некрасиво
|
1
2
3
4
|
return person != null && person.getAddress() != null && person.getAddress().getValidFrom() != null && person.getAddress().getValidFrom().isBefore(now()); |
Теперь представьте, что все они ( person , getAddress() , getValidFrom() ) являются Optional с соответствующими типами, четко указывая, что они могут быть не установлены:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
class Person { private final Optional<Address> address; public Optional<Address> getAddress() { return address; } //...} class Address { private final Optional<Instant> validFrom; public Optional<Instant> getValidFrom() { return validFrom; } //...} |
Внезапно вычисления становятся намного более удобными:
|
1
2
3
4
5
|
return person. flatMap(Person::getAddress). flatMap(Address::getValidFrom). filter(x -> x.before(now())). isPresent(); |
Это более читабельно? Трудно сказать. Но по крайней мере невозможно создать NullPointerException если Optional используется последовательно.
Преобразование Optional<T> в List<T>
Иногда мне нравится думать о Optional как о коллекции 1, имеющей 0 или 1 элемент. Это может облегчить понимание map() и flatMap() . К сожалению, в Optional нет toList() , но его легко реализовать:
|
1
2
3
4
5
|
public static <T> List<T> toList(Optional<T> option) { return option. map(Collections::singletonList). orElse(Collections.emptyList());} |
Или менее идиоматически:
|
1
2
3
4
5
6
|
public static <T> List<T> toList(Optional<T> option) { if (option.isPresent()) return Collections.singletonList(option.get()); else return Collections.emptyList();} |
Но зачем ограничиваться List<T> ? А как насчет Set<T> и других коллекций? Java 8 уже абстрагирует создание произвольной коллекции через Collectors API , представленный для Stream s . API отвратительный, но понятный:
|
1
2
3
4
5
|
public static <R, A, T> R collect(Optional<T> option, Collector<? super T, A, R> collector) { final A container = collector.supplier().get(); option.ifPresent(v -> collector.accumulator().accept(container, v)); return collector.finisher().apply(container);} |
Теперь мы можем сказать:
|
1
2
3
4
|
import static java.util.stream.Collectors.*; List<String> list = collect(opt, toList());Set<String> set = collect(opt, toSet()); |
Резюме
Optional<T> не так мощен, как Option[T] в Scala (но, по крайней мере, он не позволяет переносить null ). API не так прост, как обработка null и, вероятно, намного медленнее. Но преимущество проверки во время компиляции плюс удобочитаемость и ценность документации, которую использует Optional неизменно значительно превосходит недостатки. Также он, вероятно, заменит почти идентичный com.google.common.base.Optional<T> из Гуавы.
1 — с теоретической точки зрения как возможно, так и абстракции последовательности являются монадами , поэтому они имеют некоторые функциональные возможности
