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

Книга «Современный язык Java. Лямбда-выражения, потоки и функциональное программирование»

Время на прочтение10 мин
Количество просмотров15K
imageПривет, Хаброжители! Преимущество современных приложений — в передовых решениях, включающих микросервисы, реактивные архитектуры и потоковую обработку данных. Лямбда-выражения, потоки данных и долгожданная система модулей платформы Java значительно упрощают их реализацию.

Книга поможет вам овладеть новыми возможностями современных дополнений, таких как API Streams и система модулей платформы Java. Откройте для себя новые подходы к конкурентности и узнайте, как концепции функциональности улучшают работу с кодом.

В этой книге: • Новые возможности Java • Потоковые данные и реактивное программирование • Система модулей платформы Java.

Отрывок. Глава 11. Класс Optional как лучшая альтернатива null


Поднимите руку, если за свою карьеру Java-разработчика вы хоть раз получали исключение NullPointerException. Оставьте ее поднятой, если это исключение — наиболее частое из встречавшихся вам. К сожалению, мы не видим вас сейчас, но очень вероятно, что ваша рука поднята. Мы также подозреваем, что вы можете думать что-то вроде: «Да, согласен. Исключения NullPointerException — головная боль для любого Java-разработчика, неважно, новичка или эксперта. Но поделать с ними все равно ничего нельзя, это цена, которую мы платим за использование такой удобной и, вероятно, неизбежной конструкции, как пустые ссылки». Это общее мнение в мире (императивного) программирования; тем не менее, возможно, это не вся правда, а скорее глубоко укоренившееся предубеждение.

Британский специалист в области компьютерных наук Тони Хоар (Tony Hoare), создавший пустые ссылки еще в 1965 году, при разработке языка ALGOL W, одного из первых типизированных языков программирования с записями, память под которые выделялась в куче, позднее признался, что сделал это «просто из-за легкости реализации». Хотя он хотел гарантировать «полную безопасность использования исключение для пустых ссылок, поскольку думал, что это самый удобный способ смоделировать отсутствие значения. Много лет спустя он пожалел об этом решении, назвав его «моя ошибка на миллиард долларов». Мы все видели результаты этого решения. Например, мы можем проверять поле объекта, чтобы определить, представляет ли оно одно из двух возможных значений, лишь для того, чтобы обнаружить, что проверяем не объект, а нулевой указатель, и тут же получить это надоедливое исключение NullPointerException.

На самом деле Хоар, возможно, даже недооценил масштаб затрат на исправление миллионами разработчиков ошибок, вызванных пустыми ссылками за последние 50 лет. И действительно, абсолютное большинство созданных за последние десятилетия языков программирования1, включая Java, основывается на том же самом проектном решении, возможно, по причинам совместимости с более старыми языками или (что более вероятно), как сказал Хоар, «просто из-за легкости реализации». Мы начнем с демонстрации простого примера проблем, возникающих при использовании null.

11.1. Как смоделировать отсутствие значения


Представьте себе, что у вас есть приведенная в листинге 11.1 вложенная структура объектов для владельца автомобиля, купившего автостраховку.

Листинг 11.1. Модель данных Person/Car/Insurance

public class Person {
      private Car car;
      public Car getCar() { return car; }
}
public class Car {
      private Insurance insurance;
      public Insurance getInsurance() { return insurance; }
}
public class Insurance {
      private String name;
      public String getName() { return name; }
}

Как вы думаете, в чем проблема следующего кода?

public String getCarInsuranceName(Person person) {
      return person.getCar().getInsurance().getName();
}

Этот код выглядит вполне разумно, но у многих людей нет автомобилей, так какой же в этом случае будет результат вызова метода getCar? Зачастую (и совершенно напрасно) возвращают пустую ссылку, чтобы указать на отсутствие значения (в данном случае чтобы указать на отсутствие машины). В результате вызов метода getInsurance вернет страховку пустой ссылки, что приведет к генерации NullPointerException во время выполнения и останову программы. Но это еще не все. А что, если объект person был равен null? Что, если метод getInsurance тоже вернул null?

