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

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

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Специфика TreeMap

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

Алгоритм поиска в красно-черном дереве
Поиск начинается с корневого узла и рекурсивно спускается вниз по дереву, следуя принципам бинарного поиска:
Если искомый ключ меньше ключа текущего узла — поиск продолжается в левом поддереве
Если искомый ключ больше ключа текущего узла — поиск продолжается в правом поддереве
При равенстве ключей — немедленное возвращение true

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


Механизмы сравнения ключей

TreeMap использует один из двух возможных механизмов сравнения, что значительно влияет на семантику проверки существования:
Естественный порядок: Если ключи реализуют интерфейс Comparable, используется их естественный порядок сортировки. Это требует, чтобы все ключи были взаимно сравнимы и следовали контракту Comparable.
Внешний компаратор: Если TreeMap создан с предоставленным Comparator, используется он. Это позволяет использовать ключи, не реализующие Comparable, или устанавливать альтернативный порядок сортировки.

Процесс сравнения может быть вычислительно сложным, особенно для составных ключей или кастомных компараторов с нетривиальной логикой.


Гарантии производительности благодаря балансировке
Красно-черное дерево, используемое в TreeMap, является самобалансирующейся структурой данных, что гарантирует:
Высота дерева всегда пропорциональна логарифму количества элементов
Время поиска остается O(log n) даже в худшем случае
Автоматическую адаптацию к любым последовательностям вставки и удаления

Балансировка достигается через сложные алгоритмы перекрашивания и вращения, которые выполняются автоматически при модификации дерева.



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

ConcurrentHashMap

В ConcurrentHashMap механизм проверки существования оптимизирован для многопоточного доступа и обеспечивает уникальные гарантии:
Неблокирующее чтение: Операция containsKey в большинстве случаев не требует блокировок, что позволяет множеству потоков одновременно выполнять проверки существования.
Слабая согласованность (Weak Consistency): В условиях конкурентного доступа метод может не отражать самые последние изменения, но гарантирует eventual consistency.
Сегментированный доступ: В старых версиях поиск ограничивается одним сегментом, в новых — используются более тонкие механизмы изоляции.
Гарантии видимости: Обеспечиваются proper happens-before отношения для операций, выполненных в одном потоке.



EnumMap
EnumMap предоставляет наиболее эффективный механизм проверки существования:
Проверка превращается в простую операцию проверки наличия элемента в массиве по индексу
Индекс вычисляется на основе ordinal значения enum-константы
Сложность O(1) с минимальными накладными расходами
Гарантированная производительность независимо от размера коллекции


IdentityHashMap
Особенность проверки существования в IdentityHashMap — использование ссылочного равенства вместо семантического:
Сравнение ключей происходит через оператор ==, а не через equals()
Хэш-код вычисляется на основе System.identityHashCode()
Полезно для сценариев, где важно различать объекты по идентичности, а не по состоянию
Особенно эффективно для часто используемых или кэшированных объектов



WeakHashMap
В WeakHashMap семантика проверки существования тесно связана с системой сборки мусора:
Ключи могут быть автоматически удалены сборщиком мусора, если на них нет сильных ссылок
Результат containsKey может измениться без явного вызова методов удаления
Полезно для кэшей и временных ассоциаций, где автоматическое очищение является желательным поведением



#Java #для_новичков #beginner #Map #containsKey
👍1
Обработка особых случаев

Работа с null ключами
Разные реализации Map демонстрируют различное поведение при проверке null ключей:
HashMap: Специально обрабатывает null ключ, храня его в бакете с индексом 0. Проверка containsKey(null) возвращает true, если null ключ был ранее добавлен.
TreeMap: Не поддерживает null ключи — вызов containsKey(null) всегда выбрасывает NullPointerException.
ConcurrentHashMap: Не поддерживает null ключи — вызов containsKey(null) всегда возвращает false.
LinkedHashMap: Наследует поведение HashMap относительно null ключей.

Семантика равенства и сравнения
Процесс определения равенства ключей является критически важным для корректности операции containsKey.

Разные реализации используют различные стратегии:
HashMap и LinkedHashMap используют комбинацию проверок:
Сравнение хэш-кодов для быстрой предварительной проверки
Проверка ссылочного равенства (==) для оптимизации частого случая
Вызов equals() для точного определения семантического равенства

