Чтобы отпраздновать выпуск Java 8, который был выпущен всего несколько минут назад, я публикую черновую версию моего учебника по Java 8 Lambdas. Это хороший визуальный способ изучения API Streams, который поможет вам начать использовать лямбды в своих собственных приложениях с первого дня. Эта статья должна появиться в следующем выпуске Java Magazine, так что, пожалуйста, с нетерпением ждем финальной версии. версия, и я сделаю все возможное, чтобы включить комментарии и отзывы, если они соответствуют сроку публикации.
У Мэри была маленькая лямбда
Java-лямбды — это наиболее эффективная функция для ввода языка Java с момента выпуска обобщений в Java 5. Она в корне меняет модель программирования, допускает функциональный стиль разработки и поддерживает эффективное распараллеливание кода для использования преимуществ многоядерных систем. Хотя, как разработчик Java, вы сначала заметите улучшения производительности, которые вы получаете, используя новые лямбда-API-интерфейсы в Java 8.
В этой статье мы познакомим вас с новым API-интерфейсом Streams для работы с коллекциями и данными с использованием ретро-игры, написанной на JavaFX. Эта игра представляет собой простое приложение на Java 8, написанное с нуля для демонстрации лучших практик лямбда-программ, а также наглядное руководство по программированию с помощью Streams API. Тем не менее, мы сначала заложим основу с введением изменений языка лямбда-выражения.
Введение в Lambdas
Чтобы использовать лямбды, вы должны использовать последний Java SDK (8 или выше) и установить уровень языка Java 8 при компиляции. Вы можете скачать последнюю версию Java SDK с:
Разрабатывать лямбда-выражения намного проще, если использовать IDE, поддерживающую новый синтаксис. Большинство Java IDE были обновлены с поддержкой лямбд, и они помогут вам в режиме реального времени сообщать об ошибках и дополнять код лямбдами. NetBeans и IntelliJ заслуживают внимания как имеющие лучшую поддержку лямбда из коробки на момент выпуска Java 8, и оба хорошо работают с примером, который мы демонстрируем здесь.
Чтобы продемонстрировать, как работает новая функция lambdas, вот небольшой фрагмент кода, который перебирает список фигур и меняет синий цвет на красный:
1
2
3
4
|
for (Shape s : shapes) { if (s.getColor() == BLUE) s.setColor(RED); } |
В Java 8 вы можете переписать один и тот же код, используя forEach и лямбда-выражение следующим образом:
1
2
3
4
|
shapes.forEach(s -> { if (s.getColor() == BLUE) s.setColor(RED); }); |
Лямбда-форма использует новый метод интерфейса Collection, называемый forEach, который принимает лямбда-выражение и оценивает его для всех содержащихся в нем элементов. Подобные усовершенствования API были сделаны во всех основных классах Java, чтобы упростить использование лямбда-выражений.
У вас может возникнуть вопрос, как команда Java может добавлять новые методы в интерфейсы, не нарушая обратную совместимость. Например, если у вас есть код, который реализует интерфейс Collection и не определен метод forEach, то не нарушит ли обновление до Java 8 вашу реализацию? К счастью, другая функция, называемая методами расширения, решает эту проблему в Java 8. Реализация forEach в интерфейсе Collection показана в следующем листинге кода:
1
2
3
4
5
6
7
8
|
interface Collection<T> { default void forEach(Block<T> action) { Objects.requireNonNull(action); for (T t : this ) action.apply(t); } // Rest of Collection methods… } |
Обратите внимание на новое ключевое слово по умолчанию, которое указывает, что за методом последует реализация по умолчанию. Подклассы могут создавать свои собственные реализации метода, но если они не определены, они получат такое же стандартное поведение, как определено в интерфейсе. Это позволяет добавлять новые методы к существующим интерфейсам в основных классах Java, а также в ваши собственные библиотеки и проекты.
Фактический лямбда-синтаксис довольно прост … в его полной форме вы указываете типы и параметры слева, ставите тире, знак «больше, чем» [->] в середине, и следите за ним с помощью тела метода в фигурных скобках:
1
|
( int a, int b) -> { return a + b; } |
В случае, когда функция возвращает значение, это можно упростить, удалив фигурные скобки, ключевое слово return и точку с запятой:
1
|
(a, b) -> a + b |
Кроме того, в случае, когда есть только один параметр, вы можете оставить скобки:
1
|
a -> a * a |
И, наконец, если у вас нет параметров, вы можете просто оставить круглые скобки пустыми, что характерно для замены реализаций Runnable или других методов без параметров:
1
|
() -> { System.out.println( "done" ); } |
В дополнение к базовому синтаксису существует также специальный синтаксис «Ссылки на метод», который позволяет быстро создавать лямбда-выражения, которые ссылаются на один метод в качестве реализации. В следующей таблице приведены различные типы ссылок на методы вместе с эквивалентным длинным лямбда-синтаксисом.
Справочник по методам | Лямбда-эквивалент | |
Объекты :: ToString | obj -> Objects.toString (obj) | Ссылка на статический метод |
Объект :: ToString | obj -> obj.toString () | Ссылка на метод члена |
OBJ :: ToString | () -> obj.toString () | Ссылка на метод объекта |
Объект :: новый | () -> новый объект () | Справочник по методам конструктора |
Последнее, что важно при работе с новыми методами лямбда-выражений, — это создание интерфейсов, которые позволяют принимать лямбда-выражения. Для этой цели любой интерфейс, который имеет один явно объявленный абстрактный метод, может использоваться для принятия лямбда-выражения и поэтому называется функциональным интерфейсом.
Для удобства они ввели новую аннотацию FunctionalInterface, которая при желании может использоваться для маркировки интерфейсов, чтобы получить помощь от компилятора при проверке, чтобы убедиться, что ваш интерфейс удовлетворяет одному явно объявленному требованию абстрактного метода:
1
2
3
4
|
@FunctionalInterface interface Sum { int add( int a, int b); } |
Это рекомендуемая лучшая практика, потому что она будет выявлять угловые случаи в определении функциональных интерфейсов, таких как включение методов по умолчанию, которые позволяют иметь несколько методов, определенных в функциональном интерфейсе, поскольку они не являются абстрактными и не учитываются в требование единого абстрактного метода.
Теперь, когда у вас есть общее представление о лямбда-синтаксисе, пришло время изучить API потоков и показать мощь лямбда-выражений в контексте наглядного примера.
Ретро-игры с лямбдами
У Мэри была маленькая лямбда
Чье руно было белым, как снег
И везде, куда ходила Мария
Лямбда наверняка поехала!
В настоящее время видеоигры — это 3D-графика с высоким разрешением, кинематографические качественные сцены и уровни сложности от новичка до пацифиста. Однако в старые добрые времена игр у нас были спрайты… милые, неровные маленькие фигурки, танцующие и играющие в RPG, проходящие через хорошо продуманные и безумно сложные уровни.
Графика, основанная на спрайтах, также очень проста в программировании, что позволяет нам создать полную систему анимации, содержащую менее 400 строк кода. Полный код приложения находится в GitHub по следующему адресу:
Для всей графики, используемой в игре, изображения располагаются в стандартном плиточном формате 3 × 4, как показано на соседнем спрайт-листе для Мэри. Код для анимации спрайтов выполняется (конечно) с использованием лямбды, и он просто перемещает область просмотра вокруг мозаичного изображения, чтобы создать 3-кадровую анимацию ходьбы [по горизонтали] и изменить направление, в котором персонаж смотрит [по вертикали].
1
2
3
4
5
6
7
|
ChangeListener<Object> updateImage = (ov, o, o2) -> imageView.setViewport( new Rectangle2D(frame.get() * spriteWidth, direction.get().getOffset() * spriteHeight, spriteWidth, spriteHeight)); direction.addListener(updateImage); frame.addListener(updateImage); |
Добавьте статическое изображение для фона и несколько ключевых слушателей событий для перемещения персонажа при вводе, и у вас есть основы классической RPG-игры!
Генерация потоков
Есть несколько способов создать новый поток Java 8. Самый простой способ — начать с выбранной вами коллекции и просто вызвать методы stream () или parallelStream (), чтобы получить объект Stream, например, в следующем фрагменте кода:
1
|
anyCollection.stream(); |
Вы также можете вернуть поток из известного набора объектов, используя статические вспомогательные методы класса Stream. Например, чтобы вернуть поток, содержащий набор строк, вы можете использовать следующий код:
1
|
Stream.of( "bananas" , "oranges" , "apples" ); |
Точно так же вы можете использовать числовые подклассы Stream, такие как IntStream, чтобы вернуть сгенерированный ряд чисел:
1
|
IntStream.range( 0 , 50 ) |
Но самый интересный способ создания новой серии — это использовать методы generate и iterate в классе Stream. Они позволяют вам создавать новый поток объектов, используя лямбду, которая вызывается для возврата нового объекта. Метод итерации особенно интересен, потому что он передает ранее созданный объект в лямбду. Это позволяет вам возвращать отдельный объект для каждого вызова, например, итеративно возвращать все цвета радуги:
1
2
3
|
Stream.iterate(Color.RED, c -> Color.hsb(c.getHue() + . 1 , c.getSaturation(), c.getBrightness())); |
Чтобы продемонстрировать, как это работает визуально, мы добавим новый элемент в приложение, которое генерирует овец, когда мы наступаем на него.
Код для нового класса Barn выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public static class Barn extends MapObject { static final Image BARN = loadImage( "images/barn.png" ); public Barn(Main.Location loc) { super (BARN, loc); } @Override public void visit(Shepherd s) { SpriteView tail = s.getAnimals().isEmpty() ? s : s.getAnimals().get(s.getAnimals().size() - 1 ); Stream.iterate(tail, SpriteView.Lamb:: new ) .skip( 1 ).limit( 7 ) .forEach(s.getAnimals()::add); } } |
Этот код задает изображение, которое будет использоваться для графики на основе спрайтов, которое передается в супер-конструктор, и реализует метод посещения, который имеет логику, которая будет выполняться, когда Мэри наступит на Барн.
Первое утверждение в методе посещения просто получает последний элемент из списка животных, следующих за Мэри, или возвращает ее, если животных еще нет. Затем он используется в качестве начального числа для метода итерации, который передается конструктору Lamb для первого вызова лямбды. Агнец, сгенерированный этим, затем передается конструктору Lamb для второго вызова, и это повторяется по очереди.
Результирующий поток включает начальное число, поэтому мы можем использовать функцию пропуска, чтобы удалить его из потока, и оно теоретически бесконечно. Поскольку потоки ленивы, нам не нужно беспокоиться о создании объектов до тех пор, пока мы не добавим терминальную операцию, но простой способ зафиксировать длину потока — это использовать функцию limit, которой мы дадим параметр от 7 до произведите семь овец вслед за Марией. Последний шаг — добавить терминальную операцию, которая будет использовать поток. В этом случае мы будем использовать функцию forEach с лямбда-выражением, для которого установлена ссылка на метод для добавления метода в список животных. Результатом выполнения этой лямбды является добавление семерых ягнят после Марии подряд:
Следующий элемент, который мы собираемся добавить в игру, — это радуга, демонстрирующая фильтрацию в Streams API. Функция фильтра работает так, что она принимает предикат лямбда, который оценивается как true или false для каждого элемента в потоке. Результирующий поток содержит все элементы, для которых предикат лямбда оценивается как true.
Для логики радуги мы выполним фильтр, который возвращает каждое 4- е животное в потоке, и применим функцию JavaFX ColorAdjust, чтобы сместить оттенок в соответствии с переданным цветом. Для белого мы используем ноль (без смещения цвета). Следующий код является реализацией метода посещения для радужного MapObject:
01
02
03
04
05
06
07
08
09
10
11
12
|
s.getAnimals().stream() .filter(a -> a.getNumber() % 4 == 1 ) .forEach(a -> a.setColor( null )); s.getAnimals().stream() .filter(a -> a.getNumber() % 4 == 2 ) .forEach(a -> a.setColor(Color.YELLOW)); s.getAnimals().stream() .filter(a -> a.getNumber() % 4 == 3 ) .forEach(a -> a.setColor(Color.CYAN)); s.getAnimals().stream() .filter(a -> a.getNumber() % 4 == 0 ) .forEach(a -> a.setColor(Color.GREEN)); |
А когда Мария наступает на радугу, все ягнята окрашиваются в соответствии с указанными вами значениями цвета:
«Агнец» да Вопрос 1. Что произойдет, если вы наступите на сарай после посещения радуги?
Другой способ использования фильтрации — это использование новых методов, добавленных в API-интерфейс Collection, которые принимают лямбда-предикат. К ним относится removeIf, который отфильтровывает все элементы, которые не соответствуют данному предикату, и фильтруется, который находится в ObservableList и возвращает обратно FilteredList, содержащий только элементы, которые соответствуют предикату.
Мы будем использовать их для реализации церковного объекта, который будет фильтровать «чистых» животных. Любые животные белого цвета будут готовить церковный персонал для кормления нуждающихся. Это включает в себя увеличение счетчика «Подача еды» на знак и удаление «чистых» животных из списка. Код метода посещения церкви показан ниже.
1
2
3
4
5
6
7
8
|
Predicate<SpriteView> pure = a -> a.getColor() == null ; mealsServed.set(mealsServed.get() + s.getAnimals().filtered(pure).size() ); s.getAnimals().removeIf(pure); |
На следующем снимке экрана вы можете увидеть результат последовательного наступления на радугу и церковь.
«Агнец» да Вопрос 2. Можно ли использовать церковь, чтобы убрать всех животных после того, как они уже покрашены?
Вероятно, самая мощная операция в Streams API — это функция map. Это позволяет вам преобразовывать все элементы в потоке из одного типа объекта в другой, выполняя мощные преобразования по пути. Мы будем использовать это для создания курятника, где все животные, следующие за Марией, будут превращены в яйца.
У меня есть две реализации метода посещения курятника. Первый использует одну операцию сопоставления с лямбда-выражением, чтобы заменить элементы потока яйцами, как показано здесь:
1
2
3
4
5
|
// single map: s.getAnimals().setAll(s.getAnimals() .stream() .map(sv -> new Eggs(sv.getFollowing()) ).collect(Collectors.toList())); |
Вторая реализация использует ссылки на методы со связанным набором операций карты, чтобы сначала преобразовать поток в поток, за которым следуют животные, а затем вызвать ссылку на метод конструктора для создания яиц, передав следующую информацию параметру конструктора :
1
2
3
4
5
6
7
|
// or a double map: s.getAnimals().setAll(s.getAnimals() .stream().parallel() .map(SpriteView::getFollowing) .map(Eggs:: new ) .collect(Collectors.toList()) ); |
Оба этих фрагмента кода ведут себя и работают одинаково, поскольку потоковый API разработан так, чтобы быть ленивым и оценивать поток только при вызове терминальной операции (такой как сбор). Так что это в первую очередь вопрос стиля, который вы предпочитаете использовать. Запуск программы с новым курятником MapObject позволит вам генерировать яйца из ягнят, как показано на следующем рисунке:
«Ягненок» да Вопрос 3: Если вы посылаете цветных ягнят в курятник, какого цвета яйца?
Обратите внимание, что каждый из спрайтов содержит три маленьких прыгающих яйца. Разве не было бы хорошо, если бы мы могли вывести этих парней на цыплят?
Чтобы вылупить яйца, мы добавим новый MapObject для гнезда, где яйца будут вылуплены в группу из трех кур, используя следующий метод вывода:
1
2
3
4
5
6
|
public static Stream<SpriteView> hatch(SpriteView sv) { if (!(sv instanceof Eggs)) { return Stream.of(sv); } return Stream.iterate(sv, Chicken:: new ).skip( 1 ).limit( 3 ); } |
Обратите внимание, что этот метод возвращает поток объектов, что означает, что если бы мы использовали операцию отображения нормалей, мы бы получили поток потоков. Чтобы объединить Stream в единый список цыплят, мы можем вместо этого использовать flatMap, который будет сопоставлять поток с помощью лямбда-функции, а также сворачивать вложенные потоки в один список объектов. Реализация функции посещения гнезда с использованием flatMap показана ниже:
1
2
3
4
5
|
s.getAnimals().setAll(s.getAnimals() .stream().parallel() .flatMap(SpriteView.Eggs::hatch) .collect(Collectors.toList()) ); |
Теперь, доставив яйца в гнездо, вы получите взрыв кур, как показано на следующем снимке экрана:
«Агнец» да Вопрос 4. Приблизительно, сколько животных вы можете добавить до того, как в игре закончится память?
Последний элемент, который мы добавим, — это лиса, демонстрирующая, как уменьшить поток. Для этого мы сначала отобразим поток в список целых чисел в соответствии с масштабом животных, а затем уменьшим его, используя ссылку на метод суммирования, до единого значения. Функция Reduce принимает начальное значение (для которого мы будем использовать 0) и функцию, которая может свести два элемента в один результат. Эта лямбда будет применяться рекурсивно для всех элементов в потоке, пока не будет получено единственное значение, которое будет суммой всех весов животных.
1
2
3
4
5
6
7
8
|
Double mealSize = shepherd.getAnimals() .stream() .map(SpriteView::getScaleX) .reduce( 0.0 , Double::sum); setScaleX(getScaleX() + mealSize * . 2 ); setScaleY(getScaleY() + mealSize * . 2 ); shepherd.getAnimals().clear(); |
Затем мы берем сумму (сохраненную в переменную под названием foodSize) и используем ее для пропорционального растяжения лисы. Результат очень вкусной еды для лисы можно увидеть на следующем рисунке:
«Агнец» да Вопрос 5: Как вы можете изменить код для Фокса, чтобы он стал толстее, когда он ест?
В этой статье мы рассмотрели основной лямбда-синтаксис, включая ссылки на методы, методы расширения и функциональные интерфейсы. Затем мы подробно остановились на API-интерфейсе Streams, демонстрируя некоторые из общих операций, таких как итерация, фильтрация, отображение, flatMap и уменьшение. Как вы уже видели, лямбда-коды Java 8 значительно смещают модель программирования, позволяя вам писать более простой и элегантный код, и открывают возможности новых мощных API, таких как Streams. Теперь пришло время начать использовать эти возможности в своей собственной разработке.
Ссылка: | Выпущена Java 8! — Lambdas Tutorial от нашего партнера JCG Стивена Чина в блоге Steve On Java . |