Java for Beginner
869 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Раздел 8. Stream API и функциональный стиль

Глава 1: Философский фундамент. От шагов к преобразованиям

Императивный код — машина Тьюринга в миниатюре

Императивная парадигма программирования восходит к самым глубоким основаниям вычислительной теории. Когда вы пишете код в императивном стиле, вы буквально эмулируете работу машины Тьюринга — абстрактного вычислительного устройства, состоящего из бесконечной ленты и считывающей головки, которая перемещается по этой ленте, изменяя состояние ячеек согласно заложенным инструкциям. Ваши переменные — это ячейки памяти. Ваши операторы присваивания — это запись на ленту. Ваши циклы и условные переходы — это движение головки от одной ячейки к другой.

Рассмотрим типичную задачу: необходимо из списка заказов отфильтровать выполненные, вычислить общую сумму и вернуть топ-3 заказа по стоимости.

В императивной реализации мы начинаем с пустого состояния и последовательно его трансформируем:
public List<Order> getTopThreeCompletedOrders(List<Order> orders) {
// Инициализация временного состояния
List<Order> completedOrders = new ArrayList<>();

// Линейный проход с условным накоплением
for (Order order : orders) {
if (order.getStatus() == Status.COMPLETED) {
completedOrders.add(order);
}
}

// Сортировка через изменение состояния (in-place или новая коллекция)
completedOrders.sort((a, b) -> b.getAmount().compareTo(a.getAmount()));

// Извлечение подмножества с проверкой границ
List<Order> result = new ArrayList<>();
int limit = Math.min(3, completedOrders.size());
for (int i = 0; i < limit; i++) {
result.add(completedOrders.get(i));
}

return result;
}


Здесь каждая строчка — это шаг.

Шаг первый: создать пустой список.
Шаг второй: пройтись по исходным данным.
Шаг третий: проверить условие.
Шаг четвёртый: добавить в список.

Мы не описываем что хотим получить — мы диктуем как это получить, командуя машиной на каждом этапе.

Переменные-состояния в этом коде играют роль оперативной памяти машины Тьюринга. completedOrders — это наша лента, куда мы записываем промежуточные результаты. Переменная i в цикле — это позиция головки. Каждое присваивание — запись в ячейку. Каждое чтение — позиционирование головки.


Паттерны императивной обработки

Линейный проход — самая фундаментальная структура. Мы последовательно обрабатываем элементы от первого до последнего, поддерживая текущий индекс или итератор. Этот паттерн универсален: он работает для любой коллекции, не требует дополнительной памяти для структуры данных (только для результатов), позволяет легко внедрить логику раннего выхода.
// Линейный поиск с ранним выходом
User foundUser = null;
for (User user : users) {
if (user.getId().equals(targetId)) {
foundUser = user; // Нашли — запомнили состояние
break; // Прервали ленту — головка остановлена
}
}


Ранний выход (break, return) — мощный инструмент оптимизации императивного кода. Он позволяет избежать лишних вычислений, как только достигнуто желаемое состояние. В функциональном программировании аналогичное поведение достигается иначе — через ленивые вычисления и короткое замыкание операций, но в императивном коде это явная инструкция процессору прекратить исполнение текущего блока.

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

Это порождает разветвлённую логику внутри цикла:
List<Transaction> validTransactions = new ArrayList<>();
BigDecimal totalRisk = BigDecimal.ZERO;

for (Transaction tx : transactions) {
// Вложенные условия — сложное состояние машины
if (tx.getAmount().compareTo(THRESHOLD) > 0) {
if (tx.getTimestamp().isAfter(cutoffDate)) {
if (!tx.isFlagged()) {
validTransactions.add(tx);
totalRisk = totalRisk.add(tx.calculateRisk());
}
}
}
}


#Java #для_новичков #beginner #stream_api #imperative
👍4
Здесь мы наблюдаем эскалацию сложности состояния: несколько переменных (validTransactions, totalRisk) изменяются синхронно, и их консистентность поддерживается программистом вручную. Если забыть обновить totalRisk при добавлении транзакции — получим рассогласование состояний, баг, который сложно отследить, потому что он проявляется не сразу, а в конечном результате вычислений.

Вложенные циклы — квинтэссенция императивной вычислительной мощи и одновременно её проклятие. Когда данные имеют иерархическую структуру (категории, содержащие товары, содержащие отзывы), мы запускаем машину Тьюринга внутри машины Тьюринга. Внешняя лента — категории.

