Для кого призначена ця стаття?
- для тих, хто вважає, що вже непогано знає 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.
- Стаття на хабрі про функціональне програмування (переклад іншої статті). Багато букв і про лямбди дуже мало, оскільки там про функціональне програмування загалом.
- Любителям позалипати на вікіпедії.
- Ну і, звичайно ж, купу всього знаходив в google :)