[Программирование, Java, ООП, Функциональное программирование] Java Optional не такой уж очевидный (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
NullPointerException - одна из самых раздражающих вещей в Java мире, которую был призван решить Optional. Нельзя сказать, что проблема полностью ушла, но мы сделали большие шаги. Множество популярных библиотек и фреймворков внедрили Optional в свою экосистему. Например, JPA Specification возвращает Optional вместо null.Я думаю, что некоторые программисты настолько впечатлились теми возможностями, которые предоставляет Optional, что они стали использовать его даже там, где это не нужно, зачастую применяя неправильные паттерны. В этой статье я привел некоторые ошибочные использования этой монады и предложил свой путь для решения возникших проблем.
Optional не должен равняться nullМне кажется, никаких дополнительных объяснений здесь не требуется. Присваивание null в Optional разрушает саму идею его использования. Никто из пользователей вашего API не будет проверять Optional на эквивалентность с null. Вместо этого следует использовать Optional.empty().Знайте APIOptional обладает огромным количеством фичей, правильное использование которых позволяет добиться более простого и понятного кода.
public String getPersonName() {
Optional<String> name = getName();
if (name.isPresent()) {
return name.get();
}
return "DefaultName";
}
Идея проста: если имя отсутствует, вернуть значение по умолчанию. Можно сделать это лучше.
public String getPersonName() {
Optional<String> name = getName();
return name.orElse("DefautName");
}
Давайте рассмотрим что-нибудь посложнее. Предположим, что мы хотим вернуть Optional от имени пользователя. Если имя входит в список разрешенных, контейнер будет хранить значение, иначе — нет. Вот многословный пример.
public Optional<String> getPersonName() {
Person person = getPerson();
if (ALLOWED_NAMES.contains(person.getName())) {
return Optional.ofNullable(person.getName());
}
return Optional.empty();
}
Optional.filter упрощает код.
public Optional<String> getPersonName() {
Person person = getPerson();
return Optional.ofNullable(person.getName())
.filter(ALLOWED_NAMES::contains);
}
Этот подход стоит применять не только в контексте Optional, но ко всему процессу разработки.Если что-то доступно из коробки, попробуйте использовать это, прежде чем пытаться сделать свое.Отдавайте предпочтение контейнерам "на примитивах"В Java присутствуют специальные не дженерик Optional классы: OptionalInt, OptionalLong и OptionalDouble. Если вам требуется оперировать примитивами, лучше использовать вышеописанные альтернативы. В этом случае не будет лишних боксингов и анбоксингов, которые могут повлиять на производительность.Не пренебрегайте ленивыми вычислениямиOptional.orElse — это удобной инструмент для получения значения по умолчанию. Но если его вычисление является дорогой операцией, это может повлечь за собой проблемы с быстродействием.
public Optional<Table> retrieveTable() {
return Optional.ofNullable(constructTableFromCache())
.orElse(fetchTableFromRemote());
}
Даже если таблица присутствует в кэше, значение с удаленного сервера будет запрашиваться при каждом вызове метода. К счастью, есть простой способ избежать этого.
public Optional<Table> retrieveTable() {
return Optional.ofNullable(constructTableFromCache())
.orElseGet(this::fetchTableFromRemote);
}
Optional.orElseGet принимает на вход лямбду, которая будет вычислена только в том случае, если метод был вызван на пустом контейнере.Не оборачивайте коллекции в OptionalХотя я и видел такое не часто, иногда это происходит.
public Optional<List<String>> getNames() {
if (isDevMode()) {
return Optional.of(getPredefinedNames());
}
try {
List<String> names = getNamesFromRemote();
return Optional.of(names);
}
catch (Exception e) {
log.error("Cannot retrieve names from the remote server", e);
return Optional.empty();
}
}
Любая коллекция является контейнером сама по себе. Для того чтобы показать отсутствие элементов в ней, не требуется использовать дополнительные обертки.
public List<String> getNames() {
if (isDevMode()) {
return getPredefinedNames();
}
try {
return getNamesFromRemote();
}
catch (Exception e) {
log.error("Cannot retrieve names from the remote server", e);
return emptyList();
}
}
Чрезмерное использование Optional усложняет работу с API.Не передавайте Optional в качестве параметраА сейчас мы начинаем обсуждать наиболее спорные моменты. Почему не стоит передавать Optional в качестве параметра? На первый взгляд, это позволяет избежать NullPointerException, так ведь? Возможно, но корни проблемы уходят глубже.
public void doAction() {
OptionalInt age = getAge();
Optional<Role> role = getRole();
applySettings(name, age, role);
}
Во-первых, API имеет ненужные границы. При каждом вызове applySettings пользователь вынужден оборачивать значения в Optional. Даже предустановленные константы.Во-вторых, applySettings обладает четырьмя потенциальными поведениями в зависимости от того, являются ли переданные Optional пустыми, или нет.В-третьих, мы понятия не имеем, как реализация интерпретирует Optional. Возможно, в случае пустого контейнера происходит простая замена на значение по умолчанию. Может быть, выбрасывается NoSuchElementException. Но также вероятно, что наличие или отсутствие данных в монаде может полностью поменять бизнес-логику.Если взглянуть на javadoc к Optional, можно найти там интересную заметку.Optional главным образом предназначен для использования в качестве возвращаемого значения в тех случаях, когда нужно отразить состояние "нет результата" и где использование null может привести к ошибкам.Optional буквально символизирует нечто, что может содержать значение, а может — нет. Передавать возможное отсутствие результата в качестве параметра звучит как плохая идея. Это значит, что API знает слишком много о контексте выполнения и принимает те решения, о которых не должен быть в курсе.Что же, как мы можем улучшить этот код? Если age и role должны всегда присутствовать, мы можем легко избавиться от Optional и решать проблему отсутствующих значений на верхнем уровне.
public void doAction() {
OptionalInt age = getAge();
Optional<Role> role = getRole();
applySettings(name, age.orElse(defaultAge), role.orElse(defaultRole));
}
Теперь вызывающий код полностью контролирует значения аргументов. Это становится еще более критичным, если вы разрабатываете фреймворк или библиотеку.С другой стороны, если значения age и role могут быть опущены, вышеописанный способ не заработает. В этом случае лучшим решением будет разделение API на отдельные методы, удовлетворяющим разным пользовательским потребностям.
void applySettings(String name) { ... }
void applySettings(String name, int age) { ... }
void applySettings(String name, Role role) { ... }
void applySettings(String name, int age, Role role) { ... }
Возможно это выглядит несколько многословно, но теперь пользователь может сам решить, какой метод вызвать, избегая непредвиденных ошибок.Не используйте Optional в качестве полей классаЯ слышал разные мнения по этому вопросу. Некоторые считают, что хранение Optional напрямую в полях класса позволяет сократить NullPointerException на порядок. Мой друг, который работает в одном известном стартапе, говорит, что такой подход в их компании является утвержденным паттерном.Хотя хранение Optional в полях класса и звучит как хорошая идея, я думаю, что это может принести больше проблем, чем пользы.Отсутствие сериализуемостиOptional не имлементирует интерфейс Serializable. Это не баг, это намеренное решение, так как данный класс был спроектирован для использования в качестве возвращаемого значения. Проще говоря, любой объект, который содержит хотя одно Optional поле, нельзя сериализовать.На мой взгляд, этот аргумент является наименее убедительным, так как сейчас, в мире микросервисов и распределенных систем, платформенная сериализация не является настолько важной, как раньше.Хранение лишних ссылокOptional — это объект, который необходим пользователю всего несколько миллисекунд, после чего он может быть безболезненно удален сборщиком мусора. Но если мы храним Optional в качестве поля, он может оставаться там вплоть до самой остановки программы. Скорее всего, вы не заметите никаких проблем с производительностью на маленьких приложениях. Однако, если речь идет о большом сервисе с дюжинами бинов, последствия могут быть другие.Плохая интеграция со Spring Data/HibernateПредположим, что мы хотим построить простое Spring Boot приложение. Нам нужно получить данные из таблицы в БД. Сделать это очень просто, объявив Hibernate сущность и соответствующий репозиторий.
@Entity
@Table(name = "person")
public class Person {
@Id
private long id;
@Column(name = "firstname")
private String firstName;
@Column(name = "lastname")
private String lastName;
// constructors, getters, toString, and etc.
}
public interface PersonRepository extends JpaRepository<Person, Long> {
}
Вот возможный результат для personRepository.findAll().
Person(id=1, firstName=John, lastName=Brown)
Person(id=2, firstName=Helen, lastName=Green)
Person(id=3, firstName=Michael, lastName=Blue)
Пусть поля firstName и lastName могут быть null. Мы не хотим иметь дело с NullPointerException, так что просто заменим обычный тип поля на Optional.
@Entity
@Table(name = "person")
public class Person {
@Id
private long id;
@Column(name = "firstname")
private Optional<String> firstName;
@Column(name = "lastname")
private Optional<String> lastName;
// constructors, getters, toString, and etc.
}
Теперь все сломано.
org.hibernate.MappingException:
Could not determine type for: java.util.Optional, at table: person,
for columns: [org.hibernate.mapping.Column(firstname)]
Hibernate не может замапить значения из БД на Optional напрямую (по крайней мере, без кастомных конвертеров).Но некоторые вещи работают правильноДолжен признать, что в конечном итоге не все так плохо. Некоторые фреймворки корректно интегрируют Optional в свою экосистему.JacksonДавайте объявим простой эндпойнт и DTO.
public class PersonDTO {
private long id;
private String firstName;
private String lastName;
// getters, constructors, and etc.
}
@GetMapping("/person/{id}")
public PersonDTO getPersonDTO(@PathVariable long id) {
return personRepository.findById(id)
.map(person -> new PersonDTO(
person.getId(),
person.getFirstName(),
person.getLastName())
)
.orElseThrow();
}
Результат для GET /person/1.
{
"id": 1,
"firstName": "John",
"lastName": "Brown"
}
Как вы можете заметить, нет никакой дополнительной конфигурации. Все работает из коробки. Давайте попробует заменить String на Optional<String>.
public class PersonDTO {
private long id;
private Optional<String> firstName;
private Optional<String> lastName;
// getters, constructors, and etc.
}
Для того чтобы проверить разные варианты работы, я заменил один параметр на Optional.empty().
@GetMapping("/person/{id}")
public PersonDTO getPersonDTO(@PathVariable long id) {
return personRepository.findById(id)
.map(person -> new PersonDTO(
person.getId(),
Optional.ofNullable(person.getFirstName()),
Optional.empty()
))
.orElseThrow();
}
Как ни странно, все по-прежнему работает так, как и ожидается.
{
"id": 1,
"firstName": "John",
"lastName": null
}
Это значит, что мы можем использовать Optional в качестве полей DTO и безопасно интегрироваться со Spring Web? Ну, вроде того. Однако есть потенциальные проблемы.SpringDocSpringDoc — это библиотека для Spring Boot приложений, которая позволяет автоматически сгенерировать Open Api спецификацию.Вот пример того, что мы получим для эндпойнта GET /person/{id}.
"PersonDTO": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
Выглядит довольно убедительно. Но нам нужно сделать поле id обязательным. Это можно осуществить с помощью аннотации @NotNull или @Schema(required = true). Давайте добавим кое-какие детали. Что если мы поставим аннотацию @NotNull над полем типа Optional?
public class PersonDTO {
@NotNull
private long id;
@NotNull
private Optional<String> firstName;
private Optional<String> lastName;
// getters, constructors, and etc.
}
Это приведет к интересным результатам.
"PersonDTO": {
"required": [
"firstName",
"id"
],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
Как видим, поле id действительно добавилось в список обязательных. Так же, как и firstName. А вот здесь начинается самое интересное. Поле с Optional не может быть обязательным, так как само его наличие говорит о том, что значение потенциально может отсутствовать. Тем не менее, мы смогли запутать фреймворк с помощью всего лишь одной лишней аннотации.В чем здесь проблема? Например, если кто-то на фронтенде использует генератор типов сущностей по схеме Open Api, это приведет к получению неверной структуры, что в свою очередь может привести к повреждению данных.РешениеЧто же нам делать со всем этим? Ответ прост. Используйте Optional только для геттеров.
public class PersonDTO {
private long id;
private String firstName;
private String lastName;
public PersonDTO(long id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
public long getId() {
return id;
}
public Optional<String> getFirstName() {
return Optional.ofNullable(firstName);
}
public Optional<String> getLastName() {
return Optional.ofNullable(lastName);
}
}
Теперь этот класс можно безопасно использовать и как сущность Hibernate, и как DTO. Optional никак не влияет на хранимые данные. Он только оборачивает возможные null, чтобы корректно отрабатывать отсутствующие значения.Однако у этого подхода есть один недостаток. Его нельзя полностью интегрировать с Lombok. Optional getters не подерживаются библиотекой и, судя по некоторым обсуждениям на Github, не будут.Я писал статью по Lombok и я думаю, что это прекрасный инструмент. Тот факт, что он не интегрируются с Optional getters, довольно печален.На текущий момент единственным выходом является ручное объявление необходимых геттеров.ЗаключениеЭто все, что я хотел сказать по поводу java.util.Optional. Я знаю, что это спорная тема. Если у вас есть какие-то вопросы или предложения, пожалуйста, оставляйте свои комментарии. Спасибо за чтение!
===========
Источник:
habr.com
===========
===========
Автор оригинала: Semyon Kirekov
===========Похожие новости:
- [Разработка веб-сайтов, JavaScript] Основы отладки клиентских JS-приложений
- [Программирование, Анализ и проектирование систем, Хакатоны] Группа «М.Видео-Эльдорадо» приглашает на онлайн-контест для аналитиков
- [Программирование, C++, Алгоритмы, Математика] Пишем свой парсер математических выражений и калькулятор командной строки (перевод)
- [Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ReactJS] Архитектурный паттерн Dependency Injection в React-приложении
- [Java] Кейс использования Mapping Diagnostic Context и @Async
- [Программирование, Go, WebAssembly] Запускаем AssemblyScript в Go
- [Программирование, Проектирование и рефакторинг, API, Google API] Проектирование API: почему для представления отношений в API лучше использовать ссылки, а не ключи (перевод)
- [Python, Программирование] Как строить красивые графики на Python с Seaborn (перевод)
- [Тестирование IT-систем, JavaScript] Не используйте фикстуры в Cypress и юнит-тесты — используйте фабричные функции (перевод)
- [Разработка игр, Игры и игровые приставки] Ста-ста-статтеринг, или откуда в игре берутся микрофризы и как с ними бороться (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_java, #_oop (ООП), #_funktsionalnoe_programmirovanie (Функциональное программирование), #_java, #_optional, #_razrabotka_po (разработка по), #_backend, #_perevod (перевод), #_luchshie_praktiki (лучшие практики), #_programmirovanie (
Программирование
), #_java, #_oop (
ООП
), #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:38
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
NullPointerException - одна из самых раздражающих вещей в Java мире, которую был призван решить Optional. Нельзя сказать, что проблема полностью ушла, но мы сделали большие шаги. Множество популярных библиотек и фреймворков внедрили Optional в свою экосистему. Например, JPA Specification возвращает Optional вместо null.Я думаю, что некоторые программисты настолько впечатлились теми возможностями, которые предоставляет Optional, что они стали использовать его даже там, где это не нужно, зачастую применяя неправильные паттерны. В этой статье я привел некоторые ошибочные использования этой монады и предложил свой путь для решения возникших проблем. Optional не должен равняться nullМне кажется, никаких дополнительных объяснений здесь не требуется. Присваивание null в Optional разрушает саму идею его использования. Никто из пользователей вашего API не будет проверять Optional на эквивалентность с null. Вместо этого следует использовать Optional.empty().Знайте APIOptional обладает огромным количеством фичей, правильное использование которых позволяет добиться более простого и понятного кода. public String getPersonName() {
Optional<String> name = getName(); if (name.isPresent()) { return name.get(); } return "DefaultName"; } public String getPersonName() {
Optional<String> name = getName(); return name.orElse("DefautName"); } public Optional<String> getPersonName() {
Person person = getPerson(); if (ALLOWED_NAMES.contains(person.getName())) { return Optional.ofNullable(person.getName()); } return Optional.empty(); } public Optional<String> getPersonName() {
Person person = getPerson(); return Optional.ofNullable(person.getName()) .filter(ALLOWED_NAMES::contains); } public Optional<Table> retrieveTable() {
return Optional.ofNullable(constructTableFromCache()) .orElse(fetchTableFromRemote()); } public Optional<Table> retrieveTable() {
return Optional.ofNullable(constructTableFromCache()) .orElseGet(this::fetchTableFromRemote); } public Optional<List<String>> getNames() {
if (isDevMode()) { return Optional.of(getPredefinedNames()); } try { List<String> names = getNamesFromRemote(); return Optional.of(names); } catch (Exception e) { log.error("Cannot retrieve names from the remote server", e); return Optional.empty(); } } public List<String> getNames() {
if (isDevMode()) { return getPredefinedNames(); } try { return getNamesFromRemote(); } catch (Exception e) { log.error("Cannot retrieve names from the remote server", e); return emptyList(); } } public void doAction() {
OptionalInt age = getAge(); Optional<Role> role = getRole(); applySettings(name, age, role); } public void doAction() {
OptionalInt age = getAge(); Optional<Role> role = getRole(); applySettings(name, age.orElse(defaultAge), role.orElse(defaultRole)); } void applySettings(String name) { ... }
void applySettings(String name, int age) { ... } void applySettings(String name, Role role) { ... } void applySettings(String name, int age, Role role) { ... } @Entity
@Table(name = "person") public class Person { @Id private long id; @Column(name = "firstname") private String firstName; @Column(name = "lastname") private String lastName; // constructors, getters, toString, and etc. } public interface PersonRepository extends JpaRepository<Person, Long> { } Person(id=1, firstName=John, lastName=Brown)
Person(id=2, firstName=Helen, lastName=Green) Person(id=3, firstName=Michael, lastName=Blue) @Entity
@Table(name = "person") public class Person { @Id private long id; @Column(name = "firstname") private Optional<String> firstName; @Column(name = "lastname") private Optional<String> lastName; // constructors, getters, toString, and etc. } org.hibernate.MappingException:
Could not determine type for: java.util.Optional, at table: person, for columns: [org.hibernate.mapping.Column(firstname)] public class PersonDTO {
private long id; private String firstName; private String lastName; // getters, constructors, and etc. } @GetMapping("/person/{id}")
public PersonDTO getPersonDTO(@PathVariable long id) { return personRepository.findById(id) .map(person -> new PersonDTO( person.getId(), person.getFirstName(), person.getLastName()) ) .orElseThrow(); } {
"id": 1, "firstName": "John", "lastName": "Brown" } public class PersonDTO {
private long id; private Optional<String> firstName; private Optional<String> lastName; // getters, constructors, and etc. } @GetMapping("/person/{id}")
public PersonDTO getPersonDTO(@PathVariable long id) { return personRepository.findById(id) .map(person -> new PersonDTO( person.getId(), Optional.ofNullable(person.getFirstName()), Optional.empty() )) .orElseThrow(); } {
"id": 1, "firstName": "John", "lastName": null } "PersonDTO": {
"type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "firstName": { "type": "string" }, "lastName": { "type": "string" } } } public class PersonDTO {
@NotNull private long id; @NotNull private Optional<String> firstName; private Optional<String> lastName; // getters, constructors, and etc. } "PersonDTO": {
"required": [ "firstName", "id" ], "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "firstName": { "type": "string" }, "lastName": { "type": "string" } } } public class PersonDTO {
private long id; private String firstName; private String lastName; public PersonDTO(long id, String firstName, String lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } public long getId() { return id; } public Optional<String> getFirstName() { return Optional.ofNullable(firstName); } public Optional<String> getLastName() { return Optional.ofNullable(lastName); } } =========== Источник: habr.com =========== =========== Автор оригинала: Semyon Kirekov ===========Похожие новости:
Программирование ), #_java, #_oop ( ООП ), #_funktsionalnoe_programmirovanie ( Функциональное программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:38
Часовой пояс: UTC + 5