Java 8の分割で、結果配列の先頭にある空の文字列が削除されることがあるのはなぜですか?


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")

startに空の文字列の配列を返します["", "bc"]

ここで何が起こっているのか、Java 8で分割のルールがどのように変更されたのかを誰かが説明できますか?


Java8はそれを修正するようです。一方、動作するs.split("(?!^)")ようです。
shkschneider 2014年

2
@shkschneider私の質問で説明されている動作は、Java-8以前のバージョンのバグではありません。この振る舞いはそれほど有用ではありませんでしたが、それでも(私の質問に示されているように)正しいため、「修正済み」であるとは言えません。私はそれを改善のように見ているのでsplit("")、不可解な(正規表現を使用しない人々のための)split("(?!^)")またはsplit("(?<!^)")他の少数の正規表現の代わりに使用できます。
Pshemo 2014年

1
fedoraをFedora 21にアップグレードした後、同じ問題が発生しました。fedora21はJDK 1.8に同梱されており、これが原因でIRCゲームアプリケーションが壊れています。
LiuYan刘研

7
この質問は、Java 8のこの重大な変更に関する唯一のドキュメントのようです。Oracleは、非互換性のリストから除外しました。
Sean Van Gorder

4
JDKのこの変更により、問題の原因を突き止めるのに2時間かかりました。コードは私のコンピューター(JDK8)では問題なく実行されますが、別のマシン(JDK7)では不思議なことに失敗します。Oracleは、本当にすべきであるのドキュメント更新のstring.Split(文字列の正規表現を)これはこれまでで最も一般的な使用方法であるとして、むしろPattern.splitかのstring.Splitに比べて、(文字列の正規表現、int型の制限)。Javaは、その移植性、いわゆるWORAで知られています。これは主要な後方互換性のある変更であり、十分に文書化されていません。
PoweredByRice 2015年

回答:


84

String.split(を呼び出すPattern.split)の動作は、Java 7とJava 8で異なります。

ドキュメンテーション

ドキュメント間の比較Pattern.splitのJava 7Javaの8、我々は追加されている次の句を守ってください。

入力シーケンスの先頭に正の幅の一致がある場合、結果の配列の先頭に空の先行部分文字列が含まれます。ただし、最初に幅が一致しないと、そのような空の先行部分文字列は生成されません。

Java 7と比較して、同じ句がJava 8にも追加さString.splitれています

リファレンス実装

Pattern.splitJava 7とJava 8の参照実装のコードを比較してみましょう。コードは、バージョン7u40-b43および8-b132のgrepcodeから取得されます。

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. 正規表現長さ0の文字列と一致しない場合、何もする必要はありません。
  3. 正規表現が長さ0の文字列と一致するかどうかわからない場合は、手順1で両方の操作を実行します。

(?!\A) 文字列が文字列の先頭で終了していないことを確認します。これは、一致が文字列の先頭で空の一致であることを意味します。

Java 7以前の動作の追跡

独自のカスタム実装を指すようsplitにすべてのインスタンスを置き換える以外に、Java 7以前との下位互換性を確保するための一般的な解決策はありませんsplit


split("")異なるJavaバージョン間でコードが一貫するようにコードを変更する方法はありますか?
ダニエル

2
@Daniel:正規表現の末尾に追加(?!^)し、元の正規表現を(必要に応じて)非キャプチャグループにラップすることで、(Java 8の動作に従って)上位互換にすることは可能ですが、私は考えられません下位互換性を確保する方法(Java 7以前の古い動作に従う)。(?:...)
nhahtdh、2015年

説明ありがとう。説明できます"(?!^)"か?どのシナリオでそれとは異なり""ますか?(私は正規表現でひどいです!:-/)。
ダニエル、

1
@Daniel:その意味はPattern.MULTILINEフラグに影響され\Aますが、フラグに関係なく常に文字列の先頭で一致します。
nhahtdh 2015年

30

これはのドキュメントで指定されていますsplit(String regex, limit)

この文字列の先頭に正の幅の一致がある場合、結果の配列の先頭に空の先行部分文字列が含まれます。ただし、最初に幅が一致しないと、そのような空の先行部分文字列は生成されません。

では"abc".split("")、最初に幅がゼロに一致しているため、結果の配列に先頭の空の部分文字列は含まれていません。

ただし、2番目のスニペットで分割する"a"と、幅の一致が正(この場合は1)になるため、空の先行部分文字列が期待どおりに含まれます。

(無関係なソースコードを削除)


3
それはただの質問です。JDKからのコードのフラグメントを投稿しても大丈夫ですか?Google-Harry Potter-Oracleの著作権問題を覚えていますか?
Paul Vargas 14年

6
@PaulVargas公平を期すにはわかりませんが、JDKをダウンロードして、すべてのソースを含むsrcファイルを解凍できるので、問題はないと思います。したがって、技術的には誰でもソースを見ることができます。
Alexis C.

12
@PaulVargas「オープンソース」の「オープン」は何かを表しています。
Marko Topolnik 14年

2
@ZouZou:誰もがそれを見ることができるからといって、あなたがそれを再公開できるわけではありません
user102008

2
@Paul Vargas、IANALですが、他の多くの場合、このタイプの投稿は引用/フェアユースの状況に該当します。トピックの詳細はこちら:meta.stackexchange.com/questions/12527/…–
Alex

14

のドキュメントがsplit()Java 7からJava 8に若干変更されました。具体的には、次のステートメントが追加されました。

この文字列の先頭に正の幅の一致がある場合、結果の配列の先頭に空の先行部分文字列が含まれます。ただし、最初に幅が一致しないと、そのような空の先行部分文字列は生成されません。

(強調鉱山)

空の文字列の分割は、最初にゼロ幅の一致を生成するため、空の文字列は、上記の指定に従って、結果の配列の先頭に含まれません。対照的に、分割する2番目の例では、文字列の先頭に正の幅の一致が"a"生成されるため、結果の配列の先頭に空の文字列が実際に含まれます。


さらに数秒違いました。
Paul Vargas 14年

2
@PaulVargasは実際にここarshajiiがZouZouの数秒前に回答を投稿しましたが、残念ながらZouZouはここで先に私の質問に回答しました。私はすでに答えを知っていたので、この質問をするべきかどうか疑問に思いましたが、それは興味深いもののようで、ZouZouは彼の以前のコメントに対してある程度の評判に値しました。
Pshemo 2014年

5
新しい動作はより論理的に見えますが、明らかに下位互換性の問題です。この変更の唯一の正当化はそれ"some-string".split("")が非常にまれなケースです。
ivstas 2014年

4
.split("")何も一致せずに分割する唯一の方法ではありません。ポジティブルックアヘッド正規表現を使用しました。これはjdk7でも最初に一致し、空のヘッド要素を生成しましたが、現在は使用されていません。github.com/spray/spray/commit/...
jrudolph
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.