W3docs

Java String split() и join()

Разбивайте строки Java по разделителям с помощью split() и объединяйте массивы строк с помощью String.join().

String.split и String.join — это два метода, к которым вы будете прибегать всякий раз, когда значение, разделённое разделителем, нужно превратить в список или список нужно превратить в значение, разделённое разделителем. Они покрывают подавляющую часть разбора CSV, разделения заголовков, построения путей и тысячи небольших преобразований формата строк журналов. Это также методы, которые чаще всего используются неправильно, потому что первый аргумент split — это регулярное выражение, а не литерал — факт, который подвёл каждого Java-разработчика хотя бы однажды.

split(regex) — строка на части

Простейший вызов разделяет строку по разделителю и возвращает String[]:

String[] parts = "red,green,blue".split(",");
// ["red", "green", "blue"]

Этот аргумент является регулярным выражением. Для обычного символа пунктуации, такого как ,, форма регулярного выражения выглядит точно так же, как литеральная форма, поэтому большинство применений выглядят безобидно. Проблемы начинаются, когда разделитель является метасимволом регулярного выражения:

"127.0.0.1".split(".");      // [] — '.' matches *any* character, every char is a delimiter
"127.0.0.1".split("\\.");    // ["127", "0", "0", "1"] — escape the dot
"x|y|z".split("|");          // ["x", "|", "y", "|", "z"] — '|' is alternation, matches the empty string
"x|y|z".split("\\|");        // ["x", "y", "z"]

Метасимволы, которые требуют экранирования для литерального разделения: ., |, \, (, ), [, ], {, }, +, *, ?, ^, $. Более безопасный шаблон для литеральных многосимвольных разделителей — это Pattern.quote:

String delim = "::";
String[] xs = "a::b::c".split(Pattern.quote(delim));   // ["a", "b", "c"]

Pattern.quote оборачивает входные данные в \Q...\E, так что каждый символ внутри воспринимается буквально, включая метасимволы регулярных выражений.

Аргумент limit: пустые конечные поля

split(regex, limit) контролирует количество разделений и то, что происходит с конечными пустыми полями:

  • limit > 0 — не более limit частей; последняя содержит оставшуюся часть без разделения.
  • limit == 0 — неограниченное число разделений; конечные пустые строки удаляются.
  • limit < 0 — неограниченное число разделений; конечные пустые строки сохраняются.

Среднее поведение является тихим сюрпризом. Строка CSV с двумя отсутствующими конечными полями по умолчанию разбирается с неправильной структурой:

"a,b,,,".split(",");      // ["a", "b"]               — trailing empties stripped
"a,b,,,".split(",", -1);  // ["a", "b", "", "", ""]   — trailing empties kept
"a,b,,,".split(",",  3);  // ["a", "b", ",,"]          — third element absorbs the rest

Для любых табличных данных — CSV, TSV, строк журналов с фиксированным количеством полей — передавайте -1. День, когда поле законно окажется пустым в конце строки, станет днём, когда отсутствующий -1 превратится в разбор с неправильной структурой в последующей обработке.

Потоки и списки

Естественное продолжение:

List<String> parts = Arrays.asList(csv.split(","));         // fixed-size, backed by the array
List<String> mutable = new ArrayList<>(parts);              // copy into a growable list

// Or via streams, with cheap transformation along the way:
List<Integer> ints = Pattern.compile(",")
    .splitAsStream("1,2,3,4")
    .map(String::trim)
    .map(Integer::parseInt)
    .toList();

Pattern.compile(regex).splitAsStream(input) — это ленивая потоковая альтернатива, когда вы хотите выполнять map/filter без материализации массива заранее. Для разовых разделений подходит String#split; для разделителя, который вы используете многократно, предварительная компиляция Pattern один раз и его повторное использование позволяет пропустить повторную компиляцию.

String.join — части в строку

Обратное направление — это String.join, добавленный в Java 8. Первый аргумент — разделитель, остальные — части, либо в виде varargs, либо в виде любого Iterable<? extends CharSequence>:

String csv = String.join(",", "red", "green", "blue");      // "red,green,blue"
String csv2 = String.join(",", List.of("red", "green", "blue"));

List<String> tags = List.of("java", "strings", "split");
String hashtags  = "#" + String.join(" #", tags);            // "#java #strings #split"

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

String.join с удовольствием принимает пустой разделитель:

String concatenated = String.join("", "a", "b", "c");        // "abc"

Иногда это именно то, что нужно; для нетривиальной конкатенации предпочтите StringBuilder.

Collectors.joining для потоков

Когда части поступают из потокового конвейера, Collectors.joining — это подходящий коллектор:

String list = users.stream()
    .map(User::name)
    .collect(Collectors.joining(", "));
// "Ada, Linus, Grace"

String pretty = users.stream()
    .map(User::name)
    .collect(Collectors.joining(", ", "[", "]"));
// "[Ada, Linus, Grace]"

Форма с тремя аргументами принимает разделитель, префикс и суффикс. Это идиоматический способ отобразить список в виде вывода "(a, b, c)" без ручного удаления конечной запятой.

Остерегайтесь коллизий регулярных выражений в split

Несколько тонких ловушек, о которых JDK не предупреждает:

  • Пайп (|) — это чередование. "a|b".split("|") делает не то, что вы думаете.
  • Точка — это «любой символ». "1.2.3".split(".") возвращает пустой массив.
  • split всегда возвращает не-null массив. Даже "".split(",") возвращает [""], а не [] — это полезно знать при итерации.
  • Пустое регулярное выражение "" совпадает между каждым символом. "abc".split("") возвращает ["a", "b", "c"]. Это не баг; иногда это полезно.

Если сомневаетесь, используйте Pattern.quote(delim).

replace против replaceAll — та же ловушка

Пока мы на теме аргументов-регулярных выражений в виде строк: String#replace(target, replacement) является литеральным для обоих аргументов. String#replaceAll(regex, replacement) является регулярным выражением для первого и частичным регулярным выражением для второго (ссылки на группы $1, экранирование \\). Одни и те же слова, совершенно разные парсеры. В большинстве случаев вам нужен replace, а не replaceAll.

Практический пример

Программа, которая разбирает три строки псевдо-CSV (с пустыми полями в середине и конечным пустым полем), извлекает имена, затем отображает их обратно с помощью String.join и Collectors.joining. Вызовы split(",", -1) иллюстрируют правило конечных пустых значений, а последние две строки демонстрируют ловушку с точкой.

java— editable, runs on the server

Прочитайте первые три строки вывода. Последняя строка, Linus,Torvalds,1969,,, сообщает о 5 ячейках — два конечных пустых значения сохраняются благодаря -1. Уберите -1, и та же строка придёт всего с 3 ячейками (Linus, Torvalds, 1969), и любая логика, ожидающая фиксированное количество полей, молча сломается. Строки 1.2.3 внизу — лучшая демонстрация того, почему . нужно экранировать при разделении по регулярному выражению: "1.2.3".split(".") возвращает пустой массив, потому что . совпадает с любым символом, тогда как "1.2.3".split("\\.") возвращает ["1", "2", "3"].

Что дальше

Это завершает Часть 9 — у вас есть рабочее понимание того, как создаётся String, как работает пул, почему важна иммутабельность, два изменяемых буфера, форматирование, сравнение, преобразование и цикл разделения/объединения. Следующая часть — одна из самых мощных и наиболее обсуждаемых возможностей Java: обобщения (generics). Они превращают коллекции из «контейнера Objectов, которые нужно приводить» в «контейнер конкретного типа, который проверяет за вас компилятор», и эта единственная идея проникает почти в каждый современный Java API. Продолжайте к Введению в обобщения Java.

Практика

Практика
Вы разбираете строку CSV с фиксированной схемой из 5 полей. Последние два поля иногда пустые. Какой вызов `split` даёт вам ровно 5 строк каждый раз?
Вы разбираете строку CSV с фиксированной схемой из 5 полей. Последние два поля иногда пустые. Какой вызов `split` даёт вам ровно 5 строк каждый раз?
Was this page helpful?