Метод split разбивает строку на части по разделителю и возвращает массив строк: "I love Java".split(" ") даст {"I", "love", "Java"}. Разделитель тут задается не символом, а регулярным выражением, и отсюда растут почти все неожиданности: спецсимволы вроде точки или вертикальной черты приходится экранировать, ведущий разделитель добавляет в начало массива пустую строку, а пустые строки в конце по умолчанию отбрасываются. Есть и второй вариант метода, с дополнительным параметром limit, который ограничивает число частей.

Кратко

  • split(String regex) возвращает массив строк, разбив исходную по совпадениям с регулярным выражением. Если совпадений нет, в массиве будет один элемент: сама строка целиком.
  • Разделитель это регулярное выражение, а не просто символ. Спецсимволы регулярок (точка, вертикальная черта, звездочка, плюс, скобки и другие) нужно экранировать: в коде это выглядит как "\\|" или Pattern.quote("|").
  • Ведущий разделитель дает пустую строку первым элементом массива. Разделитель в конце не дает ничего: хвостовые пустые строки при значении limit по умолчанию отбрасываются.
  • Второй вариант метода принимает limit: положительный ограничивает длину массива, ноль отбрасывает пустые строки в конце, отрицательный сохраняет их все.
  • split(regex) это то же самое, что split(regex, 0), так и написано в исходниках класса String.
  • Пустая строка на входе дает массив из одного элемента, пустой строки, а не пустой массив. Поэтому проверка вида arr.length == 0 после split не сработает никогда.
Давай поговорим о методе String split: что он делает и зачем нужен. Несложно догадаться, что он делит строку, но как это работает на практике? Давай подробно рассмотрим работу метода и обсудим некоторые неочевидные детали, а заодно узнаем, сколько методов split есть в классе String на самом деле. Погнали!

Определение и сигнатура для Java String.split

Метод split в Java разделяет строку на подстроки, используя разделитель, который определяется с помощью регулярного выражения. Приведем сигнатуру метода и начнем наше погружение:

String[] split(String regex)
Из сигнатуры ясны две вещи:
  1. Метод возвращает массив строк.
  2. Метод принимает строку regex в качестве параметра.
Разберем каждую вещь по отдельности в разрезе определения, приведенного выше.
  1. Метод возвращает массив строк.

    В определении есть такие слова: "Метод split в Java разделяет строку на подстроки". Данные подстроки собираются методом в массив и представляют собой его возвращаемое значение.

  2. Метод принимает строку regex в качестве параметра.

    Опять же, вспомним определение: "разделяет строку на подстроки, используя разделитель, который определяется с помощью регулярного выражения". Принимаемый параметр regex это шаблон регулярного выражения, который применяется к исходной строке и по совпадениям определяет в ней символ (или комбинацию символов) разделителя.

Эдвард Руки-ножницы в черном костюме, руки с длинными лезвиями разведены в стороны

Split на практике

Теперь ближе к делу. Представим, что у нас есть строка со словами. Например, такая:
I love Java
Нам нужно разбить строку на слова. Мы видим, что в данной строке слова разделены друг от друга пробелами. Пробел это идеальный кандидат на роль разделителя в данном случае. Так выглядит код решения данной задачи:

public class Main {
    public static void main(String[] args) {
        String str = "I love Java";
        String[] words = str.split(" ");
        for (String word : words) {
            System.out.println(word);
        }
    }
}
Выводом метода main будут следующие строки:
I love Java
Посмотрим еще на несколько примеров того, как бы отработал метод split:
Строка Разделитель Результат работы метода
"I love Java" " " (символ пробела) {"I", "love", "Java"}
"192.168.0.1:8080" ":" {"192.168.0.1", "8080"}
"Красный, оранжевый, желтый" "," {"Красный", " оранжевый", " желтый"}
"Красный, оранжевый, желтый" ", " {"Красный", "оранжевый", "желтый"}
Обрати внимание на различия между последними двумя строками в таблице выше. В предпоследней строке разделителем выступает символ запятой, поэтому строка разбилась таким образом, что в некоторых словах есть ведущие пробелы. В последней же строке в качестве разделителя мы использовали символ запятой и символ пробела. Поэтому в результирующем массиве не оказалось строк с ведущими пробелами. Это просто маленькая деталь, в которой демонстрируется, как важно внимательно подбирать правильный разделитель.

Ведущий разделитель

