Почему в 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"],

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

3 ответа

Решение

Поведение 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 указать на вашу собственную реализацию.

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

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

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

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

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

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

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

(акцент мой)

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

Другие вопросы по тегам