Тип char в Java: символы, коды Unicode и суррогатные пары

Тип char в Java: символы, коды Unicode и суррогатные пары Полезное

char в Java — это примитивный тип размером 16 бит без знака, который хранит одну кодовую единицу UTF-16, то есть число от 0 до 65535. Для латиницы и кириллицы одна такая единица совпадает с символом, но для эмодзи и части редких иероглифов символ занимает два char.

Ниже разберем литералы, арифметику с char, типичные ошибки компиляции, суррогатные пары и методы класса Character. Все примеры прогнаны на OpenJDK 25.0.4 (Java 25 LTS) 23.09.2026.

Чтобы дальше не путаться, три термина:

Термин Что это Пример для «😀»
Кодовая точка (code point) номер символа в Unicode, до U+10FFFF U+1F600
Кодовая единица UTF-16 (code unit) 16-битное число, ровно одно значение char D83D и DE00
Графема то, что человек видит как один символ один смайлик, но бывает из нескольких кодовых точек

Место char среди примитивных типов

В Java восемь примитивных типов. char — единственный беззнаковый целочисленный среди них: у него нет отрицательных значений.

Тип Размер Диапазон Значение поля по умолчанию
byte 8 бит -128..127 0
short 16 бит -32768..32767 0
char 16 бит 0..65535 ('\u0000'..'\uffff') '\u0000'
int 32 бита около ±2,1 млрд 0
long 64 бита около ±9,2·10^18 0L
float 32 бита IEEE 754 0.0f
double 64 бита IEEE 754 0.0
boolean не задан спецификацией true / false false

Размер boolean спецификация JVM не фиксирует: утверждение «boolean занимает 1 бит» неверно, на практике это обычно байт в массиве и больше в полях. Значения по умолчанию относятся к полям и элементам массивов, локальную переменную нужно инициализировать явно.

Минимальный пример: объявление и литералы

public class CharBasics {
    public static void main(String[] args) {
        char letter = 'A';
        char fromCode = 1071;          // десятичный код U+042F
        char fromEscape = '\u042F';    // та же буква через Unicode-экранирование
        char tab = '\t';

        System.out.println(letter);                 // печатается как символ
        System.out.println((int) letter);           // код символа
        System.out.println(fromCode + " " + fromEscape);
        System.out.println(fromCode == fromEscape);
        System.out.println("[" + tab + "]");

        char next = letter;
        next++;                                     // ++ работает с char
        System.out.println(next);
        System.out.println((int) Character.MIN_VALUE + ".." + (int) Character.MAX_VALUE);
        System.out.println(Character.SIZE + " bit, " + Character.BYTES + " bytes");
    }
}

Вывод прогона:

A
65
Я Я
true
[   ]
B
0..65535
16 bit, 2 bytes

Что здесь важно:

  • Символьный литерал пишется в одинарных кавычках: 'A'. Двойные кавычки дают String, а не char.
  • char можно задать числом (1071), если это константа в диапазоне 0..65535. Отрицательное число или 65536 не скомпилируются.
  • println(char) печатает символ, а (int) letter — его код. Хранится одно и то же число, различается только способ вывода.
  • Кириллица в консоли зависит от кодировки терминала: в Linux-контейнере вывелось «Я», в консоли Windows со старой кодовой страницей возможны вопросительные знаки.

Экранированные последовательности

Запись Символ Код
'\n' перевод строки 10
'\t' табуляция 9
'\'' одинарная кавычка 39
'\\' обратный слеш 92
'\u0041' любой символ по шестнадцатеричному коду 65 (A)
'\101' восьмеричная запись, от \0 до \377 65 (A)

Арифметика с char: почему ‘a’ + ‘b’ = 195

char — числовой тип, поэтому его можно складывать и сравнивать. Но в арифметическом выражении char повышается до int, и результат — тоже int.

public class CharArithmetic {
    public static void main(String[] args) {
        char a = 'a';
        char b = 'b';
        System.out.println(a + b);          // сложение кодов, тип int
        System.out.println("" + a + b);     // конкатенация строк
        System.out.println(a + 1);          // int
        System.out.println((char) (a + 1)); // явное приведение обратно к char

        char c = 'x';
        c += 2;                             // составное присваивание приводит само
        System.out.println(c);

        char digit = '7';
        System.out.println(digit - '0');                    // 7
        System.out.println(Character.getNumericValue(digit)); // 7
        System.out.println((int) digit);                    // 55 - код, а не цифра
    }
}
195
ab
98
b
z
7
7
55

