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 — с теоретической точки зрения как возможно, так и абстракции последовательности являются монадами , поэтому они имеют некоторые функциональные возможности