Java for Beginner
775 subscribers
754 photos
220 videos
12 files
1.27K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

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

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Что выведет код?

import java.util.ArrayList;
import java.util.List;

public class Task281125 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

List<Integer> subList = list.subList(1, 3);
list.add(4);

System.out.println(subList.get(0));
System.out.println(subList.get(1));
}
}


#Tasks
Варианты ответа:
Anonymous Quiz
50%
2 3
10%
2 4
10%
3 4
30%
ConcurrentModificationException
Вопрос с собеседований

Что такое «слабая ссылка» в Java? 🤓


Ответ:

WeakReference
позволяет объекту быть удаленным GC, если на него нет сильных ссылок.

Используется в кэшах, чтобы не удерживать память. После очистки можно обнаружить, что объект недоступен.

Это гибкий способ управления памятью без ручного освобождения.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Развлечения Вам с утра.

Убедите нейросеть дать вам скидку!

Хоть она и матерится как сапожник я смог)) Поржал от души - пробуйте)
Я его сделал 😂😂😂
История IT-технологий сегодня — 29 ноября


ℹ️ Кто родился в этот день

Магдолна Зиманьи , урожденная Магдолна Дьёрдьи (Будапешт , 29 ноября 1934 — Будапешт, 27 марта 2016) — венгерская математик и информатик, одна из пионеров компьютерной науки в Венгрии, разработала методы и подходы в ранней информатике.


🌐 Знаковые события

1972 — Atari выпустила игру Pong, один из первых коммерчески успешных аркадных видеоигр, что стало важным шагом в развитии индустрии видеоигр. Выпуск Pong считается отправной точкой для массового распространения видеоигр и началом игровой индустрии, которая со временем выросла в крупную отрасль развлечений и технологий.


#Biography #Birth_Date #Events #29Ноября
Please open Telegram to view this post
VIEW IN TELEGRAM
С 22.11 по 28.11
Предыдущий пост(с 15.11 по 21.11)

Воскресный мотивационный пост:
Кем ты видишь себя в будущем в IT?

Запись встреч/видео:
не было

Обучающие статьи:

Java:
Коллекции в Java

Глава 2. List — списки в Java
Реализации: ArrayList и LinkedList. Сравнение производительности
Метод add
Метод get

GraphQL
Реализация GraphQL на сервере
GraphQL vs REST vs gRPC

Полезные статьи и видео:
Spring без боли: моя шпаргалка для собесов в Java
Можно ли спасти LinkedList? Пишем быстрый List на связном списке

Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍2
История IT-технологий сегодня — 30 ноября


ℹ️ Кто родился в этот день

Питер Ли (родился 30 ноября 1960 года) — американский учёный компьютерных наук, президент и глава Microsoft Research, ранее — руководитель отдела безопасности ПО в DARPA; известен трудами в области компиляторов и программных систем безопасности.

Войцех Заремба (родился 30 ноября 1988 года) — польско-американский компьютерный учёный, сооснователь OpenAI; руководил работами по робототехнике и нейросетям (в том числе Rubik’s Cube-робот) и затем над GPT/вопросами ИИ.


🌐 Знаковые события

1609 — Галилео Галилей составил первую карту лунной поверхности.

2022 — запуск ChatGPT.


#Biography #Birth_Date #Events #30Ноября
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Технический разбор: Spring WebFlux Gateway с JWT аутентификацией

Архитектурный контекст:
Это API Gateway сервис, который проксирует запросы к downstream-сервисам

Паттерн: Gateway Aggregation с аутентификацией на уровне шлюза

Детальный разбор компонентов:

Spring Security Integration
@PreAuthorize("hasRole('USER')")

Method-level security с SpEL выражением


Автоматическая проверка ролей из JWT токена
Вызывается до выполнения метода контроллера



JWT Processing

@AuthenticationPrincipal Jwt jwt

Spring Security автоматически инжектирует парсированный JWT


JWT объект предоставляет доступ к claims: jwt.getSubject(), jwt.getClaims()
Токен уже провалидирован на уровне security filter chain



