Comments 33
в мире микросервисов и распределенных систем, платформенная сериализация не является настолько важной, как раньше— вот как раз для распределенных систем, где используется Java API (а не REST или SOAP), сериализация очень даже необходима. В JavaEE доступ через remote-интерфейс к методам возвращающим Optional даст ошибку.
Мало того, что слово Optional длинновато, то Optional.ofNullable очень сильно замусоривает код. При этом я ни разу не использовал Optional.of(), потому что надо делать проверку, что туда не передается null. А если ты гарантированно знаешь, что у тебя там не null, то зачем оборачивать в Optional.
Согласен. Я использую статик-импорты, чтобы сократить код. Но для чего это разделение, мне тоже не понятно
Потому что концепция Optional — это больше, чем просто проверки на null. Optional призван заменить большие или надоевшие конструкции из if(){}
Пример 1:
Без Optional:
Animal animal = ...;
if(animal instanceof Dog){
Dog dog = (Dog)animal;
this.processDog(dog);
}
С Optional:
Animal animal =…;
Optional.of(animal)
.filter(Dog.class::isInstance)
.map(Dog.class::cast)
.ifPresent(this::processDog);
Пример 2:
Без Optional:
Animal animal =…;
Address address = null;
if(animal.getAge() > 20) {
address = this.getAnimalHomeAddress(animal);
if(!this.isRussiaAddress(address)){
address = new Address();
}
}
С Optional:
Animal animal =…;
Address address = Optional.of(animal)
.filter(a -> a.getAge() > 20)
.map(this::getAnimalHomeAddress)
.filter(this::isRussiaAddress)
.orElseGet(() -> new Address());
Objects.requireNonNull(animal).
Т.е. я пытаюсь сказать, что если использовать Optional.ofNullable следующим образом:
Optional.ofNullable(animal).ifPresent(dao::saveToStorage)
, то в результате мы лишь получим неправильный дизайн, т.к. последствия такого использования могут вылиться в человеко-дни на поиск этого места в коде, когда окажется что все последующие вызовы отработали успешно, но почему то всё работает не так как ожидалось.
Прерывать исполнение кода, если прилетел не ожидаемый null должен requireNonNull, а Optional наоборот должен ожидать null и корректно отрабатывать без NPE. Кому надо пусть бы писали Optional.of(requireNonNull(animal)) или Optional.ofNonNullable(animal) это было бы читабельней и понятней.
Но теперь об этом поздно говорить, уже давно прокомпостировали и теперь оно с нами навсегда.
Вот вот, и вовсе не факт что это лучше читатаемо. Особенно если к стримам не привыкли.
По поводу держания в голове stream-api, java 8 вышла в 2014 и это api является частью языка, можно уж его изучить было за такое время.
При этом, синтаксис streams имеет два неоспоримых преимущества:
— Когда вы декларируете явным образом операцию (map/filter/reduce/etc) и преобразование (ссылкой на предопределенную лямбду), у компилятора и jit есть больше шансов догадаться поставить в этом месте intrinsics, а то и развернуть в векторные инструкии SSE/AVX. Сколько-нибудь сложный цикл с if такой оптимизации не подлежит.
— Когда применяемое преобразование становится first-class object — вы получаете возможность его передавать. И это прямой путь к переносу нагрузки на GPU/кластеры/whatever.
Если же вы не имеете такого объема данных, чтобы его надо было ускорять векторной обработкой или параллельными вычислениями — я не вижу смысла вешать еще один уровень абстракций между линейной бизнес-логикой и кодом. Как минимум, код без лямбд легче отлаживается. Кроме того, иногда хочется куда-то в середину воткнуть операцию, не относящуюся напрямую к потоку данных. Это может быть журналирование, это может быть обращение к Performance-counter, или периодический flush()/clear() сессии Hibernate, чтобы избежать катастрофического падения производительности и распухания внутреннего кэша ORM.
В целом, у нас в разработке есть консенсус-мнение, что streams и лямбды в java очень похожи на template-metaprogramming в современном C++: хорошо для специфических применений, но прежде чем пихать в свой код — нужно взвешивать риски и потенциальную выгоду. Your mileage may vary…
Я знаю, что это спорная тема.
Это была спорная тема. Но все ответы по ней были еще в 2014, на момент выхода Java 8. Вы опоздали лет на 7. Более того, тут уже было чуть ли не 10 постов на эту же тему, начиная с 2014 и далее.
На мой взгляд, Kotlin решил эту проблему очень хорошо, наилучший синтаксис. С Optional постоянно эти if писать, вместо очевидного elvis operator: "?:"
Если вложенная структура, то foo?.bar?.baz()
Проблема Optional в том, что он пришел из функциональных языков, где сплошь и рядом используется композиция, но он плохо ложится на императивное программирование. Особенно в случае, где необходимо не просто заменить empty дефолтным значением или трансформировать результат в одно действие, а осуществить полноценное ветвление:
Optional<Person> personOpt = findPerson();
if (personOpt.isPresent()) {
Person person = personOpt.get();
person.setXXX();
updatePerson(person);
} else {
createPerson();
}
Тут у нас появляется одна дополнительная переменная (personOpt) и одно дополнительное действие (personOpt.get()).
Поэтому программирая на Java, практически всегда лучше использовать слабые контракты типа @Nullable/@Nonnull
:
@Nullable Person findPerson() {...}
...
Person person = findPerson();
if (person == null) {
person.setXXX();
updatePerson(person);
} else {
createPerson();
}
Так проще и понятнее, а IDE здесь предупредит о возможном нуле, и этого как правило будет достаточно.
Другое преимущество nullable-аннотаций в том, что они не требуют сквозного перелопачивания всего API, как в случае с Optional. Умный компилятор (Kotlin) или IDE (Idea) умеют выводить nullability-контракт транзитивно из исходников, при этом не требуя какого-либо изменения в коде. Кроме того, если использовать только парадигму Optional, то должен быть некий и аналог @Nonnull
: что-то типа Required<T>
. Однако такой практики нигде не существует. В итоге nullable-аннотациями можно указывать более сильные и гибкие контракты, нежели при помощи Optional, и делать это в более простой форме.
Ну и вообще, как вы подметили в статье, исторически Java экосистема не приспособлена для Optional. Его использование оправдано только как отсутствие результата вычисления функции. Использовать его как контейнер для данных крайне нелепо.
Например, JPA Specification возвращает Optional вместо null.
Я что-то пропустил??? Или вы не различаете Spring Data и JPA?
Любая коллекция является контейнером сама по себе. Для того чтобы показать отсутствие элементов в ней, не требуется использовать дополнительные обертки.
Интересно, сколько времени потребуется еще, чтобы пользователи осознали то же самое про String и перестали делать различие между пустой строкой и нулом.
По поводу первого примера: начиная с Java 9 есть удобный метод ifPresentOrElse
.
Optional<Person> personOpt = findPerson();
personOpt.ifPresentOrElse(
(person) -> {
person.setXXX();
updatePerson(person);
},
() -> {
createPerson();
}
)
По поводу Nullable и NotNull. Optional вынуждает проверять наличие или отсутствие значения, то есть вероятность NPE здесь почти равна нулю (кроме тех случаев, когда вместо Optional вернули null). Если же возвращается просто какой-то тип, то не всегда понятно, может ли быть здесь null, или нет. Можно элементарно забыть написать проверку, компилятор ведь не ругнется.
Required, я считаю, не нужен, так в случае, если мы используем Optional, все является обязательным, кроме того, что в него обернуто. Если бы был еще и Required, то как когда интерпретировать типы данных без оберток?
С JPA в статье действительно ошибка. Я различаю Spring Data и JPA и имел в виду именно первое. Но, видимо, писал об одном, а думал о другом. Спасибо за замечание. Неточность поправлю.
По поводу первого примера: начиная с Java 9 есть удобный метод ifPresentOrElse.
Это костыль, и хуже на порядок, чем обычное ветвление. Кроме того возникнут проблемы, чтобы вернуть что-то из лямбды:
Person person = findPerson();
if (person == null) {
person = new Persion();
createPerson(person);
} else {
person.setXXX();
updatePerson(person);
}
doSomethingWith(person);
Если же возвращается просто какой-то тип, то не всегда понятно, может ли быть здесь null, или нет.
В том-то и дело, что в общем случае у нас есть 3 типа контрактов:
- Nullable
- Гарантированно не null
- Х.з. (на усмотрение юзера)
Вы можете лично у себя в коде использовать какое-то соглашение, избавившись от последнего варианта (напр. везде вместо null возвращаем Optional), но при интеграции со сторонними API и с той же java rtl возникнут проблемы — придется для всего писать обертки Optional.ofNullable(), либо ставить проверки Objects.requireNonNull(). И в этом случае вы также можете забыть сделать соответствующее преобразование.
Кроме того, очень часто nullability бывает контекстно-зависимым, и это нельзя выразить никаким статичным контрактом. Пример из JPA:
@Entity
public class MyJPAEntity {
@Id @GeneratedValue
private Long id;
}
Поле id может быть null только при создании объекта, пока он еще не persistent. В 99.9% случаев объект будет вытащен из базы, и поле всегда будет иметь значение. Возвращать для этого поля Optional, отдавая себе отчет, что оно тут ну точно не может быть null, но каждый раз делать лишние приседания по преобразованию, будет сильно отдавать шизофренией и загрязнять код.
И вот к чему приводит подобная практика:
public static Optional<String> toString(Object obj) {
return Optional.ofNullable(obj).map(Object::toString);
}
...
System.out.println(toString(1).orElseThrow(() -> new RuntimeException("WTF!")))
System.out.println(toString("Privet").orElseThrow(() -> new RuntimeException("WTF!")))
System.out.println(toString(new Persion()).orElseThrow(() -> new RuntimeException("WTF!")))
Это вообще касается всех контрактов (checked exceptions, class cast, etc...) — большинство из них невычислимо в статике. Для этого нужно не бояться ставить проверки и писать тесты вместо параноидального желания все прибить гвоздями.
Если интересно, посмотрите давнее видео Андрея Бреслава, как они в Котлине тщетно пытались жестко прикрутить nullable-контракты и какие проблемы возникли с интеграцией с кодом на джаве. В итоге пришлось внутренне на уровне компилятора ввести третий тип с неопределенной nullability (String!!), который приводится к одному из первых двух при первой соответствующей декларации.
Начиная с Java 9 ваш пример можно записать чуть более приятным образом с помощью ifPresentOrElse
(и тут меня опередил комментатор выше). Конечно, для тех, кто привык к императивному коду, это будет менее читаемо, чем явный if
. И в целом соглашусь, что Optional
в императивном коде выглядит чужеродно.
С использованием nullable-аннотаций, на мой взгляд, есть несколько проблем:
- нельзя отличить неинициализированное по ошибке поле от необязательного без значения
- аннотации для локальных переменных выглядят сильно хуже, чем
Optional
- для
null
нет никаких средств композиции, дляOptional
естьflatMap
- здесь был бы рад ошибиться, но вроде для Java нет какого-то стандартного статического анализатора nullable-аннотаций, чтобы можно было запускать на CI, например
нельзя отличить неинициализированное по ошибке поле от необязательного без значения
Не понял. Вы собираетесь использовать поле, которое возвращает Optional и null одновременно? Это одна из худших идей. Формально ни одно значение типа Optional никогда не должно быть null, хотя фактически Java запросто допускает такое — забыли проинициализировать и все (в отличие например от Option в Scala).
аннотации для локальных переменных выглядят сильно хуже, чем Optional
Они там и не нужны — IDE автоматически выводит nullability из контрактов над методами или самого кода.
здесь был бы рад ошибиться, но вроде для Java нет какого-то стандартного статического анализатора nullable-аннотаций, чтобы можно было запускать на CI, например
Стандартного нет, но любой нестандартный справится на ура. Findbugs, Spotbugs, PMD, Sonarqube. etc… Все работают с nullable аннотациями. Единственная проблема — сами nullable аннотации никем не стандартизированы, и сейчас есть разные библиотеки.
Не понял.
Поясню на примере:
class B {
@Nullable
public A field_a_a;
@Nullable
public A fielda_a_;
@Nullable
public A fielda_aa;
@Nullable
public A fieldaa_a;
}
class MyAbstractBlaBlaBlaBuilder {
public B build() {
var b = new B();
b.field_a_a = field_a_a;
b.fielda_a_ = fielda_a_;
b.fieldaa_a = fielda_aa;
b.fieldaa_a = fieldaa_a;
return b;
}
}
Если бы вместо @Nullable
был Optional
— вы бы на первом запуске отловили NPE. С @Nullable
такая фигня может оставаться незамеченной довольно долго.
в отличие например от Option в Scala
В Scala с этим так же, как и в Java, разве что с линтерами получше.
Они там и не нужны — IDE автоматически выводит nullability из контрактов над методами или самого кода.
Нет никакой гарантии, что человек, который писал код использует такую IDE / настроил эти варнинги / обратил на них внимание. Еще меньше вероятность, что это сделает другой человек, который будет мержить это в мастер, ведь он скорее всего даже не откроет этот код в IDE.
Поясню на примере
Все-равно не доходит как Optional спасет от неправильного меппинга. Все поля @Nullable
, контракт соблюден, а проверка меппинга — это уже дело теста. Гораздо хуже, когда у вас поле Optional, и вы его забыли проинициализировать:
public class B {
public Optional<String> fieldA;
}
...
B b = ...;
System.out.println(b.fieldA.orElse("").length()); // NPE-нежданчик
И в Java от этого никак не уберечься. Поэтому все категорически не рекомендуют использовать optional-поля.
С тестами-то понятно, можно любой из этих вариантов забороть. Но далеко не все проекты могуть похвастаться хорошим покрытием. Поэтому я здесь рассматриваю, что мог быть дать Optional
сам по себе.
И да, как я уже указал, Optional
вам при первом же чтении даст корректный NPE в рантайме. Потому что контракт, что все поля инициализированные, а необязательные — empty. Если мы ожидаем, что поле инициализировано, а оно оказалось null
, NPE — это ровно то, что и должно произойти.
С @Nullable
вы давите NPE (что вообще говоря не самоцель), но лишаетесь возможности различить ситуацию неинициализированно по ошибке / необязательно без значения, потому что и то и другое в этом случае — null
.
Не так давно в схожей теме был такой пункт
Java Optional не такой уж очевидный