TreeMap: Использует либо естественный порядок (Comparable), либо предоставленный Comparator. Метод equals() ключей не используется напрямую для поиска.
IdentityHashMap: Использует исключительно ссылочное равенство (==).
EnumMap: Использует равенство enum-констант, которое по сути является ссылочным равенством.



Практические рекомендации

Эффективное использование containsKey
Паттерн проверки перед действием: Часто используется для предотвращения дублирования или выполнения условной логики:

if (!map.containsKey(key)) {
// Выполнить дорогостоящую операцию только если ключ отсутствует
Value value = computeExpensiveValue(key);
map.put(key, value);
}


Оптимизация частых проверок: Для часто проверяемых ключей кэширование результатов.

Избегание избыточных проверок: В некоторых случаях более эффективно использовать get с проверкой на null:
// Вместо:
if (map.containsKey(key)) {
Value value = map.get(key);
// обработка value
}

// Можно использовать:
Value value = map.get(key);
if (value != null) {
// обработка value
}


Выбор реализации для различных сценариев

Для частых операций проверки существования:

HashMap с хорошими хэш-функциями — оптимальный выбор
EnumMap — для enum ключей (максимальная производительность)
IdentityHashMap — когда важна ссылочная семантика


Для отсортированных данных с проверкой существования:

TreeMap — когда нужна сортировка или проверка в диапазонах

Для многопоточных сценариев:
ConcurrentHashMap — для высококонкурентного доступа
Collections.synchronizedMap() — для низкой конкуренции


Оптимизация ключей
Неизменяемость: Использование immutable ключей гарантирует консистентность хэш-кодов и предотвращает subtle ошибки.

Эффективные equals() и hashCode():
Минимизация вычислительной сложности этих методов
Кэширование хэш-кода для сложных объектов
Использование быстрых алгоритмов сравнения
Обеспечение консистентности между equals() и hashCode()

Правильный размер коллекции: Предварительное задание адекватной емкости для HashMap уменьшает необходимость операций resize и улучшает распределение.


#Java #для_новичков #beginner #Map #containsKey
👍1
Архитектура и принципы работы GraphQL
Что происходит между клиентом, схемой и источниками данных


1. Главная идея архитектуры

GraphQL-сервер — это прослойка, которая принимает декларативные запросы, проверяет их на соответствие схеме, исполняет нужные резолверы и возвращает результат.

Он не хранит данные сам по себе.
GraphQL — это не база данных, а универсальный интерфейс к любому источнику данных: SQL, NoSQL, микросервисы, REST, файлы, внешние API.
Его задача — связать клиентскую структуру запроса со структурой данных в бэкенде.


Схематично:
Клиент → GraphQL сервер → Источники данных (DB, REST, gRPC, API)


2. Schema — сердце GraphQL

Schema (схема) — это основной контракт между клиентом и сервером.

Она описывает:
какие данные доступны;
какие поля у этих данных есть;
какие операции можно выполнять.


Схема написана на SDL (Schema Definition Language), декларативном языке, напоминающем описание классов.

Пример:
type User {
id: ID!
name: String!
posts: [Post!]!
}

type Post {
id: ID!
title: String!
content: String
}

type Query {
user(id: ID!): User
allUsers: [User!]!
}


Ключевые понятия:
type — определяет структуру данных (аналог Java-класса или DTO).
! — обязательное поле (non-null).
[User!]! — список пользователей, где ни один элемент не равен null.
Query — специальный тип, описывающий операции чтения.


Схема — это не код, а декларация.
На её основе GraphQL сервер автоматически понимает, как валидировать запросы, какие поля допустимы и какие возвращать ошибки.



3. Query, Mutation, Subscription — три типа операций

GraphQL разделяет все действия на три категории.

1. Query — запрос данных (аналог GET в REST)
Используется для чтения.

Клиент описывает, какие сущности и поля нужны:
query {
user(id: 42) {
name
posts {
title
}
}
}


Сервер вернёт данные в том же формате:
{
"data": {
"user": {
"name": "Den",
"posts": [
{ "title": "gRPC и Java" },
{ "title": "GraphQL под капотом" }
]
}
}
}