Header Management Strategy
.headers(h -> {
h.add("X-User-Id", jwt.getSubject()); // User ID для сквозной идентификации
h.add(HttpHeaders.AUTHORIZATION, authHeader); // Оригинальный токен для downstream-сервисов
})


X-User-Id - кастомный header для передачи идентификатора пользователя
Проброс оригинального Authorization header для цепочки аутентификации



Reactive WebClient Configuration

return taskWebClient.get()
.uri("/api/v1/tasks/{taskId}", taskId) // URI template с подстановкой
.retrieve() // Выполнение запроса
.bodyToMono(TaskDto.class) // Десериализация ответа


Non-blocking HTTP клиент на основе Reactor Netty
Type-safe десериализация в DTO
Автоматическая обработка connection pooling и retries



Error Handling Pipeline

.onErrorMap(WebClientResponseException.class, ex -> {
if (ex.getStatusCode().is4xxClientError()) {
return new AuthClientException(ex.getResponseBodyAsString(), ex.getStatusCode().value());
} else if (ex.getStatusCode().is5xxServerError()) {
return new AuthServerException(ex.getResponseBodyAsString(), ex.getStatusCode().value());
}
return ex;
})

Дифференциация ошибок по статус-кодам


Сохранение тела ответа из downstream-сервиса
Преобразование в domain-specific исключения



Flow выполнения:
HTTP Request → Spring Security Filter Chain
JWT Validation & Role Checking
Controller Method Invocation
WebClient Request to Downstream Service
Response Processing & Error Mapping
HTTP Response to Client



Преимущества данного подхода:

Централизованная аутентификация в gateway
Сквозная передача контекста пользователя
Реактивная non-blocking архитектура
Единая точка обработки ошибок
Type-safe коммуникация между сервисами



Требуемые конфигурации:
Spring Security с JWT decoder
Настроенный WebClient с connection pooling
Global exception handler для кастомных исключений
Мониторинг и логирование межсервисных вызовов


Данная реализация демонстрирует production-ready подход к построению API Gateway в микросервисной архитектуре с полным циклом аутентификации и обработки ошибок.
👍1🤯1
По прежнему набираю несколько человек на обучение Java.


Пишите, будет интересно🫡
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21
История IT-технологий сегодня — 01 декабря


ℹ️ Кто родился в этот день

Корнелиус Шумахер (нем. Cornelius Schumacher; род. 1 декабря 1969, Тюбинген, Германия) — немецкий разработчик свободного ПО, активист свободного софта, внёс вклад в KDE — популярную среду рабочего стола для GNU/Linux.


🌐 Знаковые события

1677— состоялся запуск первой в мире коммерческой системы интерактивного кабельного телевидения в Колумбусе (Огайо, США). Система предлагала порядка 30 каналов, из которых часть были обычными вещательными, часть — платными (pay-per-view), а часть — интерактивными, с возможностью «обратной связи» пользователя через пульт.


#Biography #Birth_Date #Events #01Декабря
Please open Telegram to view this post
VIEW IN TELEGRAM
Advanced GraphQL: реактивность и Federation

GraphQL уже давно не ограничивается статическими запросами к одной базе.

Современные системы требуют:
реактивности: live updates, push-события на фронтенд;
масштабируемости: объединение схем из разных сервисов;
микросервисной интеграции: разные источники данных и форматы;
единый клиентский интерфейс: фронт видит единую схему, хотя данные приходят из нескольких микросервисов.


Эти задачи решаются через Subscriptions, Federation, Schema Stitching, GraphQL Gateway.


1. Subscriptions и live updates

1.1 Что такое Subscription

Subscription — это тип операции GraphQL, который подписывается на события и получает данные по мере их появления, в отличие от Query/Mutation, где данные запрашиваются один раз.

Используется для:
чатов и уведомлений;
реального мониторинга (метрики, логи);
обновления UI при изменении данных на сервере.


1.2 Механика на сервере

Клиент подписывается на событие через WebSocket или Server-Sent Events (SSE).
Сервер регистрирует подписку и хранит её в памяти или через pub/sub (Redis, Kafka).
При событии вызываются соответствующие резолверы Subscription, результат отправляется клиенту.


