Кодирование строки в Java — это перевод текста (String) в массив байтов (byte[]) по правилам конкретной кодировки: text.getBytes(StandardCharsets.UTF_8). Обратная операция — декодирование: new String(bytes, StandardCharsets.UTF_8). Главное правило одно: кодировать и декодировать нужно одной и той же кодировкой и всегда указывать ее явно, иначе появляются «кракозябры».
Содержание
- Три слоя: символ, код, байты
- Минимальный рабочий пример
- Сколько байтов занимает символ
- Откуда берутся кракозябры
- Кодировка по умолчанию: почему код «работает у меня»
- Строгое декодирование: ошибка вместо «�»
- Потоки: не декодируйте байты кусками
- Двоичные данные — не через String
- Если не получилось: диагностика по симптому
- Выводы
- Где применяется / связь с практикой
- FAQ
Все примеры ниже проверены на Java 25 (Eclipse Temurin 25.0.4, Linux) 24.09.2026.
Три слоя: символ, код, байты
В теме легко смешать три разные вещи, поэтому сначала короткий словарь:
- Кодовая точка (code point) — номер символа в Unicode, например буква «Я» = U+042F.
- char в Java — 16-битная кодовая единица UTF-16. Большинство символов помещается в один
char, но эмодзи и редкие иероглифы занимают два (суррогатная пара). - Байты — то, как кодовая точка записана в конкретной кодировке: «Я» в UTF-8 =
D0 AF, в windows-1251 =DF.
Сквозная цепочка на одном примере: «Я» -> U+042F -> D0 AF в UTF-8. Строка String с точки зрения API — последовательность char (единиц UTF-16), и во внешние байты она превращается только в момент кодирования. Поэтому вопрос «сколько байтов занимает строка» без названной кодировки не имеет ответа. Подробнее про устройство самих кодировок — в статье о кодировках символов и формате UTF-8.
Минимальный рабочий пример
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) {
String text = "Привет, Java";
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
System.out.println("Символов (char): " + text.length());
System.out.println("Байтов в UTF-8: " + bytes.length);
System.out.println("HEX: " + HexFormat.ofDelimiter(" ").formatHex(bytes));
String back = new String(bytes, StandardCharsets.UTF_8);
System.out.println("Обратно: " + back);
System.out.println("Совпадает: " + back.equals(text));
}
}
Вывод:
Символов (char): 12
Байтов в UTF-8: 18
HEX: d0 9f d1 80 d0 b8 d0 b2 d0 b5 d1 82 2c 20 4a 61 76 61
Обратно: Привет, Java
Совпадает: true
Разбор:
getBytes(StandardCharsets.UTF_8)кодирует строку. Кириллица в UTF-8 занимает по 2 байта, латиница, пробел и запятая — по 1, отсюда 6 x 2 + 6 = 18 байтов.new String(bytes, StandardCharsets.UTF_8)декодирует байты обратно, строка совпадает с исходной.HexFormat(есть с Java 17) печатает байты в шестнадцатеричном виде — так удобнее всего отлаживать кодировки.
Почему StandardCharsets, а не строка "UTF-8": перегрузка getBytes(String charsetName) объявляет проверяемое исключение. Код "Привет".getBytes("UTF-8") без try не компилируется:
Unsupported.java:3: error: unreported exception UnsupportedEncodingException; must be caught or declared to be thrown
byte[] b = "Привет".getBytes("UTF-8");
^
1 error
С константой StandardCharsets.UTF_8 исключения нет, а опечатка в имени кодировки невозможна. Для кодировок вне StandardCharsets (например, windows-1251) используют Charset.forName("windows-1251").
Сколько байтов занимает символ
Размер зависит от символа и кодировки. Значения ниже получены в том же окружении через s.getBytes(cs):
| Символ | Кодовая точка | length() |
UTF-8 | UTF-16 | UTF-16BE | windows-1251 |
|---|---|---|---|---|---|---|
| A | U+0041 | 1 | 1: 41 |
4: fe ff 00 41 |
2: 00 41 |
1: 41 |
| Я | U+042F | 1 | 2: d0 af |
4: fe ff 04 2f |
2: 04 2f |
1: df |
| 😀 | U+1F600 | 2 | 4: f0 9f 98 80 |
6: fe ff d8 3d de 00 |
4: d8 3d de 00 |
1: 3f (это «?») |
Что видно из таблицы:
String.length()считаетchar, а не символы: у эмодзи длина 2. Число кодовых точек даетcodePointCount(0, s.length()), но и это не всегда число видимых символов: «é» из буквыeи комбинирующего ударения U+0301 дает 2, эмодзи «семья» из трех человек через U+200D — 5 кодовых точек и 8char. Видимые символы (графемы) считаетjava.text.BreakIterator.getCharacterInstance(): для «семьи» в нашем прогоне он вернул 1.StandardCharsets.UTF_16при кодировании добавляет метку порядка байтовFE FF(BOM), поэтому «A» занимает 4 байта. Если BOM не нужен, берутUTF_16BEилиUTF_16LE— про порядок байтов подробно в статье о байтах, битах и их порядке.- Эмодзи нет в windows-1251, и
getBytesмолча заменил его на?(байт3F). Данные потеряны.
Еще одна частая ловушка: тип byte в Java знаковый (от -128 до 127). Первый байт буквы «Я» в UTF-8 (0xD0) при печати даст -48. Чтобы получить значение 0-255, используют b & 0xFF, в нашем прогоне это 208.
Откуда берутся кракозябры
Кракозябры (mojibake) появляются, когда байты записали в одной кодировке, а прочитали в другой. Сами байты при этом целы — неверна их интерпретация. Три типичных сценария:
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class Mojibake {
public static void main(String[] args) {
Charset cp1251 = Charset.forName("windows-1251");
byte[] utf8 = "Привет".getBytes(StandardCharsets.UTF_8);
System.out.println(new String(utf8, cp1251));
byte[] win = "Привет".getBytes(cp1251);
System.out.println(new String(win, StandardCharsets.UTF_8));
byte[] ascii = "Привет".getBytes(StandardCharsets.US_ASCII);
System.out.println(new String(ascii, StandardCharsets.US_ASCII));
}
}
Вывод:
Привет
������
??????
Как читать симптомы:
| Что видно | Что произошло | Как исправить |
|---|---|---|
РџСЂ... (пары вида «Р?», «С?») |
UTF-8 байты прочитаны как windows-1251 | Декодировать как UTF-8 |
������ (символ U+FFFD) |
Байты windows-1251 прочитаны как UTF-8 | Декодировать как windows-1251 |
?????? |
Текст закодирован в кодировку без кириллицы | Кодировать в UTF-8; прежний byte[] уже не восстановить |
Первый случай обычно обратим: new String(bad.getBytes(cp1251), StandardCharsets.UTF_8) вернет «Привет». Граница — байт 0x98: в windows-1251 он не занят, Java декодирует его в U+FFFD, и такой символ (например, сербская «ј» = D1 98) уже не вернуть. Для русского текста в UTF-8 этот байт не встречается. Во втором и третьем случаях данные при декодировании или кодировании уже заменены на U+FFFD или ?, и восстановить их из строки нельзя — только из исходных байтов.
Кодировка по умолчанию: почему код «работает у меня»
Методы без явной кодировки — getBytes(), new String(bytes), new InputStreamReader(in), FileReader — берут кодировку по умолчанию. С Java 18 (JEP 400) это UTF-8 на всех платформах. До Java 18 она определялась системой: на русской Windows это обычно windows-1251, на Linux и macOS — как правило UTF-8. Отсюда классика: файл, записанный на сервере, читается с кракозябрами на рабочей машине со старой JDK.
Проверить окружение можно так:
import java.nio.charset.Charset;
public class Defaults {
public static void main(String[] args) {
System.out.println("java.version: " + System.getProperty("java.version"));
System.out.println("defaultCharset: " + Charset.defaultCharset());
System.out.println("file.encoding: " + System.getProperty("file.encoding"));
System.out.println("native.encoding: " + System.getProperty("native.encoding"));
System.out.println("stdout.encoding: " + System.getProperty("stdout.encoding"));
}
}
В контейнере с локалью LANG=C вывод такой:
java.version: 25.0.4
defaultCharset: UTF-8
file.encoding: UTF-8
native.encoding: ANSI_X3.4-1968
stdout.encoding: ANSI_X3.4-1968
Кодировка по умолчанию — UTF-8, а вывод в консоль (stdout.encoding, появилось в Java 19) идет в кодировке системы, здесь ASCII. В такой среде Main печатает ???????? (char): 12, хотя строки внутри программы правильные. Это проблема вывода, а не данных: помогает UTF-8 локаль терминала или флаг java -Dstdout.encoding=UTF-8 Main. На Windows аналогичная ситуация бывает в консоли с кодовой страницей 866 или 1251.
Практический вывод: даже на Java 18+ указывайте кодировку явно. Код читается однозначно и не сломается, если его запустят на старой JDK или с -Dfile.encoding=COMPAT.
Строгое декодирование: ошибка вместо «�»
new String(bytes, UTF_8) никогда не бросает исключение на битых данных — он молча вставляет U+FFFD. Для входных данных извне (файлы, сеть, импорт) лучше узнать о проблеме сразу. Для этого используют CharsetDecoder в режиме REPORT:
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
public class Strict {
static String decodeStrict(byte[] data) throws CharacterCodingException {
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
return decoder.decode(ByteBuffer.wrap(data)).toString();
}
public static void main(String[] args) {
byte[] good = "Привет".getBytes(StandardCharsets.UTF_8);
byte[] bad = "Привет".getBytes(Charset.forName("windows-1251"));
try {
System.out.println("good: " + decodeStrict(good));
System.out.println("bad: " + decodeStrict(bad));
} catch (CharacterCodingException e) {
System.out.println("Ошибка: " + e);
}
}
}
good: Привет
Ошибка: java.nio.charset.MalformedInputException: Input length = 1
Для файлов то же поведение дает Files.readString(path): он читает в UTF-8 и бросает MalformedInputException на невалидных байтах, а не подменяет их.
Потоки: не декодируйте байты кусками
Когда данные приходят блоками (сеть, буфер файла), граница блока может разрезать многобайтовый символ. Если декодировать каждый блок отдельно через new String, символ на стыке сломается:
import java.io.ByteArrayInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Chunks {
public static void main(String[] args) throws IOException {
byte[] data = "Привет".getBytes(StandardCharsets.UTF_8);
// Неверно: декодируем куски по 5 байт по отдельности
StringBuilder wrong = new StringBuilder();
for (int i = 0; i < data.length; i += 5) {
byte[] part = Arrays.copyOfRange(data, i, Math.min(i + 5, data.length));
wrong.append(new String(part, StandardCharsets.UTF_8));
}
System.out.println("По кускам: " + wrong);
// Верно: Reader сам собирает символ из соседних кусков
InputStream in = new ByteArrayInputStream(data);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(in, StandardCharsets.UTF_8))) {
System.out.println("Через Reader: " + reader.readLine());
}
}
}
По кускам: Пр��вет
Через Reader: Привет
Буква «и» (D0 B8) попала на границу блоков и превратилась в два U+FFFD. InputStreamReader с явной кодировкой хранит незавершенный символ до следующего блока. Для текстовых файлов проще всего Files.readString и Files.newBufferedReader(path, StandardCharsets.UTF_8).
Двоичные данные — не через String
Хеши, ключи, изображения и зашифрованные данные — это не текст. Если прогнать их через new String(...), невалидные для UTF-8 байты заменятся, и массив не восстановится:
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
public class Binary {
public static void main(String[] args) {
byte[] raw = {(byte) 0xFF, (byte) 0xFE, 0x41};
String asText = new String(raw, StandardCharsets.UTF_8);
byte[] broken = asText.getBytes(StandardCharsets.UTF_8);
System.out.println("Через String: " + Arrays.toString(broken));
String b64 = Base64.getEncoder().encodeToString(raw);
byte[] restored = Base64.getDecoder().decode(b64);
System.out.println("Base64: " + b64 + " -> " + Arrays.toString(restored));
}
}
Через String: [-17, -65, -67, -17, -65, -67, 65]
Base64: //5B -> [-1, -2, 65]
Вместо 3 исходных байтов получилось 7: каждый «плохой» байт стал EF BF BD (U+FFFD в UTF-8). Для передачи байтов в текстовом виде используют Base64 или hex (HexFormat).
Если не получилось: диагностика по симптому
- Видите
РџСЂ...и пары вида «Р?», «С?» — UTF-8 прочитан как windows-1251. Найдите место без явной кодировки (new String(bytes),FileReader,InputStreamReaderбез второго аргумента). - Видите
�— байты не являются валидным UTF-8. Выясните реальную кодировку источника, а не «чините» строку. - Видите
?вместо букв — текст закодирован в кодировку без нужных символов, либо консоль не умеет UTF-8. Проверьтеstdout.encodingи кодировку записи. - Кракозябры в строковых литералах прямо в коде — исходник сохранен не в той кодировке, что ожидает
javac. С JDK 18javacпо умолчанию читает UTF-8; на старых версиях задайтеjavac -encoding UTF-8или<project.build.sourceEncoding>в Maven. - Проверяйте байты, а не вывод:
HexFormat.ofDelimiter(" ").formatHex(bytes)покажет, что на самом деле лежит в массиве.
Выводы
- Строка в байты:
getBytes(StandardCharsets.UTF_8), байты в строку:new String(bytes, StandardCharsets.UTF_8)— с одной и той же кодировкой. - Кодировку указывайте явно даже на Java 18+, где по умолчанию UTF-8: код станет переносимым между JDK и ОС.
length()считаетchar(единицы UTF-16), а не символы и не байты; размер в байтах зависит от кодировки.- Кракозябры вида
РџСЂ...для русского текста обычно обратимы,�и?— уже потеря данных; для внешних данных включайте строгое декодирование. - Двоичные данные не превращают в
String— только Base64 или hex.
Где применяется / связь с практикой
Освойте тему на практике
С байтами и кодировками разработчик сталкивается постоянно: чтение файлов и CSV, HTTP-запросы, работа с базами данных, сокеты, логи. Ошибка в одной строке с new String(bytes) без кодировки может неделями портить данные, пока кто-то не запустит сервис на другой ОС. Работу со строками, вводом-выводом и стандартной библиотекой по шагам разбирают на курсе «Java разработчик. Базовый уровень». Посмотреть формат занятий можно на открытых уроках Otus.
FAQ
Чем Charset отличается от кодировки?
В Java класс Charset описывает конкретную схему кодирования (UTF-8, windows-1251): он умеет и кодировать, и декодировать. Абстрактный набор символов (Unicode) и способ записать его в байты (UTF-8, UTF-16) — разные уровни, но в API Java оба называются charset.
Как обрезать строку до N байтов в UTF-8, не разрезав символ?
Кодировать по одному символу (по кодовым точкам) и останавливаться, пока сумма байтов не превысит лимит, либо использовать CharsetEncoder с CharBuffer и ByteBuffer нужного размера: он запишет только целые символы.
Нужен ли BOM в UTF-8 файлах для Java?
Нет. Java при чтении UTF-8 не удаляет BOM (EF BB BF) автоматически, и в начале первой строки окажется символ U+FEFF. Если файлы приходят с BOM, его нужно пропустить вручную.



