W14. Лямбда-выражения, функциональные интерфейсы, Stream API, многопоточность
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 (чистая функция) — это функция, для которой выполняется:
- Нет побочных эффектов (ничего существенного вне области видимости не меняется)
- На одни и те же входы всегда один и тот же выход (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>:T→T- Метод:
T apply(T t) - Пример:
UnaryOperator<Integer> square = x -> x * x;
- Метод:
BinaryOperator<T>: дваT→T- Метод:
T apply(T t1, T t2) - Пример:
BinaryOperator<Integer> multiply = (x, y) -> x * y;
- Метод:
Function<T,R>:T→R- Метод:
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.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();
}
}Runnable — functional 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.
Атрибуты:
- Параметры: один параметр
o(тип выводится из контекста) - Типы параметров: не указаны явно; компилятор выведет их по functional interface
- Тип результата:
String(как уtoString()) - Тело: одно выражение
o.toString()— вызовtoString()у параметра - 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();
}
}Пошаговое объяснение:
- Список:
ArrayList<Integer>и заполнение черезRandom.nextInt() - Делимость на 3:
filter(n -> n % 3 == 0)оставляет числа, кратные 3 - Без минуса:
map(n -> Math.abs(n))даёт абсолютное значение - Вывод:
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::valueOf — method 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::valueOf — method 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для строк без цифр
Цепочка операций потока:
filter(s -> !s.isEmpty())— убрать пустые строкиfilter(s -> !s.matches(".*\\d.*"))— оставить только строки без цифрdistinct()— убрать дубликатыsorted()— сортировка по естественному порядкуStringforEach(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
- Записать результат обратно в поле
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 Runnable— functional 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().
Что происходит:
- Поток завершается: выполнение потока, где было исключение, прекращается
- Остальные продолжают: другие потоки не останавливаются автоматически
- Нет распространения вверх: к потоку, вызвавшему
start(), исключение не пробрасывается - Стектрейс: по умолчанию неперехваченное исключение печатается в консоль
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
}
}Плюсы чистых функций:
- Referential transparency — вызов можно заменить на значение результата
- Кэшируемость — одинаковые входы, одинаковый выход
- Параллелизуемость — вызовы можно переупорядочивать или распараллеливать
- Тестируемость — мало подготовки состояния
- Удаляемость — если результат не нужен, вызов можно выбросить как мёртвый код
Дополнительные примеры:
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]
Пошаговый разбор:
- Original list:
[5, 9, 8, 8, 1] - After
filter(n -> n > 5):[9, 8, 8](only elements greater than 5) - After
distinct():[9, 8](duplicates removed) - After
sorted():[8, 9](sorted in ascending order) - After
limit(1):[8](only first element) forEach(System.out::print): Prints8
Важно: исходный список 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)
Ключевые приёмы:
Predicate: фильтрация по условиюFunction: преобразование данныхBinaryOperator: комбинирование значенийConsumer: свой формат выводаSupplier: генерация данных- Конвейер потока: цепочка операций
- Method references: короткий синтаксис
Collectors: сбор результатов- Статистика: агрегаты по числовому потоку
partitioningBy: разбиение по условию
Ответ: пример объединяет лямбды, стандартные functional interfaces, операции потока (filter, map, reduce, collect), method references и многошаговый конвейер обработки данных.