Чому в 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?


Здається, це виправлено Java8. Тим часом, s.split("(?!^)")здається, працює.
shkschneider

2
@shkschneider Поведінка, описана в моєму запитанні, не є помилкою попередніх версій Java-8. Така поведінка була не дуже корисною, але вона все-таки була правильною (як показано в моєму запитанні), тому не можна сказати, що вона "виправлена". Я бачу це більше схоже на поліпшення , щоб ми могли використовувати split("")замість загадкова (для людей , які не використовують регулярні вирази) split("(?!^)")або split("(?<!^)")чи кілька інших регулярних виразів.
Пшемо

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 і вище

Щоб зробити splitповедінку послідовно в різних версіях та сумісною з поведінкою в Java 8:

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

(?!\A) перевіряє, що рядок не закінчується на початку рядка, що означає, що збіг є порожнім збігом на початку рядка.

Слідкуючи за поведінкою в Java 7 і раніше

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


Будь-яка ідея, як я можу змінити split("")код, щоб він відповідав різним версіям Java?
Даніель

2
@Daniel: Це можна зробити вперед-сумісним (стежити за поведінкою Java 8) шляхом додавання (?!^)до кінця регулярного виразу і вкрити оригінальні регулярні вирази , не захоплення групи (?:...)(при необхідності), але я не можу згадати ні одного спосіб зробити його сумісним назад (дотримуйтесь старої поведінки в Java 7 та попередніх).
nhahtdh

Дякую за пояснення. Не могли б ви описати "(?!^)"? У яких сценаріях він буде відрізнятися від ""? (Мені страшно в регексе!: - /).
Даніель

1
@Daniel: На його значення впливає Pattern.MULTILINEпрапор, хоча \Aзавжди відповідає на початку рядка незалежно від прапорів.
nhahtdh

30

Це було визначено в документації України split(String regex, limit).

Якщо на початку цього рядка є відповідність позитивної ширини, то на початку результуючого масиву додається порожня провідна підрядка. Зрівняння нульової ширини на початку, однак ніколи не створює такої порожньої провідної підрядки.

У "abc".split("")вас є нульова ширина матч на самому початку , так що провідна порожня підрядка не входять в результуючому масиві.

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

(Видалено неактуальний вихідний код)


3
Це просто питання. Чи добре розмістити фрагмент коду з JDK? Пам'ятаєте проблему з авторськими правами на Google - Harry Potter - Oracle?
Пол Варгас

6
@PaulVargas Справедливості я не знаю, але я вважаю, що це нормально, оскільки ви можете завантажити JDK і розпакуйте файл src, який містить усі джерела. Тож технічно всі могли бачити джерело.
Алексіс С.

12
@PaulVargas "Відкритий" у "відкритому коді" щось означає.
Марко Топольник

2
@ZouZou: те, що всі бачать це, не означає, що ви можете його повторно опублікувати
user102008

2
@Paul Vargas, IANAL, але в багатьох інших випадках цей тип публікації підпадає під ситуацію котирування / справедливого використання. Більше про тему тут: meta.stackexchange.com/questions/12527/…
Алекс

14

Була незначна зміна в документах для split()від Java 7 до Java 8. Зокрема, було додано наступне твердження:

Якщо на початку цього рядка є відповідність позитивної ширини, то на початку результуючого масиву додається порожня провідна підрядка. Зрівняння нульової ширини на початку, однак ніколи не створює такої порожньої провідної підрядки.

(наголос мій)

Порожній розділений рядок створює збіг нульової ширини на початку, тому порожній рядок не включається на початку результуючого масиву відповідно до того, що зазначено вище. На відміну від цього, ваш другий приклад, який розщеплюється, "a"генерує позитивну ширину на початку рядка, тому порожній рядок насправді включається на початок результуючого масиву.


Ще кілька секунд змінили значення.
Пол Варгас

2
@PaulVargas насправді тут аршаджі опублікував відповідь за кілька секунд до ZouZou, але, на жаль, ZouZou відповів на моє запитання раніше тут . Мені було цікаво, чи варто мені задавати це питання, оскільки я вже знав відповідь, але це здалося цікавим, і ZouZou заслужив деяку репутацію за свій попередній коментар.
Пшемо

5
Незважаючи на те, що нова поведінка виглядає більш логічно , це, очевидно, відстала сумісність . Єдине виправдання цієї зміни - "some-string".split("")це досить рідкісний випадок.
ivstas

4
.split("")це не єдиний спосіб розділити без узгодження нічого. Ми використовували позитивний регекс lookahead, який у jdk7, який також відповідав на початку, і створив порожній головний елемент, якого зараз немає. github.com/spray/spray/commit/…
jrudolph
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.