1.3 Пример на Spring Boot с graphql-java

Схема (schema.graphqls)
type Subscription {
postAdded: Post!
}


Резолвер Subscription
@Component
public class PostSubscription implements GraphQLSubscriptionResolver {

private final Publisher<Post> postPublisher;

public PostSubscription(Publisher<Post> postPublisher) {
this.postPublisher = postPublisher;
}

public Publisher<Post> postAdded() {
return postPublisher;
}
}


Публикация события (например, после мутации)
@Component
public class PostMutation implements GraphQLMutationResolver {

private final Publisher<Post> postPublisher;
private final PostService postService;

public PostMutation(PostService postService, Publisher<Post> postPublisher) {
this.postService = postService;
this.postPublisher = postPublisher;
}

public Post createPost(CreatePostInput input) {
Post newPost = postService.create(input);
postPublisher.publish(newPost); // пушим в подписчиков
return newPost;
}
}

Таким образом фронтенд автоматически получает новые посты без повторных запросов.


1.4 Реактивная интеграция с gRPC

Микросервис может уведомлять GraphQL через gRPC стриминг (Server Streaming).
GraphQL Gateway принимает события и пушит их клиентам через Subscription.
Реализуется через Publisher или Flux (Project Reactor) в Java.


Пример с Project Reactor:
public Publisher<Post> postAdded() {
return Flux.from(postGrpcStub.subscribePosts());
}



#Java #middle #GraphQL
👍1
2. Federation / Schema stitching

2.1 Зачем нужна Federation

В микросервисной архитектуре каждая команда может иметь свой GraphQL-сервис.
Фронтенду нужен единый endpoint, а не десятки отдельных.

Schema stitching: объединяет схемы в один endpoint вручную.

Apollo Federation: более продвинутый стандарт, позволяющий каждому сервису быть федеративным узлом.

2.2 Принцип работы Federation

Subgraph Service — каждый сервис предоставляет свою часть схемы: User, Post, Comment.
Gateway / Apollo Gateway — объединяет схемы subgraph и решает, какой сервис вызывать для каждого запроса.
Reference resolver — позволяет связать типы из разных сервисов (например, User в Post).

Пример на Java (с Spring Boot + GraphQL Federation, библиотека graphql-java-federation):

Сервис Users
type User @key(fields: "id") {
id: ID!
name: String!
}


Сервис Posts
type Post {
id: ID!
title: String!
author: User @provides(fields: "name")
}



Java resolver для
Post.author
@Component
public class PostResolver implements GraphQLResolver<Post> {

private final UserGrpc.UserBlockingStub userStub;

public PostResolver(UserGrpc.UserBlockingStub userStub) {
this.userStub = userStub;
}

public User author(Post post) {
UserRequest req = UserRequest.newBuilder().setId(post.getAuthorId()).build();
UserResponse resp = userStub.getUser(req);
return mapToGraphQLUser(resp);
}
}

Gateway собирает всю федеративную схему и возвращает фронтенду единый API.



3. GraphQL Gateway и объединение данных

3.1 Роль Gateway

Аггрегирует данные из нескольких микросервисов (REST, gRPC, базы, Kafka).
Решает проблемы N+1 через batching (DataLoader).
Управляет кешированием и throttling.
Поддерживает Subscriptions и Federation.


3.2 Пример архитектуры
[Frontend SPA / Mobile] --GraphQL--> [GraphQL Gateway] --gRPC--> [UserService]
|--> [PostService]
|--> [CommentService]
|--> [External REST API]


Особенности:

Gateway использует DataLoader для агрегации запросов, уменьшения количества вызовов к сервисам.
Subscriptions могут получать события из gRPC стримов или Kafka и пушить клиенту.



4. Примеры использования и кейсы

4.1 Live feed

Мобильное приложение подписывается на postAdded.
PostService пушит новые посты через gRPC или внутренний EventBus.
Gateway трансформирует данные в GraphQL Subscription → клиент получает обновления моментально.


4.2 Микросервисная интеграция


