Генераторы и бесконечные потоки
Stream.iterate() и Stream.generate() создают потоки без заранее определённого источника данных. Их Spliterator не знает размера (NON_SIZED) и не поддерживает разделение для параллелизма. Это потоки, порождающие данные на лету по заданной функции.
Здесь данные не существуют до момента запроса. Spliterator вызывает предоставленную функцию для каждого элемента, проталкиваемого через конвейер. Бесконечность требует осторожности: без limit() или другой short-circuit операции терминальная операция никогда не завершится.
Параллельные потоки и разделение
Метод parallelStream() или .parallel() на существующем потоке активирует механизм ForkJoinPool. Но параллелизм возможен только если Spliterator поддерживает trySplit(). Этот метод разделяет оставшуюся часть данных на две половины, позволяя обработать их в разных потоках.
Понимание характеристик источника необходимо для оптимизации. Параллельный поток от Stream.iterate не только не ускорится, но может замедлиться из-за накладных расходов на координацию потоков без реального разделения работы.
Конвейер: архитектура плана
Когда мы вызываем промежуточные операции, мы не обрабатываем данные. Мы строим структуру данных — двусвязный список объектов-операций, каждый из которых знает о своём предшественнике и преемнике.
Внутренняя иерархия классов Stream API использует паттерн "Цепочка обязанностей" (Chain of Responsibility), реализованный через наследование от AbstractPipeline. Каждая промежуточная операция создаёт экземпляр анонимного подкласса: StatelessOp для операций без состояния (filter, map), StatefulOp для операций с состоянием (sorted, distinct, limit).
Когда мы пишем:
Создаётся цепочка: Head (источник) → FilterOps → MapOps → SortedOps. Каждый узел хранит ссылку на лямбду-логику и флаги операции (оптимизационные подсказки для конвейера).
Флаги операций (opFlags) — битовая маска, описывающая свойства операции. IS_SIZED означает, что операция не меняет количество элементов. NOT_SORTED означает, что операция может нарушить порядок. Эти флаги комбинируются через побитовые операции, позволяя оптимизировать конвейер: если ни одна операция не требует сохранения порядка, параллельное выполнение может избежать синхронизации на упорядочивании.
Промежуточные операции ленивы не потому, что "так принято", а потому, что их реализация не запускает обработку. Метод filter возвращает новый объект Stream, чей метод opWrapSink (создание приёмника данных) будет вызван позже, при терминальной операции. До этого момента мы просто наращиваем связный список описаний.
#Java #для_новичков #beginner #stream_api #stream
Stream.iterate() и Stream.generate() создают потоки без заранее определённого источника данных. Их Spliterator не знает размера (NON_SIZED) и не поддерживает разделение для параллелизма. Это потоки, порождающие данные на лету по заданной функции.
// Бесконечная последовательность: 0, 1, 2, 3...
Stream<Integer> natural = Stream.iterate(0, n -> n + 1);
// Бесконечный поток случайных книг
Stream<Book> randomBooks = Stream.generate(() -> generateRandomBook());
Здесь данные не существуют до момента запроса. Spliterator вызывает предоставленную функцию для каждого элемента, проталкиваемого через конвейер. Бесконечность требует осторожности: без limit() или другой short-circuit операции терминальная операция никогда не завершится.
Параллельные потоки и разделение
Метод parallelStream() или .parallel() на существующем потоке активирует механизм ForkJoinPool. Но параллелизм возможен только если Spliterator поддерживает trySplit(). Этот метод разделяет оставшуюся часть данных на две половины, позволяя обработать их в разных потоках.
// Хорошо делится: ArrayList, массивы, IntStream.range
List<Book> books = new ArrayList<>();
books.parallelStream() // Эффективное распараллеливание
// Плохо делится: Stream.iterate, Stream.generate, BufferedReader.lines()
Stream.iterate(0, n -> n + 1).parallel() // Нет выигрыша, возможен проигрыш
Понимание характеристик источника необходимо для оптимизации. Параллельный поток от Stream.iterate не только не ускорится, но может замедлиться из-за накладных расходов на координацию потоков без реального разделения работы.
Конвейер: архитектура плана
Когда мы вызываем промежуточные операции, мы не обрабатываем данные. Мы строим структуру данных — двусвязный список объектов-операций, каждый из которых знает о своём предшественнике и преемнике.
Внутренняя иерархия классов Stream API использует паттерн "Цепочка обязанностей" (Chain of Responsibility), реализованный через наследование от AbstractPipeline. Каждая промежуточная операция создаёт экземпляр анонимного подкласса: StatelessOp для операций без состояния (filter, map), StatefulOp для операций с состоянием (sorted, distinct, limit).
// Упрощённая внутренняя структура (псевдокод на основе OpenJDK)
abstract class AbstractPipeline<E_IN, E_OUT, S extends BaseStream<E_OUT, S>>
extends PipelineHelper<E_OUT> implements BaseStream<E_OUT, S> {
private final AbstractPipeline<?, E_IN, ?> previousStage; // Ссылка назад
private AbstractPipeline<?, E_OUT, ?> nextStage; // Ссылка вперёд
private int depth; // Глубина в конвейере
// Каждая операция оборачивает предыдущую
protected AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
this.previousStage = previousStage;
this.depth = previousStage.depth + 1;
previousStage.nextStage = this;
}
}
Когда мы пишем:
stream.filter(...).map(...).sorted(...)
Создаётся цепочка: Head (источник) → FilterOps → MapOps → SortedOps. Каждый узел хранит ссылку на лямбду-логику и флаги операции (оптимизационные подсказки для конвейера).
Флаги операций (opFlags) — битовая маска, описывающая свойства операции. IS_SIZED означает, что операция не меняет количество элементов. NOT_SORTED означает, что операция может нарушить порядок. Эти флаги комбинируются через побитовые операции, позволяя оптимизировать конвейер: если ни одна операция не требует сохранения порядка, параллельное выполнение может избежать синхронизации на упорядочивании.
Промежуточные операции ленивы не потому, что "так принято", а потому, что их реализация не запускает обработку. Метод filter возвращает новый объект Stream, чей метод opWrapSink (создание приёмника данных) будет вызван позже, при терминальной операции. До этого момента мы просто наращиваем связный список описаний.
#Java #для_новичков #beginner #stream_api #stream
👍3
Терминальная операция
Терминальная операция — это точка фазового перехода. План превращается в действие.
Методы collect, forEach, reduce, findFirst, anyMatch и другие запускают механизм, который:
- Создаёт "приёмник" (Sink) для последней операции конвейера
- Оборачивает его приёмником предыдущей операции, и так далее до источника
- Запрашивает у Spliterator источника обход элементов
- Для каждого элемента вызывает цепочку приёмников, пропуская данные через весь конвейер
Sink (поглотитель) — интерфейс, реализующий паттерн "Посетитель" (Visitor) для потоковых элементов. Он определяет четыре метода: begin (начало обхода), accept (обработка элемента), end (завершение), cancellationRequested (проверка необходимости остановки).
Ключевое наблюдение: элементы не "текут" через конвейер как вода в трубе. Каждый элемент полностью обрабатывается всеми операциями до того, как берётся следующий. Это "вертикальное" выполнение противопоставлено "горизонтальному" (где сначала все элементы проходят filter, затем все проходят map). Вертикальность критична для short-circuit операций — она позволяет остановиться после нахождения первого подходящего элемента, не обрабатывая остальные.
Потребление и неповторимость
После выполнения терминальной операции поток считается израсходованным. Повторный вызов терминальной операции выбросит IllegalStateException. Это архитектурное ограничение, а не техническая необходимость: многие источники (коллекции) допускали бы повторное чтение, но семантика потока как одноразового итератора упрощает внутреннюю реализацию и предотвращает ошибки при работе с источниками, которые действительно одноразовы (например, поток строк из файла, где файловый дескриптор закрывается после прочтения).
Если нужно многократное использование, применяем Supplier<Stream>:
Материализация: когда план становится данными
Не все операции сохраняют ленивость до конца. Операции с состоянием (stateful) — sorted, distinct, limit в параллельном потоке — требуют буферизации элементов для выполнения своей функции. sorted должен увидеть все элементы, прежде чем выдать первый. distinct должен хранить множество уже виденных элементов.
Эти операции вынуждают конвейер к материализации — промежуточному созданию структуры данных в памяти. Это разрушает иллюзию бесконечного потока: если подать бесконечный поток в sorted, программа зависнет в ожидании последнего элемента.
Понимание различия между stateless и stateful операциями необходимо для предсказания производительности и избежания бесконечных циклов.
#Java #для_новичков #beginner #stream_api #stream
Терминальная операция — это точка фазового перехода. План превращается в действие.
Методы collect, forEach, reduce, findFirst, anyMatch и другие запускают механизм, который:
- Создаёт "приёмник" (Sink) для последней операции конвейера
- Оборачивает его приёмником предыдущей операции, и так далее до источника
- Запрашивает у Spliterator источника обход элементов
- Для каждого элемента вызывает цепочку приёмников, пропуская данные через весь конвейер
Sink (поглотитель) — интерфейс, реализующий паттерн "Посетитель" (Visitor) для потоковых элементов. Он определяет четыре метода: begin (начало обхода), accept (обработка элемента), end (завершение), cancellationRequested (проверка необходимости остановки).
// Упрощённая логика терминальной операции (псевдокод)
public void evaluateParallel(PipelineHelper<T> helper, Spliterator<S> spliterator) {
// Создаём приёмник для конечной операции (например, collecting)
Sink<T> sink = makeSink();
// Оборачиваем в приёмники промежуточных операций, идя от хвоста к голове
for (AbstractPipeline p = lastStage; p != null; p = p.previousStage) {
sink = p.opWrapSink(sink);
}
// Запускаем обход источника
spliterator.forEachRemaining(sink::accept);
}
Ключевое наблюдение: элементы не "текут" через конвейер как вода в трубе. Каждый элемент полностью обрабатывается всеми операциями до того, как берётся следующий. Это "вертикальное" выполнение противопоставлено "горизонтальному" (где сначала все элементы проходят filter, затем все проходят map). Вертикальность критична для short-circuit операций — она позволяет остановиться после нахождения первого подходящего элемента, не обрабатывая остальные.
Потребление и неповторимость
После выполнения терминальной операции поток считается израсходованным. Повторный вызов терминальной операции выбросит IllegalStateException. Это архитектурное ограничение, а не техническая необходимость: многие источники (коллекции) допускали бы повторное чтение, но семантика потока как одноразового итератора упрощает внутреннюю реализацию и предотвращает ошибки при работе с источниками, которые действительно одноразовы (например, поток строк из файла, где файловый дескриптор закрывается после прочтения).
Stream<Book> stream = books.stream();
List<Book> first = stream.collect(toList()); // OK
List<Book> second = stream.collect(toList()); // IllegalStateException: stream has already been operated upon or closed
Если нужно многократное использование, применяем Supplier<Stream>:
Supplier<Stream<Book>> streamSupplier = () -> books.stream();
List<Book> first = streamSupplier.get().collect(toList());
List<Book> second = streamSupplier.get().filter(Book::isAvailable).collect(toList());
Материализация: когда план становится данными
Не все операции сохраняют ленивость до конца. Операции с состоянием (stateful) — sorted, distinct, limit в параллельном потоке — требуют буферизации элементов для выполнения своей функции. sorted должен увидеть все элементы, прежде чем выдать первый. distinct должен хранить множество уже виденных элементов.
Эти операции вынуждают конвейер к материализации — промежуточному созданию структуры данных в памяти. Это разрушает иллюзию бесконечного потока: если подать бесконечный поток в sorted, программа зависнет в ожидании последнего элемента.
Stream.iterate(0, n -> n + 1)
.sorted() // Опасность: попытка отсортировать бесконечность
.findFirst(); // Никогда не выполнится
Понимание различия между stateless и stateful операциями необходимо для предсказания производительности и избежания бесконечных циклов.
#Java #для_новичков #beginner #stream_api #stream
👍4
Что выведет код?
#Tasks
import java.util.Arrays;
import java.util.stream.Stream;
public class Task120226 {
public static void main(String[] args) {
String[] array = {"a", "b", "c"};
Stream<String> s1 = Arrays.stream(array);
Stream<String> s2 = Stream.of(array);
array[1] = "X";
System.out.println(s1.findFirst().orElse(""));
System.out.println(s2.skip(1).findFirst().orElse(""));
}
}
#Tasks
👍3🔥1
👍2
Что такое Deadlock, Livelock, Starvation? 🤓
Ответ:
Deadlock (взаимная блокировка) — ситуация, когда два или более потока бесконечно ждут друг друга, удерживая нужные им ресурсы (мониторы).
Livelock (активная блокировка) — потоки не блокируются, но постоянно меняют свое состояние в ответ на действия друг друга, не продвигаясь в работе (например, вежливо уступая дорогу).
Starvation (голодание) — поток не может получить доступ к общему ресурсу, потому что его постоянно перехватывают другие, более приоритетные потоки.
Deadlock предотвращают, упорядочивая захватов.
#собеседование
Ответ:
Deadlock (взаимная блокировка)
Livelock (активная блокировка) — потоки не блокируются, но постоянно меняют свое состояние в ответ на действия друг друга, не продвигаясь в работе (например, вежливо уступая дорогу).
Starvation (голодание) — поток не может получить доступ к общему ресурсу, потому что его постоянно перехватывают другие, более приоритетные потоки.
Deadlock предотвращают, упорядочивая захватов.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 13 Февраля
ℹ️ Кто родился в этот день
Уи́льям Брэ́дфорд Шо́кли (англ. William Bradford Shockley; 13 февраля 1910, Лондон — 12 августа 1989, Станфорд) — американский физик, исследователь полупроводников, лауреат Нобелевской премии по физике 1956 года. В годы Второй мировой войны Шокли участвовал в создании американской школы исследования операций и в разработке тактики стратегических бомбардировок. В январе 1948 года Шокли изобрёл плоскостной биполярный транзистор, а затем создал научную теорию, объяснявшую его работу. В 1956 году Шокли основал названную его именем лабораторию, которая стала одним из истоков Кремниевой долины.
🌐 Знаковые события
1946 — американские военные объявили о создании первого цифрового компьютера Мокли и Экерта (ЭНИАК). Он потреблял 160 киловатт, имел 17 468 ламп, около 70 000 резисторов, 10 000 конденсаторов, 1500 реле и 6000 ручных переключателей.
2009 — Unix‑время достигает 1 234 567 890 секунд (03:31:30 UTC 13 февраля 2009)
#Biography #Birth_Date #Events #13февраля
Уи́льям Брэ́дфорд Шо́кли (англ. William Bradford Shockley; 13 февраля 1910, Лондон — 12 августа 1989, Станфорд) — американский физик, исследователь полупроводников, лауреат Нобелевской премии по физике 1956 года. В годы Второй мировой войны Шокли участвовал в создании американской школы исследования операций и в разработке тактики стратегических бомбардировок. В январе 1948 года Шокли изобрёл плоскостной биполярный транзистор, а затем создал научную теорию, объяснявшую его работу. В 1956 году Шокли основал названную его именем лабораторию, которая стала одним из истоков Кремниевой долины.
1946 — американские военные объявили о создании первого цифрового компьютера Мокли и Экерта (ЭНИАК). Он потреблял 160 киловатт, имел 17 468 ламп, около 70 000 резисторов, 10 000 конденсаторов, 1500 реле и 6000 ручных переключателей.
2009 — Unix‑время достигает 1 234 567 890 секунд (03:31:30 UTC 13 февраля 2009)
#Biography #Birth_Date #Events #13февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3👍2
[Совет по Java #005]
Тема: Закрывайте ресурсы (InputStream, Connection, Scanner) в блоке try-with-resources. Это гарантирует их закрытие даже при исключении.
Проблема: Ручное управление ресурсами в блоке finally чревато ошибками и создает многословный, трудночитаемый код.
Разработчики часто забывают закрыть ресурс или делают это неправильно, пропуская проверку на null. Более того, если и при закрытии ресурса возникает исключение, оно может "проглотить" исходное исключение, произошедшее в блоке try. Это приводит к утечкам ресурсов (незакрытые файловые дескрипторы, соединения с БД, сетевые сокеты) и затрудняет диагностику корневых причин сбоев.
Даже корректно написанный finally блок не способен лаконично обработать несколько ресурсов, не прибегая к вложенным try-catch.
Решение: Начиная с Java 7, языковая конструкция try-with-resources обеспечивает автоматическое управление ресурсами.
Для использования ресурс должен реализовывать интерфейс-маркер java.lang.AutoCloseable (или его предшественник java.io.Closeable). Компилятор автоматически генерирует код, который закрывает ресурсы в порядке, обратном их объявлению, гарантируя вызов метода close() даже в случае исключения в блоке try. При этом все исключения, возникшие при закрытии, добавляются в подавленные (suppressed) исключения к основному исключению, что сохраняет полную картину ошибки.
Объяснение: Конструкция try (...) принимает одно или несколько объявлений ресурсов, разделенных точкой с запятой.
Каждый ресурс должен быть переменной инициализированного типа, реализующего AutoCloseable. Начиная с Java 9, допускается использование effectively final переменных, уже объявленных вне блока.
Компилятор транслирует try-with-resources в сложную последовательность try-catch-finally, корректно обрабатывающую иерархию исключений. Это не только сокращает объем кода, но и исключает самый распространенный класс ошибок, связанных с утечками системных ресурсов.
#Java #советы
Тема: Закрывайте ресурсы (InputStream, Connection, Scanner) в блоке try-with-resources. Это гарантирует их закрытие даже при исключении.
Проблема: Ручное управление ресурсами в блоке finally чревато ошибками и создает многословный, трудночитаемый код.
Разработчики часто забывают закрыть ресурс или делают это неправильно, пропуская проверку на null. Более того, если и при закрытии ресурса возникает исключение, оно может "проглотить" исходное исключение, произошедшее в блоке try. Это приводит к утечкам ресурсов (незакрытые файловые дескрипторы, соединения с БД, сетевые сокеты) и затрудняет диагностику корневых причин сбоев.
Даже корректно написанный finally блок не способен лаконично обработать несколько ресурсов, не прибегая к вложенным try-catch.
Решение: Начиная с Java 7, языковая конструкция try-with-resources обеспечивает автоматическое управление ресурсами.
Для использования ресурс должен реализовывать интерфейс-маркер java.lang.AutoCloseable (или его предшественник java.io.Closeable). Компилятор автоматически генерирует код, который закрывает ресурсы в порядке, обратном их объявлению, гарантируя вызов метода close() даже в случае исключения в блоке try. При этом все исключения, возникшие при закрытии, добавляются в подавленные (suppressed) исключения к основному исключению, что сохраняет полную картину ошибки.
import java.io.*;
import java.nio.file.*;
import java.sql.*;
public class ResourceManagement {
public static void main(String[] args) {
Path file = Paths.get("example.txt");
//Антипаттерн: Ручное закрытие с уязвимостями.
BufferedReader reader = null;
try {
reader = Files.newBufferedReader(file);
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close(); // Дополнительный try-catch и риск потери исключения
} catch (IOException e) {
e.printStackTrace(); // Может затереть исходное исключение
}
}
}
//Решение: try-with-resources.
try (BufferedReader br = Files.newBufferedReader(file);
FileWriter fw = new FileWriter("output.txt")) { // Несколько ресурсов
String line = br.readLine();
fw.write(line);
} catch (IOException e) {
// Исключения из close() доступны через e.getSuppressed()
System.out.println("Основное исключение: " + e);
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Подавленное: " + suppressed);
}
}
// Пример с JDBC: ресурсы должны быть объявлены в правильном порядке.
String url = "jdbc:h2:mem:test";
String sql = "SELECT * FROM users";
try (Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
// Обработка результата
}
} catch (SQLException e) {
// Автоматическое закрытие Connection, Statement, ResultSet
}
}
}
Объяснение: Конструкция try (...) принимает одно или несколько объявлений ресурсов, разделенных точкой с запятой.
Каждый ресурс должен быть переменной инициализированного типа, реализующего AutoCloseable. Начиная с Java 9, допускается использование effectively final переменных, уже объявленных вне блока.
Компилятор транслирует try-with-resources в сложную последовательность try-catch-finally, корректно обрабатывающую иерархию исключений. Это не только сокращает объем кода, но и исключает самый распространенный класс ошибок, связанных с утечками системных ресурсов.
#Java #советы
👍3
Что выведет код?
#Tasks
import java.util.Optional;
class Box130226 {
int value;
Box130226(int value) { this.value = value; }
}
public class Task130226 {
public static void main(String[] args) {
Box130226 box = new Box130226(10);
Optional<Box130226> opt = Optional.of(box);
opt.ifPresent(b -> b.value = 20);
opt.filter(b -> b.value > 15)
.map(b -> { b.value = 30; return b; });
System.out.println(box.value);
}
}
#Tasks
👍2
👍2
Что такое Optional и зачем он нужен? 🤓
Ответ:
Optional<T> — это контейнерный объект, который может содержать или не содержать ненулевое значение.
Он был введен в Java 8 для явного обозначения ситуаций, когда возвращаемое значение метода может отсутствовать (быть null), что помогает избежать злополучного NullPointerException.
Вместо возврата null метод возвращает Optional.empty(). Это заставляет вызывающий код явно обрабатывать случай отсутствия значения через методы: isPresent(), get(), orElse(), orElseGet(), orElseThrow().
Использование Optional делает код более читаемым и безопасным.
#собеседование
Ответ:
Optional<T>
Он был введен в Java 8 для явного обозначения ситуаций, когда возвращаемое значение метода может отсутствовать (быть null), что помогает избежать злополучного NullPointerException.
Вместо возврата null метод возвращает Optional.empty(). Это заставляет вызывающий код явно обрабатывать случай отсутствия значения через методы: isPresent(), get(), orElse(), orElseGet(), orElseThrow().
Использование Optional делает код более читаемым и безопасным.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
История IT-технологий сегодня — 14 Февраля
ℹ️ Кто родился в этот день
Эдмунд Георг Герман (Ехезкель) Ландау (нем. Edmund Georg Hermann (Yehezkel) Landau, 14 февраля 1877, Берлин — 19 февраля 1938, Берлин) — немецкий математик, который внёс существенный вклад в теорию чисел.
Кристофер Лэтем Шоулз (Шоулс) (англ. Christopher Latham Sholes; 14 февраля 1819, Мурсбург, штат Пенсильвания — 17 февраля 1890, Милуоки) — изобретатель первой серийно выпускаемой, коммерчески успешной печатной машинки. Кристофер с коллегами получил первый патент на печатную машинку в 1868 году. На протяжении ряда лет работал над усовершенствованием конструкции пишущей машинки, в рамках этой работы летом 1871 года появилась предшественница раскладки QWERTY, полностью она сформировалась только к 1878 году.
🌐 Знаковые события
1876 — Александр Белл подал заявку на патент США № 174465, описывающий «метод и аппарат для передачи речи и других звуков по телеграфу с помощью электрических волн» (получил патент 7 марта). Фактически речь шла о телефоне.
1995 — Borland представила первую версию Delphi.
2005 — создан сервис YouTube.
#Biography #Birth_Date #Events #14февраля
Эдмунд Георг Герман (Ехезкель) Ландау (нем. Edmund Georg Hermann (Yehezkel) Landau, 14 февраля 1877, Берлин — 19 февраля 1938, Берлин) — немецкий математик, который внёс существенный вклад в теорию чисел.
Кристофер Лэтем Шоулз (Шоулс) (англ. Christopher Latham Sholes; 14 февраля 1819, Мурсбург, штат Пенсильвания — 17 февраля 1890, Милуоки) — изобретатель первой серийно выпускаемой, коммерчески успешной печатной машинки. Кристофер с коллегами получил первый патент на печатную машинку в 1868 году. На протяжении ряда лет работал над усовершенствованием конструкции пишущей машинки, в рамках этой работы летом 1871 года появилась предшественница раскладки QWERTY, полностью она сформировалась только к 1878 году.
1876 — Александр Белл подал заявку на патент США № 174465, описывающий «метод и аппарат для передачи речи и других звуков по телеграфу с помощью электрических волн» (получил патент 7 марта). Фактически речь шла о телефоне.
1995 — Borland представила первую версию Delphi.
2005 — создан сервис YouTube.
#Biography #Birth_Date #Events #14февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
This media is not supported in your browser
VIEW IN TELEGRAM
Ну что ребят, вроде как все таки начнут блокировать телегу. Давайте обсуждать что делать дальше...
Может стоит вообще отказаться от публикаций постов и сконцентрироваться на ютубе/рутубе?
Или в VK уйти? Будете там читать? Или это прошлый век?
Или свой сайт наконец собрать и там все публиковать?
Жду вашего мнения))))
Может стоит вообще отказаться от публикаций постов и сконцентрироваться на ютубе/рутубе?
Или в VK уйти? Будете там читать? Или это прошлый век?
Или свой сайт наконец собрать и там все публиковать?
Жду вашего мнения))))
🔥1
С 07.02 по 13.02
Предыдущий пост(с 31.01 по 06.02)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
5. Бизнес-метрики в Spring Boot: Micrometer и реальная Observability
Обучающие статьи:
Раздел 8. Stream API и функциональный стиль
Глава 1: Философский фундамент. От шагов к преобразованиям
Практика: Рефакторинг мышления в «Библиотеке»
Глава 2: Анатомия Stream API. Ленивость и стоимость операций
Stream — это не данные, это план
Советы по Java:
[Совет по Java #003] Никогда не используйте float и double для точных расчетов с деньгами. Используйте BigDecimal или целые числа (копейки, центы).
[Совет по Java #004] При сравнении строк, которые могут быть null, вызывайте equals на константе или используйте Objects.equals().
[Совет по Java #005] Закрывайте ресурсы (InputStream, Connection, Scanner) в блоке try-with-resources. Это гарантирует их закрытие даже при исключении.
Полезные статьи и видео:
Паттерн Transactional Outbox — обеспечиваем консистентность между микросервисами на примере Java
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
Предыдущий пост(с 31.01 по 06.02)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
5. Бизнес-метрики в Spring Boot: Micrometer и реальная Observability
Обучающие статьи:
Раздел 8. Stream API и функциональный стиль
Глава 1: Философский фундамент. От шагов к преобразованиям
Практика: Рефакторинг мышления в «Библиотеке»
Глава 2: Анатомия Stream API. Ленивость и стоимость операций
Stream — это не данные, это план
Советы по Java:
[Совет по Java #003] Никогда не используйте float и double для точных расчетов с деньгами. Используйте BigDecimal или целые числа (копейки, центы).
[Совет по Java #004] При сравнении строк, которые могут быть null, вызывайте equals на константе или используйте Objects.equals().
[Совет по Java #005] Закрывайте ресурсы (InputStream, Connection, Scanner) в блоке try-with-resources. Это гарантирует их закрытие даже при исключении.
Полезные статьи и видео:
Паттерн Transactional Outbox — обеспечиваем консистентность между микросервисами на примере Java
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍3
История IT-технологий сегодня — 15 Февраля
ℹ️ Кто родился в этот день
Никлаус Вирт (нем. Niklaus Emil Wirth; 15 февраля 1934, Винтертур, Цюрих — 1 января 2024, Цюрих) — швейцарский информатик, программист, один из известнейших теоретиков в области разработки языков программирования, профессор компьютерных наук Швейцарской высшей технической школы Цюриха (ETHZ), лауреат премии Тьюринга 1984 года. Создатель и ведущий проектировщик языков программирования Паскаль, Модула-2, Оберон.
Джордж Джонстон Стони (англ. George Johnstone Stoney, 1826—1911) — британский ирландский физик и математик, более всего известный тем, что ввёл в науку термин «электрон», определив его как «фундаментальную единицу измерения электроэнергии». Ввёл в обиход термин волновое число и его обозначение (k) используется до сих пор.
Галиле́о Галиле́й (итал. Galileo Galilei; 15 февраля 1564, Пиза — 8 января 1642, Арчетри) — итальянский физик, механик, астроном, философ, математик, оказавший значительное влияние на науку своего времени. Одним из первых использовал телескоп для наблюдения небесных тел и сделал ряд выдающихся астрономических открытий. Галилей — основатель экспериментальной физики. Своими экспериментами он убедительно опроверг умозрительную физику Аристотеля и заложил фундамент классической механики.
🌐 Знаковые события
1995 — в США арестован хакер Кевин Митник, взломавший компьютерную сеть Пентагона.
#Biography #Birth_Date #Events #15февраля
Никлаус Вирт (нем. Niklaus Emil Wirth; 15 февраля 1934, Винтертур, Цюрих — 1 января 2024, Цюрих) — швейцарский информатик, программист, один из известнейших теоретиков в области разработки языков программирования, профессор компьютерных наук Швейцарской высшей технической школы Цюриха (ETHZ), лауреат премии Тьюринга 1984 года. Создатель и ведущий проектировщик языков программирования Паскаль, Модула-2, Оберон.
Джордж Джонстон Стони (англ. George Johnstone Stoney, 1826—1911) — британский ирландский физик и математик, более всего известный тем, что ввёл в науку термин «электрон», определив его как «фундаментальную единицу измерения электроэнергии». Ввёл в обиход термин волновое число и его обозначение (k) используется до сих пор.
Галиле́о Галиле́й (итал. Galileo Galilei; 15 февраля 1564, Пиза — 8 января 1642, Арчетри) — итальянский физик, механик, астроном, философ, математик, оказавший значительное влияние на науку своего времени. Одним из первых использовал телескоп для наблюдения небесных тел и сделал ряд выдающихся астрономических открытий. Галилей — основатель экспериментальной физики. Своими экспериментами он убедительно опроверг умозрительную физику Аристотеля и заложил фундамент классической механики.
1995 — в США арестован хакер Кевин Митник, взломавший компьютерную сеть Пентагона.
#Biography #Birth_Date #Events #15февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
6. От сырых метрик к production-мониторингу: Prometheus и Grafana
Ваше приложение собирает метрики, но вы всё ещё не понимаете, что происходит с бизнесом?🤓
В предыдущем видео мы добавили @BusinessMetric и научили OrderHub считать заказы. Но метрики без визуализации — это как камера без монитора: данные есть, а увидеть их нельзя.
В этом видео мы построим полный production-стек наблюдаемости:
• Prometheus — научимся собирать и хранить метрики
• Grafana — превратим цифры в живые дашборды
• Golden Signals — поймём язык SRE (QPS, Latency, Errors, Saturation)
• Бизнес-метрики — сделаем видимыми для всей команды
• Алерты — настроим Telegram, чтобы узнавать о проблемах первыми
Исходный код проекта на GitHub очень ждет Ваших звезд☺️
Ссылка на Youtube
Ссылка на Рутьюб
Смотрите, ставьте лайки, подписывайтесь на каналы!✌️
Ваше приложение собирает метрики, но вы всё ещё не понимаете, что происходит с бизнесом?
В предыдущем видео мы добавили @BusinessMetric и научили OrderHub считать заказы. Но метрики без визуализации — это как камера без монитора: данные есть, а увидеть их нельзя.
В этом видео мы построим полный production-стек наблюдаемости:
• Prometheus — научимся собирать и хранить метрики
• Grafana — превратим цифры в живые дашборды
• Golden Signals — поймём язык SRE (QPS, Latency, Errors, Saturation)
• Бизнес-метрики — сделаем видимыми для всей команды
• Алерты — настроим Telegram, чтобы узнавать о проблемах первыми
Исходный код проекта на GitHub очень ждет Ваших звезд
Ссылка на Youtube
Ссылка на Рутьюб
Смотрите, ставьте лайки, подписывайтесь на каналы!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥4🆒2 1
История IT-технологий сегодня — 16 Февраля
ℹ️ Кто родился в этот день
Ю́рий Ива́нович Ма́нин (16 февраля 1937, Симферополь, СССР — 7 января 2023) — советский и немецкий математик, алгебраический геометр, педагог. Один из основоположников некоммутативной алгебраической геометрии, теории квантовых вычислений и квантовой информатики.
Робе́рта Линн Уи́льямс (англ. Roberta Lynn Williams, урождённая Хойер, англ. Heuer; 16 февраля 1953 года, Лос-Анджелес, Калифорния, США) — американский дизайнер компьютерных игр, соучредитель компании Sierra On-Line. Вышедшая в 1980 году её первая игра, Mystery House, имела скромный коммерческий успех и считается первой графической приключенческой игрой. Уильямс также известна созданием и поддержкой серии King’s Quest, и разработкой видеоигры Phantasmagoria.
🌐 Знаковые события
1978 — создана первая компьютерная электронная доска объявлений — BBS.
#Biography #Birth_Date #Events #16февраля
Ю́рий Ива́нович Ма́нин (16 февраля 1937, Симферополь, СССР — 7 января 2023) — советский и немецкий математик, алгебраический геометр, педагог. Один из основоположников некоммутативной алгебраической геометрии, теории квантовых вычислений и квантовой информатики.
Робе́рта Линн Уи́льямс (англ. Roberta Lynn Williams, урождённая Хойер, англ. Heuer; 16 февраля 1953 года, Лос-Анджелес, Калифорния, США) — американский дизайнер компьютерных игр, соучредитель компании Sierra On-Line. Вышедшая в 1980 году её первая игра, Mystery House, имела скромный коммерческий успех и считается первой графической приключенческой игрой. Уильямс также известна созданием и поддержкой серии King’s Quest, и разработкой видеоигры Phantasmagoria.
1978 — создана первая компьютерная электронная доска объявлений — BBS.
#Biography #Birth_Date #Events #16февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Раздел 8. Stream API и функциональный стиль в Java
Глава 2: Анатомия Stream API. Ленивость и стоимость операций
Ленивость и Short-Circuit — ключ к эффективности
Ленивость в Stream API — не метафора, а конкретное техническое свойство, которое можно продемонстрировать экспериментально. Рассмотрим код, который выглядит как активная операция, но на самом деле является пассивным декларацией:
Метод peek — промежуточная операция, предназначенная преимущественно для отладки. Он принимает Consumer — функцию, которая получает элемент и ничего не возвращает. Интуитивно кажется, что при вызове peek с System.out::println мы должны увидеть вывод. Но поток остаётся холодным: без терминальной операции конвейер — это мёртвая структура данных, не связанная с вычислениями.
Это принципиальное отличие от коллекций. Если бы peek был методом List, он бы немедленно прошёлся по всем элементам. В Stream API peek лишь добавляет узел в граф операций, сохраняя ссылку на переданный Consumer для будущего использования.
Ленивость достигается через отложенную композицию. Каждая промежуточная операция возвращает новый объект Stream, который знает о своей предшественнице и о логике, которую нужно применить. Но само применение откладывается до момента, когда терминальная операция запросит элементы. Тогда происходит обратная реакция: терминальная операция спрашивает у предыдущей стадии элемент, та — у своей предшественницы, и так до источника.
Порядок операций: семантика vs производительность
Ленивость открывает возможности для оптимизации, но эти возможности реализуются только при правильном порядке операций. Рассмотрим два варианта одной задачи: найти первую дорогую книгу после тяжёлой трансформации.
Антипаттерн: дорогая работа перед фильтрацией
Здесь heavyTransform — метод, требующий значительных ресурсов: обращение к внешнему API, сложные вычисления, парсинг больших данных. Проблема в том, что map применяется ко всем элементам потока до того, как filter получает шанс отсеять неподходящие. Если в библиотеке 10 000 книг, из которых дорогими являются только 50, мы выполним 10 000 тяжёлых трансформаций вместо 50 (или даже меньше, если учитывать short-circuit).
Оптимальный порядок: фильтрация перед дорогой работой
Теперь heavyTransform выполняется только для книг, прошедших фильтр цены. Но главное преимущество — short-circuit поведение findFirst. Как только первая дорогая книга найдена и трансформирована, обход источника прекращается. Если первая книга в списке стоит 150, мы выполняем ровно одну тяжёлую операцию вместо 10 000.
Этот принцип универсален: дешёвые предикаты должны предшествовать дорогим трансформациям. Проверка полей объекта (цена, год, флаги) практически бесплатна по сравнению с вычислениями, вводом-выводом или аллокациями новых объектов.
#Java #для_новичков #beginner #stream_api #stream #short_circuit
Глава 2: Анатомия Stream API. Ленивость и стоимость операций
Ленивость и Short-Circuit — ключ к эффективности
Ленивость в Stream API — не метафора, а конкретное техническое свойство, которое можно продемонстрировать экспериментально. Рассмотрим код, который выглядит как активная операция, но на самом деле является пассивным декларацией:
Stream<Book> stream = library.getBooks().stream()
.peek(book -> System.out.println("Просматриваем: " + book.title()))
.filter(book -> book.year() > 1950);
System.out.println("Конвейер построен");
// На консоли только: "Конвейер построен"
// Ни одной строки "Просматриваем..." не появится
Метод peek — промежуточная операция, предназначенная преимущественно для отладки. Он принимает Consumer — функцию, которая получает элемент и ничего не возвращает. Интуитивно кажется, что при вызове peek с System.out::println мы должны увидеть вывод. Но поток остаётся холодным: без терминальной операции конвейер — это мёртвая структура данных, не связанная с вычислениями.
Это принципиальное отличие от коллекций. Если бы peek был методом List, он бы немедленно прошёлся по всем элементам. В Stream API peek лишь добавляет узел в граф операций, сохраняя ссылку на переданный Consumer для будущего использования.
Ленивость достигается через отложенную композицию. Каждая промежуточная операция возвращает новый объект Stream, который знает о своей предшественнице и о логике, которую нужно применить. Но само применение откладывается до момента, когда терминальная операция запросит элементы. Тогда происходит обратная реакция: терминальная операция спрашивает у предыдущей стадии элемент, та — у своей предшественницы, и так до источника.
Порядок операций: семантика vs производительность
Ленивость открывает возможности для оптимизации, но эти возможности реализуются только при правильном порядке операций. Рассмотрим два варианта одной задачи: найти первую дорогую книгу после тяжёлой трансформации.
Антипаттерн: дорогая работа перед фильтрацией
Optional<ProcessedBook> result = books.stream()
.map(b -> heavyTransform(b)) // Тяжёлая операция для ВСЕХ книг
.filter(b -> b.price() > 100) // Фильтрация уже обработанных
.findFirst(); // Возврат первой подходящей
Здесь heavyTransform — метод, требующий значительных ресурсов: обращение к внешнему API, сложные вычисления, парсинг больших данных. Проблема в том, что map применяется ко всем элементам потока до того, как filter получает шанс отсеять неподходящие. Если в библиотеке 10 000 книг, из которых дорогими являются только 50, мы выполним 10 000 тяжёлых трансформаций вместо 50 (или даже меньше, если учитывать short-circuit).
Оптимальный порядок: фильтрация перед дорогой работой
Optional<ProcessedBook> result = books.stream()
.filter(b -> b.price() > 100) // Сначала дешёвая проверка
.map(b -> heavyTransform(b)) // Тяжёлая операция только для подходящих
.findFirst(); // Остановка после первой успешной
Теперь heavyTransform выполняется только для книг, прошедших фильтр цены. Но главное преимущество — short-circuit поведение findFirst. Как только первая дорогая книга найдена и трансформирована, обход источника прекращается. Если первая книга в списке стоит 150, мы выполняем ровно одну тяжёлую операцию вместо 10 000.
Этот принцип универсален: дешёвые предикаты должны предшествовать дорогим трансформациям. Проверка полей объекта (цена, год, флаги) практически бесплатна по сравнению с вычислениями, вводом-выводом или аллокациями новых объектов.
#Java #для_новичков #beginner #stream_api #stream #short_circuit
👍4
Short-Circuit операции: управляемое прерывание
Short-circuit операции — терминальные операции, которые не требуют полного обхода источника для вычисления результата. Они аналогичны оператору break в императивных циклах, но реализованы через кооперативную отмену выполнения.
Каталог short-circuit операций:
findFirst() — возвращает первый элемент в порядке потока
findAny() — возвращает произвольный элемент (оптимизация для параллельных потоков)
anyMatch(Predicate) — true, если хотя бы один элемент удовлетворяет предикату
allMatch(Predicate) — true, если все элементы удовлетворяют предикату (может остановиться при первом нарушении)
noneMatch(Predicate) — true, если ни один элемент не удовлетворяет предикату
limit(long) — ограничивает количество элементов (stateful, но поддерживает short-circuit)
Механизм реализации short-circuit основан на флаге отмены. Когда операция типа findFirst находит нужный элемент, она устанавливает флаг cancellationRequested в объекте Sink. Этот флаг распространяется вверх по конвейеру, и источник прекращает обход при первой возможности.
Для anyMatch логика ещё эффективнее: как только предикат возвращает true, обход останавливается немедленно, без необходимости обрабатывать оставшиеся элементы.
Особенность limit. Операция limit(n) — промежуточная, но поддерживающая short-circuit. Она отслеживает количество пропущенных элементов и запрашивает отмену обхода после достижения лимита. Это критично для бесконечных потоков:
Без limit forEach никогда не завершился бы. С limit конвейер корректно останавливается после десятого элемента.
#Java #для_новичков #beginner #stream_api #stream #short_circuit
Short-circuit операции — терминальные операции, которые не требуют полного обхода источника для вычисления результата. Они аналогичны оператору break в императивных циклах, но реализованы через кооперативную отмену выполнения.
Каталог short-circuit операций:
findFirst() — возвращает первый элемент в порядке потока
findAny() — возвращает произвольный элемент (оптимизация для параллельных потоков)
anyMatch(Predicate) — true, если хотя бы один элемент удовлетворяет предикату
allMatch(Predicate) — true, если все элементы удовлетворяют предикату (может остановиться при первом нарушении)
noneMatch(Predicate) — true, если ни один элемент не удовлетворяет предикату
limit(long) — ограничивает количество элементов (stateful, но поддерживает short-circuit)
Механизм реализации short-circuit основан на флаге отмены. Когда операция типа findFirst находит нужный элемент, она устанавливает флаг cancellationRequested в объекте Sink. Этот флаг распространяется вверх по конвейеру, и источник прекращает обход при первой возможности.
// Упрощённая логика findFirst (псевдокод)
public Optional<T> findFirst() {
final AtomicReference<T> result = new AtomicReference<>();
evaluate(new TerminalSink<T, Void>() {
boolean found = false;
@Override
public void accept(T value) {
if (!found) {
result.set(value);
found = true;
// Запрос отмены дальнейшего обхода
cancellationRequested = true;
}
}
@Override
public boolean cancellationRequested() {
return found; // Сигнализируем об остановке
}
});
return found ? Optional.of(result.get()) : Optional.empty();
}
Для anyMatch логика ещё эффективнее: как только предикат возвращает true, обход останавливается немедленно, без необходимости обрабатывать оставшиеся элементы.
// Эффективный поиск: остановка при первом совпадении
boolean hasExpensive = books.stream()
.anyMatch(b -> b.price() > 1000); // Проверяет до первого совпадения, не все
// Неэффективная альтернатива
boolean hasExpensive = books.stream()
.filter(b -> b.price() > 1000)
.findFirst()
.isPresent(); // Больше объектов создаётся, но семантически эквивалентно
Особенность limit. Операция limit(n) — промежуточная, но поддерживающая short-circuit. Она отслеживает количество пропущенных элементов и запрашивает отмену обхода после достижения лимита. Это критично для бесконечных потоков:
Stream.iterate(0, i -> i + 1) // Бесконечная последовательность
.filter(i -> i % 2 == 0) // Чётные числа — тоже бесконечно
.limit(10) // Ограничение делает конечным
.forEach(System.out::println); // Выводит 0, 2, 4, 6, 8, 10, 12, 14, 16, 18
Без limit forEach никогда не завершился бы. С limit конвейер корректно останавливается после десятого элемента.
#Java #для_новичков #beginner #stream_api #stream #short_circuit
👍4
Вертикальная vs горизонтальная обработка
Критическое заблуждение о Stream API — представление, что элементы обрабатываются "по слоям": сначала все проходят filter, затем все проходят map, затем все попадают в collect. Реальность — вертикальная обработка: каждый элемент проходит через весь конвейер до конца, прежде чем берётся следующий.
Это архитектурное решение, необходимое для short-circuit. Если бы мы сначала фильтровали все элементы, мы не могли бы остановиться после нахождения первого подходящего — нам нужно было бы видеть все элементы, чтобы начать следующую стадию.
Рассмотрим конвейер:
При вертикальной обработке вывод будет чередоваться:
Если бы обработка была горизонтальной, мы увидели бы сначала все "Filter:", затем все "Map:", затем результат. Но тогда limit(2) не смог бы прервать выполнение после второго элемента — все фильтрации уже произошли бы.
Вертикальность имеет следствия для побочных эффектов. Если peek или map содержат логирование или модификацию внешнего состояния, порядок вызовов будет чередующимся, а не пакетным. Это затрудняет отладку, но обеспечивает корректность short-circuit семантики.
Ленивость и исключения
Ленивое выполнение влияет на обработку исключений. Если исключение возникает в лямбде промежуточной операции, оно будет брошено только при терминальной операции, что может затруднить трассировку:
Более того, если short-circuit операция останавливается до элемента с ошибкой, исключение никогда не возникнет:
Это делает потоки непредсказуемыми с точки зрения полноты валидации. Если необходима проверка всех элементов, short-circuit операции неприменимы — нужно использовать collect или forEach, требующие полного обхода.
#Java #для_новичков #beginner #stream_api #stream #short_circuit
Критическое заблуждение о Stream API — представление, что элементы обрабатываются "по слоям": сначала все проходят filter, затем все проходят map, затем все попадают в collect. Реальность — вертикальная обработка: каждый элемент проходит через весь конвейер до конца, прежде чем берётся следующий.
Это архитектурное решение, необходимое для short-circuit. Если бы мы сначала фильтровали все элементы, мы не могли бы остановиться после нахождения первого подходящего — нам нужно было бы видеть все элементы, чтобы начать следующую стадию.
Рассмотрим конвейер:
books.stream()
.filter(b -> {
System.out.println("Filter: " + b.title());
return b.year() > 1950;
})
.map(b -> {
System.out.println("Map: " + b.title());
return b.title().toUpperCase();
})
.limit(2)
.forEach(System.out::println);
При вертикальной обработке вывод будет чередоваться:
Filter: Book A (1900) // Отсеян, Map не вызывается
Filter: Book B (1960) // Прошёл
Map: Book B // Преобразован
BOOK B // forEach
Filter: Book C (1970) // Прошёл
Map: Book C // Преобразован
BOOK C // forEach
// limit(2) достигнут, остановка
Если бы обработка была горизонтальной, мы увидели бы сначала все "Filter:", затем все "Map:", затем результат. Но тогда limit(2) не смог бы прервать выполнение после второго элемента — все фильтрации уже произошли бы.
Вертикальность имеет следствия для побочных эффектов. Если peek или map содержат логирование или модификацию внешнего состояния, порядок вызовов будет чередующимся, а не пакетным. Это затрудняет отладку, но обеспечивает корректность short-circuit семантики.
Ленивость и исключения
Ленивое выполнение влияет на обработку исключений. Если исключение возникает в лямбде промежуточной операции, оно будет брошено только при терминальной операции, что может затруднить трассировку:
Stream<Book> stream = books.stream()
.map(b -> {
if (b.year() < 0) throw new IllegalStateException("Invalid year: " + b.year());
return b;
});
// Исключение не брошено
stream.forEach(System.out::println); // Исключение здесь, стектрейс указывает на forEach
Более того, если short-circuit операция останавливается до элемента с ошибкой, исключение никогда не возникнет:
books.stream()
.map(b -> {
if (b.year() < 0) throw new IllegalStateException("Invalid year");
return b;
})
.findFirst(); // Если первая книга валидна, исключение для второй (с ошибкой) не бросится
Это делает потоки непредсказуемыми с точки зрения полноты валидации. Если необходима проверка всех элементов, short-circuit операции неприменимы — нужно использовать collect или forEach, требующие полного обхода.
#Java #для_новичков #beginner #stream_api #stream #short_circuit
👍4🔥1
Что выведет код?
#Tasks
import java.util.function.Function;
public class Task160226 {
public static void main(String[] args) {
Function<Integer, Integer> f1 = i -> i * 2;
Function<Integer, Integer> f2 = i -> i + 1;
Function<Integer, Integer> f3 = f1.compose(f2);
Function<Integer, Integer> f4 = f1.andThen(f2);
System.out.println(f3.apply(5) + " " + f4.apply(5));
}
}
#Tasks
🔥4