Почему в 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:
- Если ваше регулярное выражение может соответствовать строке нулевой длины, просто добавьте
(?!\A)
в конце регулярного выражения и перенесите исходное регулярное выражение в группу без захвата(?:...)
(при необходимости). - Если ваше регулярное выражение не может соответствовать строке нулевой длины, вам не нужно ничего делать.
- Если вы не знаете, может ли регулярное выражение соответствовать строке нулевой длины или нет, выполните оба действия на шаге 1.
(?!\A)
проверяет, что строка не заканчивается в начале строки, что подразумевает, что совпадение является пустым совпадением в начале строки.
Следующее поведение в Java 7 и предыдущих версиях
Не существует общего решения для обеспечения split
обратной совместимости с Java 7 и более ранними версиями, за исключением замены всех экземпляров split
, указывающих на вашу собственную пользовательскую реализацию.
Ответ 2
Это было указано в документации split(String regex, limit)
.
Когда в начале этой строки есть совпадение с положительной шириной, то в начало результирующего массива включается пустая начальная подстрока. Однако совпадение с нулевой шириной в начале никогда не создает такую пустую начальную подстроку.
В "abc".split("")
вы получили совпадение нулевой ширины в начале, поэтому ведущая пустая подстрока не включена в результирующий массив.
Однако во втором фрагменте при разделении на "a"
вы получили положительное совпадение ширины (в данном случае 1), поэтому пустая начальная подстрока включена, как и ожидалось.
(Удален нерелевантный исходный код)
Ответ 3
В документации для split()
были внесены небольшие изменения с Java 7 на Java 8. В частности, было добавлено следующее утверждение:
Когда в начале этой строки есть совпадение с положительной шириной, то в начало результирующего массива включается пустая начальная подстрока. Совпадение нулевой ширины в начале, однако, никогда не создает такую пустую начальную подстроку.
(выделено мной)
Разделение пустой строки генерирует совпадение нулевой ширины в начале, поэтому пустая строка не включается в начало результирующего массива в соответствии с тем, что указано выше. Напротив, ваш второй пример, который разбивается на "a"
генерирует положительное совпадение ширины в начале строки, поэтому пустая строка фактически включена в начало результирующего массива.