Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 3. Сериализация и форматы обмена
YAML — формат для конфигураций
YAML (YAML Ain't Markup Language) — это текстовый формат сериализации данных, ориентированный на максимальную читаемость для человека. Первая спецификация YAML 1.0 была выпущена в 2001 году, текущая актуальная версия — YAML 1.2.2 (октябрь 2021). В отличие от JSON, YAML использует отступы для обозначения структуры вместо фигурных скобок и квадратных скобок, что делает документы визуально чище и ближе к естественному языку.
YAML не является языком разметки в строгом смысле: он не содержит тегов разметки текста, как HTML или XML. Вместо этого YAML — это формат представления графа данных (data graph), где узлами являются скаляры, последовательности и отображения (маппинги).
Где используется YAML
YAML стал де-факто стандартом для конфигурационных файлов в современной инфраструктуре и разработке:
Spring Boot: файл
Docker Compose: файл
Kubernetes: манифесты ресурсов (pods, deployments, services) пишутся в YAML.
Ansible: playbooks и inventories — YAML-файлы, описывающие сценарии автоматизации конфигурации серверов.
GitHub Actions: workflow-файлы
Helm: чарты Kubernetes используют YAML для шаблонов и values-файлов.
Во всех этих случаях ключевое преимущество YAML — читаемость для человека, которая критична, когда конфигурацию пишут и ревьюят разработчики и DevOps-инженеры.
Библиотека SnakeYAML
В Java-экосистеме стандартной библиотекой для работы с YAML является SnakeYAML. Она разработана сообществом и широко используется как самостоятельно, так и как транзитивная зависимость Spring Boot. На момент 2026 года актуальные версии:
SnakeYAML 2.x — классическая реализация, поддерживающая YAML 1.1 и частично YAML 1.2. Используется в Spring Boot 3.x.
SnakeYAML Engine — отдельный проект, полностью реализующий YAML 1.2. Более строгий парсер, но менее распространён в прикладной разработке.
SnakeYAML 2.x (выпущенная в 2023 году) внесла критическое изменение безопасности: по умолчанию отключена возможность десериализации произвольных Java-объектов через теги
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
Глава 3. Сериализация и форматы обмена
YAML — формат для конфигураций
YAML (YAML Ain't Markup Language) — это текстовый формат сериализации данных, ориентированный на максимальную читаемость для человека. Первая спецификация YAML 1.0 была выпущена в 2001 году, текущая актуальная версия — YAML 1.2.2 (октябрь 2021). В отличие от JSON, YAML использует отступы для обозначения структуры вместо фигурных скобок и квадратных скобок, что делает документы визуально чище и ближе к естественному языку.
Сериализация — это процесс преобразования структуры данных или объекта в формат, пригодный для хранения или передачи, с возможностью обратного восстановления (десериализации).
YAML не является языком разметки в строгом смысле: он не содержит тегов разметки текста, как HTML или XML. Вместо этого YAML — это формат представления графа данных (data graph), где узлами являются скаляры, последовательности и отображения (маппинги).
Граф данных — это абстрактная структура, состоящая из узлов (данных) и рёбер (связей между данными). В YAML узел может быть скаляром (строка, число, булево, null), последовательностью (упорядоченный список) или отображением (ассоциативный массив, пары ключ-значение).
Где используется YAML
YAML стал де-факто стандартом для конфигурационных файлов в современной инфраструктуре и разработке:
Spring Boot: файл
application.yml — альтернатива application.properties. Spring Boot использует SnakeYAML под капотом для парсинга YAML-конфигураций и маппинга их на @ConfigurationProperties.Docker Compose: файл
docker-compose.yml описывает мультиконтейнерные приложения: сервисы, сети, тома, переменные окружения.Kubernetes: манифесты ресурсов (pods, deployments, services) пишутся в YAML.
kubectl apply -f deployment.yml — стандартная операция развёртывания.Ansible: playbooks и inventories — YAML-файлы, описывающие сценарии автоматизации конфигурации серверов.
GitHub Actions: workflow-файлы
.github/workflows/ci.yml описывают пайплайны CI/CD.Helm: чарты Kubernetes используют YAML для шаблонов и values-файлов.
Во всех этих случаях ключевое преимущество YAML — читаемость для человека, которая критична, когда конфигурацию пишут и ревьюят разработчики и DevOps-инженеры.
Библиотека SnakeYAML
В Java-экосистеме стандартной библиотекой для работы с YAML является SnakeYAML. Она разработана сообществом и широко используется как самостоятельно, так и как транзитивная зависимость Spring Boot. На момент 2026 года актуальные версии:
SnakeYAML 2.x — классическая реализация, поддерживающая YAML 1.1 и частично YAML 1.2. Используется в Spring Boot 3.x.
SnakeYAML Engine — отдельный проект, полностью реализующий YAML 1.2. Более строгий парсер, но менее распространён в прикладной разработке.
SnakeYAML 2.x (выпущенная в 2023 году) внесла критическое изменение безопасности: по умолчанию отключена возможность десериализации произвольных Java-объектов через теги
!! (tag handles). Это закрыло классическую уязвимость YAML deserialization remote code execution (RCE), когда злоумышленник мог внедрить в YAML тег !!javax.script.ScriptEngineManager и выполнить произвольный код. В SnakeYAML 2.x для загрузки произвольных классов требуется явная настройка LoaderOptions.RCE (Remote Code Execution) — это класс уязвимостей, позволяющий атакующему выполнить произвольный код на целевой системе удалённо. В контексте десериализации RCE возникает, когда формат данных позволяет указать класс для инстанцирования, и этот класс имеет опасный конструктор или сеттер.
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
👍4
Чтение YAML в Java
Базовое чтение в Map
Простейший способ — загрузить YAML как вложенную структуру
Метод
Типы маппинга по умолчанию:
Отображения (маппинги) →
Последовательности →
Строки →
Целые числа →
Числа с плавающей точкой →
Булевы →
Null →
Потенциальная проблема: ClassCastException
Базовый подход с
Чтение в объекты: loadAs
SnakeYAML поддерживает маппинг YAML напрямую на Java-объекты через механизм JavaBeans. Для этого используется метод
YAML-файл конфигурации:
Чтение в объект:
SnakeYAML автоматически:
Маппит ключ
Маппит вложенный объект
Маппит YAML-последовательность на
Преобразует строковое значение
Кастомные конструкторы для сложных типов
Когда стандартного маппинга JavaBeans недостаточно — например, нужно преобразовать строку в специфический доменный объект — используются кастомные конструкторы SnakeYAML.
Использование в конфигурации:
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
Базовое чтение в Map
Простейший способ — загрузить YAML как вложенную структуру
Map и List:import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;
import java.util.List;
public class YamlReader {
public Map<String, Object> loadConfig(InputStream input) {
// Yaml — лёгкий объект, но не thread-safe
// Для многопоточного использования создавайте Yaml на каждый поток
Yaml yaml = new Yaml();
// load возвращает Object, который для корневого отображения
// приводится к Map<String, Object>
return yaml.load(input);
}
public void printNestedValue(Map<String, Object> config) {
// YAML-отображения маппятся на LinkedHashMap (сохраняет порядок ключей)
// YAML-последовательности маппятся на ArrayList
Map<String, Object> server = (Map<String, Object>) config.get("server");
Integer port = (Integer) server.get("port");
System.out.println("Port: " + port);
}
}
Метод
load(InputStream) читает YAML-документ и возвращает корневой объект. Типы маппинга по умолчанию:
Отображения (маппинги) →
LinkedHashMap<String, Object>Последовательности →
ArrayList<Object>Строки →
StringЦелые числа →
Integer или Long (в зависимости от размера)Числа с плавающей точкой →
DoubleБулевы →
BooleanNull →
nullLinkedHashMap — это реализация интерфейса Map в Java, которая, в отличие от обычного HashMap, сохраняет порядок вставки элементов. Это важно для YAML, так как порядок ключей в конфигурации часто семантически значим.
Потенциальная проблема: ClassCastException
Базовый подход с
Map<String, Object> требует постоянного приведения типов и не даёт статической типизации. Для глубоко вложенных конфигураций это приводит к громоздкому и ошибкоёмкому коду:// Многоуровневое извлечение значения без типобезопасности
Map<String, Object> db = (Map<String, Object>) config.get("database");
Map<String, Object> pool = (Map<String, Object>) db.get("connectionPool");
Integer maxSize = (Integer) pool.get("maxSize"); // ClassCastException, если тип другой
Чтение в объекты: loadAs
SnakeYAML поддерживает маппинг YAML напрямую на Java-объекты через механизм JavaBeans. Для этого используется метод
loadAs(InputStream, Class<T>).JavaBeans — это соглашение в Java, согласно которому класс должен иметь конструктор без параметров, публичные геттеры и сеттеры для свойств, и реализовывать интерфейс Serializable (опционально). SnakeYAML использует рефлексию для вызова сеттеров по именам ключей YAML.
import org.yaml.snakeyaml.Yaml;
public class LibraryConfig {
private String storagePath;
private int maxBooks;
private DatabaseConfig database;
private List<String> allowedFormats;
// Конструктор по умолчанию обязателен для SnakeYAML
public LibraryConfig() {}
// Геттеры и сеттеры — SnakeYAML вызывает сеттеры по имени ключа
public String getStoragePath() { return storagePath; }
public void setStoragePath(String storagePath) { this.storagePath = storagePath; }
public int getMaxBooks() { return maxBooks; }
public void setMaxBooks(int maxBooks) { this.maxBooks = maxBooks; }
public DatabaseConfig getDatabase() { return database; }
public void setDatabase(DatabaseConfig database) { this.database = database; }
public List<String> getAllowedFormats() { return allowedFormats; }
public void setAllowedFormats(List<String> allowedFormats) { this.allowedFormats = allowedFormats; }
// Вложенный класс конфигурации
public static class DatabaseConfig {
private String url;
private String username;
private String password;
private int connectionTimeout;
public DatabaseConfig() {}
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getConnectionTimeout() { return connectionTimeout; }
public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; }
}
}
YAML-файл конфигурации:
storagePath: /var/lib/library
maxBooks: 10000
allowedFormats:
- epub
- mobi
database:
url: jdbc:postgresql://localhost:5432/library
username: lib_admin
password: secret
connectionTimeout: 30
Чтение в объект:
public class ConfigLoader {
public LibraryConfig loadLibraryConfig(InputStream input) {
Yaml yaml = new Yaml();
// loadAs маппит YAML на объект указанного класса через JavaBeans
return yaml.loadAs(input, LibraryConfig.class);
}
}SnakeYAML автоматически:
Маппит ключ
storagePath на вызов setStoragePath()Маппит вложенный объект
database на инстанцирование LibraryConfig.DatabaseConfigМаппит YAML-последовательность на
List<String>Преобразует строковое значение
"10000" в int через соответствующий сеттерКастомные конструкторы для сложных типов
Когда стандартного маппинга JavaBeans недостаточно — например, нужно преобразовать строку в специфический доменный объект — используются кастомные конструкторы SnakeYAML.
public class CustomDurationConstructor extends Constructor {
public CustomDurationConstructor(Class<?> theRoot) {
super(theRoot);
// Регистрируем кастомный конструктор для тега !duration
this.yamlConstructors.put(new Tag("!duration"), new DurationConstruct());
}
// AbstractConstruct — базовый класс для пользовательских конструкторов узлов YAML
private class DurationConstruct extends AbstractConstruct {
@Override
public Object construct(Node node) {
// ScalarNode представляет скалярное значение (строка, число и т.д.)
String value = ((ScalarNode) node).getValue();
try {
// ISO-8601 формат: PT30S — 30 секунд, PT5M — 5 минут
return Duration.parse(value);
} catch (DateTimeParseException e) {
throw new YAMLException("Невалидный формат Duration: " + value, e);
}
}
}
}Использование в конфигурации:
storagePath: /var/lib/library
maxBooks: 10000
sessionTimeout: !duration PT30M
public class AdvancedConfig {
private Duration sessionTimeout;
public Duration getSessionTimeout() { return sessionTimeout; }
public void setSessionTimeout(Duration sessionTimeout) { this.sessionTimeout = sessionTimeout; }
}
// Загрузка с кастомным конструктором
Yaml yaml = new Yaml(new CustomDurationConstructor(AdvancedConfig.class));
AdvancedConfig config = yaml.load(input);Тег (tag) в YAML — это метка, явно указывающая тип узла. Стандартные теги: !!str , !!int, !!float, !!bool, !!null, !!map, !!seq. Пользовательские теги начинаются с ! (локальные) или !! (глобальные). Теги позволяют YAML-документу нести информацию о типах, необходимую для корректной десериализации.
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
👍4
Запись YAML из Java
Метод
Поддержка списков, вложенных объектов и многострочных строк
Списки (последовательности)
Вложенные объекты (отображения)
Многострочные строки
YAML предоставляет два оператора для многострочных строк:
Literal block scalar (
Folded block scalar (
Сравнение YAML и JSON для конфигураций
Читаемость
YAML превосходит JSON в читаемости за счёт отсутствия скобок и кавычек. Сравним одинаковую конфигурацию:
JSON:
YAML:
В YAML отсутствуют запятые после каждого элемента, нет обрамляющих скобок, строки не требуют кавычек (кроме случаев, когда значение может быть интерпретировано как другой тип — например,
Комментарии
YAML поддерживает комментарии двумя способами:
YAML не поддерживает многострочные комментарии напрямую, но можно использовать несколько строк с
JSON официально не поддерживает комментарии (RFC 8259). Некоторые парсеры допускают комментарии как расширение, но это нарушает стандарт. Для конфигураций, где комментарии критичны (объяснение значения параметра, временное отключение опции, TODO), YAML является единственным разумным выбором из двух.
Строгость и предсказуемость
JSON более строг и предсказуем: каждый документ — ровно один объект или массив, синтаксис однозначен. YAML имеет сложную спецификацию с множеством особенностей: неявная типизация (
Производительность
JSON парсится быстрее, так как грамматика проще и не требует отслеживания отступов. YAML-парсер должен вычислять отступы каждой строки, обрабатывать сложные правила скаляров и тегов. Для конфигураций, которые читаются один раз при старте приложения, эта разница несущественна. Для высокочастотного обмена данными (миллионы сообщений в секунду) JSON предпочтительнее.
Вывод
Используйте YAML для конфигураций, которые пишут и читают люди:
Используйте JSON для машинно-генерируемых данных, API-контрактов, логов и сценариев, где производительность парсинга критична.
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
Метод
dump(Object data, Writer writer) сериализует Java-объект в YAML-представление.public class YamlWriter {
public String writeConfig(LibraryConfig config) {
// DumperOptions — настройки форматирования вывода
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); // блочный стиль вместо inline
options.setPrettyFlow(true); // красивые отступы
options.setIndent(2); // отступ в 2 пробела
Yaml yaml = new Yaml(options);
StringWriter writer = new StringWriter();
yaml.dump(config, writer);
return writer.toString();
}
}Flow style и block style — это два способа записи коллекций в YAML. Block style использует отступы (как Python), flow style использует JSON-подобный синтаксис со скобками. Для конфигураций block style предпочтителен из-за читаемости.
Поддержка списков, вложенных объектов и многострочных строк
Списки (последовательности)
allowedFormats:
- epub
- mobi
# Альтернативный flow-style (менее читаемый)
allowedFormats: [pdf, epub, mobi]
Вложенные объекты (отображения)
database:
url: jdbc:postgresql://localhost:5432/library
pool:
min: 5
max: 20
Многострочные строки
YAML предоставляет два оператора для многострочных строк:
Literal block scalar (
|): сохраняет переводы строки как есть.Folded block scalar (
>): заменяет одиночные переводы строки пробелами, сохраняя только пустые строки как разделители абзацев.description: |
Это многострочный текст.
Каждая строка сохраняет свой перевод.
Итоговая строка содержит \n между строками.
license: >
Это текст, который в исходном YAML
разбит на строки для читаемости,
но в результирующей строке будет
представлен одним абзацем с пробелами
вместо переводов строк.
Block scalar — это способ записи скалярных значений (строк), занимающих несколько строк, в YAML. Оператор (pipe) используется для verbatim (дословного) сохранения строк, оператор > (greater-than) — для сворачивания строк в один абзац.
Сравнение YAML и JSON для конфигураций
Читаемость
YAML превосходит JSON в читаемости за счёт отсутствия скобок и кавычек. Сравним одинаковую конфигурацию:
JSON:
{
"server": {
"port": 8080,
"ssl": {
"enabled": true,
"certificate": "/etc/ssl/cert.pem"
}
},
"features": ["auth", "logging", "metrics"]
}YAML:
server:
port: 8080
ssl:
enabled: true
certificate: /etc/ssl/cert.pem
features:
- auth
- logging
- metrics
В YAML отсутствуют запятые после каждого элемента, нет обрамляющих скобок, строки не требуют кавычек (кроме случаев, когда значение может быть интерпретировано как другой тип — например,
"true" как строка vs true как булево). Это снижает визуальный шум и упрощает ревью изменений в системах контроля версий.Комментарии
YAML поддерживает комментарии двумя способами:
# — однострочный комментарий до конца строки.YAML не поддерживает многострочные комментарии напрямую, но можно использовать несколько строк с
#.JSON официально не поддерживает комментарии (RFC 8259). Некоторые парсеры допускают комментарии как расширение, но это нарушает стандарт. Для конфигураций, где комментарии критичны (объяснение значения параметра, временное отключение опции, TODO), YAML является единственным разумным выбором из двух.
Строгость и предсказуемость
JSON более строг и предсказуем: каждый документ — ровно один объект или массив, синтаксис однозначен. YAML имеет сложную спецификацию с множеством особенностей: неявная типизация (
yes может быть интерпретировано как булево true в YAML 1.1), якоря и алиасы (&anchor и *alias), сложные правила отступов. Это делает YAML более подверженным ошибкам при ручном редактировании: лишний пробел может изменить структуру, а неявная типизация — привести к неожиданным значениям.Якорь (anchor) и алиас (alias) — это механизм YAML для повторного использования узлов. &name создаёт якорь для узла, *name создаёт ссылку (алиас) на этот узел. Это позволяет избежать дублирования, но усложняет документ.
Производительность
JSON парсится быстрее, так как грамматика проще и не требует отслеживания отступов. YAML-парсер должен вычислять отступы каждой строки, обрабатывать сложные правила скаляров и тегов. Для конфигураций, которые читаются один раз при старте приложения, эта разница несущественна. Для высокочастотного обмена данными (миллионы сообщений в секунду) JSON предпочтительнее.
Вывод
Используйте YAML для конфигураций, которые пишут и читают люди:
application.yml, CI/CD пайплайны, Kubernetes-манифесты.Используйте JSON для машинно-генерируемых данных, API-контрактов, логов и сценариев, где производительность парсинга критична.
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
👍4
Путь байтов в памяти JVM при работе с YAML
1. Загрузка YAML-файла
Когда приложение читает
Файл считывается через
SnakeYAML использует
Парсер SnakeYAML читает символы и строит внутреннее представление документа — дерево узлов (Node graph). Каждый узел YAML (скаляр, последовательность, отображение) представлен объектом в куче:
2. Маппинг на Java-объекты
После построения дерева узлов начинается фаза конструирования:
Для каждого ключа отображения
Значения узлов преобразуются в целевые типы: строки остаются строками, числа конвертируются через
Промежуточные структуры — дерево узлов, массивы символов, объекты рефлексии — становятся мусором после завершения конструирования. Если конфигурация читается один раз при старте приложения, все эти объекты собираются при первой Minor GC после инициализации.
3. Работа GC с конфигурационными объектами
Объект конфигурации (например,
Промежуточные объекты парсинга (дерево узлов,
4. Потенциальная утечка: кэш классов SnakeYAML
SnakeYAML кэширует
5. Запись YAML: обратный путь
При сериализации Java-объекта в YAML:
Байты уходят в файловую систему или сетевой сокет через системные вызовы ОС.
Все промежуточные структуры (дерево узлов, буферы символов) создаются в Young Generation и собираются при следующей Minor GC.
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
1. Загрузка YAML-файла
Когда приложение читает
application.yml из classpath или файловой системы, данные проходят следующий путь:Файл считывается через
InputStream — байты передаются из кэша страниц ОС через нативную память в буфер byte[] в куче JVM (аналогично Files.readAllBytes(), описанному в предыдущих уроках).SnakeYAML использует
Reader (обычно InputStreamReader с кодировкой UTF-8) для декодирования байтов в символы char[]. Этот массив создаётся в Young Generation, в Eden.Парсер SnakeYAML читает символы и строит внутреннее представление документа — дерево узлов (Node graph). Каждый узел YAML (скаляр, последовательность, отображение) представлен объектом в куче:
ScalarNode, MappingNode, SequenceNode. Для конфигурации размером 5 КБ количество узлов может достигать нескольких десятков.Node graph — это промежуточное представление YAML-документа в памяти парсера перед маппингом на Java-объекты. Оно отражает структуру документа независимо от целевого типа данных.
2. Маппинг на Java-объекты
После построения дерева узлов начинается фаза конструирования:
Constructor обходит дерево узлов. Для каждого MappingNode он создаёт целевой Java-объект через рефлексию: вызов Class.newInstance() (или Constructor.newInstance() в современных версиях) аллоцирует объект в куче.Для каждого ключа отображения
Constructor ищет соответствующий сеттер через Introspector (механизм JavaBeans introspection). Introspector анализирует класс и кэширует PropertyDescriptor — дескрипторы свойств. Эти дескрипторы создаются при первом обращении к классу и хранятся в кэше, но первое использование класса порождает множество временных объектов рефлексии.Introspection (интроспекция) — это механизм Java, позволяющий во время выполнения анализировать структуру классов: получать список методов, полей, конструкторов, аннотаций. java.beans.Introspector — стандартный класс для JavaBeans-интроспекции.
Значения узлов преобразуются в целевые типы: строки остаются строками, числа конвертируются через
Integer.parseInt() или аналогичные методы, вложенные отображения рекурсивно конструируются.Промежуточные структуры — дерево узлов, массивы символов, объекты рефлексии — становятся мусором после завершения конструирования. Если конфигурация читается один раз при старте приложения, все эти объекты собираются при первой Minor GC после инициализации.
3. Работа GC с конфигурационными объектами
Объект конфигурации (например,
LibraryConfig) обычно сохраняется в Old Generation (Tenured), так как он живёт всё время жизни приложения. Ссылки на него хранятся в контексте Spring (singleton-бины) или в статических полях.Singleton — это паттерн проектирования, гарантирующий, что у класса есть только один экземпляр, и предоставляющий глобальную точку доступа к нему. В Spring Boot все бины по умолчанию — singleton.
Промежуточные объекты парсинга (дерево узлов,
char[] исходного файла, временные объекты рефлексии) создаются в Young Generation и уничтожаются Minor GC. Важно, что SnakeYAML не использует пул строк для ключей YAML — каждый ключ создаётся как новый объект String в куче. При большом количестве ключей это создаёт давление на Eden, но так как конфигурация читается редко, это не критично.4. Потенциальная утечка: кэш классов SnakeYAML
SnakeYAML кэширует
TypeDescription и конструкторы классов во внутренних Map. Эти кэши растут по мере обработки новых классов. В долгоживущих приложениях, которые динамически загружают множество различных YAML-схем, кэш может занимать значительный объём Old Generation. Это не утечка в классическом смысле (кэш необходим), но требует мониторинга через heap dump.Heap dump — это снимок всей кучи JVM в определённый момент времени. Он содержит все объекты, их размеры, ссылки между ними и классы. Анализ heap dump позволяет находить утечки памяти и понимать распределение объектов по поколениям.
5. Запись YAML: обратный путь
При сериализации Java-объекта в YAML:
Representer обходит поля объекта и строит дерево узлов в памяти (аллокации в Young Generation).Emitter преобразует дерево узлов в поток символов char[].Writer кодирует символы в UTF-8 байты и записывает в OutputStream.Байты уходят в файловую систему или сетевой сокет через системные вызовы ОС.
Все промежуточные структуры (дерево узлов, буферы символов) создаются в Young Generation и собираются при следующей Minor GC.
#Java #для_новичков #beginner #IO #NIO #Serialize #YAML
👍4
[Совет по Java #069]
Тема: Всегда переопределяйте
Проблема: Интерфейс
Если это правило нарушено, то
Решение: При реализации
Для этого используйте в
Для примитивных полей используйте
Объяснение:
При этом
Это нарушает принцип "Set не содержит дубликатов" — фактически дубликаты по
#Java #советы
Тема: Всегда переопределяйте
compareTo() в соответствии с equals(), если планируете использовать объекты в TreeSet/TreeMap.Проблема: Интерфейс
Comparable и его метод compareTo() определяют естественный порядок объектов. Контракт Comparable требует, чтобы compareTo() был согласован с equals(), то есть для любых двух объектов a и b должно выполняться: a.compareTo(b) == 0 тогда и только тогда, когда a.equals(b) == true. Если это правило нарушено, то
TreeSet и TreeMap, которые используют compareTo() для сравнения и упорядочивания, перестают корректно работать. Например, если два объекта имеют одинаковый compareTo (возвращают 0), но разные equals, они будут считаться дубликатами, и второй не будет добавлен в TreeSet, либо TreeMap перезапишет значение. Это противоречит ожиданиям, основанным на equals. Ошибка проявляется в виде "потери" элементов, неправильного размера множества или некорректного поиска по ключу.Решение: При реализации
Comparable для классов, которые будут использоваться в упорядоченных коллекциях, всегда проверяйте, что compareTo возвращает 0 для объектов, которые считаются равными по equals. Для этого используйте в
compareTo те же поля, что и в equals, и в одинаковом порядке. Если поля не могут быть сравнимы (например, null), обработайте это явно. Если невозможно достичь согласованности (например, у вас сложная логика сравнения), используйте отдельный Comparator с четкой документацией, но лучше пересмотреть дизайн. Для примитивных полей используйте
Integer.compare, Double.compare и т.д. Для строк — String.compareTo. Также помните, что compareTo должно быть транзитивным и антисимметричным.import java.util.Objects;
public class Person implements Comparable<Person> {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//Нарушение: compareTo не согласован с equals
// @Override
// public int compareTo(Person o) {
// return this.age - o.age; // Только по возрасту
// }
// // equals по имени и возрасту
// public boolean equals(Object o) {
// if (!(o instanceof Person)) return false;
// Person p = (Person) o;
// return name.equals(p.name) && age == p.age;
// }
//Корректно: compareTo использует те же поля, что и equals
@Override
public int compareTo(Person o) {
int nameComp = this.name.compareTo(o.name);
if (nameComp != 0) return nameComp;
return Integer.compare(this.age, o.age);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && name.equals(p.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return name + "(" + age + ")";
}
public static void main(String[] args) {
// Демонстрация нарушения
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Bob", 30);
// Если compareTo только по возрасту, то p1.compareTo(p2) == 0,
// хотя equals вернет false (имена разные).
// TreeSet считает их одинаковыми
TreeSet<Person> set = new TreeSet<>();
set.add(p1);
set.add(p2);
System.out.println("Size: " + set.size()); // Должно быть 1, если compareTo=0
System.out.println("Contains Alice? " + set.contains(p1)); // true
System.out.println("Contains Bob? " + set.contains(p2)); // true? Зависит от реализации
// На самом деле, если compareTo=0, то при добавлении второго он будет считаться дубликатом
// и не будет добавлен, но contains для Bob вернет true, потому что TreeSet использует compareTo
// для поиска, а не equals. Это делает поведение непредсказуемым.
}
}
Объяснение:
TreeSet и TreeMap хранят элементы в красно-черном дереве, где сравнение выполняется через compareTo или Comparator. При добавлении элемента дерево проверяет, существует ли уже узел с тем же значением compareTo (т.е. возвращает 0). Если существует, то новый элемент считается дубликатом, и в TreeSet он не добавляется, а в TreeMap перезаписывается значение. При этом
equals вообще не используется для проверки дубликатов; она применяется только для contains и remove? На самом деле, TreeSet использует compareTo для поиска: если compareTo возвращает 0, объект считается равным, и возвращается существующий, без вызова equals. Поэтому нарушение контракта приводит к тому, что объекты, которые по логике должны быть разными, становятся неразличимыми для коллекции. Это нарушает принцип "Set не содержит дубликатов" — фактически дубликаты по
equals могут быть, но они не добавляются из-за compareTo. И наоборот, объекты с одинаковым equals, но разным compareTo, будут восприниматься как разные, что нарушает контракт Set. Поэтому всегда используйте в compareTo те же поля, что и в equals, и в том же порядке, чтобы обеспечить согласованность. Если поля могут быть null, используйте Objects.compare или явные проверки.#Java #советы
👍3
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 3. Сериализация и форматы обмена
Protocol Buffers (Protobuf) — бинарный формат сериализации от Google
Protocol Buffers (Protobuf) — это механизм нейтрального к платформе и языку расширяемого сериализации структурированных данных, разработанный Google. Первый публичный релиз состоялся в 2008 году, хотя внутри Google формат использовался с 2001 года. На момент 2026 года актуальная версия спецификации — proto3 (Protocol Buffers версии 3), выпущенная в 2016 году, с последующими обновлениями языка и runtime.
Protobuf решает фундаментальную проблему текстовых форматов вроде JSON и XML: они удобны для человека, но неэффективны для машины. Каждый символ в JSON занимает байт (или несколько в UTF-8), каждая фигурная скобка, запятая и кавычка — это накладные расходы. В высоконагруженных распределённых системах, где сервисы обмениваются миллионами сообщений в секунду, эти накладные расходы становятся bottleneck на уровне CPU, памяти и сетевой пропускной способности. Protobuf заменяет текстовое представление на компактное бинарное, при этом сохраняя строгую типизацию и возможность эволюции схемы.
Архитектура: схема как контракт
В основе Protobuf лежит идея schema-first (сначала схема). Разработчик описывает структуру данных в файле с расширением
Пример .proto файла
Ключевые элементы синтаксиса:
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
Глава 3. Сериализация и форматы обмена
Protocol Buffers (Protobuf) — бинарный формат сериализации от Google
Protocol Buffers (Protobuf) — это механизм нейтрального к платформе и языку расширяемого сериализации структурированных данных, разработанный Google. Первый публичный релиз состоялся в 2008 году, хотя внутри Google формат использовался с 2001 года. На момент 2026 года актуальная версия спецификации — proto3 (Protocol Buffers версии 3), выпущенная в 2016 году, с последующими обновлениями языка и runtime.
Protobuf решает фундаментальную проблему текстовых форматов вроде JSON и XML: они удобны для человека, но неэффективны для машины. Каждый символ в JSON занимает байт (или несколько в UTF-8), каждая фигурная скобка, запятая и кавычка — это накладные расходы. В высоконагруженных распределённых системах, где сервисы обмениваются миллионами сообщений в секунду, эти накладные расходы становятся bottleneck на уровне CPU, памяти и сетевой пропускной способности. Protobuf заменяет текстовое представление на компактное бинарное, при этом сохраняя строгую типизацию и возможность эволюции схемы.
Bottleneck — это узкое место в системе, которое ограничивает общую производительность. В контексте сериализации bottleneck часто проявляется в виде высокой нагрузки на CPU при парсинге текстовых форматов, избыточном потреблении памяти кучи и низкой пропускной способности сети из-за раздутого размера сообщений.
Архитектура: схема как контракт
В основе Protobuf лежит идея schema-first (сначала схема). Разработчик описывает структуру данных в файле с расширением
.proto на специальном языке описания интерфейсов (IDL — Interface Definition Language), а затем компилирует этот файл в код на целевом языке программирования. Этот подход кардинально отличается от JSON, где схема существует только в головах разработчиков или в отдельном документе (OpenAPI), но не является частью процесса сериализации.IDL (Interface Definition Language) — это язык формальной спецификации интерфейсов программных компонентов. В контексте Protobuf IDL описывает структуру сообщений, их поля, типы и правила версионирования.
Пример .proto файла
syntax = "proto3";
package library;
// Опция java_package переопределяет пакет для сгенерированных Java-классов
option java_package = "com.example.library.proto";
option java_multiple_files = true;
// Сообщение Book описывает структуру книги
message Book {
// Поле с номером тега 1. Тег — это уникальный идентификатор поля в пределах сообщения
string title = 1;
// Поле с номером тега 2
string author = 2;
// int32 — 32-битное целое со знаком
int32 year = 3;
// double — 64-битное число с плавающей точкой
double price = 4;
// bool — булево значение
bool available = 5;
// repeated — повторяющееся поле, аналог списка или массива
repeated string tags = 6;
// Вложенное сообщение
message Publisher {
string name = 1;
string country = 2;
}
Publisher publisher = 7;
}
Ключевые элементы синтаксиса:
syntax = "proto3" — указание версии языка. proto3 — современная версия, упрощённая по сравнению с proto2. В proto3 все поля технически optional, нет required-полей, нет значений по умолчанию для полей сообщений.message — единица структуры данных, аналог класса в ООП. Каждое сообщение компилируется в класс на целевом языке.field_type field_name = field_number — объявление поля. Тип, имя и номер тега. Номер тега — целое число от 1 до 536870911 (2^29 - 1), но рекомендуется использовать 1–15 для частых полей, так как они кодируются одним байтом.repeated — модификатор, указывающий, что поле является коллекцией. В proto3 repeated-поля кодируются как packed по умолчанию для скалярных типов, что экономит пространство.option — метаданные компиляции. java_package задаёт пакет Java-классов, java_multiple_files = true генерирует отдельный файл для каждого сообщения вместо одного большого файла с вложенными классами.Тег (field number) — это уникальный числовой идентификатор поля в пределах сообщения Protobuf. Тег, а не имя поля, записывается в бинарное представление. Это позволяет изменять имена полей в схеме, не ломая бинарную совместимость, так как парсер опирается на номера тегов.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
👍4
Бинарное кодирование: wire format
Protobuf использует собственный бинарный формат передачи — wire format. Он основан на принципе TLV (Tag-Length-Value) для сложных типов и TV (Tag-Value) для скаляров переменной длины. Каждое поле сообщения кодируется независимо, и порядок полей в бинарном потоке не обязан совпадать с порядком объявления в .proto файле.
Структура поля в wire format
Каждое поле кодируется как:
Тег состоит из двух компонентов, упакованных в varint:
field number (номер поля, 3 бита wire type + оставшиеся биты на номер)
wire type (тип кодирования значения, 3 бита)
Wire types определены спецификацией:
0 — Varint (целые числа, булевы, enum)
1 — 64-bit (fixed64, sfixed64, double)
2 — Length-delimited (string, bytes, вложенные сообщения, packed repeated)
3 — Start group (устаревший, не используется в proto3)
4 — End group (устаревший)
5 — 32-bit (fixed32, sfixed32, float)
Varint: переменная длина целых чисел
Varint позволяет кодировать малые числа компактно: числа от 0 до 127 занимают 1 байт. Это главная причина, по которой рекомендуется использовать теги 1–15 для частых полей — теговый байт тоже кодируется как varint, и малые номера полей занимают 1 байт.
ZigZag: кодирование отрицательных чисел
Стандартный varint плохо подходит для отрицательных чисел в знаковых типах (sint32, sint64), так как в дополнительном коде (two's complement) отрицательные числа имеют установленные старшие биты и кодируются максимальной длиной (5 байт для sint32, 10 байт для sint64). Для решения этой проблемы Protobuf использует ZigZag encoding.
Таким образом,
Length-delimited: строки и вложенные сообщения
Для типов с переменной длиной (string, bytes, вложенные message) используется wire type 2.
Формат:
Где
Packed repeated fields
В proto3 скалярные числовые repeated-поля кодируются как packed по умолчанию. Это означает, что вместо отдельного тега для каждого элемента массива весь массив кодируется как один length-delimited блок:
Это значительно экономит пространство по сравнению с unpacked-форматом, где каждый элемент имел бы свой тег.
Компиляция: от .proto к Java
Компилятор
Для Java процесс выглядит так:
Флаг
Ключевые особенности сгенерированного кода:
Immutability (неизменяемость): после создания через
Builder pattern: создание объекта идёт через вложенный класс
LazyStringList: оптимизированная реализация
CodedOutputStream / CodedInputStream: низкоуровневые потоки для записи и чтения wire format. Работают напрямую с
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
Protobuf использует собственный бинарный формат передачи — wire format. Он основан на принципе TLV (Tag-Length-Value) для сложных типов и TV (Tag-Value) для скаляров переменной длины. Каждое поле сообщения кодируется независимо, и порядок полей в бинарном потоке не обязан совпадать с порядком объявления в .proto файле.
Wire format — это конкретное бинарное представление сообщения Protobuf при передаче по сети или записи на диск. Оно определяет, как типы данных, номера полей и значения упаковываются в последовательность байтов.
Структура поля в wire format
Каждое поле кодируется как:
[tag][value]
Тег состоит из двух компонентов, упакованных в varint:
field number (номер поля, 3 бита wire type + оставшиеся биты на номер)
wire type (тип кодирования значения, 3 бита)
Wire types определены спецификацией:
0 — Varint (целые числа, булевы, enum)
1 — 64-bit (fixed64, sfixed64, double)
2 — Length-delimited (string, bytes, вложенные сообщения, packed repeated)
3 — Start group (устаревший, не используется в proto3)
4 — End group (устаревший)
5 — 32-bit (fixed32, sfixed32, float)
Varint: переменная длина целых чисел
Varint (variable-length integer) — это способ кодирования целых чисел в виде последовательности байтов переменной длины. Каждый байт varint содержит 7 бит данных и 1 бит-флаг продолжения (most significant bit). Если флаг установлен, следует ещё один байт.
Значение 1:
Бинарное: 00000001
Varint: 00000001 (1 байт)
Значение 150:
Бинарное: 10010110
Varint: 10010110 00000001 (2 байта)
Разбор: младшие 7 бит первого байта = 1001010 (22), старший бит = 1 (продолжение)
второй байт = 0000001 (1), итого: 1 * 128 + 22 = 150
Varint позволяет кодировать малые числа компактно: числа от 0 до 127 занимают 1 байт. Это главная причина, по которой рекомендуется использовать теги 1–15 для частых полей — теговый байт тоже кодируется как varint, и малые номера полей занимают 1 байт.
ZigZag: кодирование отрицательных чисел
Стандартный varint плохо подходит для отрицательных чисел в знаковых типах (sint32, sint64), так как в дополнительном коде (two's complement) отрицательные числа имеют установленные старшие биты и кодируются максимальной длиной (5 байт для sint32, 10 байт для sint64). Для решения этой проблемы Protobuf использует ZigZag encoding.
ZigZag encoding — это схема отображения целых чисел со знаком на целые без знака, при которой малые по модулю числа (положительные и отрицательные) отображаются на малые положительные числа. Формула: (n << 1) ^ (n >> 31) для 32-битных чисел.
Исходное значение -> Закодированное
0 -> 0
-1 -> 1
1 -> 2
-2 -> 3
2 -> 4
Таким образом,
-1 кодируется как 1 и занимает 1 байт вместо 5. Для полей, которые могут содержать отрицательные значения, в .proto следует использовать sint32 или sint64 вместо int32/int64.Length-delimited: строки и вложенные сообщения
Для типов с переменной длиной (string, bytes, вложенные message) используется wire type 2.
Формат:
[tag][length][data]
Где
length — varint, указывающий количество байт в data. Это позволяет парсеру пропустить неизвестные поля (unknown fields) или поля с неправильным типом, не читая их содержимое байт за байтом.Packed repeated fields
В proto3 скалярные числовые repeated-поля кодируются как packed по умолчанию. Это означает, что вместо отдельного тега для каждого элемента массива весь массив кодируется как один length-delimited блок:
[tag][total_length][value1][value2][value3]...
Это значительно экономит пространство по сравнению с unpacked-форматом, где каждый элемент имел бы свой тег.
Компиляция: от .proto к Java
Компилятор
protoc (Protocol Buffers Compiler) трансформирует .proto файлы в исходный код на целевом языке.Для Java процесс выглядит так:
# Установка компилятора (например, через Maven plugin или скачивание бинарника)
protoc --java_out=./src/main/java library.proto
Флаг
--java_out указывает директорию для сгенерированных Java-файлов. Результат компиляции сообщения Book из примера выше:// Сгенерированный класс Book.java
package com.example.library.proto;
public final class Book extends GeneratedMessageV3 implements BookOrBuilder {
// Приватные поля — immutable объект
private volatile Object title_;
private volatile Object author_;
private int year_;
private double price_;
private boolean available_;
// Repeated поле — хранится как ProtocolStringList (оптимизированная реализация List<String>)
private LazyStringList tags_;
private Publisher publisher_;
// Приватный конструктор — объекты создаются через Builder
private Book() {
title_ = "";
author_ = "";
tags_ = LazyStringArrayList.EMPTY;
}
// Геттеры
public String getTitle() { ... }
public String getAuthor() { ... }
public int getYear() { ... }
public List<String> getTagsList() { ... }
public Publisher getPublisher() { ... }
// Сериализация в OutputStream
public void writeTo(CodedOutputStream output) throws IOException {
// Кодирование каждого поля в wire format
if (!getTitleBytes().isEmpty()) {
GeneratedMessageV3.writeString(output, 1, title_);
}
if (!getAuthorBytes().isEmpty()) {
GeneratedMessageV3.writeString(output, 2, author_);
}
if (year_ != 0) {
output.writeInt32(3, year_);
}
// ... и так далее для каждого поля
}
// Десериализация из InputStream
public static Book parseFrom(InputStream input) throws IOException {
return parseFrom(input, DEFAULT_INSTANCE);
}
// Вложенный класс Builder — реализация паттерна Builder
public static final class Builder extends GeneratedMessageV3.Builder<Builder> {
// Мутабельная копия полей для построения объекта
public Builder setTitle(String value) { ... }
public Builder setAuthor(String value) { ... }
public Builder setYear(int value) { ... }
public Builder addTags(String value) { ... }
public Book build() { ... } // создаёт immutable Book
}
// Singleton-экземпляр DEFAULT_INSTANCE для пустого сообщения
private static final Book DEFAULT_INSTANCE;
static {
DEFAULT_INSTANCE = new Book();
}
}
Ключевые особенности сгенерированного кода:
Immutability (неизменяемость): после создания через
build() объект Book не может быть изменен. Все поля private final (или private volatile для строк), сеттеров нет. Изменение требует создания нового объекта через toBuilder().Builder pattern: создание объекта идёт через вложенный класс
Builder, что позволяет конструировать объект пошагово и гарантирует валидность на момент build().LazyStringList: оптимизированная реализация
List<String> для repeated string-полей. Хранит строки как ByteString или byte[] до первого обращения как String, что экономит перекодирование UTF-8.CodedOutputStream / CodedInputStream: низкоуровневые потоки для записи и чтения wire format. Работают напрямую с
byte[], минуя промежуточные текстовые представления.Immutability (неизменяемость) — это свойство объекта, при котором его состояние не может быть изменено после создания. Неизменяемые объекты потокобезопасны по определению, так как их нельзя изменить из любого потока. В Protobuf immutability гарантирует, что сериализованное сообщение не изменится во время передачи.
Builder pattern (паттерн строитель) — это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово. В Protobuf Builder изолирует мутабельное состояние на этапе конструирования, а финальный объект остаётся immutable.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
👍4
Практический пример: сериализация и десериализация
Метод
Версионирование и эволюция схемы
Одно из ключевых преимуществ Protobuf — встроенная поддержка backward и forward compatibility через правила изменения схемы.
Правила безопасного изменения схемы
Добавление полей: можно добавлять новые поля с новыми тегами. Старый код проигнорирует неизвестные теги (wire parser пропускает их по length-delimiter или varint-размеру). Новый код получит default value для отсутствующих в старом сообщении полей.
Удаление полей: поле можно удалить, но его тег нельзя повторно использовать (чтобы избежать коллизий со старыми сообщениями, где это поле ещё может присутствовать). Рекомендуется помечать удалённые поля как
Изменение типа: нельзя менять wire type поля (например, int32 на string), так как это сломает бинарный парсинг. Но можно менять тип в пределах совместимых wire type: int32 на int64 (wire type 0), string на bytes (wire type 2).
Изменение имени: имя поля можно менять свободно, так как в wire format хранится только тег, а не имя.
Default values: в proto3 поля всегда имеют zero-value по умолчанию (0 для чисел, пустая строка, false, первое значение enum). Это означает, что невозможно отличить "поле не было установлено" от "поле было установлено в значение по умолчанию". Для явного контроля наличия поля в proto3 используется обёртка
Преимущества Protobuf
Компактность
Бинарное представление Protobuf значительно меньше JSON. Для сообщения
JSON с отступами: ~280 байт.
JSON minified: ~200 байт.
Protobuf binary: ~55-65 байт (в зависимости от длины строк).
Экономия достигается за счёт:
Отсутствия имён полей в бинарном потоке (только числовые теги).
Varint-кодирования малых целых чисел.
Packed repeated-полей.
Отсутствия запятых, скобок, кавычек.
Для высоконагруженных систем экономия 70-80% трафика критична. В микросервисной архитектуре, где сервисы обмениваются через сеть, снижение размера сообщений прямо пропорционально снижению задержек (latency) и стоимости сетевой инфраструктуры.
Скорость
Protobuf парсится быстрее JSON по нескольким причинам:
Отсутствие текстового разбора: не нужно искать кавычки, запятые, скобки, обрабатывать escape-последовательности. Парсер читает байты последовательно, используя заранее известную структуру.
Нет рефлексии при десериализации: сгенерированный код содержит жёстко заданные инструкции вида "тег 1 — строка, записать в поле title". В Jackson для JSON требуется рефлексия или runtime-генерация bytecode.
Zero-copy для строк:
Предсказуемость аллокаций: размер сообщения известен до сериализации (вызов
В бенчмарках (JMH — Java Microbenchmark Harness) сериализация/десериализация Protobuf в Java обычно в 3–10 раз быстрее Jackson JSON для типичных сообщений, и в 20+ раз быстрее для сложных вложенных структур.
Строгая типизация
Кроссплатформенность и межъязыковая совместимость
Один .proto файл компилируется в C++, Java, Python, Go, C#, JavaScript, Ruby, Objective-C, PHP, Dart, Kotlin и другие языки. Это означает, что бэкенд на Java, мобильное приложение на Kotlin и фронтенд на JavaScript могут использовать одну и ту же схему данных, гарантируя совместимость на уровне wire format.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
import com.example.library.proto.Book;
import com.example.library.proto.Book.Publisher;
import com.google.protobuf.ByteString;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class ProtobufExample {
public void createAndSerialize() throws Exception {
// Создание объекта через Builder
Book book = Book.newBuilder()
.setTitle("Clean Code")
.setAuthor("Robert C. Martin")
.setYear(2008)
.setPrice(42.50)
.setAvailable(true)
.addTags("programming")
.addTags("software engineering")
.setPublisher(
Publisher.newBuilder()
.setName("Prentice Hall")
.setCountry("USA")
.build()
)
.build();
// Сериализация в байтовый массив
byte[] bytes = book.toByteArray();
System.out.println("Serialized size: " + bytes.length + " bytes");
// Для сравнения: JSON-представление этого же объекта заняло бы ~250-300 байт
// Запись в файл
try (FileOutputStream fos = new FileOutputStream("book.pb")) {
book.writeTo(fos);
}
// Десериализация из файла
try (FileInputStream fis = new FileInputStream("book.pb")) {
Book restored = Book.parseFrom(fis);
System.out.println("Restored: " + restored.getTitle() + " by " + restored.getAuthor());
}
// Десериализация из байтового массива
Book fromBytes = Book.parseFrom(bytes);
}
}
Метод
toByteArray() сериализует сообщение в компактный byte[]. Метод parseFrom() выполняет обратную операцию. Обратите внимание: parseFrom() не выбрасывает checked-исключений при невалидном формате — вместо этого выбрасывается InvalidProtocolBufferException (unchecked, наследник IOException в старых версиях, но в proto3 runtime это RuntimeException).Версионирование и эволюция схемы
Одно из ключевых преимуществ Protobuf — встроенная поддержка backward и forward compatibility через правила изменения схемы.
Backward compatibility (обратная совместимость) — это свойство системы, при котором новая версия может корректно обрабатывать данные, созданные старой версией. Forward compatibility (прямая совместимость) — это свойство, при котором старая версия может корректно обрабатывать данные, созданные новой версией.
Правила безопасного изменения схемы
Добавление полей: можно добавлять новые поля с новыми тегами. Старый код проигнорирует неизвестные теги (wire parser пропускает их по length-delimiter или varint-размеру). Новый код получит default value для отсутствующих в старом сообщении полей.
Удаление полей: поле можно удалить, но его тег нельзя повторно использовать (чтобы избежать коллизий со старыми сообщениями, где это поле ещё может присутствовать). Рекомендуется помечать удалённые поля как
reserved.Изменение типа: нельзя менять wire type поля (например, int32 на string), так как это сломает бинарный парсинг. Но можно менять тип в пределах совместимых wire type: int32 на int64 (wire type 0), string на bytes (wire type 2).
Изменение имени: имя поля можно менять свободно, так как в wire format хранится только тег, а не имя.
Default values: в proto3 поля всегда имеют zero-value по умолчанию (0 для чисел, пустая строка, false, первое значение enum). Это означает, что невозможно отличить "поле не было установлено" от "поле было установлено в значение по умолчанию". Для явного контроля наличия поля в proto3 используется обёртка
google.protobuf.BoolValue, Int32Value и т.д. (well-known types).message BookV2 {
string title = 1;
string author = 2;
int32 year = 3;
// Новое поле — старый код проигнорирует тег 8
string isbn = 8;
// Зарезервированные теги и имена — нельзя использовать повторно
reserved 4, 5, 6;
reserved "price", "available";
}Преимущества Protobuf
Компактность
Бинарное представление Protobuf значительно меньше JSON. Для сообщения
Book из примера:JSON с отступами: ~280 байт.
JSON minified: ~200 байт.
Protobuf binary: ~55-65 байт (в зависимости от длины строк).
Экономия достигается за счёт:
Отсутствия имён полей в бинарном потоке (только числовые теги).
Varint-кодирования малых целых чисел.
Packed repeated-полей.
Отсутствия запятых, скобок, кавычек.
Для высоконагруженных систем экономия 70-80% трафика критична. В микросервисной архитектуре, где сервисы обмениваются через сеть, снижение размера сообщений прямо пропорционально снижению задержек (latency) и стоимости сетевой инфраструктуры.
Latency — это задержка между отправкой запроса и получением ответа. В распределённых системах latency складывается из времени сериализации, передачи по сети, десериализации и обработки. Компактные форматы снижают время передачи и десериализации.
Скорость
Protobuf парсится быстрее JSON по нескольким причинам:
Отсутствие текстового разбора: не нужно искать кавычки, запятые, скобки, обрабатывать escape-последовательности. Парсер читает байты последовательно, используя заранее известную структуру.
Нет рефлексии при десериализации: сгенерированный код содержит жёстко заданные инструкции вида "тег 1 — строка, записать в поле title". В Jackson для JSON требуется рефлексия или runtime-генерация bytecode.
Zero-copy для строк:
ByteString и LazyStringList позволяют избежать копирования строк при передаче между слоями приложения.Предсказуемость аллокаций: размер сообщения известен до сериализации (вызов
getSerializedSize()), что позволяет выделить точный byte[] без переаллокаций.В бенчмарках (JMH — Java Microbenchmark Harness) сериализация/десериализация Protobuf в Java обычно в 3–10 раз быстрее Jackson JSON для типичных сообщений, и в 20+ раз быстрее для сложных вложенных структур.
JMH (Java Microbenchmark Harness) — это фреймворк от OpenJDK для написания корректных микробенчмарков Java-кода. Он решает проблемы JIT-оптимизаций, warmup-фазы и статистической значимости результатов.
Строгая типизация
.proto файл является контрактом. Компилятор protoc гарантирует, что сгенерированный код соответствует схеме. Невозможно случайно записать строку в числовое поле — это будет ошибкой компиляции, а не runtime-ошибкой парсинга. Это снижает количество ошибок интеграции между сервисами.Кроссплатформенность и межъязыковая совместимость
Один .proto файл компилируется в C++, Java, Python, Go, C#, JavaScript, Ruby, Objective-C, PHP, Dart, Kotlin и другие языки. Это означает, что бэкенд на Java, мобильное приложение на Kotlin и фронтенд на JavaScript могут использовать одну и ту же схему данных, гарантируя совместимость на уровне wire format.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
👍4
Интеграция с gRPC
gRPC использует .proto файлы для определения не только сообщений, но и сервисов с методами:
Недостатки Protobuf
Нечитаемость для человека
Бинарный wire format не поддаётся чтению без специальных инструментов. Для отладки требуется
Требование схемы и кодогенерации
Каждое изменение структуры данных требует:
Изменения .proto файла.
Перекомпиляции всех зависимых проектов.
Редеплоя артефактов.
Это создаёт friction в agile-разработке, где структуры данных часто меняются на ранних этапах. JSON позволяет добавить новое поле в ответ API, не трогая клиентский код. В Protobuf это тоже возможно (благодаря forward compatibility), но требует дисциплины: новое поле должно получить новый тег, а клиент должен быть перекомпилирован, если он хочет работать с новым полем типобезопасно.
Кривая обучения
Разработчику нужно освоить:
Синтаксис .proto и семантику proto3.
Правила версионирования и reserved fields.
Различия между scalar types и их wire-типами (int32 vs sint32 vs fixed32).
Особенности generated code (Builder pattern, immutability, default values).
Интеграцию protoc в build pipeline (Maven, Gradle, Bazel).
Ограниченные типы данных
Protobuf не поддерживает напрямую:
Большие целые числа без знака больше 64 бит (нет nativе BigInteger).
Даты и время (есть well-known type
Полиморфизм (есть
Графы с циклическими ссылками (Protobuf предназначен для деревьев, не для произвольных графов).
Сравнение Protobuf и JSON
По скорости
Protobuf значительно быстрее при сериализации и десериализации.
Jackson JSON требует:
Парсинга текстовой строки посимвольно.
Построения промежуточного дерева (
Обработки escape-последовательностей и Unicode.
Динамического поиска полей по имени.
Protobuf использует:
Последовательное чтение байтов с известной структурой.
Прямую запись в поля объекта без рефлексии (сгенерированный код).
Простые битовые операции для varint и zigzag.
Предсказуемость аллокаций: размер сообщения известен до сериализации (вызов
В типичных микросервисных нагрузках разница составляет 3–10x в пользу Protobuf. При этом стоит учитывать, что в современных JVM с JIT-компиляцией Jackson может достигать высокой производительности после warmup-фазы, но Protobuf остаётся предсказуемо быстрее из-за отсутствия текстового парсинга.
По размеру
Protobuf обычно в 2–5 раз компактнее minified JSON и в 5–10 раз компактнее pretty-printed JSON. Ключевые факторы:
В JSON имя каждого поля повторяется в каждом сообщении. В Protobuf имя заменено на 1–2 байта тега.
Числа в JSON — ASCII-текст.
Булевы значения в JSON:
JSON требует запятых, скобок, кавычек. Protobuf не имеет разделителей между полями.
По строгости
Protobuf требует схему на этапе компиляции.
Это:
Плюс: ошибки типов ловятся на этапе компиляции, а не в runtime.
Плюс: контракт между сервисами формализован и версионируется.
Минус: меньшая гибкость для ad-hoc структур и прототипирования.
JSON не требует схемы. Это:
Плюс: быстрое прототипирование и гибкость.
Минус: runtime-ошибки при несовпадении типов или отсутствии полей.
Минус: неявный контракт, который легко нарушить.
По отладке и observability
JSON превосходит Protobuf в читаемости. Логи, перехваченные HTTP-запросы, сообщения в Kafka можно сразу прочитать. Protobuf требует инструментов:
По интеграции с вебом
JSON нативно поддерживается браузерами и JavaScript.
Protobuf в браузере требует:
Использования protobuf-js для сериализации/десериализации.
Передачи бинарных данных через ArrayBuffer или base64.
gRPC-Web для взаимодействия с gRPC-сервисами (требует прокси, например Envoy).
Поэтому для публичных API, потребляемых браузерными клиентами, JSON остаётся стандартом. Protobuf доминирует во внутреннем межсервисном взаимодействии (backend-to-backend).
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
gRPC — это фреймворк удалённого вызова процедур (RPC — Remote Procedure Call), разработанный Google и построенный поверх HTTP/2 с Protobuf как форматом сериализации сообщений.
gRPC использует .proto файлы для определения не только сообщений, но и сервисов с методами:
service LibraryService {
rpc GetBook(GetBookRequest) returns (Book);
rpc ListBooks(ListBooksRequest) returns (stream Book);
rpc CreateBooks(stream Book) returns (CreateBooksResponse);
}RPC (Remote Procedure Call) — это парадигма межпроцессного взаимодействия, при которой вызов удалённой функции выглядит как вызов локальной. gRPC генерирует клиентский stub и серверный skeleton из .proto файла, скрывая детали сетевого взаимодействия.
Stub — это сгенерированный клиентский код, который предоставляет интерфейс, идентичный серверному сервису, но реализующий сетевое взаимодействие под капотом. Skeleton — серверная заглушка, принимающая сетевые запросы и делегирующая их реальной реализации.
Недостатки Protobuf
Нечитаемость для человека
Бинарный wire format не поддаётся чтению без специальных инструментов. Для отладки требуется
protoc --decode или специализированные декодеры. Это усложняет отладку сетевых проблем: нельзя просто посмотреть перехваченный TCP-пакет в текстовом виде, как с JSON.Требование схемы и кодогенерации
Каждое изменение структуры данных требует:
Изменения .proto файла.
Перекомпиляции всех зависимых проектов.
Редеплоя артефактов.
Это создаёт friction в agile-разработке, где структуры данных часто меняются на ранних этапах. JSON позволяет добавить новое поле в ответ API, не трогая клиентский код. В Protobuf это тоже возможно (благодаря forward compatibility), но требует дисциплины: новое поле должно получить новый тег, а клиент должен быть перекомпилирован, если он хочет работать с новым полем типобезопасно.
Кривая обучения
Разработчику нужно освоить:
Синтаксис .proto и семантику proto3.
Правила версионирования и reserved fields.
Различия между scalar types и их wire-типами (int32 vs sint32 vs fixed32).
Особенности generated code (Builder pattern, immutability, default values).
Интеграцию protoc в build pipeline (Maven, Gradle, Bazel).
Ограниченные типы данных
Protobuf не поддерживает напрямую:
Большие целые числа без знака больше 64 бит (нет nativе BigInteger).
Даты и время (есть well-known type
Timestamp, но это message, а не примитив).Полиморфизм (есть
oneof и Any, но они более ограничены, чем наследование в ООП).Графы с циклическими ссылками (Protobuf предназначен для деревьев, не для произвольных графов).
Сравнение Protobuf и JSON
По скорости
Protobuf значительно быстрее при сериализации и десериализации.
Jackson JSON требует:
Парсинга текстовой строки посимвольно.
Построения промежуточного дерева (
JsonNode) или рефлексии для маппинга на POJO.Обработки escape-последовательностей и Unicode.
Динамического поиска полей по имени.
Protobuf использует:
Последовательное чтение байтов с известной структурой.
Прямую запись в поля объекта без рефлексии (сгенерированный код).
Простые битовые операции для varint и zigzag.
Предсказуемость аллокаций: размер сообщения известен до сериализации (вызов
getSerializedSize()), что позволяет выделить точный byte[] без переаллокаций.В типичных микросервисных нагрузках разница составляет 3–10x в пользу Protobuf. При этом стоит учитывать, что в современных JVM с JIT-компиляцией Jackson может достигать высокой производительности после warmup-фазы, но Protobuf остаётся предсказуемо быстрее из-за отсутствия текстового парсинга.
JIT (Just-In-Time compilation) — это компиляция байткода Java в нативный машинный код во время выполнения. HotSpot JVM компилирует часто выполняемые методы в оптимизированный машинный код, что значительно ускоряет их выполнение после периода "разогрева" (warmup).
По размеру
Protobuf обычно в 2–5 раз компактнее minified JSON и в 5–10 раз компактнее pretty-printed JSON. Ключевые факторы:
В JSON имя каждого поля повторяется в каждом сообщении. В Protobuf имя заменено на 1–2 байта тега.
Числа в JSON — ASCII-текст.
12345 занимает 5 байт. В Protobuf 12345 как varint занимает 2 байта.Булевы значения в JSON:
true — 4 байта. В Protobuf: 1 байт (тег + значение).JSON требует запятых, скобок, кавычек. Protobuf не имеет разделителей между полями.
По строгости
Protobuf требует схему на этапе компиляции.
Это:
Плюс: ошибки типов ловятся на этапе компиляции, а не в runtime.
Плюс: контракт между сервисами формализован и версионируется.
Минус: меньшая гибкость для ad-hoc структур и прототипирования.
JSON не требует схемы. Это:
Плюс: быстрое прототипирование и гибкость.
Минус: runtime-ошибки при несовпадении типов или отсутствии полей.
Минус: неявный контракт, который легко нарушить.
По отладке и observability
JSON превосходит Protobuf в читаемости. Логи, перехваченные HTTP-запросы, сообщения в Kafka можно сразу прочитать. Protobuf требует инструментов:
protoc --decode, gRPC reflection, специализированные декодеры в Wireshark. В современных системах этот недостаток часто компенсируется: gRPC сервисы экспортируют схемы через reflection, а observability-платформы (Jaeger, Zipkin) умеют декодировать Protobuf на лету.Observability (наблюдаемость) — это свойство системы, позволяющее понимать её внутреннее состояние по внешним выходным данным: метрикам, логам и трассировкам. В распределённых системах observability критична для диагностики проблем.
По интеграции с вебом
JSON нативно поддерживается браузерами и JavaScript.
Protobuf в браузере требует:
Использования protobuf-js для сериализации/десериализации.
Передачи бинарных данных через ArrayBuffer или base64.
gRPC-Web для взаимодействия с gRPC-сервисами (требует прокси, например Envoy).
Поэтому для публичных API, потребляемых браузерными клиентами, JSON остаётся стандартом. Protobuf доминирует во внутреннем межсервисном взаимодействии (backend-to-backend).
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
👍4
Путь байтов в памяти JVM при работе с Protobuf
1. Генерация кода и загрузка классов
.proto файл компилируется
В отличие от JSON, где парсер (Jackson) использует рефлексию для анализа POJO-классов во время выполнения, Protobuf-классы уже содержат весь необходимый код сериализации/десериализации. Это означает, что нет runtime-аллокаций объектов
2. Создание сообщения: Builder и аллокации
При вызове
Создаётся объект
Каждый вызов
При вызове
Важно:
3. Сериализация: от объекта к байтам
Вызов
Сначала вычисляется размер сериализованного сообщения через
Выделяется
Возвращается
Если
4. Десериализация: от байтов к объекту
Вызов
Создаётся
Парсер читает байты последовательно. Для каждого поля:
Читает тег (varint) — определяет field number и wire type.
По field number находит соответствующее поле в сгенерированном коде (switch по номеру).
Читает значение: для varint — декодирует число; для length-delimited — читает длину, затем
Устанавливает значение в Builder.
После прочтения всех полей вызывается
5. Работа GC с Protobuf-объектами
Protobuf спроектирован с учётом минимизации давления на GC:
Immutable объекты: после создания
Отсутствие рефлексии: нет постоянного создания объектов
Повторное использование Builder: в высоконагруженных системах можно использовать пул Builder-ов, чтобы избежать аллокаций на каждое сообщение. Однако стандартный generated code не поддерживает пулинг из коробки — это требует ручной реализации или использования фреймворков вроде grpc-java, который переиспользует буферы.
ByteString и пул строк:
Large messages: если сообщение содержит большое поле
6. Вложенные сообщения и глубина рекурсии
Вложенные сообщения в Protobuf создают дерево объектов в куче. Каждый уровень вложенности — это отдельная аллокация. Для глубоко вложенных сообщений (глубина 10+) это создаёт давление на Eden.
7. Сравнение аллокаций: Protobuf vs JSON
Для сообщения
Protobuf сериализация: аллокации =
Jackson JSON сериализация: аллокации =
Protobuf десериализация: аллокации = CodedInputStream + Book.Builder + Book + ByteString для каждой строки.
Jackson JSON десериализация: аллокации = JsonParser + JsonNode дерево или POJO + String для каждого поля + объекты рефлексии (при первом использовании класса).
Protobuf создаёт в 2–5 раз меньше объектов в куче при сериализации/десериализации, что напрямую снижает частоту Minor GC.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
1. Генерация кода и загрузка классов
.proto файл компилируется
protoc в .java файлы, которые затем компилируются javac в .class файлы. При загрузке классов JVM (например, Book.class) класс попадает в Metaspace — область памяти для метаданных классов. Book.class, Book.Builder.class, BookOrBuilder.class и внутренние классы занимают Metaspace. Эти классы остаются там до выгрузки ClassLoader.В отличие от JSON, где парсер (Jackson) использует рефлексию для анализа POJO-классов во время выполнения, Protobuf-классы уже содержат весь необходимый код сериализации/десериализации. Это означает, что нет runtime-аллокаций объектов
Method, Field, Constructor при каждой операции — всё решено на этапе кодогенерации.2. Создание сообщения: Builder и аллокации
Book book = Book.newBuilder()
.setTitle("Clean Code")
.setYear(2008)
.build();
При вызове
Book.newBuilder():Создаётся объект
Book.Builder в Young Generation (Eden). Builder содержит мутабельные поля, соответствующие всем полям сообщения.Каждый вызов
setTitle() создаёт внутреннее представление строки. В Protobuf строки хранятся как ByteString — обёртка над byte[], содержащая UTF-8 байты. При передаче Java-String происходит кодирование UTF-16 (внутреннее представление Java-строки) в UTF-8. Это создаёт временный byte[] в EdenByteString — это immutable обёртка над массивом байтов в Protobuf Java runtime. Она аналогична String, но для сырых байтов, и предоставляет zero-copy операции (например, substring() без копирования массива).
При вызове
build() Builder создаёт финальный immutable объект Book. Это новая аллокация в Eden. Builder проверяет валидность (например, required-поля в proto2; в proto3 такой проверки нет) и копирует состояние в Book. После build() объект Builder становится мусором (если на него не осталось ссылок).Важно:
Book — immutable. Все его поля либо примитивы (хранятся в самом объекте), либо ссылки на immutable объекты (ByteString, другие сообщения). Это означает, что Book безопасно передавать между потоками без синхронизации.3. Сериализация: от объекта к байтам
Вызов
book.toByteArray():Сначала вычисляется размер сериализованного сообщения через
getSerializedSize(). Этот метод рекурсивно обходит все поля, суммируя размеры тегов, значений и length-delimiters. Для вложенных сообщений вызывается их getSerializedSize(). Этот проход не создаёт новых объектов, только читает поля и выполняет арифметику на стеке.Выделяется
byte[] точного размера в Young Generation (Eden). Это единственная аллокация для всей сериализации (не считая временных объектов для строк, если они ещё не закодированы).CodedOutputStream оборачивает byte[] и пишет в него wire format. Каждое поле кодируется напрямую: примитивы через битовые операции, строки через копирование ByteString/byte[] в целевой массив.Возвращается
byte[]. Этот массив — единственный объект, который покидает метод сериализации. Все промежуточные вычисления происходят на стеке или с использованием существующих объектов.Если
byte[] передаётся в сетевой слой (Netty, gRPC), он может быть обёрнут в ByteBuf — абстракцию над байтовым буфером в Netty. ByteBuf может использовать прямую (direct) память вне кучи JVM, что позволяет передать данные в сокет через zero-copy без участия GC.Direct memory (прямая память, off-heap) — это область нативной памяти вне кучи JVM, выделяемая через ByteBuffer.allocateDirect(). Данные в direct memory не подлежат сборке мусора JVM и могут передаваться напрямую в системные вызовы ОС (например, запись в сокет), минуя копирование из кучи.
4. Десериализация: от байтов к объекту
Вызов
Book.parseFrom(byte[] data):Создаётся
CodedInputStream — лёгкий объект, оборачивающий byte[] и отслеживающий позицию чтения. Аллокация в Eden.parseFrom создаёт Book.Builder (аллокация в Eden).Парсер читает байты последовательно. Для каждого поля:
Читает тег (varint) — определяет field number и wire type.
По field number находит соответствующее поле в сгенерированном коде (switch по номеру).
Читает значение: для varint — декодирует число; для length-delimited — читает длину, затем
byte[], оборачивает в ByteString.Устанавливает значение в Builder.
После прочтения всех полей вызывается
build(), который создаёт immutable Book в Eden.CodedInputStream и Builder становятся мусором. Если сообщение большое и парсинг длительный, Builder может пережить Minor GC и попасть в Survivor Space.5. Работа GC с Protobuf-объектами
Protobuf спроектирован с учётом минимизации давления на GC:
Immutable объекты: после создания
Book не изменяется. GC не тратит время на отслеживание изменений состояния.Отсутствие рефлексии: нет постоянного создания объектов
Field, Method, аннотаций. Всё статически скомпилировано.Повторное использование Builder: в высоконагруженных системах можно использовать пул Builder-ов, чтобы избежать аллокаций на каждое сообщение. Однако стандартный generated code не поддерживает пулинг из коробки — это требует ручной реализации или использования фреймворков вроде grpc-java, который переиспользует буферы.
Object pooling (пулинг объектов) — это паттерн, при котором созданные объекты не уничтожаются, а возвращаются в пул для повторного использования. Это снижает нагрузку на GC, но требует аккуратного управления состоянием объектов.
ByteString и пул строк:
ByteString хранит byte[] напрямую. Если одна и та же строка встречается в тысячах сообщений, каждое сообщение содержит свой ByteString со своим byte[]. В отличие от Java-String, ByteString не интернируется автоматически. Для часто повторяющихся строковых значений можно использовать ByteString.copyFromUtf8(string).toStringUtf8() с ручным кэшированием, но это нестандартная практика.Large messages: если сообщение содержит большое поле
bytes (например, изображение 10 МБ), соответствующий byte[] создаётся в куче. Если это repeated поле с множеством больших объектов, они могут быстро заполнить Old Generation. В таких случаях используются потоковые API или разбиение на чанки.6. Вложенные сообщения и глубина рекурсии
Вложенные сообщения в Protobuf создают дерево объектов в куче. Каждый уровень вложенности — это отдельная аллокация. Для глубоко вложенных сообщений (глубина 10+) это создаёт давление на Eden.
CodedInputStream имеет лимит на глубину вложенности (по умолчанию 100), чтобы предотвратить stack overflow и утечки памяти при парсинге злонамеренных сообщений.7. Сравнение аллокаций: Protobuf vs JSON
Для сообщения
Book из примера:Protobuf сериализация: аллокации =
Book.Builder (если ещё не создан) + byte[] результата. Временных объектов минимум.Jackson JSON сериализация: аллокации =
StringWriter или byte[] + промежуточные char[]/byte[] для кодирования + объекты JsonGenerator. При сериализации в String — объект String + внутренний byte[].Protobuf десериализация: аллокации = CodedInputStream + Book.Builder + Book + ByteString для каждой строки.
Jackson JSON десериализация: аллокации = JsonParser + JsonNode дерево или POJO + String для каждого поля + объекты рефлексии (при первом использовании класса).
Protobuf создаёт в 2–5 раз меньше объектов в куче при сериализации/десериализации, что напрямую снижает частоту Minor GC.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
👍4
Продвинутые возможности proto3
Well-known types
Proto3 предоставляет стандартные типы в пакете
Maps
Proto3 поддерживает нативные ассоциативные массивы:
На уровне wire format map кодируется как repeated message, где каждый элемент — message с двумя полями: key и value. В Java маппится на Map<String, Book>.
Oneof
oneof — это конструкция, гарантирующая, что только одно из перечисленных полей может быть установлено в один момент времени. Аналог union в C.
В Java oneof генерирует enum PayloadCase и методы hasBook(), hasError(), getPayloadCase().
Custom options
Protobuf позволяет определять собственные опции через расширения:
Эти опции не влияют на wire format, но доступны через reflection API Protobuf для генерации валидаторов, документации и т.д.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
Well-known types
Proto3 предоставляет стандартные типы в пакете
google.protobuf:Timestamp — точка во времени (секунды + наносекунды с эпохи Unix).Duration — временной интервал.Any — контейнер для произвольного сообщения (типа Object в Java).Empty — пустое сообщение для методов без параметров/результата.Struct, Value, ListValue — динамически типизированные структуры (аналог JSON-объектов).Wrapper types: Int32Value, StringValue, BoolValue — обёртки для скалярных типов, позволяющие отличать отсутствие поля от значения по умолчанию.import "google/protobuf/timestamp.proto";
message Event {
string name = 1;
google.protobuf.Timestamp occurred_at = 2;
}
Maps
Proto3 поддерживает нативные ассоциативные массивы:
message Library {
map<string, Book> books_by_isbn = 1;
}На уровне wire format map кодируется как repeated message, где каждый элемент — message с двумя полями: key и value. В Java маппится на Map<String, Book>.
Oneof
oneof — это конструкция, гарантирующая, что только одно из перечисленных полей может быть установлено в один момент времени. Аналог union в C.
message Result {
oneof payload {
Book book = 1;
string error = 2;
}
}В Java oneof генерирует enum PayloadCase и методы hasBook(), hasError(), getPayloadCase().
Custom options
Protobuf позволяет определять собственные опции через расширения:
extend google.protobuf.FieldOptions {
string validation_regex = 50001;
}
message User {
string email = 1 [(validation_regex) = "^[a-z]+@[a-z]+\\.[a-z]+$"];
}Эти опции не влияют на wire format, но доступны через reflection API Protobuf для генерации валидаторов, документации и т.д.
#Java #для_новичков #beginner #IO #NIO #Serialize #Protobuf
👍4
[Совет по Java #070]
Тема:
Проблема:
Однако метод
Решение: Для получения элементов в порядке приоритета всегда используйте метод
Для отладки и логирования используйте
Объяснение:
Это свойство кучи обеспечивает быстрый доступ к минимальному элементу за O(1) и извлечение за O(log n). Однако хранение в виде кучи не подразумевает полной сортировки массива: порядок элементов может быть любым, главное, чтобы выполнялось условие кучи (например, каждый родитель меньше своих детей).
Итератор обходит массив по индексам, следуя физическому расположению, которое не сохраняет глобальный порядок. Поэтому единственный способ получить элементы в порядке приоритета — многократно вызывать
#Java #советы
Тема:
PriorityQueue не гарантирует порядок при итерации — только при извлечении (poll()).Проблема:
PriorityQueue — это реализация очереди на основе бинарной кучи (heap). Она упорядочивает элементы согласно их естественному порядку или переданному компаратору, но порядок гарантируется только при извлечении элементов через poll(), remove() или peek() (который показывает минимальный элемент). Однако метод
iterator() и все операции обхода (например, forEach, toArray()) не следуют порядку приоритета. Итератор проходит по внутреннему массиву кучи в том порядке, в котором элементы физически расположены в памяти, а этот порядок не является отсортированным. Многие разработчики ошибочно полагают, что итерация по PriorityQueue вернёт элементы в порядке возрастания приоритета, и используют её в циклах, не догадываясь, что получают случайный порядок. Это приводит к логическим ошибкам, особенно при выводе содержимого, сериализации или агрегации данных.Решение: Для получения элементов в порядке приоритета всегда используйте метод
poll() в цикле, который удаляет и возвращает наименьший (или наибольший, в зависимости от компаратора) элемент. Если нужно просто просмотреть элементы без удаления, скопируйте очередь в массив и отсортируйте его, либо создайте новую PriorityQueue и последовательно извлекайте элементы. Для отладки и логирования используйте
toArray() и затем сортируйте, либо преобразуйте в список и применяйте Collections.sort(). Никогда не полагайтесь на порядок итератора, так как он специфичен для реализации и может меняться между версиями JDK.import java.util.*;
public class PriorityQueueOrder {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(5);
pq.add(1);
pq.add(3);
pq.add(2);
pq.add(4);
//Итератор не гарантирует порядок
System.out.print("Итерация (неупорядоченно): ");
for (Integer i : pq) {
System.out.print(i + " "); // Может вывести 1, 2, 3, 4, 5, но это не гарантировано
}
System.out.println();
//poll() выдает элементы в правильном порядке
System.out.print("Извлечение через poll(): ");
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " "); // 1, 2, 3, 4, 5
}
System.out.println();
// Восстанавливаем очередь
pq.addAll(Arrays.asList(5, 1, 3, 2, 4));
//Просмотр без удаления: копия и сортировка
List<Integer> sorted = new ArrayList<>(pq);
Collections.sort(sorted);
System.out.println("Копия + сортировка: " + sorted);
//Альтернатива: извлечение с сохранением (создаем копию)
PriorityQueue<Integer> copy = new PriorityQueue<>(pq);
List<Integer> inOrder = new ArrayList<>();
while (!copy.isEmpty()) {
inOrder.add(copy.poll());
}
System.out.println("Из копии: " + inOrder);
}
}
Объяснение:
PriorityQueue хранит элементы в массиве, где позиция родителя всегда меньше (или больше) дочерних элементов согласно компаратору. Это свойство кучи обеспечивает быстрый доступ к минимальному элементу за O(1) и извлечение за O(log n). Однако хранение в виде кучи не подразумевает полной сортировки массива: порядок элементов может быть любым, главное, чтобы выполнялось условие кучи (например, каждый родитель меньше своих детей).
Итератор обходит массив по индексам, следуя физическому расположению, которое не сохраняет глобальный порядок. Поэтому единственный способ получить элементы в порядке приоритета — многократно вызывать
poll(), который удаляет корень и восстанавливает свойство кучи. При проектировании API, возвращающего PriorityQueue, обязательно документируйте, что итерация не гарантирует упорядоченность, и предоставляйте методы для извлечения отсортированных данных. #Java #советы
👍4
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 3. Сериализация и форматы обмена
Apache Avro — бинарный формат сериализации с динамической схемой
Apache Avro — это формат сериализации данных и протокол удалённого вызова процедур (RPC), разработанный в рамках проекта Apache Hadoop. Avro был создан Дугом Каттингом в 2009 году как ответ на необходимость иметь компактный, быстрый и расширяемый формат для хранения и передачи больших данных в распределённых системах. На момент 2026 года актуальная версия спецификации — Avro 1.12.x, а проект остаётся одним из столпов экосистемы Apache Big Data.
В отличие от Protocol Buffers и Thrift, где схема является внешним контрактом, разделяемым между producer и consumer, Avro следует принципу self-describing (самоописывающихся) данных: каждый блок данных несёт в себе свою схему или ссылку на неё. Это фундаментальное архитектурное решение определяет все сильные и слабые стороны Avro.
Архитектура Avro: схема + данные
Avro разделяет понятия схемы (schema) и данных (datum). Схема описывает структуру данных на языке JSON. Данные сериализуются в бинарный формат, который интерпретируется исключительно через схему. Без схемы бинарный поток Avro — это бессмысленная последовательность байтов, так как в нём отсутствуют теги полей и информация о типах.
Это кардинально отличается от Protobuf, где wire format содержит теги (field numbers), и парсер может прочитать сообщение, зная только .proto файл. В Avro парсер должен иметь схему, чтобы понять, где заканчивается одно поле и начинается другое. Например, для строки Avro записывает длину в varint, затем байты. Для массива — блоки элементов с указанием размера. Но чтобы понять, что следующее значение — это строка, а не int, нужна схема.
Почему это важно
Такой подход даёт две ключевые возможности:
Компактность: в бинарном потоке нет никаких метаданных на уровне полей — ни имён, ни тегов, ни типов. Это делает Avro ещё компактнее Protobuf для многих сценариев.
Динамическая типизация: любой потребитель, получивший данные вместе со схемой, может их десериализовать без предварительно сгенерированных классов.
Но есть и цена: схема должна быть доступна при чтении. В долгосрочном хранении (HDFS, S3) схема обычно записывается в заголовок файла. В потоковой передаче (Kafka) схема регистрируется в Schema Registry и передаётся по ссылке (ID схемы), а не целиком.
Схема Avro
Схема Avro — это JSON-документ, описывающий тип данных. Avro поддерживает примитивные типы, сложные типы и логические типы.
Примитивные типы
Сложные типы
Пример схемы
Ключевые элементы:
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
Глава 3. Сериализация и форматы обмена
Apache Avro — бинарный формат сериализации с динамической схемой
Apache Avro — это формат сериализации данных и протокол удалённого вызова процедур (RPC), разработанный в рамках проекта Apache Hadoop. Avro был создан Дугом Каттингом в 2009 году как ответ на необходимость иметь компактный, быстрый и расширяемый формат для хранения и передачи больших данных в распределённых системах. На момент 2026 года актуальная версия спецификации — Avro 1.12.x, а проект остаётся одним из столпов экосистемы Apache Big Data.
В отличие от Protocol Buffers и Thrift, где схема является внешним контрактом, разделяемым между producer и consumer, Avro следует принципу self-describing (самоописывающихся) данных: каждый блок данных несёт в себе свою схему или ссылку на неё. Это фундаментальное архитектурное решение определяет все сильные и слабые стороны Avro.
Self-describing data — это данные, которые содержат внутри себя или в непосредственной близости полное описание своей структуры (схему). Это позволяет любому потребителю, не имеющему предварительного знания о формате, корректно интерпретировать содержимое.
Big Data — это термин, обозначающий наборы данных, объём, скорость поступления и разнообразие которых настолько велики, что традиционные инструменты обработки не справляются с ними. Hadoop — это фреймворк для распределённой обработки Big Data на кластерах commodity-оборудования.
Архитектура Avro: схема + данные
Avro разделяет понятия схемы (schema) и данных (datum). Схема описывает структуру данных на языке JSON. Данные сериализуются в бинарный формат, который интерпретируется исключительно через схему. Без схемы бинарный поток Avro — это бессмысленная последовательность байтов, так как в нём отсутствуют теги полей и информация о типах.
Это кардинально отличается от Protobuf, где wire format содержит теги (field numbers), и парсер может прочитать сообщение, зная только .proto файл. В Avro парсер должен иметь схему, чтобы понять, где заканчивается одно поле и начинается другое. Например, для строки Avro записывает длину в varint, затем байты. Для массива — блоки элементов с указанием размера. Но чтобы понять, что следующее значение — это строка, а не int, нужна схема.
Почему это важно
Такой подход даёт две ключевые возможности:
Компактность: в бинарном потоке нет никаких метаданных на уровне полей — ни имён, ни тегов, ни типов. Это делает Avro ещё компактнее Protobuf для многих сценариев.
Динамическая типизация: любой потребитель, получивший данные вместе со схемой, может их десериализовать без предварительно сгенерированных классов.
Но есть и цена: схема должна быть доступна при чтении. В долгосрочном хранении (HDFS, S3) схема обычно записывается в заголовок файла. В потоковой передаче (Kafka) схема регистрируется в Schema Registry и передаётся по ссылке (ID схемы), а не целиком.
Schema Registry — это сервис (например, Confluent Schema Registry), который хранит версии схем Avro, Protobuf и JSON Schema. Producer регистрирует схему и получает уникальный ID. Consumer получает ID вместе с сообщением и запрашивает схему из реестра. Это избавляет от необходимости передавать полную схему в каждом сообщении.
Схема Avro
Схема Avro — это JSON-документ, описывающий тип данных. Avro поддерживает примитивные типы, сложные типы и логические типы.
Примитивные типы
null — отсутствие значения.boolean — true/false.int — 32-битное целое со знаком.long — 64-битное целое со знаком.float — 32-битное IEEE 754 float.double — 64-битное IEEE 754 double.bytes — последовательность байтов.string — строка в UTF-8.Сложные типы
record — именованная коллекция полей (аналог struct или class).enum — именованное множество значений.array — упорядоченная коллекция однотипных элементов.map — ассоциативный массив со строковыми ключами.union — объединение типов, записывается как JSON-массив. Например, ["null", "string"] означает nullable string.fixed — массив байтов фиксированного размера.Пример схемы
{
"type": "record",
"name": "Book",
"namespace": "com.example.library",
"doc": "Описание книги в библиотеке",
"fields": [
{
"name": "title",
"type": "string",
"doc": "Название книги"
},
{
"name": "author",
"type": "string"
},
{
"name": "year",
"type": ["null", "int"],
"default": null
},
{
"name": "price",
"type": "double",
"default": 0.0
},
{
"name": "tags",
"type": {
"type": "array",
"items": "string"
},
"default": []
},
{
"name": "metadata",
"type": {
"type": "map",
"values": "string"
},
"default": {}
}
]
}Ключевые элементы:
type: "record" — объявляет именованную запись.name и namespace — полное имя типа, аналог package + class в Java.doc — документация, которая может генерироваться в Javadoc при кодогенерации.fields — массив полей. Каждое поле имеет name, type и опционально default, doc, order (для сортировки).type: ["null", "int"] — union type, реализующий nullable int. Порядок в union важен: при сериализации Avro записывает индекс типа (varint), затем значение. null имеет индекс 0, int — индекс 1.Union type (объединение типов) — это тип данных, значение которого может принадлежать одному из нескольких указанных типов. В Avro union записывается как массив типов, и при сериализации перед значением записывается индекс выбранного типа.
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
👍4
Динамическая типизация: GenericRecord vs SpecificRecord
Avro предоставляет два основных API для работы с данными: Generic и Specific.
Generic API
Generic API не требует кодогенерации. Данные представляются как
В этом примере
Specific API
Specific API требует кодогенерации: из схемы Avro генерируются Java-классы через
Specific API быстрее Generic, так как доступ к полям идёт через
Версионирование: Reader Schema и Writer Schema
Avro имеет наиболее элегантную среди бинарных форматов систему schema evolution (эволюции схемы). Ключевая концепция — разделение схемы записи (writer schema) и схемы чтения (reader schema).
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
Avro предоставляет два основных API для работы с данными: Generic и Specific.
Generic API
Generic API не требует кодогенерации. Данные представляются как
GenericRecord — динамическая структура, аналогичная Map<String, Object>, но типобезопасная на уровне схемы.import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.io.DecoderFactory;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
public class AvroGenericExample {
// Схема как JSON-строка
private static final String BOOK_SCHEMA = "{"
+ "\"type\":\"record\","
+ "\"name\":\"Book\","
+ "\"namespace\":\"com.example\","
+ "\"fields\":["
+ " {\"name\":\"title\", \"type\":\"string\"},"
+ " {\"name\":\"author\", \"type\":\"string\"},"
+ " {\"name\":\"year\", \"type\":[\"null\",\"int\"], \"default\":null},"
+ " {\"name\":\"price\", \"type\":\"double\", \"default\":0.0}"
+ "]}";
public byte[] serializeBook() throws Exception {
// Парсинг схемы из JSON-строки
Schema schema = new Schema.Parser().parse(BOOK_SCHEMA);
// Создание GenericRecord — динамического контейнера данных
// GenericRecord хранит значения полей в массиве Object[]
GenericRecord book = new GenericData.Record(schema);
book.put("title", "Clean Code");
book.put("author", "Robert C. Martin");
book.put("year", 2008); // Avro автоматически оборачивает в union
book.put("price", 42.50);
// GenericDatumWriter — сериализатор для GenericRecord
// DatumWriter — это интерфейс, абстрагирующий запись Avro-данных
DatumWriter<GenericRecord> writer = new GenericDatumWriter<>(schema);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// BinaryEncoder — кодировщик в бинарный формат Avro
// EncoderFactory — фабрика для создания encoder-ов (бинарных, JSON и др.)
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
// Сериализация: writer читает поля из GenericRecord по схеме
// и записывает их в encoder в бинарном формате
writer.write(book, encoder);
encoder.flush(); // сброс буфера encoder в поток
return out.toByteArray();
}
public GenericRecord deserializeBook(byte[] data) throws Exception {
Schema schema = new Schema.Parser().parse(BOOK_SCHEMA);
// GenericDatumReader — десериализатор
DatumReader<GenericRecord> reader = new GenericDatumReader<>(schema);
ByteArrayInputStream in = new ByteArrayInputStream(data);
// BinaryDecoder — декодировщик из бинарного формата
Decoder decoder = DecoderFactory.get().binaryDecoder(in, null);
// Десериализация: reader читает байты через decoder
// и строит GenericRecord по схеме
return reader.read(null, decoder);
}
}
В этом примере
GenericRecord — это реализация интерфейса IndexedRecord, которая хранит значения полей в массиве Object[]. Поле title имеет позицию 0, author — 1, year — 2 и т.д. Метод put(String name, Object value) ищет позицию поля по имени через схему и записывает значение в массив. Это медленнее прямого доступа к полю, но даёт полную динамичность.Specific API
Specific API требует кодогенерации: из схемы Avro генерируются Java-классы через
avro-tools или Maven-плагин. Сгенерированные классы наследуют org.apache.avro.specific.SpecificRecordBase и предоставляют типизированные геттеры и сеттеры.// Сгенерированный класс (упрощённо)
public class Book extends org.apache.avro.specific.SpecificRecordBase {
private CharSequence title;
private CharSequence author;
private Integer year;
private double price;
// Avro требует конструктор по умолчанию
public Book() {}
// put(int field, Object value) — индексированный доступ
public void put(int field, Object value) {
switch (field) {
case 0: title = (CharSequence) value; break;
case 1: author = (CharSequence) value; break;
case 2: year = (Integer) value; break;
case 3: price = (Double) value; break;
default: throw new AvroRuntimeException("Bad index");
}
}
public Object get(int field) {
switch (field) {
case 0: return title;
case 1: return author;
case 2: return year;
case 3: return price;
default: throw new AvroRuntimeException("Bad index");
}
}
// Типизированные геттеры/сеттеры
public CharSequence getTitle() { return title; }
public void setTitle(CharSequence value) { this.title = value; }
}
Specific API быстрее Generic, так как доступ к полям идёт через
switch по индексу, а не через поиск по имени в HashMap. Однако он требует перекомпиляции при изменении схемы.Версионирование: Reader Schema и Writer Schema
Avro имеет наиболее элегантную среди бинарных форматов систему schema evolution (эволюции схемы). Ключевая концепция — разделение схемы записи (writer schema) и схемы чтения (reader schema).
Writer schema — это схема, которая использовалась при сериализации данных. Reader schema — это схема, которую ожидает потребитель при десериализации. Avro гарантирует совместимость, если reader schema может быть получена из writer schema через набор правил преобразования.
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
👍4
Правила резолюции схем
Avro определяет строгие правила, как reader schema может отличаться от writer schema:
Добавление поля с default: writer записывает без нового поля, reader читает и использует default. Это backward compatible.
Удаление поля с default: writer записывает с полем, reader игнорирует его. Это forward compatible.
Изменение типа: разрешено только между совместимыми типами (например, int -> long). Avro выполняет implicit promotion.
Изменение порядка полей: разрешено — поля идентифицируются по имени, а не по позиции.
Изменение имени поля: эквивалентно удалению старого и добавлению нового. Для сохранения совместимости используются алиасы (
При десериализации Avro сопоставляет поля writer и reader по имени. Поле
Implicit promotion (неявное повышение типа) — это правило Avro, позволяющее читать значение одного типа как другой без потери данных. Например,
Сжатие и блоки
Avro поддерживает сжатие на уровне блоков данных. При записи в файл данные группируются в блоки фиксированного размера (по умолчанию), и каждый блок сжимается независимо.
Codec (кодек) — это алгоритм сжатия/распаковки данных. В Avro кодек применяется к блокам записей, а не к каждой записи отдельно. Это повышает эффективность сжатия за счёт большего объёма данных на входе кодека.
Структура Avro-файла
Avro-файл (Object Container File) имеет следующую структуру:
Magic (4 байта):
Metadata (map): содержит
Sync marker (16 байт): случайная последовательность, разделяющая блоки.
Blocks: последовательность блоков, каждый из которых содержит:
Количество записей в блоке (long).
Размер сжатых данных (long).
Сжатые данные (сериализованные записи).
Sync marker.
Такая структура позволяет эффективно разбивать файлы на части (split) в Hadoop MapReduce: каждый mapper может начать чтение с ближайшего sync marker.
Использование в Kafka
Avro широко используется как формат сообщений в Apache Kafka, особенно в экосистеме Confluent. Интеграция работает через Schema Registry:
В сообщении Kafka с Avro первые 5 байт — служебные: 1 байт magic (
Это означает, что схема не передаётся в каждом сообщении — только 4-байтовый ID. Это критично для производительности: передача полной JSON-схемы (1–5 КБ) в каждом сообщении сделала бы Kafka непригодной для high-throughput сценариев.
Путь байтов в памяти JVM при работе с Avro
1. Парсинг схемы: от JSON к объекту Schema
Когда вы вызываете
JSON-строка схемы — это объект
Результат парсинга — объект
Полное имя типа (
Список полей (
Для каждого поля: имя, позиция, тип, default value, aliases.
Схема хранится в виде дерева объектов в куче.
Объект
2. Создание GenericRecord
При создании
Аллоцируется объект
Ссылку на
Массив
Массив
При вызове
Имя поля
Значение
Utf8 — это класс Avro, реализующий
Если используется
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
Avro определяет строгие правила, как reader schema может отличаться от writer schema:
Добавление поля с default: writer записывает без нового поля, reader читает и использует default. Это backward compatible.
Удаление поля с default: writer записывает с полем, reader игнорирует его. Это forward compatible.
Изменение типа: разрешено только между совместимыми типами (например, int -> long). Avro выполняет implicit promotion.
Изменение порядка полей: разрешено — поля идентифицируются по имени, а не по позиции.
Изменение имени поля: эквивалентно удалению старого и добавлению нового. Для сохранения совместимости используются алиасы (
aliases).// Writer schema (старая версия)
{
"type": "record",
"name": "Book",
"fields": [
{"name": "title", "type": "string"},
{"name": "author", "type": "string"}
]
}
// Reader schema (новая версия)
{
"type": "record",
"name": "Book",
"fields": [
{"name": "title", "type": "string"},
{"name": "author", "type": "string"},
{"name": "year", "type": ["null", "int"], "default": null}
]
}
При десериализации Avro сопоставляет поля writer и reader по имени. Поле
year отсутствует в writer schema, поэтому reader использует default: null. Это работает автоматически на уровне DatumReader, без участия прикладного кода.Implicit promotion (неявное повышение типа) — это правило Avro, позволяющее читать значение одного типа как другой без потери данных. Например,
int может быть прочитан как long, float как double. Обратное направление запрещено, так как может привести к потере точности.Сжатие и блоки
Avro поддерживает сжатие на уровне блоков данных. При записи в файл данные группируются в блоки фиксированного размера (по умолчанию), и каждый блок сжимается независимо.
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.file.CodecFactory;
public void writeCompressedFile(Schema schema, List<GenericRecord> records) throws Exception {
// DataFileWriter — высокоуровневый writer для Avro-файлов с заголовком
DataFileWriter<GenericRecord> writer = new DataFileWriter<>(new GenericDatumWriter<>(schema));
// Установка кодека сжатия
// Snappy — быстрый кодек от Google, оптимизированный на скорость
// Deflate — стандартный zlib-сжатие
// Zstandard (zstd) — современный кодек с лучшим соотношением скорость/сжатие
writer.setCodec(CodecFactory.snappyCodec());
// Схема записывается в заголовок файла
writer.create(schema, new File("books.avro"));
for (GenericRecord record : records) {
writer.append(record);
}
writer.close();
}
Codec (кодек) — это алгоритм сжатия/распаковки данных. В Avro кодек применяется к блокам записей, а не к каждой записи отдельно. Это повышает эффективность сжатия за счёт большего объёма данных на входе кодека.
Структура Avro-файла
Avro-файл (Object Container File) имеет следующую структуру:
Magic (4 байта):
Obj\x01 — идентификатор формата.Metadata (map): содержит
avro.schema (JSON-схема в виде строки) и avro.codec (имя кодека).Sync marker (16 байт): случайная последовательность, разделяющая блоки.
Blocks: последовательность блоков, каждый из которых содержит:
Количество записей в блоке (long).
Размер сжатых данных (long).
Сжатые данные (сериализованные записи).
Sync marker.
Такая структура позволяет эффективно разбивать файлы на части (split) в Hadoop MapReduce: каждый mapper может начать чтение с ближайшего sync marker.
Split — это разбиение большого файла на части для параллельной обработки. В Hadoop InputFormat определяет, как файл разбивается на splits, которые затем обрабатываются отдельными mapper-ами.
Использование в Kafka
Avro широко используется как формат сообщений в Apache Kafka, особенно в экосистеме Confluent. Интеграция работает через Schema Registry:
import io.confluent.kafka.serializers.KafkaAvroSerializer;
import io.confluent.kafka.serializers.KafkaAvroDeserializer;
import org.apache.kafka.clients.producer.ProducerRecord;
public class KafkaAvroProducer {
public void sendBook(KafkaProducer<String, GenericRecord> producer, GenericRecord book) {
// KafkaAvroSerializer автоматически:
// 1. Регистрирует схему в Schema Registry (если ещё не зарегистрирована)
// 2. Получает schema ID
// 3. Записывает в сообщение: [magic byte (1)][schema ID (4)][payload]
ProducerRecord<String, GenericRecord> record =
new ProducerRecord<>("books-topic", book.get("title").toString(), book);
producer.send(record);
}
}
В сообщении Kafka с Avro первые 5 байт — служебные: 1 байт magic (
0x00), 4 байта ID схемы в Schema Registry (big-endian int). Остальное — бинарный payload Avro. Consumer получает ID, запрашивает схему из реестра и десериализует payload.Это означает, что схема не передаётся в каждом сообщении — только 4-байтовый ID. Это критично для производительности: передача полной JSON-схемы (1–5 КБ) в каждом сообщении сделала бы Kafka непригодной для high-throughput сценариев.
Throughput — это пропускная способность системы, измеряемая количеством операций или объёмом данных, обрабатываемых за единицу времени. В Kafka throughput измеряется в сообщениях в секунду или мегабайтах в секунду.
Путь байтов в памяти JVM при работе с Avro
1. Парсинг схемы: от JSON к объекту Schema
Когда вы вызываете
new Schema.Parser().parse(jsonString), происходит следующее:JSON-строка схемы — это объект
String в куче (Heap), обычно в Young Generation (Eden). Если схема загружается из файла, предварительно создаётся byte[] с содержимым файла, который декодируется в String через InputStreamReader.Schema.Parser использует Jackson (да, внутри Avro используется Jackson для парсинга JSON-схемы) для разбора JSON. Это создаёт временные объекты: JsonNode, ArrayNode, ObjectNode, Iterator. Все они аллоцируются в Eden.Результат парсинга — объект
Schema (или Schema.RecordSchema, Schema.ArraySchema и т.д.). Этот объект содержит:Полное имя типа (
name + namespace).Список полей (
List<Schema.Field>).Для каждого поля: имя, позиция, тип, default value, aliases.
Схема хранится в виде дерева объектов в куче.
Объект
Schema обычно кэшируется приложением (singleton) и живёт в Old Generation (Tenured), так как используется многократно. Промежуточные объекты Jackson (дерево JSON) становятся мусором и собираются при следующей Minor GC.2. Создание GenericRecord
GenericRecord record = new GenericData.Record(schema);
record.put("title", "Clean Code");
При создании
GenericData.Record:Аллоцируется объект
Record в Eden. Record содержит:Ссылку на
Schema (уже существует в Old Gen).Массив
Object[] values размером, равным количеству полей. Этот массив создаётся в Eden.Массив
int[] fieldFlags для отслеживания установленных полей.При вызове
put("title", value):Имя поля
"title" — строка в куче. Avro ищет позицию поля через Schema.getField(String). Schema кэширует Map<String, Field> (обычно HashMap), поэтому поиск выполняется быстро, но создаёт временный объект Integer для хэш-кода (автобоксинг).Значение
"Clean Code" — строка Java. Для поля типа string Avro ожидает CharSequence, а не обязательно java.lang.String. Это позволяет использовать Utf8 — оптимизированную реализацию CharSequence от Avro, которая хранит строку как byte[] в UTF-8, избегая двойного хранения (UTF-16 в Java String + UTF-8 в Avro).Utf8 — это класс Avro, реализующий
CharSequence и хранящий строку как изменяемый массив байтов в UTF-8. Это экономит память при сериализации, так как не требуется перекодирование из UTF-16 Java-строки в UTF-8 байты.Если используется
Utf8 вместо String, аллокация String избегается. Однако если прикладной код передаёт String, Avro может либо сконвертировать её в Utf8 (создав новый объект), либо оставить как String (в зависимости от настроек GenericData).#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
👍3
3. Сериализация: от GenericRecord к byte[]
Вызов
Читает значение из
Определяет тип поля из схемы.
Вызывает соответствующий метод
Для строк (
Для union-типов (
После завершения
Временные объекты сериализации — внутренние структуры
4. Десериализация: от byte[] к GenericRecord
Вызов
BinaryDecoder читает байты из InputStream. Для каждого поля схемы:
Читает значение согласно типу поля.
Для строк: читает длину (varint), затем выделяет byte[] нужного размера, читает байты, оборачивает в Utf8.
Для union: читает индекс типа (varint), затем значение соответствующего типа.
Записывает значение в Record.values[pos].
Важная особенность: если reader schema отличается от writer schema, GenericDatumReader выполняет schema resolution (разрешение схемы) на лету. Это означает, что для каждого поля reader сопоставляет его с полем writer по имени, проверяет совместимость типов, применяет promotion (например, int -> long) и default values. Schema resolution требует дополнительных вычислений и создаёт временные объекты (например, ResolvingDecoder), но не требует аллокаций на каждое поле — логика реализована через state machine в decoder.
После десериализации BinaryDecoder становится мусором. Если decoder создан с DecoderFactory.get().binaryDecoder(in, reuse), он может быть переиспользован, избегая аллокации.
5. Работа GC с Avro-объектами
Avro создаёт значительно больше временных объектов, чем Protobuf, но меньше, чем Jackson JSON:
Schema: хранится в Old Generation как singleton. Размер объекта Schema зависит от сложности схемы: простая схема с 5 полями — несколько сотен байт, сложная вложенная схема — несколько килобайт.
GenericRecord: создаётся для каждой записи. Содержит
Utf8: Avro предпочитает
DataFileWriter / DataFileReader: при работе с файлами Avro использует буферизацию.
Object reuse: Avro предоставляет механизмы повторного использования объектов для снижения давления на GC. При чтении файла можно передать существующий
6. Avro в Kafka: путь байтов
При работе с Kafka и Confluent Serializer:
Producer вызывает
Сериализатор извлекает схему из
Проверяет кэш схем (локальный
Если нет — отправляет HTTP-запрос в Schema Registry для регистрации. Получает ID (int).
Сериализует record в
Формирует итоговое сообщение:
Этот
На стороне Consumer:
Читает magic byte и schema ID.
Проверяет локальный кэш схем по ID. Если нет — запрашивает схему из Schema Registry по HTTP.
Использует схему для создания
Десериализует payload в
Кэширование схем на клиенте критично: без него каждое сообщение порождало бы HTTP-запрос к реестру. Кэш — это
Сравнение Avro и Protobuf
Кодогенерация
Protobuf требует кодогенерации: .proto файл компилируется в Java-классы через protoc. Avro предоставляет выбор: Generic API (без кодогенерации) и Specific API (с кодогенерацией через
Оверхед схемы
В Protobuf схема существует только как внешний контракт. В бинарном сообщении нет схемы — только теги полей. В Avro бинарное сообщение без схемы нечитаемо. Поэтому Avro-файлы содержат схему в заголовке, а Kafka-сообщения содержат ID схемы из реестра.
При записи в файл: оверхед схемы амортизируется — одна схема на миллионы записей.
При передаче по сети через Schema Registry: оверхед — 5 байт на сообщение (magic + ID).
Без Schema Registry: пришлось бы передавать полную JSON-схему в каждом сообщении, что сделало бы Avro непригодным для messaging.
Типизация
Protobuf — статически типизирован через сгенерированные классы. Avro Generic — динамически типизирован через
Schema evolution
Avro имеет более мощную и гибкую систему schema evolution благодаря разделению reader/writer schema. Protobuf поддерживает forward/backward compatibility через правила добавления/удаления полей, но не имеет встроенного механизма разрешения различий между схемами на уровне runtime — это должно быть реализовано прикладным кодом.
Производительность
Protobuf быстрее Avro по нескольким причинам:
Сгенерированный код Protobuf содержит жёстко закодированные инструкции сериализации (switch по тегу). Avro Generic использует рефлексию-подобный обход схемы.
Protobuf не требует schema resolution при чтении (если reader/writer schema совпадают). Avro всегда выполняет resolution, даже когда схемы идентичны.
Protobuf использует immutable generated объекты с zero-copy строками. Avro Generic использует mutable
Однако Avro Specific API сопоставим по производительности с Protobuf, а в некоторых сценариях (большие файлы с блоковым сжатием) может быть быстрее за счёт эффективной интеграции с Hadoop/S3.
Экосистема
Protobuf: доминирует в gRPC, микросервисах, мобильной разработке. Поддержка в Kubernetes (etcd), Envoy, многих облачных провайдерах.
Avro: доминирует в Hadoop, Spark, Kafka, data lakes (Delta Lake, Iceberg). Интеграция с Hive, Presto/Trino, Flink.
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
Вызов
writer.write(record, encoder):GenericDatumWriter получает схему и обходит поля в порядке их объявления в схеме. Для каждого поля:Читает значение из
Record.values[pos] через record.get(pos).Определяет тип поля из схемы.
Вызывает соответствующий метод
Encoder: writeString(), writeInt(), writeDouble() и т.д.BinaryEncoder (по умолчанию) пишет данные в буфер. Внутренне Avro использует BufferedBinaryEncoder, который аккумулирует данные в byte[] буфере (обычно 2 КБ) перед записью в целевой OutputStream. Этот буфер создаётся один раз и переиспользуется.Для строк (
writeString): Avro записывает длину в varint, затем байты UTF-8. Если значение — Utf8, байты копируются напрямую. Если String, выполняется перекодирование UTF-16 -> UTF-8, что создаёт временный byte[].Для union-типов (
["null", "int"]): сначала записывается индекс выбранного типа (varint: 0 для null, 1 для int), затем само значение. Для null записывается только индекс 0.После завершения
encoder.flush() копирует оставшиеся данные из внутреннего буфера в OutputStream. Возвращается byte[] (если использовался ByteArrayOutputStream).Временные объекты сериализации — внутренние структуры
BinaryEncoder, возможно временные byte[] для строк — создаются в Eden. BinaryEncoder может быть переиспользован, если передать его вторым аргументом в EncoderFactory.get().binaryEncoder(out, reuse).4. Десериализация: от byte[] к GenericRecord
Вызов
reader.read(null, decoder):GenericDatumReader создаёт новый GenericData.Record (аллокация в Eden + Object[] для значений).BinaryDecoder читает байты из InputStream. Для каждого поля схемы:
Читает значение согласно типу поля.
Для строк: читает длину (varint), затем выделяет byte[] нужного размера, читает байты, оборачивает в Utf8.
Для union: читает индекс типа (varint), затем значение соответствующего типа.
Записывает значение в Record.values[pos].
Важная особенность: если reader schema отличается от writer schema, GenericDatumReader выполняет schema resolution (разрешение схемы) на лету. Это означает, что для каждого поля reader сопоставляет его с полем writer по имени, проверяет совместимость типов, применяет promotion (например, int -> long) и default values. Schema resolution требует дополнительных вычислений и создаёт временные объекты (например, ResolvingDecoder), но не требует аллокаций на каждое поле — логика реализована через state machine в decoder.
После десериализации BinaryDecoder становится мусором. Если decoder создан с DecoderFactory.get().binaryDecoder(in, reuse), он может быть переиспользован, избегая аллокации.
5. Работа GC с Avro-объектами
Avro создаёт значительно больше временных объектов, чем Protobuf, но меньше, чем Jackson JSON:
Schema: хранится в Old Generation как singleton. Размер объекта Schema зависит от сложности схемы: простая схема с 5 полями — несколько сотен байт, сложная вложенная схема — несколько килобайт.
GenericRecord: создаётся для каждой записи. Содержит
Object[] values. Для потоковой обработки (Kafka consumer) каждое сообщение порождает новый GenericRecord. При обработке 10 000 сообщений/сек это 10 000 объектов GenericRecord + 10 000 массивов Object[] в Eden в секунду. Minor GC запускается часто, но эти объекты короткоживущие и собираются быстро.Utf8: Avro предпочитает
Utf8 вместо String для строковых полей. Utf8 — mutable объект, содержащий byte[]. В потоковой обработке GenericDatumReader может переиспользовать один и тот же экземпляр Utf8, если decoder настроен на reuse. Это снижает давление на GC.DataFileWriter / DataFileReader: при работе с файлами Avro использует буферизацию.
DataFileWriter хранит блок записей в памяти до достижения порога синхронизации (sync interval, обычно несколько мегабайт). Этот буфер — byte[] в куче, который может попасть в Old Generation, если файл большой.Object reuse: Avro предоставляет механизмы повторного использования объектов для снижения давления на GC. При чтении файла можно передать существующий
GenericRecord в reader.read(existingRecord, decoder) — в этом случае decoder перезапишет значения в существующем объекте вместо создания нового. Это критично для high-throughput обработки.// Переиспользование GenericRecord для снижения аллокаций
GenericRecord reuse = new GenericData.Record(schema);
while (fileReader.hasNext()) {
// read перезаписывает reuse вместо создания нового объекта
GenericRecord record = fileReader.next(reuse);
process(record);
}
6. Avro в Kafka: путь байтов
При работе с Kafka и Confluent Serializer:
Producer вызывает
KafkaAvroSerializer.serialize(topic, record).Сериализатор извлекает схему из
GenericRecord.getSchema().Проверяет кэш схем (локальный
Map<Schema, Integer>) — если схема уже зарегистрирована, использует кэшированный ID.Если нет — отправляет HTTP-запрос в Schema Registry для регистрации. Получает ID (int).
Сериализует record в
byte[] через GenericDatumWriter + BinaryEncoder.Формирует итоговое сообщение:
[magic(1)][schemaId(4)][payload(N)].Этот
byte[] передаётся в Kafka Producer, который копирует его в ByteBuffer (send buffer) и отправляет брокеру.На стороне Consumer:
KafkaAvroDeserializer получает сообщение.Читает magic byte и schema ID.
Проверяет локальный кэш схем по ID. Если нет — запрашивает схему из Schema Registry по HTTP.
Использует схему для создания
GenericDatumReader.Десериализует payload в
GenericRecord.Кэширование схем на клиенте критично: без него каждое сообщение порождало бы HTTP-запрос к реестру. Кэш — это
ConcurrentHashMap в Old Generation, живущий всё время жизни consumer/producer.Сравнение Avro и Protobuf
Кодогенерация
Protobuf требует кодогенерации: .proto файл компилируется в Java-классы через protoc. Avro предоставляет выбор: Generic API (без кодогенерации) и Specific API (с кодогенерацией через
avro-tools или Maven-плагин). Это делает Avro более гибким для динамических сценариев, где схема известна только во время выполнения.Оверхед схемы
В Protobuf схема существует только как внешний контракт. В бинарном сообщении нет схемы — только теги полей. В Avro бинарное сообщение без схемы нечитаемо. Поэтому Avro-файлы содержат схему в заголовке, а Kafka-сообщения содержат ID схемы из реестра.
При записи в файл: оверхед схемы амортизируется — одна схема на миллионы записей.
При передаче по сети через Schema Registry: оверхед — 5 байт на сообщение (magic + ID).
Без Schema Registry: пришлось бы передавать полную JSON-схему в каждом сообщении, что сделало бы Avro непригодным для messaging.
Типизация
Protobuf — статически типизирован через сгенерированные классы. Avro Generic — динамически типизирован через
GenericRecord. Avro Specific — статически типизирован, но сгенерированные классы менее удобны, чем в Protobuf (нет Builder pattern, поля — package-private или с простыми геттерами/сеттерами).Schema evolution
Avro имеет более мощную и гибкую систему schema evolution благодаря разделению reader/writer schema. Protobuf поддерживает forward/backward compatibility через правила добавления/удаления полей, но не имеет встроенного механизма разрешения различий между схемами на уровне runtime — это должно быть реализовано прикладным кодом.
Производительность
Protobuf быстрее Avro по нескольким причинам:
Сгенерированный код Protobuf содержит жёстко закодированные инструкции сериализации (switch по тегу). Avro Generic использует рефлексию-подобный обход схемы.
Protobuf не требует schema resolution при чтении (если reader/writer schema совпадают). Avro всегда выполняет resolution, даже когда схемы идентичны.
Protobuf использует immutable generated объекты с zero-copy строками. Avro Generic использует mutable
GenericRecord с Object[].Однако Avro Specific API сопоставим по производительности с Protobuf, а в некоторых сценариях (большие файлы с блоковым сжатием) может быть быстрее за счёт эффективной интеграции с Hadoop/S3.
Экосистема
Protobuf: доминирует в gRPC, микросервисах, мобильной разработке. Поддержка в Kubernetes (etcd), Envoy, многих облачных провайдерах.
Avro: доминирует в Hadoop, Spark, Kafka, data lakes (Delta Lake, Iceberg). Интеграция с Hive, Presto/Trino, Flink.
#Java #для_новичков #beginner #IO #NIO #Serialize #Avro
👍4
[Совет по Java #071]
Тема:
Проблема: Фабричный метод
Это обеспечивает атомарность каждого отдельного вызова. Однако составные операции, состоящие из нескольких последовательных вызовов методов, не являются атомарными.
Например, проверка размера и последующее удаление элемента:
Даже если обход происходит в одном потоке, но метод синхронизирован только на уровне методов, итерация не защищена, так как она выполняется вне синхронизированного контекста.
Решение: Для безопасного выполнения составных операций необходимо вручную синхронизироваться на том же объекте-мониторе, который используется оберткой.
Обычно это сам объект списка, возвращенный
Для высококонкурентных структур, где итерация должна быть потокобезопасной, рассмотрите
Объяснение:
Мутексом по умолчанию является сам объект обертки. Поэтому, синхронизируясь на
Итерация особенно уязвима, потому что итератор не захватывает блокировку на весь период обхода. Синхронизация всего блока гарантирует, что никто не сможет модифицировать список во время итерации или выполнения нескольких операций.
#Java #советы
Тема:
Collections.synchronizedList синхронизирует только отдельные методы, но не составные операции (например, итерация).Проблема: Фабричный метод
Collections.synchronizedList(List) возвращает потокобезопасную обертку, в которой все публичные методы (add, get, remove, size и др.) синхронизированы на внутреннем мьютексе (обычно на самом объекте списка). Это обеспечивает атомарность каждого отдельного вызова. Однако составные операции, состоящие из нескольких последовательных вызовов методов, не являются атомарными.
Например, проверка размера и последующее удаление элемента:
if (!list.isEmpty()) list.remove(0). Между этими двумя вызовами другой поток может изменить список, и операция удалит не тот элемент или выбросит исключение. Особенно опасна итерация с помощью for-each или Iterator. Итератор, полученный из synchronizedList, не синхронизирован сам по себе, и если во время обхода другой поток модифицирует список, возникает ConcurrentModificationException. Даже если обход происходит в одном потоке, но метод синхронизирован только на уровне методов, итерация не защищена, так как она выполняется вне синхронизированного контекста.
Решение: Для безопасного выполнения составных операций необходимо вручную синхронизироваться на том же объекте-мониторе, который используется оберткой.
Обычно это сам объект списка, возвращенный
Collections.synchronizedList. Рекомендуется использовать блок synchronized(syncList) { ... }, внутри которого выполнять все необходимые вызовы, включая итерацию. Если вы используете синхронизированную обертку, всегда синхронизируйтесь на ней для составных операций. Альтернативно, для сценариев с частыми чтениями и редкими модификациями подходит CopyOnWriteArrayList, который обеспечивает безопасную итерацию без дополнительной синхронизации. Для высококонкурентных структур, где итерация должна быть потокобезопасной, рассмотрите
ConcurrentLinkedDeque или ConcurrentSkipListSet, но они не реализуют интерфейс List. В любом случае, явная синхронизация составных операций остается ответственностью разработчика.public class SynchronizedListIteration {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("A");
list.add("B");
list.add("C");
//Опасная итерация без синхронизации
// Может выбросить ConcurrentModificationException, если другой поток модифицирует список
for (String s : list) {
System.out.println(s);
}
//Правильная итерация с синхронизацией на списке
synchronized (list) {
for (String s : list) {
System.out.println(s);
}
}
//Неатомарная составная операция
// Между size() и remove() другой поток может изменить список
if (!list.isEmpty()) {
list.remove(0); // Может удалить не тот элемент
}
//Правильная составная операция в синхронизированном блоке
synchronized (list) {
if (!list.isEmpty()) {
String first = list.remove(0);
System.out.println("Removed: " + first);
}
}
// Для итерации с удалением элементов также требуется синхронизация
synchronized (list) {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove(); // Безопасно внутри блока
}
}
}
}
}Объяснение:
Collections.synchronizedList возвращает экземпляр внутреннего класса SynchronizedList, который делегирует все вызовы к оригинальному списку, оборачивая их в synchronized(mutex). Мутексом по умолчанию является сам объект обертки. Поэтому, синхронизируясь на
list, мы используем тот же самый монитор, что обеспечивает взаимное исключение с методами обертки. Без явной синхронизации составные операции не являются потокобезопасными, так как между отдельными методами блокировка освобождается. Итерация особенно уязвима, потому что итератор не захватывает блокировку на весь период обхода. Синхронизация всего блока гарантирует, что никто не сможет модифицировать список во время итерации или выполнения нескольких операций.
#Java #советы
👍4
Раздел 12. Работа с файлами, I/O и сетью (NIO.2)
Глава 4. Работа с сетью (Socket, HTTP, HttpClient)
Основы сетевого взаимодействия: IP-адреса, порты, протоколы
Сетевое взаимодействие — это фундамент, на котором строится любое распределённое приложение. Будь то веб-сервис, микросервисная архитектура, потоковое видео или онлайн-игра, всё сводится к передаче данных между двумя точками через сеть.
В Java сетевая функциональность реализована в пакетах
IP-адрес: логический адрес в сети
IP-адрес (Internet Protocol address) — это числовой идентификатор, присваиваемый каждому устройству, подключённому к сети, работающей по протоколу IP.
IP-адрес выполняет две функции: идентификация хоста (устройства) и маршрутизация — определение пути, по которому данные должны добраться до получателя.
IPv4
IPv4 (Internet Protocol version 4) — четвёртая версия протокола, введённая в 1981 году. Адрес IPv4 представляет собой 32-битное число, разделённое на четыре октета (группы по 8 бит), записываемых в десятичной нотации через точку:
32 бита дают теоретический лимит в 4 294 967 296 уникальных адресов. На практике адресное пространство исчерпано из-за неэффективного распределения в ранние годы интернета и роста числа устройств. Чтобы продлить жизнь IPv4, были введены технологии NAT (Network Address Translation — преобразование сетевых адресов), позволяющие множеству устройств в локальной сети использовать один внешний IP-адрес, и CIDR (Classless Inter-Domain Routing — бесклассовая междоменная маршрутизация), которая позволяет гибко делить адресное пространство на подсети произвольного размера.
В Java адрес IPv4 представляется классом
Метод
IPv6
IPv6 (Internet Protocol version 6) — шестая версия, разработанная как преемник IPv4. Адрес IPv6 — это 128-битное число, записываемое в шестнадцатеричной нотации через двоеточия:
128 бит дают 3.4 × 10^38 адресов — количество, достаточное для присвоения IP каждому атому на поверхности Земли. IPv6 устраняет необходимость в NAT, упрощает маршрутизацию за счёт фиксированного размера заголовка (в отличие от переменного в IPv4) и встроено поддерживает IPsec — протокол шифрования и аутентификации на сетевом уровне.
В Java IPv6 представлен классом
Специальные адреса
Приватные адреса (RFC 1918):
Multicast:
Broadcast (широковещательный адрес): в IPv4
Порт: точка входа в приложение
Если IP-адрес идентифицирует устройство в сети, то порт (port) идентифицирует конкретное приложение или процесс на этом устройстве. Порт — это 16-битное число от 0 до 65535, которое добавляется к IP-адресу, образуя полный адрес конечной точки связи.
Диапазоны портов
Well-known ports (0–1023): зарезервированы IANA для стандартных протоколов. Например: 22 — SSH, 80 — HTTP, 443 — HTTPS, 53 — DNS. На Unix-подобных системах привязка к этим портам требует прав суперпользователя (root).
Registered ports (1024–49151): зарегистрированы IANA для конкретных сервисов, но не требуют root. Например: 3306 — MySQL, 5432 — PostgreSQL, 8080 — альтернативный HTTP.
Dynamic / Private / Ephemeral ports (49152–65535): временные порты, которые ОС назначает клиентским приложениям при исходящих соединениях. Когда ваш браузер открывает соединение с сервером на порту 443, ОС выбирает свободный эфемерный порт из этого диапазона для локальной стороны соединения.
Передача
#Java #для_новичков #beginner #IO #NIO #Ip #Port #TCP #UDP
Глава 4. Работа с сетью (Socket, HTTP, HttpClient)
Основы сетевого взаимодействия: IP-адреса, порты, протоколы
Сетевое взаимодействие — это фундамент, на котором строится любое распределённое приложение. Будь то веб-сервис, микросервисная архитектура, потоковое видео или онлайн-игра, всё сводится к передаче данных между двумя точками через сеть.
В Java сетевая функциональность реализована в пакетах
java.net и java.nio.channels, а понимание нижележащих протоколов — обязательное требование для разработчика, который проектирует производительные и надёжные системы.Протокол — это формальный набор правил, определяющий формат, порядок и обработку данных при их передаче между устройствами. Протоколы работают на разных уровнях абстракции: от физического уровня (электрические сигналы в кабеле) до прикладного (HTTP, gRPC). В Java-разработке нас интересуют прежде всего транспортный и прикладной уровни модели TCP/IP.
TCP/IP — это стек протоколов, лежащий в основе интернета. Вместо семиуровневой модели OSI в инженерной практике используется упрощённая четырёхуровневая модель: канальный уровень (Ethernet, Wi-Fi), сетевой (IP), транспортный (TCP, UDP) и прикладной (HTTP, DNS, SSH).
IP-адрес: логический адрес в сети
IP-адрес (Internet Protocol address) — это числовой идентификатор, присваиваемый каждому устройству, подключённому к сети, работающей по протоколу IP.
IP-адрес выполняет две функции: идентификация хоста (устройства) и маршрутизация — определение пути, по которому данные должны добраться до получателя.
IPv4
IPv4 (Internet Protocol version 4) — четвёртая версия протокола, введённая в 1981 году. Адрес IPv4 представляет собой 32-битное число, разделённое на четыре октета (группы по 8 бит), записываемых в десятичной нотации через точку:
192.168.1.1.32 бита дают теоретический лимит в 4 294 967 296 уникальных адресов. На практике адресное пространство исчерпано из-за неэффективного распределения в ранние годы интернета и роста числа устройств. Чтобы продлить жизнь IPv4, были введены технологии NAT (Network Address Translation — преобразование сетевых адресов), позволяющие множеству устройств в локальной сети использовать один внешний IP-адрес, и CIDR (Classless Inter-Domain Routing — бесклассовая междоменная маршрутизация), которая позволяет гибко делить адресное пространство на подсети произвольного размера.
В Java адрес IPv4 представляется классом
Inet4Address, наследником InetAddress.import java.net.InetAddress;
import java.net.UnknownHostException;
public class IpExample {
public static void main(String[] args) throws UnknownHostException {
// Разрешение доменного имени в IP-адрес через DNS
InetAddress address = InetAddress.getByName("www.example.com");
System.out.println(address.getHostAddress()); // 93.184.216.34
System.out.println(address instanceof java.net.Inet4Address); // true
}
}
Метод
getByName() выполняет DNS-резолюцию (Domain Name System resolution — преобразование доменного имени в IP-адрес). Это блокирующий вызов: поток приостанавливается до получения ответа от DNS-сервера. В современном Java для неблокирующей работы используется java.net.InetAddress в сочетании с CompletableFuture или HttpClient.IPv6
IPv6 (Internet Protocol version 6) — шестая версия, разработанная как преемник IPv4. Адрес IPv6 — это 128-битное число, записываемое в шестнадцатеричной нотации через двоеточия:
2001:0db8:85a3:0000:0000:8a2e:0370:7334. Для краткости ведущие нули в группе можно опускать, а одну последовательность из нулевых групп — заменять двойным двоеточием: 2001:db8:85a3::8a2e:370:7334.128 бит дают 3.4 × 10^38 адресов — количество, достаточное для присвоения IP каждому атому на поверхности Земли. IPv6 устраняет необходимость в NAT, упрощает маршрутизацию за счёт фиксированного размера заголовка (в отличие от переменного в IPv4) и встроено поддерживает IPsec — протокол шифрования и аутентификации на сетевом уровне.
В Java IPv6 представлен классом
Inet6Address. JVM автоматически предпочитает IPv6, если хост поддерживает оба стека (свойство java.net.preferIPv4Stack и java.net.preferIPv6Addresses управляют этим поведением).InetAddress ipv6 = InetAddress.getByName("::1"); // loopback в IPv6
System.out.println(ipv6 instanceof java.net.Inet6Address); // trueLoopback (петлевой адрес) — это специальный адрес, который всегда указывает на текущий хост. В IPv4 это 127.0.0.1, в IPv6 — ::1. Пакеты, отправленные на loopback, не выходят за пределы сетевого стека ОС и не попадают в физический интерфейс.
Специальные адреса
Приватные адреса (RFC 1918):
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 — используются только внутри локальных сетей, не маршрутизируются в интернете.Multicast:
224.0.0.0/4 в IPv4, ff00::/8 в IPv6 — адреса для групповой рассылки, когда один пакет доставляется множеству получателей.Broadcast (широковещательный адрес): в IPv4
255.255.255.255 или адрес подсети с единицами в хостовой части — отправка пакета всем устройствам в сегменте сети. В IPv6 broadcast отсутствует, его заменяет multicast.Маршрутизация (routing) — это процесс выбора пути для сетевого пакета от источника к получателю через промежуточные узлы (роутеры). Каждый роутер принимает решение на основе таблицы маршрутизации и заголовка IP-пакета.
Порт: точка входа в приложение
Если IP-адрес идентифицирует устройство в сети, то порт (port) идентифицирует конкретное приложение или процесс на этом устройстве. Порт — это 16-битное число от 0 до 65535, которое добавляется к IP-адресу, образуя полный адрес конечной точки связи.
Сокет (socket) — это абстракция, представляющая одну конечную точку сетевого соединения. В TCP сокет однозначно определяется четвёркой: IP-источника, порт-источника, IP-назначения, порт-назначения. В Java сокет представлен классами java.net.Socket (клиент) и java.net.ServerSocket (сервер).
Диапазоны портов
Well-known ports (0–1023): зарезервированы IANA для стандартных протоколов. Например: 22 — SSH, 80 — HTTP, 443 — HTTPS, 53 — DNS. На Unix-подобных системах привязка к этим портам требует прав суперпользователя (root).
Registered ports (1024–49151): зарегистрированы IANA для конкретных сервисов, но не требуют root. Например: 3306 — MySQL, 5432 — PostgreSQL, 8080 — альтернативный HTTP.
Dynamic / Private / Ephemeral ports (49152–65535): временные порты, которые ОС назначает клиентским приложениям при исходящих соединениях. Когда ваш браузер открывает соединение с сервером на порту 443, ОС выбирает свободный эфемерный порт из этого диапазона для локальной стороны соединения.
Эфемерный порт (ephemeral port) — это временный порт, автоматически выделяемый стеком TCP/IP операционной системы для исходящего соединения. После закрытия соединения порт переходит в состояние TIME_WAIT и освобождается через несколько минут.
import java.net.InetSocketAddress;
// InetSocketAddress объединяет IP-адрес и порт
InetSocketAddress serverAddr = new InetSocketAddress("192.168.1.100", 8080);
InetSocketAddress localAddr = new InetSocketAddress("0.0.0.0", 0); // порт 0 = эфемерный
Передача
0 в качестве порта при создании Socket или ServerSocket означает "выбрать любой свободный эфемерный порт". После привязки реальный порт можно узнать через socket.getLocalPort().#Java #для_новичков #beginner #IO #NIO #Ip #Port #TCP #UDP
👍4