Для кого призначено цю статтю?
- Для тих, хто читав першу частину цієї статті;
- для тих, хто вважає, що вже непогано знає 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.
- Стаття на хабре про функціональне програмування (переклад іншої статті). Багатобуків і про лямбди дуже мало, тому що там про функціональне програмування загалом.
- Любителям позалипати на вікіпедії .
- Ну і, звичайно ж, купу всього знаходив у гугле :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