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

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

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
3. Роль Protocol Buffers (protobuf)

Protocol Buffers — это бинарный формат сериализации данных, разработанный Google. Он выполняет две функции:

Описание структуры данных (через .proto файл).
Это аналог схемы JSON или XML, но строгий и типизированный.

Сериализация и десериализация (преобразование объектов в компактную бинарную форму и обратно).

Пример .proto файла не только описывает сообщения, но и определяет сервис (то есть API интерфейс).

Почему protobuf — ключевой элемент:
Он компактен: бинарный формат в несколько раз меньше JSON.
Он типобезопасен: при компиляции проверяются типы.
Он быстр: сериализация и десериализация работают на уровне байтов, без парсинга текста.


4. Как происходит сериализация и десериализация


Сериализация — это процесс превращения объекта в поток байтов для передачи по сети.
Десериализация — обратный процесс.


В gRPC:
Клиент вызывает метод stub.method(request).
request сериализуется с помощью Protocol Buffers в бинарный поток.
Поток отправляется через HTTP/2.
Сервер принимает поток, десериализует его обратно в объект CarRequest.
После обработки сервер сериализует ответ (CarResponse) и отправляет обратно.

Важно: gRPC сам управляет сериализацией. Вам не нужно ничего кодировать вручную — всё делает сгенерированный stub.


5. Что делает protoc и зачем нужны плагины

protoc — это компилятор Protocol Buffers. Он принимает .proto файл и генерирует исходный код для нужного языка.

Например:
protoc --java_out=./build/generated proto/car.proto


gRPC добавляет ещё один плагин — --grpc-java_out, который генерирует код для stub'ов.
protoc --plugin=protoc-gen-grpc-java=path/to/protoc-gen-grpc-java \
--grpc-java_out=./build/generated \
--java_out=./build/generated \
proto/car.proto


Таким образом, protoc создаёт:
Классы-сообщения (CarRequest, CarResponse)
gRPC классы (CarServiceGrpc, Stub и ImplBase)


Для каждого языка есть свой плагин:
--grpc-java_out для Java
--grpc-python_out для Python
--grpc-go_out для Go
и т. д.


Это и есть причина, почему gRPC мультиплатформенный — интерфейс описывается один раз в .proto, а код для всех языков генерируется автоматически.

6. Почему gRPC быстрее REST

gRPC построен поверх HTTP/2, а REST — чаще всего поверх HTTP/1.1. Разница принципиальна.

Ключевые причины производительности:
HTTP/2 поддерживает мультиплексирование — можно отправлять несколько запросов в одном соединении без блокировки.
Сжатие заголовков (HPACK) уменьшает накладные расходы.
Бинарная сериализация (protobuf) — меньше данных, быстрее парсинг.
Постоянное соединение — нет затрат на открытие/закрытие TCP для каждого запроса.
Streaming — можно передавать поток данных, а не ждать полного ответа (например, поток логов или большого файла).


7. Суммарно: что происходит при вызове метода в gRPC

Пошагово:
Клиент вызывает метод stub.someMethod(request).
Stub сериализует объект через protobuf.
Сериализованные данные упаковываются в HTTP/2 фрейм и отправляются на сервер.
Сервер принимает фрейм, десериализует данные.
Вызвается метод реализации (ImplBase).
Сервер формирует ответ, сериализует через protobuf.
Ответ отправляется обратно по тому же соединению.
Клиент получает и десериализует ответ.


Для разработчика — это выглядит как обычный вызов функции.
Под капотом же происходит оптимизированное сетевое взаимодействие с минимальными потерями.



#Java #middle #gRPC
👍2
Раздел 6. Коллекции в Java

Глава 5. Map — отображения (словари)

Реализации: HashMap, LinkedHashMap, TreeMap и остальные

JCF предоставляет богатый набор реализаций Map, каждая оптимизирована под конкретные нужды.

