W14. Лямбда-выражения, функциональные интерфейсы, Stream API, многопоточность

Автор

Eugene Zouev, Munir Makhmutov

Дата публикации

9 декабря 2025 г.

1. Резюме

1.1 Парадигма функционального программирования

Functional programming (функциональное программирование) — парадигма, в которой вычисление трактуется как вычисление математических функций, а изменяемое состояние и «мутабельные» данные по возможности избегаются. В отличие от imperative programming (императивного программирования), где в центре как программа шаг за шагом меняет состояние последовательностью операторов, функциональный стиль подчёркивает что должно получиться в виде значения выражений.

1.1.1 Императивный и функциональный подходы

В императивном стиле (он доминирует в C, C++, Java, C#) программы строятся вокруг:

  • Mutable state (изменяемого состояния): переменные могут менять значение в ходе выполнения
  • Side effects (побочных эффектов): функции могут менять глобальные данные или поля объектов
  • Iteration (итерации): повторение через циклы (for, while)
  • Statements (операторов): программа — последовательность команд

Функциональный стиль, наоборот, подчёркивает:

  • Immutable data (неизменяемые данные): после присваивания значение не меняется
  • Pure functions (чистые функции): результат зависит только от аргументов, без побочных эффектов
  • Recursion (рекурсию): вместо циклов — рекурсивные вызовы
  • Expressions (выражения): программа из выражений, дающих значения
  • Functions as first-class objects (функции как значения первого класса): их можно хранить в переменных, передавать и возвращать
1.1.2 Чистые функции и их плюсы

Pure function (чистая функция) — это функция, для которой выполняется:

  1. Нет побочных эффектов (ничего существенного вне области видимости не меняется)
  2. На одни и те же входы всегда один и тот же выход (referential transparency — ссылочная прозрачность)

Например, две функции на Java:

// NOT a pure function - has side effects
public class Example1 {
    private int value;
    public int add(int next) {
        this.value += next;  // Modifies object state!
        return this.value;
    }
}

// Pure function - no side effects
public class Example2 {
    public int sum(int x, int y) {
        return x + y;  // Only depends on inputs
    }
}

У чистых функций есть ряд преимуществ:

  • Проще тестировать: не нужно поднимать сложное состояние
  • Thread-safe: несколько потоков могут вызывать их без синхронизации
  • Кэшируемость: одинаковые входы → одинаковый выход, удобно для memoization
  • Параллелизуемость: вызовы можно переупорядочивать или выполнять параллельно без потери корректности
1.1.3 Пример: НОД в императивном и функциональном стиле

Наибольший общий делитель (НОД, GCD) по алгоритму Евклида:

Императивный подход (цикл и изменяемые локальные переменные):

int gcd(int x, int y) {
    int a = x, b = y;
    while (a != 0) {
        int temp = a;
        a = b % a;
        b = temp;
    }
    return b;
}

Функциональный подход (рекурсия без мутации):

int gcd(int x, int y) {
    return (y == 0) ? x : gcd(y, x % y);
}

Функциональная версия короче, без локальных переменных-«счётчиков», вместо итерации — рекурсия.

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

Lambda expressions (лямбда-выражения, с Java 8) — это способ записать anonymous function (анонимную функцию) без имени и передавать её как значение. Это компактная запись реализации functional interface одним выражением или блоком.

1.2.1 Синтаксис лямбды

Базовый синтаксис:

(parameters) -> expression

или для тела из нескольких операторов:

(parameters) -> { statements; }

Варианты синтаксиса:

  • Ноль параметров: () -> System.out.println("Hello")
  • Один параметр (скобки можно опустить): x -> x * x или (x) -> x * x
  • Несколько параметров: (x, y) -> x + y
  • Вывод типов: (x, y) -> x + y (типы выводит компилятор) и (int x, int y) -> x + y (типы явные)
  • Многострочное тело: (x, y) -> { int sum = x + y; return sum; }
1.2.2 Функции как значения первого класса

В функциональном стиле функции — такие же значения, как числа, строки или массивы. Значит:

  • функцию можно присвоить переменной;
  • передать в другую функцию;
  • вернуть из функции;
  • создать анонимно.

Обычное объявление метода/функции (для сравнения):

int sum(int x, int y) { return x + y; }

Лямбда записывает параметры и тело как значение:

(int x, int y) -> { return x + y; }

Это значение можно связать с переменной:

var fun = (int x, int y) -> { return x + y; };
1.3 Функциональные интерфейсы

Functional interface — интерфейс ровно с одним абстрактным методом (при этом могут быть default и static). Лямбда может реализовать только такой интерфейс.

1.3.1 Объявление функционального интерфейса

Аннотация @FunctionalInterface (необязательна, но желательна) фиксирует намерение:

@FunctionalInterface
interface Func {
    int action(int x, int y);  // Single abstract method
}

Лямбда может реализовать этот интерфейс:

Func lambda = (int x, int y) -> { return x + y; };

Тип лямбды — тот functional interface, чей единственный абстрактный метод совпадает по сигнатуре с лямбдой.

1.3.2 Стандартные интерфейсы в java.util.function

Пакет даёт готовые functional interfaces:

  • Predicate<T>: аргумент T, результат boolean
    • Метод: boolean test(T t)
    • Пример: Predicate<Integer> isPositive = x -> x > 0;
  • UnaryOperator<T>: TT
    • Метод: T apply(T t)
    • Пример: UnaryOperator<Integer> square = x -> x * x;
  • BinaryOperator<T>: два TT
    • Метод: T apply(T t1, T t2)
    • Пример: BinaryOperator<Integer> multiply = (x, y) -> x * y;
  • Function<T,R>: TR
    • Метод: R apply(T t)
    • Пример: Function<Integer, String> convert = x -> String.valueOf(x) + " dollars";
  • Consumer<T>: принимает T, ничего не возвращает (void)
    • Метод: void accept(T t)
    • Пример: Consumer<Integer> printer = x -> System.out.println(x);
  • Supplier<T>: без аргументов, возвращает T
    • Метод: T get()
    • Пример: Supplier<Integer> random = () -> (int)(Math.random() * 100);
1.3.3 Method references (ссылки на методы)

Если лямбда только делегирует вызов существующему методу, можно использовать method reference. Синтаксис: ClassName::methodName или object::methodName.

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

// Lambda version
Finder finder = (s1, s2) -> MyClass.doFind(s1, s2);
// Method reference version
Finder finder = MyClass::doFind;

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

StringConverter converter = new StringConverter();
// Lambda version
Deserializer des = (v1) -> converter.convertToInt(v1);
// Method reference version
Deserializer des = converter::convertToInt;

Ссылка на конструктор:

// Lambda version
Factory factory = chars -> new String(chars);
// Constructor reference version
Factory factory = String::new;

Ссылка на «встроенный» метод (через объект):

// Lambda version
MyPrinter printer = s -> System.out.println(s);
// Method reference version
MyPrinter printer = System.out::println;

Запись String::valueOf означает статический метод valueOf класса String.

1.3.4 Захват переменных (closures)

Лямбда может ссылаться на переменные, объявленные снаружи её тела; тогда говорят, что лямбда captures (захватывает) эти переменные, и такая лямбда называется closure (замыканием).

String myString = "Test";
Factory myFactory = (chars) -> myString + ":" + new String(chars);

Важное ограничение: захваченные переменные должны быть effectively final — после инициализации их значение нельзя менять; иначе компилятор выдаст ошибку. (Для static-полей это правило не в том виде.)

1.4 Stream API

Stream API (с Java 8) даёт функциональный способ обрабатывать коллекции. Stream — последовательность элементов с поддержкой последовательных и параллельных агрегирующих операций.

1.4.1 Создание потоков

У коллекций есть метод stream(), возвращающий поток:

List<Integer> numbers = Arrays.asList(5, 9, 8, 8, 1);
Stream<Integer> numberStream = numbers.stream();
1.4.2 Операции над потоком

Операции делятся на два класса.

Intermediate operations (промежуточные — возвращают новый поток, можно чейнить):

  • filter(Predicate) — оставить элементы по условию
  • map(Function) — преобразовать каждый элемент
  • flatMap(Function) — «сплющить» вложенные структуры
  • sorted() — сортировка
  • distinct() — убрать дубликаты
  • peek(Consumer) — побочное действие без изменения потока как значения
  • skip(n) — пропустить первые n элементов
  • limit(n) — взять не больше n первых элементов

Terminal operations (терминальные — дают результат или побочный эффект и завершают конвейер):

  • forEach(Consumer) — действие для каждого элемента
  • toArray() — собрать в массив
  • collect(Collector) — собрать в коллекцию и т.п.
  • findFirst() — первый элемент
  • findAny() — произвольный элемент (удобно в параллельных потоках)
  • count() — число элементов
  • reduce() — свёртка в одно значение

Конвейер (stream pipeline) состоит из:

  1. источника (например коллекции);
  2. нуля или нескольких промежуточных операций;
  3. ровно одной терминальной операции.
1.4.3 Важные свойства потоков

Поток не меняет исходную коллекцию:

List<Integer> numbers = Arrays.asList(5, 9, 8, 8, 1);
numbers.stream()
    .filter(n -> n > 5)
    .forEach(System.out::println);  // Prints: 9, 8, 8
System.out.println(numbers);  // Still: [5, 9, 8, 8, 1]

Операции над потоком не меняют numbers. Чтобы сохранить результат, нужно, например, собрать его через collect():

List<Integer> filtered = numbers.stream()
    .filter(n -> n > 5)
    .collect(Collectors.toList());
1.5 Регулярные выражения (regex)

Regular expressions (regex) — шаблоны для сопоставления с подстроками; удобны для фильтрации и проверки строк, в том числе вместе со Stream API.

1.5.1 Частый синтаксис regex
  • . — любой один символ
  • [abc] — один из символов a, b, c
  • [a-z] — символ из диапазона
  • [^abc] — ни один из перечисленных
  • ^ — начало (в зависимости от контекста)
  • $ — конец
  • * — ноль или больше раз (жадный квантификатор)
  • + — один или больше раз
  • ? — ноль или один раз
  • {n} — ровно n раз
  • {n,m} — от n до m раз
  • {n,} — не менее n раз
  • \d — цифра [0-9]
  • \D — не цифра
  • \w — «словесный» символ [a-zA-Z0-9_]
  • \W — не \w
  • \s — пробельный символ
  • \S — не пробельный
  • | — альтернатива
  • () — группа
  • \ — экранирование спецсимвола
1.5.2 Regex в Java

У String есть методы для работы с шаблонами:

  • matches(regex) — совпадение всей строки с шаблоном
  • replaceAll(regex, replacement) — заменить все вхождения
  • split(regex) — разбить по вхождениям

Удалить все цифры из строки:

String clean = str.replaceAll("\\d", "");  // убрать все цифры

Оставить только строки без цифр:

list.stream()
    .filter(s -> !s.matches(".*\\d.*"))  // строки без цифр
1.6 Параллелизм и многопоточность

Concurrency (параллельное выполнение) — возможность одновременно выполнять несколько частей программы. На многоядерных машинах multithreading (многопоточность) помогает использовать ядра.

1.6.1 Процессы и потоки

Process (процесс) — изолированная программа со своим адресным пространством; процессы напрямую память друг друга не делят.

Thread (поток) — лёгкая единица выполнения внутри процесса; потоки одного процесса разделяют память и могут обращаться к общим данным. У каждого потока:

  • свой стек вызовов;
  • свои локальные переменные;
  • свой кэш (в модели памяти — нюансы видимости).

Общее у потоков одного процесса:

  • куча (heap) процесса;
  • static-поля;
  • ссылки на одни и те же объекты.
1.6.2 Создание потоков в Java

Два типичных пути:

1. Реализовать Runnable:

public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    
    public static void main(String[] args) {
        (new Thread(new HelloRunnable())).start();
    }
}

