Java Optional — Отец холиваров

java8-optional
  1. Что такое Optional, почему он полезен? ->
  2. Как использовать Optional ->
  3. Использование, уместное и не очень ->
  4. Холивары ->
  5. Итоги ->

Что такое Optional, почему он полезен?

  • Optional<T> был введен в Java 8
  • Может находиться в 2ух состояниях
    • Содержит ссылку на T (также «present» или присутствует)
    • Пуст (также «absent» или отсутствует.  Не употребляйте «null»)
  • Реализации для примитивных типов
    • OptionalInt, OptionalLong, OptionalDoube
  • Optional — ссылочный тип, он может быть null (! Никогда так не делайте)

Правило #1: Никогда не используйте null как значение Optional или в качестве возвращаемого значения

// Поиск клиента в списке List<Customer> по заданному ид
// Early draft API: Stream.search(Predicate)
Customer customerByID(List<Customer> custList, int custID) { 
  return custList.stream()
                 .search(c -­‐> c.getID() == custID);
}   
// Что будет, если в List нет элемента с custID?
// Предположительно search() вернет null
// customerByID() вернет null

Если нам нужно получить не клиента, а его имя?

// Возвращаем имя клиента
Customer customerByID(List<Customer> custList, int custID) { 
  return custList.stream()
                 .search(c -­‐> c.getID() == custID)
                 .getName();  /// Получим NullPointerException если не найдем Customer
}
// Возвращаем имя клиента
Customer customerByID(List<Customer> custList, int custID) { 
  Customer cust =custList.stream()
                         .search(c -­‐> c.getID() == custID)
  return cust != null ? cust.getName() : "Не найдено"; // А вот это очень легко забыть!
}

Итак:

Optional — это ограничительный механизм для методов возвращаемые значения которых требуют отдавать некоторый «no result», представленный  null, который может вызвать дальнейшие ошибки

Как использовать Optional

Рассмотрим пример с использованием Optional

// Будем использовать настоящие методы Streams API: findFirst() и findAny()
String customerNameByID(List<Customer> custList, int custID) {
  return custList.stream()
                 .filter(c ‐> c.getID() == custID)
                 .findFirst()  // Вернет Optional
                 .getName();   // Так будет ошибка. Так как нужно как-то из Optional достать Customer
}
// Можно попробовать сделать так
opt.get().getName();  // Рискуем получить NoSuchElementException, если Optional пуст
// Хм, давайте просто добавим проверку!
opt.isPresent() ? opt.get().getName() : "Не найдено"; // Безопасно, но не сильно лучше чем проверка на null в лоб :)

Правило #2: Никогда не используйте Optional.get() без предварительной проверки Optional.isPresent()

Правило #3: Выбирайте отличные способы работы с Optional, чем проверка на isPresent + get()

Как же тогда быть?

// .orElse()
// orElse(default)
Optional<Data> opt = ...
Data data = opt.orElse(DEFAULT_DATA); // Если пуст, вернет указанное значение

// orElseGet(supplier)
Optional<Data> opt = ...
Data data = opt.orElseGet(Data::new); // Если пуст, вернет результат вызова метода new

// orElseThrow(exsupplier)
Optional<Data> opt = ...
Data data = opt.orElseThrow(IllegalStateException::new); // Если пуст, бросит ошибку

// .map()
opt.map(Customer::getName).orElse("Не найдено"); // map вызовет метод getName на объекте, если opt не пуст, иначе вернет пустой Optional
// не путать с isPresent()!
// Плохо с if
Optional<Task> oTask = getTask(...);
if (oTask.isPresent()) {
  executor.runTask(oTask.get());    
}   

// Уже лучше:
getTask(...).ifPresent(task ­‐> executor.runTask(task));

// Идеально!
getTask(...).ifPresent(executor::runTask);

Дополнительные методы

  • Статичные фабричные
    • Optional.empty() — создает пустой Optional
    • Optional.of(T) — создает Optional T (T не может быть null)
  • flatMap(Function<T, Optional<U>>)
    • Работает как map(), но использует для трансформации функцию, возвращающую Optional
  • Optional.equals() и hashCode()
// Конвертируем List<CustomerID> в List<Customer>, игнорируя неизвестные
// Пусть .findByID() возвращает Optional<Customer>
// Java 8
List<Customer> list = custIDlist.stream()
                                .map(Customer::findByID)
                                .filter(Optional::isPresent)
                                .map(Optional::get)
                                .collect(Collectors.toList());

// Java 9 Фича Optional.stream(), позволяет использовать в flatMap
List<Customer> list = custIDlist.stream()
                                .map(Customer::findByID)
                                .flatMap(Optional::stream)
                                .collect(Collectors.toList());

Адаптер null-optional

  • Есть nullable значение, нужен Optional
    • Optional<T> opt = Optional.ofNullable(ref);
  • Есть Optional, нужен nullable
    • opt.orElse(null); ! В других случаях избегайте этого

Использование, уместное и не очень.

Рассмотрим на примерах

// Плохая идея
String process(String s) {
    return Optional.ofNullable(s).orElseGet(this::getDefault);
}