'a' + 'b' дает 97 + 98 = 195, а не строку «ab». Чтобы склеить символы, выражение должно начинаться со строки. Приведение (int) '7' дает код 55, а не цифру 7: для цифры вычитают '0' или вызывают Character.getNumericValue.

Типичная ошибка: c = c + 1

Неверно:

public class CharError {
    public static void main(String[] args) {
        char c = 'a';
        c = c + 1;
        System.out.println(c);
    }
}

Результат — javac отказывается компилировать:

CharError.java:4: error: incompatible types: possible lossy conversion from int to char
        c = c + 1;
              ^
1 error

c + 1 имеет тип int, а неявно сузить int до char компилятор не разрешает. Исправление: c = (char) (c + 1); либо c += 1; или c++. Составное присваивание и инкремент делают приведение сами, поэтому в примере выше c += 2 сработал.

Ловушка: \u обрабатывается раньше компиляции

Последовательности \u javac заменяет на символы еще до разбора кода. Поэтому перевод строки через \u000a ломает литерал:

public class Escape {
    public static void main(String[] args) {
        char nl = '\u000a';
        System.out.println((int) nl);
    }
}
Escape.java:3: error: illegal line end in character literal
        char nl = '\u000a';
                  ^
1 error

Исправление: для перевода строки писать '\n', для кода 10 — (char) 10. То же касается \u000d и \u0027 (одинарная кавычка).

Суррогатные пары: когда один char — не символ

Когда в Java 1.0 появился char, Unicode умещался в 16 бит. Позже диапазон расширили до U+10FFFF, и символы за пределами U+FFFF (эмодзи, часть иероглифов, исторические письменности) стали кодироваться в UTF-16 двумя char: старшим и младшим суррогатом. Цепочка на одном примере: 😀 -> кодовая точка U+1F600 -> кодовые единицы D83D DE00.

public class Surrogates {
    public static void main(String[] args) {
        String s = "Hi \uD83D\uDE00";                    // "Hi " + эмодзи U+1F600
        System.out.println(s.length());                        // кодовые единицы UTF-16
        System.out.println(s.codePointCount(0, s.length()));   // кодовые точки

        char unit = s.charAt(3);                               // только половина эмодзи
        System.out.printf("U+%04X high=%b%n", (int) unit, Character.isHighSurrogate(unit));

        int cp = s.codePointAt(3);                             // целая кодовая точка
        System.out.printf("U+%X, charCount=%d%n", cp, Character.charCount(cp));

        // Неверно: разворот строки по char рвет суррогатную пару
        StringBuilder naive = new StringBuilder();
        for (int i = s.length() - 1; i >= 0; i--) naive.append(s.charAt(i));
        naive.chars().forEach(u -> System.out.printf("%04X ", u));
        System.out.println();

        // Верно: StringBuilder.reverse() учитывает пары
        new StringBuilder(s).reverse().codePoints()
            .forEach(p -> System.out.printf("U+%04X ", p));
        System.out.println();

        System.out.println(new String(Character.toChars(0x1F600)).equals(s.substring(3)));
    }
}
5
4
U+D83D high=true
U+1F600, charCount=2
DE00 D83D 0020 0069 0048 
U+1F600 U+0020 U+0069 U+0048 
true

Разбор:

  • length() вернул 5, хотя видимых символов четыре: смайлик занимает две кодовые единицы.
  • charAt(3) вернул только старший суррогат D83D — это не символ, а половина пары.
  • Ручной разворот по char поставил DE00 перед D83D. Такая последовательность уже не валидный UTF-16, на экране вместо смайлика будут знаки замены.
  • Для работы с полными символами используют int и методы с codePoint в названии: codePointAt, codePointCount, codePoints(), Character.toChars.

Граница этой модели: и кодовая точка не всегда равна видимому символу. Флаги, эмодзи с оттенком кожи и буквы с комбинируемыми диакритиками состоят из нескольких кодовых точек. Например, флаг России — это 4 char, 2 кодовые точки и 1 графема. Для разбиения на графемы в JDK есть конструкция \X в регулярных выражениях (Pattern.compile("\\X") нашел в флаге ровно одно совпадение) и класс java.text.BreakIterator.

Класс Character: обертка и полезные методы

