Для кого призначена ця стаття?
  • для тих, хто вважає, що вже непогано знає 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), що суперечить принципам функціонального програмування. Проте, такий підхід цілком відповідає ООП. ;)

Де я шукав інформацію або що почитати ще