Удаление по значению в ArrayList
Операция remove(Object o) в ArrayList сочетает в себе линейный поиск (аналогично contains) и структурную реорганизацию (аналогично удалению по индексу).
Детальный процесс выполнения remove(element)
Фаза поиска:
Выполняется последовательный обход массива от начала до конца для поиска первого вхождения элемента. Поиск использует семантику equals() и специально обрабатывает null значения.
Фаза удаления:
Если элемент найден на позиции i:
Сохраняется значение для возврата
Выполняется сдвиг элементов от i+1 до size-1 на одну позицию влево
Последняя позиция обнуляется
Размер уменьшается, modCount увеличивается
Особенности:
Удаляется только первое вхождение элемента
Если элемент не найден, возвращается false
Если элемент найден и удален, возвращается true
Временная сложность
Операция имеет сложность O(n) в худшем случае:
Поиск: O(n) в худшем случае
Удаление: O(n) в худшем случае (при удалении из начала)
Удаление по значению в LinkedList
Процесс аналогичен удалению по индексу, но с предварительным поиском по значению.
Детальный процесс
Фаза поиска:
Последовательный обход узлов от головы до хвоста с использованием equals() для сравнения.
Фаза удаления:
При нахождении узла с совпадающим значением выполняется операция "вырезания" аналогично удалению по индексу.
Особенности:
Удаляется только первый найденный узел с совпадающим значением
Возвращает true при успешном удалении, false при отсутствии элемента
Сравнительный анализ операций удаления
Временная сложность
Удаление по индексу:
ArrayList: O(n) в худшем случае (удаление из начала)
LinkedList: O(n) в худшем случае (удаление из середины)
Удаление по значению:
ArrayList: O(n) поиск + O(n) удаление = O(n)
LinkedList: O(n) поиск + O(1) удаление = O(n)
Потребление памяти
ArrayList:
Не требует дополнительной памяти (кроме временных переменных)
Может оставлять "пустоты" в массиве (обнуленные ссылки)
LinkedList:
Освобождает память узла (24-32 байта на узел)
Требует времени garbage collector для очистки
Многопоточные аспекты
Проблемы конкурентного доступа
Несинхронизированные реализации:
Обе операции не являются thread-safe и могут привести к:
Состояниям гонки при одновременных модификациях
Повреждению внутренних структур данных
Непредсказуемому поведению итераторов
Стратегии синхронизации
Явная синхронизация:
Thread-safe альтернативы:
CopyOnWriteArrayList для сценариев "частое чтение, редкая запись"
Collections.synchronizedList() с external locking
Concurrent коллекции для специализированных use cases
Memory consistency
Операции remove и contains требуют proper memory barriers для обеспечения visibility изменений между потоками. Без синхронизации нет гарантий, что один поток увидит изменения, сделанные другим потоком.
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
Операция remove(Object o) в ArrayList сочетает в себе линейный поиск (аналогично contains) и структурную реорганизацию (аналогично удалению по индексу).
Детальный процесс выполнения remove(element)
Фаза поиска:
Выполняется последовательный обход массива от начала до конца для поиска первого вхождения элемента. Поиск использует семантику equals() и специально обрабатывает null значения.
Фаза удаления:
Если элемент найден на позиции i:
Сохраняется значение для возврата
Выполняется сдвиг элементов от i+1 до size-1 на одну позицию влево
Последняя позиция обнуляется
Размер уменьшается, modCount увеличивается
Особенности:
Удаляется только первое вхождение элемента
Если элемент не найден, возвращается false
Если элемент найден и удален, возвращается true
Временная сложность
Операция имеет сложность O(n) в худшем случае:
Поиск: O(n) в худшем случае
Удаление: O(n) в худшем случае (при удалении из начала)
Удаление по значению в LinkedList
Процесс аналогичен удалению по индексу, но с предварительным поиском по значению.
Детальный процесс
Фаза поиска:
Последовательный обход узлов от головы до хвоста с использованием equals() для сравнения.
Фаза удаления:
При нахождении узла с совпадающим значением выполняется операция "вырезания" аналогично удалению по индексу.
Особенности:
Удаляется только первый найденный узел с совпадающим значением
Возвращает true при успешном удалении, false при отсутствии элемента
Сравнительный анализ операций удаления
Временная сложность
Удаление по индексу:
ArrayList: O(n) в худшем случае (удаление из начала)
LinkedList: O(n) в худшем случае (удаление из середины)
Удаление по значению:
ArrayList: O(n) поиск + O(n) удаление = O(n)
LinkedList: O(n) поиск + O(1) удаление = O(n)
Потребление памяти
ArrayList:
Не требует дополнительной памяти (кроме временных переменных)
Может оставлять "пустоты" в массиве (обнуленные ссылки)
LinkedList:
Освобождает память узла (24-32 байта на узел)
Требует времени garbage collector для очистки
Многопоточные аспекты
Проблемы конкурентного доступа
Несинхронизированные реализации:
Обе операции не являются thread-safe и могут привести к:
Состояниям гонки при одновременных модификациях
Повреждению внутренних структур данных
Непредсказуемому поведению итераторов
Стратегии синхронизации
Явная синхронизация:
synchronized(list) {
if (list.contains(element)) {
list.remove(element);
}
}Thread-safe альтернативы:
CopyOnWriteArrayList для сценариев "частое чтение, редкая запись"
Collections.synchronizedList() с external locking
Concurrent коллекции для специализированных use cases
Memory consistency
Операции remove и contains требуют proper memory barriers для обеспечения visibility изменений между потоками. Без синхронизации нет гарантий, что один поток увидит изменения, сделанные другим потоком.
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
👍1
Оптимизации и специализированные сценарии
Эффективные паттерны использования
Для ArrayList:
Удаление с конца более эффективно, чем с начала
Пакетное удаление может быть оптимизировано через создание нового списка
Использование removeAll(Collection) для массовых операций
Для LinkedList:
Удаление из начала/конца значительно эффективнее, чем из середины
Использование removeFirst()/removeLast() для операций на концах
Итераторные методы более эффективны для последовательного удаления
Избегание неэффективных паттернов
Антипаттерны:
Частые удаления из начала ArrayList
Использование contains перед remove без необходимости (двойной обход)
Игнорирование возможности использования Iterator.remove()
Оптимизированные подходы:
Влияние на итераторы
Fail-fast семантика
Обе операции изменяют modCount, что влияет на поведение итераторов:
Любое изменение списка invalidates все активные итераторы
Последующие операции итератора вызывают ConcurrentModificationException
Только Iterator.remove() может безопасно удалять элементы во время итерации
Безопасное удаление во время итерации
Правильный подход:
Неправильный подход:
Производительность в реальных сценариях
Сравнение операций contains:
ArrayList: 5-100 нс в зависимости от позиции элемента
LinkedList: 10-200 нс в зависимости от позиции и размера списка
Сравнение операций remove:
ArrayList (удаление из конца): 10-20 нс
ArrayList (удаление из начала): 100-5000 нс для 1000 элементов
LinkedList (удаление из конца): 10-30 нс
LinkedList (удаление из начала): 10-30 нс
LinkedList (удаление из середины): 50-5000 нс для 1000 элементов
Влияние размера коллекции
Малые коллекции (до 100 элементов):
Различия между ArrayList и LinkedList минимальны, часто доминируют другие факторы.
Средние коллекции (100-10,000 элементов):
ArrayList обычно превосходит LinkedList для большинства операций, кроме вставки/удаления в начале.
Большие коллекции (более 10,000 элементов):
ArrayList значительно превосходит LinkedList для операций доступа и поиска, но страдает при частых вставках/удалениях в начале.
Практические рекомендации
Критерии выбора реализации
Выбор ArrayList когда:
Преобладают операции случайного доступа и поиска
Редкие вставки/удаления в начале списка
Известен или может быть оценен конечный размер
Критически важна memory locality и кэширование
Выбор LinkedList когда:
Частые вставки/удаления в начале списка
Последовательный доступ является доминирующим паттерном
Размер списка сильно варьируется
Память не является основным ограничением
Оптимизация алгоритмов
Для частых операций contains/remove:
Рассмотреть использование HashSet для операций проверки существования
Использовать специализированные структуры данных для specific use cases
Кэшировать результаты частых проверок
Для пакетных операций:
Использовать removeAll() вместо цикла с индивидуальными удалениями
Рассмотреть создание новых коллекций вместо модификации существующих
Использовать stream API для декларативных операций
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
Эффективные паттерны использования
Для ArrayList:
Удаление с конца более эффективно, чем с начала
Пакетное удаление может быть оптимизировано через создание нового списка
Использование removeAll(Collection) для массовых операций
Для LinkedList:
Удаление из начала/конца значительно эффективнее, чем из середины
Использование removeFirst()/removeLast() для операций на концах
Итераторные методы более эффективны для последовательного удаления
Избегание неэффективных паттернов
Антипаттерны:
Частые удаления из начала ArrayList
Использование contains перед remove без необходимости (двойной обход)
Игнорирование возможности использования Iterator.remove()
Оптимизированные подходы:
// Вместо:
if (list.contains(element)) {
list.remove(element); // Двойной обход
}
// Использовать:
boolean removed = list.remove(element); // Один обход с ранним выходом
Влияние на итераторы
Fail-fast семантика
Обе операции изменяют modCount, что влияет на поведение итераторов:
Любое изменение списка invalidates все активные итераторы
Последующие операции итератора вызывают ConcurrentModificationException
Только Iterator.remove() может безопасно удалять элементы во время итерации
Безопасное удаление во время итерации
Правильный подход:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (shouldRemove(element)) {
iterator.remove(); // Безопасное удаление
}
}
Неправильный подход:
for (String element : list) {
if (shouldRemove(element)) {
list.remove(element); // ConcurrentModificationException
}
}Производительность в реальных сценариях
Сравнение операций contains:
ArrayList: 5-100 нс в зависимости от позиции элемента
LinkedList: 10-200 нс в зависимости от позиции и размера списка
Сравнение операций remove:
ArrayList (удаление из конца): 10-20 нс
ArrayList (удаление из начала): 100-5000 нс для 1000 элементов
LinkedList (удаление из конца): 10-30 нс
LinkedList (удаление из начала): 10-30 нс
LinkedList (удаление из середины): 50-5000 нс для 1000 элементов
Влияние размера коллекции
Малые коллекции (до 100 элементов):
Различия между ArrayList и LinkedList минимальны, часто доминируют другие факторы.
Средние коллекции (100-10,000 элементов):
ArrayList обычно превосходит LinkedList для большинства операций, кроме вставки/удаления в начале.
Большие коллекции (более 10,000 элементов):
ArrayList значительно превосходит LinkedList для операций доступа и поиска, но страдает при частых вставках/удалениях в начале.
Практические рекомендации
Критерии выбора реализации
Выбор ArrayList когда:
Преобладают операции случайного доступа и поиска
Редкие вставки/удаления в начале списка
Известен или может быть оценен конечный размер
Критически важна memory locality и кэширование
Выбор LinkedList когда:
Частые вставки/удаления в начале списка
Последовательный доступ является доминирующим паттерном
Размер списка сильно варьируется
Память не является основным ограничением
Оптимизация алгоритмов
Для частых операций contains/remove:
Рассмотреть использование HashSet для операций проверки существования
Использовать специализированные структуры данных для specific use cases
Кэшировать результаты частых проверок
Для пакетных операций:
Использовать removeAll() вместо цикла с индивидуальными удалениями
Рассмотреть создание новых коллекций вместо модификации существующих
Использовать stream API для декларативных операций
#Java #для_новичков #beginner #List #ArrayList #LinkedList #remove #contains
👍3
Архитектура Spring Cloud Gateway: Внутренние компоненты и жизненный цикл запроса
В основе Spring Cloud Gateway лежат три ключевые абстракции, которые формируют декларативную модель конфигурации: Route, Predicate и Filter. Эти сущности организованы в строгую иерархию, где каждая выполняет свою роль в обработке входящего запроса.
Route (Маршрут)
Является центральной конфигурационной единицей. Он определяет полный путь обработки конкретного запроса от получения до возврата ответа. Маршрут инкапсулирует три основных элемента: уникальный идентификатор, целевой URI (куда будет перенаправлен запрос после обработки) и упорядоченные коллекции предикатов и фильтров. Внутренне маршрут представлен классом org.springframework.cloud.gateway.route.Route, который является иммутабельным объектом. Иммутабельность критически важна, поскольку маршруты могут динамически обновляться в runtime (например, через обновление конфигурации из Spring Cloud Config), и необходимо гарантировать согласованное состояние во время обработки запроса.
Конфигурация маршрута в YAML демонстрирует эту структуру:
Predicate (Предикат)
Условие, которое определяет, должен ли данный маршрут быть применён к входящему запросу. Предикаты реализуют функциональный интерфейс Predicate<ServerWebExchange>, где ServerWebExchange является контейнером для HTTP-запроса и ответа, а также дополнительных атрибутов, накопленных в процессе обработки. Предикаты оцениваются в определённом порядке, и первый маршрут, чьи предикаты возвращают true для данного ServerWebExchange, выбирается для дальнейшей обработки. Типичные предикаты включают проверку пути (Path=/api/**), метода HTTP (Method=GET,POST), наличия заголовков (Header=X-Request-Id, \\d+), параметров запроса, хоста, кук и даже сложные временные условия (After=2023-01-20T17:42:47.789-07:00[America/Denver]). Механизм предикатов позволяет реализовать сложную логику маршрутизации без написания imperative-кода.
Filter (Фильтр)
Компонент, который модифицирует ServerWebExchange до или после вызова целевого сервиса. Фильтры организованы в цепочку (Gateway Filter Chain) и выполняются в строгом порядке. Они реализуют интерфейс GatewayFilter, который содержит единственный метод Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain). Фильтры разделяются на две категории по моменту выполнения: "pre-filters" выполняются до передачи запроса целевому сервису (модификация запроса, аутентификация, логирование), а "post-filters" — после получения ответа от целевого сервиса (модификация ответа, добавление заголовков, метрик). Порядок выполнения фильтров внутри цепочки определяется их конфигурацией в маршруте.
Reactor Netty как HTTP-сервер: архитектурный фундамент
Spring Cloud Gateway построен на реактивном стеке Spring WebFlux, который в свою очередь использует Reactor Netty в качестве HTTP-сервера. Это фундаментальный архитектурный выбор, отличающий SCG от традиционных сервлетных контейнеров.
#Java #middle #Spring_Cloud_Gateway
В основе Spring Cloud Gateway лежат три ключевые абстракции, которые формируют декларативную модель конфигурации: Route, Predicate и Filter. Эти сущности организованы в строгую иерархию, где каждая выполняет свою роль в обработке входящего запроса.
Route (Маршрут)
Является центральной конфигурационной единицей. Он определяет полный путь обработки конкретного запроса от получения до возврата ответа. Маршрут инкапсулирует три основных элемента: уникальный идентификатор, целевой URI (куда будет перенаправлен запрос после обработки) и упорядоченные коллекции предикатов и фильтров. Внутренне маршрут представлен классом org.springframework.cloud.gateway.route.Route, который является иммутабельным объектом. Иммутабельность критически важна, поскольку маршруты могут динамически обновляться в runtime (например, через обновление конфигурации из Spring Cloud Config), и необходимо гарантировать согласованное состояние во время обработки запроса.
Конфигурация маршрута в YAML демонстрирует эту структуру:
spring:
cloud:
gateway:
routes:
- id: user_service_route
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Gateway-Tag, processed
Predicate (Предикат)
Условие, которое определяет, должен ли данный маршрут быть применён к входящему запросу. Предикаты реализуют функциональный интерфейс Predicate<ServerWebExchange>, где ServerWebExchange является контейнером для HTTP-запроса и ответа, а также дополнительных атрибутов, накопленных в процессе обработки. Предикаты оцениваются в определённом порядке, и первый маршрут, чьи предикаты возвращают true для данного ServerWebExchange, выбирается для дальнейшей обработки. Типичные предикаты включают проверку пути (Path=/api/**), метода HTTP (Method=GET,POST), наличия заголовков (Header=X-Request-Id, \\d+), параметров запроса, хоста, кук и даже сложные временные условия (After=2023-01-20T17:42:47.789-07:00[America/Denver]). Механизм предикатов позволяет реализовать сложную логику маршрутизации без написания imperative-кода.
Filter (Фильтр)
Компонент, который модифицирует ServerWebExchange до или после вызова целевого сервиса. Фильтры организованы в цепочку (Gateway Filter Chain) и выполняются в строгом порядке. Они реализуют интерфейс GatewayFilter, который содержит единственный метод Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain). Фильтры разделяются на две категории по моменту выполнения: "pre-filters" выполняются до передачи запроса целевому сервису (модификация запроса, аутентификация, логирование), а "post-filters" — после получения ответа от целевого сервиса (модификация ответа, добавление заголовков, метрик). Порядок выполнения фильтров внутри цепочки определяется их конфигурацией в маршруте.
Reactor Netty как HTTP-сервер: архитектурный фундамент
Spring Cloud Gateway построен на реактивном стеке Spring WebFlux, который в свою очередь использует Reactor Netty в качестве HTTP-сервера. Это фундаментальный архитектурный выбор, отличающий SCG от традиционных сервлетных контейнеров.
#Java #middle #Spring_Cloud_Gateway
👍1
Netty — это асинхронный event-driven фреймворк сетевого программирования, работающий на уровне NIO (Non-blocking I/O). В контексте JVM это означает, что вместо использования blocking socket operations и пула потоков (thread-per-connection модель), Netty использует небольшое количество потоков-селекторов (event loop threads), которые обрабатывают события на множестве соединений. Каждое соединение ассоциировано с каналом (Channel), а события (чтение данных, запись данных, изменение состояния соединения) диспетчеризуются через цепочки обработчиков (ChannelPipeline).
Когда Spring Boot приложение со SCG стартует, автоконфигурация ReactorNettyAutoConfiguration создаёт и настраивает экземпляр HttpServer Netty. Конфигурация по умолчанию устанавливает количество потоков event loop group равным количеству доступных процессорных ядер (Runtime.getRuntime().availableProcessors()), что является оптимальным для CPU-bound операций. Каждый поток event loop обслуживает множество соединений, переключаясь между ними по мере появления событий ввода-вывода.
В памяти JVM это приводит к совершенно иной структуре по сравнению с традиционными сервлетными контейнерами. Вместо большого пула потоков (200-500 объектов Thread в heap), каждый со своим стеком (1MB по умолчанию), создаётся небольшое количество долгоживущих потоков Netty. Основные структуры данных в heap — это буферы ByteBuf (которые Netty эффективно пуллит через ByteBufAllocator), объекты Channel и их контексты, а также реактивные потоки (Mono, Flux) и их операторы, представляющие pipeline обработки запроса.
Жизненный цикл запроса: от байтов в сокете до ответа
Фаза 1: Обработка в Netty и преобразование в ServerWebExchange
Когда клиент устанавливает TCP-соединение и отправляет HTTP-запрос, Netty event loop thread получает событие channelRead. Сырые байты из сокета декодируются в объект HttpRequest Netty. Затем через адаптер ReactorServerHttpRequest этот запрос оборачивается в реактивный тип ServerHttpRequest Spring WebFlux. Создаётся контейнер ServerWebExchange, который будет нести состояние запроса через весь pipeline обработки. Критически важно, что на этом этапе тело запроса ещё не читается полностью — оно представлено как реактивный поток Flux<DataBuffer>, что позволяет обрабатывать запросы потоково, без буферизации всего тела в памяти.
Фаза 2: Сопоставление маршрута через HandlerMapping
Обработка передаётся в DispatcherHandler Spring WebFlux, который ищет подходящий обработчик для запроса. В контексте SCG ключевым является RoutePredicateHandlerMapping — специализированная реализация HandlerMapping. Его задача — найти подходящий маршрут для текущего запроса.
Процесс сопоставления начинается с получения всех доступных маршрутов через RouteLocator. RouteLocator — это абстракция, которая предоставляет поток маршрутов. Реализация по умолчанию CachingRouteLocator кэширует маршруты для производительности, но поддерживает механизмы инвалидации при динамическом обновлении конфигурации. Каждый маршрут проверяется последовательно: для каждого предиката маршрута вызывается метод test(ServerWebExchange). Проверка предикатов выполняется синхронно (хотя сами предикаты могут выполнять асинхронные операции) до первого совпадения.
Сложность предиката Path демонстрирует детали реализации: при конфигурации Path=/api/users/** создаётся PathRoutePredicateFactory. Внутри он использует PathPatternParser из Spring WebFlux для компиляции строки шаблона в оптимизированную структуру данных PathPattern. При сопоставлении выполняется не простое строковое сравнение, а эффективный алгоритм сопоставления с извлечением переменных пути (например, /api/users/{id}). Это существенно быстрее, чем регулярные выражения, и не создает издержек при большом количестве маршрутов.
Когда маршрут найден, он сохраняется в атрибутах ServerWebExchange под ключом Route.class.getName(), и управление передаётся соответствующему обработчику — FilteringWebHandler.
#Java #middle #Spring_Cloud_Gateway
Когда Spring Boot приложение со SCG стартует, автоконфигурация ReactorNettyAutoConfiguration создаёт и настраивает экземпляр HttpServer Netty. Конфигурация по умолчанию устанавливает количество потоков event loop group равным количеству доступных процессорных ядер (Runtime.getRuntime().availableProcessors()), что является оптимальным для CPU-bound операций. Каждый поток event loop обслуживает множество соединений, переключаясь между ними по мере появления событий ввода-вывода.
В памяти JVM это приводит к совершенно иной структуре по сравнению с традиционными сервлетными контейнерами. Вместо большого пула потоков (200-500 объектов Thread в heap), каждый со своим стеком (1MB по умолчанию), создаётся небольшое количество долгоживущих потоков Netty. Основные структуры данных в heap — это буферы ByteBuf (которые Netty эффективно пуллит через ByteBufAllocator), объекты Channel и их контексты, а также реактивные потоки (Mono, Flux) и их операторы, представляющие pipeline обработки запроса.
Жизненный цикл запроса: от байтов в сокете до ответа
Фаза 1: Обработка в Netty и преобразование в ServerWebExchange
Когда клиент устанавливает TCP-соединение и отправляет HTTP-запрос, Netty event loop thread получает событие channelRead. Сырые байты из сокета декодируются в объект HttpRequest Netty. Затем через адаптер ReactorServerHttpRequest этот запрос оборачивается в реактивный тип ServerHttpRequest Spring WebFlux. Создаётся контейнер ServerWebExchange, который будет нести состояние запроса через весь pipeline обработки. Критически важно, что на этом этапе тело запроса ещё не читается полностью — оно представлено как реактивный поток Flux<DataBuffer>, что позволяет обрабатывать запросы потоково, без буферизации всего тела в памяти.
Фаза 2: Сопоставление маршрута через HandlerMapping
Обработка передаётся в DispatcherHandler Spring WebFlux, который ищет подходящий обработчик для запроса. В контексте SCG ключевым является RoutePredicateHandlerMapping — специализированная реализация HandlerMapping. Его задача — найти подходящий маршрут для текущего запроса.
Процесс сопоставления начинается с получения всех доступных маршрутов через RouteLocator. RouteLocator — это абстракция, которая предоставляет поток маршрутов. Реализация по умолчанию CachingRouteLocator кэширует маршруты для производительности, но поддерживает механизмы инвалидации при динамическом обновлении конфигурации. Каждый маршрут проверяется последовательно: для каждого предиката маршрута вызывается метод test(ServerWebExchange). Проверка предикатов выполняется синхронно (хотя сами предикаты могут выполнять асинхронные операции) до первого совпадения.
Сложность предиката Path демонстрирует детали реализации: при конфигурации Path=/api/users/** создаётся PathRoutePredicateFactory. Внутри он использует PathPatternParser из Spring WebFlux для компиляции строки шаблона в оптимизированную структуру данных PathPattern. При сопоставлении выполняется не простое строковое сравнение, а эффективный алгоритм сопоставления с извлечением переменных пути (например, /api/users/{id}). Это существенно быстрее, чем регулярные выражения, и не создает издержек при большом количестве маршрутов.
Когда маршрут найден, он сохраняется в атрибутах ServerWebExchange под ключом Route.class.getName(), и управление передаётся соответствующему обработчику — FilteringWebHandler.
#Java #middle #Spring_Cloud_Gateway
👍1
Фаза 3: Выполнение цепочки фильтров
FilteringWebHandler — это сердце логики преобразования запроса. Он получает выбранный маршрут и строит цепочку фильтров, упорядочивая их согласно конфигурации. Цепочка представляет собой реактивный pipeline, где каждый фильтр — это оператор, трансформирующий ServerWebExchange.
Порядок выполнения фильтров строго определён:
Сначала выполняются все GlobalFilter (глобальные фильтры), зарегистрированные в контексте приложения. Глобальные фильтры выполняются в порядке, определённом их значением getOrder().
Затем выполняются фильтры, специфичные для маршрута, в том порядке, в котором они объявлены в конфигурации маршрута.
Каждый фильтр получает контроль над ServerWebExchange и может либо модифицировать его, либо передать управление следующему фильтру в цепочке через вызов chain.filter(exchange), либо завершить обработку, вернув ответ непосредственно из шлюза. Последний сценарий используется, например, когда фильтр аутентификации обнаруживает невалидный токен и возвращает 401 Unauthorized.
Пример кастомного pre-фильтра для логирования:
#Java #middle #Spring_Cloud_Gateway
FilteringWebHandler — это сердце логики преобразования запроса. Он получает выбранный маршрут и строит цепочку фильтров, упорядочивая их согласно конфигурации. Цепочка представляет собой реактивный pipeline, где каждый фильтр — это оператор, трансформирующий ServerWebExchange.
Порядок выполнения фильтров строго определён:
Сначала выполняются все GlobalFilter (глобальные фильтры), зарегистрированные в контексте приложения. Глобальные фильтры выполняются в порядке, определённом их значением getOrder().
Затем выполняются фильтры, специфичные для маршрута, в том порядке, в котором они объявлены в конфигурации маршрута.
Каждый фильтр получает контроль над ServerWebExchange и может либо модифицировать его, либо передать управление следующему фильтру в цепочке через вызов chain.filter(exchange), либо завершить обработку, вернув ответ непосредственно из шлюза. Последний сценарий используется, например, когда фильтр аутентификации обнаруживает невалидный токен и возвращает 401 Unauthorized.
Пример кастомного pre-фильтра для логирования:
@Component
public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
private static final Logger log = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);
public LoggingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
log.info("Incoming request: {} {} from {}",
request.getMethod(),
request.getURI().getPath(),
request.getRemoteAddress());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
ServerHttpResponse response = exchange.getResponse();
log.info("Completed request: {} {} -> {} in {}ms",
request.getMethod(),
request.getURI().getPath(),
response.getStatusCode(),
duration);
}));
};
}
public static class Config {
// Конфигурационные свойства фильтра
}
}
Этот фильтр демонстрирует важный паттерн: логирование в pre-фильтре, а измерение времени и логирование результата — в post-части, реализованной через then(Mono.fromRunnable(...)). Обратите внимание, что весь фильтр — это функция, возвращающая Mono<Void>, и логирование выполняется в реактивном стиле без блокирования потоков.
#Java #middle #Spring_Cloud_Gateway
👍1
Фаза 4: Проксирование запроса к целевому сервису
После выполнения всех pre-фильтров управление доходит до ключевого фильтра — NettyRoutingFilter (или WebClientHttpRoutingFilter в альтернативной реализации). Именно этот фильтр выполняет фактическое проксирование запроса к целевому сервису.
Процесс проксирования включает несколько шагов:
Преобразование URI назначения: Если URI маршрута использует схему lb:// (например, lb://USER-SERVICE), вызывается LoadBalancerClientFilter (или реактивный ReactorLoadBalancer), который преобразует логическое имя сервиса в физический адрес, выбирая конкретный экземпляр с учётом алгоритма балансировки нагрузки.
Подготовка запроса прокси: NettyRoutingFilter создаёт новый HTTP-запрос Netty, копируя метод, заголовки и тело из оригинального запроса. При этом он может применять трансформации, определённые фильтрами (например, перезапись пути, добавление заголовков).
Асинхронное выполнение запроса: Запрос отправляется через реактивный HTTP-клиент Netty (HttpClient). Клиент Netty работает в неблокирующем режиме — он ставит запрос в очередь на отправку и немедленно возвращает Mono<HttpClientResponse> без блокировки потока. Event loop thread освобождается для обработки других соединений.
Обработка ответа: Когда от целевого сервиса приходит ответ, Netty генерирует событие, которое обрабатывается реактивным pipeline. Тело ответа также остаётся в реактивном представлении (Flux<DataBuffer>), что позволяет streamingly передавать большие ответы без буферизации в памяти.
Конфигурация для балансировки нагрузки с помощью Spring Cloud LoadBalancer:
Фаза 5: Пост-обработка ответа и завершение
После получения ответа от целевого сервиса выполняется оставшаяся часть цепочки фильтров — post-фильтры. Эти фильтры получают доступ как к оригинальному запросу, так и к ответу от целевого сервиса. Они могут модифицировать статус-код, заголовки, тело ответа.
Пример post-фильтра для добавления стандартных заголовков безопасности:
После выполнения всех post-фильтров финальный ответ записывается обратно в исходный канал Netty к клиенту. Netty берёт на себя эффективную отправку данных, включая chunked encoding для stream-ответов.
#Java #middle #Spring_Cloud_Gateway
После выполнения всех pre-фильтров управление доходит до ключевого фильтра — NettyRoutingFilter (или WebClientHttpRoutingFilter в альтернативной реализации). Именно этот фильтр выполняет фактическое проксирование запроса к целевому сервису.
Процесс проксирования включает несколько шагов:
Преобразование URI назначения: Если URI маршрута использует схему lb:// (например, lb://USER-SERVICE), вызывается LoadBalancerClientFilter (или реактивный ReactorLoadBalancer), который преобразует логическое имя сервиса в физический адрес, выбирая конкретный экземпляр с учётом алгоритма балансировки нагрузки.
Подготовка запроса прокси: NettyRoutingFilter создаёт новый HTTP-запрос Netty, копируя метод, заголовки и тело из оригинального запроса. При этом он может применять трансформации, определённые фильтрами (например, перезапись пути, добавление заголовков).
Асинхронное выполнение запроса: Запрос отправляется через реактивный HTTP-клиент Netty (HttpClient). Клиент Netty работает в неблокирующем режиме — он ставит запрос в очередь на отправку и немедленно возвращает Mono<HttpClientResponse> без блокировки потока. Event loop thread освобождается для обработки других соединений.
Обработка ответа: Когда от целевого сервиса приходит ответ, Netty генерирует событие, которое обрабатывается реактивным pipeline. Тело ответа также остаётся в реактивном представлении (Flux<DataBuffer>), что позволяет streamingly передавать большие ответы без буферизации в памяти.
Конфигурация для балансировки нагрузки с помощью Spring Cloud LoadBalancer:
spring:
cloud:
gateway:
routes:
- id: user_service_lb
uri: lb://user-service
predicates:
- Path=/users/**
loadbalancer:
configurations: default
# Конфигурация для кэширования resolved адресов
discovery:
locator:
enabled: true
lower-case-service-id: true
При использовании lb:// схема автоматически активирует интеграцию с Service Discovery (Eureka, Consul) через ReactiveLoadBalancer. Выбор экземпляра выполняется с учётом состояния здоровья и выбранного алгоритма (round-robin по умолчанию).
Фаза 5: Пост-обработка ответа и завершение
После получения ответа от целевого сервиса выполняется оставшаяся часть цепочки фильтров — post-фильтры. Эти фильтры получают доступ как к оригинальному запросу, так и к ответу от целевого сервиса. Они могут модифицировать статус-код, заголовки, тело ответа.
Пример post-фильтра для добавления стандартных заголовков безопасности:
@Component
public class SecurityHeadersGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (!response.getHeaders().containsKey("X-Content-Type-Options")) {
response.getHeaders().add("X-Content-Type-Options", "nosniff");
}
if (!response.getHeaders().containsKey("X-Frame-Options")) {
response.getHeaders().add("X-Frame-Options", "DENY");
}
if (!response.getHeaders().containsKey("Content-Security-Policy")) {
response.getHeaders().add("Content-Security-Policy",
"default-src 'self'; frame-ancestors 'none'");
}
}));
}
}
Важно отметить, что этот фильтр реализует интерфейс GlobalFilter и будет применён ко всем маршрутам автоматически. Глобальные фильтры выполняются до фильтров, специфичных для маршрута, если только их порядок (через аннотацию @Order или реализацию Ordered) не указывает иное.
После выполнения всех post-фильтров финальный ответ записывается обратно в исходный канал Netty к клиенту. Netty берёт на себя эффективную отправку данных, включая chunked encoding для stream-ответов.
#Java #middle #Spring_Cloud_Gateway
👍1
Типы фильтров и их специализация
Pre-filters / Post-filters — это логическая группировка, определяемая моментом выполнения относительно вызова целевого сервиса. Технически большинство фильтров могут работать и как pre, и как post, в зависимости от их реализации. Паттерн разделения на pre/post часто реализуется через вызов chain.filter(exchange).then(...) для post-логики.
Сетевые фильтры (Netty layer) работают на более низком уровне, ближе к транспорту. NettyRoutingFilter и NettyWriteResponseFilter являются примерами таких фильтров. Они манипулируют непосредственно объектами Netty (HttpClientRequest, HttpClientResponse) и работают с реактивными потоками байтов (ByteBuf). Эти фильтры критически важны для производительности, так как обеспечивают эффективную передачу данных без лишних копирований.
Global filters применяются ко всем маршрутам автоматически. Они регистрируются как Spring Beans и могут быть упорядочены. Типичные use cases: централизованное логирование, сбор метрик, добавление стандартных заголовков, кэширование. Глобальные фильтры выполняются до фильтров маршрута, если только их порядок не указывает иное.
Per-route filters конфигурируются для конкретных маршрутов и применяются только к ним. Они декларативно задаются в конфигурации маршрута (YAML, properties) или через Java DSL.
Пример сложного per-route фильтра с кастомной конфигурацией:
Управление памятью и производительностью в JVM
Архитектура SCG на Reactor Netty имеет глубокие последствия для управления памятью в JVM.
Вместо пулов потоков с фиксированным размером стека, основное потребление памяти связано с:
Буферами Netty (ByteBuf): Netty использует пул буферов через ByteBufAllocator. Это позволяет эффективно переиспользовать буферы для чтения/записи данных, минимизируя аллокации и сборку мусора. Буферы могут быть off-heap (direct buffers), что уменьшает нагрузку на GC, но требует явного управления памятью.
Реактивные потоки и лямбда-выражения: Каждый запрос создаёт цепочку реактивных операторов (Mono, Flux), которые представляются как объекты в heap. Лямбда-выражения в фильтрах также становятся объектами. Хотя эти объекты обычно недолгоживущие (short-lived) и эффективно обрабатываются молодым поколением сборщика мусора (young generation GC), при высокой нагрузке может создаваться значительное давление на GC.
Кэши маршрутов и предикатов: CachingRouteLocator и компилированные PathPattern кэшируются, что увеличивает постоянное потребление памяти (old generation), но значительно ускоряет обработку запросов.
Для оптимизации производительности важно:
Настроить размеры пулов буферов Netty согласно ожидаемому размеру запросов/ответов
Использовать профилирование для выявления memory leaks в кастомных фильтрах (невыполненные подписки на реактивные потоки)
Настроить сборщик мусора для низких пауз (G1GC или Shenandoah для больших heap-ов)
Мониторить количество аллокаций и pressure на young generation через JMX или Micrometer
#Java #middle #Spring_Cloud_Gateway
Pre-filters / Post-filters — это логическая группировка, определяемая моментом выполнения относительно вызова целевого сервиса. Технически большинство фильтров могут работать и как pre, и как post, в зависимости от их реализации. Паттерн разделения на pre/post часто реализуется через вызов chain.filter(exchange).then(...) для post-логики.
Сетевые фильтры (Netty layer) работают на более низком уровне, ближе к транспорту. NettyRoutingFilter и NettyWriteResponseFilter являются примерами таких фильтров. Они манипулируют непосредственно объектами Netty (HttpClientRequest, HttpClientResponse) и работают с реактивными потоками байтов (ByteBuf). Эти фильтры критически важны для производительности, так как обеспечивают эффективную передачу данных без лишних копирований.
Global filters применяются ко всем маршрутам автоматически. Они регистрируются как Spring Beans и могут быть упорядочены. Типичные use cases: централизованное логирование, сбор метрик, добавление стандартных заголовков, кэширование. Глобальные фильтры выполняются до фильтров маршрута, если только их порядок не указывает иное.
Per-route filters конфигурируются для конкретных маршрутов и применяются только к ним. Они декларативно задаются в конфигурации маршрута (YAML, properties) или через Java DSL.
Пример сложного per-route фильтра с кастомной конфигурацией:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("custom_rewrite", r -> r
.path("/legacy/api/**")
.filters(f -> f
.rewritePath("/legacy/api/(?<segment>.*)", "/modern/${segment}")
.addRequestParameter("source", "gateway")
.circuitBreaker(config -> config
.setName("myCircuitBreaker")
.setFallbackUri("forward:/fallback/default"))
)
.uri("lb://backend-service"))
.build();
}
В этой конфигурации DSL демонстрируется несколько фильтров в цепочке: rewritePath изменяет путь запроса с помощью регулярного выражения, addRequestParameter добавляет параметр, а circuitBreaker оборачивает вызов в контур устойчивости с fallback.
Управление памятью и производительностью в JVM
Архитектура SCG на Reactor Netty имеет глубокие последствия для управления памятью в JVM.
Вместо пулов потоков с фиксированным размером стека, основное потребление памяти связано с:
Буферами Netty (ByteBuf): Netty использует пул буферов через ByteBufAllocator. Это позволяет эффективно переиспользовать буферы для чтения/записи данных, минимизируя аллокации и сборку мусора. Буферы могут быть off-heap (direct buffers), что уменьшает нагрузку на GC, но требует явного управления памятью.
Реактивные потоки и лямбда-выражения: Каждый запрос создаёт цепочку реактивных операторов (Mono, Flux), которые представляются как объекты в heap. Лямбда-выражения в фильтрах также становятся объектами. Хотя эти объекты обычно недолгоживущие (short-lived) и эффективно обрабатываются молодым поколением сборщика мусора (young generation GC), при высокой нагрузке может создаваться значительное давление на GC.
Кэши маршрутов и предикатов: CachingRouteLocator и компилированные PathPattern кэшируются, что увеличивает постоянное потребление памяти (old generation), но значительно ускоряет обработку запросов.
Для оптимизации производительности важно:
Настроить размеры пулов буферов Netty согласно ожидаемому размеру запросов/ответов
Использовать профилирование для выявления memory leaks в кастомных фильтрах (невыполненные подписки на реактивные потоки)
Настроить сборщик мусора для низких пауз (G1GC или Shenandoah для больших heap-ов)
Мониторить количество аллокаций и pressure на young generation через JMX или Micrometer
#Java #middle #Spring_Cloud_Gateway
👍1
Раздел 6. Коллекции в Java
Глава 2. List — списки
Практика В проекте «Библиотека» заменить массив книг на ArrayList. Реализовать методы: добавить книгу, найти по названию, удалить по индексу
Убедитесь, что ваш проект готов, и вспомните ключевые возможности ArrayList:
Динамический размер (растёт автоматически)
Доступ по индексу O(1)
Добавление в конец O(1) в среднем
Удаление O(n) (сдвиг элементов)
Поиск indexOf O(n)
Откройте проект «Библиотека»: Запустите IntelliJ IDEA и откройте проект LibraryProject.
Проверьте структуру: У вас должен быть класс Book с полями title, author, year, конструктором и методом printDetails(). Класс Library с массивом книг и методом addBook.
Замена массива на ArrayList
Замените поле массива:
Откройте файл Library.java.
Удалите объявление массива Book[] books и переменную bookCount.
Вместо этого объявите приватное поле:
private List<Book> books = new ArrayList<>();
Это будет наш основной список книг.
Инициализация:
Убедитесь, что books инициализируется в конструкторе (если не сделали при объявлении).
Можно также создать отдельный конструктор Library(), где books = new ArrayList<>(); — на ваш выбор.
Реализация метода добавления книги
Создайте или обновите метод addBook(Book book):
Метод должен быть публичным и принимать объект Book.
Используйте метод add() из ArrayList для добавления книги в конец списка.
Добавьте вывод сообщения: "Книга [title] успешно добавлена".
(Опционально) Проверьте, что книга не null перед добавлением.
Реализация метода поиска книги по названию
Создайте метод findBookByTitle(String title):
Метод должен возвращать объект Book или null, если не найден.
Переберите весь список с помощью цикла for или for-each.
Для каждой книги сравните её title с искомым (используйте equals(), учитывая регистр или используйте equalsIgnoreCase() для нечувствительности к регистру).
Если совпадение найдено — верните эту книгу.
Если цикл завершился — верните null.
Добавьте вывод: "Книга найдена: [title]" или "Книга не найдена".
Альтернативный способ (по желанию):
Используйте метод indexOf() с временным объектом-заглушкой, у которого title совпадает с искомым (переопределите equals() в Book, если нужно).
Реализация метода удаления книги по индексу
Создайте метод removeBookByIndex(int index):
Метод должен возвращать boolean: true — если удаление прошло успешно, false — если индекс неверный.
Проверьте, что индекс в допустимом диапазоне: >= 0 и < books.size().
Если индекс неверный — выведите сообщение "Неверный индекс" и верните false.
Если индекс корректный — используйте метод remove(index) из ArrayList.
Сохраните удаленную книгу (Book removedBook = books.remove(index);) и выведите сообщение "Книга удалена: [title удаленной книги]".
Верните true.
#Java #для_новичков #beginner #List #ArrayList #LinkedList #Практика
Глава 2. List — списки
Практика В проекте «Библиотека» заменить массив книг на ArrayList. Реализовать методы: добавить книгу, найти по названию, удалить по индексу
Убедитесь, что ваш проект готов, и вспомните ключевые возможности ArrayList:
Динамический размер (растёт автоматически)
Доступ по индексу O(1)
Добавление в конец O(1) в среднем
Удаление O(n) (сдвиг элементов)
Поиск indexOf O(n)
Откройте проект «Библиотека»: Запустите IntelliJ IDEA и откройте проект LibraryProject.
Проверьте структуру: У вас должен быть класс Book с полями title, author, year, конструктором и методом printDetails(). Класс Library с массивом книг и методом addBook.
Замена массива на ArrayList
Замените поле массива:
Откройте файл Library.java.
Удалите объявление массива Book[] books и переменную bookCount.
Вместо этого объявите приватное поле:
private List<Book> books = new ArrayList<>();
Это будет наш основной список книг.
Инициализация:
Убедитесь, что books инициализируется в конструкторе (если не сделали при объявлении).
Можно также создать отдельный конструктор Library(), где books = new ArrayList<>(); — на ваш выбор.
Реализация метода добавления книги
Создайте или обновите метод addBook(Book book):
Метод должен быть публичным и принимать объект Book.
Используйте метод add() из ArrayList для добавления книги в конец списка.
Добавьте вывод сообщения: "Книга [title] успешно добавлена".
(Опционально) Проверьте, что книга не null перед добавлением.
Реализация метода поиска книги по названию
Создайте метод findBookByTitle(String title):
Метод должен возвращать объект Book или null, если не найден.
Переберите весь список с помощью цикла for или for-each.
Для каждой книги сравните её title с искомым (используйте equals(), учитывая регистр или используйте equalsIgnoreCase() для нечувствительности к регистру).
Если совпадение найдено — верните эту книгу.
Если цикл завершился — верните null.
Добавьте вывод: "Книга найдена: [title]" или "Книга не найдена".
Альтернативный способ (по желанию):
Используйте метод indexOf() с временным объектом-заглушкой, у которого title совпадает с искомым (переопределите equals() в Book, если нужно).
Реализация метода удаления книги по индексу
Создайте метод removeBookByIndex(int index):
Метод должен возвращать boolean: true — если удаление прошло успешно, false — если индекс неверный.
Проверьте, что индекс в допустимом диапазоне: >= 0 и < books.size().
Если индекс неверный — выведите сообщение "Неверный индекс" и верните false.
Если индекс корректный — используйте метод remove(index) из ArrayList.
Сохраните удаленную книгу (Book removedBook = books.remove(index);) и выведите сообщение "Книга удалена: [title удаленной книги]".
Верните true.
#Java #для_новичков #beginner #List #ArrayList #LinkedList #Практика
👍3
Обновление Main для тестирования
Теперь протестируем новые методы в классе Main.
Создайте объект Library и добавьте книги:
Создайте несколько объектов Book (минимум 4-5).
Добавьте их в библиотеку через addBook().
Протестируйте поиск:
Вызовите findBookByTitle() с существующим и несуществующим названием.
Если книга найдена — вызовите printDetails() на возвращенном объекте.
Протестируйте удаление:
Выведите текущий размер списка (books.size()).
Удалите книгу по валидному индексу (например, 1).
Удалите по невалидному индексу (например, -1 или больше size).
Выведите размер после удаления.
Дополнительно: Добавьте метод printAllBooks() в Library, который перебирает весь список и вызывает printDetails() для каждой книги — используйте его для проверки состояния библиотеки.
Тестирование и отладка
Запустите проект:
Run 'Main.main()' — наблюдайте за сообщениями в консоли.
Проверьте поведение:
Добавление: Список растёт, нет ограничений по размеру.
Поиск: Находит по точному совпадению.
Удаление: Корректно удаляет и сдвигает элементы.
Отладка:
Установите breakpoint в методе removeBookByIndex и findBookByTitle.
Шагайте по коду (F8) и смотрите, как меняется размер списка и содержимое.
Полезные советы для новичков
Импорты: Не забудьте import java.util.ArrayList; и import java.util.List;
Generics: List<Book> books — всегда используйте generics.
Проверка на null: В addBook: if (book == null) return;
Регистр в поиске: equalsIgnoreCase() для нечувствительности к регистру.
Вывод размера: books.size() вместо books.length (это массив!).
Удаление: remove(index) возвращает удаленный элемент — удобно для сообщений.
Практическое задание
Задача 1: Добавьте в Library метод getBookCount(), возвращающий books.size().
Задача 2: Реализуйте метод removeBookByTitle(String title), который находит книгу по названию и удаляет её (используйте цикл или indexOf + remove).
Задача 3: Добавьте метод printAllBooks(), который выводит все книги с номерами (индекс + 1).
#Java #для_новичков #beginner #List #ArrayList #LinkedList #Практика
Теперь протестируем новые методы в классе Main.
Создайте объект Library и добавьте книги:
Создайте несколько объектов Book (минимум 4-5).
Добавьте их в библиотеку через addBook().
Протестируйте поиск:
Вызовите findBookByTitle() с существующим и несуществующим названием.
Если книга найдена — вызовите printDetails() на возвращенном объекте.
Протестируйте удаление:
Выведите текущий размер списка (books.size()).
Удалите книгу по валидному индексу (например, 1).
Удалите по невалидному индексу (например, -1 или больше size).
Выведите размер после удаления.
Дополнительно: Добавьте метод printAllBooks() в Library, который перебирает весь список и вызывает printDetails() для каждой книги — используйте его для проверки состояния библиотеки.
Тестирование и отладка
Запустите проект:
Run 'Main.main()' — наблюдайте за сообщениями в консоли.
Проверьте поведение:
Добавление: Список растёт, нет ограничений по размеру.
Поиск: Находит по точному совпадению.
Удаление: Корректно удаляет и сдвигает элементы.
Отладка:
Установите breakpoint в методе removeBookByIndex и findBookByTitle.
Шагайте по коду (F8) и смотрите, как меняется размер списка и содержимое.
Полезные советы для новичков
Импорты: Не забудьте import java.util.ArrayList; и import java.util.List;
Generics: List<Book> books — всегда используйте generics.
Проверка на null: В addBook: if (book == null) return;
Регистр в поиске: equalsIgnoreCase() для нечувствительности к регистру.
Вывод размера: books.size() вместо books.length (это массив!).
Удаление: remove(index) возвращает удаленный элемент — удобно для сообщений.
Практическое задание
Задача 1: Добавьте в Library метод getBookCount(), возвращающий books.size().
Задача 2: Реализуйте метод removeBookByTitle(String title), который находит книгу по названию и удаляет её (используйте цикл или indexOf + remove).
Задача 3: Добавьте метод printAllBooks(), который выводит все книги с номерами (индекс + 1).
#Java #для_новичков #beginner #List #ArrayList #LinkedList #Практика
👍2