在Java 8之前,当我们分割空字符串(例如

String[] tokens = "abc".split("");


)时,分割机制将在标记有|的位置分割br />因为每个字符前后都有空白""。因此,结果将首先生成此数组

|a|b|c|


,随后将删除结尾的空字符串(因为我们没有为limit参数明确提供负值),因此最终将返回

["", "a", "b", "c", ""]



在Java 8中,拆分机制似乎已更改。现在,当我们使用

["", "a", "b", "c"]


时,我们将获得["a", "b", "c"]数组而不是["", "a", "b", "c"],因此看起来开始时的空字符串也被删除了。但是该理论失败了,因为例如

"abc".split("")


返回数组["", "bc"]处带有空字符串。

有人可以解释这里发生的事情以及Java 8中拆分规则的变化吗?

评论

Java8似乎可以解决该问题。同时,s.split(“(?!^)”)似乎可以工作。

@shkschneider我的问题中描述的行为不是Java-8之前版本的错误。这种行为不是特别有用,但是仍然是正确的(如我的问题所示),因此我们不能说它是“固定的”。我认为它更像是一种改进,因此我们可以使用split(“”)而不是神秘的(对于不使用正则表达式的人来说)split(“(?!^)”)或split(“(?<!^)”)或其他少数正则表达式。

将fedora升级到Fedora 21后,遇到同样的问题,fedora 21随JDK 1.8一起提供,因此我的IRC游戏应用程序已损坏。

这个问题似乎是Java 8中这一重大更改的唯一文档。Oracle将其排除在不兼容列表之外。

JDK中的这一更改仅花费了我2个小时来找出问题所在。该代码在我的计算机(JDK8)上运行良好,但在另一台计算机(JDK7)上却神秘地失败了。 Oracle确实应该更新String.split(String regex)的文档,而不是Pattern.split或String.split(String regex,int limit),因为这是迄今为止最常见的用法。 Java因其可移植性(又称为WORA)而闻名。这是一个重大的重大突破,根本没有得到很好的记录。

#1 楼

String.split(称为Pattern.split)的行为在Java 7和Java 8之间发生了变化。

文档

比较Java 7和Java 8中的Pattern.split的文档,我们观察到添加以下子句:


当输入序列的开头存在正宽度匹配时,则在结果数组的开头包含一个空的前导子字符串。但是开头的零宽度匹配永远不会产生这样的空前导子字符串。


与Java 7相比,Java 8中的String.split也添加了相同的子句。

参考实现

让我们比较一下Java 7和Java 8中的参考实现的Pattern.split的代码。该代码是从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)并将原始正则表达式换成非capt小组(?:...)(如有必要)。
如果您的正则表达式不能匹配零长度的字符串,则无需执行任何操作。
如果您不知道正则表达式是否可以匹配零-长度字符串是否正确,请同时执行步骤1中的所有操作。字符串。

Java 7及更低版本中的以下行为

没有通用的解决方案可以使(?!\A)与Java 7及更低版本向后兼容,而不能替换split的所有实例以指向您自己的自定义实现。

评论


知道如何更改split(“”)代码,使其在不同的Java版本之间保持一致吗?

–丹尼尔(Daniel)
2015年10月29日在22:22

@Daniel:通过在正则表达式的末尾添加(?!^)并将原始正则表达式包装在非捕获组(?:...)中,可以使其向前兼容(遵循Java 8的行为)( (如有必要),但我想不出任何办法使其向后兼容(遵循Java 7和以前的旧行为)。

– nhahtdh
15-10-30在3:50



感谢您的解释。您可以描述“(?!^)”吗?在什么情况下它将与“”不同? (我在regex上很糟糕!:-/)。

–丹尼尔(Daniel)
2015年10月31日,下午3:44

@Daniel:它的含义受Pattern.MULTILINE标志影响,而\ A始终匹配字符串的开头而不考虑标志。

– nhahtdh
2015年11月2日,下午2:38

#2 楼

这已在split(String regex, limit)的文档中指定。


如果此字符串的开头存在正宽度匹配
,则开头将包含一个空的前导子字符串
结果数组的值。开头为零宽度的匹配项,但是永远不会
产生这样的空前导子串。不包含在结果数组中。

然而,当在"abc".split("")上进行拆分时,在第二个片段中,您获得了正宽度匹配(在这种情况下为1),因此按预期包括了空的前导子字符串。删除了无关的源代码)

评论


这只是一个问题。可以从JDK中发布一段代码吗?还记得Google-Harry Potter-Oracle的版权问题吗?

– Paul Vargas
2014年3月28日在17:24



@PaulVargas坦白地说,我不知道,但我认为可以,因为您可以下载JDK,并解压缩包含所有源代码的src文件。因此,从技术上讲,每个人都可以看到源。

– Alexis C.
2014年3月28日在17:28



@PaulVargas“开源”中的“开放”确实代表着某种东西。

– Marko Topolnik
2014年3月28日在17:49

@ZouZou:仅仅因为每个人都可以看到它并不意味着您可以重新发布它

–user102008
2014年5月14日20:20

@Paul Vargas,IANAL,但在许多其他情况下,此类帖子属于引用/合理使用情况。有关此主题的更多信息,请访问:meta.stackexchange.com/questions/12527/…

– Alex Pakka
2014年5月16日下午5:16

#3 楼

从Java 7到Java 8,split()的文档进行了细微更改,具体来说,添加了以下语句:


当此开头有一个正宽匹配时字符串,然后在结果数组的开头包含一个空的前导子字符串。但是开头的零宽度匹配永远不会产生这样的空前导子字符串。


(强调我的)

空字符串拆分会生成零宽度匹配开头,因此根据上面指定的内容,结果数组的开头将不包含空字符串。相比之下,在"a"上分割的第二个示例在字符串的开头生成一个正宽度匹配,因此实际上在结果数组的开头包含一个空字符串。

评论


再过几秒钟就变得与众不同。

– Paul Vargas
2014年3月28日在17:11

@PaulVargas实际上在这里arshajii在走走几秒钟之前就发布了答案,但不幸的是走走在这里早些时候回答了我的问题。我想知道是否应该问这个问题,因为我已经知道了一个答案,但是这似乎很有趣,邹邹的早期评论也应得到一定的声誉。

– Pshemo
2014年3月28日在17:13

尽管新行为看起来更合乎逻辑,但是显然这是向后兼容性中断。进行此更改的唯一理由是“ some-string” .split(“”)是非常罕见的情况。

–ivstas
14-10-29在6:45



.split(“”)不是不匹配任何内容的唯一分割方法。我们在jdk7中使用了正向前瞻正则表达式,该正则表达式也在开始时就匹配了,并产生了一个空的head元素,该元素现在消失了。 github.com/spray/spray/commit/…

– jrudolph
15年2月10日在10:59