2. Наследовать Thread:

public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from a thread!");
    }
    
    public static void main(String[] args) {
        (new HelloThread()).start();
    }
}

Runnablefunctional interface (единственный абстрактный метод run()), поэтому удобно писать лямбду:

Thread thread = new Thread(() -> System.out.println("Hello from lambda thread!"));
thread.start();
1.6.3 start() и run()

Критично различать:

  • start() — создаёт новый поток и в нём вызывает run();
  • run() — обычный вызов метода в текущем потоке, нового потока нет.

Для запуска параллельного выполнения всегда нужен start(). Прямой вызов run() многопоточности не даёт.

1.6.4 Синхронизация и гонки

При одновременном доступе к общим данным возможны race conditions (состояния гонки) и неверные результаты.

Пример гонки:

class Counter {
    private int counter = 0;
    
    public void increment() {
        counter++;  // NOT atomic! Actually three steps:
                    // 1. Read current value
                    // 2. Add 1
                    // 3. Write back
    }
    
    public int getValue() {
        return counter;
    }
}

Если два потока одновременно вызовут increment(), оба могут прочитать одно и то же значение, увеличить и записать — одно увеличение «потеряется».

Решение: ключевое слово synchronized

synchronized задаёт взаимное исключение (mutex): в каждый момент времени фрагмент кода может выполнять только один поток:

class Counter {
    private int counter = 0;
    
    public synchronized void increment() {
        counter++;
    }
    
    public synchronized int getValue() {
        return counter;
    }
}

Счётчик при нескольких потоках обновится корректно.

1.6.5 Deadlock

Deadlock (взаимная блокировка) — потоки навсегда ждут друг друга, удерживая ресурсы. Часто из‑за того, что:

  • нужны одни и те же блокировки;
  • порядок их захвата у потоков разный.

Например, поток A держит Lock 1 и ждёт Lock 2, а поток B держит Lock 2 и ждёт Lock 1 — оба зависнут.

Профилактика: у всех потоков один и тот же порядок захвата блокировок.

1.6.6 Исключения в потоках

Исключение в потоке обычно гасит только этот поток — остальные продолжают работу; в поток, который вызвал start(), оно «вверх» не пробрасывается. Обрабатывайте исключения внутри run() через try/catch.


2. Определения

  • Functional programming: парадигма, в которой вычисление — вычисление функций, без опоры на изменяемое состояние и «мутабельные» данные там, где это возможно.
  • Imperative programming: парадигма последовательных операторов, меняющих состояние; акцент на как выполняется программа.
  • Pure function: функция без побочных эффектов, результат зависит только от аргументов; на одних входах — один и тот же выход.
  • Side effect: изменение состояния вне области функции (глобальные переменные, поля объектов, I/O и т.п.).
  • Referential transparency: свойство выражения быть заменённым на его значение без изменения поведения программы (типично для чистых функций).
  • Lambda expression: анонимная функция, записываемая как (parameters) -> expression или с блоком { ... }.
  • Anonymous function: функция без имени; часто короткий фрагмент логики «на месте».
  • First-class object (значение первого класса): сущность, которую можно присвоить, передать, вернуть, создать в рантайме; функции в FP — такие значения.
  • Functional interface: интерфейс Java ровно с одним абстрактным методом; помечают @FunctionalInterface, реализуют лямбдой.
  • Method reference: сокращение для лямбды, только вызывающей метод: ClassName::methodName или object::methodName.
  • Closure: лямбда, захватывающая переменные внешней области; они должны быть effectively final.
  • Effectively final: переменная не объявлена final, но после инициализации не переприсваивается — её можно захватывать в лямбде.
  • Stream: последовательность элементов с операциями агрегирования (в т.ч. параллельными); функциональный взгляд на обработку коллекций.
  • Intermediate operation: операция потока, возвращающая новый поток (цепочка: filter(), map(), sorted()…).
  • Terminal operation: операция, завершающая конвейер и дающая результат или побочный эффект (forEach(), collect(), count()…).
  • Regular expression (regex): шаблон символов для поиска и преобразования строк.
  • Concurrency: одновременное выполнение частей программы или нескольких программ; рост пропускной способности и отзывчивости.
  • Process: изолированная программа со своим адресным пространством.
  • Thread: поток выполнения внутри процесса; общая память с другими потоками процесса, свой стек.
  • Race condition: гонка при одновременном доступе к общим данным; результат может зависеть от порядка и времени шагов.
  • Synchronization: согласование доступа к общим ресурсам (блокировки, synchronized).
  • Mutex: взаимное исключение — не более одного потока в критической секции.
  • Deadlock: взаимная блокировка потоков, ожидающих ресурсы друг у друга.
  • Predicate<T>: функциональный интерфейс T -> boolean (test).
  • Consumer<T>: T -> void (accept), часто побочные эффекты.
  • Supplier<T>: () -> T (get).
  • Function<T,R>: T -> R (apply).
  • UnaryOperator<T>: T -> T (apply).
  • BinaryOperator<T>: (T,T) -> T (apply).

3. Примеры

3.1. Атрибуты лямбда-выражения (Лаба 13, Задание 1)

Опишите атрибуты следующего лямбда-выражения:

(o) -> o.toString();
Нажмите, чтобы увидеть решение

Ключевая идея: разобрать структуру лямбды — параметры, вывод типов, тело и соответствие functional interface.

Атрибуты:

  1. Параметры: один параметр o (тип выводится из контекста)
  2. Типы параметров: не указаны явно; компилятор выведет их по functional interface
  3. Тип результата: String (как у toString())
  4. Тело: одно выражение o.toString() — вызов toString() у параметра
  5. Functional interface: лямбда подойдёт к любому интерфейсу с одним абстрактным методом, если:
    • метод принимает один параметр (любого типа)
    • метод возвращает String

Например, это может быть Function<Object, String> или похожий контракт.

Ответ: лямбда принимает один объект и возвращает String через toString(); тип параметра выводится из контекста, тело — одно выражение.

3.2. Фильтрация и преобразование списка (Лаба 13, Задание 1)

Создайте список целых чисел, заполните его случайными положительными и отрицательными значениями. С помощью лямбд выведите все, что делятся на 3, и уберите знак «минус», если он был.

Нажмите, чтобы увидеть решение

Ключевая идея: Stream API: filter() отбирает элементы, map() преобразует; лямбды задают условия и преобразования.

Решение:

import java.util.*;
import java.util.stream.*;

public class Exercise1 {
    public static void main(String[] args) {
        // Create list with random positive and negative values
        List<Integer> numbers = new ArrayList<>();
        Random random = new Random();
        
        // Fill with 20 random numbers between -50 and 50
        for (int i = 0; i < 20; i++) {
            numbers.add(random.nextInt(101) - 50);
        }
        
        System.out.println("Original list: " + numbers);
        
        // Filter divisible by 3 and convert to absolute value
        System.out.println("Numbers divisible by 3 (absolute):");
        numbers.stream()
            .filter(n -> n % 3 == 0)           // Keep only divisible by 3
            .map(n -> Math.abs(n))             // Remove negative sign
            .forEach(n -> System.out.print(n + " "));
        
        System.out.println();
    }
}

Пошаговое объяснение:

  1. Список: ArrayList<Integer> и заполнение через Random.nextInt()
  2. Делимость на 3: filter(n -> n % 3 == 0) оставляет числа, кратные 3
  3. Без минуса: map(n -> Math.abs(n)) даёт абсолютное значение
  4. Вывод: forEach(n -> System.out.print(n + " ")) печатает каждый элемент

Альтернативное решение со ссылками на методы:

numbers.stream()
    .filter(n -> n % 3 == 0)
    .map(Math::abs)                    // Method reference
    .forEach(System.out::println);     // Method reference

Пример вывода:

Original list: [-45, 12, -30, 7, 18, -9, 22, 0, 33, -27, 8, 15, -3, 41, 6, -18, 29, 21, -12, 36]
Numbers divisible by 3 (absolute):
45 12 30 18 9 0 33 27 15 3 6 18 21 12 36

Ответ: см. решение выше: stream(), filter(), map(), forEach() с лямбдами.

3.3. Смысл выражения String::valueOf (Лаба 13, Задание 2)

Что означает выражение String::valueOf?

Нажмите, чтобы увидеть решение

Ключевая идея: method reference — сокращённая запись лямбды, которая только вызывает уже существующий метод.

