JavaRush /Курси /JAVA 25 SELF /Вступ до лямбда-виразів

Вступ до лямбда-виразів

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

1. Знайомство

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

Java тривалий час була класичною мовою ООП. Якщо ви хотіли передати фрагмент поведінки — наприклад, що робити під час натискання кнопки, — доводилося писати анонімні класи:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("Кнопку натиснуто!");
    }
});

Починаючи з Java 8, з’явилися лямбда-вирази:

button.setOnClickListener(() -> System.out.println("Кнопку натиснуто!"));

Тут () -> System.out.println("Кнопку натиснуто!") — і є лямбда-вираз.

Формально: лямбда-вираз — це компактна форма запису реалізації єдиного абстрактного методу функціонального інтерфейсу.

Чому це важливо?

  • Передавати поведінку як значення (наприклад, що робити з кожним елементом списку).
  • Писати компактний і зрозумілий код.
  • Використовувати сучасні API Java: Stream API, обробку подій, асинхронні завдання та багато іншого.

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

Загальний вигляд

(параметри) -> вираз
// або
(параметри) -> { блок коду }

Приклади для різних інтерфейсів

1. Без параметрів (наприклад, Runnable):

Runnable r = () -> System.out.println("Привіт, лямбда!");
r.run();

2. Один параметр (наприклад, Consumer):

Consumer<String> printer = s -> System.out.println("Ви передали: " + s);
printer.accept("Java");

Якщо параметр лише один і його тип можна вивести, дужки можна опустити.
Тип String — це тип параметра s.

3. Кілька параметрів (наприклад, Comparator):

Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5

Тип Integer — це тип параметрів a і b.

4. Багаторядкове тіло (потрібні фігурні дужки та return, якщо є явне повернення):

Function<Integer, Integer> square = x -> {
    int result = x * x;
    return result;
};
System.out.println(square.apply(6)); // 36

Перший Integer — тип параметра функції, другий Integer — тип результату.

Скорочення та лаконічність

  • Якщо тіло — один вираз, фігурні дужки та return можна опустити.
  • Якщо параметрів немає — пишемо порожні дужки: () -> ...
  • Якщо один параметр — можна без дужок: x -> ...
  • Якщо параметрів більше одного — потрібні дужки: (a, b) -> ...

Підсумкова таблиця

Подивімося, як одну й ту саму ідею можна записати лямбдою або анонімним класом:

Інтерфейс Лямбда-вираз (приклад) Еквівалент анонімного класу
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

Приклад із власним інтерфейсом

Нехай маємо функціональний інтерфейс:

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}

Раніше це реалізовували так:

Operation sum = new Operation() {
    @Override
    public void apply(int a, int b) {
        return a + b;
    }
};

Тепер — коротко:

Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8

Приклади для стандартних інтерфейсів

Runnable:

Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();

Comparator:

List<String> list = Arrays.asList("яблуко", "банан", "ківі");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);

Function:

Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123

3. Область видимості та захоплення змінних

Змінні із зовнішнього контексту (effectively final)

Лямбда-вирази можуть використовувати змінні з охоплювального методу. Але є правило: такі змінні мають бути effectively final — тобто або явно оголошені як final, або після ініціалізації не змінюватися.

Приклад:

String prefix = "Результат: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix тут "заморожено" — після цього його не можна змінювати
System.out.println(f.apply(5)); // Результат: 10

Якщо спробувати змінити prefix після того, як його використано у лямбді, компілятор видасть помилку.

Чому так?
Лямбда-вираз можна викликати й після виходу з методу, де оголошено змінну. Щоб уникнути непередбачуваних помилок, Java дозволяє використовувати лише незмінні локальні змінні.

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

В анонімних класах діє те саме правило: змінні із зовнішнього методу мають бути final/effectively final. Проте відрізняється поведінка з областю видимості: усередині анонімного класу this посилається на сам анонімний клас, а в лямбді — на зовнішній клас.

public class Demo {
    public void test() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // Виведе: Demo$1 (анонімний клас)
            }
        };

        Runnable r2 = () -> System.out.println(this); // Виведе: Demo (зовнішній клас)

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

Лямбда і поля класу

Лямбда-вираз може звертатися до полів зовнішнього класу без додаткових обмежень:

public class Counter {
    private int base = 10;

    public void printSum(int x) {
        Function<Integer, Integer> sum = y -> base + y + x;
        System.out.println(sum.apply(5));
    }
}

Тут base — поле класу, його можна змінювати.
x — параметр методу; він має бути effectively final.

4. Практика: напишемо кілька лямбда-виразів

Приклад: фільтрація списку чисел

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);

nums.stream()
    .filter(n -> n % 2 == 0)
    .forEach(n -> System.out.println("Парне: " + n));

Докладніше про Stream API ви дізнаєтеся на 30-му рівні :P

Приклад: функція перетворення рядка

Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA

Приклад: власний функціональний інтерфейс

@FunctionalInterface
interface StringTransformer {
    String transform(String s);
}

StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Привіт")); // Привіт!

Приклад: використання змінних із зовнішнього контексту

int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor не можна змінити після цього!

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

Помилка № 1: спроба змінити змінну, захоплену лямбдою.
Такий код не скомпілюється — змінна має бути effectively final:

int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Помилка: sum не final!

Якщо потрібно накопичувати значення — використовуйте масив або об’єкт-обгортку.

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

Помилка № 3: забули фігурні дужки та return у багаторядковому лямбда-виразі.
Якщо тіло лямбди — не один вираз, потрібні дужки та return:

Function<Integer, Integer> square = x -> {
    int y = x * x;
    return y;
};

Помилка № 4: некоректне визначення типу лямбда-виразу.
Лямбда-вираз завжди реалізує функціональний інтерфейс. Не можна просто написати:

var f = x -> x + 1; // Помилка! Невідомо, який тип інтерфейсу.

Потрібно явно вказати тип:

Function<Integer, Integer> f = x -> x + 1;
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