В данной статье я хотел бы рассказать о новой функции появившейся в Java 8 – лямбдах. Я бы даже не сказал, что это такое уж мега-новшество, скорее просто удобная запись. Но, тем не менее, я считаю, что это штука полезная и есть смысл ее использовать. И уж тем более понимать, ведь даже если ее не используете Вы, ее может использовать кто-то другой в том же проекте, и тут уж придется разбираться. Так что начнем.






Мотивация.



Начнем, пожалуй, с описания проблемы которую лямбды ползволяют решать. Поскольку Java язык строго типизированный, для передачи стратегий используются анонимные классы, зачастую реализующие простые одно-двух методные интерфейсы. Примером тому является интерфейс Comparator содержащий два метода compare и equals. Данный интерфейс зачастую используется для различных сортировок, то есть метод sort принимает на вход массив и класс реализующий интерфейс Comparator. В таких ситуациях обычно используют анонимные классы, поскольку далее в программе он редко бывает нужен. Таким образом данный класс – просто обертка для двух методов, поскольку подругому Java передавать методы не умела. Честно говоря, для передачи одного метода(ну или двух) создание целого класса – несколько тяжеловесная операция.

Чаще всего подобная задача возникает при необходимости сделать какую-либо выборку, сортировку либо модификацию для представителей соответствующих некоторому критерию.

Рассмотрим, в качестве примера, сервис, в котором у пользователей есть профили. Профиль представлен классом Profile:
public class Profile {

public enum Gender {
MALE, FEMALE
}

String name;
Date birthdate;
Gender gender;
String emailAddress;

public int getAge() {
// ...
}

public void visualizeProfile() {
// ...
}
}

К примеру, профили пользователей хранятся в списке List.

Рассмотрим возможность выборки из данного списка по определенному критерию и сравним насколько удобены подход с введением дополнительного метода для поиска, подход с анонимным или локальным вложенным классом реализующим стратегию и подход с лямбдой.

Подход №1: Создание метода для поиска профилей пользователей, соотвествующих одному критерию



Простейший подход – объявить необходимое число методов, по одному для каждого поискового критерия. То есть метод, может выбирать пользователей по полу, возрасту, имени и так далее. Следующий метод отобразит все профили пользователей, возраст которых не привышает заданный:
public static void visualizeProfilesNotOlderThan(List<Profile> profiles, int age) {
for (Profile p : profiles) {
if (p.getAge() <= age) {
p.visualizeProfile();
}
}
}

Подобный подход не слишком-то расширяемый. Скажем если возраст необходимо будет измерять не в целых числах, а в числах с плавающей точкой, то это означает, что необходимо будет поменять API, что, зачастую, нежелательно и приводит можеству проблем, как то обратная совместимость и прочее. Кроме того, данный подход, достаточно ограничен, если например, необходимо несколько изменить условие выборки – нет никакой возможности это сделать, кроме как добавить новую функцию. Так например, если необходимо выбрать все профили возраст владельца которого строго больше указанного.

Подход №2: Создание обобщенного поискового метода



Следующий метод является обобщением метода visualizeProfilesNotOlderThan, он выводит профили в пределах некоторого интервала возростов, включая или исключая концы интервала в зависимости от последнего параметра:
public static void visualizeProfilesWithinAgeRange(
List<Profile> profiles, int start, int end, boolean strict) {
for (Profile p : profiles) {
if ((low <= p.getAge() && p.getAge() <= high && !strict) || (low < p.getAge() && p.getAge() < high && strict)) {
p.visualizeProfile();
}
}
}


Но что если нужно сформировать выборку по полу или по комбинации пола и возроста. Попытка реализовать это в пределах данного подхода приводит к комбинаторному росту количества методов, что безусловно, усложняет как сам класс так и добавление в него новых полей. Таким образом, хотя данный метод и более общий, первая из приведенных выше проблем остается. Решение ее заключается в отделении поискового механизма от критерия поиска. Традиционный подход к этому – оборачивание критерия в класс-стратегию.

Подход №3: Оборачивание кода критерия в класс



Следующий метод не содержит кода характеризующего критерий поиска:
public static void visualizeProfilesThatMacth(
List<Profile> profiles, CheckProfile tester) {
for (Profile p : profiles) {
if (tester.test(p)) {
p.visualizeProfile();
}
}
}

Поисковые критерии должны реализовывать интерфейс CheckProfile:
interface CheckProfile {
boolean test(Profile p);
}

