$ sudo teach IT
МОДУЛЬ 10 · УРОК 4

Lambda-выражения

Узнаём, что такое лямбда-выражения и как они делают код компактнее. Разбираем функциональные интерфейсы, стандартные функции и method references

~35 минут Для новичков Java

Проблема: слишком многословный код

До знакомства с лямбдами давайте посмотрим на типичную проблему: создание анонимного класса для простой операции. Представьте, что вы хотите отсортировать список имён по длине:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Анна");
        names.add("Б");
        names.add("Виктор");
        names.add("Дима");

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

        System.out.println(names);
    }
}

Этот код работает, но выглядит громоздко: анонимный класс, перегруженный метод, повторение типов. Весь компаратор — это по сути одна строка: Integer.compare(a.length(), b.length()).

Аналогия: Представьте, что вы хотите попросить друга купить хлеб. Вы можете написать ему письмо на трёх страницах: «Уважаемый друг, я хотел бы вас попросить, если у вас будет возможность, в случае если вы идёте в магазин, купить мне буханку хлеба...». Или просто сказать: «Купи хлеб». Lambda — это «Купи хлеб» вместо трёхстраничного письма.

Что такое Lambda-выражение

Lambda-выражение (лямбда-выражение) — это анонимная функция, которая позволяет компактно записать реализацию функционального интерфейса. Введено в Java 8.

Синтаксис lambda:

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

Стрелка -> читается как «преобразуется в» или «применяется к». Давайте рассмотрим несколько примеров:

() -> System.out.println("Привет")

(x) -> x * 2

(a, b) -> a + b

(a, b) -> {
    int sum = a + b;
    return sum * 2;
}

Правила синтаксиса:

  • Параметры берутся в круглые скобки
  • Если параметр один и его тип выводится, скобки можно не ставить: x -> x * 2
  • Если тело — одно выражение, можно не ставить фигурные скобки и return
  • Если тело — несколько выражений, используются фигурные скобки и явный return
  • Типы параметров обычно выводятся автоматически

Функциональный интерфейс

Lambda-выражения в Java могут использоваться только с функциональными интерфейсами. Функциональный интерфейс — это интерфейс с ровно одним абстрактным методом.

Аналогия: Функциональный интерфейс — как форма для заполнения. У неё есть одно поле (абстрактный метод), и lambda — это то, чем вы заполняете это поле.

Для обозначения функционального интерфейса используется аннотация @FunctionalInterface:

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator multiply = (a, b) -> a * b;

        System.out.println(add.calculate(3, 5));       // 8
        System.out.println(multiply.calculate(3, 5));  // 15
    }
}

Аннотация @FunctionalInterface не обязательна, но полезна: компилятор проверит, что в интерфейсе ровно один абстрактный метод.

Стандартные функциональные интерфейсы

Java提供ляет четыре основных функциональных интерфейса в пакете java.util.function. Они покрывают большинство типичных сценариев:

Consumer<T> — принимает значение и ничего не возвращает («потребитель»):

import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        Consumer<String> print = s -> System.out.println(s);
        Consumer<String> shout = s -> System.out.println(s.toUpperCase());

        print.accept("Привет");   // Привет
        shout.accept("Привет");   // ПРИВЕТ

        Consumer<Integer> doubleIt = n -> System.out.println(n * 2);
        doubleIt.accept(5);       // 10
    }
}

Supplier<T> — не принимает аргументов и возвращает значение («поставщик»):

import java.util.function.Supplier;
import java.util.Random;

public class Main {
    public static void main(String[] args) {
        Supplier<Integer> randomNum = () -> new Random().nextInt(100);
        Supplier<String> greeting = () -> "Привет, мир!";

        System.out.println(randomNum.get()); // Случайное число 0-99
        System.out.println(greeting.get());  // Привет, мир!
    }
}