UserService и PostService разрабатываются разными командами.
Gateway объединяет их схемы через Federation.
Фронтенд видит единый API: user(id: 1) { name posts { title } }, не зная, что posts приходит из другого сервиса.


4.3 Agreggation + caching

Gateway кеширует данные User на 5 минут.
PostService вызывается только для новых постов.
DataLoader агрегирует все запросы к UserService за одну операцию.



5. Лучшие практики

Использовать Federation для масштабируемых командных проектов.
Subscription через WebSocket + Publisher/Flux для реактивных интерфейсов.
DataLoader для оптимизации N+1 вызовов в распределённых сервисах.
Разделять ответственность: микросервисы предоставляют свои типы и резолверы, Gateway агрегирует.
Event-driven подход для live updates: gRPC streaming, Kafka, Redis Pub/Sub.
Мониторинг: трассировка на уровне каждого subgraph, latency, throughput.
Эволюция схем: добавление новых полей без ломки клиентов, депрекация старых.



#Java #middle #GraphQL
👍1
Что выведет код?

import java.util.Optional;

public class Task011225 {
public static void main(String[] args) {
Optional<String> emptyOpt = Optional.empty();
Optional<String> valueOpt = Optional.of("hello");
Optional<String> nullOpt = Optional.ofNullable(null);

System.out.println(valueOpt.get());
System.out.println(nullOpt.isPresent());
System.out.println(emptyOpt.isPresent());
System.out.println(emptyOpt.get());
}
}


#Tasks
Вопрос с собеседований

Для чего нужен метод join() в потоках? 🤓


Ответ:

join()
заставляет текущий поток ждать завершения другого потока.

Это полезно для синхронизации выполнения, когда результат параллельной работы нужен дальше.

Правильное использование предотвращает гонки данных и обеспечивает согласованность программы.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
История IT-технологий сегодня — 02 декабря


ℹ️ Кто родился в этот день

Бенджамин Мако Хилл (родился 2 декабря 1980) - свободный программист, активист free software-сообщества, участвовал в проектах Debian/Ubuntu, соавтор технических книг по свободному ПО.

🌐 Знаковые события

1971 — советская автоматическая межпланетная станция «Марс-3» впервые в мире совершила мягкую посадку на поверхность Марса.

2010 — на iPhone запущен мессенджер Viber.


#Biography #Birth_Date #Events #02Декабря
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Глава 2. List — списки

Метод set


Операция замены элемента в списке фундаментально отличается от операций добавления и удаления, поскольку не изменяет размер коллекции, а лишь модифицирует ее содержимое. Эта операция раскрывает компромисс между скоростью доступа к элементам и стоимостью их модификации, который по-разному разрешается в ArrayList и LinkedList. В то время как одна реализация обеспечивает практически мгновенную замену любого элемента, другая требует значительных затрат на предварительный поиск, демонстрируя тем самым trade-off между разными аспектами производительности.


ArrayList: непосредственная замена в массиве

Архитектурные предпосылки эффективной замены


ArrayList, основанный на динамическом массиве, предоставляет идеальные условия для операции замены элементов. Его внутренняя структура — непрерывный блок памяти в виде массива Object[] — позволяет осуществлять прямой доступ к любой позиции за постоянное время. Эта архитектурная особенность делает операцию set одной из наиболее эффективных операций в ArrayList.

Детальный процесс выполнения set(index, element)

Фаза валидации и проверки
Перед выполнением собственно замены элемента система осуществляет серию проверок, обеспечивающих корректность операции:

Валидация индекса:
Происходит тщательная проверка того, что указанный индекс находится в допустимом диапазоне от 0 (включительно) до текущего размера списка (исключительно). Эта проверка включает сравнение запрошенного индекса со значением поля size и при необходимости выброс исключения IndexOutOfBoundsException с детализированным сообщением.

Проверка ссылочной целостности:
Неявно обеспечивается, что внутренний массив elementData инициализирован и находится в консистентном состоянии, готовом к операции модификации.

Фаза извлечения и замены элемента
После успешной валидации начинается непосредственно процесс замены:

Прямой доступ к массиву:
Благодаря массиву как базовой структуре данных, позиция целевого элемента вычисляется как прямое смещение — для индекса i элемент находится в elementData[i].