Character — класс-обертка над char. Он нужен там, где требуется объект: в коллекциях (List<Character>, Map<Character, Integer>), при сравнении с null. Переход между char и Character компилятор делает сам (автоупаковка), метод charValue() возвращает примитив явно.

public class CharMethods {
    public static void main(String[] args) {
        String input = "Otus 2026, Java!";
        int letters = 0, digits = 0, spaces = 0, other = 0;
        for (char ch : input.toCharArray()) {
            if (Character.isLetter(ch)) letters++;
            else if (Character.isDigit(ch)) digits++;
            else if (Character.isWhitespace(ch)) spaces++;
            else other++;
        }
        System.out.println(letters + " " + digits + " " + spaces + " " + other);

        System.out.println(Character.toUpperCase('я'));
        System.out.println(Character.isLetter('Я') + " " + Character.isDigit('٣'));
        System.out.println(Character.getNumericValue('٣'));
        System.out.println(Character.isLetter(0x1F600));
        System.out.println(Character.getName('Я'));
    }
}
8 4 2 2
Я
true true
3
false
CYRILLIC CAPITAL LETTER YA

Счетчик нашел 8 букв, 4 цифры, 2 пробела и 2 прочих знака (запятую и восклицательный). Литерал 'я' в исходнике требует, чтобы файл читался в UTF-8: это кодировка по умолчанию начиная с Java 18, на более старых JDK нужен флаг javac -encoding UTF-8.

Метод Что делает На что обратить внимание
isLetter, isDigit, isWhitespace проверяют категорию символа работают по Unicode, не только ASCII
toUpperCase, toLowerCase меняют регистр одного символа для строк с учетом языка — String.toUpperCase(Locale)
getNumericValue числовое значение цифры для букв и римских цифр тоже возвращает числа
isHighSurrogate, charCount работа с суррогатными парами charCount = 2 для кодовых точек выше U+FFFF
toChars, getName кодовая точка -> char[] и имя символа принимают int, а не char

Обратите внимание на isDigit('٣'): арабско-индийская цифра три считается цифрой. Если нужна проверка именно ASCII-цифр (номер телефона, код подтверждения), надежнее условие ch >= '0' && ch <= '9'.

Когда брать char, а когда String или int

Задача Что использовать Почему
один служебный символ: разделитель, флаг, клавиша char компактно, сравнивается через ==
текст от пользователя, имена, сообщения String может содержать эмодзи и суррогатные пары
перебор символов с учетом эмодзи int и codePoints() char видит половинки пар
подсчет частоты символов в коллекции Map<Character, Integer> или Map<Integer, Integer> по кодовым точкам коллекции хранят только объекты

Выводы

  • char — 16-битный беззнаковый тип (0..65535), хранит одну кодовую единицу UTF-16, а не обязательно целый символ.
  • В арифметике char повышается до int: 'a' + 'b' = 195, а c = c + 1 не компилируется без приведения; c++ и c += 1 работают.
  • Символы за пределами U+FFFF, включая эмодзи, занимают два char (суррогатную пару); для них нужны методы с codePoint.
  • \u-последовательности обрабатываются до компиляции, поэтому '\u000a' вызывает ошибку — используйте '\n'.
  • Класс Character дает проверки по Unicode; для строгой проверки ASCII-цифр нужен явный диапазон.

Где применяется / связь с практикой

Работа с char встречается в парсерах и валидаторах ввода, подсчете частот символов, обработке текста и в задачах на собеседованиях (палиндромы, анаграммы, разворот строки). Именно в таких задачах всплывают суррогатные пары и неявное повышение до int.

Освойте тему на практике

Если хотите системно разобрать типы данных, строки, коллекции и ООП на практике с проверкой кода преподавателем, посмотрите курс Java Developer. Basic. Попробовать формат бесплатно можно на открытых уроках Otus.

FAQ

Чем char в Java отличается от char в C?
В C char обычно 8 бит и может быть знаковым или беззнаковым в зависимости от компилятора, а в Java char всегда 16 бит без знака и хранит кодовую единицу UTF-16.

Можно ли сравнивать char через ==?
Да, для примитива char == сравнивает коды. Для объектов Character надежнее equals или распаковка в char, иначе сравниваются ссылки.

Можно ли использовать char в switch?
Да, char допустим в switch наравне с int и String, метки задаются символьными литералами вида case 'a' ->.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)