Predicate<T> — принимает значение и возвращает boolean («предикат», условие):

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Predicate<String> isLong = s -> s.length() > 5;

        System.out.println(isEven.test(4));  // true
        System.out.println(isEven.test(7));  // false
        System.out.println(isLong.test("Длинное"));  // true
        System.out.println(isLong.test("А"));        // false
    }
}

Function<T, R> — принимает значение типа T и возвращает значение типа R («функция»):

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> length = s -> s.length();
        Function<Integer, String> toBin = n -> Integer.toBinaryString(n);

        System.out.println(length.apply("Привет")); // 6
        System.out.println(toBin.apply(10));         // 1010
    }
}

Lambda с коллекциями: сортировка

Теперь давайте вернёмся к примеру сортировки и перепишем его с использованием lambda:

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Анна");
        names.add("Б");
        names.add("Виктор");
        names.add("Дима");

        Collections.sort(names, (a, b) -> Integer.compare(a.length(), b.length()));

        System.out.println(names);
        // [Б, Анна, Дима, Виктор]
    }
}

Вместо шести строк анонимного класса — одна строка lambda. Вместо Compare(a, b) { return Integer.compare(...) } — просто (a, b) -> Integer.compare(...).

Можно ещё короче, используя Comparator.comparingInt():

import java.util.ArrayList;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Анна");
        names.add("Б");
        names.add("Виктор");
        names.add("Дима");

        names.sort(Comparator.comparingInt(String::length));

        System.out.println(names);
    }
}

Здесь мы использовали method reference (String::length) — об этом подробнее позже.

Lambda с коллекциями: другие примеры

Lambda-выражения широко используются с коллекциями. Вот несколько полезных примеров:

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(2);
        numbers.add(8);
        numbers.add(1);
        numbers.add(9);

        Collections.sort(numbers, (a, b) -> b - a);
        System.out.println("По убыванию: " + numbers);

        Collections.sort(numbers, (a, b) -> a - b);
        System.out.println("По возрастанию: " + numbers);

        ArrayList<String> words = new ArrayList<>();
        words.add("Банан");
        words.add("Ананас");
        words.add("Арбуз");

        words.sort((a, b) -> a.compareTo(b));
        System.out.println("Алфавитно: " + words);

        words.sort((a, b) -> b.compareTo(a));
        System.out.println("Обратный алфавит: " + words);
    }
}

Method Reference: ::

Method Reference (ссылка на метод) — это ещё более короткий способ записи lambda. Он используется, когда lambda просто вызывает существующий метод.

Синтаксис: Класс::метод или объект::метод

Вот четыре типа method references:

1. Ссылка на статический метод: ClassName::staticMethod

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Анна", "Борис", "Вика");

        names.forEach(name -> System.out.println(name));

        names.forEach(System.out::println);
    }
}

2. Ссылка на метод экземпляра: ClassName::instanceMethod

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Анна", "Борис", "Вика");

        names.forEach(name -> System.out.println(name.toUpperCase()));

        names.forEach(name -> System.out.println(String.valueOf(name.length())));

        names.forEach(name -> System.out.println(name.length()));

        names.forEach(name -> System.out.println(name.isEmpty()));
    }
}

3. Ссылка на конструктор: ClassName::new

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Анна", "Борис", "Вика");

        List<ArrayList> lists = names.stream()
            .map(ArrayList::new)
            .collect(Collectors.toList());

        System.out.println(lists.size());
    }
}

Effectively final

Lambda-выражения могут обращаться к переменным из окружающего метода. Но есть важное ограничение: эти переменные должны быть effectively final — то есть не изменяться после инициализации.

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        String prefix = "Привет, "; // effectively final

        List<String> names = Arrays.asList("Анна", "Борис", "Вика");

        names.forEach(name -> System.out.println(prefix + name));
        // Привет, Анна
        // Привет, Борис
        // Привет, Вика
    }
}

Переменная prefix effectively final — она инициализирована и больше не изменяется. Lambda может её использовать.