String::valueOfmethod reference на статический метод valueOf класса String.

Эквивалентность:

// Method reference
Function<Integer, String> converter1 = String::valueOf;

// Equivalent lambda expression
Function<Integer, String> converter2 = x -> String.valueOf(x);

Использование:

Метод valueOf переводит значения разных типов (int, double, Object, …) в String. Ссылку String::valueOf можно подставить везде, где ожидается functional interface, совпадающий по сигнатуре с одной из перегрузок valueOf.

Пример:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> strings = numbers.stream()
    .map(String::valueOf)  // Convert each integer to String
    .collect(Collectors.toList());

Ответ: String::valueOfmethod reference к статическому String.valueOf, приводящему значение к строке; эквивалентно x -> String.valueOf(x).

3.4. Фильтрация строк с помощью regex (Лаба 13, Задание 2)

Создайте список строк со случайным содержимым (латинские буквы и цифры, разная длина). Продублируйте все элементы, добавив копии в тот же список. С помощью лямбд выведите отсортированный список непустых уникальных строк без цифр.

Подсказка: используйте регулярное выражение.

Нажмите, чтобы увидеть решение

Ключевая идея: поток + regex для фильтрации строк; distinct() и sorted() — уникальные отсортированные значения.

Решение:

import java.util.*;
import java.util.stream.*;

public class Exercise2 {
    public static void main(String[] args) {
        // Create list with random strings
        List<String> strings = new ArrayList<>();
        Random random = new Random();
        
        // Generate 10 random strings with letters and numbers
        for (int i = 0; i < 10; i++) {
            StringBuilder sb = new StringBuilder();
            int length = random.nextInt(8) + 1; // Length 1-8
            
            for (int j = 0; j < length; j++) {
                // Random letter or digit
                if (random.nextBoolean()) {
                    // Add letter (a-z or A-Z)
                    char letter = random.nextBoolean() ?
                        (char)('a' + random.nextInt(26)) :
                        (char)('A' + random.nextInt(26));
                    sb.append(letter);
                } else {
                    // Add digit (0-9)
                    sb.append(random.nextInt(10));
                }
            }
            strings.add(sb.toString());
        }
        
        // Add some strings with only letters for testing
        strings.add("hello");
        strings.add("world");
        strings.add("");  // Empty string to test filtering
        
        // Duplicate all values
        List<String> duplicated = new ArrayList<>(strings);
        strings.addAll(duplicated);
        
        System.out.println("Original list with duplicates: " + strings);
        
        // Filter: non-empty, unique, no numbers, sorted
        System.out.println("\nFiltered result:");
        strings.stream()
            .filter(s -> !s.isEmpty())              // Remove empty strings
            .filter(s -> !s.matches(".*\\d.*"))     // Remove strings with digits
            .distinct()                             // Remove duplicates
            .sorted()                               // Sort alphabetically
            .forEach(System.out::println);
    }
}

Разбор шаблона regex:

  • .*\\d.* читается так:
    • .* — любая последовательность символов (в т.ч. пустая)
    • \\d — одна цифра
    • .* — любая последовательность символов (в т.ч. пустая)
  • в целом шаблон совпадает со строкой, где есть хотя бы одна цифра
  • !s.matches(".*\\d.*") даёт true для строк без цифр

Цепочка операций потока:

  1. filter(s -> !s.isEmpty()) — убрать пустые строки
  2. filter(s -> !s.matches(".*\\d.*")) — оставить только строки без цифр
  3. distinct() — убрать дубликаты
  4. sorted() — сортировка по естественному порядку String
  5. forEach(System.out::println) — печать каждого элемента

Пример вывода:

Original list with duplicates: [a3b, Xy2Z, hello, 9test, world, abc, X5Y, , abc7, letters, word9, a3b, Xy2Z, hello, 9test, world, abc, X5Y, , abc7, letters, word9, hello, world, ]

Filtered result:
abc
hello
letters
world

Альтернатива: collect() в новый список:

List<String> filtered = strings.stream()
    .filter(s -> !s.isEmpty())
    .filter(s -> !s.matches(".*\\d.*"))
    .distinct()
    .sorted()
    .collect(Collectors.toList());

System.out.println(filtered);

Ответ: см. решение: filter с шаблоном .*\\d.* отсекает строки с цифрами; далее distinct() и sorted().

3.5. Поток без присваивания результата (Лаба 13, Задание 3)

Если к Set применить Stream API и не присвоить результат обратно, изменится ли исходное множество?

Нажмите, чтобы увидеть решение

Ключевая идея: операции потока не меняют исходную коллекцию; меняется только значение нового потока/результата после терминала.

Нет, промежуточные операции сами по себе не меняют исходную коллекцию. Явная мутация через forEach с побочными эффектами возможна, но это антипаттерн для потоков.

Пример:

Set<Integer> numbers = new HashSet<>(Arrays.asList(5, 9, 8, 1));

// Исходный `numbers` не меняется
numbers.stream()
    .filter(n -> n > 5)
    .map(n -> n * 2);

System.out.println(numbers);  // Still: [1, 5, 8, 9]

// Чтобы сохранить результат, нужен терминал вроде `collect`
Set<Integer> modified = numbers.stream()
    .filter(n -> n > 5)
    .map(n -> n * 2)
    .collect(Collectors.toSet());

System.out.println(modified);  // [16, 18]
System.out.println(numbers);   // Still: [1, 5, 8, 9]

Почему? Потоки следуют функциональной модели: описывают новые потоки значений, а не «ломают» исходную коллекцию — так проще рассуждать о корректности и параллелизме.

Ответ: нет: без сохранения результата исходная коллекция не меняется — поток лишь описывает конвейер над данными.

3.6. Устранение гонки через synchronized (Лаба 13, Задание 3)

Объект Counter используют несколько потоков (например thread1 и thread2). Оператор counter++ не атомарен и раскладывается на три шага:

  1. Прочитать текущее значение
  2. Увеличить прочитанное значение на 1
  3. Записать результат обратно в поле
class Counter {
    private int counter = 0;
    public void increment() {
        counter++;
    }
    public int getValue() {
        return counter;
    }
}

Устраните race condition, чтобы обращения к счётчику из потоков шли корректно: чтение и запись счётчика должны выполняться последовательно (взаимное исключение).

Нажмите, чтобы увидеть решение

Ключевая идея: synchronized даёт mutex: критическая секция выполняется не более чем в одном потоке одновременно.

Проблема:

Операция counter++ не атомарна. Несколько потоков могут одновременно испортить друг другу шаги read–modify–write:

Время | Поток 1               | Поток 2               | Значение счётчика
-----|----------------------|----------------------|---------------
0    |                      |                      | 0
1    | Чтение counter (0)   |                      | 0
2    | Увеличить до 1        | Чтение counter (0)   | 0
3    | Запись 1             | Увеличить до 1       | 1
4    |                      | Запись 1             | 1

Итог: оба потока «увеличили», но в поле осталась 1 вместо 2!

Решение 1: synchronized-методы

class Counter {
    private int counter = 0;
    
    public synchronized void increment() {
        counter++;  // теперь потокобезопасно
    }
    
    public synchronized int getValue() {
        return counter;  // согласованное чтение
    }
}

Решение 2: блок synchronized

class Counter {
    private int counter = 0;
    private final Object lock = new Object();
    
    public void increment() {
        synchronized(lock) {
            counter++;
        }
    }
    
    public int getValue() {
        synchronized(lock) {
            return counter;
        }
    }
}

Решение 3: AtomicInteger (современный вариант)

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet();  // атомарно
    }
    
    public int getValue() {
        return counter.get();
    }
}

Проверка решения:

public class TestCounter {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        // два потока, каждый +1000 раз
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        
        t1.start();
        t2.start();
        
        // дождаться обоих потоков
        t1.join();
        t2.join();
        
        System.out.println("Final counter value: " + counter.getValue());
        // при корректной синхронизации ожидается 2000
    }
}

Почему это устраняет гонку:

  • synchronized создаёт mutex (взаимное исключение)
  • замок может удерживать только один поток
  • остальные ждут освобождения замка
  • с точки зрения других потоков три микрошага counter++ сливаются в неделимый фрагмент

Ответ: пометьте increment() и getValue() как synchronized, чтобы взаимное исключение убрало гонку при доступе к счётчику.

3.7. Неабстрактные методы в функциональном интерфейсе (Лаба 13, Задание 4)

Сколько неабстрактных (default) методов допускается в functional interface? (Имеется в виду: есть ли лимит.)

Нажмите, чтобы увидеть решение

Ключевая идея: у functional interface ровно один абстрактный метод; default/static методов может быть сколько угодно.

Ответ: неограниченно — число default- и static-методов в functional interface не ограничено; важен ровно один абстрактный.

Правила:

  • должен быть ровно один абстрактный метод (не считая особых случаев компилятора)
  • default-методов — любое количество
  • static-методов — любое количество
  • методы Object (equals, hashCode, toString) не считаются дополнительными SAM для этого правила

Пример:

@FunctionalInterface
interface MyInterface {
    // один абстрактный метод (обязательно)
    void doSomething(String s);
    
    // несколько default-методов (разрешено)
    default void method1() {
        System.out.println("Default method 1");
    }
    
    default void method2() {
        System.out.println("Default method 2");
    }
    
    default void method3() {
        System.out.println("Default method 3");
    }
    
    // static-методы (тоже разрешены)
    static void staticMethod() {
        System.out.println("Static method");
    }
}

Это по-прежнему functional interface: абстрактный метод один, остальное — default и static.

3.8. Когда Runnable, а когда наследовать Thread (Лаба 13, Задание 5)

Когда в Java использовать Runnable, а когда наследовать Thread?

Нажмите, чтобы увидеть решение

Ключевая идея: чаще реализуют Runnable, а не наследуют Thread — гибче композиция и наследование класса остаётся свободным.

Выбирайте Runnable, если:

  • нужно разделить задачу и механизм её запуска
  • класс уже наследует другой класс (множественного наследования в Java нет)
  • нужно реализовать несколько интерфейсов
  • важна композиция вместо наследования «ради потока»
  • планируются пулы потоков и ExecutorService
  • Runnablefunctional interface, удобны лямбды