Извлечение предыдущего значения:
Перед заменой система сохраняет ссылку на текущий элемент в указанной позиции. Это значение будет возвращено как результат операции, обеспечивая возможность отката или анализа изменений.

Непосредственная замена:
Новый элемент помещается в ту же позицию массива. Эта операция представляет собой простое присваивание ссылки в ячейке массива.

Обновление метаданных:
Несмотря на то, что размер списка не изменяется, операция set инкрементирует счетчик модификаций (modCount). Это критически важно для поддержания корректности fail-fast итераторов, которые должны обнаруживать любые структурные изменения коллекции.


Отсутствие структурных изменений

Ключевой характеристикой операции set в ArrayList является то, что она не вызывает реорганизации внутренней структуры данных. В отличие от операций add и remove, которые могут требовать расширения массива или сдвига элементов, set затрагивает только одну ячейку памяти, что делает ее исключительно легковесной.


#Java #для_новичков #beginner #List #ArrayList #LinkedList #set
👍1
Производительность и оптимизации

Временная сложность
Операция set в ArrayList имеет временную сложность O(1) в худшем случае. Время выполнения практически идентично для замены элемента в любой позиции списка и не зависит от общего количества элементов.

Влияние на memory model

Локальность ссылок:
Поскольку операция затрагивает только одну ячейку массива, она оказывает минимальное влияние на кэширование процессора и может даже улучшить локальность, если новый элемент часто используется впоследствии.

Отсутствие аллокаций:
Операция не создает новых объектов и не требует выделения памяти, что делает ее friendly по отношению к garbage collector.

Барьеры памяти в многопоточных сценариях

При работе в многопоточной среде операция set требует proper synchronization для обеспечения visibility изменений. Присваивание ссылки в массиве само по себе является atomic операцией, но без дополнительных барьеров памяти нет гарантии, что изменение будет видно другим потокам.


LinkedList: поиск с последующей заменой

Архитектурные особенности замены в связном списке


LinkedList, реализованный как двусвязный список, подходит к операции замены элементов принципиально иным образом. Его децентрализованная структура, состоящая из отдельных узлов, распределенных в куче, требует предварительного поиска целевого узла перед выполнением собственно замены.

Структура узла и организация данных
Каждый узел LinkedList содержит три ключевых компонента, которые участвуют в операции замены:
Node<E> {
E item; // хранимый элемент (подлежит замене)
Node<E> next; // ссылка на следующий узел
Node<E> prev; // ссылка на предыдущий узел
}


Важно отметить, что операция set затрагивает только поле item узла, оставляя ссылки next и prev неизменными.

Детальный процесс выполнения set(index, element)
Фаза валидации и стратегического планирования


Как и в ArrayList, операция начинается с проверки корректности входных данных:


Проверка границ индекса:

Убеждаются, что индекс находится в допустимом диапазоне [0, size-1].

В зависимости от положения целевого индекса выбирается наиболее эффективная точка начала обхода:

Для индексов в первой половине списка (index < size / 2) обход начинается с головы (head)
Для индексов во второй половине обход начинается с хвоста (tail)
Эта оптимизация уменьшает среднее количество шагов поиска примерно вдвое.


Фаза поиска целевого узла
После определения начальной точки начинается процесс последовательного обхода:

Инициализация указателя обхода:
Создается временная переменная, которая устанавливается на начальный узел (head или tail).

Последовательное перемещение по цепочке:
Для каждого шага обхода:
При движении от головы указатель перемещается к node.next
При движении от хвоста указатель перемещается к node.prev
Счетчик текущей позиции инкрементируется или декрементируется соответственно


Достижение целевой позиции:
Процесс продолжается до тех пор, пока текущая позиция не совпадет с запрошенным индексом.

Фаза непосредственной замены
Когда целевой узел найден:

Сохранение предыдущего значения:
Из поля item целевого узла извлекается и сохраняется текущий элемент для последующего возврата.

Замена элемента:
В поле item целевого узла записывается ссылка на новый объект.