Все они реализуют Map<K, V>, но различаются по:
Хранению: Хэш-таблица (HashMap), связный список + хэш (LinkedHashMap), красно-черное дерево (TreeMap).
Порядку: Нет (HashMap), вставки (LinkedHashMap), сортировка по ключам (TreeMap).
Производительности: O(1) для хэш, O(log n) для дерево.
Дополнительно: Специальные для enum, слабых ссылок и т.д.



1. HashMap<K, V>: Основная реализация

Описание: HashMap — сердце Map в Java, основанная на хэш-таблице. Хранит пары в массиве "бакетов" (buckets), где каждый бакет — список узлов (node) с ключом, значением и хэш-кодом.

Внутренняя структура:
Массив Node<K,V>[] table (initial capacity 16, load factor 0.75).
При put: Вычисляет hash = key.hashCode() ^ (hash >>> 16) для равномерности.
Индекс бакета: hash & (table.length - 1).
Коллизия: LinkedList или Tree (с
Java 8, если > 8 узлов — дерево для O(log n)).
Ресайз: При >75% заполнения — удваивает размер, перехэширует все элементы.


Big O:
put/get/remove/containsKey: O(1) средний, O(n) worst (плохие хэши).
Итерация: O(capacity).


Особенности:
Порядок: Нет (iteration order не гарантирован).
Null: Один null-ключ, несколько null-значений.
Thread-safe: Нет (ConcurrentModificationException при параллельном доступе).
Initial capacity/load factor: new HashMap<>(16, 0.75f) для оптимизации.


Нюансы и ловушки:
hashCode/equals: Критичны! Плохой hashCode — деградация до O(n). Изменение ключа после put — потеря элемента.
Ресайз: O(n) время, но редко.
Java 8+: Tree nodes для коллизий (>8 узлов).
Remove: Если null-ключ — специальная обработка.
Итерация: entrySet(), keySet(), values() — O(capacity), не size().


Пример кода:
javaimport java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("Алексей", 35);
ages.put("Мария", 28);
ages.put("Алексей", 36); // Перезапись

System.out.println(ages.get("Алексей")); // 36
ages.put(null, 0); // Null-ключ
System.out.println(ages.size()); // 3

// Итерация
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
Когда использовать: 99% случаев — быстрый поиск/кэш (userId -> User).


2. LinkedHashMap<K, V>: HashMap с порядком

Описание: Расширение HashMap с двусвязным списком для сохранения порядка вставки (insertion-order) или доступа (access-order для LRU-кэша).

Внутренняя структура: HashMap + Entry с prev/next ссылками. Два режима: INSERTION_ORDER (default) или ACCESS_ORDER (constructor flag).

Big O: O(1) для put/get/remove, как HashMap.

Особенности:
Порядок: Вставки (по умолчанию) или доступа (get/put обновляет позицию).
Null: Да.
Thread-safe: Нет.

Нюансы:
LRU кэш: new LinkedHashMap<>(16, 0.75f, true) — access-order, removeEldestEntry для eviction.
Итерация: В порядке вставки/доступа.
Ресайз: Как HashMap, но сохраняет порядок.


Пример:
javaimport java.util.LinkedHashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("A", 3); // Обновляет, но порядок сохраняется

for (String key : map.keySet()) {
System.out.println(key); // A, B — порядок вставки
}

// Access-order
Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
lru.put("A", 1);
lru.get("A"); // "A" перемещается в конец
}
}
Когда использовать: Кэш с порядком (LRU), когда нужен predictable iteration.



#Java #для_новичков #beginner #Map #HashMap #LinkedHashMap #TreeMap #Hashtable #IdentityHashMap #EnumMap #WeakHashMap
👍2
3. TreeMap<K, V>: Отсортированная Map

Описание: Реализация SortedMap<K, V> на красно-черном дереве (red-black tree). Ключи всегда отсортированы.

Внутренняя структура: Дерево узлов с left/right/child ссылками, цветом для баланса.

Big O:
O(log n) для put/get/remove/containsKey.