Наследуйте Thread, если:

  • нужно переопределять не только run(), но и прочее поведение Thread
  • действительно нужен специализированный подкласс Thread
  • (редко; чаще это не лучший путь)

Сравнение:

// Runnable (предпочтительно)
public class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task running");
    }
}
// использование
Thread thread = new Thread(new MyTask());
thread.start();

// или лямбда (Runnable — functional interface)
Thread thread = new Thread(() -> System.out.println("Task running"));
thread.start();

// наследование Thread (менее гибко)
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
// использование
MyThread thread = new MyThread();
thread.start();

Ответ: в большинстве случаев предпочтительнее Runnable: класс может наследовать другое, проще композиция, лямбды и API java.util.concurrent. Наследовать Thread имеет смысл, если нужны специфические переопределения поведения потока.

3.9. Разница между start() и run() (Лаба 13, Задание 6)

В чём разница между start() и run() у класса Thread?

Нажмите, чтобы увидеть решение

Ключевая идея: start() создаёт новый поток и вызывает в нём run(); прямой run() — без нового потока.

Фундаментальная разница:

  • start(): создаёт новый поток и в нём вызывает run()
  • run(): выполняет run() в текущем потоке (новый поток не создаётся)

Подробное сравнение:

Аспект start() run()
Новый поток? Да Нет
Параллельно с вызывающим? Да Нет
Повторный вызов Нет (IllegalThreadStateException) Да (обычный метод)
Роль Запуск выполнения в новом потоке Тело задачи потока

Пример:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running in thread: " + 
            Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        
        // `start()` — новые потоки
        t1.start();  // Output: Running in thread: Thread-0
        t2.start();  // Output: Running in thread: Thread-1
        
        // `run()` — без нового потока
        MyThread t3 = new MyThread();
        t3.run();    // Output: Running in thread: main
    }
}

Наглядно:

start() -> JVM creates new thread -> new thread calls run()
run()   -> Current thread executes run() directly

Ответ: start() создаёт новый поток и вызывает в нём run(); прямой run() — обычный вызов в текущем потоке, параллелизма нет. Запуск — только через start().

3.10. Исключение в потоке Java (Лаба 13, Задание 7)

Что происходит, если в потоке Java возникает исключение?

Нажмите, чтобы увидеть решение

Ключевая идея: исключение в потоке изолируется в этом потоке и не всплывает к потоку, вызвавшему start().

Что происходит:

  1. Поток завершается: выполнение потока, где было исключение, прекращается
  2. Остальные продолжают: другие потоки не останавливаются автоматически
  3. Нет распространения вверх: к потоку, вызвавшему start(), исключение не пробрасывается
  4. Стектрейс: по умолчанию неперехваченное исключение печатается в консоль
  5. UncaughtExceptionHandler: если задан, вызывается обработчик потока

Пример:

public class ThreadExceptionDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("Thread 1 starting");
            throw new RuntimeException("Exception in Thread 1");
        });
        
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(100);
                System.out.println("Thread 2 still running!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        t1.start();
        t2.start();
        
        System.out.println("Main thread continues");
    }
}

Вывод:

Thread 1 starting
Main thread continues
Exception in thread "Thread-0" java.lang.RuntimeException: Exception in Thread 1
    at ThreadExceptionDemo.lambda$main$0(ThreadExceptionDemo.java:5)
    at java.lang.Thread.run(Thread.java:748)
Thread 2 still running!

Обработка исключений в потоках:

Thread thread = new Thread(() -> {
    try {
        // код потока, где может быть исключение
        int result = 10 / 0;
    } catch (Exception e) {
        System.out.println("Caught exception: " + e.getMessage());
        // Handle appropriately
    }
});
thread.start();

Ответ: исключение гасит текущий поток (со стектрейсом), остальные продолжают; к породившему потоку оно не «всплывает». Обрабатывайте в run() через try/catch.

3.11. Лямбда и свой функциональный интерфейс (Лекция 13, Пример 1)

Объявите свой functional interface и реализуйте его лямбда-выражением.

Нажмите, чтобы увидеть решение

Ключевая идея: любой интерфейс с одним абстрактным методом можно реализовать лямбдой с подходящей сигнатурой.

Свой functional interface:

@FunctionalInterface
interface Func {
    int action(int x, int y);
}

class SomeClass {
    // лямбда реализует интерфейс
    Func lambda = (int x, int y) -> { return x + y; };
    
    public void demonstrateLambda() {
        // вызов лямбды
        int result = lambda.action(5, 3);
        System.out.println("Result: " + result);  // 8
    }
    
    public static void main(String[] args) {
        SomeClass obj = new SomeClass();
        obj.demonstrateLambda();
    }
}

Упрощённый вариант:

@FunctionalInterface
interface Func {
    int action(int x, int y);
}

public class LambdaDemo {
    public static void main(String[] args) {
        // разные лямбды для одного интерфейса
        Func sum = (x, y) -> x + y;
        Func multiply = (x, y) -> x * y;
        Func max = (x, y) -> x > y ? x : y;
        
        System.out.println("Sum: " + sum.action(5, 3));        // 8
        System.out.println("Multiply: " + multiply.action(5, 3)); // 15
        System.out.println("Max: " + max.action(5, 3));        // 5
    }
}

Лямбда как параметр метода:

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

public class Calculator {
    // метод принимает functional interface
    public static int calculate(int x, int y, Operation op) {
        return op.execute(x, y);
    }
    
    public static void main(String[] args) {
        // разные операции — разные лямбды
        int sum = calculate(10, 5, (a, b) -> a + b);
        int diff = calculate(10, 5, (a, b) -> a - b);
        int product = calculate(10, 5, (a, b) -> a * b);
        
        System.out.println("Sum: " + sum);       // 15
        System.out.println("Diff: " + diff);     // 5
        System.out.println("Product: " + product); // 50
    }
}

Несколько реализаций:

@FunctionalInterface
interface StringFunction {
    String run(String str);
}

public class Main {
    public static void main(String[] args) {
        StringFunction exclaim = (s) -> s + "!";
        StringFunction ask = (s) -> s + "?";
        StringFunction shout = (s) -> s.toUpperCase() + "!!!";
        
        printFormatted("Hello", exclaim);  // Hello!
        printFormatted("Hello", ask);      // Hello?
        printFormatted("Hello", shout);    // HELLO!!!
    }
    
    public static void printFormatted(String str, StringFunction format) {
        String result = format.run(str);
        System.out.println(result);
    }
}

Ответ: объявите functional interface с @FunctionalInterface и одним абстрактным методом, затем присвойте лямбду совместимой сигнатуры, например Func lambda = (x, y) -> x + y;.

3.12. Ссылка на статический метод (Лекция 13, Пример 2)

Покажите method reference на статические методы.

Нажмите, чтобы увидеть решение

Ключевая идея: method reference заменяет тривиальную лямбду «только вызов метода».

Пример: ссылка на статический метод:

@FunctionalInterface
public interface Finder {
    public int find(String s1, String s2);
}

public class MyClass {
    // Static method
    public static int doFind(String s1, String s2) {
        return s1.lastIndexOf(s2);
    }
}

public class MethodReferenceDemo {
    public static void main(String[] args) {
        // лямбда
        Finder finderLambda = (s1, s2) -> MyClass.doFind(s1, s2);
        
        // method reference (короче)
        Finder finderRef = MyClass::doFind;
        
        // результат одинаковый
        System.out.println(finderLambda.find("Hello World", "o"));  // 7
        System.out.println(finderRef.find("Hello World", "o"));     // 7
    }
}

Ещё примеры статических ссылок:

import java.util.*;
import java.util.function.*;

public class StaticMethodReferences {
    public static void main(String[] args) {
        List<String> numbers = Arrays.asList("1", "2", "3", "4", "5");
        
        // Integer::parseInt
        List<Integer> integers = numbers.stream()
            .map(Integer::parseInt)  // Same as: s -> Integer.parseInt(s)
            .collect(Collectors.toList());
        System.out.println(integers);  // [1, 2, 3, 4, 5]
        
        // Math::abs
        List<Integer> nums = Arrays.asList(-5, 3, -2, 8, -1);
        List<Integer> absolute = nums.stream()
            .map(Math::abs)  // Same as: n -> Math.abs(n)
            .collect(Collectors.toList());
        System.out.println(absolute);  // [5, 3, 2, 8, 1]
        
        // String::valueOf
        List<Integer> values = Arrays.asList(10, 20, 30);
        List<String> strings = values.stream()
            .map(String::valueOf)  // Same as: n -> String.valueOf(n)
            .collect(Collectors.toList());
        System.out.println(strings);  // [10, 20, 30]
    }
}

Статическая ссылка для сравнения:

import java.util.*;

class Person {
    String name;
    int age;
    
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    static int compareByAge(Person p1, Person p2) {
        return Integer.compare(p1.age, p2.age);
    }
    
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class ComparisonExample {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 35)
        );
        
        // Sort using static method reference
        people.sort(Person::compareByAge);
        
        System.out.println(people);
        // [Bob (25), Alice (30), Charlie (35)]
    }
}

Ответ: для статики — ИмяКласса::статическийМетод; например MyClass::doFind эквивалентно (s1,s2) -> MyClass.doFind(s1,s2).

3.13. Ссылка на метод экземпляра (Лекция 13, Пример 3)

Покажите method reference на методы экземпляра.

Нажмите, чтобы увидеть решение

Ключевая идея: ссылка obj::meth фиксирует конкретный экземпляр obj.

Пример: ссылка на метод экземпляра:

@FunctionalInterface
public interface Deserializer {
    public int deserialize(String v1);
}

public class StringConverter {
    public int convertToInt(String v1) {
        return Integer.valueOf(v1);
    }
}

public class InstanceMethodDemo {
    public static void main(String[] args) {
        StringConverter stringConverter = new StringConverter();
        
        // лямбда
        Deserializer desLambda = (v1) -> stringConverter.convertToInt(v1);
        
        // Using instance method reference (cleaner)
        Deserializer desRef = stringConverter::convertToInt;
        
        // результат одинаковый
        System.out.println(desLambda.deserialize("42"));   // 42
        System.out.println(desRef.deserialize("100"));     // 100
    }
}