Есть еще один важный нюанс. Если исходная строка начинается с разделителя, первым элементом результирующего массива будет пустая строка. На примере это будет выглядеть так: Исходная строка: " I love Java" Разделитель: " " Результирующий массив: { "", "I", "love", "Java" } Но если исходная строка заканчивается разделителем, а не начинается, результат будет иным: Исходная строка: "I love Java " Разделитель: " " Результирующий массив: { "I", "love", "Java" } Смотрим в коде на вариации работы метода split с символом разделителя в конце и/или начале исходной строки:

public class Main {
    public static void main(String[] args) {
        print("I love Java".split(" "));
        print(" I love Java".split(" "));
        print("I love Java ".split(" "));
        print(" I love Java ".split(" "));
    }

    static void print(String[] arr) {
        System.out.println(Arrays.toString(arr));
    }
}
Вывод метода main будет таким:
[I, love, Java] [, I, love, Java] [I, love, Java] [, I, love, Java]
Еще раз обрати внимание: когда в исходной строке первый символ это символ разделителя, в результате в массиве первым элементом будет пустая строка. Тут есть одна оговорка, и она прямо прописана в документации метода: пустая строка появляется в начале, только если совпадение в начале имело ненулевую длину. Если же шаблон совпадает с пустотой, ведущей пустой строки не будет. Поэтому "abc".split("") вернет {"a", "b", "c"}, а не {"", "a", "b", "c"}.

Перегруженный собрат

В классе String есть еще один метод split с такой сигнатурой:

String[] split(String regex, int limit)
У этого метода есть дополнительный параметр limit: он определяет, какое количество раз шаблон regex будет применяться к исходной строке. Ниже пояснения:

limit > 0

Применяется limit-1 раз. При этом длина массива не будет превышать значение limit. Последним элементом массива будет часть строки, следующая за последним найденным разделителем. Пример:

public class Main {
    public static void main(String[] args) {
        print("I love Java".split(" ", 1));
        print("I love Java".split(" ", 2));
        /*
         Вывод: 
         [I love Java]
         [I, love Java]
        */
    }

    static void print(String[] arr) {
        System.out.println(Arrays.toString(arr));
    }
}

limit < 0

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

public class Main {
    public static void main(String[] args) {
        // Обратите внимание на пробел в конце строки
        print("I love Java ".split(" ", -1));
        print("I love Java ".split(" ", -2));
        print("I love Java ".split(" ", -12));
        /*
         Вывод:
        [I, love, Java, ]
        [I, love, Java, ]
        [I, love, Java, ]
        
        Обратите внимание: последний элемент массива это
        пустая строка, возникшая из-за пробела
        в конце исходной строки. 
        */
    }

    static void print(String[] arr) {
        System.out.println(Arrays.toString(arr));
    }
}

limit 0

Как и в случае с limit < 0, шаблон для поиска разделителя применяется к строке столько раз, сколько это возможно. Итоговый массив может быть любой длины. Если последние элементы равны пустой строке, в итоговом массиве они будут отброшены. Пример:

public class Main {
    public static void main(String[] args) {
        // Обратите внимание на пробел в конце строки
        print("I love Java ".split(" ", 0));
        print("I love Java ".split(" ", -1));
        /*
         Вывод:
        [I, love, Java]
        [I, love, Java, ]
        Первый вызов отбросил пустую строку в конце, второй ее сохранил
        */
    }

    static void print(String[] arr) {
        System.out.println(Arrays.toString(arr));
    }
}
Все три случая удобно держать рядом:
Значение limit Сколько раз применяется шаблон Пустые строки в конце Пример для строки "I love Java " с пробелом на конце
limit больше нуля Не больше чем limit-1 раз, длина массива не превысит limit Остаются, если попали в массив split(" ", 2) даст {"I", "love Java "}
limit равен нулю Столько раз, сколько получится Отбрасываются split(" ", 0) даст {"I", "love", "Java"}
limit меньше нуля Столько раз, сколько получится Сохраняются все split(" ", -1) даст {"I", "love", "Java", ""}
Если мы посмотрим на реализацию обоих методов в исходниках класса String, увидим, что оба они ведут в один и тот же внутренний метод, а вариант с одним аргументом просто подставляет нулевой limit:

    public String[] split(String regex) {
        return split(regex, 0, false);
    }

    public String[] split(String regex, int limit) {
        return split(regex, limit, false);
    }
Что за третий аргумент false, разберемся прямо сейчас.

А с Java 21 методов стало три

