Java изначально полностью объектно-ориентированный язык. За исключением примитивных типов, все в Java – это объекты. Даже массивы являются объектами. Экземпляры каждого класса – объекты. Не существует ни единой возможности определить отдельно (вне класса –
прим. перев.) какую-нибудь функцию. И нет никакой возможности передать метод как аргумент или вернуть тело метода как результат другого метода. Все так. Но так было до Java 8.
![Lambda-выражения на примерах - 1]()
Со времен старого доброго Swing, надо было писать анонимные классы, когда нужно было передать некую функциональность в какой-нибудь метод. Например, так выглядело добавление обработчика событий:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
//Event listener implementation goes here...
}
});
Здесь мы хотим добавить некоторый код в слушатель событий от мыши. Мы определили анонимный класс
MouseAdapter
и сразу создали объект из него. Таким способом мы передали дополнительную функциональность в метод
addMouseListener
.
Короче говоря, не так-то просто передать простой метод (функциональность) в Java через аргументы. Это ограничение вынудило разработчиков Java 8 добавить в спецификацию языка такую возможность как Lambda-выражения.
Зачем яве Lambda-выражения?
С самого начала, язык Java особо не развивался, если не считать такие вещи как аннотации (Annotations), дженерики (Generics) и пр. В первую очередь, Java всегда оставался объектно-ориентированным. После работы с функциональными языками, такими как JavaScript, можно понять насколько Java строго объектно-ориентирован и строго типизирован. Функции в Java не нужны. Сами по себе их нельзя встретить в мире Java.
В функциональных языках программирования на первый план выходят функции. Они существуют сами по себе. Можно присваивать их переменным и передавать через аргументы другим функциям. JavaScript один из лучших примеров функциональных языков программирования. На просторах Интернета можно найти хорошие статьи, в которых детально описаны преимущества JavaScript как функционального языка. Функциональные языки имеют в своем арсенале такие мощные инструменты как замыкания (Closure), которые обеспечивают ряд преимуществ на традиционными способами написания приложений. Замыкание – это функция с привязанной к ней средой — таблицей, хранящей ссылки на все нелокальные переменные функции. В Java замыкания можно имитировать через Lambda-выражения. Безусловно между замыканиями и Lambda-выражениями есть отличия и не малые, но лямбда выражения являются хорошей альтернативой замыканиям.
В своем саркастичном и забавном блоге, Стив Иег (Steve Yegge) описывает насколько мир Java строго завязан на имена существительные (сущности, объекты –
прим. перев.). Если вы не читали его блог, рекомендую. Он забавно и интересно описывает точную причину того, почему в Java добавили Lambda-выражения.
Lambda-выражения привносят в Java функциональное звено, которого так давно не хватало. Lambda-выражения вносят в язык функциональность на равне с объектами. Хотя это и не на 100% верно, можно видеть, что Lambda-выражения не являясь замыканиями предоставляют схожие возможности. В функциональном языке lambda-выражения – это функции; но в Java, lambda-выражения – представляются объектами, и должны быть связаны с конкретным объектным типом, который называется функциональный интерфейс. Далее мы рассмотри, что он из себя представляет.
В статье Марио Фаско (Mario Fusco) “Зачем в Java нужны Lambda-выражения” (“Why we need Lambda Expression in Java”) подробно описано, зачем всем современным языкам нужны возможности замыканий.
Введение в Lambda-выражения
Lambda-выражения – это анонимные функции (может и не 100% верное определение для Java, но зато привносит некоторую ясность). Проще говоря, это метод без объявления, т.е. без модификаторов доступа, возвращающие значение и имя.
Короче говоря, они позволяют написать метод и сразу же использовать его. Особенно полезно в случае однократного вызова метода, т.к. сокращает время на объявление и написание метода без необходимости создавать класс.
Lambda-выражения в Java обычно имеют следующий синтаксис
(аргументы) -> (тело)
. Например:
(арг1, арг2...) -> { тело }
(тип1 арг1, тип2 арг2...) -> { тело }
Далее идет несколько примеров настоящих Lambda-выражений:
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
Структура Lambda-выражений
Давайте изучим структуру lambda-выражений:
- Lambda-выражения могут иметь от 0 и более входных параметров.
- Тип параметров можно указывать явно либо может быть получен из контекста. Например (
int a
) можно записать и так (a
)
- Параметры заключаются в круглые скобки и разделяются запятыми. Например (
a, b
) или (int a, int b
) или (String a
, int b
, float c
)
- Если параметров нет, то нужно использовать пустые круглые скобки. Например
() -> 42
- Когда параметр один, если тип не указывается явно, скобки можно опустить. Пример:
a -> return a*a
- Тело Lambda-выражения может содержать от 0 и более выражений.
- Если тело состоит из одного оператора, его можно не заключать в фигурные скобки, а возвращаемое значение можно указывать без ключевого слова
return
.
- В противном случае фигурные скобки обязательны (блок кода), а в конце надо указывать возвращаемое значение с использованием ключевого слова
return
(в противном случае типом возвращаемого значения будет void
).
Что такое функциональный интерфейс
В Java, маркерные интерфейсы (Marker interface) – это интерфейсы без объявления методов и полей. Другими словами маркерные интерфейсы – это пустые интерфейсы. Точно также, функциональные интерфейсы (Functional Interface) – это интерфейсы только с одним абстрактным методом, объявленным в нем.
java.lang.Runnable
– это пример функционального интерфейса. В нем объявлен только один метод
void run()
. Также есть интерфейс
ActionListener
– тоже функциональный. Раньше нам приходилось использовать анонимные классы для создания объектов, реализующих функциональный интерфейс. С Lambda-выражениями, все стало проще.
Каждое lambda-выражение может быть неявно привязано к какому-нибудь функциональному интерфейсу. Например, можно создать ссылку на
Runnable
интерфейс, как показано в следующем примере:
Runnable r = () -> System.out.println("hello world");
Подобное преобразование всегда осуществляется неявно, когда мы не указываем функциональный интерфейс:
new Thread(
() -> System.out.println("hello world")
).start();
В примере выше, компилятор автоматически создает lambda-выражение как реализацию
Runnable
интерфейса из конструктора класса
Thread
:
public Thread(Runnable r) { }
.
Приведу несколько примеров lambda-выражений и соответствующих функциональных интерфейсов:
Consumer<Integer> c = (int x) -> { System.out.println(x) };
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
Predicate<String> p = (String s) -> { s == null };
Аннотация
@FunctionalInterface
, добавленная в Java 8 согласно Java Language Specification, проверяет является ли объявляемый интерфейс функциональным. Кроме того, в Java 8 включен ряд готовых функциональных интерфейсов для использования с Lambda-выражениями.
@FunctionalInterface
выдаст ошибку компиляции, если объявляемый интерфейс не будет функциональным.
Далее приводится пример определения функционального интерфейса:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
Как следует из определения, функциональный интерфейс может иметь только один абстрактный метод. Если попытаться добавить еще один абстрактный метод, то вылезет ошибка компиляции. Пример:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
public void doSomeMoreWork();
}
Error
Unexpected @FunctionalInterface annotation
@FunctionalInterface ^ WorkerInterface is not a functional interface multiple
non-overriding abstract methods found in interface WorkerInterface 1 error
После определения функционального интерфейса, мы можем его использовать и получать все преимущества Lambda-выражений. Пример:
// определении функционального интерфейса
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
// вызов метода doSomeWork через анонимный класс
// (классический способ)
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker вызван через анонимный класс");
}
});
// вызов метода doSomeWork через Lambda-выражения
// (нововведение Java 8)
execute( () -> System.out.println("Worker вызван через Lambda") );
}
}
Вывод:
Worker вызван через анонимный класс
Worker вызван через Lambda
Здесь мы определили свой собственный функциональный интерфейс и воспользовались lambda-выражением. Метод
execute()
способен принимать lambda-выражения в качестве аргумента.
Примеры Lambda-выражений
Лучший способ вникнуть в Lambda-выражения – это рассмотреть несколько примеров:
Поток
Thread
можно проинициализировать двумя способами:
// Старый способ:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
// Новый способ:
new Thread(
() -> System.out.println("Hello from thread")
).start();
Управление событиями в Java 8 также можно осуществлять через Lambda-выражения. Далее представлены два способа добавления обработчика события
ActionListener
в компонент пользовательского интерфейса:
// Старый способ:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Кнопка нажата. Старый способ!");
}
});
// Новый способ:
button.addActionListener( (e) -> {
System.out.println("Кнопка нажата. Lambda!");
});
Простой пример вывода всех элементов заданного массива. Заметьте, что есть более одного способа использования lambda-выражения. Ниже мы создаем lambda-выражение обычным способом, используя синтаксис стрелки, а также мы используем оператор двойного двоеточия
(::)
, который в Java 8 конвертирует обычный метод в lambda-выражение:
// Старый способ:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
System.out.println(n);
}
// Новый способ:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
// Новый способ с использованием оператора двойного двоеточия ::
list.forEach(System.out::println);
В следующем примере мы используем функциональный интерфейс
Predicate
для создания теста и печати элементов, прошедших этот тест. Таким способом вы можете помещать логику в lambda-выражения и делать что-либо на ее основе.
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
System.out.print("Выводит все числа: ");
evaluate(list, (n)->true);
System.out.print("Не выводит ни одного числа: ");
evaluate(list, (n)->false);
System.out.print("Вывод четных чисел: ");
evaluate(list, (n)-> n%2 == 0 );
System.out.print("Вывод нечетных чисел: ");
evaluate(list, (n)-> n%2 == 1 );
System.out.print("Вывод чисел больше 5: ");
evaluate(list, (n)-> n > 5 );
}
public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.print(n + " ");
}
}
System.out.println();
}
}
Вывод:
Выводит все числа: 1 2 3 4 5 6 7
Не выводит ни одного числа:
Вывод четных чисел: 2 4 6
Вывод нечетных чисел: 1 3 5 7
Вывод чисел больше 5: 6 7
Поколдовав над Lambda-выражениями можно вывести квадрат каждого элемента списка. Заметьте, что мы используем метод
stream()
, чтобы преобразовать обычный список в поток. Java 8 предоставляет шикарный класс
Stream
(
java.util.stream.Stream
). Он содержит тонны полезных методов, с которыми можно использовать lambda-выражения. Мы передаем lambda-выражение
x -> x*x
в метод
map()
, который применяет его ко всем элементам в потоке. После чего мы используем
forEach
для печати всех элементов списка.
// Старый способ:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
// Новый способ:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
Дан список, нужно вывести сумму квадратов всех элемента списка. Lambda-выражения позволяет достигнуть этого написанием всего одной строки кода. В этом примере применен метод свертки (редукции)
reduce()
. Мы используем метод
map()
для возведения в квадрат каждого элемента, а потом применяем метод
reduce()
для свертки всех элементов в одно число.
// Старый способ:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
int x = n * n;
sum = sum + x;
}
System.out.println(sum);
// Новый способ:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);
Отличие Lambda-выражений от анонимных класов
Главное отличие состоит в использовании ключевого слова
this
. Для анонимных классов ключевое слово ‘
this
’ обозначает объект анонимного класса, в то время как в lambda-выражении ‘
this
’ обозначает объект класса, в котором lambda-выражение используется.
Другое их отличие заключается в способе компиляции. Java компилирует lambda-выражения с преобразованием их в
private
-методы класса. При этом используется инструкция
invokedynamic, появившаяся в Java 7 для динамической привязки метода. Тал Вайс (Tal Weiss) описал в своем блоге как Java компилирует lambda-выражения в байт-код
Заключение
Марк Рейнхолд (Mark Reinhold - Oracle’s Chief Architect), назвал Lambda-выражения самым значительным изменением в модели программирования, которое когда-либо происходило — даже более значительным, чем дженерики (generics). Должно быть он прав, т.к. они дают Java программистам возможности функциональных языков программирования, которых так давно все ждали. Наряду с такими новшествами как методы виртуального расширения (Virtual extension methods), Lambda-выражения позволяют писать очень качественный код.
Я надеюсь, что это статья позволила вам взглянуть под капот Java 8. Удачи :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