Глава 1 Привет, лямбда-выражение!
Раздел 1
Стиль кодирования Java претерпевает огромные изменения.
Наша повседневная работа станет проще, удобнее и выразительнее. Java, новый метод программирования, появился в других языках программирования несколько десятилетий назад. После того, как эти новые функции будут добавлены в Java, мы сможем писать код, который станет более кратким, элегантным, более выразительным и содержащим меньше ошибок. Мы можем реализовать различные стратегии и шаблоны проектирования с меньшим количеством кода.
В этой книге мы рассмотрим программирование в функциональном стиле на примерах из повседневного программирования. Прежде чем использовать этот новый и элегантный способ проектирования и написания кода, давайте сначала посмотрим, что в нем такого хорошего.
изменил ваше мышление
Императивный стиль. Это подход, который язык Java обеспечивает с момента своего создания. Используя этот стиль, мы должны сообщать Java, что делать на каждом этапе, а затем наблюдать, как он фактически выполняет это шаг за шагом. Это, конечно, хорошо, но кажется несколько рудиментарным. Код выглядит немного многословным, и нам бы хотелось, чтобы язык стал немного умнее; мы должны просто говорить ему, что мы хотим, а не рассказывать, как это сделать. К счастью, Java наконец-то может помочь нам реализовать это желание. Давайте рассмотрим несколько примеров, чтобы понять преимущества и различия этого стиля.
нормальный способ
Начнем с двух знакомых примеров. Это командный метод проверки наличия Чикаго в указанной коллекции городов — помните, что код, приведенный в этой книге, является лишь частичным фрагментом.
Скопируйте код кода следующим образом:
логическое значение найдено = ложь;
for(String city : города) {
if(city.equals("Чикаго")) {
найдено = правда;
перерыв;
}
}
System.out.println("Нашли Чикаго?:" + найдено);
Эта императивная версия выглядит немного многословной и элементарной, она разделена на несколько частей исполнения; Сначала инициализируйте логический тег Found, а затем просмотрите каждый элемент в коллекции; если искомый город найден, установите этот тег, затем выйдите из цикла и, наконец, распечатайте результаты поиска;
лучший способ
Прочитав этот код, внимательные Java-программисты быстро придумают более краткий и понятный способ, например такой:
Скопируйте код кода следующим образом:
System.out.println("Нашли Чикаго?:" + city.contains("Чикаго");
Это также императивный стиль написания — метод contains делает это за нас напрямую.
фактические улучшения
Написание такого кода имеет несколько преимуществ:
1. Больше не нужно возиться с этой изменяемой переменной
2. Инкапсулируйте итерацию в нижний слой
3. Код стал проще
4. Код стал более понятным и целенаправленным
5. Делайте меньше обходных путей и более тесно интегрируйте код и потребности бизнеса.
6. Меньше ошибок
7. Простота понимания и обслуживания.
Возьмем более сложный пример.
Этот пример слишком прост. Императивный запрос о том, существует ли элемент в коллекции, можно увидеть повсюду в Java. Теперь предположим, что мы хотим использовать императивное программирование для выполнения некоторых более сложных операций, таких как анализ файлов, взаимодействие с базами данных, вызов WEB-сервисов, параллельное программирование и т. д. Теперь мы можем использовать Java для написания более лаконичного, элегантного и безошибочного кода, а не только в этом простом сценарии.
старый способ
Давайте посмотрим на другой пример. Мы определяем диапазон цен и рассчитываем общую цену со скидкой разными способами.
Скопируйте код кода следующим образом:
окончательные цены List<BigDecimal> = Arrays.asList(
новый BigDecimal("10"), новый BigDecimal("30"), новый BigDecimal("17"),
новый BigDecimal("20"), новый BigDecimal("15"), новый BigDecimal("18"),
новый BigDecimal("45"), новый BigDecimal("12"));
Предполагая, что существует скидка 10%, если она превышает 20 юаней, давайте сначала реализуем ее обычным способом.
Скопируйте код кода следующим образом:
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(BigDecimal цена: цены) {
if(price.compareTo(BigDecimal.valueOf(20)) > 0)
итогоцен со скидкой =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Общая цена со скидкой: " + totalOfDiscountedPrices);
Этот код должен быть очень знакомым; сначала используйте переменную для хранения общей цены, затем переберите все цены, найдите те, которые превышают 20 юаней, вычислите их цены со скидкой и добавьте их к общей цене, наконец, распечатайте общую цену; цена после скидки.
Вот вывод программы:
Скопируйте код кода следующим образом:
Всего цен со скидкой: 67,5
Результат полностью правильный, но код немного беспорядочный. Мы не виноваты, что можем писать только так, как умеем. Однако такой код немного примитивен. Он не только страдает паранойей базового типа, но и нарушает принцип единой ответственности. Если вы работаете дома и у вас есть дети, которые хотят стать программистами, вам придется спрятать свой код на случай, если они увидят его, вздохнут от разочарования и скажут: «Вы этим зарабатываете на жизнь?»
Есть лучший способ
Мы можем добиться большего – и намного лучше. Наш код чем-то похож на техническое задание. Это может сократить разрыв между бизнес-требованиями и реализованным кодом, уменьшив вероятность неправильной интерпретации требований.
Мы больше не позволяем Java создавать переменную и назначать ее бесконечно. Нам нужно взаимодействовать с ней на более высоком уровне абстракции, как в следующем коде.
Скопируйте код кода следующим образом:
окончательный BigDecimal totalOfDiscountedPrices =
цены.поток()
.filter(цена -> цена.compareTo(BigDecimal.valueOf(20)) > 0)
.map(цена -> цена.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Общая цена со скидкой: " + totalOfDiscountedPrices);
Прочтите это вслух — отфильтруйте цены, превышающие 20 юаней, преобразуйте их в цены со скидкой, а затем сложите. Этот код точно такой же, как процесс, который мы использовали для описания наших требований. В Java также очень удобно сложить длинную строку кода и выровнять ее построчно по точке перед именем метода, как указано выше.
Код очень простой, но мы используем много нового в Java8. Сначала мы вызываем потоковый метод прайс-листа. Это открывает двери для бесчисленного количества удобных итераторов, о которых мы поговорим позже.
Мы используем некоторые специальные методы, такие как фильтр и карта, вместо прямого обхода всего списка. Эти методы не похожи на методы JDK, которые мы использовали ранее, они принимают в качестве параметра анонимную функцию — лямбда-выражение. (Мы обсудим это подробно позже). Мы вызываем метод уменьшения() для расчета суммы цен, возвращаемых методом карты().
Как и в случае с методом contains, тело цикла скрыто. Однако метод карты (и метод фильтра) намного сложнее. Он вызывает переданное лямбда-выражение для расчета каждой цены в прайс-листе и помещает результат в новую коллекцию. Наконец, мы вызываем метод уменьшения этой новой коллекции, чтобы получить окончательный результат.
Это вывод приведенного выше кода:
Скопируйте код кода следующим образом:
Всего цен со скидкой: 67,5
области для улучшения
Это значительное улучшение по сравнению с предыдущей реализацией:
1. Хорошо структурировано, но не загромождено
2. Никаких низкоуровневых операций.
3. Легко улучшить или изменить логику.
4. Итерация по библиотеке методов
5. Эффективная ленивая оценка тела цикла;
6. Легко распараллеливается
Ниже мы поговорим о том, как это реализуется в Java.
Лямбда-выражения здесь, чтобы спасти мир
Лямбда-выражения — это ярлык, который избавляет нас от проблем императивного программирования. Эта новая функция, предоставляемая Java, изменила наш первоначальный метод программирования, сделав код, который мы пишем, не только кратким и элегантным, менее подверженным ошибкам, но и более эффективным, простым в оптимизации, улучшении и распараллеливании.
Раздел 2. Самая большая выгода от функционального программирования
Код функционального стиля имеет более высокое соотношение сигнал/шум; пишется меньше кода, но больше делается для каждой строки или выражения. По сравнению с императивным программированием функциональное программирование принесло нам большую пользу:
Избегается явная модификация или присвоение переменных, которые часто являются источником ошибок и затрудняют распараллеливание кода. В программировании командной строки мы постоянно присваиваем значения переменной totalOfDiscountedPrices в теле цикла. В функциональном стиле код больше не подвергается явным операциям модификации. Чем меньше переменных изменено, тем меньше ошибок в коде.
Код функционального стиля можно легко распараллелить. Если вычисление занимает много времени, мы можем легко запускать элементы списка одновременно. Если мы хотим распараллелить императивный код, нам также придется беспокоиться о проблемах, вызванных одновременным изменением переменной totalOfDiscountedPrices. В функциональном программировании мы получаем доступ к этой переменной только после ее полной обработки, что устраняет проблемы с безопасностью потоков.
Код более выразительный. Императивное программирование разделено на несколько шагов, чтобы объяснить, что делать — создать значение инициализации, перебрать цены, добавить цены со скидкой к переменным и т. д. — тогда как функциональному программированию достаточно, чтобы метод карты списка возвращал значение, включая скидку. Просто создайте новый список цен и затем накопите их.
Функциональное программирование проще; для достижения того же результата требуется меньше кода, чем императивное программирование. Более чистый код означает меньше кода, который нужно писать, меньше читать и меньше поддерживать — см. «Достаточно ли менее лаконично, чтобы быть лаконичным?» на стр. 7.
Функциональный код более интуитивен — чтение кода похоже на описание проблемы — и его легко понять, если мы знакомы с синтаксисом. Метод карты выполняет заданную функцию (вычисляет цену скидки) для каждого элемента коллекции, а затем возвращает набор результатов, как показано на рисунке ниже.
Рисунок 1. Карта выполняет заданную функцию для каждого элемента коллекции.
С помощью лямбда-выражений мы можем в полной мере раскрыть возможности функционального программирования на Java. Используя функциональный стиль, вы можете писать код более выразительный, краткий, имеющий меньше назначений и ошибок.
Поддержка объектно-ориентированного программирования — главное преимущество Java. Функциональное программирование и объектно-ориентированное программирование не являются взаимоисключающими. Реальное изменение стиля — переход от программирования из командной строки к декларативному программированию. В Java 8 можно эффективно интегрировать функциональность и объектно-ориентированность. Мы можем продолжать использовать стиль ООП для моделирования сущностей предметной области, их состояний и отношений. Кроме того, мы также можем использовать функции для моделирования поведения или переходов состояний, рабочего процесса и обработки данных, а также создавать составные функции.
Раздел 3. Зачем использовать функциональный стиль?
Мы увидели преимущества функционального программирования, но стоит ли использовать этот новый стиль? Это просто небольшое улучшение или полное изменение? Есть еще много практических вопросов, на которые необходимо ответить, прежде чем мы действительно потратим на это время.
Скопируйте код кода следующим образом:
Сяо Мин спросил:
Означает ли меньше кода простоту?
Простота означает меньше, но не беспорядок. В конечном итоге это означает возможность эффективно выразить намерение. Преимущества имеют далеко идущие последствия.
Написание кода похоже на объединение ингредиентов. Простота означает возможность смешивать ингредиенты с приправами. Написание краткого кода требует тяжелой работы. Требуется меньше кода для чтения, а действительно полезный код прозрачен для вас. Шорткод, который трудно понять или который скрывает детали, скорее короткий, чем лаконичный.
Простой код на самом деле означает гибкий дизайн. Простой код без волокиты. Это означает, что мы можем быстро опробовать идеи, двигаться дальше, если они работают хорошо, и быстро пропустить, если они не работают.
Написание кода на Java несложно, а синтаксис прост. И мы уже очень хорошо знаем существующие библиотеки и API. Что действительно сложно, так это использовать его для разработки и поддержки приложений корпоративного уровня.
Нам необходимо убедиться, что коллеги закрывают соединение с базой данных в нужное время, что они не продолжают занимать транзакции, что исключения правильно обрабатываются на соответствующем уровне, что блокировки устанавливаются и снимаются правильно и т. д.
По отдельности любая из этих проблем не имеет большого значения. Однако в сочетании со сложностью месторождения проблема становится очень сложной, ресурсы для разработки ограничены, а обслуживание затруднено.
Что произойдет, если мы инкапсулируем эти стратегии во множество небольших фрагментов кода и позволим им независимо управлять ограничениями? Тогда нам не придется постоянно тратить энергию на реализацию стратегий. Это огромное улучшение, давайте посмотрим, как это происходит в функциональном программировании.
Сумасшедшие итерации
Мы писали различные итерации для обработки списков, наборов и карт. Использование итераторов в Java очень распространено, но это слишком сложно. Они не только занимают несколько строк кода, их еще и трудно инкапсулировать.
Как нам просмотреть коллекцию и распечатать их? Вы можете использовать цикл for. Как отфильтровать некоторые элементы из коллекции? По-прежнему используйте цикл for, но вам нужно добавить несколько дополнительных изменяемых переменных. Как после выбора этих значений использовать их для нахождения окончательного значения, такого как минимальное значение, максимальное значение, среднее значение и т. д.? Затем вам придется переработать и изменить переменные.
Такая итерация подобна панацее: она может все, но все редко. Java теперь предоставляет встроенные итераторы для многих операций: например, те, которые выполняют только циклы, те, которые выполняют операции отображения, те, которые фильтруют значения, те, которые выполняют операции сокращения, а также существует множество удобных функций, таких как максимум, минимум и средний и т. д. Кроме того, эти операции можно хорошо комбинировать, поэтому мы можем объединить их для реализации бизнес-логики, которая проста и требует меньше кода. Более того, написанный код легко читается, поскольку он логически соответствует порядку описания задачи. Мы увидим несколько таких примеров в главе 2 «Использование коллекций» на стр. 19, и эта книга полна таких примеров.
Применить стратегию
Политики реализуются во всех приложениях уровня предприятия. Например, нам нужно подтвердить, что операция была правильно аутентифицирована в целях безопасности, нам нужно убедиться, что транзакция может быть выполнена быстро, а журнал изменений обновляется правильно. Эти задачи обычно представляют собой кусок обычного кода на стороне сервера, похожий на следующий псевдокод:
Скопируйте код кода следующим образом:
Транзакция транзакция = getFromTransactionFactory();
//... операция, выполняемая внутри транзакции...
checkProgressAndCommitOrRollbackTransaction();
ОбновлениеАудитТрейл();
В этом подходе есть две проблемы. Во-первых, это часто приводит к дублированию усилий, а также к увеличению затрат на техническое обслуживание. Во-вторых, легко забыть об исключениях, которые могут возникнуть в бизнес-коде и повлиять на жизненный цикл транзакции и обновление журнала изменений. Это должно быть реализовано с помощью блоков try иfinally, но каждый раз, когда кто-то касается этого кода, нам приходится повторно подтверждать, что эта стратегия не была разрушена.
Есть другой способ: мы можем удалить фабрику и поставить перед ней этот код. Вместо получения объекта транзакции передайте исполняемый код хорошо поддерживаемой функции, например:
Скопируйте код кода следующим образом:
runWithinTransaction((Транзакция транзакции) -> {
//... операция, выполняемая внутри транзакции...
});
Для вас это небольшой шаг, но он избавит вас от многих неприятностей. Стратегия одновременной проверки статуса и обновления журнала абстрагирована и инкапсулирована в метод runWithinTransaction. Мы отправляем этому методу фрагмент кода, который необходимо запустить в контексте транзакции. Нам больше не нужно беспокоиться о том, что кто-то забудет выполнить этот шаг или неправильно обработает исключение. Об этом уже заботится функция, реализующая политику.
Мы расскажем, как использовать лямбда-выражения для применения этой стратегии в главе 5.
Стратегия расширения
Стратегии, кажется, повсюду. Помимо их применения, корпоративные приложения также нуждаются в их расширении. Мы надеемся добавить или удалить некоторые операции с помощью некоторой информации о конфигурации. Другими словами, мы можем обработать их до того, как будет выполнена основная логика модуля. Это очень распространено в Java, но об этом нужно подумать и спроектировать заранее.
Компоненты, которые необходимо расширить, обычно имеют один или несколько интерфейсов. Нам необходимо тщательно спроектировать интерфейс и иерархическую структуру классов реализации. Это может сработать хорошо, но в результате у вас останется куча интерфейсов и классов, которые необходимо поддерживать. Такая конструкция может легко стать громоздкой и сложной в обслуживании, что в конечном итоге сводит на нет цель масштабирования.
Есть еще одно решение — функциональные интерфейсы и лямбда-выражения, которые мы можем использовать для разработки масштабируемых стратегий. Нам не нужно создавать новый интерфейс или использовать то же имя метода. Мы можем больше сосредоточиться на реализуемой бизнес-логике, о которой мы упомянем при использовании лямбда-выражений для оформления на странице 73.
Параллелизм стал проще
Крупное приложение приближается к этапу выпуска, когда внезапно возникает серьезная проблема с производительностью. Команда быстро определила, что узким местом производительности является огромный модуль, обрабатывающий огромные объемы данных. Кто-то из команды предположил, что производительность системы можно улучшить, если полностью использовать преимущества многоядерности. Однако, если этот огромный модуль написан в старом стиле Java, радость от этого предложения скоро будет развеяна.
Команда быстро поняла, что перевод этого гиганта с последовательного выполнения на параллельное потребует много усилий, добавит дополнительную сложность и легко вызовет ошибки, связанные с многопоточностью. Нет ли лучшего способа улучшить производительность?
Возможно ли, что последовательный и параллельный код одинаковы, независимо от того, выбираете ли вы последовательное или параллельное выполнение, точно так же, как нажатие переключателя и выражение вашей идеи?
Звучит так, будто это возможно только в Нарнии, но если мы полностью разовьемся в функциональном плане, все это станет реальностью. Встроенные итераторы и функциональный стиль устранят последнее препятствие на пути распараллеливания. Конструкция JDK позволяет переключаться между последовательным и параллельным выполнением всего лишь с помощью нескольких незаметных изменений кода, о которых мы упомянем в разделе «Завершение перехода к распараллеливанию» на странице 145.
рассказывать истории
Многое теряется в процессе превращения бизнес-требований в реализацию кода. Чем больше чего теряется, тем выше вероятность ошибки и стоимость управления. Если код выглядит так, как будто он описывает требования, его будет легче читать, его будет легче обсуждать с людьми, отвечающими за требования, и будет легче удовлетворить их потребности.
Например, вы слышите, как менеджер по продукту говорит: «Получите цены на все акции, найдите те, цена которых превышает 500 юаней, и рассчитайте общую сумму активов, которые могут принести дивиденды». Используя новые возможности, предоставляемые Java, вы можете написать:
Скопируйте код кода следующим образом:
Ticketers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
Этот процесс преобразования практически без потерь, поскольку конвертировать практически нечего. Это функциональный стиль в действии, и вы увидите еще много примеров этого в книге, особенно в главе 8 «Создание программ с помощью лямбда-выражений», стр. 137.
Сосредоточьтесь на карантине
При разработке системы обычно необходимо изолировать основной бизнес и требуемую для него детальную логику. Например, система обработки заказов может захотеть использовать разные стратегии налогообложения для разных источников транзакций. Изолирование налоговых расчетов от остальной логики обработки делает код более пригодным для повторного использования и масштабируемости.
В объектно-ориентированном программировании мы называем это изоляцией проблем, и для решения этой проблемы обычно используется шаблон стратегии. Обычно решение состоит в том, чтобы создать несколько интерфейсов и классов реализации.
Мы можем добиться того же эффекта с меньшим количеством кода. Мы также можем быстро опробовать собственные идеи продуктов, не придумывая кучу кода и не топчась на месте. Подробнее о том, как создать этот шаблон и выполнить изоляцию проблем с помощью облегченных функций, мы поговорим в разделе Изоляция проблем с помощью лямбда-выражений на странице 63.
ленивая оценка
При разработке приложений уровня предприятия мы можем взаимодействовать с WEB-сервисами, вызывать базы данных, обрабатывать XML и т. д. Нам необходимо выполнить множество операций, но не все из них необходимы постоянно. Избегание определенных операций или хотя бы отсрочка некоторых временно ненужных операций — один из самых простых способов повысить производительность или сократить время запуска программы и времени отклика.
Это всего лишь мелочь, но чтобы реализовать ее на чистом ООП-способе, требуется много работы. Чтобы отложить инициализацию некоторых тяжеловесных объектов, нам приходится обрабатывать различные ссылки на объекты, проверять наличие нулевых указателей и т. д.
Однако если вы используете новый класс Optinal и некоторые предоставляемые им API функционального стиля, этот процесс станет очень простым, а код станет более понятным. Мы обсудим это в разделе «Отложенная инициализация» на стр. 105.
Улучшение тестируемости
Чем меньше логики обработки в коде, тем меньше вероятность исправления ошибок. Вообще говоря, функциональный код легче модифицировать и легче тестировать.
Кроме того, как и в главе 4 «Проектирование с помощью лямбда-выражений» и главе 5 «Использование ресурсов», лямбда-выражения можно использовать в качестве облегченного макета объекта, чтобы сделать тестирование исключений более понятным и понятным. Лямбда-выражения также могут служить отличным подспорьем при тестировании. Многие распространенные тестовые примеры могут принимать и обрабатывать лямбда-выражения. Тестовые случаи, написанные таким образом, могут отразить суть функциональности, которая нуждается в регрессионном тестировании. В то же время различные реализации, которые необходимо протестировать, можно выполнить, передав различные лямбда-выражения.
Собственные автоматизированные тестовые примеры JDK также являются хорошим примером применения лямбда-выражений — если вы хотите узнать больше, вы можете взглянуть на исходный код в репозитории OpenJDK. С помощью этих тестовых программ вы можете увидеть, как лямбда-выражения параметризуют ключевые поведения тестового примера; например, они строят тестовую программу следующим образом: «Создайте контейнер для результатов», а затем «Добавьте некоторые параметризованные постусловия. Проверьте».
Мы увидели, что функциональное программирование не только позволяет писать качественный код, но и элегантно решает различные проблемы в процессе разработки. Это означает, что разработка программ станет быстрее и проще с меньшим количеством ошибок — если вы будете следовать нескольким рекомендациям, которые мы представим позже.
Раздел 4: Эволюция, а не революция
Нам не нужно переключаться на другой язык, чтобы воспользоваться преимуществами функционального программирования; все, что нам нужно, — это изменить способ использования Java. Такие языки, как C++, Java и C#, поддерживают императивное и объектно-ориентированное программирование. Но теперь они начинают осваивать функциональное программирование. Мы только что рассмотрели оба стиля кода и обсудили преимущества, которые может принести функциональное программирование. Теперь давайте рассмотрим некоторые из его ключевых концепций и примеров, которые помогут нам изучить этот новый стиль.
Команда разработчиков языка Java потратила много времени и энергии на добавление возможностей функционального программирования в язык Java и JDK. Чтобы воспользоваться преимуществами, которые он приносит, нам нужно сначала представить несколько новых концепций. Мы можем улучшить качество нашего кода, если будем следовать следующим правилам:
1. Декларативный
2. Продвигайте неизменность
3. Избегайте побочных эффектов
4. Предпочитайте выражения утверждениям
5. Проектирование с использованием функций высшего порядка
Давайте посмотрим на эти практические рекомендации.
декларативный
Ядро того, что мы знаем как императивное программирование, — это вариативность и программирование, управляемое командами. Мы создаем переменные, а затем постоянно изменяем их значения. Мы также предоставляем подробные инструкции для выполнения, такие как создание флага индекса итерации, увеличение его значения, проверка завершения цикла, обновление N-го элемента массива и т. д. Раньше из-за особенностей инструментов и аппаратных ограничений мы могли писать код только таким способом. Мы также видели, что в неизменяемой коллекции декларативный метод contains проще использовать, чем императивный. Все сложные задачи и низкоуровневые операции реализованы в библиотечных функциях, и нам больше не нужно беспокоиться об этих деталях. Для простоты нам также следует использовать декларативное программирование. Неизменяемость и декларативное программирование — суть функционального программирования, и теперь Java наконец делает это реальностью.
Продвигайте неизменность
Код с изменяемыми переменными будет иметь множество путей действия. Чем больше вещей вы меняете, тем легче разрушить исходную структуру и внести больше ошибок. Код с несколькими изменяемыми переменными сложен для понимания и распараллеливания. Неизменяемость по существу устраняет эти опасения. Java поддерживает неизменяемость, но не требует ее — но мы можем. Нам нужно изменить старую привычку изменять состояние объекта. Нам следует как можно чаще использовать неизменяемые объекты. При объявлении переменных, членов и параметров старайтесь объявлять их окончательными, как в знаменитом высказывании Джошуа Блоха в «Эффективной Java»: «Относитесь к объектам как к неизменяемым». При создании объектов старайтесь создавать неизменяемые объекты, например String. При создании коллекции попробуйте создать неизменяемую или неизменяемую коллекцию, например, используя такие методы, как Arrays.asList() и unmodifyingList() коллекции. Избегая изменчивости, мы можем писать чистые функции, то есть функции без побочных эффектов.
избежать побочных эффектов
Предположим, вы пишете фрагмент кода, который получает цену акции из Интернета и записывает ее в общую переменную. Если нам нужно получить много цен, нам придется выполнять эти трудоемкие операции последовательно. Если мы хотим воспользоваться преимуществами многопоточности, нам придется иметь дело с трудностями, связанными с многопоточностью и синхронизацией, чтобы предотвратить условия гонки. Конечным результатом является то, что производительность программы очень низкая, и люди забывают есть и спать, чтобы поддерживать поток. Если бы побочные эффекты были устранены, мы могли бы полностью избежать этих проблем. Функция без побочных эффектов обеспечивает неизменность и не изменяет какие-либо входные данные или что-либо еще в пределах ее области действия. Функции такого типа легко читаются, имеют мало ошибок и легко оптимизируются. Поскольку побочных эффектов нет, нет необходимости беспокоиться о гонках или одновременных модификациях. Мало того, мы можем легко выполнять эти функции параллельно, о чем мы поговорим на странице 145.
Предпочитаю выражения
Заявления — это горячая картошка, потому что они требуют внесения изменений. Выражения способствуют неизменности и композиции функций. Например, мы сначала используем оператор for для расчета общей цены после скидок. Такой код приводит к вариативности и многословию кода. Использование более выразительных, декларативных версий методов карты и суммы не только позволяет избежать операций модификации, но также позволяет объединять функции в цепочку. При написании кода следует стараться использовать выражения вместо операторов. Это делает код проще и понятнее. Код будет выполняться согласно бизнес-логике, как и тогда, когда мы описывали проблему. Краткую версию, несомненно, легче модифицировать в случае изменения требований.
Проектирование с использованием функций высшего порядка
Java не обеспечивает неизменность, как функциональные языки, такие как Haskell, но позволяет нам изменять переменные. Таким образом, Java не является и никогда не будет чисто функциональным языком программирования. Однако мы можем использовать функции более высокого порядка для функционального программирования на Java. Функции высшего порядка выводят повторное использование на новый уровень. Благодаря функциям высокого порядка мы можем легко повторно использовать зрелый код, который является небольшим, специализированным и очень связным. В ООП мы привыкли передавать объекты методам, создавать в методах новые объекты и затем возвращать объекты. Функции высшего порядка делают с функциями то же самое, что методы с объектами. С функциями более высокого порядка мы можем.
1. Передача функции в функцию
2. Создайте новую функцию внутри функции.
3. Возврат функций внутри функций
Мы уже видели пример передачи параметров из функции в другую функцию, а позже увидим примеры создания и возврата функций. Давайте еще раз посмотрим на пример «передачи параметров в функцию»:
Скопируйте код кода следующим образом:
цены.поток()
.filter(price -> цена.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> цена.multiply(BigDecimal.valueOf(0.9)))
сообщить об ошибке • обсудить
.reduce(BigDecimal.ZERO, BigDecimal::add);
В этом коде мы передаем функцию Price -> Price.multiply(BigDecimal.valueOf(0.9)) в функцию карты. Переданная функция создается при вызове карты функции более высокого порядка. Вообще говоря, функция имеет тело функции, имя функции, список параметров и возвращаемое значение. Эта функция, созданная на лету, имеет список параметров, за которым следует стрелка (->), а затем короткое тело функции. Типы параметров определяются компилятором Java, а тип возвращаемого значения также является неявным. Это анонимная функция, у нее нет имени. Но мы не называем это анонимной функцией, мы называем это лямбда-выражением. Передача анонимных функций в качестве параметров не является чем-то новым в Java. Раньше мы часто передавали анонимные внутренние классы. Даже если анонимный класс имеет только один метод, нам все равно придется пройти ритуал создания класса и создания его экземпляра. С помощью лямбда-выражений мы можем наслаждаться упрощенным синтаксисом. Мало того, мы всегда привыкли абстрагировать некоторые понятия в различные объекты, но теперь мы можем абстрагировать некоторые виды поведения в лямбда-выражения. Программирование с использованием этого стиля кодирования все еще требует некоторого размышления. Нам необходимо преобразовать наше уже укоренившееся императивное мышление в функциональное мышление. Поначалу это может быть немного болезненно, но вскоре вы к этому привыкнете. По мере дальнейшего углубления эти нефункциональные API постепенно останутся позади. Давайте сначала остановим эту тему. Давайте посмотрим, как Java обрабатывает лямбда-выражения. Раньше мы всегда передавали объекты методам, теперь мы можем хранить функции и передавать их. Давайте взглянем на секрет способности Java принимать функции в качестве параметров.
Раздел 5: Добавлен синтаксический сахар.
Этого также можно добиться, используя оригинальные функции Java, но лямбда-выражения добавляют некоторый синтаксический сахар, экономя некоторые шаги и упрощая нашу работу. Код, написанный таким образом, не только быстрее развивается, но и лучше выражает наши идеи. Многие интерфейсы, которые мы использовали в прошлом, имели только один метод: Runnable, Callable и т. д. Эти интерфейсы можно найти повсюду в библиотеке JDK, и там, где они используются, их обычно можно реализовать с помощью функции. Библиотечные функции, которым раньше требовался только интерфейс с одним методом, теперь могут передавать облегченные функции благодаря синтаксическому сахару, обеспечиваемому функциональными интерфейсами. Функциональный интерфейс — это интерфейс только с одним абстрактным методом. Посмотрите на те интерфейсы, у которых есть только один метод: Runnable, Callable и т. д., это определение применимо к ним. В JDK8 таких интерфейсов больше — Функция, Предикат, Потребитель, Поставщик и т. д. (более подробный список интерфейсов приведен на стр. 157 Приложения 1). Функциональные интерфейсы могут иметь несколько статических методов и методов по умолчанию, которые реализованы в интерфейсе. Мы можем использовать аннотацию @FunctionalInterface для аннотации функционального интерфейса. Компилятор не использует эту аннотацию, но может более четко определить тип этого интерфейса. Мало того, что если мы аннотируем интерфейс с этой аннотацией, компилятор насильственно проверит, соответствует ли он правилам функциональных интерфейсов. Если метод получает функциональный интерфейс в качестве параметра, параметры, которые мы можем передать, включают:
1. Анонимные внутренние классы, самый старый путь
2. выражение Lambda, как мы это сделали в методе карты
3. Ссылка на метод или конструктор (мы поговорим об этом позже)
Если параметр метода является функциональным интерфейсом, компилятор с радостью примет выражение Lambda или ссылку на метод в качестве параметра. Если мы передаем выражение лямбды методу, компилятор сначала преобразует выражение в экземпляр соответствующего функционального интерфейса. Это преобразование больше, чем просто генерирует внутренний класс. Методы этого синхронно сгенерированного экземпляра соответствуют абстрактным методам функционального интерфейса параметра. Например, метод карты получает функцию функционального интерфейса в качестве параметра. При вызове метода карты компилятор Java будет генерировать его синхронно, как показано на рисунке ниже.
Параметры выражения лямбда должны соответствовать параметрам абстрактного метода интерфейса. Этот сгенерированный метод вернет результат выражения Lambda. Если тип возврата непосредственно не соответствует абстрактному методу, этот метод будет преобразовать возвращаемое значение в соответствующий тип. У нас уже был обзор того, как выражения Lambda передаются методам. Давайте быстро рассмотрим, о чем мы только что говорили, а затем начнем исследование выражений Lambda.
Подвести итог
Это совершенно новая область Java. Благодаря функциям высшего порядка мы теперь можем написать элегантный и беглый код функционального стиля. Код, написанный таким образом, является кратким и простым для понимания, имеет мало ошибок и способствует обслуживанию и параллелизации. Компилятор Java работает своей магией, и где мы получаем параметры функционального интерфейса, мы можем пройти в Lambda Expressions или ссылки на методы. Теперь мы можем войти в мир выражений Lambda и библиотеки JDK, адаптированные для них, чтобы почувствовать их удовольствие. В следующей главе мы начнем с наиболее распространенных операций по программированию и раскрыть силу выражений Lambda.