А вот так нельзя:

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        int counter = 0; // НЕ final

        List<String> names = Arrays.asList("Анна", "Борис", "Вика");

        names.forEach(name -> {
            counter++; // Ошибка компиляции!
            System.out.println(counter + ": " + name);
        });
    }
}

Компилятор не позволит изменить переменную counter внутри lambda, потому что это нарушает правило effectively final.

Правило effectively final существует, потому что lambda может выполняться в другом потоке или позже. Если бы lambda могла изменять переменную, это создало бы серьёзные проблемы потокобезопасности.

Lambda vs Анонимный класс

Давайте сравним lambda и анонимный класс для одной и той же задачи:

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Анна", "Борис", "Вика");

        names.forEach(new java.util.function.Consumer<String>() {
            @Override
            public void accept(String name) {
                System.out.println(name);
            }
        });

        names.forEach(name -> System.out.println(name));

        names.forEach(System.out::println);
    }
}

Все три варианта делают одно и то же, но второй и третий гораздо короче и чище.

Важные отличия lambda от анонимного класса:

  • Lambda может использоваться только с функциональными интерфейсами (с одним абстрактным методом)
  • Aнонимный класс может реализовывать интерфейс с любым количеством методов
  • Lambda не создаёт отдельный класс — это более эффективно
  • Lambda имеет доступ к effectively final переменным
  • Aнонимный класс может иметь собственные поля и методы

Практический пример: обработка списка

Давайте создадим программу, которая обрабатывает список студентов с помощью lambda-выражений:

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;

public class StudentProcessor {
    public static void main(String[] args) {
        ArrayList<String> students = new ArrayList<>();
        students.add("Анна");
        students.add("Борис");
        students.add("Виктор");
        students.add("Дима");
        students.add("Елена");

        students.sort((a, b) -> Integer.compare(a.length(), b.length()));
        System.out.println("По длине имени: " + students);

        Predicate<String> shortName = name -> name.length() <= 4;
        Predicate<String> longName = name -> name.length() > 4;

        System.out.print("Короткие имена: ");
        for (String name : students) {
            if (shortName.test(name)) {
                System.out.print(name + " ");
            }
        }
        System.out.println();

        System.out.print("Длинные имена: ");
        for (String name : students) {
            if (longName.test(name)) {
                System.out.print(name + " ");
            }
        }
        System.out.println();
    }
}

Комбинирование функциональных интерфейсов

Predicate и Function имеют методы для комбинирования, что позволяет создавать сложные условия из простых:

import java.util.function.Predicate;
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Predicate<Integer> isPositive = n -> n > 0;

        Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
        Predicate<Integer> isEvenOrPositive = isEven.or(isPositive);
        Predicate<Integer> isNotEven = isEven.negate();

        System.out.println(isEvenAndPositive.test(4));   // true
        System.out.println(isEvenAndPositive.test(-4));  // false
        System.out.println(isEvenOrPositive.test(-3));   // true
        System.out.println(isNotEven.test(3));           // true

        Function<String, Integer> length = String::length;
        Function<Integer, String> timesTen = n -> n + "0";

        Function<String, String> composed = length.andThen(timesTen);

        System.out.println(composed.apply("Привет")); // 60
    }
}

Итоги урока

  • Lambda-выражение — это компактная запись анонимной функции: (параметры) -> тело
  • Lambda может использоваться только с функциональными интерфейсами (один абстрактный метод)
  • Consumer<T> — принимает значение, ничего не возвращает
  • Supplier<T> — не принимает аргументов, возвращает значение
  • Predicate<T> — принимает значение, возвращает boolean
  • Function<T, R> — принимает T, возвращает R
  • Method reference (::) — ещё короче запись, когда lambda просто вызывает метод
  • Переменные из окружающего кода должны быть effectively final
  • Lambda компактнее анонимного класса и эффективнее (не создаёт отдельный класс)
  • Predicate и Function поддерживают комбинирование: and(), or(), negate()

Тест по Lambda-выражениям

5 вопросов

Лямбда-выражения

Premium