11.1.1. Снижение количества исключений NullPointerException с помощью проверки на безопасность


Как избежать неожиданных NullPointerException? Обычно можно добавить проверки на null везде, где нужно (а иногда, превышая требования безопасного программирования, и там, где не нужно), причем зачастую в различных стилях. Первая наша попытка написать метод, который бы предотвращал генерацию NullPointerException, показана в листинге 11.2.

image

Этот метод выполняет проверку на null при каждом разыменовании переменной, возвращая строковое значение «Unknown», если хоть одна из переменных, встречавшихся в этой цепочке разыменования, представляет собой пустое значение. Единственное исключение из данного правила — мы не проверяем на null название страховой компании, поскольку знаем, что (как и у любой другой компании) у нее обязано быть название. Обратите внимание, что этой последней проверки нам удается избежать только за счет знаний предметной области, но этот факт не отражен в Java-классах, моделирующих наши данные.

Приведенный в листинге 11.2 метод мы описали как «глубокие сомнения», поскольку в нем заметен повторяющийся паттерн: каждый раз в случае сомнений, не равна ли переменная null, приходится добавлять еще один вложенный блок if, повышая тем самым уровень отступов кода. Очевидно, что эта методика плохо масштабируется и снижает удобочитаемость, так что лучше попробовать другое решение. Во избежание приведенной проблемы пойдем другим путем, как показано в листинге 11.3.

Во второй попытке мы пробуем избежать глубокого вложения блоков if с помощью другой стратегии: всякий раз, когда мы натыкаемся на равную null переменную, возвращаем строковое значение «Unknown». Но это решение тоже далеко от идеала; теперь метод насчитывает четыре различные точки выхода, что сильно усложняет его сопровождение. Вдобавок возвращаемое в случае null значение по умолчанию — строка «Unknown» — повторяется в трех местах и (мы надеемся) не содержит орфографических ошибок! Во избежание этого можно, конечно, вынести повторяющуюся строку в константу.

image

Более того, этот процесс подвержен ошибкам. Что, если вы забудете проверить, равно ли null одно из свойств? В этой главе мы приведем аргументы в пользу того, что использование null для представления отсутствия значения — принципиально ошибочный подход. Требуется лучший способ моделирования отсутствия и наличия значения.

11.1.2. Проблемы, возникающие с null


Если подвести итог вышесказанного, использование пустых ссылок в Java приводит к следующим, как теоретическим, так и практическим проблемам.

  • Служит источником ошибок. NullPointerException — наиболее распространенное (с огромным отрывом) исключение в Java.
  • «Раздувает» код. Ухудшает удобочитаемость из-за необходимости заполнять код проверками на null, зачастую глубоко вложенными.
  • Бессмысленно. У него отсутствует какой-либо семантический смысл, в частности, такой подход к моделированию отсутствия значения в языке со статической типизацией принципиально ошибочен.
  • Нарушает идеологию языка Java. Java всегда скрывает указатели от разработчиков, за одним исключением: нулевой указатель.
  • Создает брешь в системе типов. null не включает никакого типа или другой информации, так что его можно присвоить любому типу ссылки. Это может приводить к проблемам при передаче null в другую часть системы, где отсутствует информация о том, чем изначально должен быть этот null.

В качестве основы для других допустимых решений в следующем подразделе мы вкратце рассмотрим возможности, предлагаемые другими языками программирования.

11.1.3. Альтернативы null в других языках программирования


В последние годы такие языки, как Groovy, сумели обойти данную проблему за счет появления оператора безопасного вызова (?.), предназначенного для безопасной работы со значениями, потенциально равными null. Чтобы разобраться, как этот процесс осуществляется на практике, рассмотрим следующий код на языке Groovy. В нем извлекается название страховой компании, в которой заданный человек застраховал машину:

def carInsuranceName = person?.car?.insurance?.name

Вам должно быть ясно, что делает этот код. У человека может не быть машины, для моделирования чего мы присваиваем null ссылке car объекта person. Аналогично машина может оказаться незастрахованной. Оператор безопасного вызова языка Groovy позволяет безопасно работать с потенциально пустыми ссылками, без генерации исключения NullPointerException, передавая пустую ссылку далее по цепочке вызовов и возвращая null, если любое значение из цепочки равно null.

Аналогичную возможность предлагалось внедрить в Java 7, но затем было принято решение этого не делать. Однако, как ни странно, оператор безопасного вызова не так уж нужен в Java. Первая мысль любого Java-разработчика при столкновении с NullPointerException — быстро исправить проблему, добавив оператор if для проверки значения на null перед вызовом его метода. Решение проблемы подобным образом, без учета того, допустим ли null в этой конкретной ситуации для вашего алгоритма или модели данных, приводит не к исправлению, а к скрытию ошибки. И впоследствии для очередного разработчика (вероятно, вас самого через неделю или месяц) найти и исправить эту ошибку будет гораздо сложнее. Фактически при этом вы просто сметаете мусор под ковер. null-безопасный оператор разыменования языка Groovy — всего лишь большая и более мощная метла, с помощью которой можно делать подобные глупости, особо не волнуясь о последствиях.

Другие функциональные языки программирования, например Haskell и Scala, смотрят на эту проблему иначе. В Haskell есть тип Maybe, по сути инкапсулирующий опциональное значение. Объект типа Maybe может содержать значение заданного типа или ничего не содержать. В Haskell отсутствует понятие пустой ссылки. В Scala для инкапсуляции наличия или отсутствия значения типа T предусмотрена схожая логическая структура Option[T], которую мы обсудим в главе 20. При этом необходимо явным образом проверять, присутствует ли значение, с помощью операций типа Option, который обеспечивает «проверки на null». Теперь уже невозможно «забыть проверить на null», поскольку проверки требует сама система типов.

Ладно, мы немного отклонились от темы, и все это звучит довольно абстрактно. Наверное, вам интересно, что в этом смысле предлагает Java 8. Вдохновившись идеей опционального значения, создатели Java 8 ввели в ее состав новый класс, java.util.Optional! В этой главе мы покажем, в чем состоит преимущество использования его для моделирования потенциально отсутствующих значений вместо присвоения им пустой ссылки. Мы также поясним, почему такой переход от null к Optional требует от программиста пересмотра идеологии работы с опциональными значениями в модели предметной области. Наконец, мы изучим возможности этого нового класса Optional и приведем несколько практических примеров его эффективного использования. В итоге вы научитесь проектировать улучшенные API, в которых пользователю уже из сигнатуры метода понятно, возможно ли здесь опциональное значение.

11.2. Знакомство с классом Optional


В Java 8 под влиянием языков Haskell и Scala появился новый класс java.util.Optional для инкапсуляции опционального значения. Например, если вы знаете, что человек может владеть машиной, а может и не владеть, то не следует объявлять переменную car в классе Person с типом Car и присваивать ей пустую ссылку, если у человека нет машины; вместо этого ее тип должен быть Optional, как показано на рис. 11.1.

image

При наличии значения класс Optional служит адаптером для него. И наоборот, отсутствие значения моделируется с помощью пустого опционала, возвращаемого методом Optional.empty. Этот статический фабричный метод возвращает специальный экземпляр-одиночку класса Optional. Наверное, вам интересно, в чем состоит различие пустой ссылки и Optional.empty(). Семантически их можно считать одним и тем же, но на практике между ними существуют колоссальные различия. Попытка разыменования null неизбежно приводит к NullPointerException, а Optional.empty() — корректный, работоспособный объект типа Optional, к которому можно удобно обращаться. Скоро вы увидите, как именно.

