JavaRush /Java блог /Random UA /Популярно про лямбда-вираження в Java. З прикладами та за...
Стас Пасинков
26 рівень
Киев

Популярно про лямбда-вираження в Java. З прикладами та завданнями. Частина 2

Стаття з групи Random UA
Для кого призначено цю статтю?
  • Для тих, хто читав першу частину цієї статті;

  • для тих, хто вважає, що вже непогано знає 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

Він досить простий:
  1. Передаємо посилання на статичний методІм'яКласса:: ім'яСтатическогоМетода?

    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
        }
    }
  2. Передаємо посилання на нестатичний метод, використовуючи існуючий об'єктім'яПеременнойСОбъектом:: ім'яМетоду

    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
        }
    }
  3. Передаємо посилання не статичний метод використовуючи клас, у якому реалізований такий методІм'яКласса:: ім'яМетоду

    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);
            }
        }
    }
  4. Передаємо посилання на конструктор Ім'яКласса::new
    Використання посилань на методи дуже зручно, коли є готовий метод, який вас повністю влаштовує, і ви хотіли б використовувати його як callback-а. У такому разі замість того, щоб писати лямбда-вираз з кодом того методу, або ж лямбда-вираз, де ми просто викликаємо цей метод, ми просто передаємо посилання на нього. І все.

Цікава різниця між анонімним класом та лямбда-виразом

В анонімному класі ключове слово thisвказує на об'єкт цього анонімного класу. А якщо використовувати thisвсередині лямбди, ми отримаємо доступ до об'єкта класу, що обрамляє. Того, де ми цей вислів, власне, й написали. Так відбувається тому, що лямбда-вирази під час компіляції перетворюються на приватний метод того класу, де вони написані. Використовувати цю «фічу» я б не рекомендував, оскільки має побічний ефект (side effect), що суперечить принципам функціонального програмування. Проте такий підхід цілком відповідає ОВП. ;)

Звідки я брав інформацію або що ще почитати

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