Типичные ссылки на методы экземпляра:

import java.util.*;

public class InstanceReferences {
    public static void main(String[] args) {
        // Reference to println method of System.out object
        List<String> words = Arrays.asList("Hello", "World", "Java");
        words.forEach(System.out::println);
        
        // Reference to instance method
        String prefix = "Item: ";
        Function<String, String> addPrefix = prefix::concat;
        System.out.println(addPrefix.apply("Apple"));  // Item: Apple
        
        // Multiple instance method references
        List<String> names = Arrays.asList("alice", "bob", "charlie");
        names.stream()
            .map(String::toUpperCase)  // Instance method of String class
            .forEach(System.out::println);
        // ALICE
        // BOB
        // CHARLIE
    }
}

Ссылка на метод у конкретных объектов:

class Printer {
    private String prefix;
    
    public Printer(String prefix) {
        this.prefix = prefix;
    }
    
    public void print(String message) {
        System.out.println(prefix + message);
    }
}

public class PrinterDemo {
    public static void main(String[] args) {
        Printer errorPrinter = new Printer("[ERROR] ");
        Printer infoPrinter = new Printer("[INFO] ");
        
        // Instance method references
        Consumer<String> logError = errorPrinter::print;
        Consumer<String> logInfo = infoPrinter::print;
        
        logError.accept("Something went wrong");  // [ERROR] Something went wrong
        logInfo.accept("Application started");    // [INFO] Application started
        
        // Use with streams
        List<String> messages = Arrays.asList("Starting", "Processing", "Complete");
        messages.forEach(infoPrinter::print);
        // [INFO] Starting
        // [INFO] Processing
        // [INFO] Complete
    }
}

Ответ: для метода экземпляра — объект::метод, например после StringConverter converter = new StringConverter(); пишут converter::convertToInt.

3.14. Ссылка на конструктор (Лекция 13, Пример 4)

Покажите method reference на конструктор (ClassName::new).

Нажмите, чтобы увидеть решение

Ключевая идея: ClassName::new — ссылка на конструктор, согласованная с сигнатурой functional interface.

Пример: ссылка на конструктор:

@FunctionalInterface
public interface Factory {
    public String create(char[] val);
}

public class ConstructorReferenceDemo {
    public static void main(String[] args) {
        // лямбда
        Factory factory1 = chars -> new String(chars);
        
        // Using constructor reference (cleaner)
        Factory factory2 = String::new;
        
        // результат одинаковый
        char[] chars = {'H', 'e', 'l', 'l', 'o'};
        System.out.println(factory1.create(chars));  // Hello
        System.out.println(factory2.create(chars));  // Hello
    }
}

Ссылки на конструкторы разных типов:

import java.util.*;
import java.util.function.*;

class Person {
    private String name;
    private int age;
    
    // Constructor with String parameter
    public Person(String name) {
        this.name = name;
        this.age = 0;
    }
    
    // Constructor with String and int parameters
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class ConstructorReferences {
    public static void main(String[] args) {
        // Reference to single-parameter constructor
        Function<String, Person> personFactory1 = Person::new;
        Person p1 = personFactory1.apply("Alice");
        System.out.println(p1);  // Alice (0)
        
        // Reference to two-parameter constructor
        BiFunction<String, Integer, Person> personFactory2 = Person::new;
        Person p2 = personFactory2.apply("Bob", 25);
        System.out.println(p2);  // Bob (25)
        
        // Using with streams
        List<String> names = Arrays.asList("Charlie", "David", "Eve");
        List<Person> people = names.stream()
            .map(Person::new)  // Creates Person for each name
            .collect(Collectors.toList());
        people.forEach(System.out::println);
        // Charlie (0)
        // David (0)
        // Eve (0)
    }
}

Ссылки на конструкторы коллекций:

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class CollectionConstructors {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B", "C", "D", "E");
        
        // Constructor reference for ArrayList
        Supplier<List<String>> listFactory = ArrayList::new;
        List<String> newList = listFactory.get();
        newList.addAll(list);
        System.out.println(newList);  // [A, B, C, D, E]
        
        // Constructor reference for HashSet
        Supplier<Set<String>> setFactory = HashSet::new;
        Set<String> newSet = setFactory.get();
        newSet.addAll(list);
        System.out.println(newSet);  // [A, B, C, D, E]
        
        // Convert list to set using constructor reference
        Set<String> set = list.stream()
            .collect(Collectors.toCollection(HashSet::new));
        System.out.println(set);  // [A, B, C, D, E]
    }
}

Ссылка на конструктор массива:

import java.util.*;
import java.util.function.*;

public class ArrayConstructorReference {
    public static void main(String[] args) {
        // Array constructor reference
        IntFunction<String[]> arrayFactory = String[]::new;
        
        String[] array = arrayFactory.apply(5);  // Creates array of size 5
        System.out.println("Array length: " + array.length);  // 5
        
        // Using with streams
        List<String> list = Arrays.asList("One", "Two", "Three");
        String[] array2 = list.stream()
            .toArray(String[]::new);  // Convert to array
        System.out.println(Arrays.toString(array2));  // [One, Two, Three]
    }
}

Ответ: конструктор — ИмяКласса::new, например String::new эквивалентно chars -> new String(chars); перегрузка выбирается по сигнатуре SAM.

3.15. Лямбда и захват переменных (closures) (Лекция 13, Пример 5)

Покажите, как лямбда захватывает переменные внешней области (closure, effectively final).

Нажмите, чтобы увидеть решение

Ключевая идея: замыкание захватывает внешние переменные, но только effectively final.

Базовый пример замыкания:

@FunctionalInterface
public interface Factory {
    public String create(char[] val);
}

public class SomeClass {
    String myString = "Test";
    
    // Lambda captures myString variable
    Factory myFactory = (chars) -> myString + ":" + new String(chars);
    
    public void demonstrate() {
        char[] data = {'H', 'i'};
        System.out.println(myFactory.create(data));  // Test:Hi
    }
    
    public static void main(String[] args) {
        SomeClass obj = new SomeClass();
        obj.demonstrate();
    }
}

Требование effectively final:

public class ClosureDemo {
    public static void main(String[] args) {
        String prefix = "Hello";  // Effectively final
        int number = 42;          // Effectively final
        
        // Lambda captures both variables
        Runnable task = () -> {
            System.out.println(prefix + " " + number);
        };
        
        task.run();  // Hello 42
        
        // ошибка компиляции:
        // prefix = "Goodbye";  // Can't modify captured variable
        // number = 100;        // Can't modify captured variable
    }
}

Практические примеры замыканий:

import java.util.*;
import java.util.function.*;

public class ClosureExamples {
    public static void main(String[] args) {
        // пример 1: захват для фильтра
        int threshold = 50;
        List<Integer> numbers = Arrays.asList(25, 60, 30, 80, 45, 90);
        
        List<Integer> filtered = numbers.stream()
            .filter(n -> n > threshold)  // Captures threshold
            .collect(Collectors.toList());
        System.out.println(filtered);  // [60, 80, 90]
        
        // пример 2: захват для преобразования
        String suffix = " dollars";
        List<Integer> prices = Arrays.asList(10, 20, 30);
        
        List<String> formatted = prices.stream()
            .map(p -> p + suffix)  // Captures suffix
            .collect(Collectors.toList());
        System.out.println(formatted);  // [10 dollars, 20 dollars, 30 dollars]
        
        // пример 3: несколько захваченных переменных
        String greeting = "Hello";
        String punctuation = "!";
        
        Function<String, String> greet = name -> 
            greeting + ", " + name + punctuation;
        
        System.out.println(greet.apply("World"));  // Hello, World!
        System.out.println(greet.apply("Java"));   // Hello, Java!
    }
}

Счётчик и зачем нужен effectively final:

public class CounterClosure {
    public static void main(String[] args) {
        // нельзя: переменная должна быть effectively final
        /*
        int counter = 0;
        Runnable increment = () -> {
            counter++;  // ERROR: Cannot modify captured variable
        };
        */
        
        // Solution 1: Use an array (mutable container)
        int[] counter = {0};
        Runnable increment1 = () -> {
            counter[0]++;  // OK - modifying array contents, not array reference
        };
        
        // Solution 2: Use AtomicInteger
        java.util.concurrent.atomic.AtomicInteger atomicCounter = 
            new java.util.concurrent.atomic.AtomicInteger(0);
        Runnable increment2 = () -> {
            atomicCounter.incrementAndGet();  // OK - calling method on object
        };
        
        // Solution 3: Use a wrapper class
        class Counter {
            int value = 0;
        }
        Counter counterObj = new Counter();
        Runnable increment3 = () -> {
            counterObj.value++;  // OK - modifying field, not object reference
        };
    }
}

Исключение для static-полей:

public class StaticCapture {
    static int staticCounter = 0;
    
    public static void main(String[] args) {
        // Static variables can be modified in lambdas
        Runnable task = () -> {
            staticCounter++;  // OK - static variables are not captured
            System.out.println("Counter: " + staticCounter);
        };
        
        task.run();  // Counter: 1
        task.run();  // Counter: 2
        task.run();  // Counter: 3
    }
}

Ответ: лямбда — closure и может читать внешние переменные, если они effectively final, например String prefix = "Test"; Factory f = (chars) -> prefix + ":" + new String(chars);.

3.16. Императивно и функционально: алгоритм НОД (Лекция 13, Пример 6)

Сравните императивную и функциональную реализации алгоритма Евклида для НОД (GCD).

Нажмите, чтобы увидеть решение

Ключевая идея: один алгоритм можно записать императивно (цикл, мутация) и функционально (рекурсия, без локальной мутации).

Императивно (итерация):

public class ImperativeGCD {
    int gcd(int x, int y) {
        int a = x, b = y;
        while (a != 0) {
            int temp = a;
            a = b % a;
            b = temp;
        }
        return b;
    }
    