Обновление метаданных:
Как и в ArrayList, инкрементируется счетчик модификаций (modCount) для поддержания корректности итераторов.


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

Временная сложность
Операция set в LinkedList имеет временную сложность O(n) в худшем случае, где n — количество элементов в списке. Однако благодаря оптимизации двунаправленного поиска средняя сложность составляет O(n/4) = O(n).

Распределение стоимости операции
Время поиска: Составляет подавляющую часть общей стоимости операции — O(n)
Время замены: Пренебрежимо мало — O(1)


Зависимость от паттерна доступа
Худший случай: Замена элемента в середине большого списка
Лучший случай: Замена первого или последнего элемента
Средний случай: Замена элемента на расстоянии ~n/4 от ближайшего конца


#Java #для_новичков #beginner #List #ArrayList #LinkedList #set
👍1
Сравнительный анализ ArrayList и LinkedList

Количественные характеристики производительности

Время выполнения:

ArrayList: 5-15 наносекунд (постоянно)
LinkedList: 10-50 наносекунд × количество пройденных узлов


Потребление памяти во время операции:
ArrayList: Не требует дополнительной памяти
LinkedList: Не требует дополнительной памяти (кроме временных переменных обхода)



Качественные различия

Локальность памяти:
ArrayList: Отличная — операция затрагивает одну ячейку в непрерывном блоке
LinkedList: Плохая — узел может находиться в произвольном месте кучи


Влияние на garbage collector:
ArrayList: Минимальное — заменяемая ссылка становится кандидатом на сборку
LinkedList: Аналогично ArrayList


Сценарии преимущественного использования

ArrayList превосходит когда:
Частые замены элементов в произвольных позициях
Критически важна предсказуемость времени выполнения



Работа с большими списками

LinkedList может быть предпочтителен когда:

Замены преимущественно происходят near концов списка
Преобладают другие операции, где LinkedList имеет преимущество

Размер списка невелик


Специализированные реализации List

CopyOnWriteArrayList

Механизм замены:
Использует стратегию "копирование при записи", что кардинально меняет семантику операции:
Создается полная копия внутреннего массива
В копии заменяется элемент в указанной позиции
Ссылка на внутренний массив атомарно заменяется на новую копию
Старый массив остается доступным для текущих читателей


Производительность:
Время выполнения: O(n) из-за необходимости копирования всего массива
Потребление памяти: Удвоенное во время операции
Thread-safe: Да, без блокировок для читателей



Vector

Устаревший synchronized подход:

Все операции, включая set, синхронизированы
Излишний overhead в single-threaded сценариях
Постоянное время доступа аналогично ArrayList



Многопоточные аспекты операции set

Проблемы конкурентного доступа

Несинхронизированные реализации:
ArrayList и LinkedList не обеспечивают thread-safe выполнение операции set:
Возможность lost updates при concurrent модификациях
Риск повреждения структур данных
Отсутствие гарантий visibility изменений

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

Стратегии обеспечения потокобезопасности
Явная синхронизация:
synchronized(list) {
list.set(index, newValue);
}


Thread-safe обертки:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.set(index, newValue); // Внутренняя синхронизация


Concurrent коллекции:
CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();
copyOnWriteList.set(index, newValue); // Atomic замена с копированием



Memory consistency guarantees

Для обеспечения видимости изменений между потоками необходимо установление happens-before отношений через:
Synchronized блоки
Volatile переменные
Atomic классы
Lock механизмы


Влияние на итераторы и представления

Fail-fast семантика

Операция set инкрементирует счетчик modCount, что приводит к выбросу ConcurrentModificationException при обнаружении изменения во время итерации:
Итераторы сохраняют ожидаемое значение modCount
При каждой операции итератор проверяет соответствие текущего modCount
Несоответствие приводит к немедленному исключению


Особенности ListIterator
ListIterator предоставляет собственный метод set, который имеет важные отличия:
Не инкрементирует modCount родительского списка
Может быть вызван многократно для замены текущего элемента
Более эффективен для LinkedList, так использует текущую позицию итератора



#Java #для_новичков #beginner #List #ArrayList #LinkedList #set
👍1