Как правильно конвертировать 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 encoding (кодировка)]]
- [[19. Что такое compact strings в Java 9+]]
- [[20. Как узнать, сколько памяти занимает String]]