JavaRush /Курси /JAVA 25 SELF /Лямбда-вирази: синтаксис, області видимості

Лямбда-вирази: синтаксис, області видимості

JAVA 25 SELF
Рівень 48 , Лекція 0
Відкрита

1. Вступ

Давайте чесно: писати анонімні класи заради одного-двох рядків коду — це як наймати величезну вантажівку, щоб перевезти з пекарні до магазину одну булочку.

Наприклад, якщо ви хочете відсортувати список рядків за довжиною, до Java 8 доводилося писати ось так:

List<String> list = Arrays.asList("яблуко", "банан", "ківі");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Здавалося б, завдання просте, а коду — на пів екрана. Особливо дратує, коли таких операцій багато: код стає «шумним», втрачається суть задачі. Програмісти плакали, страждали, а потім придумали лямбда-вирази. Ми вже трохи їх вивчали, тепер повторимо й поглибимо наші знання.

Що таке лямбда-вираз

Лямбда-вираз — це компактна форма запису реалізації функціонального інтерфейсу, тобто інтерфейсу з єдиним абстрактним методом (наприклад, Comparator, Runnable, Consumer та багато інших).

Простіше кажучи, лямбда-вираз дозволяє вам написати функцію «на льоту», безпосередньо там, де вона потрібна, без оголошення окремого класу або методу.

Загальний синтаксис:

(параметры) -> { тело }

Приклади:

  • (a, b) -> a + b — функція додавання двох чисел
  • x -> x * x — функція зведення числа в квадрат
  • () -> System.out.println("Hello!") — функція без параметрів

Зв’язок із функціональними інтерфейсами:
Лямбда-вираз завжди можна присвоїти змінній типу функціонального інтерфейсу або передати як аргумент у метод, який очікує такий інтерфейс.

2. Синтаксис лямбда-виразів

Без параметрів

Runnable r = () -> System.out.println("Привіт, світе!");
r.run(); // Виведе: Привіт, світе!

Один параметр
Якщо параметр один, дужки можна опустити:

Consumer<String> print = s -> System.out.println(s);
print.accept("Java — це круто!");

Кілька параметрів
Дужки обов’язкові:

Comparator<String> cmp = (a, b) -> a.length() - b.length();

Тіло з одного виразу
Якщо тіло складається з одного виразу, фігурні дужки і return не потрібні:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

Тіло з блоку
Якщо потрібно кілька інструкцій, використовуйте фігурні дужки і return (якщо є значення, що повертається):

Function<Integer, Integer> abs = x -> {
    if (x < 0) {
        return -x;
    }
    return x;
};
System.out.println(abs.apply(-3)); // 3

Типи параметрів
Найчастіше типи параметрів можна не вказувати — компілятор сам здогадається з контексту. Але якщо хочеться, можна й явно:

Comparator<String> cmp = (String a, String b) -> a.length() - b.length();

Лямбда без значення, що повертається
Якщо інтерфейс повертає void, просто пишіть інструкції:

list.forEach(s -> System.out.println("Елемент: " + s));

Таблиця: Варіанти синтаксису лямбда-виразів

Що робимо Приклад Коментар
Без параметрів
() -> System.out.println("Hi")
Наприклад, для Runnable
Один параметр
x -> x * x
Можна без дужок
Кілька параметрів
(a, b) -> a + b
Дужки обов’язкові
Один вираз
x -> x + 1
Без return і фігурних дужок
Блок коду
x -> { int y = x + 1; return y * 2; }
З return, якщо є результат

3. Застосування: де і як використовувати лямбда-вирази

Лямбда-вирази найчастіше використовують там, де потрібно передати «поведінку» — функцію — як аргумент. Це стало справжньою революцією для колекцій, потоків (Stream API), подій та багато іншого.

Сортування списку

До Java 8:

list.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

З лямбда-виразом:

list.sort((a, b) -> a.length() - b.length());

Створення потоку (Thread)

Thread t = new Thread(() -> System.out.println("Потік запущено!"));
t.start();

Обробка колекцій

list.forEach(s -> System.out.println(s.toUpperCase()));

Фільтрація списку

List<String> longWords = list.stream()
    .filter(s -> s.length() > 5)
    .collect(Collectors.toList());

Приклад: розвиваємо наш навчальний застосунок

Припустімо, у нас є список користувачів:

List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

Вивести всіх користувачів, імена яких довші за 4 символи:

users.stream()
    .filter(name -> name.length() > 4)
    .forEach(name -> System.out.println("Користувач: " + name));

4. Область видимості змінних у лямбда-виразах

Лямбда-вирази можуть використовувати змінні з навколишнього методу, але є нюанси!

Змінні та лямбди

Лямбда в Java може «захоплювати» лише ті змінні, які після ініціалізації більше не змінюються. Якщо змінну оголошено з final — усе очевидно. Але навіть якщо слово final не написано, компілятор сам перевіряє, чи змінюється значення, чи ні. Якщо ні, то він вважає її «ніби фінальною» і спокійно допускає її в лямбду.

Приклад:

int minLength = 4; // значення ніде не змінюється
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});

Тут усе працює, тому що minLength залишається тим самим значенням.

Але якщо після використання в лямбді ви спробуєте переприсвоїти minLength, то отримаєте помилку компіляції:

int minLength = 4;
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});
minLength = 10; // Помилка! Лямбда вже "зафіксувала" значення

По суті, правило дуже просте: змінна, що потрапила в лямбду, має бути незмінною.

Відмінність від анонімних класів

В анонімних класах і лямбда-виразах змінні із зовнішнього методу працюють однаково: тільки final/ефективно final.

АЛЕ!
У лямбда-виразі this посилається на зовнішній об’єкт (поточний екземпляр класу), а в анонімному класі — на екземпляр анонімного класу. Це важливо, якщо ви всередині лямбди звертаєтеся до полів або методів поточного класу.

Приклад:

public class Example {
    String name = "Зовнішній клас";

    void demo() {
        Runnable r1 = new Runnable() {
            String name = "Анонімний клас";
            @Override
            public void run() {
                System.out.println(this.name); // "Анонімний клас"
            }
        };

        Runnable r2 = () -> System.out.println(this.name); // "Зовнішній клас"

        r1.run();
        r2.run();
    }
}

5. Типові помилки під час роботи з лямбда-виразами

Помилка № 1: Використання не final/не ефективно final змінної. Якщо ви вирішили змінити змінну після того, як використали її в лямбді, компілятор одразу повідомить про помилку. Це робиться для безпеки: інакше було б незрозуміло, яке значення змінної використовувати.

Помилка № 2: Плутанина з this. У лямбда-виразі this — це зовнішній клас, а в анонімному класі — сам анонімний клас. Якщо ви намагаєтесь викликати метод зовнішнього класу з лямбди, усе працюватиме, а от із анонімного класу — ні (якщо ви розраховували на контекст зовнішнього класу).

Помилка № 3: Лямбда без контексту. Лямбда-вираз не можна використовувати сам по собі — його потрібно або присвоїти змінній функціонального інтерфейсу, або передати туди, де цей інтерфейс очікується. Спроба просто написати x -> x + 1 поза контекстом викличе помилку.

Помилка № 4: Занадто складна лямбда. Якщо лямбда-вираз стає довшим за 3–5 рядків, його складно читати. У таких випадках краще винести логіку в окремий метод.

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