Строки и Stream API: split, chars, codePoints и границы Unicode
Автор: Казачкин Даниил Михайлович · Обновлено
Чтобы обработать строку через Stream API, сначала выберите единицу обработки. Строки файла, поля записи, UTF-16-единицы и Unicode code points — разные последовательности. Ошибка выбора проявляется на пустых полях, emoji и символах с комбинируемыми знаками.
Что поступает в конвейер
| Источник | Тип результата | Что означает элемент |
|---|---|---|
Stream.of(text) | Stream<String> | Вся строка целиком, один элемент |
text.lines() | Stream<String> | Отдельная строка текста |
Arrays.stream(text.split(",", -1)) | Stream<String> | Поле после разделения регулярным выражением |
text.chars() | IntStream | UTF-16 code unit |
text.codePoints() | IntStream | Code point с объединением корректных суррогатных пар |
У String.length() единицей также является UTF-16 code unit. IntStream нужен, потому что code point может не помещаться в один char. Таблица описывает контракты строковых методов, а не универсальное понятие «буква». String API.
Пустые поля — часть данных
Полный StringStreams.java показывает четыре независимых преобразования:
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Locale;
import java.util.regex.Pattern;
public class StringStreams {
public static void main(String[] args) {
String row = " Java, кот,,🦊,";
String[] fields = row.split(",", -1);
System.out.println("fields=" + fields.length);
System.out.println(Arrays.stream(fields)
.map(String::strip)
.filter(field -> !field.isEmpty())
.map(field -> field.toLowerCase(Locale.ROOT))
.toList());
String text = "A🦊é";
System.out.println("units=" + text.chars().count());
System.out.println("points=" + text.codePoints().count());
System.out.println(text.codePoints()
.mapToObj(point -> new String(Character.toChars(point)))
.toList());
String decomposed = "e\u0301";
System.out.println("combining points=" + decomposed.codePoints().count());
System.out.println("graphemes=" + Pattern.compile("\\X")
.matcher(decomposed).results().count());
System.out.println(Normalizer.normalize(decomposed, Normalizer.Form.NFC)
.equals("é"));
System.out.println("one\n\ntwo\n".lines().filter(line -> !line.isBlank()).toList());
}
}Сборка: javac -encoding UTF-8 --release 25 StringStreams.java; запуск: java StringStreams. Ожидается:
fields=5
[java, кот, 🦊]
units=4
points=3
[A, 🦊, é]
combining points=2
graphemes=1
true
[one, two]В первой записи два пустых поля, включая завершающее. Параметр -1 сохраняет хвостовые пустые поля; затем программа осознанно удаляет пустые значения. Для таблицы с фиксированными колонками так делать нельзя: столбцы сместятся. Здесь это список тегов, где пустые записи по условию не нужны.
Разделитель split является регулярным выражением
Точка . в regex означает не буквальную точку. Когда разделитель приходит как обычный текст, используйте Pattern.quote(delimiter), а не случайную последовательность экранирования. В Java-строке обратный слеш сам экранируется, поэтому выражение для grapheme cluster записано как "\\X". Синтаксис Pattern.
Простой split не является полноценным CSV-парсером: кавычки, запятые внутри поля и многострочные значения требуют правил формата. Прежде чем «исправлять Stream», запишите, какие входы ваша модель вообще допускает. Ошибка разбора возникает ещё до коллекционных операций.
Code point ещё не равен видимому символу
В A🦊é лиса занимает две UTF-16-единицы, но один code point. В записи e плюс комбинируемый акцент две кодовые точки могут выглядеть как один символ. Для редакторского курсора и пользовательского ограничения длины это существенно: нарезка по char может повредить пару, а нарезка по code points — разделить визуально цельный знак.
Normalizer в примере приводит канонически эквивалентные последовательности к NFC. Это не перевод, не транслитерация и не универсальное удаление различий между строками. Выбор нормализации — часть правил сравнения вашей задачи. Normalizer API.
Для независимых от языка меток Locale.ROOT делает выбранное преобразование регистра воспроизводимым. Для пользовательских имён на конкретном языке могут требоваться локальные правила, поэтому не превращайте этот приём в универсальную «очистку текста». Locale.ROOT.
Практика: выберите правильную единицу
Измените строку тегов на "one||two|" и разделяйте по буквальному |. Сначала сохраните все четыре поля, затем примените правило удаления пустых тегов. Для второго шага придумайте пример с ведущим пустым полем и объясните, почему такая очистка допустима для тегов, но разрушает табличную запись.
Отдельно сравните длины "A", "🦊" и "e\u0301" через length, codePoints().count() и regex \X. Предскажите результат перед запуском. Сборка должна использовать UTF-8, а терминал — отображать эту кодировку; квадратик вместо emoji ещё не доказывает, что программа повредила строку.
Частые вопросы
Можно ли привести каждый int из chars к char?
Это даст отдельные UTF-16-единицы, а не обязательно целые Unicode-символы. Для обхода code points используйте соответствующий метод и восстанавливайте строки через Character.toChars, как в полном примере.
Почему фильтрация пустых строк иногда ломает данные?
Пустое поле может обозначать отсутствующее значение на определённой позиции. Удалив его, вы меняете расположение остальных колонок. Правило для списка тегов нельзя без проверки переносить на таблицу или CSV.