    public static void main(String[] args) {
        ImperativeGCD calculator = new ImperativeGCD();
        System.out.println("GCD(48, 18) = " + calculator.gcd(48, 18));  // 6
        System.out.println("GCD(100, 35) = " + calculator.gcd(100, 35)); // 5
        System.out.println("GCD(17, 19) = " + calculator.gcd(17, 19));   // 1
    }
}

Черты императивного подхода:

  • Uses a loop (while)
  • Has three local variables (a, b, temp)
  • Variables change their values on each iteration
  • Organized as a series of steps
  • More verbose

Функционально (рекурсия):

public class FunctionalGCD {
    int gcd(int x, int y) {
        return (y == 0) ? x : gcd(y, x % y);
    }
    
    public static void main(String[] args) {
        FunctionalGCD calculator = new FunctionalGCD();
        System.out.println("GCD(48, 18) = " + calculator.gcd(48, 18));  // 6
        System.out.println("GCD(100, 35) = " + calculator.gcd(100, 35)); // 5
        System.out.println("GCD(17, 19) = " + calculator.gcd(17, 19));   // 1
    }
}

Черты функционального подхода:

  • Uses recursion instead of loops
  • No local variables
  • Parameters never change their values
  • Much more concise and readable
  • Closer to the mathematical definition

Как работает функциональная версия:

gcd(48, 18):
  → gcd(18, 48 % 18)
  → gcd(18, 12)
  → gcd(12, 18 % 12)
  → gcd(12, 6)
  → gcd(6, 12 % 6)
  → gcd(6, 0)
  → 6

Таблица сравнения:

Aspect Imperative Functional
Variables 3 local variables No local variables
Mutation Variables change values Variables immutable
Control flow While loop Recursion
Code length Longer Shorter
Readability More steps to follow Concise, declarative

Обе реализации:

public class GCDComparison {
    // Imperative version
    static int gcdIterative(int x, int y) {
        int a = x, b = y;
        while (a != 0) {
            int temp = a;
            a = b % a;
            b = temp;
        }
        return b;
    }
    
    // Functional version
    static int gcdRecursive(int x, int y) {
        return (y == 0) ? x : gcdRecursive(y, x % y);
    }
    
    public static void main(String[] args) {
        int x = 48, y = 18;
        
        System.out.println("Iterative GCD(" + x + ", " + y + ") = " + 
            gcdIterative(x, y));  // 6
        System.out.println("Recursive GCD(" + x + ", " + y + ") = " + 
            gcdRecursive(x, y));  // 6
    }
}

Замечание: Many “conventional” languages can be used to program in functional style! Java supports both paradigms, allowing you to choose the most appropriate approach for your problem.

Ответ: императивно — цикл и меняющиеся a, b; функционально — рекурсия return (y == 0) ? x : gcd(y, x % y);. Результат тот же, вторая запись короче.

3.17. Чистые и «грязные» функции (Лекция 13, Пример 7)

Сравните чистые и «грязные» функции (с побочными эффектами и зависимостью от состояния).

Нажмите, чтобы увидеть решение

Ключевая идея: чистая функция зависит только от аргументов и не портит окружение; «грязная» может менять состояние и давать разный эффект при тех же входах.

Нечистая функция (есть побочные эффекты):

public class Example1 {
    private int value;
    
    public int add(int next) {
        this.value += next;  // Side effect: modifies object state
        return this.value;
    }
}

public class ImpureDemo {
    public static void main(String[] args) {
        Example1 obj = new Example1();
        
        System.out.println(obj.add(5));   // 5
        System.out.println(obj.add(5));   // 10  (different result!)
        System.out.println(obj.add(5));   // 15  (different result!)
        
        // Same input (5), different outputs each time
        // Result depends on current state
    }
}

Проблемы нечистых функций:

  • Result depends on hidden state
  • Same input produces different outputs
  • Cannot be used in parallel safely
  • Harder to test and debug
  • Cannot be cached/memoized

Чистая функция (без побочных эффектов):

public class Example2 {
    public int sum(int x, int y) {
        return x + y;  // No side effects, only depends on parameters
    }
}

public class PureDemo {
    public static void main(String[] args) {
        Example2 obj = new Example2();
        
        System.out.println(obj.sum(5, 3));  // 8
        System.out.println(obj.sum(5, 3));  // 8  (same result)
        System.out.println(obj.sum(5, 3));  // 8  (same result)
        
        // Same inputs always produce same output
        // No dependency on state
    }
}

Плюсы чистых функций:

  1. Referential transparency — вызов можно заменить на значение результата
  2. Кэшируемость — одинаковые входы, одинаковый выход
  3. Параллелизуемость — вызовы можно переупорядочивать или распараллеливать
  4. Тестируемость — мало подготовки состояния
  5. Удаляемость — если результат не нужен, вызов можно выбросить как мёртвый код

Дополнительные примеры:

public class FunctionComparison {
    // IMPURE: Depends on current time (external state)
    static int getCurrentHour() {
        return java.time.LocalTime.now().getHour();
    }
    
    // PURE: Only depends on parameter
    static int addHours(int currentHour, int hoursToAdd) {
        return (currentHour + hoursToAdd) % 24;
    }
    
    // IMPURE: Modifies external state (prints to console)
    static int calculateAndPrint(int x, int y) {
        int result = x + y;
        System.out.println("Result: " + result);  // Side effect!
        return result;
    }
    
    // PURE: Just returns the calculation
    static int calculate(int x, int y) {
        return x + y;
    }
    
    // IMPURE: Modifies the list
    static void addToList(List<Integer> list, int value) {
        list.add(value);  // Side effect!
    }
    
    // PURE: Returns new list without modifying original
    static List<Integer> withAddedValue(List<Integer> list, int value) {
        List<Integer> newList = new ArrayList<>(list);
        newList.add(value);
        return newList;
    }
    
    public static void main(String[] args) {
        // Testing pure vs impure list operations
        List<Integer> original = new ArrayList<>(Arrays.asList(1, 2, 3));
        
        // Impure: modifies original
        addToList(original, 4);
        System.out.println(original);  // [1, 2, 3, 4] - modified!
        
        // Pure: creates new list
        List<Integer> original2 = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<Integer> modified = withAddedValue(original2, 4);
        System.out.println(original2);  // [1, 2, 3] - unchanged
        System.out.println(modified);   // [1, 2, 3, 4] - new list
    }
}

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

import java.util.*;
import java.util.stream.*;

class BankAccount {
    private double balance;
    
    // IMPURE: Modifies state
    public void depositImpure(double amount) {
        this.balance += amount;  // Side effect
    }
    
    // PURE: Returns new account state
    public BankAccount depositPure(double amount) {
        BankAccount newAccount = new BankAccount();
        newAccount.balance = this.balance + amount;
        return newAccount;  // New object, original unchanged
    }
    
    public double getBalance() {
        return balance;
    }
}

public class BankingExample {
    public static void main(String[] args) {
        // Impure approach
        BankAccount account1 = new BankAccount();
        account1.depositImpure(100);
        account1.depositImpure(50);
        System.out.println("Balance: " + account1.getBalance());  // 150
        
        // Pure approach (functional style)
        BankAccount account2 = new BankAccount();
        BankAccount after1 = account2.depositPure(100);
        BankAccount after2 = after1.depositPure(50);
        System.out.println("Original: " + account2.getBalance());  // 0
        System.out.println("After deposits: " + after2.getBalance());  // 150
    }
}

В Stream API операции по возможности чистые:

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

// All these stream operations use pure functions
List<Integer> result = numbers.stream()
    .filter(n -> n > 2)           // Pure predicate
    .map(n -> n * 2)              // Pure transformation
    .collect(Collectors.toList());

System.out.println(numbers);  // [1, 2, 3, 4, 5] - unchanged!
System.out.println(result);   // [6, 8, 10]

Ответ: «грязная» функция меняет поля объекта (this.value += next) и зависит от истории; чистая — только return x + y без побочных эффектов и с детерминированным результатом.

3.18. Интерфейс Predicate (Туториал 13, Задание 1)

Продемонстрируйте Predicate<T>: проверка, что число положительное.

Нажмите, чтобы увидеть решение

Ключевая идея: Predicate<T> — предикат T -> boolean, метод test.

Определение Predicate:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Пример реализации:

import java.util.function.Predicate;

public class LambdaApp {
    public static void main(String[] args) {
        // Create a predicate using lambda expression
        Predicate<Integer> isPositive = x -> x > 0;
        
        // Test various numbers
        System.out.println(isPositive.test(5));    // true
        System.out.println(isPositive.test(-7));   // false
        System.out.println(isPositive.test(0));    // false
        
        // Can also create other predicates
        Predicate<Integer> isEven = x -> x % 2 == 0;
        System.out.println(isEven.test(4));        // true
        System.out.println(isEven.test(7));        // false
        
        // Combine predicates using and(), or(), negate()
        Predicate<Integer> isPositiveAndEven = isPositive.and(isEven);
        System.out.println(isPositiveAndEven.test(6));   // true
        System.out.println(isPositiveAndEven.test(5));   // false
        System.out.println(isPositiveAndEven.test(-4));  // false
    }
}

Типичное использование со Stream API:

List<Integer> numbers = Arrays.asList(-5, 3, -2, 8, 0, 12, -7);

// Filter using predicate
List<Integer> positiveNumbers = numbers.stream()
    .filter(x -> x > 0)  // Predicate lambda
    .collect(Collectors.toList());

System.out.println(positiveNumbers);  // [3, 8, 12]

Ответ: Predicate<Integer> isPositive = x -> x > 0; — предикат «строго положительное»; проверка isPositive.test(5) даёт true.

3.19. Интерфейс UnaryOperator (Туториал 13, Задание 2)

Продемонстрируйте UnaryOperator<T>: возведение числа в квадрат.

Нажмите, чтобы увидеть решение

Ключевая идея: UnaryOperator<T>T -> T, один и тот же тип на входе и выходе.

Определение UnaryOperator:

@FunctionalInterface
public interface UnaryOperator<T> {
    T apply(T t);
}

Пример реализации:

import java.util.function.UnaryOperator;