2. Mutation — изменение данных (аналог POST/PUT/DELETE)
Mutation обозначает действия, которые модифицируют состояние системы.
Они могут создавать, обновлять или удалять записи.

mutation {
createUser(input: { name: "Alex" }) {
id
name
}
}


GraphQL возвращает результат — обновлённые данные или подтверждение операции.

3. Subscription — подписка на события (реактивный поток)
Subscription создаёт постоянное соединение между клиентом и сервером (обычно через WebSocket).
Когда на сервере происходят изменения, клиент получает уведомления в реальном времени.

subscription {
userCreated {
id
name
}
}


Если на сервере создан новый пользователь, событие userCreated автоматически отправляется всем подписанным клиентам.


4. Как работает запрос GraphQL: путь от клиента до данных

Чтобы понять механику, посмотрим на полный цикл обработки запроса.

Шаг 1. Клиент отправляет запрос

Клиент (например, браузер) отправляет HTTP POST на /graphql с JSON-телом:
{
"query": "query { user(id: 42) { name posts { title } } }"
}

GraphQL-запрос — это декларативное описание структуры данных, не код и не SQL.


Шаг 2. Сервер парсит и валидирует запрос

GraphQL-сервер:
Парсит текст запроса.
Проверяет, что все поля и типы существуют в схеме.
Проверяет типы аргументов (id действительно ID!).
Отклоняет запрос, если нарушен контракт схемы.


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



#Java #middle #GraphQL
Шаг 3. Сервер вызывает резолверы

Резолвер (resolver) — это функция, которая знает, как получить данные для конкретного поля.


Например, в Java через библиотеку graphql-java:
GraphQLObjectType userType = newObject()
.name("User")
.field(field -> field
.name("id")
.type(Scalars.GraphQLID))
.field(field -> field
.name("name")
.type(Scalars.GraphQLString))
.field(field -> field
.name("posts")
.type(new GraphQLList(postType))
.dataFetcher(env -> postService.getByUser(env.getSource())))
.build();



Резолверы вызываются рекурсивно:

Сначала выполняется user(id: 42).
Затем для каждого пользователя — posts.
Затем для каждого поста — title.

GraphQL умеет оптимизировать выполнение: например, группировать одинаковые вызовы (DataLoader паттерн).


Шаг 4. Формирование ответа

После выполнения всех резолверов GraphQL собирает результат в структуру, повторяющую форму запроса, и отправляет JSON-ответ клиенту:
{
"data": {
"user": {
"name": "Den",
"posts": [{ "title": "gRPC и Java" }]
}
}
}


Если на каком-то шаге произошла ошибка, она не прерывает всё выполнение.

GraphQL вернёт частичные данные + список ошибок:
{
"data": { "user": null },
"errors": [{ "message": "User not found" }]
}



5. Почему GraphQL — не база данных

Эта путаница встречается часто.
GraphQL не хранит и не управляет данными.
Он не заменяет SQL или ORM.


GraphQL — это API-уровень, который:
принимает запросы,
интерпретирует их,
вызывает нужные источники данных,
собирает и возвращает результат в едином формате.


Под капотом это может быть:
JDBC-запросы в PostgreSQL,
вызовы REST API других микросервисов,
gRPC-вызовы,
кэш Redis,
файловая система или внешние API.

GraphQL — унифицированный интерфейс доступа к любым данным, независимо от их источника.



6. Интеграция GraphQL в бэкенд

GraphQL не заменяет ваш backend — он становится поверх него.

В Java это обычно выглядит так:
Spring Boot + graphql-spring-boot-starter — стандартный способ поднять GraphQL-сервер.
Схема (.graphqls) описывается декларативно.
Резолверы реализуются как обычные Spring-бины.


Пример:
@Component
public class UserResolver implements GraphQLQueryResolver {
private final UserService userService;
public UserResolver(UserService userService) { this.userService = userService; }

public User user(Long id) {
return userService.findById(id);
}

public List<User> allUsers() {
return userService.findAll();
}
}


GraphQL сам вызывает нужный метод в зависимости от запроса клиента.

Таким образом, GraphQL интегрируется поверх существующего слоя сервисов и репозиториев, не требуя переписывания бизнес-логики.


#Java #middle #GraphQL