Як правильно перетворити String в byte[] і назад?
Конвертація рядка на байти і назад — одна з найчастіших операцій при роботі з файлами, мережею і базами даних.
🟢 Junior Level
Конвертація рядка на байти і назад — одна з найчастіших операцій при роботі з файлами, мережею і базами даних.
Головне правило: ЗАВЖДИ явно вказуйте кодування!
import java.nio.charset.StandardCharsets;
String str = "Hello, World!";
// String → byte[]
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
// byte[] → String
String restored = new String(bytes, StandardCharsets.UTF_8);
System.out.println(restored); // "Hello, World!"
Ніколи не робіть так:
// ❌ ПОГАНО — використовує кодування ОС (різне на Windows і Linux!)
byte[] bytes = str.getBytes();
String restored = new String(bytes);
Чому: Кодування за замовчуванням залежить від операційної системи. На Windows це може бути Windows-1251, на Linux — UTF-8. Рядок, сконвертований на одній системі, перетвориться на «кракозябри» на іншій.
**String → byte[] — як запис розмови на диктофон (текст → байти). byte[] → String — як відтворення запису (байти → текст). Якщо неправильно обрати формат запису (кодування), почуєте шум замість слів.
🟡 Middle Level
Правильні способи конвертації
String → byte[]:
// Java 7+ — рекомендований спосіб (константа, без алокацій)
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
// Якщо кодування визначається динамічно
byte[] bytes = str.getBytes(Charset.forName("Windows-1251"));
byte[] → String:
// Java 7+ — рекомендований спосіб
String str = new String(bytes, StandardCharsets.UTF_8);
// Якщо кодування динамічне
String str = new String(bytes, Charset.forName("Windows-1251"));
Потокова робота з великими даними:
// Читання файлу з кодуванням
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line = reader.readLine();
}
// Запис файлу з кодуванням
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("Hello");
}
Таблиця типових помилок
| Помилка | Наслідки | Рішення |
|---|---|---|
getBytes() без кодування |
«Кракозябри» при зміні ОС | Завжди getBytes(StandardCharsets.UTF_8) |
new String(bytes) без кодування |
Неможливо відновити початковий текст | Завжди new String(bytes, StandardCharsets.UTF_8) |
Плутати str.length() з bytes.length |
Неправильна логіка роботи з даними | "Привіт".length() = 6, "Привіт".getBytes(UTF_8).length = 12 |
| Конвертація binary даних в String | Втрата даних, corruption | Для binary — використовуйте byte[]/ByteBuffer напряму |
Порівняння кодувань для конвертації
| Кодування | Байт/символ (Latin) | Байт/символ (кирилиця) | Коли використовувати |
|---|---|---|---|
| UTF-8 | 1 | 2 | Інтернет, JSON, HTTP — стандарт де-факто |
| UTF-16 | 2 | 2 | Внутрішній формат Java, Windows API |
| Latin-1 | 1 | N/A | Western European only |
| Windows-1251 | N/A | 1 | Legacy Windows-системи |
Коли НЕ конвертувати String → byte[]
- Binary data (зображення, PDF, protobuf) — працюйте з
byte[]напряму - Ultra-low-latency системи — конвертація додає 50–200ns overhead
- Дані невідомого кодування — використовуйте автодетект або
BOMInputStream
🔴 Senior Level
Internal Implementation
String.getBytes(Charset):
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(value, coder, charset);
}
StringCoding.encode — що відбувається:
- Перевірка
coder(Latin-1 або UTF-16) - Вибір алгоритму кодування:
- Latin-1 → UTF-8: 1 байт → 1 байт (ASCII), 1 байт → 2 байти (extended Latin-1, U+0080–U+00FF)
- UTF-16 → UTF-8: 2 байти → 1–3 байти (залежить від code point)
- Для UTF-8: посимвольна конвертація через
CharsetEncoder - Алокація нового
byte[]з потрібним розміром
new String(byte[], Charset):
public String(byte[] bytes, Charset charset) {
this(bytes, 0, bytes.length, charset);
}
// → StringCoding.decode → CharsetDecoder → byte[] value + coder
CharsetDecoderдекодує байти в символи- Визначається
coder: якщо всі символи в діапазоні U+0000–U+00FF → LATIN1, інакше → UTF16 - Невалідні байти → заміна на
\uFFFD(replacement character) - Створюється новий String з
byte[] value+coder
Trade-offs кодувань
UTF-8:
- Плюси: Стандарт де-факто, ASCII-сумісна, variable-length (економія для латиниці)
- Мінуси: Кирилиця = 2 байти/символ, ієрогліфи = 3 байти, variable-length ускладнює random access
UTF-16:
- Плюси: Фіксований розмір для BMP (2 bytes/char), внутрішній формат Java (мінімум конвертації)
- Мінуси: Endianness (LE vs BE), BOM, 2x розмір для ASCII, не ASCII-сумісна
Latin-1 (ISO-8859-1):
- Плюси: 1 байт/символ, мінімальний overhead, фіксований розмір
- Мінуси: Тільки 256 символів, кирилиця в розширеній Latin-1, не Unicode
Edge Cases (мінімум 3)
1. BOM (Byte Order Mark):
// UTF-8 файл з BOM: EF BB BF
byte[] bomUtf8 = {(byte)0xEF, (byte)0xBB, (byte)0xBF, 'H', 'i'};
String s = new String(bomUtf8, StandardCharsets.UTF_8);
// s.charAt(0) = '\uFEFF' — невидимий BOM символ!
// s.startsWith("Hi") → false!
Рішення: BOMInputStream (Apache Commons IO) або ручна перевірка перших 3 байт:
if (bytes.length >= 3 && bytes[0] == (byte)0xEF && bytes[1] == (byte)0xBB && bytes[2] == (byte)0xBF) {
s = new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8);
}
2. Malformed input — невалідні UTF-8 байти:
byte[] malformed = {(byte) 0xFF, (byte) 0xFE, (byte) 0x80};
String s = new String(malformed, StandardCharsets.UTF_8);
// Невалідні послідовності → '\uFFFD' (replacement character)
// s = "\uFFFD\uFFFD\uFFFD"
Поведінка керується через CodingErrorAction:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPORT); // Кидає MalformedInputException
decoder.onMalformedInput(CodingErrorAction.IGNORE); // Пропускає
decoder.onMalformedInput(CodingErrorAction.REPLACE); // За замовчуванням → \uFFFD
3. Truncated multi-byte sequences:
// Кирилиця 'А' = D0 90 в UTF-8
byte[] truncated = {(byte) 0xD0}; // Тільки перший байт
String s = new String(truncated, StandardCharsets.UTF_8);
// '\uFFFD' — неповна послідовність
Критично для streaming I/O: якщо буфер обривається посередині multi-byte послідовності, потрібно зберегти «хвіст» і додати до наступного буфера.
4. Security — encoding bypass:
// SQL injection через multi-byte encoding bypass
String input = "%C0%27 OR 1=1 --";
// В деяких старих системах %C0%27 декодується як одинарна лапка
// Це обходить фільтри, які перевіряють input ДО декодування
// Завжди валідуйте input ПІСЛЯ декодування!
5. Surrogate pairs і довжина байтів:
String emoji = "\uD83D\uDE00"; // "😀" — surrogate pair
emoji.length(); // 2 (char)
emoji.getBytes(StandardCharsets.UTF_8).length; // 4 (bytes)
emoji.getBytes(StandardCharsets.UTF_16).length; // 6 (2 BOM + 4 data)
// StandardCharsets.UTF_16 включає BOM. UTF_16BE/UTF_16LE — без BOM, було б 4 байти.
// Не використовуйте bytes.length для визначення кількості символів!
Продуктивність
| Операція | UTF-8 | UTF-16 | Latin-1 |
|---|---|---|---|
| Encode 100 chars (Latin-1) | ~100ns | ~50ns | ~20ns |
| Encode 100 chars (Cyrillic) | ~200ns | ~50ns | N/A |
| Decode 200 bytes (UTF-8) | ~150ns | N/A | N/A |
| Decode 200 bytes (UTF-16) | N/A | ~40ns | N/A |
| Encode 10KB text (mixed) | ~8μs | ~3μs | ~2μs |
Алокації:
getBytes(UTF_8): алокує новийbyte[]розміру ~N–3N (залежить від вмісту)new String(bytes, UTF_8): алокує новийbyte[]+ об’єкт String (~24 bytes overhead)- Для 1M конвертацій: ~100MB алокацій → Young GC pressure
Thread Safety
StandardCharsets.UTF_8— thread-safe (immutable singleton)Charset.forName(...)— thread-safe (кешує результати)CharsetEncoder/CharsetDecoder— NOT thread-safe! Один інстанс не можна використовувати з кількох потоків одночасно- Рішення: створюйте новий encoder/decoder на кожен виклик або використовуйте
ThreadLocal<CharsetEncoder>
Production War Story
Сценарій 1: HTTP API — читання request body (Spring Boot):
// Spring Boot — автоматично використовує UTF-8
@PostMapping
public void handle(@RequestBody String body) { ... }
// Raw Servlet — потрібно вказати явно
request.setCharacterEncoding("UTF-8");
String body = request.getReader().readLine();
Проблема: клієнт надсилав POST-запит в Windows-1251, Spring читав як UTF-8 → «кракозябри». Fix: заголовок Content-Type: text/plain; charset=Windows-1251 або міграція клієнта на UTF-8.
Сценарій 2: Kafka messages — серіалізація/десеріалізація:
// Producer
byte[] bytes = jsonString.getBytes(StandardCharsets.UTF_8);
producer.send(new ProducerRecord<>(topic, keyBytes, bytes));
// Consumer
String message = new String(record.value(), StandardCharsets.UTF_8);
Проблема: один з продюсерів використовував getBytes() без кодування (дефолт на Windows = Cp1251 (Windows, російська локаль)). Consumer на Linux читав як UTF-8 → corrupted messages. Fix: єдиний стандарт UTF-8 на рівні Kafka-контракту.
Сценарій 3: Highload парсер логів (500K lines/sec):
- Конвертація кожного рядка
new String(bytes, UTF_8)→ 500K алокацій/sec - Young GC кожні 2 секунди, pause 15ms
- Fix: zero-copy через
ByteBuffer+ кастомний parser, без конвертації в String - Результат: Young GC кожні 8 секунд, pause 5ms
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())"
# GC логи — алокації від конвертації
java -Xlog:gc*:file=gc.log ...
# JFR — Object Allocation
java -XX:StartFlightRecording=filename=recording.jfr ...
# В JFR: Memory → Object Allocation → filter by java.lang.String
# JOL — розмір String після конвертації
System.out.println(GraphLayout.parseInstance(s).toFootprint());
// Runtime-перевірка
System.out.println(Charset.defaultCharset()); // Залежить від ОС!
// Бенчмарк конвертації
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
byte[] b = str.getBytes(StandardCharsets.UTF_8);
}
long elapsed = System.nanoTime() - start;
System.out.println("1M encode: " + elapsed / 1_000_000 + "ms");
Best Practices для Highload
- Завжди вказуйте
Charset— використовуйтеStandardCharsets.UTF_8(константа, без алокацій на lookup) - Для JSON/XML/HTTP: UTF-8 — стандарт де-факто
- Для binary protocols (Kafka, gRPC, TCP): працюйте з
byte[]/ByteBufнапряму, без конвертації в String - Для streaming I/O:
InputStreamReader/OutputStreamWriterз явним Charset — буферизація всередині - BOM handling:
BOMInputStream(Apache Commons IO) або ручна перевірка перших байт - Для ultra-low-latency: уникайте String — використовуйте
ByteBuf(Netty),ByteBuffer, zero-copy підходи - Security: валідуйте input ПІСЛЯ декодування, використовуйте constant-time порівняння для secrets
CharsetEncoder/CharsetDecoder— не шарте між потоками, використовуйтеThreadLocalабо per-call створення- Для великих даних: streaming через
Reader/Writer, не завантажуйте весь файл вbyte[]
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
str.getBytes(StandardCharsets.UTF_8)— правильний спосіб конвертації String → byte[]new String(bytes, StandardCharsets.UTF_8)— правильний спосіб byte[] → StringgetBytes()іnew String(bytes)без кодування — залежать від ОС, викликають «кракозябри»- BOM в UTF-8 (3 байти
EF BB BF) створює невидимий символ\uFEFFна початку рядка - Невалідні UTF-8 байти замінюються на
\uFFFD(replacement character) CharsetEncoder/CharsetDecoder— NOT thread-safe, потрібенThreadLocal
Часті уточнюючі запитання:
- Чому
new String(bytes)без кодування — це погано? — Дефолтне кодування залежить від ОС. String, закодована на Windows (Cp1251), стане «кракозябрами» на Linux (UTF-8). - Як обробляти BOM при читанні UTF-8 файла? —
BOMInputStreamабо перевірка: якщо перші 3 байтиEF BB BF, пропустити їх. - Що буде при конвертації binary даних в String? — Втрата даних. Binary — працюйте з
byte[]/ByteBufferнапряму. - Як прискорити конвертацію в highload? — Zero-copy:
ByteBuffer,ByteBuf(Netty), streaming черезReader/Writer.
Червоні прапорці (НЕ говорити):
- ❌ “
getBytes()без кодування — нормально” — залежить від ОС, ламається при міграції - ❌ “
CharsetEncoderможна шарити між потоками” — NOT thread-safe - ❌ “BOM — це тільки проблема UTF-16” — UTF-8 теж може мати BOM
- ❌ “Можна використовувати
bytes.lengthдля визначення кількості символів” — для UTF-8 1 символ = 1-4 байти
Пов’язані теми:
- [[17. Що таке кодування String]]
- [[19. Що таке компактні рядки в Java 9+]]
- [[20. Як дізнатися, скільки пам’яті займає String]]