public class LambdaApp {
    public static void main(String[] args) {
        // Create a unary operator to square numbers
        UnaryOperator<Integer> square = x -> x * x;
        
        System.out.println(square.apply(5));   // 25
        System.out.println(square.apply(-3));  // 9
        System.out.println(square.apply(0));   // 0
        
        // Other examples
        UnaryOperator<String> toUpperCase = s -> s.toUpperCase();
        System.out.println(toUpperCase.apply("hello"));  // HELLO
        
        UnaryOperator<Double> half = x -> x / 2.0;
        System.out.println(half.apply(10.0));  // 5.0
    }
}

Совместно со Stream API:

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

// Square all numbers using map
List<Integer> squared = numbers.stream()
    .map(x -> x * x)  // UnaryOperator-like lambda
    .collect(Collectors.toList());

System.out.println(squared);  // [1, 4, 9, 16, 25]

Цепочка UnaryOperator:

UnaryOperator<Integer> square = x -> x * x;
UnaryOperator<Integer> addOne = x -> x + 1;

// Compose: addOne(square(x))
UnaryOperator<Integer> squareThenAddOne = square.andThen(addOne);
System.out.println(squareThenAddOne.apply(5));  // 26 (5² + 1)

Ответ: UnaryOperator<Integer> square = x -> x * x; — возведение в квадрат; square.apply(5)25.

3.20. Интерфейс BinaryOperator (Туториал 13, Задание 3)

Продемонстрируйте BinaryOperator<T>: умножение двух чисел.

Нажмите, чтобы увидеть решение

Ключевая идея: BinaryOperator<T>(T,T) -> T для однотипных операндов.

Определение BinaryOperator:

@FunctionalInterface
public interface BinaryOperator<T> {
    T apply(T t1, T t2);
}

Пример реализации:

import java.util.function.BinaryOperator;

public class LambdaApp {
    public static void main(String[] args) {
        // Create a binary operator to multiply numbers
        BinaryOperator<Integer> multiply = (x, y) -> x * y;
        
        System.out.println(multiply.apply(3, 5));    // 15
        System.out.println(multiply.apply(10, -2));  // -20
        System.out.println(multiply.apply(7, 0));    // 0
        
        // Other examples
        BinaryOperator<Integer> add = (x, y) -> x + y;
        System.out.println(add.apply(10, 20));  // 30
        
        BinaryOperator<String> concat = (s1, s2) -> s1 + s2;
        System.out.println(concat.apply("Hello", "World"));  // HelloWorld
        
        BinaryOperator<Integer> max = (x, y) -> x > y ? x : y;
        System.out.println(max.apply(5, 10));  // 10
    }
}

Со Stream API (reduce):

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

// Multiply all numbers together using reduce
int product = numbers.stream()
    .reduce(1, (x, y) -> x * y);  // BinaryOperator lambda
    
System.out.println(product);  // 120 (1*2*3*4*5)

// Sum all numbers
int sum = numbers.stream()
    .reduce(0, (x, y) -> x + y);
    
System.out.println(sum);  // 15

Готовые BinaryOperator:

import java.util.function.BinaryOperator;

// Integer operations
BinaryOperator<Integer> max = BinaryOperator.maxBy(Integer::compareTo);
BinaryOperator<Integer> min = BinaryOperator.minBy(Integer::compareTo);

System.out.println(max.apply(5, 10));  // 10
System.out.println(min.apply(5, 10));  // 5

Ответ: BinaryOperator<Integer> multiply = (x, y) -> x * y; — умножение; multiply.apply(3, 5)15.

3.21. Интерфейс Function (Туториал 13, Задание 4)

Продемонстрируйте Function<T,R>: перевод Integer в строку с суффиксом «dollars».

Нажмите, чтобы увидеть решение

Ключевая идея: Function<T,R> — произвольное T -> R (типы могут различаться).

Определение Function:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Пример реализации:

import java.util.function.Function;

public class LambdaApp {
    public static void main(String[] args) {
        // Create a function that converts Integer to String
        Function<Integer, String> convert = 
            x -> String.valueOf(x) + " dollars";
        
        System.out.println(convert.apply(5));    // 5 dollars
        System.out.println(convert.apply(100));  // 100 dollars
        System.out.println(convert.apply(0));    // 0 dollars
        
        // Other examples
        Function<String, Integer> length = s -> s.length();
        System.out.println(length.apply("Hello"));  // 5
        
        Function<Double, Integer> round = d -> (int) Math.round(d);
        System.out.println(round.apply(3.7));  // 4
    }
}

Со Stream API (map):

List<Integer> prices = Arrays.asList(10, 25, 50, 100);

// Convert integers to formatted strings
List<String> formatted = prices.stream()
    .map(x -> String.valueOf(x) + " dollars")  // Function lambda
    .collect(Collectors.toList());

System.out.println(formatted);  
// [10 dollars, 25 dollars, 50 dollars, 100 dollars]

Цепочка Function:

Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, String> toString = x -> "Result: " + x;

// Compose: toString(multiplyBy2(x))
Function<Integer, String> combined = multiplyBy2.andThen(toString);

System.out.println(combined.apply(5));  // Result: 10

Через method references:

// Instead of: x -> String.valueOf(x)
Function<Integer, String> convert = String::valueOf;
System.out.println(convert.apply(42));  // 42

Ответ: Function<Integer, String> convert = x -> String.valueOf(x) + " dollars";Integer → строка; convert.apply(5)"5 dollars".

3.22. Интерфейс Consumer (Туториал 13, Задание 5)

Продемонстрируйте Consumer<T>: форматированный вывод в консоль.

Нажмите, чтобы увидеть решение

Ключевая идея: Consumer<T>void accept(T); типичный случай — побочные эффекты (лог, вывод).

Определение Consumer:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Пример реализации:

import java.util.function.Consumer;

public class LambdaApp {
    public static void main(String[] args) {
        // Create a consumer that prints formatted output
        Consumer<Integer> printer = 
            x -> System.out.printf("%d dollars \n", x);
        
        printer.accept(600);   // 600 dollars
        printer.accept(1500);  // 1500 dollars
        
        // Other examples
        Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());
        upperPrinter.accept("hello");  // HELLO
        
        Consumer<List<String>> listPrinter = 
            list -> list.forEach(System.out::println);
        listPrinter.accept(Arrays.asList("A", "B", "C"));
    }
}

Со Stream API (forEach):

List<Integer> numbers = Arrays.asList(5, 10, 15, 20);

// Print each number with formatting
numbers.forEach(x -> System.out.printf("%d dollars\n", x));

// Or using method reference
numbers.forEach(System.out::println);

Цепочка Consumer:

Consumer<String> print = s -> System.out.print(s);
Consumer<String> newLine = s -> System.out.println();

// Chain consumers
Consumer<String> printWithNewLine = print.andThen(newLine);
printWithNewLine.accept("Hello");

Практический пример с изменением коллекции:

List<String> names = new ArrayList<>(Arrays.asList("alice", "bob", "charlie"));

// Consumer that modifies the list
Consumer<List<String>> capitalizeAll = list -> {
    for (int i = 0; i < list.size(); i++) {
        list.set(i, list.get(i).toUpperCase());
    }
};

capitalizeAll.accept(names);
System.out.println(names);  // [ALICE, BOB, CHARLIE]

Ответ: Consumer<Integer> printer = x -> System.out.printf("%d dollars \n", x); печатает сумму; printer.accept(600) выводит строку с 600.

3.23. Интерфейс Supplier (Туториал 13, Задание 6)

Продемонстрируйте Supplier<T>: фабрика объектов User.

Нажмите, чтобы увидеть решение

Ключевая идея: Supplier<T>T get() без аргументов; удобно для фабрик и ленивого создания.

Определение Supplier:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Пример реализации:

import java.util.Scanner;
import java.util.function.Supplier;

class User {
    private String name;
    
    public User(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

public class LambdaApp {
    public static void main(String[] args) {
        // Create a supplier that creates User objects
        Supplier<User> userFactory = () -> {
            Scanner in = new Scanner(System.in);
            System.out.println("Enter the name: ");
            String name = in.nextLine();
            return new User(name);
        };
        
        // Create users using the factory
        User user1 = userFactory.get();
        User user2 = userFactory.get();
        
        System.out.println("user1 name: " + user1.getName());
        System.out.println("user2 name: " + user2.getName());
    }
}

Другие примеры Supplier:

import java.util.function.Supplier;
import java.time.LocalDateTime;

public class SupplierExamples {
    public static void main(String[] args) {
        // Random number supplier
        Supplier<Integer> randomInt = () -> (int)(Math.random() * 100);
        System.out.println(randomInt.get());  // Random number
        System.out.println(randomInt.get());  // Different random number
        
        // Current timestamp supplier
        Supplier<LocalDateTime> timestamp = () -> LocalDateTime.now();
        System.out.println(timestamp.get());
        
        // Default value supplier
        Supplier<String> defaultName = () -> "Anonymous";
        System.out.println(defaultName.get());  // Anonymous
        
        // Lazy computation
        Supplier<Double> expensiveCalculation = () -> {
            System.out.println("Performing expensive calculation...");
            return Math.pow(2, 20);
        };
        // Calculation only happens when get() is called
        System.out.println(expensiveCalculation.get());
    }
}

Практика: ленивые вычисления:

public class LazyExample {
    // Supplier allows lazy evaluation - value is computed only when needed
    public static String getValue(boolean condition, Supplier<String> supplier) {
        if (condition) {
            return supplier.get();  // Only computed if condition is true
        }
        return "default";
    }
    
    public static void main(String[] args) {
        // This expensive operation won't execute if condition is false
        String result = getValue(false, () -> {
            System.out.println("Computing expensive value...");
            return "expensive result";
        });
        System.out.println(result);  // "default", computation never happened
    }
}

Ответ: Supplier<User> userFactory = () -> { /* создать User */ }; — фабрика по требованию; новый объект — вызовом userFactory.get().

3.24. Операции Stream API (Туториал 13, Задание 7)

Продемонстрируйте Stream API: фильтрация, distinct, сортировка и limit для списка целых.

Нажмите, чтобы увидеть решение

Ключевая идея: Stream API связывает промежуточные и одну терминальную операцию в декларативный конвейер.

Полный пример:

import java.util.*;

public class StreamApp {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(9);
        numbers.add(8);
        numbers.add(8);
        numbers.add(1);
        
        // Print original list
        numbers.forEach((n) -> System.out.print(n));
        System.out.println();  // Output: 59881
        
        // Apply stream operations
        numbers.stream()
            .filter(n -> n > 5)        // Keep only numbers > 5: [9, 8, 8]
            .distinct()                // Remove duplicates: [9, 8]
            .sorted()                  // Sort: [8, 9]
            .limit(1)                  // Take first element: [8]
            .forEach(System.out::print);
        
        System.out.println("\n" + numbers);
    }
}

Вывод:

59881
8
[5, 9, 8, 8, 1]

Пошаговый разбор:

  1. Original list: [5, 9, 8, 8, 1]
  2. After filter(n -> n > 5): [9, 8, 8] (only elements greater than 5)
  3. After distinct(): [9, 8] (duplicates removed)
  4. After sorted(): [8, 9] (sorted in ascending order)
  5. After limit(1): [8] (only first element)
  6. forEach(System.out::print): Prints 8

Важно: исходный список numbers остаётся [5, 9, 8, 8, 1] — операции потока не меняют источник.

Расширенный пример с collect():

List<Integer> numbers = Arrays.asList(5, 9, 8, 8, 1);

// Collect results into a new list
List<Integer> processed = numbers.stream()
    .filter(n -> n > 5)
    .distinct()
    .sorted()
    .collect(Collectors.toList());

System.out.println("Original: " + numbers);      // [5, 9, 8, 8, 1]
System.out.println("Processed: " + processed);   // [8, 9]

Ещё примеры со Stream:

// Count elements
long count = numbers.stream()
    .filter(n -> n > 5)
    .count();
System.out.println("Count: " + count);  // 3

// Find first
Optional<Integer> first = numbers.stream()
    .filter(n -> n > 5)
    .findFirst();
System.out.println("First: " + first.get());  // 9

// Map transformation
List<Integer> doubled = numbers.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());
System.out.println("Doubled: " + doubled);  // [10, 18, 16, 16, 2]

// Reduce (sum)
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum);  // 31

Ответ: конвейер оставляет значения > 5, убирает дубликаты, сортирует, берёт первый элемент и печатает; исходный список неизменён; в примере выводится 8.

3.25. Создание и запуск потока через Runnable (Туториал 13, Задание 8)

Покажите создание и запуск потока через Runnable.

Нажмите, чтобы увидеть решение

Ключевая идея: Runnable описывает что выполнить; Thread — механизм запуска, их лучше разделять.

Способ 1: implements Runnable

public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    
    public static void main(String[] args) {
        (new Thread(new HelloRunnable())).start();
    }
}

Способ 2: наследование Thread

public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from a thread!");
    }
    
    public static void main(String[] args) {
        (new HelloThread()).start();
    }
}

Способ 3: анонимный класс

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from anonymous thread!");
            }
        });
        thread.start();
    }
}

Способ 4: лямбда (удобно для коротких задач)

public class LambdaThread {
    public static void main(String[] args) {
        // Runnable is a functional interface
        Thread thread = new Thread(() -> {
            System.out.println("Hello from lambda thread!");
        });
        thread.start();
        
        // Even more concise for single statements
        new Thread(() -> System.out.println("Quick thread!")).start();
    }
}

Несколько потоков:

public class MultiThreadExample {
    public static void main(String[] args) {
        System.out.println("Main thread: " + Thread.currentThread().getName());
        
        // Create first thread
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1: " + i);
                try {
                    Thread.sleep(500);  // Sleep 500ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        // Create second thread
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2: " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        // Start both threads
        thread1.start();
        thread2.start();
        
        System.out.println("Main thread continues...");
    }
}

Вывод (порядок строк может меняться due to concurrent execution):

Main thread: main
Main thread continues...
Thread 1: 1
Thread 2: 1
Thread 1: 2
Thread 2: 2
Thread 1: 3
Thread 2: 3
Thread 1: 4
Thread 2: 4
Thread 1: 5
Thread 2: 5

Ожидание завершения потоков:

public class ThreadJoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("Thread working...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread finished!");
        });
        
        thread.start();
        System.out.println("Main thread waiting...");
        thread.join();  // Wait for thread to complete
        System.out.println("Main thread continues after thread finished");
    }
}

Ответ: реализуйте Runnable или наследуйте Thread, затем start(). Кратко: new Thread(() -> System.out.println("Hello")).start();.

3.26. Полный пример: лямбды и потоки (Туториал 13, Задание 9)

Напишите связную программу, демонстрирующую лямбды, functional interfaces и операции Stream API.

Нажмите, чтобы увидеть решение

Ключевая идея: связать лямбды, стандартные functional interfaces и потоки в одном декларативном конвейере.

Полная программа:

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class CompleteFunctionalExample {
    public static void main(String[] args) {
        // Create a list of Person objects
        List<Person> people = Arrays.asList(
            new Person("Alice", 30, 50000),
            new Person("Bob", 25, 45000),
            new Person("Charlie", 35, 60000),
            new Person("David", 28, 48000),
            new Person("Eve", 32, 55000),
            new Person("Frank", 27, 42000),
            new Person("Grace", 29, 52000)
        );
        
        System.out.println("=== Original List ===");
        people.forEach(System.out::println);
        
        // пример 1: фильтр через Predicate
        System.out.println("\n=== People over 30 ===");
        Predicate<Person> isOver30 = p -> p.getAge() > 30;
        people.stream()
            .filter(isOver30)
            .forEach(System.out::println);
        
        // Example 2: Transform using Function
        System.out.println("\n=== Names in Uppercase ===");
        Function<Person, String> getUpperName = p -> p.getName().toUpperCase();
        people.stream()
            .map(getUpperName)
            .forEach(System.out::println);
        
        // Example 3: Calculate using BinaryOperator
        System.out.println("\n=== Total Salary ===");
        BinaryOperator<Double> sum = (a, b) -> a + b;
        double totalSalary = people.stream()
            .map(Person::getSalary)
            .reduce(0.0, sum);
        System.out.println("Total: $" + totalSalary);
        
        // Example 4: Consumer for custom formatting
        System.out.println("\n=== Formatted Output ===");
        Consumer<Person> formatter = p -> 
            System.out.printf("%-10s | Age: %2d | Salary: $%,.2f%n", 
                p.getName(), p.getAge(), p.getSalary());
        people.forEach(formatter);
        
        // Example 5: Complex stream pipeline
        System.out.println("\n=== High Earners (Sorted by Name) ===");
        List<String> highEarners = people.stream()
            .filter(p -> p.getSalary() > 50000)     // Filter high earners
            .sorted(Comparator.comparing(Person::getName))  // Sort by name
            .map(p -> p.getName() + ": $" + p.getSalary())  // Format
            .collect(Collectors.toList());          // Collect to list
        highEarners.forEach(System.out::println);
        
        // Example 6: Grouping and statistics
        System.out.println("\n=== Salary Statistics ===");
        DoubleSummaryStatistics stats = people.stream()
            .mapToDouble(Person::getSalary)
            .summaryStatistics();
        System.out.println("Average: $" + stats.getAverage());
        System.out.println("Min: $" + stats.getMin());
        System.out.println("Max: $" + stats.getMax());
        
        // Example 7: Partitioning
        System.out.println("\n=== Partition by Age ===");
        Map<Boolean, List<Person>> partitioned = people.stream()
            .collect(Collectors.partitioningBy(p -> p.getAge() >= 30));
        System.out.println("30 or older: " + partitioned.get(true).size());
        System.out.println("Under 30: " + partitioned.get(false).size());
        
        // Example 8: Custom Supplier
        System.out.println("\n=== Random Person Generator ===");
        List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill");
        Random random = new Random();
        Supplier<Person> randomPerson = () -> {
            String name = names.get(random.nextInt(names.size()));
            int age = 20 + random.nextInt(30);
            double salary = 40000 + random.nextInt(30000);
            return new Person(name, age, salary);
        };
        
        // Generate 3 random people
        Stream.generate(randomPerson)
            .limit(3)
            .forEach(System.out::println);
    }
}

class Person {
    private String name;
    private int age;
    private double salary;
    
    public Person(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    public double getSalary() { return salary; }
    
    @Override
    public String toString() {
        return String.format("%s (age %d, salary $%.2f)", name, age, salary);
    }
}

Вывод:

=== Original List ===
Alice (age 30, salary $50000.00)
Bob (age 25, salary $45000.00)
Charlie (age 35, salary $60000.00)
...

=== People over 30 ===
Charlie (age 35, salary $60000.00)
Eve (age 32, salary $55000.00)

=== Names in Uppercase ===
ALICE
BOB
CHARLIE
...

=== Total Salary ===
Total: $352000.0

=== Formatted Output ===
Alice      | Age: 30 | Salary: $50,000.00
Bob        | Age: 25 | Salary: $45,000.00
...

=== High Earners (Sorted by Name) ===
Charlie: $60000.0
Eve: $55000.0
Grace: $52000.0

=== Salary Statistics ===
Average: $50285.714285714286
Min: $42000.0
Max: $60000.0

=== Partition by Age ===
30 or older: 4
Under 30: 3

=== Random Person Generator ===
Jane (age 35, salary $62000.00)
Jack (age 28, salary $51000.00)
John (age 42, salary $68000.00)

Ключевые приёмы:

  1. Predicate: фильтрация по условию
  2. Function: преобразование данных
  3. BinaryOperator: комбинирование значений
  4. Consumer: свой формат вывода
  5. Supplier: генерация данных
  6. Конвейер потока: цепочка операций
  7. Method references: короткий синтаксис
  8. Collectors: сбор результатов
  9. Статистика: агрегаты по числовому потоку
  10. partitioningBy: разбиение по условию

Ответ: пример объединяет лямбды, стандартные functional interfaces, операции потока (filter, map, reduce, collect), method references и многошаговый конвейер обработки данных.