Особенности:
Порядок: Сортировка по ключам (Comparable или Comparator).
Null-ключ: Нет (NPE).
Null-значение: Да.
Thread-safe: Нет.


Нюансы:
Comparator: new TreeMap<>(comparator) для custom сортировки.
Дополнительные методы: firstKey(), lastKey(), headMap(K to), tailMap(K from), subMap.
Custom ключи: Comparable<K> или Comparator.
Баланс: Автоматический, O(log n) гарантировано.


Пример:
javaimport java.util.TreeMap;
import java.util.Map;

public class Main {
public static void main(String[] args) {
Map<Integer, String> map = new TreeMap<>();
map.put(3, "Три");
map.put(1, "Один");
map.put(2, "Два");

for (Integer key : map.keySet()) {
System.out.println(key + ": " + map.get(key)); // 1: Один, 2: Два, 3: Три
}

System.out.println(map.firstKey()); // 1
}
}
Когда использовать: Отсортированные ключи (диапазонные запросы, алфавитный словарь).



Остальные реализации: Hashtable, IdentityHashMap, EnumMap, WeakHashMap

Hashtable<K, V> (устаревшая)

Описание: Первая Map в Java (1.0), synchronized версия HashMap.
Особенности: Thread-safe (synchronized методы), нет null, порядок нет.
Big O: O(1).
Нюансы: Медленнее HashMap (locks на каждый метод), legacy — используйте ConcurrentHashMap.
Когда: Только legacy код.


IdentityHashMap<K, V>

Описание: HashMap с == вместо equals/hashCode (по ссылке).
Особенности: Для объектов, где важна идентичность (graph algorithms).
Big O: O(1).
Нюансы: Load factor 0.5, double hash для коллизий.
Когда: Редко, для identity сравнения.


EnumMap<K extends Enum<K>, V>

Описание: Оптимизированная Map для enum ключей (массив вместо хэш).
Особенности: O(1), порядок enum, нет null-ключа.
Нюансы: Ключи — enum, values any.
Когда: State machines, enum configs.


WeakHashMap<K, V>

Описание: HashMap с weak keys (GC может удалить entry, если ключ недостижим).
Особенности: Для кэшей, где память критична.
Big O: O(1).
Нюансы: Values strong, cleanup не мгновенный.
Когда: Canonicalizing mappings (interning).


Полезные советы для новичков

HashMap 95% случаев: Начните с неё.
LinkedHashMap для кэша: removeEldestEntry для LRU.
TreeMap для сортировки: Comparator для reverse.
Custom ключи: IDE генерирует equals/hashCode.
Initial capacity: new HashMap<>(expectedSize) для избежания ресайза.
Ошибки: NPE в TreeMap с null-ключом; ClassCast в TreeMap без Comparable.


#Java #для_новичков #beginner #Map #HashMap #LinkedHashMap #TreeMap #Hashtable #IdentityHashMap #EnumMap #WeakHashMap
👍3
Protocol Buffers: сердце gRPC

Если gRPC — это двигатель взаимодействия сервисов, то Protocol Buffers (protobuf) — это его сердце.
Именно protobuf определяет, как описываются данные, как они сериализуются, и как из одной схемы генерируются типобезопасные классы для разных языков.


Чтобы по-настоящему понимать gRPC, нужно уверенно работать с .proto-файлами.

1. Что такое .proto файл

.proto — это файл описания структуры данных и интерфейсов (API).

Он играет сразу три роли:
Документирует контракт между клиентом и сервером (описывает, какие методы и какие данные доступны).
Генерирует код для разных языков с помощью protoc (компилятора Protocol Buffers).
Определяет схему сериализации — то, как объекты превращаются в байты и обратно.
Фактически .proto — это единый источник правды для вашего API.



2. Базовая структура .proto файла

Пример простого файла:
syntax = "proto3";

package car;

option java_multiple_files = true;
option java_package = "com.example.car";
option java_outer_classname = "CarProto";

// Определение сообщений
message Car {
string model = 1;
int32 year = 2;
CarStatus status = 3;
}

