Почему в 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?

Ответы

Ответ 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", генерирует совпадение положительной ширины в начале строки, поэтому пустая строка фактически включена в начале результирующего массива.