Как правильно конвертировать String в byte[] и обратно
Конвертация строки в массив байтов (String $\to$ byte[]) и восстановление строки из байтов (byte[] $\to$ String) — базовая операция при работе с сетью, базами данных, шифрование...
🟢 Junior Level
Конвертация строки в массив байтов (String $\to$ byte[]) и восстановление строки из байтов (byte[] $\to$ String) — базовая операция при работе с сетью, базами данных, шифрованием и файловой системой.
Главное правило
ВСЕГДА явно указывайте кодировку через константы класса java.nio.charset.StandardCharsets! Никогда не используйте методы без параметров кодировки.
import java.nio.charset.StandardCharsets;
String original = "Привет, мир! 🚀";
// 1. Правильная конвертация String -> byte[]
byte[] bytes = original.getBytes(StandardCharsets.UTF_8);
// 2. Правильная конвертация byte[] -> String
String restored = new String(bytes, StandardCharsets.UTF_8);
System.out.println(restored); // "Привет, мир! 🚀"
Почему нельзя вызывать str.getBytes() без аргументов?
Методы без аргументов используют системную кодировку по умолчанию. До Java 18 на Windows это была кодировка Windows-1251 (или Cp1252), а на Linux — UTF-8. В результате файл или сетевой пакет, сформированный на Windows, при чтении на сервере под Linux превращался в нечитаемые символы («кракозябры»).
🟡 Middle Level
Способы указания кодировки: почему StandardCharsets лучше
В Java существует три способа передачи кодировки, но только один из них считается эталонным:
// ❌ 1. Устаревший и опасный способ (строковый литерал):
// Требует перехвата проверяемого исключения UnsupportedEncodingException!
// При каждом вызове выполняет поиск Charset по строковому имени в Map.
byte[] b1 = str.getBytes("UTF-8");
// 🟡 2. Динамический поиск:
// Не бросает проверяемых исключений, но тратит такты CPU на lookup в кэше
byte[] b2 = str.getBytes(Charset.forName("UTF-8"));
// ✅ 3. Рекомендуемый стандарт (StandardCharsets):
// Статическая константа-синглтон. Ноль аллокаций, ноль поисков, 100% compile-time безопасность.
byte[] b3 = str.getBytes(StandardCharsets.UTF_8);
String s3 = new String(b3, StandardCharsets.UTF_8);
Разница между длиной строки и длиной массива байтов
Критическая ошибка начинающих инженеров — предполагать, что длина строки равна количеству байтов:
String text = "Привет";
System.out.println(text.length()); // 6 (символов)
System.out.println(text.getBytes(StandardCharsets.UTF_8).length); // 12 (байтов!)
В кодировке UTF-8 кириллические символы занимают по 2 байта каждый, а эмодзи — по 4 байта. Поэтому размер сетевого буфера или столбца в СУБД всегда нужно рассчитывать по длине массива byte[], а не по str.length().
Конвертация бинарных данных: почему String запрещен
Если у вас есть сырые бинарные данные (криптографический хэш SHA-256, массив байтов изображения, зашифрованный ключ AES), их категорически нельзя преобразовывать в String через new String(bytes, StandardCharsets.UTF_8):
- В UTF-8 есть невалидные последовательности байт, которые декодер молча превратит в символ замены
\uFFFD. - Обратный вызов
getBytes()вернет уже искаженные данные, и файл/ключ будет испорчен.
Правильные способы представления бинарных данных в виде текста:
- Base64 (для сетевых протоколов и JSON):
String base64 = Base64.getEncoder().encodeToString(binaryBytes); byte[] restored = Base64.getDecoder().decode(base64); - Шестнадцатеричная строка (Hex Format, Java 17+):
String hex = HexFormat.of().formatHex(binaryBytes); byte[] restored = HexFormat.of().parseHex(hex);
🔴 Senior Level
Нагрузка на GC и производительность при конвертации
Каждая операция str.getBytes() и new String(bytes) выполняет:
- Валидацию кодировки и подсчет результирующего размера.
- Выделение нового массива в Heap:
getBytes()всегда аллоцирует массив байтов точной длины, аnew String()аллоцирует сам заголовокStringи внутренний массивbyte[] value. - Побайтовое копирование данных с транскодированием.
В высоконагруженных сервисах (Highload, 100 000 RPS) непрерывные вызовы конвертации строк в байты и обратно создают гигантское давление на Garbage Collector (Young Gen Pressure), приводя к частым сборкам мусора.
Решения для Highload и Zero-Copy:
- Использование Netty
ByteBufили Java NIOByteBuffer: парсинг протоколов напрямую в байтах без создания промежуточных объектовString. - Использование потокового ввода-вывода (
Reader/Writer) вместо загрузки файла целиком в память:// Чтение без промежуточного создания byte[] всего файла try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { ... } }
Потоковое декодирование и разрыв многобайтовых последовательностей
В сетевом программировании (TCP/Sockets) данные приходят фрагментами (чанками).
Если 2-байтовый символ кириллицы (например, буква 'Д' = 0xD0, 0x94) случайно окажется на границе двух TCP-пакетов (первый пакет кончается байтом 0xD0, а второй начинается байтом 0x94):
- Вызов
new String(packet1, UTF_8)завершится ошибкой: байт0xD0превратится в символ замены\uFFFD. - Вызов
new String(packet2, UTF_8)аналогично испортит0x94.
Решение: Использование CharsetDecoder с сохранением состояния:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
ByteBuffer in = ByteBuffer.allocate(1024);
CharBuffer out = CharBuffer.allocate(1024);
// Метод decode сохраняет незавершенные байты внутри своего состояния
// до прихода следующей порции данных (endOfInput = false)
decoder.decode(in, out, false);
Безопасность: пароли и уязвимости в памяти
Конвертация паролей из сетевого запроса в String — серьезная уязвимость безопасности:
- Строка
Stringиммутабельна и не может быть принудительно стерта из памяти. Она остается в Java Heap до тех пор, пока сборщик мусора не соизволит ее удалить, и может быть прочитана злоумышленником через Heap Dump. - Best Practice: Пароли и секретные ключи должны храниться исключительно в виде
char[]илиbyte[]. Сразу после проверки хэша массив обязан затираться нулями:byte[] passwordBytes = readPasswordFromNetwork(); try { authenticate(passwordBytes); } finally { Arrays.fill(passwordBytes, (byte) 0); // Гарантированное уничтожение в памяти }
4 Tricky Questions
1. Почему вызов StandardCharsets.UTF_8 предпочтительнее str.getBytes("UTF-8") и Charset.forName("UTF-8")?
Ответ:
- Отсутствие проверяемых исключений: Метод
str.getBytes("UTF-8")требует обязательногоtry-catchдляUnsupportedEncodingException, хотя UTF-8 гарантированно поддерживается любой JVM. - Производительность: Передача строки
"UTF-8"заставляет JVM при каждом вызове обращаться к внутренней хеш-таблице кодировок (Charset.availableCharsets()) и производить регистронезависимый поиск имени. - Безопасность типов:
StandardCharsets.UTF_8— это готовая статическая константа типаCharset. Она передает ссылку на уже созданный синглтон напрямую за 1 машинный такт без аллокаций и поиска.
2. Что вернет new String(new byte[] {(byte) 0xD0}, StandardCharsets.UTF_8) и как настроить JVM, чтобы выбрасывалось исключение вместо подмены данных?
Ответ:
Конструктор new String(bytes, charset) по умолчанию использует политику CodingErrorAction.REPLACE.
Поскольку байт 0xD0 является первым байтом 2-байтовой последовательности UTF-8, а второго байта нет, метод расценит его как ошибку и вернет строку, содержащую один символ замены \uFFFD (символ ). Никакого исключения выброшено не будет.
Чтобы в случае некорректных байтов выбрасывалось исключение java.nio.charset.MalformedInputException, необходимо использовать CharsetDecoder:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT);
CharBuffer result = decoder.decode(ByteBuffer.wrap(bytes));
3. Почему для конвертации массива байтов хэша SHA-256 в строку нельзя использовать new String(hashBytes, StandardCharsets.UTF_8)?
Ответ:
Потому что хэш SHA-256 — это псевдослучайный набор из 32 байтов, где каждый байт может принимать любое значение от 0x00 до 0xFF.
Стандарт UTF-8 строго регламентирует допустимые последовательности: например, байты с 0xF5 по 0xFF вообще запрещены, а байты от 0x80 до 0xBF могут быть только продолжением многобайтовых символов.
При декодировании через UTF-8 декодер сочтет такие байты мусором и заменит их на \uFFFD. Восстановить исходный массив байтов хэша через str.getBytes() станет невозможно. Бинарные данные должны конвертироваться в String исключительно через Base64 или Hex (HexFormat).
4. В чем преимущество класса java.util.HexFormat (появившегося в Java 17) перед String.format("%02x") при конвертации байтов в строку?
Ответ:
- Метод
String.format("%02x", b)внутри парсит строку формата, использует рефлексию, создает объектFormatterи выполняет автобоксинг примитивного байта вByte, что делает его невероятно медленным (микросекунды на операцию). - Класс
HexFormat(Java 17+) спроектирован специально для высокоскоростной конвертации: он иммутабелен, потокобезопасен, использует прямые битовые сдвиги (>> 4) и внутренние таблицы символов, работая в десятки раз быстрее и создавая минимальное количество мусора в куче.
🎯 Шпаргалка для интервью
30-секундный ответ
«Для конвертации
Stringвbyte[]используетсяstr.getBytes(StandardCharsets.UTF_8), а обратно —new String(bytes, StandardCharsets.UTF_8). Вызовы без явного указания кодировки строго запрещены, так как зависят от платформы. Для бинарных данных (хэши, зашифрованные данные) конвертация вStringчерез кодировки недопустима — нужно использоватьBase64илиHexFormat(Java 17+). Пароли не следует переводить вStringиз соображений безопасности (их держат вbyte[]илиchar[]с последующим обнулением черезArrays.fill())».
Ключевые тезисы для Senior-интервью
StandardCharsets: Всегда передавать готовые константыStandardCharsets.UTF_8(ноль оверхеда на lookup).- Длина: Длина в
char(str.length()) не равна длине массиваbyte[](кириллица в UTF-8 = 2 байта, эмодзи = 4 байта). - Бинарные данные: Использовать
Base64илиHexFormat.of().formatHex(bytes), никогда не использоватьnew String(bytes). - Потоковое декодирование: При чтении чанков из сокетов использовать
CharsetDecoderс обработкой незавершенных многобайтовых последовательностей. - Безопасность паролей: Избегать
Stringиз-за невозможности затирания в куче; затирать массивы нулями сразу после проверки.
Красные флаги (Чего говорить категорически нельзя)
- ❌ Использовать
str.getBytes()без аргументов. - ❌ Передавать имя кодировки строковым литералом
"UTF-8"и писатьcatch (UnsupportedEncodingException e). - ❌ Пытаться преобразовывать бинарные данные (файлы картинок, хэши) в
Stringчерез стандартный конструктор. - ❌ Считать, что 1 буква всегда равна 1 байту в UTF-8.