// Перечисление (enum)
enum CarStatus {
ACTIVE = 0;
INACTIVE = 1;
}

// Определение сервиса
service CarService {
rpc BuyCar (CarRequest) returns (CarResponse);
}

message CarRequest {
string model = 1;
}

message CarResponse {
string confirmation = 1;
}



3. Ключевые элементы .proto

3.1. syntax

Первая строка файла:
syntax = "proto3";


Обязательно указывает версию синтаксиса.
На практике используется только proto3, потому что она проще, строже типизирована и лучше поддерживается в gRPC.


3.2. package

Задает логическое пространство имён, чтобы избежать конфликтов:
package car;


В
Java и других языках это превращается в пакеты/модули.

3.3. option

Позволяет задавать настройки генерации кода, например:
option java_package = "com.example.car";
option java_multiple_files = true;
option java_outer_classname = "CarProto";


Без этого весь код попадёт в один файл, что неудобно для больших схем.

3.4. message — описание структуры данных

message — это аналог класса в объектно-ориентированных языках.
Каждое поле внутри него — это свойство (переменная), которое сериализуется в бинарный поток.


Пример:
message User {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}


string name = 1; — поле с типом string и номером 1.

int32 age = 2; — целочисленное поле.

repeated string hobbies = 3; — массив строк.

Важно: номер поля (= 1, = 2, = 3) — это не просто индекс. Это ключ в бинарной сериализации, который должен быть уникален и неизменен.


3.5. enum — перечисление значений

enum — это список допустимых констант.
enum CarStatus {
ACTIVE = 0;
INACTIVE = 1;
SOLD = 2;
}


Значение 0 обязательно — это значение по умолчанию.

При сериализации хранится не текстовое имя ("ACTIVE"), а его числовое значение (0), что делает protobuf компактным.


3.6. service — описание API

service определяет набор удалённых методов, которые сервер предоставляет клиенту.

Это аналог интерфейса в Java:
service CarService {
rpc BuyCar (CarRequest) returns (CarResponse);
rpc ListCars (Empty) returns (CarList);
}


Каждый rpc определяет:
имя метода (BuyCar),
входной тип (CarRequest),
выходной тип (CarResponse).


4. Типы данных в Protocol Buffers

Protobuf поддерживает ограниченный, но универсальный набор типов.

Некоторые часто используемые:

string - Текст
bool - Логическое значение
int32, int64 - Целые числа
float, double - Числа с плавающей точкой
bytes - Массив байтов
repeated - Массив
map<key, value> - Словарь

Пример:
message Garage {
map<string, Car> cars = 1;
}



#Java #middle #gRPC #proto
👍2
5. Нумерация полей — почему это критично

Каждое поле имеет свой уникальный номер — это его идентификатор в бинарном потоке.
message Car {
string model = 1;
int32 year = 2;
}


Если поменять номера, клиент и сервер перестанут понимать друг друга.
Например, если у старой версии клиента year = 2, а у новой year = 3, при сериализации они будут читать разные данные.



6. Почему важно резервировать поля

Когда вы удаляете или переименовываете поле, нельзя просто убрать строку — нужно зарезервировать номер и имя.


Пример:
message Car {
string model = 1;
reserved 2; // резервируем номер
reserved "status_old"; // резервируем имя
}


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


7. Эволюция и миграция схем (Schema Evolution)

Protobuf специально спроектирован так, чтобы позволять обновлять схемы без поломки совместимости.
Главное — соблюдать несколько правил.


Что можно делать безопасно:
Добавлять новые поля с новыми номерами.
Удалять поля (с их резервированием).
Изменять имя поля (номер должен остаться прежним).
Изменять порядок полей — не влияет на сериализацию.


Что делать нельзя:
Менять тип поля (например, int32 → string).
Менять номер поля.
Удалять поле без reserved.


Пример миграции

Старая версия:
message User {
string name = 1;
int32 age = 2;
}


Новая версия:
message User {
string name = 1;
reserved 2;
string email = 3;
}


Старый клиент, который не знает про email, просто проигнорирует это поле.
А новый клиент не столкнётся с конфликтом, потому что старый 2 зарезервирован.



8. Компиляция .proto файла и генерация кода

protoc — компилятор, который читает .proto и создаёт Java-классы.

Пример команды:
protoc \
--java_out=./build/generated \
--grpc-java_out=./build/generated \
proto/car.proto


Результат:
Для каждого message создаются классы с Builder-паттерном.
Для service создаются классы CarServiceGrpc, CarServiceImplBase, CarServiceStub.



9. Пример полного цикла

Файл car.proto:
syntax = "proto3";

service CarService {
rpc BuyCar (CarRequest) returns (CarResponse);
}

message CarRequest {
string model = 1;
int32 budget = 2;
}

message CarResponse {
string message = 1;
}


Сгенерированный код в Java (упрощённо):
// Отправитель (клиент)
CarRequest request = CarRequest.newBuilder()
.setModel("BMW")
.setBudget(20000)
.build();

CarResponse response = stub.buyCar(request);
System.out.println(response.getMessage());


Серверная реализация:
public class CarServiceImpl extends CarServiceGrpc.CarServiceImplBase {
@Override
public void buyCar(CarRequest request, StreamObserver<CarResponse> responseObserver) {
String msg = "Car purchased: " + request.getModel();
CarResponse response = CarResponse.newBuilder().setMessage(msg).build();

responseObserver.onNext(response);
responseObserver.onCompleted();
}
}


#Java #middle #gRPC #proto
👍3
Раздел 6. Коллекции в Java

Глава 5. Map — отображения (словари)

Основные методы: put - глубокое погружение в механизм добавления элементов

Метод put является фундаментальной операцией в интерфейсе Map, выполняющей добавление или обновление пар "ключ-значение". Несмотря на простоту вызова, внутри этой операции скрывается сложный механизм, варьирующийся в зависимости от конкретной реализации Map. Понимание внутренних процессов метода put позволяет разработчикам писать более эффективный код и избегать распространенных ошибок.

Общий алгоритм работы put

При вызове метода put(key, value) в любой реализации Map происходит последовательность взаимосвязанных процессов, которые можно разделить на несколько логических этапов.

Фаза предварительной обработки:
Валидация входных параметров (ключа и значения)
Вычисление хэш-кода ключа (для хэш-базированных реализаций)
Определение целевого местоположения элемента в структуре данных


Фаза поиска и разрешения коллизий:
Поиск существующего элемента с таким же ключом
Обработка коллизий (случаев, когда разные ключи претендуют на одно местоположение)
Принятие решения о добавлении нового элемента или обновлении существующего


Фаза модификации структуры:
Непосредственное добавление или обновление элемента
Балансировка и реструктуризация внутренней структуры данных
Проверка необходимости расширения емкости и выполнение resize операций



Детальный разбор для HashMap

Вычисление хэш-кода и определение индекса
В HashMap процесс начинается с вычисления хэш-кода ключа. Однако простое использование key.hashCode() недостаточно из-за потенциально плохого распределения хэш-кодов. Внутренний механизм применяет дополнительную хэш-функцию, которая "размешивает" биты хэш-кода, чтобы уменьшить количество коллизий. Этот процесс включает XOR старших и младших битов хэш-кода, что улучшает распределение даже для ключей с плохими хэш-функциями.

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

Поиск в цепочке коллизий
Когда индекс определен, система проверяет целевой бакет.

Возможны три сценария:
Бакет пуст: Самый простой случай — создается новый узел и помещается в бакет. Операция практически мгновенна.
Бакет содержит один элемент: Происходит сравнение ключей. Если ключи идентичны (по equals), значение обновляется. Если ключи разные — возникает коллизия, и новый элемент добавляется в начало связного списка.
Бакет содержит несколько элементов: Начинается последовательный обход цепочки коллизий. Каждый элемент проверяется на соответствие ключа. Если совпадение найдено — значение обновляется. Если конец цепочки достигнут без нахождения совпадения — новый элемент добавляется в конец списка.


Преобразование в дерево (Java 8+)
В современных версиях Java при достижении цепочкой определенного порога (обычно 8 элементов) происходит преобразование связного списка в красно-черное дерево. Это значительно улучшает производительность поиска в длинных цепочках — с O(n) до O(log n).

Процесс преобразования включает:
Создание дерева из элементов цепочки
Балансировку дерева согласно правилам красно-черных деревьев
Поддержание свойств дерева для обеспечения эффективности операций


#Java #для_новичков #beginner #Map #put
👍2
Механизм увеличения размера (resize)
Когда количество элементов превышает пороговое значение (емкость × коэффициент загрузки), запускается процесс resize.

Это одна из самых затратных операций в HashMap:
Создается новый массив бакетов большего размера (обычно в 2 раза)
Все существующие элементы перераспределяются по новому массиву
Для каждого элемента пересчитывается индекс на основе нового размера массива
При перераспределении цепочки коллизий могут разделяться между разными бакетами

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



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

В LinkedHashMap процесс наследует всю сложность HashMap, но добавляет дополнительный слой — поддержание порядка элементов.


При добавлении каждого нового элемента:
Выполняются все стандартные операции HashMap
Новый элемент добавляется в конец двусвязного списка, поддерживающего порядок
Устанавливаются связи между новым элементом и предыдущим хвостом списка


При обновлении существующего элемента в режиме access-order элемент перемещается в конец списка, что требует:

Разрыва связей с соседними элементами в текущей позиции
Установки новых связей для включения элемента в конец списка
Обновления ссылок головы и хвоста списка при необходимости



Специфика TreeMap

В TreeMap процесс put кардинально отличается от хэш-базированных реализаций, так как основан на бинарном дереве поиска:
Поиск позиции для вставки: Начинается с корня дерева, и алгоритм рекурсивно спускается вниз, сравнивая новый ключ с ключами существующих узлов. Сравнение происходит либо через естественный порядок ключей (если они реализуют Comparable), либо через предоставленный Comparator.

Балансировка дерева: После добавления нового узла выполняется балансировка красно-черного дерева.

Этот процесс включает:
Перекрашивание узлов для соблюдения свойств красно-черного дерева
Выполнение вращений (left-rotate, right-rotate) для восстановления баланса
Обеспечение того, что путь от корня к любому листу содержит одинаковое количество черных узлов


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


Обработка особых случаев

Работа с null ключами

Разные реализации Map по-разному обрабатывают null ключи:
HashMap: Разрешает один null ключ, который хранится в бакете с индексом 0
TreeMap: Не разрешает null ключи (выбрасывает NullPointerException), если только не предоставлен специальный компаратор, обрабатывающий null
ConcurrentHashMap: Не разрешает null ключи из-за ограничений многопоточности


Коллизии и равенство ключей

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

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

Разработчикам необходимо обеспечивать согласованность между hashCode() и equals() — если два объекта равны по equals(), их хэш-коды должны быть одинаковыми.


#Java #для_новичков #beginner #Map #put
👍2
Влияние на производительность

Факторы, влияющие на скорость операции put
Качество хэш-функции: Плохая хэш-функция, создающая много коллизий, значительно замедляет операцию, увеличивая длину цепочек.
Коэффициент загрузки: Высокий коэффициент загрузки уменьшает частоту операций resize, но увеличивает среднюю длину цепочек коллизий.
Начальная емкость: Слишком маленькая начальная емкость приводит к частым операциям resize, слишком большая — к избыточному потреблению памяти.
Размер данных: В TreeMap производительность зависит от сбалансированности дерева, в HashMap — от равномерности распределения хэшей.

Сравнительная производительность
HashMap: O(1) в среднем случае, O(log n) в худшем (с деревьями)
LinkedHashMap: O(1) с небольшими накладными расходами на поддержание порядка
TreeMap: O(log n) в любом случае благодаря сбалансированному дереву