// А вот так лучше
String process(String s) {
    return (s != null) ? s : getDefault();
}

Правило #4: В целом, создавать Optional только для того что бы получить из него значения — плохая идея. Лучше использовать тренарный оператор «?»

Optional<BigDecimal> first = getFirstValue();
Optional<BigDecimal> second = getSecondValue();

// Сложить first и second, рассматривая пустой как ZERO
// Вернуть Optional суммы
// Если оба пусты, вернуть пустой Optional

// Заумно
Optional<BigDecimal> result =
  Stream.of(first, second)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .reduce(BigDecimal::add);

// Ммм. Еще заумней... Попробуйте проверить корректно ли это?
Optional<BigDecimal> result =
  first.map(b ‐> second.map(b::add).orElse(b))
       .map(Optional::of)
       .orElse(second);

// Не самый короткий и искусный вариант, но самый понятный
Optional<BigDecimal> result;
if (!first.isPresent() && !second.isPresent()) {
  result = Optional.empty();
} else {
  result = Optional.of(first.orElse(ZERO).add(second.orElse(ZERO)));
}

Правило #5: Если есть вложенная цепочка Optional или промежуточный результат Optional<Optional<T>>, вероятно, это излишне

Этот проблемный метод get()

В интернете существует много обсуждений и много вопросов, связанных с этим методом. Основной момент — он ведет себя не так, как кажется на первый взгляд. Вызывая его, нужно осознавать, если Optional будет пуст, вы получите Exception! Общие рекомендации — не использовать его или делать крайне осторожно.

  • Метод get() —  «attractive nuisance» или «заманчивая неприятность»
    • не сильно полезен
    • легко забыть обезопасить вызов
    • легко сбивает с толку стиль isPresent() + get()
    • неправильно используется в большом количестве случаев => Не совсем удачное API
  • Планы по ликвидации проблемы. (Возможно будет уже в Java9?)
    • представить замену get()
    • @Deprecated для get()

Правило #6: Не используйте Optional в полях объекта, параметрах методов и коллекциях

  • Помните, Optional это box!
    • требует 16 байт
    • это отдельный объект (создает нагрузку на GC)
    • Одиночный Optional — не проблема, но если в вашей структуре данных их много, это может привести к проблемам с производительностью
  • Не нужно пытаться заменить каждый null на Optional
    • null — безопасен, если хорошо контролируется
    • null в private поле легко проверяется
    • null в параметрах — это не плохо (кончено, если код содержит предпроверки)

Холивары

  • Optional должен иметь возможность быть «present» со значением null!
  • Optional не защищает от всех NPE, поэтому он бесполезен
  • Optional должен быть serializable!
  • Optional должен быть частью языка, а не частью вспомогательной библиотеки!
  • Optional не должен быть final!
  • Optional должен расширять интерфейс Iterable, что позволит использовать его в циклах for!
  • У Optional должны быть дочерние классы Present и Empty!
  • Optional.ifPresent() должен возвращать «this» вместо void. Что бы использовать его в цепочках!
  • Нужно добавить в Java @Nullable / @NonNull вместо Optional!

Итоги

Новые методы Optional в Java9

  • Stream<T> Optional.stream()
  • void Optional.ifPresentOrElse(Consumer<T>, Runnable)
  • Optional<T> Optional.or(Supplier<Optional<T>>)

Правила

  1. Никогда не используйте null как значение Optional или в качестве возвращаемого значения
  2. Никогда не используйте Optional.get() без предварительной проверки Optional.isPresent()
  3. Выбирайте отличные способы работы с Optional, чем проверка на isPresent + get()
  4. В целом, создавать Optional только для того что бы получить из него значения — плохая идея. Лучше использовать тренарный оператор «?»
  5. Если есть вложенная цепочка Optional или промежуточный результат Optional<Optional<T>>, вероятно, это излишне
  6. Не используйте Optional в полях объекта, параметрах методов и коллекциях

Добавлю немного от себя:

Optional, наряду с stream API, и lambda — удобная фича языка, позволяющая улучшить читаемость кода, оптимизировать проверки, избежать не обрабатываемых exception. Главное правило — не забывать, что код нужно писать так, что бы коллега мог легко понять что тут происходит, и открыв его через пару месяцев после написания не пришлось заново разбирать всю логику :)

Код ради кода — не есть это хорошо

С большой силой приходит большая ответственность


Оригинал: Optional – The Mother of all Bikesheds


3 Комментария

Добавьте свой →

  1. Интересная на самом деле статья, т. к. не давно у нас на проекте появилассь поддержка java 8 и все принялись писать код изпользуя Optional, не изучив толком как работает данный класс.
    У меня есть пример где применял этот класс в рамках учебной практики по учебнику Head First https://github.com/turbomann/HF/blob/master/src/main/java/trx0eth7/chapter1/BeerSong.java

    • Да,он так и манит написать Optional.ofNullable(someVar).orElse(null) :)
      Еще не начали повально циклы на stream`ы переписывать?

      • Так и есть) И это приводит порой к нечитаемому виду. Если полезть то как реализованы stream`ы, то по сути это такой же код, который мы писали до.
        А вообще спасибо за статью.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *