Що таке кодування String?
Комп'ютер розуміє тільки числа. Кодування — це «словник», який каже: «Символу 'А' відповідає число 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. Що таке компактні рядки в Java 9+]]