Пример реализации интерфейса CheckProfile, критерий поиска – люди подходящие под призыв в армию(не ждали?): то есть мужчины в возрасте от 18 до 25:
class CheckProfileArmyService implements CheckProfile {
public boolean test(Profile p) {
return p.gender == Profile.Gender.MALE &&
p.getAge() >= 18 &&
p.getAge() < 25;
}
}

Для использования этого класса необходимо создать новый экземпляр класса-критерия и передать его в метод visualizeProfilesThatMacth method:
visualizeProfilesThatMacth(profiles, new CheckProfileArmyService());

Хотя этот подход значительно более гибкий, хотелось бы все же избежать объявления дополнительных классов которые впоследствии не будут использоваться. Сделать это можно применив анонимные классы.

Подход №4: Оборачивание кода критерия в анонимный класс



В данной ситуации второй аргумент – анонимный класс реализующий интерфейс CheckProfile:
visualizeProfilesThatMacth(
profiles,
new CheckProfile() {
public boolean test(Profile p) {
return p.getGender() == Profile.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() < 25;
}
}
);

В данном случаи объем кода уменьшается, поскольку не требуется описывать новый класс для кажого необходимого критерия. Однако снижается и повторное использование кода, в случаи если один и тот же критерий используется несколько раз его лучше описать как класс. Синтаксис описания анонимных классов довольно громозткий, учитывая что интерфейс CheckProfile содержит только один метод. Тут нам, как раз и пригодятся, лямбды.

Подход №5: Указание поискового критерия в лямбда-выражении



Интерфейс CheckProfile - функциональный интерфейс, то есть содержащий только один метод. Потому для такого интерфейса имя метода, при реализации, может быть опущено. Для этого, вместо анонимного класса можно использовать лямбда-выражение:
visualizeProfilesThatMacth (
profiles,
(Profile p) -> p.getGender() == Profile.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() < 25
);

Мы используем стандартный функциональный интерфейс вместо интерфейса CheckProfile тем самым дополнительно уменьшая количество кода.

Подход №6: Использование стандартного функционального интерфейса с лямбда-выражением



Вернемся еще раз к интерфейсу CheckProfile:
interface CheckProfile {
boolean test(Profile p);
}


Данный интерфейс очень простой, он, как уже было замечено, функциональный, то есть содержит всего один метод. Этот метод принимает один параметр и возвращает буленовское значение. Интерфейс CheckProfile можно заменить на один из страндартных функциональных интерфейсов определенных в пакете java.util.function. Таким образом, можно избавится от необходимости определять интерфейс, и тем самым снова уменьшить объем написанного кода.

Вместо CheckProfile может быть использован стандартный функциональный интерфейс Predicate. Этот интерфейс содержит один метод boolean test(T t):
interface Predicate<T> {
boolean test(T t);
}


Так будет выглядеть метод, если использовать интерфейс Predicate:
public void visualizeProfilesThatMacthPredicate(
List<Profile> profiles, Predicate<Profile> tester) {
for (Profile p : profiles) {
if (tester.test(p)) {
p.visualizeProfile();
}
}
}

Теперь используем лямбда-выражение для передачи стратегии выбора:
visualizeProfilesThatMacthPredicate(
profiles,
p -> p.getGender() == Profile.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() < 25
);

В целом, мы можем еще обобщить метод visualizeProfilesThatMacthPredicate с помощью стандартных функциональных интерфейсов и лямбда-выражений. Об этом далее.

Подход №7: Использование лямбда-выражений в приложении



Вернемся еще раз к методу visualizeProfilesThatMacthPredicate, и выясним где еще могли бы пригодится лямбда-выражения:
public void visualizeProfilesThatMacthPredicate(
List<Profile> profiles, Predicate<Profile> tester) {
for (Profile p : profiles) {
if (tester.test(p)) {
p.visualizeProfile();
}
}
}

В этом методе вызывается метод visualizeProfile экземпляров класса Profile. Можно заменить этот вызов на лямбда-выражение. В данном случаи, в качестве функционального интерфейсам можно взять интерфейс Consumer, содержащий единственный метод void accept(T t). В результате получим:
public void processProfiles(
List<Profile> profiles,
Predicate<Profile> tester,
Consumer<Profile> processor) {
for (Profile p : profiles) {
if (tester.test(p)) {
processor.accept(p);
}
}
}

В результате вызов этого обобщенного метода будет выглядеть так:
processProfiles (
profiles,
p -> p.getGender() == Profile.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() < 25,
p -> p.visualizeProfile()
);

