Наверное вам уже приходилось иметь дело с объектами, которые меняют свою структуру по законам своего внутреннего развития или в зависимости от внешних условий. На практике это означает, что части объекта для внешнего мира могут быть иногда доступны, а иногда – нет.
Какие это могут быть объекты? Многие сервисные компании, торговые предприятия и банки предоставляют различные виды услуг в зависимости от времени суток. Различные устройства могут выполнять или не выполнять те или иные действия в зависимости от своего состояния. Если вы читаете этот текст с экрана смартфона или ноутбука, он может перестать его показывать, если заряд аккумуляторов опуститься ниже определенной границы.
В этой и последующей статье этого tutorial мы рассмотрим простенькие объекты с динамической структурой, способы их моделирования и использования в Java с помощью методов класса Optional.
Про использование методов этого класса написано немало, в том числе и на русском языке, в том числе и на Хабре. В этой серии статей я попытаюсь посмотреть на проблему пошире, рассмотреть преимущества и недостатки выбранного решения проблемы, а также альтернативные подходы.
В этой первой статье цикла мы поговорим о том, как не используя Optional в Java можно обходиться без NullPointerException. Я надеюсь, вы увидите, что без Optional жить можно, но с ним жизнь должна стать приятнее.
В следующих статьях мы поговорим об использовании Optional при создании, трансформации и использовании объектов с динамической структурой, нововведениях в классе в Java 9, а также о том, как закрыть остающиеся дыры в функциональности этого класса.
Итак, давайте порассуждаем о том, как на Java можно реализовать доступ к элементам с динамической структурой, например – нашего первого прибора: простого кофе-автомата.
Следует сразу оговориться, что физическая структура прибора со временем не меняется. Говоря о динамике структуры, мы глядим на прибор глазами пользователя. Если по каким-то причинам прибор не может приготовить пользователю кофе, то этой функциональности для пользователя в нем на данный момент фактически нет.
Как можно это выразить с помощью Java?
Предположим, что мы получили задание написать блок управления таким прибором и нам необходима функция вроде
device.getCoffeePortion()
Какого типа элемент должен возвращать этот метод? Ведь иногда прибор может быть способен дать пользователю кофе, а иногда нет.
Существует не так уж много способов решения этой проблемы (по крайней мере известных мне).
Возвращаем либо объект либо null
Самым распространенным решением является возвращения некоторого обьекта в позитивном случае и null в отрицательном. Пользователь должен поэтому всегда проверять возвращаемое значение и использовать его только если оно не null.
Проблема заключается в том, что пользователь вашего объекта должен как-то знать или догадаться о возможности возвращения нулевого объекта. Программист, вызывающий ваш метод в своем коде, может без проверки передать его как параметр в цепочке вызовов других функций, записать в список и т. д. Это означает, что расплата в в виде NullPointerException может прийти существенно позже и в существенно удаленном от вызова функции месте.
NullPointerException заведомый чемпион среди ошибок Java программистов, и по количеству, и по затраченным на их поиски и устранение время и нервы.
Интересно отметить, что у этой катастрофы есть автор, назвавший её своей “billion-dollar mistake”. Null появился намного раньше Java в 1965 году при разработке языка ALGOL.
Спустя 44 года Tony Hoare, создатель этого чудовища (но и замечательный ученый) написал в своей статье “Null References: The Billion Dollar Mistake” (QCon, August 25, 2009),
I call it my billion-dollar mistake … My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Как создатель объекта вы можете несколько смягчить ситуацию используя аннотированный возвращаемый результат, например:
@Nullabble CoffeePortion getCoffeePortion()
Если программист использует современное IDE, то оно в определённых ситуациях укажет ему на необходимость проверки на нулевой объект. Но это происходит далеко не всегда и особо рассчитывать на это не стоит.
Итак, возвращать null как результат операции – это плохо. Значит надо этого избегать. А как?
Сначала хорошо попросите
Одним из вариантов решения является снабжения каждого get… метода напарником has… Пользователь объекта должен быть как-то предупрежден, что перед запросом get… он должен спросить, а имеется ли значение, которое надо возвращать. В нашем случае пара выглядела бы так:
boolean hasCofeePortion()
CoffeePortion getCoffeePortion()
Если первая функция возвращает true, она дает тем самым добро на вызов функции -напарника. Пользователь теоретически может попробовать не вызывать первую has… функцию. Если же значение на момент вызова будет недоступно, функция get… вернет не null, как было в первом варианте, а выбросит Exception.
Таким образом проблема передачи “опасного” объекта по цепочке вызова исключается. В этом преимущество подхода. Но у подхода есть два существенных недостатка. Во-первых, умножается количество функций. Во-вторых, нет гарантии, что на момент вызова has… объект был доступен, а к моменту get … перестал быть доступным. (В нашем простом примере это невозможно, но в сложных системах, где состояние объектов зависит от времени или внешних воздействий, а для подготовки вызова get… требуется провести другие операции, эта опасность реальна.
Итак, сначала спросить, а потом взять – не очень хорошая стратегия. А нельзя наоборот? Именно эту идею использует следующий подход.
Объект всегда выдается, но не всегда живой
В этом подходе объект всегда возвращается, но содержит дополнительный признак. Это признак говорит нам, грубо говоря, есть он на самом деле или его нет. Заострив до крайности, можно этот подход сформулировать так: вы получаете либо живой объект, либо его труп. Но даже если это труп, он немного говорящий.
А именно, подход говорит, что метод типа isActive() или isPresent() вы всегда вы можете без опаски вызывать. Даже у “мертвого” объекта. И при положительном ответе работать с объектом дальше.
Очевидным недостатком этого подхода является навешивание на объект технического признака, никак не объяснимого с точки зрения его собственного функционирования (business logic). В определенных ситуациях этот признак может входить в противоречие с этой логикой. Например, представим, что некий объект связан с двумя другими объектами. С точки зрения одного объекта он может быть активен или “жив”, а с точки зрения другого “мертв”.
Однако, с технической точки зрения, у этого подхода могут быть и свои, иногда неожиданные, преимущества. Например, если нам надо сохранять или пересылать составной объект в виде JSON или XML, разумно пересылать его полностью, включая и его на данный момент деактивированные части. Ведь может оказаться так, что после восстановления объекта (считывания из файла или потока) деактивированные части когда-нибудь придется активировать.
Итак, этот подход хорош тем, что не приводит к NullPointerException, но “ненатурален”. Нельзя ли найти что-то похожее, но без описанных недостатков?
Возвращается лист, возможно пустой
Один из самых рекомендованных экспертами до появления в Java 8 Optional подходов был подход, описанный ниже.
Идея его крайне проста. Вместо объекта мы возвращаем массив или список объектов. Этот массив или список может быть либо пустым либо содержать точно один объект.
Фактически мы принуждаем тем самым пользователей нашего метода на месте, сразу же после вызова функции разобраться со списком. Вряд ли он будет этот список передавать дальше как параметр других методов.
Недостатком метода является его очевидная вычурность Уж очень сильно начинают различаться ситуации, когда get… метод возвращает элементарное значение, например int, заведомо существующий объект и условно существующий объект.
И чтобы преодолеть это противоречие, а также по другим причинам, разработчики языка Java и решили ввести Optional. О достоинствах и недостатках этого класса мы поговорим в следующих статьях этого цикла. И уже с следующей статье я обещаю объяснить, как связаны между собой котёнок на заглавной иллюстрации и тема этого цикла.
До встречи!
Иллюстрация: ThePixelman
Комментарии (16)
jeka_odessit
25.01.2018 10:08imho, Я бы не разбивал эту статью на две и более части, в этой статье вообще ничего не раскрывается интересного. hasX перед getX вообще оверкил :/
visirok Автор
25.01.2018 11:41Проблема представления и использования обьектов с динамической структурой решается в Java и без использования Optional. Многие проекты по разным причинам до сих пор не могут использовать Java 8/Java 9. В этой статье я попытался систематизировать подходы к решению проблемы именно для этих случаев. Информацию о подходах я почерпнул из литературы и guidlines проектов, в которых участвовал сам. Хотите верьте, хотите нет — но вариант с hasX...getX из реального проекта.
Moriline
25.01.2018 10:08Почему это идея со списком или итератором или массивом так уж плоха(а уж в сравнении с Optional — это ещё два раза посмотреть!)? ПОЧЕМУ именно логически она плоха? На примере кофе-машины — разве она не может выдать НЕСКОЛЬКО порций кофе? И разве она так не делает? Но разберем тезисы подробнее:
Фактически мы принуждаем тем самым пользователей нашего метода на месте, сразу же после вызова функции разобраться со списком. Вряд ли он будет этот список передавать дальше как параметр других методов.
Нет, не принуждаем! Откуда это взято? И список и итератор или массив можно и потом передать в методы и сохранить и по сети передать — всё зависит от задачи.
Недостатком метода является его очевидная вычурность
В ЧЕМ КОНКРЕТНО эта «очевидная вычурность» проявляется? И как тогда проявляется «неочевидная невычурность»? К чему использовать такие «интересные» речевые обороты?
Уж очень сильно начинают различаться ситуации, когда get… метод возвращает элементарное значение, например int, заведомо существующий объект и условно существующий объект.
А тут вообще натягиваем сову на глобус! В коде, видите ли, сильно ситуации различаются с методами? В этом проблема? В том что есть методы которые возвращают РАЗНЫЕ значения? Так по моему они только этим и занимаются — на вход принимают значения, обрабатывают их и выдают результат на выходе. Разные на входе и разные на выходе.
А теперь практика — как это должно выглядеть например с итератором:
interface IMarks { Iterator<String> findById(Long ... ids); void print(PrintStream ps, Iterator<String> values); void printOne(PrintStream ps, String value); } public final class Marks implements IMarks{ Map<Long, String> test = new HashMap<>(); { test.put(1L, "1"); test.put(2L, "2"); test.put(3L, "3"); test.put(4L, "4"); test.put(5L, "5"); } @Override public Iterator<String> findById(Long... ids) { List<Long>idsList = Arrays.asList(ids); List<String> list = new ArrayList<String>(); for(Entry<Long, String>entry : test.entrySet()) { if(idsList.contains(entry.getKey())) list.add(entry.getValue()); } return list.iterator(); } public static void main(String[] args) { IMarks marks = new Marks(); Iterator<String>it = marks.findById(2L, 3L, 5L); if(it.hasNext()) marks.printOne(System.out, it.next()); if(it.hasNext()) marks.print(System.out, it); } @Override public void print(PrintStream ps, Iterator<String> values) { while (values.hasNext()) { ps.println(values.next()); } } @Override public void printOne(PrintStream ps, String value) { ps.println(value); } }
Смотрите метод main. Сравните сигнатуры методов print и printOne.
Ну а если использовать аналогию с кофе, то будет примерно так:
Iterator<CoffeePortion> coffeePortions() // или List<CoffeePortion> coffeePortions()
А вишенкой на торте последний вопрос — так зачем нужен Optional, если и так можно делать?visirok Автор
25.01.2018 12:01Прежде всего, спасибо Вам за потраченные усилия на обоснования Ваших тезисов.
Попытаюсь представить мои возражения в порядке соответствующем Вшему тексту.
Идея со списком или массивом активно рекомендовалась экспертами как одно из лучших решений проблемы избежания NullPointerException до появления Optional. Но так было за неимением лучшего решения.
Передавать список или массив дальше я не считаю хорошей идеей, поскольку вызывающие клиенты могут не знать, что на самом деле ожидается строго 1 или 0 обьектов в списке.
Про «вычурность». Никак не умоляя Вашего класса в программировании, должен сказать, что именно представленный Вами пример подпадает под моё определение «вычурности». Другими словами, если вспомнить о том, какую задачу (представление обьекта либо его отсутствия) мы решаем, то подобное решение неоправдано сложно для понимания, использования и реализации. Но это моё личное мнение, я Вам его не навязываю.
Зачем нужен Optional, если и так можно сделать. Да, можно. Можно сделать и напрямую на ассемблере или даже на машине Тьюринга. Вопрос в затратах на изготовление, использование и сопровождение.
Надеюсь, следующие статьи серии Вас в этом убедят.
Поверьте, я действительно признателен Вам за Ваш комментарий. Он мне показывает ещё раз, что я не зря затеял написание этого tutorial.Moriline
25.01.2018 12:41Спасибо Вам за ответ на комментарий.
Да, можно. Можно сделать и напрямую на ассемблере или даже на машине Тьюринга.
Я оценил Ваш тонкий логический ход. Отвечаю в том же ключе: А можно еще ввести новый объект с интерфейсами и абстракциями, самостоятельной проверкой на null или исполнением(да это же паттерн NullObject!). А можно просто все проверки на null написать в отдельном классе и добавить туда методы работы с объектом.( да это же наш Optional!) Это лучшее решение?
Передавать список или массив дальше я не считаю хорошей идеей, поскольку вызывающие клиенты могут не знать, что на самом деле ожидается строго 1 или 0 обьектов в списке.
Здесь проблемы нет — работайте всегда со списком или массивом. Также как работаете в других местах. Такое впечатление что эти клиенты не знают как работать со списком значений полученных из метода! Я же специально привел примеры кода работы со списком.
Но как говорил Торвальдс: “Talk is cheap. Show me the code.”
Посмотрим на Вашу вторую часть с примерами кода. Огромная просьба будет к Вам — приведите пожалуйста примеры когда методы возвращают один обьект и несколько обьектов. Например — тот же поиск обьектов по Id и нескольким Id. Во избежание манипуляций, а также для более объемного и подробного материала для дальнейшего обсуждения.visirok Автор
25.01.2018 13:21Договорились. Только рассказа о списке Вам придётся ждать до третьей статьи серии, поскольку соответствующие методы добавлены в Optional только в Java 9.
Throwable
25.01.2018 12:08Не использовать вообще нулевые значения.
У большинства стандартных типов существует некое дефолтное значение, которым можно заменить null. Например для числовых типов это ноль, для String это пустая строка "", для списка — пустой список, etc.
Поля класса сразу инициализируйте дефолтными значениями.
Для собственных типов можно использовать можно искусственно определить нулевое значение и при необходимости сравнивать с ним.
public class MyClass { public static final MyClass EMPTY = new MyClass(); public final String name; // Optional field, "" by default public final String address; protected MyClass() {this("","");} public MyClass(@Nonnull String name) { this(name,""); } public MyClass(@Nonnull String name, @Nonnull String address) { this.name= name; this.address = address; } }
Возвращается лист, возможно пустой
Проблема в том, что лист mutable. Лучше возвращать Iterator или Stream.visirok Автор
25.01.2018 13:11То что Вы предлагаете, фактически является разновидностью варианта «Объект всегда выдается, но не всегда живой». Его можно назвать примерно так: «Объект всегда выдается, но иногда как зародыш».
Признаться, я не понял зачем нужен EMPTY если не имплементируется equals(). И как раз на Вашем примере мы видим, что надо весьма специальным методом отличать нормальный обьект от «зародыша». В другом обьекте при таком подходе надо знать его собственный метод распознования «зародыша». Поэтому и вводят в больших проектах при использовании подобного подхода общий интерфейс с методом типа isActive() или isPresent().
Ну а список можно сделать immutable с помощью Collections.unmodifiableList()Throwable
26.01.2018 10:35Идея как раз в том, чтобы исключить искусственные методы типа isActive() и isPresent() и соответствующие проверки (они ни чем не лучше проверки на null). Возвращаемый "пустой" объект имеет полностью осмысленное поведение при вызове своих методов, и не требует специальных кейсов в логике программы. Например с пустой сторокой можно делать все те же операции, или например пустой список можно также итерировать в цикле без специальной проверки.
я не понял зачем нужен EMPTY если не имплементируется equals()
Чтобы инициализировать дефолтными значениями поля этого типа в других классах.
public class AnotherClass { public MyClass myClass = MyClass.EMPTY; }
Это в случае если класс MyClass immutable. Тогда проверку на empty можно делать простым сравнением "==", но при желании можно добавить и equals()/hashCode(). Хотя как я уже сказал выше, поведение кода не должно отличаться в случае пустого или непустого объекта и не должно содержать искусственных проверок.
Ну а список можно сделать immutable с помощью Collections.unmodifiableList()
Это заведомо плохой способ, т.к. из сигнатуры метода семантически не ясно, является ли он modifiable или нет. Кроме того в случае интерфейса или абстрактного класса, наследуемые классы могут иметь совершенно разное поведение. Поэтому один и тот же код
userService.getUsers().add(new User());
в зависимости от имплементации UserService в одних случаях будет прокатывать, а в других нет.
visirok Автор
26.01.2018 14:25Вы с помощью частного примера пытаетесь обосновать общий подход. Но Ваш пример весьма искусственный. Это Value-Object и к тому же по определению immutable.
И давайте уточним ещё раз наши позиции. Я не утверждаю, что подход с использованием массива, списка, итератора для для представления объектов с динамической структурой всегда плох. Я утверждаю, что в большинстве реальных случаев он работает хуже, чем использование Optional.
Но доказательство этого тезиса я попытаюсь дать в следующей статье серии.
avost
27.01.2018 13:40+1Плохая идея. Дефолтовое значение зачастую входит в пространство допустимых значений переменной и тогда начинают появляться другие магические дефолтные значения и прочее уродство. Пррстой пример — функция поиска подстроки в строке. 0 — резрешённое значения, будем возвращать при неуспехе -1 в надежде, что у нас нет таких длинных строк — "гениально"! А теперь, чтение байта из файла… Опс, что-то пошло не так… Мы уже не можем вернуть ни 0 ни -1 — это допустимые значения. Что ж, давайте возвращать не байт, а int… "гениально"!
lany
27.01.2018 15:26Про Optional уже были статьи на Хабре. Например, вот постарее или вот совсем недавно. Когда пишете новую статью, полезно хотя бы себе самому ответить, что она добавит к уже существующим статьям, а в чём повторится.
visirok Автор
27.01.2018 15:56Вы правы. Обе статьи я упоминаю во второй части этой серии, которую я как раз дорабатываю. Но с другой стороны, то что написано в этой первой части, в тех статьях (и известных мне других tutorials и обзорах) нет. Так что в этой первой части новизна и отличие от ранее опубликованых статей наличиствует.
CyberSoft
А где примеры? Следующие части тоже «сухим текстом» будут?
По теме: «Объект всегда выдается, но не всегда живой» похоже на паттерн NullObject. В принципе самый оптимальный вариант на все времена. ИМХО, единственное преимущество Optional — в методах map()/filter(), что врядли делали, используя упомянутый паттерн.
visirok Автор
Примеры начнутся начиная со следующей части, которую я планирую опубликовать в ближайшие дни.
По второму пункту Вашего комментария позволю себе с Вами не согласиться. Надеюсь и Вы измените своё мнение после прочтения следующей статьи серии.
CyberSoft
Жду с нетерпением!