Для каждой позиции внешней ленты мы разворачиваем внутреннюю ленту — товары этой категории.
// O(n × m × k) — полиномиальная сложность
List<Review> allRecentReviews = new ArrayList<>();

for (Category category : categories) {
for (Product product : category.getProducts()) {
for (Review review : product.getReviews()) {
if (review.getDate().isAfter(lastMonth)) {
allRecentReviews.add(review);
}
}
}
}


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


Сила императивного подхода

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

Оптимизация аллокаций — яркий пример. В примере с заказами выше мы создали два промежуточных списка: completedOrders и result.

Опытный разработчик может соптимизировать это до одного прохода с ограниченной очередью (Bounded Priority Queue), избегая хранения всех выполненных заказов в памяти:
public List<Order> getTopThreeOptimized(List<Order> orders) {
// Очередь с обратным порядком (минимальная сумма на вершине)
PriorityQueue<Order> minHeap = new PriorityQueue<>(
Comparator.comparing(Order::getAmount)
);

for (Order order : orders) {
if (order.getStatus() == Status.COMPLETED) {
if (minHeap.size() < 3) {
minHeap.offer(order);
} else if (order.getAmount().compareTo(minHeap.peek().getAmount()) > 0) {
minHeap.poll(); // Удаляем минимальный из топ-3
minHeap.offer(order);
}
}
}

// Преобразование в список и сортировка по убыванию
List<Order> result = new ArrayList<>(minHeap);
result.sort(Comparator.comparing(Order::getAmount).reversed());
return result;
}


Этот код сложнее для понимания, но он использует O(1) дополнительной памяти (константный размер кучи) вместо O(n) для хранения всех выполненных заказов. В императивном стиле такая оптимизация достижима, потому что мы управляем состоянием явно.

Прерывание выполнения в любой точке — ещё одно преимущество.

Предположим, мы ищем первый элемент, удовлетворяющий сложному условию, и должны выполнить постобработку при нахождении:
for (Item item : items) {
if (complexValidation(item)) {
processFoundItem(item); // Постобработка
return result; // Мгновенный выход из метода
}
}


Никаких дополнительных проверок, никакого Optional, никакого пропагирования состояния "найдено/не найдено" наружу — просто остановка машины в нужный момент.


#Java #для_новичков #beginner #stream_api #imperative
👍3
Слабость: связанность и состояние как источник ошибок

Высокая связанность (coupling) кода и бизнес-логики проявляется в том, что алгоритмическая механика неотделима от семантики предметной области. Когда вы читаете императивный цикл, вы видите if (order.getStatus() == Status.COMPLETED) вперемешку с i++ и list.add(). Бизнес-правило "учитывать только выполненные заказы" склеено с технической деталью "добавлять в ArrayList".

Это затрудняет тестирование. Вы не можете протестировать логику фильтрации отдельно от логики накопления. Вы не можете легко заменить ArrayList на LinkedList или на потоковый вывод без модификации всего метода. Вы не можете повторно использовать этот цикл для другой операции — скажем, подсчёта количества вместо сбора в список — без копирования и модификации кода.

Но главный враг императивного кода — изменяемое состояние. Каждая переменная, которая может быть перезаписана, — потенциальная точка отказа. Рассмотрим многопоточный контекст: если два потока одновременно выполняют накопление в общий список, получаем race condition (состояние гонки) — классический конкурентный баг, когда результат зависит от недетерминированного порядка выполнения операций. Даже в однопоточном коде состояние требует умственной нагрузки: отслеживая, какое значение находится в переменной на каждой строке, вы вынуждены эмулировать выполнение программы в голове.

Состояние размножается. Переменная completedOrders требует переменной i для итерации. i требует проверки границ. Проверка границ требует вычисления size(). Вычисление размера требует консистентности структуры данных. Если внутри цикла случайно модифицировать коллекцию, по которой итерируемся (например, удалить элемент), индексы съезжают, и мы получаем ConcurrentModificationException или пропуск элементов.
Вложенные циклы с состоянием особенно опасны. Легко перепутать индексы i, j, k, обратиться к products.get(i) вместо products.get(j), и компилятор не подскажет — типы совпадают, индексы валидны, логика сломана.

Возвращаясь к машине Тьюринга: императивный код заставляет вас думать как машина. Вы управляете лентой, головкой, таблицей переходов. Но программирование — это искусство абстракции, и следующий шаг эволюции — подняться над механикой вычислений к декларативному описанию преобразований, где фокус смещается с "как" на "что".


#Java #для_новичков #beginner #stream_api #imperative
👍3