Потокобезопасность и параллелизм

В несинхронизированных реализациях Map операция put не является атомарной, что может привести к:
Потере данных при конкурентной модификации
Повреждению внутренней структуры данных
Бесконечным циклам в цепочках коллизий


ConcurrentHashMap решает эти проблемы через:
Сегментированную блокировку (в старых версиях)
CAS (Compare-And-Swap) операции и fine-grained блокировку (в новых версиях)
Позволяет выполнять конкурентные put операции на разных сегментах



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

Оптимизация производительности


Для HashMap:
Выбирайте адекватную начальную емкость, чтобы избежать частых resize операций
Используйте ключи с хорошими хэш-функциями
Рассмотрите возможность использования immutable ключей


Для TreeMap:

Обеспечьте согласованность Comparator или естественного порядка
Используйте для данных, которые требуют сортировки или диапазонных запросов


Общие рекомендации:
Избегайте частых put операций в критичных по производительности участках кода
Используйте bulk операции при добавлении больших объемов данных
Рассмотрите альтернативные реализации для специфических use cases


#Java #для_новичков #beginner #Map #put
👍2
Типы RPC в gRPC

Одно из ключевых преимуществ gRPC — это гибкость модели обмена данными.
REST традиционно работает в стиле “один запрос — один ответ”.
gRPC, в отличие от него, поддерживает четыре типа взаимодействия, и каждый из них решает свою задачу.



1. Unary RPC (один запрос — один ответ)

Это самый простой и самый распространённый тип — аналог классического REST-вызова.
Клиент отправляет один запрос, сервер обрабатывает его и возвращает один ответ.


Клиент → (один запрос) → Сервер
Сервер → (один ответ) → Клиент


Пример .proto
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}

message UserRequest {
int32 id = 1;
}

message UserResponse {
string name = 1;
int32 age = 2;
}


Сервер (Java)
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
UserResponse response = UserResponse.newBuilder()
.setName("Alice")
.setAge(30)
.build();

responseObserver.onNext(response);
responseObserver.onCompleted();
}
}


Клиент
UserResponse response = stub.getUser(
UserRequest.newBuilder().setId(1).build()
);
System.out.println(response.getName());


Где применяется:
CRUD-операции (создание, получение, обновление, удаление).
Любые точечные вызовы, где не требуется поток данных.
По сути: это "REST, но бинарный, типобезопасный и в 10 раз быстрее".



2. Server Streaming RPC (поток ответов от сервера)

В этом типе клиент делает один запрос, а сервер возвращает несколько ответов последовательно — поток сообщений.

Клиент → (один запрос) → Сервер
Сервер → (много ответов в потоке) → Клиент


Сеанс продолжается, пока сервер не закончит отправку данных.

Пример .proto
service OrderService {
rpc ListOrders (OrdersRequest) returns (stream Order);
}

message OrdersRequest {
string user = 1;
}

message Order {
string id = 1;
string product = 2;
}


Сервер (Java)
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void listOrders(OrdersRequest request, StreamObserver<Order> responseObserver) {
for (int i = 1; i <= 3; i++) {
Order order = Order.newBuilder()
.setId("ORD-" + i)
.setProduct("Product " + i)
.build();

responseObserver.onNext(order);
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
}
responseObserver.onCompleted();
}
}


Клиент

stub.listOrders(OrdersRequest.newBuilder().setUser("Bob").build())
.forEachRemaining(order -> System.out.println(order.getProduct()));


Где применяется:
Поток обновлений или уведомлений.
Стриминг данных (например, список записей, логи, результаты аналитики).
Долгие вычисления, когда сервер постепенно отдаёт результаты.


Пример из реального мира:
Сервер передаёт клиенту “живой” поток котировок акций или данных из IoT-устройств.


#Java #middle #gRPC #proto
👍2
3. Client Streaming RPC (поток запросов от клиента)

Теперь наоборот — клиент отправляет поток запросов, а сервер отвечает одним итоговым сообщением.

