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

110

До 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"].

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

Pshemo
источник
Кажется, что Java8 это исправляет. Между тем s.split("(?!^)")вроде работает.
шкшнайдер
2
@shkschneider Поведение, описанное в моем вопросе, не является ошибкой версий до Java-8. Такое поведение не было особенно полезным, но все же было правильным (как показано в моем вопросе), поэтому мы не можем сказать, что оно было «исправлено». Я вижу это больше похоже на улучшение , чтобы мы могли использовать split("")вместо загадочная (для людей , которые не используют регулярные выражения) split("(?!^)")или split("(?<!^)")или несколько других регулярных выражений.
Pshemo
1
Возникла такая же проблема после обновления Fedora до Fedora 21, Fedora 21 поставляется с JDK 1.8, и мое игровое приложение IRC не работает из-за этого.
LiuYan 刘 研
7
Этот вопрос, по-видимому, является единственной документацией об этом критическом изменении в Java 8. Oracle исключила его из своего списка несовместимостей .
Шон Ван Гордер
4
Это изменение в JDK стоило мне 2 часов поиска того, что не так. Код работает нормально на моем компьютере (JDK8), но загадочным образом не работает на другом компьютере (JDK7). Oracle ДЕЙСТВИТЕЛЬНО ДОЛЖЕН обновлять документацию String.split (String regex) , а не Pattern.split или String.split (String regex, int limit), поскольку это, безусловно, наиболее распространенное использование. Java известна своей портативностью, также известной как WORA. Это серьезное обратное изменение, которое вообще не задокументировано.
PoweredByRice

Ответы:

84

Поведение 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 и выше

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

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

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

Следуя поведению в Java 7 и ранее

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

nhahtdh
источник
Есть идеи, как изменить split("")код, чтобы он согласовывался между разными версиями Java?
Дэниел
2
@Daniel: Это можно сделать вперед-совместимым (следить за поведением Java 8) путем добавления (?!^)к концу регулярного выражения и укрыть оригинальные регулярные выражения , не захват группы (?:...)(при необходимости), но я не могу вспомнить ни одного способ сделать его обратно совместимым (следуйте старому поведению в Java 7 и ранее).
nhahtdh
Спасибо за объяснение. Не могли бы вы описать "(?!^)"? В каких сценариях это будет отличаться ""? (Я ужасен в регулярных выражениях!: - /).
Даниэль
1
@Daniel: его значение зависит от Pattern.MULTILINEфлага, но \Aвсегда соответствует началу строки независимо от флагов.
nhahtdh 02
30

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

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

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

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

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

Алексис С.
источник
3
Это просто вопрос. Можно ли разместить фрагмент кода из JDK? Помните проблему авторских прав с Google - Гарри Поттер - Oracle?
Пол Варгас
6
@PaulVargas Честно говоря, я не знаю, но полагаю, что все в порядке, поскольку вы можете загрузить JDK и распаковать файл src, содержащий все исходные коды. Так что технически каждый мог видеть источник.
Alexis C.
12
@PaulVargas Слово «открытый» в «открытом исходном коде» действительно что-то означает.
Марко Топольник
2
@ZouZou: то, что все это видят, не означает, что вы можете опубликовать его повторно
user102008,
2
@Paul Vargas, IANAL, но во многих других случаях сообщения такого типа подпадают под ситуацию цитаты / добросовестного использования. Больше по теме здесь: meta.stackexchange.com/questions/12527/…
Alex Pakka
14

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

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

(курсив мой)

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

аршаджи
источник
Еще несколько секунд изменили ситуацию.
Пол Варгас
2
@PaulVargas на самом деле здесь arshajii отправил ответ за несколько секунд до ZouZou, но, к сожалению, ZouZou ответил на мой вопрос ранее здесь . Мне было интересно, стоит ли мне задавать этот вопрос, поскольку я уже знал ответ, но он казался интересным, и ZouZou заслужил некоторую репутацию за свой предыдущий комментарий.
Pshemo
5
Несмотря на то, что новое поведение выглядит более логичным , очевидно, что это нарушение обратной совместимости . Единственное оправдание этого изменения "some-string".split("")- это довольно редкий случай.
ivstas
4
.split("")это не единственный способ разделить ничего не сопоставив. Мы использовали регулярное выражение положительного просмотра вперед, которое в jdk7 также соответствовало в начале и создавало пустой элемент заголовка, которого теперь нет. github.com/spray/spray/commit/…
jrudolph