Что такое String encoding (кодировка)?
Компьютер понимает только числа. Кодировка — это «словарь», который говорит: «Символу 'А' соответствует число 1040, символу 'B' — число 66» и т.д.
🟢 Junior Level
Кодировка (encoding) — это набор правил, который определяет, как символы (буквы, цифры, знаки) превращаются в байты для хранения и передачи, и обратно.
Компьютер понимает только числа. Кодировка — это «словарь», который говорит: «Символу ‘А’ соответствует число 1040, символу ‘B’ — число 66» и т.д.
Простая аналогия: Кодировка — как язык перевода. Вы говорите переводчику: «Переведи эту фразу на немецкий» (encode), а потом: «Переведи обратно на русский» (decode). Если оба переводчика используют одинаковый словарь — всё поймут правильно.
Пример:
String s = "Привет";
// Encode: String → byte[]
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
// Decode: byte[] → String
String restored = new String(bytes, StandardCharsets.UTF_8);
Самые важные кодировки: | Кодировка | Для чего | Размер “Привет” | | —————- | ——————————————————————— | —————– | | UTF-8 | Интернет, файлы, базы данных | 12 байт | | UTF-16 | Внутренний формат Java 8 и ранее / для нелатинских символов в Java 9+ | 12 байт | | Windows-1251 | Старые Windows-системы | 6 байт |
Главное правило: ВСЕГДА явно указывайте кодировку! Никогда не надейтесь на кодировку по умолчанию — она разная на Windows и Linux.
🟡 Middle Level
Как Java хранит строки внутри
До Java 9: Строки хранились как char[] в кодировке UTF-16 (2 байта на символ всегда).
Java 9+ (Compact Strings): Java автоматически выбирает оптимальный формат:
- Только латинские символы (U+0000–U+00FF) → Latin-1 (1 байт/символ)
- Есть нелатинские символы → UTF-16 (2 байта/символ)
- Решение хранится в поле
coder(0 = Latin-1, 1 = UTF-16)
Конвертация при I/O
Когда строка выходит за пределы JVM (запись в файл, отправка по сети, запрос в БД), она должна быть преобразована в байты с указанной кодировкой:
// ✅ ХОРОШО — явная кодировка
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
String str = new String(bytes, StandardCharsets.UTF_8);
// ❌ ПЛОХО — кодировка по умолчанию зависит от ОС!
byte[] bytes = str.getBytes(); // Windows-1251 на Windows, UTF-8 на Linux
String str = new String(bytes); // Может дать «кракозябры»
Таблица типичных ошибок
| Ошибка | Последствия | Решение |
|---|---|---|
new String(bytes) без кодировки |
«Кракозябры» при смене ОС | Всегда new String(bytes, StandardCharsets.UTF_8) |
getBytes() без кодировки |
Невозможно декодировать на другой системе | Всегда getBytes(StandardCharsets.UTF_8) |
Путать str.length() и bytes.length |
Неправильная логика работы с данными | "Привет".length() = 6, "Привет".getBytes(UTF_8).length = 12 |
Сравнение популярных кодировок
| Кодировка | Байт/символ | Поддержка | Совместимость |
|---|---|---|---|
| UTF-8 | 1–4 (variable) | Все Unicode | ASCII-совместимая |
| UTF-16 LE/BE | 2–4 | Все Unicode | Требует BOM или соглашения |
| Latin-1 | 1 | Western European | Не поддерживает кириллицу |
| Windows-1251 | 1 | Кириллица | Только Windows |
| ASCII | 1 | 128 символов | Не поддерживает кириллицу |
Когда НЕ использовать конвертацию в String
- Binary data (изображения, protobuf, архивы) — работайте с
byte[]напрямую - Ultra-low-latency системы — конвертация добавляет 50–200ns overhead
- Потоковые данные неизвестной кодировки — используйте
BOMInputStreamили автодетект
🔴 Senior Level
Internal Implementation — Compact Strings и кодирование
public final class String {
@Stable
private final byte[] value; // byte[] вместо char[] (Java 9+)
private final byte coder; // LATIN1=0 или UTF16=1
}
При вызове getBytes(Charset) происходит:
- Проверка
coderтекущего String - Выбор алгоритма кодирования в
StringCoding.encode(value, coder, charset) - Для UTF-8: посимвольная конвертация через
CharsetEncoder- Latin-1 → UTF-8: 1 байт → 1 байт (ASCII), 1 байт → 2 байта (extended Latin-1)
- UTF-16 → UTF-8: 2 байта → 1–3 байта (зависит от code point)
При new String(byte[], Charset):
StringCoding.decode(bytes, charset)→byte[] value+coder- Если декодер обнаружил невалидные байты → замена на
\uFFFD(replacement character)
Edge Cases (минимум 3)
1. BOM (Byte Order Mark):
byte[] withBom = {(byte)0xEF, (byte)0xBB, (byte)0xBF, (byte)'H', (byte)'i'};
String s = new String(withBom, StandardCharsets.UTF_8);
// s.charAt(0) = '\uFEFF' — невидимый символ!
// s.startsWith("Hi") → false!
Решение: BOMInputStream (Apache Commons IO) или ручная проверка первых 3 байт.
2. Malformed input — невалидные UTF-8 последовательности:
byte[] bad = {(byte) 0xFF, (byte) 0xFE};
String s = new String(bad, StandardCharsets.UTF_8);
// Результат: "\uFFFD\uFFFD" — замена на replacement character
Поведение зависит от CodingErrorAction (REPLACE по умолчанию, можно изменить на REPORT или IGNORE).
3. Charset encoding roundtrip — потеря данных:
String s = "Привет";
byte[] ascii = s.getBytes(StandardCharsets.US_ASCII);
// "??????" — кириллица не поддерживается в ASCII, заменена на '?'
// Обратная конвертация уже невозможна — данные утеряны!
4. Security — encoding bypass:
// SQL injection через multi-byte encoding bypass
String input = "%C0%27 OR 1=1 --";
// Некоторые старые системы декодируют %C0%27 как одинарную кавычку
// Всегда валидируйте input ПОСЛЕ декодирования, а не до!
Производительность
| Операция | UTF-8 | UTF-16 | Latin-1 |
|---|---|---|---|
| Encode “Hello” | ~5ns | ~3ns | ~2ns |
| Encode “Привет” | ~15ns | ~5ns | N/A |
| Decode 12 bytes | ~10ns | ~5ns | ~3ns |
| Encode 10KB text | ~500ns | ~150ns | ~80ns |
// UTF-8 для кириллицы = 2 байта/символ с multi-byte encoding логикой. // UTF-16 = прямой 1:1 copy для BMP символов — быстрее.
Память (Java 9+):
- Latin-1 строка: 24 bytes (объект) + 16 + N (byte[]) → ~40+N bytes
- UTF-16 строка: 24 bytes (объект) + 16 + 2N (byte[]) → ~40+2N bytes
- Экономия для ASCII/Latin-1: ~50% памяти
Thread Safety
Классы StandardCharsets, Charset, CharsetEncoder, CharsetDecoder:
StandardCharsets.UTF_8— thread-safe (immutable singleton)CharsetEncoder/CharsetDecoder— NOT thread-safe! Один инстанс нельзя использовать из нескольких потоков одновременно- Решение: создавайте новый encoder/decoder на каждый поток или используйте
ThreadLocal
Production War Story
Сценарий: Микросервис на Linux (Spring Boot) читает данные из legacy Windows-системы.
Windows-система отправляла данные в Windows-1251. Linux-сервис читал байты как UTF-8 (дефолтная кодировка Linux) → «кракозябры» в логах. Клиенты жаловались на некорректные ответы.
Диагностика: Charset.defaultCharset() на Linux = UTF-8, на Windows = Windows-1251 (или Cp1252).
Fix:
// Явное указание кодировки на уровне протокола
String text = new String(bytes, Charset.forName("Windows-1251"));
Долгосрочное решение: договориться об UTF-8 на уровне API-контракта между всеми сервисами.
Сценарий 2: HTTP API response с кириллицей — без Content-Type: application/json; charset=UTF-8 браузер интерпретировал ответ как ISO-8859-1, кириллица превращалась в «кракозябры».
Monitoring
# Проверить дефолтную кодировку
java -XX:+PrintFlagsFinal -version 2>&1 | grep file.encoding
# java.nio.file.DefaultCharset = UTF-8
# Проверить доступные кодировки
jrunscript -e "print(java.nio.charset.Charset.availableCharsets().keySet())"
# JFR — можно отслеживать I/O операции с кодировками
java -XX:StartFlightRecording=filename=recording.jfr ...
# GC логи — косвенно: меньший размер строк → меньший heap
java -Xlog:gc*:file=gc.log ...
// Runtime-проверка кодировки
System.out.println(Charset.defaultCharset());
// JOL — реальный размер String после конвертации
String latin = "Hello";
String cyrillic = "Привет";
System.out.println(GraphLayout.parseInstance(latin).toFootprint()); // ~34 bytes (Latin-1)
System.out.println(GraphLayout.parseInstance(cyrillic).toFootprint()); // ~42 bytes (UTF-16)
Best Practices для Highload
- Всегда указывайте
Charset— используйтеStandardCharsets.UTF_8(константа, без аллокаций) - Для JSON/XML/HTTP: UTF-8 — стандарт де-факто
- Для binary data: работайте с
byte[]/ByteBufferнапрямую, не конвертируйте в String CharsetEncoder/CharsetDecoder— не шарьте между потоками, создавайте per-thread или используйтеThreadLocal- Для ultra-low-latency: избегайте String для I/O — используйте
ByteBuf(Netty), zero-copy подходы - BOM handling:
BOMInputStreamили ручная проверка первых байт перед декодированием - Security: валидируйте input ПОСЛЕ декодирования, используйте constant-time сравнение для secrets
🎯 Шпаргалка для интервью
Обязательно знать:
- Кодировка — правила преобразования символов в байты и обратно
- UTF-8 — стандарт де-факто для интернета, JSON, HTTP (ASCII-совместимая, 1-4 байта/символ)
- Java 9+: Compact Strings — Latin-1 (1 байт/символ) или UTF-16 (2 байта/символ) автоматически
getBytes()без кодировки зависит от ОС — на Windows и Linux будет разный результатCharsetEncoder/CharsetDecoder— NOT thread-safe, нельзя шарить между потоками- BOM (Byte Order Mark) — невидимый символ
\uFEFFв начале UTF-8 файла
Частые уточняющие вопросы:
- Почему нельзя использовать
getBytes()без кодировки? — Кодировка по умолчанию зависит от ОС. На Windows может быть Cp1251, на Linux — UTF-8. - Как обрабатывать BOM? —
BOMInputStream(Apache Commons IO) или ручная проверка первых 3 байтов (EF BB BF). - Что будет при декодировании невалидных UTF-8 байтов? — Замена на
\uFFFD(replacement character). Можно настроить черезCodingErrorAction. - Как Java 9+ хранит строки? —
byte[]+coder: Latin-1 для U+0000–U+00FF, UTF-16 для остальных.
Красные флаги (НЕ говорить):
- ❌ “Кодировка по умолчанию — всегда UTF-8” — зависит от ОС и локали
- ❌ “
CharsetEncoderthread-safe” — НЕ thread-safe, нуженThreadLocalили per-call создание - ❌ “Можно конвертировать binary данные в String” — потеря данных, работайте с
byte[]напрямую - ❌ “BOM — это проблема только UTF-16” — UTF-8 тоже может иметь BOM
Связанные темы:
- [[18. Как правильно конвертировать String в byte[] и обратно]]
- [[19. Что такое compact strings в Java 9+]]