Помнишь вопрос из вступления, сколько же методов split в классе String на самом деле? Долгое время ответ был «два», но в Java 21 у них появился третий родственник:

String[] splitWithDelimiters(String regex, int limit)
Он делает ровно то же, что и split, только не выбрасывает разделители, а возвращает их вместе с частями. В массиве каждая часть идет в паре со своим разделителем, и только у последней части разделителя нет:

public class Main {
    public static void main(String[] args) {
        String userInfo = "135|bender|bender@gmail.com";
        System.out.println(Arrays.toString(userInfo.splitWithDelimiters("\\|", 0)));
        // Вывод: [135, |, bender, |, bender@gmail.com]
    }
}
Параметр limit тут работает по тем же правилам, что и у обычного split. А тот самый третий аргумент во внутреннем методе как раз и означает «разделители не возвращать»: split зовет его с false, а splitWithDelimiters с true.

Различные примеры

В рабочей практике иногда бывает так, что у нас есть строка, составленная по определенным правилам. Данная строка может "приходить" в нашу программу откуда угодно:
  • из стороннего сервиса;
  • из запроса к нашему серверу;
  • из конфигурационного файла;
  • и т.д.
Обычно в такой ситуации программисту известны "правила игры". Скажем, программист знает, что у него есть информация о пользователе, которая хранится по такому шаблону:
user_id|user_login|user_email
Для примера возьмем конкретные значения:
135|bender|bender@gmail.com
И вот перед программистом стоит задача: написать метод, который отправляет электронное письмо пользователю. В его распоряжении информация о юзере, записанная в указанном выше формате. Ну а подзадача, которую мы продолжим разбирать, это вычленить email-адрес из общей информации о пользователе. Это один из примеров, когда метод split может оказаться полезным. Ведь если мы взглянем на шаблон, мы поймем: чтобы вычленить email-адрес пользователя из всей информации, нам нужно всего лишь разделить строку с помощью метода split. Тогда email-адрес будет лежать в последнем элементе результирующего массива. Приведем пример такого метода, который принимает в себя строку, содержащую информацию о пользователе, и возвращает email пользователя. Для упрощения предположим, что данная строка всегда соответствует нужному нам формату:

public class Main {
    public static void main(String[] args) {
        String userInfo = "135|bender|bender@gmail.com";
        System.out.println(getUserEmail(userInfo));
        // Вывод: bender@gmail.com
    }

    static String getUserEmail(String userInfo) {
        String[] data = userInfo.split("\\|");
        return data[2]; // или data[data.length - 1]
    }
}
Обратите внимание на разделитель: "\\|". Так как в регулярных выражениях “|” это специальный символ, на котором завязана определенная логика, использовать его как обычный (тот, который мы хотим найти в исходной строке) просто так не выйдет: его нужно экранировать. И вот тут легко запутаться в двух уровнях экранирования. Самому регулярному выражению хватает одного обратного слеша, \|. Но записываем-то мы это выражение строковым литералом Java, а в литерале обратный слеш и сам служебный, так что его тоже надо удвоить. Отсюда и "\\|" в коде: первый слеш экранирует второй, а до движка регулярных выражений доезжает ровно \|. Если возиться со слешами не хочется, есть готовый способ: метод Pattern.quote() превращает любую строку в шаблон, который ищется буквально. Вместо split("\\|") можно написать split(Pattern.quote("|")), и никакие спецсимволы внутри уже не помешают. Рассмотрим другой пример. Допустим, у нас есть информация о заказе, которая записана примерно в таком формате:
item_number_1,item_name_1,item_price_1;item_number_2,item_name_2,item_price_2;...;item_number_n,item_name_n,item_price_n
Ну или же возьмем конкретные значения:
1,огурцы,20.05;2,помидоры,123.45;3,зайцы,0.50
Перед нами стоит задача: рассчитать итоговую стоимость заказа. Здесь нам придется применять метод split несколько раз. Первым шагом мы разделим строку через символ ";", на составные части. Тогда в каждой такой части у нас будет информация об отдельном товаре, которую мы сможем обработать в дальнейшем. А затем, в рамках каждого товара будем разделять информацию с помощью символа "," и брать из результирующего массива элемент с определенным индексом (в котором хранится цена), приводить ее к числовому виду и составлять итоговую стоимость заказа. Напишем метод, который все это посчитает:

public class Main {
    public static void main(String[] args) {
        String orderInfo = "1,огурцы,20.05;2,помидоры,123.45;3,зайцы,0.50";
        System.out.println(getTotalOrderAmount(orderInfo));
        // Вывод: 144.0
    }

    static double getTotalOrderAmount(String orderInfo) {
        double totalAmount = 0d;
        final String[] items = orderInfo.split(";");

        for (String item : items) {
            final String[] itemInfo = item.split(",");
            totalAmount += Double.parseDouble(itemInfo[2]);
        }

        return totalAmount;
    }
}
Попробуй самостоятельно разобраться с тем, как работает данный метод. На основе данных примеров можно сказать, что метод split используется тогда, когда у нас есть некоторая информация в строковом виде, из которой нам необходимо вычленить какую-то более специфическую информацию.

Что есть рядом со split

Метод split закрывает не все задачи с разбиением строк, и рядом с ним живет несколько инструментов, о которых полезно знать заранее.

Один шаблон на много строк

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

private static final Pattern SEPARATOR = Pattern.compile("\\s*,\\s*");

// дальше в коде
String[] parts = SEPARATOR.split(line);

Сразу в стрим

У Pattern с Java 8 есть метод splitAsStream(): он отдает не массив, а поток строк, и это удобно, когда части все равно нужно тут же отфильтровать или преобразовать.

List<String> tags = Pattern.compile("\\s*,\\s*")
        .splitAsStream("java, spring, sql")
        .toList();
// [java, spring, sql]

Обратная операция

Если части нужно склеить назад, писать цикл не надо: с Java 8 есть String.join().

String csv = String.join(",", "java", "spring", "sql");
// java,spring,sql
Второй вариант метода принимает не набор аргументов, а любую коллекцию строк, так что результат split в него отправляется без переделок.

Разбить по строкам

А вот для переводов строк split лучше не звать вовсе. С Java 11 у String есть метод lines(), который сам разбирается со всеми тремя вариантами перевода строки: \n, \r и \r\n.

"первая\nвторая\nтретья".lines().forEach(System.out::println);

Итоги

Мы рассмотрели с тобой метод split класса String. Он нужен для разделения строки на составные части с помощью специального разделителя. Метод возвращает массив строк (составные части строки). Принимает в себя регулярное выражение, по которому находит символ(ы) разделителя. Мы рассмотрели различные тонкости работы данного метода:
  • ведущий символ разделителя;
  • перегруженный собрат с двумя аргументами.
Также попробовали смоделировать некоторые ситуации “из жизни”, в которых использовали метод split для решения пусть и вымышленных, но вполне реалистичных проблем.

Вопросы и ответы

Почему split не разбивает строку по точке?

Потому что точка в регулярном выражении означает любой символ. Вызов "a.b.c".split(".") совпадет с каждым символом строки, все части окажутся пустыми, а пустые строки в конце отбрасываются, так что вернется массив нулевой длины. Точку нужно экранировать: split("\\.") или, что надежнее, split(Pattern.quote(".")).

Откуда в начале массива берется пустая строка?

Она появляется, если исходная строка начинается с разделителя. Для " I love Java" с пробелом в качестве разделителя результат будет из четырех элементов, где первый это пустая строка. Обратной ситуации нет: разделитель в конце строки лишнего элемента не добавит, потому что при limit по умолчанию пустые строки в конце отбрасываются.

Что вернет split для пустой строки?

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

Как разбить строку и сразу убрать лишние пробелы вокруг частей?

Пробелы можно забрать в сам разделитель. Для строки "Красный, оранжевый, желтый" вызов split(",\\s*") или split("\\s*,\\s*") сразу даст части без ведущих пробелов. Альтернатива это разбить по запятой и пройтись методом strip по каждому элементу.

Что быстрее, если делить много строк одним и тем же разделителем?

Заранее скомпилированный шаблон. В исходниках String есть быстрый путь: если разделитель это один символ и он не входит в набор спецсимволов регулярок, либо два символа, где первый обратный слеш, метод делит строку напрямую, без регулярных выражений. Во всех остальных случаях Pattern компилируется заново на каждый вызов, и тогда выгоднее один раз сделать Pattern.compile и потом звать split у готового шаблона.

Как склеить части обратно в строку?

Для этого есть парный метод String.join, он появился в Java 8. Вызов String.join("-", "Java", "is", "cool") вернет строку Java-is-cool, а вторым вариантом метода можно склеить любую коллекцию строк. По сути это операция, обратная split.

Читайте также