Клиент → (много запросов) → Сервер
Сервер → (один ответ) → Клиент


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

Пример .proto
service UploadService {
rpc UploadPhotos (stream PhotoChunk) returns (UploadStatus);
}

message PhotoChunk {
bytes content = 1;
}

message UploadStatus {
string message = 1;
}



Сервер (
Java)
public class UploadServiceImpl extends UploadServiceGrpc.UploadServiceImplBase {
@Override
public StreamObserver<PhotoChunk> uploadPhotos(StreamObserver<UploadStatus> responseObserver) {
return new StreamObserver<PhotoChunk>() {
int totalBytes = 0;

@Override
public void onNext(PhotoChunk chunk) {
totalBytes += chunk.getContent().size();
}

@Override
public void onError(Throwable t) {
System.err.println("Upload failed: " + t.getMessage());
}

@Override
public void onCompleted() {
UploadStatus status = UploadStatus.newBuilder()
.setMessage("Uploaded " + totalBytes + " bytes")
.build();
responseObserver.onNext(status);
responseObserver.onCompleted();
}
};
}
}


Клиент
StreamObserver<PhotoChunk> requestObserver = asyncStub.uploadPhotos(
new StreamObserver<UploadStatus>() {
@Override
public void onNext(UploadStatus status) {
System.out.println(status.getMessage());
}
@Override
public void onError(Throwable t) {}
@Override
public void onCompleted() {}
}
);

// Отправляем несколько чанков
requestObserver.onNext(PhotoChunk.newBuilder().setContent(ByteString.copyFrom(new byte[1000])).build());
requestObserver.onNext(PhotoChunk.newBuilder().setContent(ByteString.copyFrom(new byte[500])).build());
requestObserver.onCompleted();


Где применяется:
Отправка файлов по частям.
Отчёты, собираемые из нескольких частей.
Потоковое логирование от клиента на сервер.



#Java #middle #gRPC #proto
👍1
4. Bidirectional Streaming RPC (двунаправленный поток)

Самый мощный и сложный тип.
Клиент и сервер одновременно отправляют данные потоками.
Они не ждут завершения друг друга — общение идёт асинхронно в обе стороны.


Клиент ⇄ (двунаправленный поток) ⇄ Сервер

Пример .proto
service ChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
string user = 1;
string text = 2;
}


Сервер (Java)
public class ChatServiceImpl extends ChatServiceGrpc.ChatServiceImplBase {
@Override
public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessage> responseObserver) {
return new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
// Эхо-сообщение обратно клиенту
ChatMessage reply = ChatMessage.newBuilder()
.setUser("Server")
.setText("Echo: " + message.getText())
.build();
responseObserver.onNext(reply);
}

@Override
public void onError(Throwable t) {
System.err.println("Error: " + t.getMessage());
}

@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
}


Клиент
StreamObserver<ChatMessage> requestObserver = asyncStub.chat(
new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage value) {
System.out.println(value.getUser() + ": " + value.getText());
}
@Override
public void onError(Throwable t) {}
@Override
public void onCompleted() {}
}
);

requestObserver.onNext(ChatMessage.newBuilder().setUser("Client").setText("Hello!").build());
requestObserver.onNext(ChatMessage.newBuilder().setUser("Client").setText("How are you?").build());
requestObserver.onCompleted();


Где применяется:
Чаты и видеоконференции.
Онлайн-игры и взаимодействие в реальном времени.
Телеметрия, двунаправленные датчики, IoT.
Главная особенность: оба канала открыты, и клиент, и сервер могут посылать данные независимо друг от друга.



5. Под капотом


Все типы RPC работают поверх HTTP/2, где каждый поток — это отдельный канал в рамках одного TCP-соединения.
gRPC использует этот механизм для организации стримов.
По сути, StreamObserver в
Java — это высокоуровневая абстракция над HTTP/2-стримом, обеспечивающая асинхронность и реактивное взаимодействие.


#Java #middle #gRPC #proto
👍2