Что если в результате процессинга необходимо получить какие-то данные? В таком случаи необходимо просто использовать соответствующий функциональный интерфейс Function содержащий метод R apply(T t). Вот пример его использования в рамках описанной выше задачи:
public static List<String> processProfilesWithFunction(
List<Profile> profiles,
Predicate<Profile> tester,
Function<Profile, String> processor{
List<String> resultSet = new ArrayList<String>();
for (Profile p: profiles) {
if (tester.test(p)) {
resultSet.add(processor.apply(p));
}
}
return resultSet;
}

Следующий метод вернет список адресов тех профилей, которые соответствуют поисковому критерию:
processProfilesWithFunction (
profiles,
p -> p.getGender() == Profile.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getAddress()
);


Лямбда-выражения в GUI приложениях



Зачастую, для обработки событий в GUI приложениях, таких как нажатие кнопки мыши или клавиши на клавиатуре, производится путем объявления некоторых обработчиков, реализующих заранее заданный интерфейс. Часто, интерфейсы обработчиков являются функциональными, поскольку предполагают определение всего одного метода – «обработать событие».

Таким образом большинство обработчиков в GUI приложениях выглядят следующим образом:
btn.onAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
System.out.println("Button pressed!");
}
});

Как уже было замечено ранее, применение лямбда-выражений значительно сокращает эту запись:
btn.onAction(
event -> System.out.println("Button pressed!")
);


Особенности применения лямбда-выражений



Синтаксис лямбда-выражений



Лямбда выражение состоит из следующих частей:
• Список входящих параметров, разделенных запятой(в случаи если параметров больше одного заключаются в скобки).
• Стрелка «->»
• Тело метода, состоящее из одного выражения или нескольких, заключенных в фигурные скобки. В случи указания единственного выражения, его значение будет вычислено и вернется в качестве результата выполнения метода.

Свойство замыкания лямбда-выражений



Лямбда-выражения имеют доступ к области видимости в которой они были объявленны. Кроме того, лямбда-выражения, в отличии от локальных и анонимных классов, не порождают собственной области видимости объекта, то есть, говоря простым языком, не переопределяют переменную this. С точки зрения областей видимости, лямбда является просто блоком кода.

Определение типа лямбда-выражения



Как определить тип лямбда-выражения? Вернемся к примеру:

p -> p.getGender() == Profile.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

Как Вы помните мы использовали одно и то же лямбда выражение в двух случаях:
public void visualizeProfiles(List<Profile> profiles, CheckProfile tester)

и
public void visualizeProfilesThatMacthPredicate(List<Profile> profiles, Predicate<Profile> tester)

Когда во время выполнения Java вызывает метод visualizeProfiles, он выполняется из расчета на тип CheckProfile, так что тип лямбда-выражения - CheckProfile. В случаи же когда вызывается метод visualizeProfilesThatMacthPredicate, ожидаемый тип данных - Predicate, по-этому, лямбда-выражение будет этого типа. Тип данных, который ожидают эти методы называется - целевым типом. Для определения типа лямбда-выражения компилятор Java использует целевой тип контекста или ситуации в которой применяется это выражение. Следовательно, лямбда-выражения можно использовать только в ситуациях, когда компилятор может опредилить целевой тип:

• Декларация переменных
• Присвоение
• Оператор возврата результата
• Инициализация массивов
• Аргумент вызова метода или конструктора
• Тело лямбда выражения
• Условный оператор(в том чиле ?:)
• Выражение привидения типов

Целевой тип для аргумента метода



Для аргументов методов компилятор Java опредиляет целевой тип, исходя из таких особенностей языка, как перегрузка методов и интерфейс типа аргумента.

Рассмотрим слудющие два функциональных интерфейса java.lang.Runnable и java.util.concurrent.Callable:
public interface Runnable {
void run();
}

public interface Callable<V> {
V call();
}

Метод Runnable.run не возвращает значения, а метод Callable.call – возвращает.

Предположим теперь, что у нас есть перегруженный метод(то есть у одного и того же класса есть два или более методов с одинаковым названием, но различными сигнатурами) invoke:
void invoke(Runnable r) {
r.run();
}

<T> T invoke(Callable<T> c) {
return c.call();
}

Какой метод будет вызван в следующем случаи?
String s = invoke(() -> "done");

Будет вызван метод invoke(Callable), поскольку этот метод возвращает значение, а invoke(Runnable) – не возвращает. Потому в данном случаи целевым типом лямбда-выражения () -> "done" будет Callable.

Использование существующих методов



Как уже было показано, лямбда-выражения используются для создания анонимных методов. Однако, иногда бывает удобно использовать лямбду для вызова уже существующего метода. В таких случаях, наиболее прозразчный подход – использовать имя существующего метода. Ссылки на методы позволяют делать это. Ссылки на методы – компактные лямбда-выражения, для именованных методов.

