Строки в Java кажутся простыми, из коробки поддерживается UTF-16, но стоит вам добавить эмодзи, и привычный length() выдаёт «магические» числа. В этой статье разберём, как на самом деле устроены строки в Java, почему эмодзи занимают два char, и как корректно подсчитать количество символов, видимых глазом.
Проблема: length() возвращает не то, что вы ждёте
Возьмём классический пример:
|
1 2 |
String s = "😊"; // Улыбающийся смайлик System.out.println(s.length()); // 2 |
Мы ожидаем увидеть 1, но вывод — 2. Всё дело во внутреннем представлении строк: Java хранит текст в кодировке UTF-16, где каждый символ кодируется одним или двумя 16-битными значениями типа char.
- Базовые символы Unicode (U+0000 … U+FFFF) умещаются в один
char. - Символы с кодом > U+FFFF (например, почти все эмодзи) кодируются суррогатной парой – двумя значениями
char(старший и младший суррогат).
Метод String.length() возвращает количество char-единиц, а не количество логических символов (code points). Для эмодзи 😊 (U+1F60A) это действительно 2.
Решение в первом приближении: codePointCount()
Чтобы получить количество Unicode code points в строке, используйте метод:
|
1 2 |
int realLength = s.codePointCount(0, s.length()); System.out.println(realLength); // 1 |
Метод codePointCount(int beginIndex, int endIndex) подсчитывает code points в заданном диапазоне, правильно обрабатывая суррогатные пары.
Альтернатива через стримы (Java 8+):
|
1 |
long count = s.codePoints().count(); // тоже 1 |
codePointCount() работает быстро и подходит для 99% задач, где нужно считать именно код-пойнты (числовые значения символов). Но не всё так просто.
Но и это не идеально: графемы
Что если строка содержит составной символ, например:
- Флаг:
"🇺🇳"(флаг ООН) – состоит из двух региональных индикаторов: 🇺 и 🇳. - Семья:
"👨👩👦"– здесь несколько code points (мужчина, женщина, мальчик и соединители). - Буквы с диакритикой:
"é"– букваe+ комбинирующий знак ударения (U+0301).
Метод codePointCount() вернёт для "👨👩👦" значение 5, хотя визуально мы видим один символ. Такие визуально неделимые единицы называются графемами (grapheme clusters).
В стандарте Unicode графема – это пользовательский символ, который отображается как единый знак.
Класс java.text.BreakIterator умеет разбивать текст на границы графем, слов, предложений и т.д. Используем его для подсчёта видимых символов:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import java.text.BreakIterator; public static int graphemeLength(String str) { BreakIterator it = BreakIterator.getCharacterInstance(); it.setText(str); int count = 0; int start = it.first(); while (start != BreakIterator.DONE) { int end = it.next(); if (end == BreakIterator.DONE) break; count++; start = end; } return count; } |
Если ваше приложение интенсивно работает с эмодзи, флагами или текстами на языках с диакритикой (например, вьетнамский), используйте BreakIterator для подсчёта видимых символов.