Вопрос-ответ

Why in Java 8 split sometimes removes empty strings at start of result array?

Почему в Java 8 split иногда удаляет пустые строки в начале результирующего массива?

До Java 8, когда мы разделяли пустую строку, например

String[] tokens = "abc".split("");

механизм разделения будет разделяться в местах, отмеченных знаком |

|a|b|c|

потому что пустое пространство "" существует до и после каждого символа. Таким образом, в результате сначала будет сгенерирован этот массив.

["", "a", "b", "c", ""]

и позже будет удалять конечные пустые строки (потому что мы явно не указали отрицательное значение для limit аргумента), поэтому он, наконец, вернется

["", "a", "b", "c"]

В Java 8 механизм разделения, похоже, изменился. Теперь, когда мы используем

"abc".split("")

мы получим ["a", "b", "c"] массив вместо ["", "a", "b", "c"].

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

Но эта теория терпит неудачу, поскольку

"abc".split("a")

возвращает ["", "bc"], поэтому ведущая пустая строка не была удалена.

Кто-нибудь может объяснить, что здесь происходит? Как изменились правила split в Java 8?

Переведено автоматически
Ответ 1

Поведение String.split (который вызывает Pattern.split) меняется между Java 7 и Java 8.

Документация

Сравнивая документацию Pattern.split в Java 7 и Java 8, мы видим, что добавлено следующее предложение:


Когда в начале входной последовательности есть совпадение с положительной шириной, то в начало результирующего массива включается пустая начальная подстрока. Однако совпадение нулевой ширины в начале никогда не создает такую пустую начальную подстроку.


Такое же предложение также добавлено в String.split в Java 8 по сравнению с Java 7.

Эталонная реализация

Давайте сравним код Pattern.split эталонной реализации в Java 7 и Java 8. Код извлечен из grepcode для версий 7u40-b43 и 8-b132.

Java 7

public String[] split(CharSequence input, int limit) {
int index = 0;
boolean matchLimited = limit > 0;
ArrayList<String> matchList = new ArrayList<>();
Matcher m = matcher(input);

// Add segments before each match found
while(m.find()) {
if (!matchLimited || matchList.size() < limit - 1) {
String match = input.subSequence(index, m.start()).toString();
matchList.add(match);
index = m.end();
} else if (matchList.size() == limit - 1) { // last one
String match = input.subSequence(index,
input.length()).toString();
matchList.add(match);
index = m.end();
}
}

// If no match was found, return this
if (index == 0)
return new String[] {input.toString()};

// Add remaining segment
if (!matchLimited || matchList.size() < limit)
matchList.add(input.subSequence(index, input.length()).toString());

// Construct result
int resultSize = matchList.size();
if (limit == 0)
while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
resultSize--;
String[] result = new String[resultSize];
return matchList.subList(0, resultSize).toArray(result);
}

Java 8

public String[] split(CharSequence input, int limit) {
int index = 0;
boolean matchLimited = limit > 0;
ArrayList<String> matchList = new ArrayList<>();
Matcher m = matcher(input);

// Add segments before each match found
while(m.find()) {
if (!matchLimited || matchList.size() < limit - 1) {
if (index == 0 && index == m.start() && m.start() == m.end()) {
// no empty leading substring included for zero-width match
// at the beginning of the input char sequence.
continue;
}
String match = input.subSequence(index, m.start()).toString();
matchList.add(match);
index = m.end();
} else if (matchList.size() == limit - 1) { // last one
String match = input.subSequence(index,
input.length()).toString();
matchList.add(match);
index = m.end();
}
}

// If no match was found, return this
if (index == 0)
return new String[] {input.toString()};

// Add remaining segment
if (!matchLimited || matchList.size() < limit)
matchList.add(input.subSequence(index, input.length()).toString());

// Construct result
int resultSize = matchList.size();
if (limit == 0)
while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
resultSize--;
String[] result = new String[resultSize];
return matchList.subList(0, resultSize).toArray(result);
}

Добавление следующего кода в Java 8 исключает совпадение нулевой длины в начале входной строки, что объясняет поведение, описанное выше.

            if (index == 0 && index == m.start() && m.start() == m.end()) {
// no empty leading substring included for zero-width match
// at the beginning of the input char sequence.
continue;
}

Поддержание совместимости

Следующее поведение в Java 8 и выше

Чтобы сделать split поведение согласованным в разных версиях и совместимым с поведением в Java 8:


  1. Если ваше регулярное выражение может соответствовать строке нулевой длины, просто добавьте (?!\A) в конце регулярного выражения и перенесите исходное регулярное выражение в группу без захвата (?:...) (при необходимости).

  2. Если ваше регулярное выражение не может соответствовать строке нулевой длины, вам не нужно ничего делать.

  3. Если вы не знаете, может ли регулярное выражение соответствовать строке нулевой длины или нет, выполните оба действия на шаге 1.

(?!\A) проверяет, что строка не заканчивается в начале строки, что подразумевает, что совпадение является пустым совпадением в начале строки.

Следующее поведение в Java 7 и предыдущих версиях

Не существует общего решения для обеспечения split обратной совместимости с Java 7 и более ранними версиями, за исключением замены всех экземпляров split, указывающих на вашу собственную пользовательскую реализацию.

Ответ 2

Это было указано в документации split(String regex, limit).


Когда в начале этой строки есть совпадение с положительной шириной, то в начало результирующего массива включается пустая начальная подстрока. Однако совпадение с нулевой шириной в начале никогда не создает такую пустую начальную подстроку.


В "abc".split("") вы получили совпадение нулевой ширины в начале, поэтому ведущая пустая подстрока не включена в результирующий массив.

Однако во втором фрагменте при разделении на "a" вы получили положительное совпадение ширины (в данном случае 1), поэтому пустая начальная подстрока включена, как и ожидалось.

(Удален нерелевантный исходный код)

Ответ 3

В документации для split() были внесены небольшие изменения с Java 7 на Java 8. В частности, было добавлено следующее утверждение:


Когда в начале этой строки есть совпадение с положительной шириной, то в начало результирующего массива включается пустая начальная подстрока. Совпадение нулевой ширины в начале, однако, никогда не создает такую пустую начальную подстроку.


(выделено мной)

Разделение пустой строки генерирует совпадение нулевой ширины в начале, поэтому пустая строка не включается в начало результирующего массива в соответствии с тем, что указано выше. Напротив, ваш второй пример, который разбивается на "a" генерирует положительное совпадение ширины в начале строки, поэтому пустая строка фактически включена в начало результирующего массива.

java regex java-8