Статьи

Необязательный в шпаргалке Java 8

java.util.Optional<T> в Java 8 — плохой родственник scala.Option[T] и Data.Maybe в Haskell . Но это не значит, что это бесполезно. Если эта концепция является новой для вас, представьте себе Optional как контейнер, который может содержать или не содержать какое-либо значение. Так же, как все ссылки в Java могут указывать на некоторый объект или иметь null , Option может содержать некоторую (не нулевую!) Ссылку или быть пустым.

aridalsvannet

aridalsvannet

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

Ссылка: Необязательный в шпаргалке по Java 8 от нашего партнера JCG Томаша Нуркевича в блоге Java и соседей .