Рассмотрим опять класс Profile:
public class Profile {

public enum Gender {
MALE, FEMALE
}

String name;
Date birthday;
Gender gender;
String emailAddress;

public int getAge() {
// ...
}

public Calendar getBirthday() {
return birthday;
}

public static int compareByAge(Profile a, Profile b) {
return a.birthday.compareTo(b.birthday);
}
}

Предположим, что ваши профили содержатся в массиве, и необходимо отсортировать его по возрасту указанному в профиле. Можно использовать следующий код.
Profile[] profilesAsArray = profiles.toArray(new Profile[profiles.size()]);

class ProfileAgeComparator implements Comparator<Profile> {
public int compare(Profile a, Profile b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}

Arrays.sort(profilesAsArray, new ProfileAgeComparator());

В этом случаи сигнатура вызываемого метода sort будет следующей:
static <T> void sort(T[] a, Comparator<? super T> c)

Как мы уже знаем, класс ProfileAgeComparator – функциональный интерфейс. Соответственно, вместо объявления нового класса реализующего Comparator<Profilegt;, можно использовать лямбда-выражение:
Arrays.sort(profilesAsArray,
(Profile a, Profile b) -> {
return a.getBirthday().compareTo(b.getBirthday());
}
);

Но метод для сравнения профилей по возрасту уже определен в классе Profile. Можно вместо этого вызвать его в теле лямбды:
Arrays.sort(profilesAsArray,
(a, b) -> Profile.compareByAge(a, b)
);

Поскольку метод уже определен, то лямбда-выражение можно заменить на ссылку на метод:
Arrays.sort(profilesAsArray, Profile::compareByAge);

Ссылка на метод Profile::compareByAge семантически не отличается от лямбда-выражения (a, b) -> Profile.compareByAge(a, b), обе имеют следующие характеристики:
• Список их параметров соответствует методу интерфейса Comparator.compare, то есть (Profile, Profile).
• Тело выражения выполняет Profile.compareByAge.

Виды ссылок на методы



Существует четыре вида ссылок на методы:

• Ссылка на статический метод(ContainingClass::staticMethodName)
• Ссылка на метод конкретного объекта(ContainingObject::instanceMethodName)
• Ссылка на метод произвольного объекта конкретного типа(ContainingType::methodName)
• Ссылка на конструктор(ClassName::new)

Ссылка на статический метод



Ссылка на метод Profile::compareByAge является ссылкой на статический метод.

Ссылка на метод конкретного объекта



Следующий пример иллюстрирует применение ссылки на метод конкретного объекта:
class ComparisonProvider {
public int compareByName(Profile a, Profile b) {
return a.getName().compareTo(b.getName());
}

public int compareByAge(Profile a, Profile b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(profilesAsArray, myComparisonProvider::compareByName);

Ссылка на метод myComparisonProvider::compareByName вызывает метод compareByName относящийся к объекту myComparisonProvider. JRE делает вывод аргументов метода, в данном случаи (Profiles, Profiles).

Ссылка на метод произвольного объекта конкретного типа



Следующий пример иллюстрирует применение ссылки на метод произвольного объекта конкретного типа:
String[] stringArray = { "Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

Эквивалентным лямбда выражением для ссылки на метод String::compareToIgnoreCase было бы выражение принимающее формальные параметры (String a, String b), и вызывающее метод a.compareToIgnoreCase(b).

Ссылка на конструктор



Сослаться на конструктор можно точно тем же образом, что и на статический метод класса под названием new. Следующий метод копирует элементы из одной коллекции в другую:
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
DEST transferElements(
SOURCE sourceCollection,
Supplier<DEST> collectionFactory) {

DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}

Функциональный интерфейс Supplier содержит один метод get не принимающий аргументов и возвращающий объект. Следовательно, Вы можете вызвать метод transferElements со следующим лямбда выражением:
Set<Profile> profileSetLambda = transferElements(profiles, () -> { return new HashSet<>(); });

Если использовать ссылку на конструктор, то выглядеть это будет так:
Set<Profile> profileSet = transferElements(profile, HashSet::new);

Компилятор Java выводит, что Вы хотите создать экземпляр коллекции HashSet состоящей из элементов типа Profile. Можно так же записать это явно указывая тип элементов результата:
Set<Profiles> profileSet = transferElements(profile, HashSet<Profiles>::new);


Вобщем-то это все что я хотел рассказать про лямбды. Надеюсь это было полезно.
PS: итак, Java стала еще на шаг ближе к Groovy:).
3

Комментарии

Для того, чтоб оставлять комментарии или зарегистрируйтесь.