Критерии выбора формата сериализации
Производительность
Производительность измеряется по трём метрикам: скорость сериализации (объект → байты), скорость десериализации (байты → объект) и размер полученного сообщения.
Java Native Serialization медленна из-за рефлексии и записи метаданных класса для каждого объекта. Protobuf и Kryo в 10–100 раз быстрее. JSON медленнее бинарных форматов из-за парсинга текста и boxing'а чисел.
Boxing — это автоматическая упаковка примитивных типов в объектные обёртки. В JSON число 42 парсится как строка, затем преобразуется в int, что требует дополнительных операций по сравнению с прямым чтением 4 байт из бинарного потока.
Читаемость
Текстовые форматы (JSON, XML, YAML) человекочитаемы, что упрощает отладку, логирование и ручное тестирование API. Бинарные форматы требуют специальных инструментов (hex-редакторов, декодеров protobuf) для анализа. В production-системах читаемость часто жертвуется ради производительности, но для конфигураций, API-спецификаций и логов предпочтительны текстовые форматы.
Версионность (эволюция схем)
Версионность — способность формата адаптироваться к изменениям структуры данных без поломки существующих клиентов и серверов.
Java Native Serialization требует ручного управления
Wire-совместимость (wire compatibility) — это свойство формата, при котором изменение схемы не нарушает формат бинарного потока. Например, в protobuf изменение int32 на int64 wire-совместимо, так как оба типа кодируются как varint, но семантически может привести к truncation на старых клиентах.
Безопасность
Безопасность — критичнейший критерий, особенно для десериализации данных из недоверенных источников.
Java Native Serialization имеет печальную репутацию: десериализация
CVE (Common Vulnerabilities and Exposures) — это стандартизированный каталог публично известных уязвимостей информационной безопасности. Каждой уязвимости присваивается уникальный идентификатор, например CVE-2015-4852 (уязвимость десериализации в Oracle WebLogic).
ObjectInputFilter — интерфейс, введённый в Java 9 (JEP 290), позволяющий задавать белые и чёрные списки классов для десериализации, ограничивать глубину графа объектов, количество ссылок и размер данных.
JSON безопаснее, так как парсер создаёт только простые структуры (строки, числа, массивы, объекты), не вызывая произвольных конструкторов Java. Однако и JSON имеет уязвимости: XXE (XML External Entity) при использовании небезопасных XML-парсеров, или DOS-атаки через Billion Laughs в YAML/XML.
XXE (XML External Entity) — тип атаки на XML-парсеры, при котором злоумышленник внедряет внешнюю сущность в XML-документ, заставляя парсер обращаться к внутренним файлам сервера или внешним ресурсам.
Protobuf и Avro безопасны по дизайну: они не поддерживают произвольную загрузку классов, работают только со схемой, строго типизированы и не позволяют встроить исполняемый код в сообщение.
Кроссплатформенность
Если система полиглотная (использует несколько языков программирования), Java Native Serialization и Kryo неприменимы — они привязаны к JVM. JSON, XML, protobuf, Avro, FlatBuffers имеют реализации на десятках языков и являются lingua franca для межсервисного взаимодействия.
Полиглотная система (polyglot system) — это система, построенная с использованием нескольких языков программирования. Например, backend на Java/Go, ML-сервис на Python, frontend на JavaScript. В таких системах формат обмена данных должен быть языконезависимым.
Размер данных
Бинарные форматы значительно компактнее текстовых. Например, объект с тремя полями в JSON может занимать 100 байт из-за повторяющихся ключей и скобок. Тот же объект в protobuf — 10–20 байт. Это критично для мобильных приложений, IoT-устройств и высоконагруженных сетевых протоколов.
#Java #для_новичков #beginner #IO #NIO #Serialize
Производительность
Производительность измеряется по трём метрикам: скорость сериализации (объект → байты), скорость десериализации (байты → объект) и размер полученного сообщения.
Java Native Serialization медленна из-за рефлексии и записи метаданных класса для каждого объекта. Protobuf и Kryo в 10–100 раз быстрее. JSON медленнее бинарных форматов из-за парсинга текста и boxing'а чисел.
Boxing — это автоматическая упаковка примитивных типов в объектные обёртки. В JSON число 42 парсится как строка, затем преобразуется в int, что требует дополнительных операций по сравнению с прямым чтением 4 байт из бинарного потока.
Читаемость
Текстовые форматы (JSON, XML, YAML) человекочитаемы, что упрощает отладку, логирование и ручное тестирование API. Бинарные форматы требуют специальных инструментов (hex-редакторов, декодеров protobuf) для анализа. В production-системах читаемость часто жертвуется ради производительности, но для конфигураций, API-спецификаций и логов предпочтительны текстовые форматы.
Версионность (эволюция схем)
Версионность — способность формата адаптироваться к изменениям структуры данных без поломки существующих клиентов и серверов.
Java Native Serialization требует ручного управления
serialVersionUID и ломается при изменении сигнатуры класса (удаление/переименование полей, изменение типов). Protobuf поддерживает добавление полей (старые клиенты игнорируют неизвестные теги), удаление полей (если они не обязательны), изменение типов в рамках wire-совместимости. Avro поддерживает схемы чтения и записи разных версий с автоматическим разрешением различий.Wire-совместимость (wire compatibility) — это свойство формата, при котором изменение схемы не нарушает формат бинарного потока. Например, в protobuf изменение int32 на int64 wire-совместимо, так как оба типа кодируются как varint, но семантически может привести к truncation на старых клиентах.
Безопасность
Безопасность — критичнейший критерий, особенно для десериализации данных из недоверенных источников.
Java Native Serialization имеет печальную репутацию: десериализация
ObjectInputStream.readObject() может выполнить произвольный код, если в classpath есть классы с уязвимыми конструкторами, методами readObject() или ObjectInputValidation. Это привело к множеству критических CVE (Common Vulnerabilities and Exposures). Начиная с Java 9, добавлен механизм JEP 290 — фильтрация входящих классов через ObjectInputFilter.CVE (Common Vulnerabilities and Exposures) — это стандартизированный каталог публично известных уязвимостей информационной безопасности. Каждой уязвимости присваивается уникальный идентификатор, например CVE-2015-4852 (уязвимость десериализации в Oracle WebLogic).
// JEP 290: фильтрация десериализации с Java 9
public void safeDeserialize(InputStream in) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(in);
// Разрешаем десериализовать только классы из доверенных пакетов
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"maxdepth=5;maxrefs=10000;com.myapp.*;!*"
);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
}
ObjectInputFilter — интерфейс, введённый в Java 9 (JEP 290), позволяющий задавать белые и чёрные списки классов для десериализации, ограничивать глубину графа объектов, количество ссылок и размер данных.
JSON безопаснее, так как парсер создаёт только простые структуры (строки, числа, массивы, объекты), не вызывая произвольных конструкторов Java. Однако и JSON имеет уязвимости: XXE (XML External Entity) при использовании небезопасных XML-парсеров, или DOS-атаки через Billion Laughs в YAML/XML.
XXE (XML External Entity) — тип атаки на XML-парсеры, при котором злоумышленник внедряет внешнюю сущность в XML-документ, заставляя парсер обращаться к внутренним файлам сервера или внешним ресурсам.
Protobuf и Avro безопасны по дизайну: они не поддерживают произвольную загрузку классов, работают только со схемой, строго типизированы и не позволяют встроить исполняемый код в сообщение.
Кроссплатформенность
Если система полиглотная (использует несколько языков программирования), Java Native Serialization и Kryo неприменимы — они привязаны к JVM. JSON, XML, protobuf, Avro, FlatBuffers имеют реализации на десятках языков и являются lingua franca для межсервисного взаимодействия.
Полиглотная система (polyglot system) — это система, построенная с использованием нескольких языков программирования. Например, backend на Java/Go, ML-сервис на Python, frontend на JavaScript. В таких системах формат обмена данных должен быть языконезависимым.
Размер данных
Бинарные форматы значительно компактнее текстовых. Например, объект с тремя полями в JSON может занимать 100 байт из-за повторяющихся ключей и скобок. Тот же объект в protobuf — 10–20 байт. Это критично для мобильных приложений, IoT-устройств и высоконагруженных сетевых протоколов.
#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Путь байтов в памяти JVM при сериализации
Сериализация: от объекта к байтам
Когда вы вызываете
Объект в куче. Объект
Дескриптор класса (полное имя класса,
Типы и значения полей (примитивы записываются как есть, объекты — рекурсивно)
Для каждого нового класса в иерархии — его дескриптор
Обход графа и отслеживание ссылок.
Буферизация.
ByteArrayOutputStream. Если сериализация идёт в память,
Возвращаемый массив.
Работа GC. Все временные объекты — внутренние буферы
ObjectStreamClass — это внутренний класс JVM, представляющий метаданные сериализуемого класса: имя,
Десериализация: от байтов к объекту
При
Байты в куче. Исходный массив байтов или поток находится в куче (если это
Чтение дескрипторов.
Создание объекта без конструктора. JVM выделяет память под объект в Young Generation (Eden) через механизм
Заполнение полей.
Восстановление графа. Если в потоке есть backreference (ссылка на ранее десериализованный объект),
Вызов readObject(). Если класс реализует
Работа GC. Вновь созданные объекты размещаются в Eden. Если десериализуемый граф большой (миллионы объектов), они могут быстро заполнить Young Generation и вызвать Minor GC в процессе десериализации. Если объекты сохраняются в долгоживущую структуру (коллекцию, кэш), они переходят в Old Generation.
Потоковая сериализация и GC
При сериализации в
Буфер
Буфер в куче переиспользуется — новые объекты не создаются при каждом сбросе. Это снижает давление на GC.
Если сериализация выполняется в цикле с высокой частотой (например, в RPC-сервере, обрабатывающем 10 000 запросов в секунду), объекты
Оптимизация: пул переиспользуемых потоков. В высоконагруженных системах
В этом примере
#Java #для_новичков #beginner #IO #NIO #Serialize
Сериализация: от объекта к байтам
Когда вы вызываете
ObjectOutputStream.writeObject(user):Объект в куче. Объект
user и все объекты, на которые он ссылается (глубокий граф), находятся в куче (Heap). ObjectOutputStream обходит граф, начиная с корневого объекта. Для каждого объекта он записывает:Дескриптор класса (полное имя класса,
serialVersionUID, флаги)Типы и значения полей (примитивы записываются как есть, объекты — рекурсивно)
Для каждого нового класса в иерархии — его дескриптор
Обход графа и отслеживание ссылок.
ObjectOutputStream поддерживает таблицу handle — целочисленных идентификаторов для уже записанных объектов. Если в графе есть циклические ссылки (A ссылается на B, B ссылается на A) или повторяющиеся ссылки, второе вхождение объекта записывается не полностью, а как backreference (handle ранее записанного объекта). Это предотвращает бесконечную рекурсию и дублирование.Буферизация.
ObjectOutputStream пишет данные во внутренний буфер byte[] (обычно 512 байт или больше). Этот буфер создаётся в Young Generation, в Eden. Когда буфер заполняется, он сбрасывается (flush) в нижележащий поток: ByteArrayOutputStream, FileOutputStream или SocketOutputStream.ByteArrayOutputStream. Если сериализация идёт в память,
ByteArrayOutputStream накапливает байты в динамически расширяющемся массиве. При превышении ёмкости создаётся новый массив вдвое большего размера, данные копируются, старый массив становится мусором. Это может породить несколько временных массивов в Young Generation, которые собираются Minor GC.Возвращаемый массив.
baos.toByteArray() создаёт новый массив точного размера и копирует в него данные. Исходный внутренний буфер ByteArrayOutputStream остаётся в куче до сборки мусора. Итоговый массив байтов — это объект в куче, который может быть передан по сети, сохранён в файл или закэширован.Работа GC. Все временные объекты — внутренние буферы
ObjectOutputStream, промежуточные массивы ByteArrayOutputStream, объекты ObjectStreamClass (метаданные классов) — создаются в Young Generation. При короткой сериализации они уничтожаются при следующей Minor GC. Если сериализация выполняется в цикле (например, сериализация 10 000 объектов для пакетной отправки), буферы могут пережить несколько циклов Minor GC и мигрировать в Survivor Space, а затем в Old Generation.ObjectStreamClass — это внутренний класс JVM, представляющий метаданные сериализуемого класса: имя,
serialVersionUID, поля, методы writeObject/readObject. Эти объекты кэшируются внутри ObjectOutputStream для повторного использования, но при сериализации множества разных классов они накапливаются.Десериализация: от байтов к объекту
При
ObjectInputStream.readObject():Байты в куче. Исходный массив байтов или поток находится в куче (если это
ByteArrayInputStream) или читается из нативного буфера (если это FileInputStream или SocketInputStream).Чтение дескрипторов.
ObjectInputStream читает заголовок потока (magic number, версия протокола), затем дескриптор класса. Он ищет класс в Metaspace через Class.forName(). Если класс не загружен, ClassLoader загружает его байткод в Metaspace.Создание объекта без конструктора. JVM выделяет память под объект в Young Generation (Eden) через механизм
sun.misc.Unsafe.allocateInstance() (или аналог в новых версиях). Это выделение памяти без вызова конструктора — объект создаётся "сырым", с полями в состоянии по умолчанию (null, 0, false).Заполнение полей.
ObjectInputStream читает значения полей из потока и записывает их напрямую в память объекта (через JNI или Unsafe). Для объектных полей — рекурсивная десериализация. Для transient-полей — значение не читается, остаётся по умолчанию.Восстановление графа. Если в потоке есть backreference (ссылка на ранее десериализованный объект),
ObjectInputStream подставляет ссылку на существующий объект из таблицы handles. Это восстанавливает точную топологию исходного графа, включая циклы.Вызов readObject(). Если класс реализует
private void readObject(ObjectInputStream in), JVM вызывает этот метод после заполнения полей. Это позволяет выполнить пост-инициализацию: восстановить transient-поля, вычислить производные значения, проверить инварианты.Работа GC. Вновь созданные объекты размещаются в Eden. Если десериализуемый граф большой (миллионы объектов), они могут быстро заполнить Young Generation и вызвать Minor GC в процессе десериализации. Если объекты сохраняются в долгоживущую структуру (коллекцию, кэш), они переходят в Old Generation.
Потоковая сериализация и GC
При сериализации в
FileOutputStream или SocketOutputStream:Буфер
ObjectOutputStream (в куче) периодически сбрасывается. Сброс вызывает системный вызов write() (POSIX), который копирует байты из кучи JVM в нативную память ядра (кэш страниц или сетевой буфер).Буфер в куче переиспользуется — новые объекты не создаются при каждом сбросе. Это снижает давление на GC.
Если сериализация выполняется в цикле с высокой частотой (например, в RPC-сервере, обрабатывающем 10 000 запросов в секунду), объекты
ObjectOutputStream и ObjectInputStream создаются и закрываются для каждого запроса. Каждый такой объект — это десятки килобайт в куче. При интенсивной нагрузке они быстро заполняют Eden и вызывают частые Minor GC.Оптимизация: пул переиспользуемых потоков. В высоконагруженных системах
ObjectOutputStream создаётся один раз и сбрасывается через reset() между запросами. Метод reset() очищает внутренние таблицы handles, позволяя сериализовать новый граф объектов без создания нового потока.public class PooledSerializer {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
private final ObjectOutputStream oos;
public PooledSerializer() throws IOException {
this.oos = new ObjectOutputStream(baos);
}
public synchronized byte[] serialize(Object obj) throws IOException {
baos.reset(); // сбрасываем буфер, не создавая новый
oos.reset(); // сбрасываем таблицу handles
oos.writeObject(obj);
oos.flush();
return baos.toByteArray(); // копирует текущий буфер
}
}В этом примере
baos.reset() очищает счётчик байтов, но не освобождает внутренний массив. oos.reset() очищает таблицу handles, позволяя сериализовать новые объекты без утечки памяти. Это существенно снижает давление на GC в сценариях с высокой частотой сериализации.#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 3. Сериализация и форматы обмена
Нативная сериализация Java: интерфейс Serializable
Интерфейс
Он не требует реализации методов, не содержит констант и не накладывает контрактов на поведение класса. Его единственное назначение — сигнализировать JVM и стандартной библиотеке, что экземпляры данного класса разрешено преобразовывать в поток байтов и восстанавливать из него.
Этот механизм глубоко интегрирован в JVM: он обходит граф объектов, записывает метаданные классов, управляет версионностью и обеспечивает восстановление ссылочной целостности. Однако за простотой использования скрывается сложная внутренняя архитектура, множество подводных камней и серьёзные ограничения, которые делают нативную сериализацию устаревшим выбором для новых систем.
Маркерный интерфейс Serializable
Маркерный интерфейс — это интерфейс в Java без методов и полей, служащий метаданным для компилятора или runtime.
При попытке сериализовать объект, класс которого не реализует
Если
#Java #для_новичков #beginner #IO #NIO #Serialize #Serializable
Глава 3. Сериализация и форматы обмена
Нативная сериализация Java: интерфейс Serializable
Интерфейс
java.io.Serializable — это центральный элемент механизма нативной сериализации Java, существующий с версии JDK 1.1.Он не требует реализации методов, не содержит констант и не накладывает контрактов на поведение класса. Его единственное назначение — сигнализировать JVM и стандартной библиотеке, что экземпляры данного класса разрешено преобразовывать в поток байтов и восстанавливать из него.
Этот механизм глубоко интегрирован в JVM: он обходит граф объектов, записывает метаданные классов, управляет версионностью и обеспечивает восстановление ссылочной целостности. Однако за простотой использования скрывается сложная внутренняя архитектура, множество подводных камней и серьёзные ограничения, которые делают нативную сериализацию устаревшим выбором для новых систем.
Маркерный интерфейс Serializable
Serializable — это маркерный интерфейс (marker interface). В отличие от обычных интерфейсов, он не объявляет ни одного метода. Его роль чисто декларативная: наличие implements Serializable в объявлении класса изменяет поведение JVM при обработке этого класса механизмом сериализации.import java.io.Serializable;
public class Document implements Serializable {
// Маркерный интерфейс не требует методов
private String title;
private String content;
}
Маркерный интерфейс — это интерфейс в Java без методов и полей, служащий метаданным для компилятора или runtime.
Cloneable, Serializable, RandomAccess — примеры таких интерфейсов. Они не влияют на структуру класса напрямую, но изменяют поведение встроенных механизмов JVM. В современной Java роль маркерных интерфейсов частично вытесняется аннотациями (например, @Entity в JPA), но Serializable остаётся неизменным с 1997 года по причинам обратной совместимости.Аннотация (annotation) — это форма метаданных в Java, введённая в Java 5, которая предоставляет данные о программе, не являющиеся частью самой программы. Аннотации не влияют непосредственно на операции кода, но могут обрабатываться компилятором, инструментами или во время выполнения через рефлексию.
При попытке сериализовать объект, класс которого не реализует
Serializable, ObjectOutputStream.writeObject() выбросит NotSerializableException. Это проверка выполняется рекурсивно: если класс реализует Serializable, но одно из его полей — объект класса без Serializable, исключение возникнет при попытке записать это поле.public class Container implements Serializable {
private Document doc; // Document должен быть Serializable
private Metadata meta; // Metadata тоже должен быть Serializable
}Если
Metadata не реализует Serializable, сериализация Container завершится NotSerializableException на этапе обхода поля meta. Это ограничение каскадируется по всему графу объектов: каждый объект, достижимый от корневого, должен быть сериализуемым, либо должен быть помечен как transient.#Java #для_новичков #beginner #IO #NIO #Serialize #Serializable
👍4
Что сериализуется и что нет
Сериализуемое состояние
Механизм нативной сериализации записывает состояние экземпляра объекта — значения всех полей экземпляра, за исключением тех, что явно исключены. Это включает:
Поля всех уровней доступности:
Поля примитивных типов: их значения записываются в бинарном виде.
Поля ссылочных типов: объекты, на которые ссылаются поля, сериализуются рекурсивно. Если поле ссылается на другой объект, механизм обходит ссылку и сериализует целевой объект, затем возвращается к текущему.
При сериализации
Что не сериализуется
Статические поля не сериализуются. Они принадлежат классу, а не экземпляру, и их значения хранятся в Metaspace (в структурах класса), а не в куче вместе с объектом. При десериализации статические поля получают текущие значения, определённые в загруженном классе, а не значения из потока.
Это логично: если бы статические поля сериализовались, восстановление объекта на другой JVM с другим состоянием класса привело бы к конфликтам.
Transient-поля явно исключаются из сериализации. Ключевое слово
Важно:
Поля-суперклассы
Если класс реализует
При десериализации
Механизм обхода графа объектов
Отслеживание ссылок и циклы
Ключевая особенность: каждый объект в графе записывается ровно один раз. Для этого
Объект записывается полностью (дескриптор класса + поля).
Объекту присваивается handle — следующее свободное целое число.
Handle сохраняется в таблице.
При повторной встрече того же объекта (через другую ссылку или в цикле):
В поток записывается маркер backreference (TC_REFERENCE) + handle ранее записанного объекта.
Полная сериализация не выполняется.
Этот механизм предотвращает бесконечную рекурсию при циклических ссылках и гарантирует, что после десериализации топология графа сохранится:
Handle (в контексте сериализации Java) — это целочисленный идентификатор, присваиваемый каждому уникальному объекту в потоке сериализации. Handles начинаются с базового значения (обычно
#Java #для_новичков #beginner #IO #NIO #Serialize
Сериализуемое состояние
Механизм нативной сериализации записывает состояние экземпляра объекта — значения всех полей экземпляра, за исключением тех, что явно исключены. Это включает:
Поля всех уровней доступности:
public, protected, package-private и private. Сериализация обходит модификаторы доступа через механизм reflection или нативный доступ JNI, поэтому приватные поля сохраняются и восстанавливаются без геттеров и сеттеров.Поля примитивных типов: их значения записываются в бинарном виде.
Поля ссылочных типов: объекты, на которые ссылаются поля, сериализуются рекурсивно. Если поле ссылается на другой объект, механизм обходит ссылку и сериализует целевой объект, затем возвращается к текущему.
public class Library implements Serializable {
private String name;
private List<Book> books; // Book тоже должен быть Serializable
private Librarian head; // Librarian тоже должен быть Serializable
}При сериализации
Library механизм рекурсивно обойдёт список books: каждый элемент Book будет сериализован со своими полями, включая ссылки на Author, Publisher и т.д. Затем будет сериализован head (Librarian) со всеми его полями. Результат — глубокая копия всего графа объектов в виде потока байтов.Глубокая копия (deep copy) — это копия объекта, при которой копируются не только сам объект, но и все объекты, на которые он ссылается, рекурсивно. В отличие от поверхностной копии (shallow copy), где копируются только ссылки на те же внутренние объекты.
Что не сериализуется
Статические поля не сериализуются. Они принадлежат классу, а не экземпляру, и их значения хранятся в Metaspace (в структурах класса), а не в куче вместе с объектом. При десериализации статические поля получают текущие значения, определённые в загруженном классе, а не значения из потока.
Это логично: если бы статические поля сериализовались, восстановление объекта на другой JVM с другим состоянием класса привело бы к конфликтам.
public class Config implements Serializable {
private static String globalPrefix = "app"; // не сериализуется
private String instanceName; // сериализуется
}Transient-поля явно исключаются из сериализации. Ключевое слово
transient указывает механизму, что поле не должно быть записано в поток. При десериализации такое поле получает значение по умолчанию: null для ссылочных типов, 0 для чисел, false для boolean.public class UserSession implements Serializable {
private String username;
private transient String password; // не сериализуется
private transient Connection dbConn; // не сериализуется
private transient byte[] cachedToken; // не сериализуется
}transient используется для чувствительных данных (пароли, ключи), ресурсов, привязанных к runtime (соединения с БД, потоки, сокеты), и вычисляемых/кэшированных значений, которые можно восстановить после загрузки.Важно:
transient не влияет на видимость поля — оно остаётся приватным или публичным, просто исключается из сериализационного протокола.Поля-суперклассы
Если класс реализует
Serializable, а его суперкласс — нет, то поля суперкласса не сериализуются. При десериализации суперкласс инициализируется через свой no-arg конструктор (конструктор без аргументов). Если такого конструктора нет — InvalidClassException.public class Base { // не Serializable
private int baseField;
public Base() {} // обязателен no-arg конструктор
}
public class Derived extends Base implements Serializable {
private int derivedField; // только это поле сериализуется
}При десериализации
Derived поле baseField не восстанавливается из потока. Вместо этого вызывается Base(), и baseField получает значение по умолчанию (0), независимо от того, каким оно было при сериализации. Это частый источник багов: разработчик забывает, что суперкласс не serializable, и удивляется, почему поля предка обнуляются.No-arg конструктор (no-argument constructor, default constructor) — это конструктор без параметров. В Java, если класс не объявляет явных конструкторов, компилятор генерирует no-arg конструктор автоматически. Если объявлен хотя бы один конструктор с параметрами, no-arg конструктор нужно объявлять явно.
Механизм обхода графа объектов
ObjectOutputStream не просто записывает байты полей. Он обходит весь граф объектов, начиная с корневого, используя алгоритм, похожий на обход в ширину (BFS) или обход в глубину (DFS) с отслеживанием посещённых узлов.Отслеживание ссылок и циклы
Ключевая особенность: каждый объект в графе записывается ровно один раз. Для этого
ObjectOutputStream поддерживает внутреннюю таблицу handles — отображение от сериализованного объекта к целочисленному идентификатору (int). При первой встрече объекта:Объект записывается полностью (дескриптор класса + поля).
Объекту присваивается handle — следующее свободное целое число.
Handle сохраняется в таблице.
При повторной встрече того же объекта (через другую ссылку или в цикле):
В поток записывается маркер backreference (TC_REFERENCE) + handle ранее записанного объекта.
Полная сериализация не выполняется.
public class Node implements Serializable {
private String name;
private Node next; // может образовать цикл
}
// Создаём цикл: A -> B -> C -> A
Node a = new Node("A");
Node b = new Node("B");
Node c = new Node("C");
a.next = b;
b.next = c;
c.next = a; // цикл!
// Сериализация не зацикливается благодаря таблице handles
oos.writeObject(a); // записывает A, B, C, затем backreference на AЭтот механизм предотвращает бесконечную рекурсию при циклических ссылках и гарантирует, что после десериализации топология графа сохранится:
c.next будет ссылаться на тот же объект a, что и корневой указатель. Без handles граф с циклами привёл бы к StackOverflowError при сериализации.Handle (в контексте сериализации Java) — это целочисленный идентификатор, присваиваемый каждому уникальному объекту в потоке сериализации. Handles начинаются с базового значения (обычно
baseWireHandle = 0x7E0000) и инкрементируются. Они используются для кодирования backreferences.#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
serialVersionUID: идентификатор версии класса
Что такое serialVersionUID
При десериализации она сравнивает
Этот механизм защищает от десериализации объектов, сериализованных несовместимой версией класса. Например, если класс
Генерация по умолчанию
Если класс не объявляет
Имя класса.
Модификаторы класса (
Имена интерфейсов, которые реализует класс.
Имена полей, их модификаторы, типы.
Имена методов, их модификаторы, сигнатуры (возвращаемый тип, параметры).
Имена конструкторов, их сигнатуры.
Имена и сигнатуры методов
Имена статических полей (кроме
Этот хеш вычисляется через алгоритм, похожий на Secure Hash Algorithm (SHA), но упрощённый. Результат — 64-битное целое число
Проблема автоматической генерации
Автоматическая генерация
Если объект
Это происходит даже если изменение семантически совместимо: добавление метода не влияет на состояние объекта, но меняет вычисленный хеш.
Решение: явное объявление
Рекомендуется всегда объявлять
При явном объявлении JVM не вычисляет UID динамически, а использует указанное значение. Теперь добавление методов, изменение порядка полей, рефакторинг внутренней структуры не влияет на совместимость сериализации — до тех пор, пока изменения совместимы с форматом потока.
Как выбрать начальное значение? Традиционно используется
Когда менять serialVersionUID
Несовместимые изменения включают:
Удаление полей.
Изменение типа поля (например,
Изменение модификаторов класса с
Перемещение класса в другой пакет (имя класса входит в поток).
Изменение порядка полей суперкласса (влияет на порядок в потоке).
Изменение класса с
Совместимые изменения
При явном
Добавление полей: новые поля в потоке отсутствуют, поэтому при десериализации они получают значения по умолчанию (
Добавление классов в иерархию: если добавить новый суперкласс, который сам реализует
Удаление классов из иерархии: если удалить сериализуемый суперкласс, поля этого суперкласса в потоке игнорируются.
Добавление/удаление методов
Изменение модификатора доступа поля: не влияет, так как сериализация обходит модификаторы.
Пометка поля как transient: поле перестаёт читаться из потока, получает значение по умолчанию.
При десериализации объекта версии 1 в класс версии 2 поле
Обратная совместимость: новый объект в старый класс
Если объект сериализован версией 2 (с полем
#Java #для_новичков #beginner #IO #NIO #Serialize
Что такое serialVersionUID
serialVersionUID — это статическое поле типа long, которое выступает уникальным идентификатором версии класса для механизма сериализации. При сериализации JVM записывает serialVersionUID класса в поток.При десериализации она сравнивает
serialVersionUID из потока с serialVersionUID загруженного класса. Если значения не совпадают — InvalidClassException.public class Document implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
}Этот механизм защищает от десериализации объектов, сериализованных несовместимой версией класса. Например, если класс
Document v1 имел поля title и author, а в v2 поле author было удалено и добавлено content, десериализация v1-объекта в v2-класс без контроля версий привела бы к повреждению данных или ClassCastException.Генерация по умолчанию
Если класс не объявляет
serialVersionUID явно, JVM вычисляет его автоматически на основе структуры класса. Алгоритм генерации (документирован в спецификации Java Object Serialization) учитывает:Имя класса.
Модификаторы класса (
public, final, abstract, interface).Имена интерфейсов, которые реализует класс.
Имена полей, их модификаторы, типы.
Имена методов, их модификаторы, сигнатуры (возвращаемый тип, параметры).
Имена конструкторов, их сигнатуры.
Имена и сигнатуры методов
writeObject, readObject, writeReplace, readResolve.Имена статических полей (кроме
serialVersionUID itself).Этот хеш вычисляется через алгоритм, похожий на Secure Hash Algorithm (SHA), но упрощённый. Результат — 64-битное целое число
long.SHA (Secure Hash Algorithm) — семейство криптографических хеш-функций, которые преобразуют входные данные произвольной длины в строку фиксированной длины (дайджест). В контексте serialVersionUID используется не криптографический SHA, а похожий алгоритм хеширования структурных элементов класса.
Проблема автоматической генерации
Автоматическая генерация
serialVersionUID кажущеися удобной, но на практике является источником хрупкости. Любое изменение класса — даже добавление приватного метода, изменение порядка полей, добавление комментария (нет, комментарии не влияют, но форматирование может влиять на порядок элементов в некоторых компиляторах) — может изменить вычисленный serialVersionUID.// Версия 1
public class User implements Serializable {
private String name;
private int age;
}
// Версия 2 — добавлен метод, serialVersionUID изменился!
public class User implements Serializable {
private String name;
private int age;
public void greet() { // новый метод изменяет вычисленный UID
System.out.println("Hello");
}
}
Если объект
User был сериализован версией 1, а десериализуется версией 2, JVM вычислит новый serialVersionUID для загруженного класса, сравнит с UID из потока, обнаружит несоответствие и выбросит:java.io.InvalidClassException: User; local class incompatible:
stream classdesc serialVersionUID = -123456789,
local class serialVersionUID = 987654321
Это происходит даже если изменение семантически совместимо: добавление метода не влияет на состояние объекта, но меняет вычисленный хеш.
Решение: явное объявление
Рекомендуется всегда объявлять
serialVersionUID явно:public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}При явном объявлении JVM не вычисляет UID динамически, а использует указанное значение. Теперь добавление методов, изменение порядка полей, рефакторинг внутренней структуры не влияет на совместимость сериализации — до тех пор, пока изменения совместимы с форматом потока.
Как выбрать начальное значение? Традиционно используется
1L. Если позже потребуется несовместимое изменение, значение увеличивают: 2L, 3L и т.д. Некоторые команды используют timestamp или хеш от начальной структуры класса, но на практике последовательные номера проще в управлении.Когда менять serialVersionUID
serialVersionUID следует изменять (инкрементировать) при несовместимых изменениях структуры класса. Несовместимые изменения включают:
Удаление полей.
Изменение типа поля (например,
int → String).Изменение модификаторов класса с
Serializable на несериализуемый.Перемещение класса в другой пакет (имя класса входит в поток).
Изменение порядка полей суперкласса (влияет на порядок в потоке).
Изменение класса с
enum на обычный или наоборот.Совместимые изменения
При явном
serialVersionUID можно вносить изменения, не ломающие десериализацию старых объектов:Добавление полей: новые поля в потоке отсутствуют, поэтому при десериализации они получают значения по умолчанию (
null, 0, false). JVM использует ObjectInputStream.defaultReadObject(), который читает из потока только те поля, которые есть в текущем классе, игнорируя лишние данные в потоке.Добавление классов в иерархию: если добавить новый суперкласс, который сам реализует
Serializable, старые объекты десериализуются корректно — новые поля суперкласса получат значения по умолчанию.Удаление классов из иерархии: если удалить сериализуемый суперкласс, поля этого суперкласса в потоке игнорируются.
Добавление/удаление методов
writeObject/readObject: не влияет на serialVersionUID при явном объявлении.Изменение модификатора доступа поля: не влияет, так как сериализация обходит модификаторы.
Пометка поля как transient: поле перестаёт читаться из потока, получает значение по умолчанию.
// Версия 1
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
// Версия 2 — совместимое изменение: добавлено поле
public class User implements Serializable {
private static final long serialVersionUID = 1L; // тот же UID!
private String name;
private String email; // новое поле, старые объекты получат null
}
При десериализации объекта версии 1 в класс версии 2 поле
email получит null. Это безопасно, но требует, чтобы код класса корректно обрабатывал null (например, через инициализацию в readObject() или ленивую инициализацию в геттере).Обратная совместимость: новый объект в старый класс
Если объект сериализован версией 2 (с полем
email), а десериализуется классом версии 1 (без email), JVM прочитает из потока поле email, но не найдёт соответствующего поля в классе. В этом случае данные поля сохраняются во внутренней структуре ObjectInputStream как no-data или skipped fields, но не восстанавливаются. Это не вызывает исключения, но данные теряются.#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Расширенный контроль: writeObject, readObject, readResolve
writeObject и readObject
Класс может переопределить поведение сериализации, объявив приватные методы:
JVM вызывает эти методы через рефлексию, если они объявлены. Метод
Этот паттерн позволяет сериализовать
readResolve
Метод
Без
writeReplace
Аналогично,
Этот паттерн рекомендуется Джошуа Блохом (Joshua Bloch) в "Effective Java" как предпочтительный способ сериализации сложных объектов
#Java #для_новичков #beginner #IO #NIO #Serialize
writeObject и readObject
Класс может переопределить поведение сериализации, объявив приватные методы:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
JVM вызывает эти методы через рефлексию, если они объявлены. Метод
writeObject вызывается после записи дескриптора класса, но перед записью полей. Внутри него можно выполнить out.defaultWriteObject() (записать стандартные поля) и затем дописать произвольные данные.public class SecureDocument implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private transient String secretContent; // не сериализуем напрямую
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // записываем title и другие нетранзиентные поля
// Шифруем секретный контент перед записью
String encrypted = encrypt(secretContent);
out.writeUTF(encrypted);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // читаем title
// Дешифруем секретный контент
String encrypted = in.readUTF();
this.secretContent = decrypt(encrypted);
}
private String encrypt(String data) { /* ... */ return data; }
private String decrypt(String data) { /* ... */ return data; }
}Этот паттерн позволяет сериализовать
transient-поля в зашифрованном виде, выполнять валидацию инвариантов, восстанавливать производные структуры.readResolve
Метод
readResolve вызывается после полной десериализации объекта и позволяет заменить десериализованный экземпляр на другой объект. Это критично для реализации singleton и enum-like паттернов.Singleton — это порождающий паттерн проектирования, гарантирующий, что класс имеет только один экземпляр, и предоставляющий глобальную точку доступа к нему. При десериализации singleton механизм создаёт новый объект, нарушая гарантию единственности. readResolve возвращает существующий экземпляр.
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
// При десериализации возвращаем существующий INSTANCE, а не новый объект
private Object readResolve() {
return INSTANCE;
}
}Без
readResolve десериализация Singleton создала бы новый объект в куче, и getInstance() вернул бы старый INSTANCE, в то время как десериализованный объект — отдельная сущность. readResolve гарантирует, что любая десериализация вернёт канонический экземпляр.writeReplace
Аналогично,
writeReplace вызывается перед сериализацией и позволяет заменить объект на другой перед записью в поток. Используется для прокси-сериализации (serialization proxy pattern), когда внутреннее представление объекта не должно попадать в поток.Прокси-сериализация (serialization proxy pattern) — это паттерн, при котором вместо самого объекта сериализуется лёгкий прокси-объект (proxy), содержащий только необходимые данные, а при десериализации прокси через readResolve восстанавливает оригинальный объект через его конструктор или фабрику. Это защищает инварианты, предотвращает создание "сырых" объектов и позволяет менять внутреннюю структуру класса, не ломая совместимость.
public class ComplexObject implements Serializable {
private static final long serialVersionUID = 1L;
private final int x;
private final int y;
// инвариант: x >= 0 && y >= 0
public ComplexObject(int x, int y) {
if (x < 0 || y < 0) throw new IllegalArgumentException();
this.x = x;
this.y = y;
}
// Вместо себя сериализуем прокси
private Object writeReplace() {
return new SerializationProxy(x, y);
}
// Запрещаем десериализацию ComplexObject напрямую
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final int x;
private final int y;
SerializationProxy(int x, int y) {
this.x = x;
this.y = y;
}
// При десериализации прокси восстанавливаем ComplexObject через конструктор
private Object readResolve() {
return new ComplexObject(x, y); // инвариант проверяется!
}
}
}Этот паттерн рекомендуется Джошуа Блохом (Joshua Bloch) в "Effective Java" как предпочтительный способ сериализации сложных объектов
#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 3. Сериализация и форматы обмена
JSON — современный стандарт обмена данными
JSON (JavaScript Object Notation) — это текстовый формат сериализации данных, изначально выведенный из синтаксиса литералов объектов языка JavaScript. Несмотря на происхождение, JSON является полностью языконезависимым форматом и определён стандартом RFC 8259 (декабрь 2017), который заменил RFC 7159. Сегодня JSON де-факто является стандартным форматом обмена данными в веб-сервисах, конфигурационных файлах, логах, сообщениях брокеров и хранилищах документов.
Что такое JSON
JSON представляет собой текстовый формат, основанный на двух структурах:
Коллекция пар ключ-значение — в разных языках это реализуется как объект, запись, структура, словарь, хэш-таблица или ассоциативный массив.
Упорядоченный список значений — в большинстве языков это массив, вектор, список или последовательность.
Формат спроектирован так, чтобы быть легко читаемым для человека и легко парсящимся для машины. Это достигается за счёт минималистичного синтаксиса и строгих правил: каждый документ JSON — это либо один объект, либо один массив.
JSON как основа REST API
JSON стал доминирующим форматом представления ресурсов в REST API благодаря нескольким факторам:
Нативная поддержка в JavaScript, который исполняется в браузере клиента.
Простота генерации и парсинга на стороне сервера — любой язык имеет библиотеку для работы с JSON.
Читаемость при отладке — в отличие от бинарных форматов, JSON можно просмотреть в инструментах разработчика браузера или в логах.
Отсутствие необходимости в схеме для базового использования — в отличие от XML, JSON не требует DTD или XSD для валидации.
#Java #для_новичков #beginner #IO #NIO #Serialize #JSON
Глава 3. Сериализация и форматы обмена
JSON — современный стандарт обмена данными
JSON (JavaScript Object Notation) — это текстовый формат сериализации данных, изначально выведенный из синтаксиса литералов объектов языка JavaScript. Несмотря на происхождение, JSON является полностью языконезависимым форматом и определён стандартом RFC 8259 (декабрь 2017), который заменил RFC 7159. Сегодня JSON де-факто является стандартным форматом обмена данными в веб-сервисах, конфигурационных файлах, логах, сообщениях брокеров и хранилищах документов.
Сериализация — это процесс преобразования структуры данных или объекта в формат, который можно сохранить на диск, передать по сети или записать в базу данных, с возможностью последующего восстановления (десериализации) в исходную структуру.
RFC (Request for Comments) — серия документов Интернет-инженерного совета (IETF), описывающих стандарты, протоколы и технологии Интернета. RFC 8259 является официальной спецификацией формата JSON.
Что такое JSON
JSON представляет собой текстовый формат, основанный на двух структурах:
Коллекция пар ключ-значение — в разных языках это реализуется как объект, запись, структура, словарь, хэш-таблица или ассоциативный массив.
Упорядоченный список значений — в большинстве языков это массив, вектор, список или последовательность.
Формат спроектирован так, чтобы быть легко читаемым для человека и легко парсящимся для машины. Это достигается за счёт минималистичного синтаксиса и строгих правил: каждый документ JSON — это либо один объект, либо один массив.
Парсинг — это процесс анализа строки или потока данных в соответствии с правилами формального языка (грамматикой) с целью построения структурированного представления, обычно дерева разбора или абстрактного синтаксического дерева (AST).
JSON как основа REST API
REST (Representational State Transfer) — это архитектурный стиль проектирования распределённых систем, введённый Роем Филдингом в его докторской диссертации 2000 года. REST не является протоколом или стандартом в строгом смысле; это набор ограничений и принципов.
Ключевое ограничение — statelessness (отсутствие состояния): каждый запрос от клиента к серверу должен содержать всю информацию, необходимую для его обработки, и сервер не хранит контекст клиента между запросами.
JSON стал доминирующим форматом представления ресурсов в REST API благодаря нескольким факторам:
Нативная поддержка в JavaScript, который исполняется в браузере клиента.
Простота генерации и парсинга на стороне сервера — любой язык имеет библиотеку для работы с JSON.
Читаемость при отладке — в отличие от бинарных форматов, JSON можно просмотреть в инструментах разработчика браузера или в логах.
Отсутствие необходимости в схеме для базового использования — в отличие от XML, JSON не требует DTD или XSD для валидации.
DTD (Document Type Definition) и XSD (XML Schema Definition) — это механизмы определения структуры и допустимых элементов XML-документа. JSON изначально не имел встроенного механизма схем, хотя позже появились JSON Schema и OpenAPI.
#Java #для_новичков #beginner #IO #NIO #Serialize #JSON
👍3
Структура JSON
Спецификация JSON определяет шесть типов значений:
Объекты
Объект — это неупорядоченный набор пар ключ-значение, заключённый в фигурные скобки. Ключ — это строка, значение — любой допустимый JSON-тип. Пары разделяются запятыми.
В Java объект JSON обычно маппится на
Массивы
Массив — это упорядоченная последовательность значений, заключённая в квадратные скобки. Значения разделяются запятыми. Индексация начинается с нуля.
В Java массив JSON маппится на
Строки
Строка — это последовательность из нуля или более символов Unicode, заключённая в двойные кавычки. JSON поддерживает escape-последовательности:
Строки в JSON кодируются в UTF-8, UTF-16 или UTF-32. На практике UTF-8 доминирует, так как обеспечивает обратную совместимость с ASCII и экономит память для латинских текстов.
Числа
Числа в JSON записываются в десятичной системе и могут быть целыми или с плавающей точкой. Поддерживается экспоненциальная нотация. Нет различия между целыми и вещественными числами на уровне синтаксиса — это одна грамматическая категория.
Спецификация JSON не ограничивает точность или диапазон чисел. Однако на практике числа с плавающей точкой часто интерпретируются как IEEE 754 double-precision (64 бита), что даёт точность около 15–17 значащих цифр. Для финансовых расчётов, где критична точность до копеек, это может привести к ошибкам округления. В таких случаях числа передаются как строки.
Булевы значения
Два литерала:
Null
Литерал
Преимущества JSON
Читаемость
JSON читается без специальных инструментов. Структура вложенности визуально очевидна благодаря отступам. Это снижает порог входа для разработчиков и упрощает отладку. В отличие от бинарных форматов, JSON можно открыть в любом текстовом редакторе или даже в терминале через
Универсальная поддержка
Каждый современный язык программирования имеет библиотеку для работы с JSON. В Java экосистеме существуют Jackson, Gson, JSON-B, JSON-P, org.json и многие другие. В JavaScript JSON — нативный формат. В Python — модуль
Простота
Грамматика JSON минимальна: всего два контейнерных типа и четыре скалярных. Нет пространств имён, нет схем, нет атрибутов, нет комментариев (официально — комментарии не поддерживаются, хотя некоторые парсеры их принимают). Эта простота снижает сложность парсеров и уменьшает вероятность ошибок при сериализации.
Интеграция с веб-стеком
JSON родственен JavaScript, что делает его естественным выбором для веб-приложений. Fetch API, XMLHttpRequest, WebSocket — все эти API работают с JSON напрямую. Браузер предоставляет нативный объект
Недостатки JSON
Размер и текстовая природа
JSON — текстовый формат. Каждое число, булево значение или null записывается символами, а не бинарными данными. Ключи объектов повторяются в каждом документе. Для массовых данных это приводит к значительному overhead.
В этом примере ключи
Отсутствие строгой типизации
JSON не имеет схемы встроенной в сам документ. Поле
Отсутствие поддержки бинарных данных
JSON не имеет встроенного типа для бинарных данных. Для передачи бинарного контента (изображения, PDF, аудио) используется кодирование в Base64, что увеличивает размер данных примерно на 33%. Альтернативы — передача бинарных данных отдельно (multipart/form-data) или использование бинарных форматов.
Отсутствие комментариев
Спецификация JSON явно запрещает комментарии. Это создаёт проблемы для конфигурационных файлов, где комментарии полезны для документирования. Решения: использование JSON5 (расширение JSON с комментариями), YAML или TOML для конфигурации, либо отдельные файлы документации.
Производительность парсинга
Парсинг JSON требует посимвольного разбора строки, построения дерева объектов в памяти и часто — рефлексии для маппинга на Java-объекты. Для высоконагруженных систем это может стать bottleneck. Оптимизации включают потоковый парсинг (Jackson Streaming API), бинарные форматы на основе JSON (BSON, MessagePack) или полный отказ от JSON в пользу protobuf/Avro.
#Java #для_новичков #beginner #IO #NIO #Serialize #JSON
Спецификация JSON определяет шесть типов значений:
Объекты
Объект — это неупорядоченный набор пар ключ-значение, заключённый в фигурные скобки. Ключ — это строка, значение — любой допустимый JSON-тип. Пары разделяются запятыми.
{
"user": {
"id": 42,
"name": "Alice",
"active": true
}
}В Java объект JSON обычно маппится на
Map<String, Object> или на POJO (Plain Old Java Object — простой Java-объект без специальных требований к наследованию или аннотациям). Ключи в объекте JSON уникальны в пределах одного объекта, но спецификация не запрещает дубликаты — поведение при дубликатах зависит от парсера. Большинство парсеров используют последнее встреченное значение.Массивы
Массив — это упорядоченная последовательность значений, заключённая в квадратные скобки. Значения разделяются запятыми. Индексация начинается с нуля.
{
"orders": [
{ "id": 1, "total": 99.99 },
{ "id": 2, "total": 150.00 }
]
}В Java массив JSON маппится на
List<Object>, Object[] или типизированный список List<Order>, если используется библиотека с поддержкой дженериков.Строки
Строка — это последовательность из нуля или более символов Unicode, заключённая в двойные кавычки. JSON поддерживает escape-последовательности:
\n (перевод строки), \t (табуляция), \\ (обратный слеш), \" (кавычка), \uXXXX (символ Unicode в шестнадцатеричном виде).{
"description": "Line 1\nLine 2\tTabbed",
"emoji": "\uD83D\uDE00"
}Строки в JSON кодируются в UTF-8, UTF-16 или UTF-32. На практике UTF-8 доминирует, так как обеспечивает обратную совместимость с ASCII и экономит память для латинских текстов.
UTF-8 (Unicode Transformation Format, 8-bit) — это кодировка переменной длины, которая кодирует символы Unicode от 1 до 4 байтами. Символы ASCII (0–127) занимают 1 байт, что делает UTF-8 эффективной для англоязычных текстов.
Числа
Числа в JSON записываются в десятичной системе и могут быть целыми или с плавающей точкой. Поддерживается экспоненциальная нотация. Нет различия между целыми и вещественными числами на уровне синтаксиса — это одна грамматическая категория.
{
"integer": 42,
"negative": -17,
"float": 3.14159,
"exponential": 6.022e23,
"small": 1e-10
}Спецификация JSON не ограничивает точность или диапазон чисел. Однако на практике числа с плавающей точкой часто интерпретируются как IEEE 754 double-precision (64 бита), что даёт точность около 15–17 значащих цифр. Для финансовых расчётов, где критична точность до копеек, это может привести к ошибкам округления. В таких случаях числа передаются как строки.
IEEE 754 — это стандарт представления чисел с плавающей точкой в компьютерах. Double-precision использует 64 бита: 1 бит знака, 11 бит экспоненты и 52 бита мантиссы. Не все десятичные дроби точно представимы в двоичной системе, отсюда ошибки округления.
Булевы значения
Два литерала:
true и false. В Java маппятся на примитив boolean или объект Boolean.{
"enabled": true,
"debug": false
}Null
Литерал
null представляет отсутствие значения. В Java маппится на null.{
"middleName": null
}Преимущества JSON
Читаемость
JSON читается без специальных инструментов. Структура вложенности визуально очевидна благодаря отступам. Это снижает порог входа для разработчиков и упрощает отладку. В отличие от бинарных форматов, JSON можно открыть в любом текстовом редакторе или даже в терминале через
curl.Универсальная поддержка
Каждый современный язык программирования имеет библиотеку для работы с JSON. В Java экосистеме существуют Jackson, Gson, JSON-B, JSON-P, org.json и многие другие. В JavaScript JSON — нативный формат. В Python — модуль
json в стандартной библиотеке. В Go — пакет encoding/json. Это означает, что сервис на Java может бесшовно обмениваться данными с сервисом на Python, Go или Node.js.Простота
Грамматика JSON минимальна: всего два контейнерных типа и четыре скалярных. Нет пространств имён, нет схем, нет атрибутов, нет комментариев (официально — комментарии не поддерживаются, хотя некоторые парсеры их принимают). Эта простота снижает сложность парсеров и уменьшает вероятность ошибок при сериализации.
Интеграция с веб-стеком
JSON родственен JavaScript, что делает его естественным выбором для веб-приложений. Fetch API, XMLHttpRequest, WebSocket — все эти API работают с JSON напрямую. Браузер предоставляет нативный объект
JSON с методами parse() и stringify().Недостатки JSON
Размер и текстовая природа
JSON — текстовый формат. Каждое число, булево значение или null записывается символами, а не бинарными данными. Ключи объектов повторяются в каждом документе. Для массовых данных это приводит к значительному overhead.
{
"users": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
}В этом примере ключи
"id" и "name" повторяются для каждого элемента. В бинарном формате (например, Protocol Buffers) ключи заменяются числовыми тегами, а строковые значения могут быть закодированы более эффективно. JSON также не поддерживает сжатие на уровне формата — для уменьшения размера применяется внешнее сжатие (gzip, brotli).Protocol Buffers (protobuf) — бинарный формат сериализации, разработанный Google. В отличие от JSON, protobuf требует предварительного определения схемы (proto-файл), но обеспечивает компактное бинарное представление и строгую типизацию.
Отсутствие строгой типизации
JSON не имеет схемы встроенной в сам документ. Поле
"age" может содержать число в одном запросе и строку в другом. Это создаёт риск ошибок при десериализации, если клиент и сервер не согласовали контракт. Для решения этой проблемы используются JSON Schema, OpenAPI/Swagger или кодогенерация по proto-файлам.JSON Schema — это спецификация (draft 2020-12) для описания структуры JSON-документов. Она позволяет определять типы полей, обязательность, диапазоны значений, регулярные выражения для строк и другие ограничения.
Отсутствие поддержки бинарных данных
JSON не имеет встроенного типа для бинарных данных. Для передачи бинарного контента (изображения, PDF, аудио) используется кодирование в Base64, что увеличивает размер данных примерно на 33%. Альтернативы — передача бинарных данных отдельно (multipart/form-data) или использование бинарных форматов.
Base64 — это схема кодирования бинарных данных в текстовый формат с использованием 64 символов ASCII (A–Z, a–z, 0–9, +, /). Каждые 3 байта (24 бита) кодируются 4 символами Base64 (по 6 бит каждый), отсюда overhead в 33%.
Отсутствие комментариев
Спецификация JSON явно запрещает комментарии. Это создаёт проблемы для конфигурационных файлов, где комментарии полезны для документирования. Решения: использование JSON5 (расширение JSON с комментариями), YAML или TOML для конфигурации, либо отдельные файлы документации.
Производительность парсинга
Парсинг JSON требует посимвольного разбора строки, построения дерева объектов в памяти и часто — рефлексии для маппинга на Java-объекты. Для высоконагруженных систем это может стать bottleneck. Оптимизации включают потоковый парсинг (Jackson Streaming API), бинарные форматы на основе JSON (BSON, MessagePack) или полный отказ от JSON в пользу protobuf/Avro.
Bottleneck — это узкое место в системе, которое ограничивает общую производительность. В контексте JSON-парсинга bottleneck часто возникает из-за аллокации объектов в куче и работы GC при обработке больших JSON-документов.
#Java #для_новичков #beginner #IO #NIO #Serialize #JSON
👍3
Путь байтов в памяти JVM при работе с JSON
1. Получение JSON-строки
JSON-данные обычно поступают в JVM одним из трёх способов: как строка из HTTP-ответа, как содержимое файла, считанное через
Если JSON приходит по HTTP, процесс начинается на уровне сетевого стека ОС. Пакеты TCP собираются в сегменты, данные копируются из kernel space (пространство ядра) в user space (пользовательское пространство) в буфер сокета. Servlet-контейнер (Tomcat, Jetty, Netty) или фреймворк (Spring Boot) читает байты из сокета в буфер — обычно это
2. Декодирование байтов в строку
Байты из сети или файла представляют JSON в кодировке UTF-8. JVM декодирует их в объект
В Java 9+ строки хранятся как
3. Парсинг JSON: аллокация объектов в куче
Парсер (Jackson, Gson или стандартный
Jackson создаст следующие объекты в куче:
Один
Один
Два
Четыре
Два
Строковые ключи
Все эти объекты создаются в Young Generation, в области Eden. Для JSON-документа размером 10 КБ количество объектов может достигать сотен. При обработке тысяч запросов в секунду это создаёт огромное давление на Minor GC.
4. Маппинг на POJO: рефлексия и дополнительные аллокации
При десериализации JSON в Java-объекты (POJO) парсер использует рефлексию — механизм Java для интроспекции классов во время выполнения.
Рефлексия требует:
Создания объекта
Вызова
Установки полей через
Jackson оптимизирует это через
5. Работа GC с JSON-объектами
После десериализации JSON-строка и промежуточные структуры парсера (токены, узлы дерева) становятся мусором. Если использовался Jackson в режиме потокового парсинга (Streaming API), дерево не строится целиком — объекты создаются по мере чтения и сразу собираются GC. В режиме древовидного парсинга (Tree Model) всё дерево JSON хранится в памяти до явного освобождения.
Для высоконагруженных систем критично минимизировать аллокации:
В этом примере
6. Сериализация: обратный путь
При сериализации Java-объекта в JSON происходит обратный процесс:
Jackson обходит поля объекта через кэшированные сериализаторы.
Генерирует JSON-текст как
Если вывод идёт в
Если вывод идёт в
При сериализации в HTTP-ответ через Spring Boot используется
7. Ключевые строки и пул строк
Ключи JSON-объектов (
Интернирование строк — это процесс помещения строки в пул строк JVM через метод
8. Бинарные JSON-форматы и память
Для снижения давления на GC и размера данных используются бинарные форматы на основе JSON:
BSON (Binary JSON) — используется в MongoDB. Содержит типовые теги и длины полей, что ускоряет навигацию без полного парсинга.
MessagePack — бинарный формат, совместимый с JSON по структуре, но более компактный.
CBOR (Concise Binary Object Representation) — стандарт RFC 8949, разработанный IETF как бинарная альтернатива JSON.
Эти форматы парсятся быстрее, так как не требуют посимвольного разбора текста, и создают меньше объектов в куче.
#Java #для_новичков #beginner #IO #NIO #Serialize #JSON
1. Получение JSON-строки
JSON-данные обычно поступают в JVM одним из трёх способов: как строка из HTTP-ответа, как содержимое файла, считанное через
Files.readString(), или как сообщение из брокера (Kafka, RabbitMQ).Если JSON приходит по HTTP, процесс начинается на уровне сетевого стека ОС. Пакеты TCP собираются в сегменты, данные копируются из kernel space (пространство ядра) в user space (пользовательское пространство) в буфер сокета. Servlet-контейнер (Tomcat, Jetty, Netty) или фреймворк (Spring Boot) читает байты из сокета в буфер — обычно это
byte[] или ByteBuffer в куче JVM.Kernel space и user space — это два режима работы процессора. Kernel space — привилегированный режим, в котором работает код операционной системы и драйверов. User space — непривилегированный режим, в котором работают обычные приложения, включая JVM. Переход между режимами (context switch) — дорогая операция.
2. Декодирование байтов в строку
Байты из сети или файла представляют JSON в кодировке UTF-8. JVM декодирует их в объект
String — это массив byte[] (начиная с Java 9; ранее char[]) в куче, плюс объект-обёртка String со ссылкой на массив, хэш-кодом и флагами.В Java 9+ строки хранятся как
byte[] с кодировкой LATIN1 (1 байт на символ) или UTF-16 (2 байта на символ), в зависимости от содержимого. Компактные строки (compact strings) экономят память для ASCII-текстов. Для JSON, состоящего преимущественно из ASCII, это означает экономию ~50% по сравнению со старым char[].Compact strings — оптимизация, введённая в Java 9 (JEP 254). Строки, содержащие только символы Latin-1 (коды 0–255), хранятся в массиве
byte[]по одному байту на символ. Если встречается символ вне Latin-1, массив переключается на UTF-16 (2 байта на символ).
3. Парсинг JSON: аллокация объектов в куче
Парсер (Jackson, Gson или стандартный
org.json) читает строку посимвольно и строит дерево объектов. Для JSON-документа:{
"users": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
}Jackson создаст следующие объекты в куче:
Один
JsonNode (или LinkedHashMap при десериализации в Map) для корневого объекта.Один
ArrayNode (или ArrayList) для массива "users".Два
ObjectNode (или LinkedHashMap) для объектов пользователей.Четыре
IntNode / Integer для полей "id".Два
TextNode / String для полей "name".Строковые ключи
"users", "id", "name" — если они не интернированы, создаются как отдельные объекты String.Все эти объекты создаются в Young Generation, в области Eden. Для JSON-документа размером 10 КБ количество объектов может достигать сотен. При обработке тысяч запросов в секунду это создаёт огромное давление на Minor GC.
4. Маппинг на POJO: рефлексия и дополнительные аллокации
При десериализации JSON в Java-объекты (POJO) парсер использует рефлексию — механизм Java для интроспекции классов во время выполнения.
Рефлексия требует:
Создания объекта
Constructor через Class.getDeclaredConstructor().Вызова
Constructor.newInstance() — аллокация POJO в куче.Установки полей через
Field.set() или вызова сеттеров.Jackson оптимизирует это через
SerializerCache и DeserializerCache — кэши, хранящие скомпилированные сериализаторы и десериализаторы. После первого использования класса его метаданные кэшируются, и последующие операции не требуют полной рефлексии. Однако первый парсинг класса — дорогая операция, создающая множество временных объектов.5. Работа GC с JSON-объектами
После десериализации JSON-строка и промежуточные структуры парсера (токены, узлы дерева) становятся мусором. Если использовался Jackson в режиме потокового парсинга (Streaming API), дерево не строится целиком — объекты создаются по мере чтения и сразу собираются GC. В режиме древовидного парсинга (Tree Model) всё дерево JSON хранится в памяти до явного освобождения.
Для высоконагруженных систем критично минимизировать аллокации:
// Потоковый парсинг Jackson — минимум аллокаций
public void processLargeJson(InputStream in) throws IOException {
// JsonFactory — лёгкий фабричный объект для создания парсеров
JsonFactory factory = new JsonFactory();
// JsonParser — потоковый парсер, читает JSON без построения дерева
try (JsonParser parser = factory.createParser(in)) {
while (parser.nextToken() != JsonToken.END_OBJECT) {
String fieldName = parser.getCurrentName();
if ("users".equals(fieldName)) {
parser.nextToken(); // переходим к массиву
while (parser.nextToken() == JsonToken.START_OBJECT) {
// Обрабатываем каждый объект пользователя
int id = 0;
String name = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
String key = parser.getCurrentName();
parser.nextToken();
if ("id".equals(key)) {
id = parser.getIntValue();
} else if ("name".equals(key)) {
name = parser.getValueAsString();
}
}
// Обрабатываем пользователя без создания промежуточных объектов
processUser(id, name);
}
}
}
}
}
В этом примере
JsonParser читает JSON как поток токенов, не создавая дерево объектов. Память используется минимально, и давление на GC снижается на порядки по сравнению с mapper.readValue(json, UserList.class).6. Сериализация: обратный путь
При сериализации Java-объекта в JSON происходит обратный процесс:
Jackson обходит поля объекта через кэшированные сериализаторы.
Генерирует JSON-текст как
char[] или byte[] внутри StringWriter или OutputStream.Если вывод идёт в
OutputStream (HTTP-ответ, файл), байты записываются напрямую без промежуточной строки.Если вывод идёт в
String, создаётся объект String в куче, который затем передаётся дальше.При сериализации в HTTP-ответ через Spring Boot используется
MappingJackson2HttpMessageConverter, который пишет JSON напрямую в OutputStream ответа, минуя создание промежуточной строки. Это экономит аллокации и снижает нагрузку на GC.7. Ключевые строки и пул строк
Ключи JSON-объектов (
"id", "name", "users") — это строки, которые повторяются в каждом документе. Jackson может интернировать часто встречающиеся ключи через JsonFactory.Feature.INTERN_FIELD_NAMES. Интернирование помещает строки в пул строк (String Pool) в Metaspace, делая их доступными для повторного использования между запросами. Это снижает аллокации, но увеличивает потребление Metaspace — пул строк не собирается обычным GC.Интернирование строк — это процесс помещения строки в пул строк JVM через метод
String.intern(). Если строка с таким содержимым уже есть в пуле, возвращается существующий объект; иначе строка добавляется в пул. Интернирование экономит память при множестве одинаковых строк, но может привести к утечке памяти в Metaspace при интернировании уникальных строк.8. Бинарные JSON-форматы и память
Для снижения давления на GC и размера данных используются бинарные форматы на основе JSON:
BSON (Binary JSON) — используется в MongoDB. Содержит типовые теги и длины полей, что ускоряет навигацию без полного парсинга.
MessagePack — бинарный формат, совместимый с JSON по структуре, но более компактный.
CBOR (Concise Binary Object Representation) — стандарт RFC 8949, разработанный IETF как бинарная альтернатива JSON.
Эти форматы парсятся быстрее, так как не требуют посимвольного разбора текста, и создают меньше объектов в куче.
#Java #для_новичков #beginner #IO #NIO #Serialize #JSON
👍4
Раздел 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
Раздел 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