Важное практическое смысловое отличие в использовании объектов Optional вместо null: объявление переменной типа Optional ясно говорит, что в этом месте допускается пустое значение. И наоборот, всегда используя тип Car и, возможно, иногда присваивая переменным этого типа пустые ссылки, вы подразумеваете, что полагаетесь только на свои знания предметной области, чтобы понять, относится ли null к области определения заданной переменной.

С учетом этого можно переделать исходную модель из листинга 11.1. Используем класс Optional, как показано в листинге 11.4.

Отметим, что использование класса Optional обогащает семантику модели. Поле типа Optional в классе Person и поле типа Optional в классе Car отражают тот факт, что у человека может быть машина, а может и не быть, равно как машина может быть застрахована, а может и не быть.

В то же время объявление названия страховой компании как String, а не Optional явно говорит о том, что у страховой компании должно быть название. Таким образом, вы точно знаете, что получите NullPointerException при разыменовании названия страховой компании; добавлять проверку на null не нужно, ведь это только скрыло бы проблему. У страховой компании должно быть название, так что, если вам попадется компания без названия, нужно выяснить, что не так с данными, а не добавлять фрагмент кода для скрытия этого обстоятельства.

image

Благодаря последовательному использованию опциональных значений создается четкое различие между значением, которое заведомо может отсутствовать, и значением, которое отсутствует из-за ошибки в алгоритме или проблемы в данных. Важно отметить, что класс Optional не предназначен для замены всех пустых ссылок до единой. Его задача — помочь спроектировать более понятные API, чтобы по сигнатуре метода можно было понять, может ли там встретиться опциональное значение. Система типов Java заставляет распаковывать опционал для обработки отсутствия значения.

Об авторах


Рауль-Габриэль Урма — генеральный директор и один из основателей компании Cambridge Spark (Великобритания), ведущего образовательного сообщества для исследователей данных и разработчиков. Рауль был избран одним из участников программы Java Champions в 2017 году. Он работал в компаниях Google, eBay, Oracle и Goldman Sachs. Защитил диссертацию по вычислительной технике в Кембриджском университете. Кроме того, он имеет диплом магистра технических наук Имперского колледжа Лондона, окончил его с отличием и получил несколько премий за рационализаторские предложения. Рауль представил более 100 технических докладов на международных конференциях.

Марио Фуско — старший инженер-разработчик программного обеспечения в компании Red Hat, участвующий в разработке ядра Drools — механизма обработки правил JBoss. Обладает богатым опытом разработки на языке Java, участвовал (зачастую в качестве ведущего разработчика) во множестве корпоративных проектов в различных отраслях промышленности, начиная от СМИ и заканчивая финансовым сектором. В числе его интересов функциональное программирование и предметно-ориентированные языки. На основе этих двух увлечений он создал библиотеку lambdaj с открытым исходным кодом, желая разработать внутренний DSL Java для работы с коллекциями и сделать возможным применение некоторых элементов функционального программирования в Java.

Алан Майкрофт — профессор на факультете компьютерных наук Кембриджского университета, где он преподает с 1984 года. Он также сотрудник колледжа Робинсона, один из основателей Европейской ассоциации языков и систем программирования, а также один из основателей и попечителей Raspberry Pi Foundation. Имеет ученые степени в области математики (Кембридж) и компьютерных наук (Эдинбург). Алан — автор более 100 научных статей. Был научным руководителем для более 20 кандидатов на соискание PhD. Его исследования в основном касаются сферы языков программирования и их семантики, оптимизации и реализации. Какое-то время он работал в AT&T Laboratories и научно-исследовательском отделе Intel, а также участвовал в основании компании Codemist Ltd., выпустившей компилятор языка C для архитектуры ARM под названием Norcroft.

» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — Java

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Теги:
Хабы:
+9
Комментарии19

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия