Как стать автором
Обновить

Комментарии 33

в мире микросервисов и распределенных систем, платформенная сериализация не является настолько важной, как раньше
— вот как раз для распределенных систем, где используется Java API (а не REST или SOAP), сериализация очень даже необходима. В JavaEE доступ через remote-интерфейс к методам возвращающим Optional даст ошибку.
Больше всего меня возмущает, что они разделили на 2 метода Optional.ofNullable() и Optional.of(). Кто то знает зачем? Достаточно было бы последнего, если бы он мог принимать null.
Мало того, что слово 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());

Немного ошибся в примере 2 в if конструкциях, но сути это не меняет.
Только если animal == null, if(animal instanceof Dog) не вылетит с исключением, а Optional.of(animal) да. Даже если вы используете Optional не для избавления от проверок на null, а для избавления от if, нет никакой необходимости выбрасывать NPE в Optional::of.
Разработчик должен использовать Optional::of в том месте, где animal по его мнению никогда не должен быть null. Т.е. если бы мы рассматривали данное место на Java 7, то это выглядело бы вот так:
Objects.requireNonNull(animal).
Т.е. я пытаюсь сказать, что если использовать Optional.ofNullable следующим образом:
Optional.ofNullable(animal).ifPresent(dao::saveToStorage)
, то в результате мы лишь получим неправильный дизайн, т.к. последствия такого использования могут вылиться в человеко-дни на поиск этого места в коде, когда окажется что все последующие вызовы отработали успешно, но почему то всё работает не так как ожидалось.
Как по мне, это нарушает принцип single responsibility, видеть такое в системном коде языка, странно, тем более, что его не дураки писали.
Прерывать исполнение кода, если прилетел не ожидаемый null должен requireNonNull, а Optional наоборот должен ожидать null и корректно отрабатывать без NPE. Кому надо пусть бы писали Optional.of(requireNonNull(animal)) или Optional.ofNonNullable(animal) это было бы читабельней и понятней.
Но теперь об этом поздно говорить, уже давно прокомпостировали и теперь оно с нами навсегда.
Самое забавное — это что от переписывания на Optional и stream-подобный синтаксис, количество строк кода не сильно изменилось. И если код с «if» мог читать любой разработчик средней квалификации, то новый — только тот, кто дополнительно держит в голове stream-api. Понятно применение streams и лямбд там, где потенциально можно ускорить обработку большого объема данных в многопоточной среде. В бизнес-логике оно зачем нужно? Создается впечатление, что новую фичу языка толкают не от большой нужды, а потому что модно…
Высоту кода не изменяет, но можно сократить лестницу из вложенных if

Вот вот, и вовсе не факт что это лучше читатаемо. Особенно если к стримам не привыкли.

Если правильно использовать стримы, то, по моему опыту, код проще читается, что для бизнес-логики очень важно. Много где нужно отфильтровать/преобразовать/сгруппировать какой-то поток данных, стримы позволяют написать понятную последовательную обработку. Функциональный стиль для этого часто лучше подходит, не зря практически во все ЯП стали его завозить.
По поводу держания в голове 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…
чет примеры не особо удачные потому короче не получилось, визуально лямбды выглядят приятнее, но полезнее optional не стал
Почитал issue на гитхабе ломбока — грандиозный тред получился.
«Не оборачивайте коллекции в Optional» — а разве его нельзя использовать в том случае, когда нужно разделить в ответе пустую коллекцию и ее отсутствие?

Необходимость заострять внимание на таком разделении скорее всего говорит о более глубоких архитектурных проблемах

Ну да. С коллекциями все просто. Есть данные — отправил коллекцию.
Нет данных — отправил тоже коллекцию, только пустую

Я знаю, что это спорная тема.


Это была спорная тема. Но все ответы по ней были еще в 2014, на момент выхода Java 8. Вы опоздали лет на 7. Более того, тут уже было чуть ли не 10 постов на эту же тему, начиная с 2014 и далее.

На мой взгляд, Kotlin решил эту проблему очень хорошо, наилучший синтаксис. С Optional постоянно эти if писать, вместо очевидного elvis operator: "?:"
Если вложенная структура, то foo?.bar?.baz()

Справедливости ради, if постоянно не надо писать. Можно: foo.map(f -> f.bar).map(b -> b.baz).

В стандартном Optional не хватает ещё состояния error. При добавлении этого состояния количество кода существенно сокращается из-за вынесения обработки ошибок в лямбды. Правда, теряется нативный контроль за типами ошибок.

Думаю, для таких случаев стоит использовать монаду Try

Проблема 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-аннотаций, на мой взгляд, есть несколько проблем:


  1. нельзя отличить неинициализированное по ошибке поле от необязательного без значения
  2. аннотации для локальных переменных выглядят сильно хуже, чем Optional
  3. для null нет никаких средств композиции, для Optional есть flatMap
  4. здесь был бы рад ошибиться, но вроде для 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.

В опросе не хватает пункта: Использую Kotlin/other language name
Не так давно в схожей теме был такой пункт

Здесь вопрос именно в том, как люди используют Optional в Java. В Kotlin null-safety реализована на уровне компилятора, поэтому эти споры там неактуальны

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории