Для кого предназначена эта статья?
- Для тех, кто читал первую часть этой статьи;
- для тех, кто считает, что уже неплохо знает Java Core, но понятия не имеет о лямбда-выражениях в Java. Или, возможно, что-то уже слышал про лямбды, но без подробностей.
- для тех, у кого есть какое-то понимание лямбда-выражений, но использовать их до сих пор боязно и непривычно.
Доступ к внешним переменным
Скомпилируется ли такой код с анонимным классом?int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
Нет. Переменная counter
должна быть final
. Или не обязательно final
, но в любом случае изменять свое значение она не может.
Тот же принцип используется и в лямбда-выражениях. Они имеют доступ ко всем переменным, которые им «видны» из того места, где они объявлены. Но лямбда не должна их изменять (присваивать новое значение).
Правда, есть вариант обхода этого ограничения в анонимных классах. Достаточно лишь создать переменную ссылочного типа и менять внутреннее состояние объекта. При этом сама переменная будет указывать на тот же объект, и в таком случае можно смело указывать её как final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
Здесь у нас переменная counter
является ссылкой на объект типа AtomicInteger
. А для изменения состояния этого объекта используется метод incrementAndGet()
. Значение самой переменной во время работы программы не меняется и всегда указывает на один и тот же объект, что позволяет нам объявить переменную сразу с ключевым словом final
.
Эти же примеры, но с лямбда-выражениями:
int counter = 0;
Runnable r = () -> counter++;
Не скомпилируется по той же причине, что и вариант с анонимным классом: counter
не должна меняться во время работы программы.
Зато вот так — все нормально:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Это касается и вызова методов. Изнутри лямбда-выражения можно не только обращаться ко всем «видимым» переменным, но и вызывать те методы, к которым есть доступ.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
}
}
Хотя метод staticMethod()
и приватный, но он «доступен» для вызова внутри метода main()
, поэтому точно так же доступен для вызова изнутри лямбды, которая создана в методе main
.
Момент выполнения кода лямбда-выражения
Возможно, вам этот вопрос покажется слишком простым, но его всё следует задать: когда выполнится код внутри лямбда-выражения? В момент создания? Или же в тот момент, когда (еще и неизвестно где) оно будет вызвано? Проверить довольно просто.System.out.println("Запуск программы");
// много всякого разного кода
// ...
System.out.println("Перед объявлением лямбды");
Runnable runnable = () -> System.out.println("Я - лямбда!");
System.out.println("После объявления лямбды");
// много всякого другого кода
// ...
System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
Вывод на экран:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Видно, что код лямбда-выражения выполнился в самом конце, после того, как был создан тред и лишь когда процесс выполнения программы дошел до фактического выполнения метода run()
. А вовсе не в момент его объявления. Объявив лямбда-выражение, мы лишь создали объект типа Runnable
и описали поведение его метода run()
. Сам же метод был запущен значительно позже.
Method References (Ссылки на методы)?
Не имеет прямого отношения к самим лямбдам, но я считаю, что будет логично сказать об этом пару слов в этой статье. Допустим, у нас есть лямбда-выражение, которое не делает ничего особенного, а просто вызывает какой-то метод.x -> System.out.println(x)
Ему передали некий х
, а оно — просто вызвало System.out.println()
и передало туда х
.
В таком случае, мы можем заменить его на ссылку на нужный нам метод. Вот так:
System.out::println
Да, без скобок в конце! Более полный пример:
List<String> strings = new LinkedList<>();
strings.add("мама");
strings.add("мыла");
strings.add("раму");
strings.forEach(x -> System.out.println(x));
В последней строке мы используем метод forEach()
, который принимает объект интерфейса Consumer
. Это снова же функциональный интерфейс, у которого только один метод void accept(T t)
. Соответственно, мы пишем лямбда-выражение, которое принимает один параметр (поскольку он типизирован в самом интерфейсе, тип параметра мы не указываем, а указываем, что называться он у нас будет х)
. В теле лямбда-выражения пишем код, который будет выполняться при вызове метода accept()
.
Здесь мы просто выводим на экран то, что попало в переменную х
. Сам же метод forEach()
проходит по всем элементам коллекции, вызывает у переданного ему объекта интерфейса Consumer
(нашей лямбды) метод accept()
, куда и передает каждый элемент из коллекции.
Как я уже сказал, такое лямбда-выражение (просто вызывающее другой метод) мы можем заменить ссылкой на нужный нам метод. Тогда наш код будет выглядеть так:
List<String> strings = new LinkedList<>();
strings.add("мама");
strings.add("мыла");
strings.add("раму");
strings.forEach(System.out::println);
Главное, чтобы совпадали принимаемые параметры методов (println()
и accept())
. Поскольку метод println()
может принимать что угодно (он перегружен для всех примитивов и для любых объектов, мы можем вместо лямбда-выражения передать в forEach()
просто ссылку на метод println()
. Тогда forEach()
будет брать каждый элемент коллекции и передавать его напрямую в метод println()
.
Кто сталкивается с этим впервые, обратите внимание, что мы не вызываем метод System.out.println()
(с точками между словами и со скобочками в конце), а именно передаем саму ссылку на этот метод.
При такой записи
strings.forEach(System.out.println());
у нас будет ошибка компиляции. Поскольку перед вызовом forEach()
Java увидит, что вызывается System.out.println()
, поймет, что возвращается void
и будет пытаться этот void
передать в forEach()
, который там ждет объект типа Consumer
.
Синтаксис использования Method References
Он довольно прост:Передаем ссылку на статический метод
ИмяКласса:: имяСтатическогоМетода?
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("мама"); strings.add("мыла"); strings.add("раму"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // do something } }
Передаем ссылку на не статический метод используя существующий объект
имяПеременнойСОбъектом:: имяМетода
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("мама"); strings.add("мыла"); strings.add("раму"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // do something } }
Передаем ссылку на не статический метод используя класс, в котором реализован такой метод
ИмяКласса:: имяМетода
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add(new User("Вася")); users.add(new User("Коля")); users.add(new User("Петя")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
Передаем ссылку на конструктор
ИмяКласса::new
Использование ссылок на методы очень удобно, когда есть готовый метод , который вас полностью устраивает, и вы бы хотели использовать его в качестве callback-а. В таком случае, вместо того чтобы писать лямбда-выражение с кодом того метода, или же лямбда-выражение, где мы просто вызываем этот метод, мы просто передаем ссылку на него. И всё.
Интересное различие между анонимным классом и лямбда-выражением
В анонимном классе ключевое словоthis
указывает на объект этого анонимного класса. А если использовать this
внутри лямбды, мы получим доступ к объекту обрамляющего класса. Того, где мы это выражение, собственно, и написали. Так происходит потому, что лямбда-выражения при компиляции превращаются в приватный метод того класса, где они написаны.
Использовать эту «фичу» я бы не рекомендовал, поскольку у неё есть побочный эффект (side effect), что противоречит принципам функционального программирования. Зато такой подход вполне соответствует ООП. ;)
Откуда я брал информацию или что почитать еще
- Туториал на официальном сайте Oracle. Много, детально, с примерами, но на английском.
- Тот же "ораклавский" туториал, но глава именно про Method References.
- Статья на хабре про функциональное программирование (перевод другой статьи). Многобуков и про лямбды очень мало, так как там о функциональном программировании в общем.
- Любителям позалипать на википедии.
- Ну и, конечно же, кучу всего находил в гугле :)