Java String.split()有时会给出空字符串

我正在制作一个基于文本的骰子滚筒。 它接受像“2d10 + 5”这样的字符串,并且作为滚动的结果返回一个字符串。 我的问题出现在tokenizer中,它将字符串拆分为有用的部分,以便我解析信息。

String[] tokens = message.split("(?=[dk\\+\\-])");

这产生了奇怪的,意想不到的结果。 我不知道究竟是什么导致了他们。 它可能是正则表达式,我的误解,或Java只是Java。 这是发生了什么:

  • 3d6+4产生字符串数组[3, d6, +4] 。 这是对的。
  • d%产生字符串数组[d%] 。 这是对的。
  • d20产生字符串数组[d20] 。 这是对的。
  • d%+3产生字符串数组[, d%, +3] 。 这是不正确的。
  • d20+2产生字符串数组[, d20, +2] 。 这是不正确的。

在第四个和第五个例子中,一些奇怪的东西导致额外的空字符串出现在数组的前面。 这并不是字符串前面缺少数字,因为其他例子反驳了这一点。 这不是百分号的存在,也不是加号。

现在我只是继续通过空白字符串的for循环,但这感觉就像一个创可贴解决方案。 有没有人知道是什么原因造成arrays前面的空白字符串? 我该如何解决?

通过源代码挖掘,我得到了这种行为背后的确切问题。

String.split()方法在内部使用Pattern.split() 。 返回结果数组之前的split方法检查最后一个匹配的索引,或者是否实际匹配。 如果最后匹配的索引为0 ,则表示您的模式仅匹配字符串开头的空字符串或根本不匹配,在这种情况下,返回的数组是包含相同元素的单个元素数组。

这是源代码:

 public String[] split(CharSequence input, int limit) { int index = 0; boolean matchLimited = limit > 0; ArrayList 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); // Consider this assignment. For a single empty string match // m.end() will be 0, and hence index will also be 0 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()}; // Rest of them is not required 

如果上面代码中的最后一个条件 - index == 0 ,为true,则返回单个元素数组和输入字符串。

现在,考虑index可以为0

  1. 什么时候根本没有比赛。 (正如上面那条评论所述)
  2. 如果在开头找到匹配,并且匹配字符串的长度为0 ,那么if块中的索引值(在while循环内) -

     index = m.end(); 

    将为0.唯一可能的匹配字符串是空字符串 (长度= 0)。 这就是这种情况。 而且不应该有任何进一步的匹配,否则index将更新为不同的索引。

所以,考虑你的情况:

  • 对于d% ,在第一个d之前,模式只有一个匹配。 因此索引值将为0 。 但由于没有任何进一步的匹配,索引值不会更新, if条件变为true ,并返回带有原始字符串的单个元素数组。

  • 对于d20+2 ,将有两个匹配,一个在d之前,一个在+之前。 因此索引值将被更新,因此将返回上述代码中的ArrayList ,其中包含空字符串作为分隔符的分割结果,该分隔符是字符串的第一个字符,如@Stema的答案中所述。

因此,要获得所需的行为(仅在不在开头时拆分分隔符,您可以在正则表达式模式中添加负面的后视):

 "(? 

这将拆分为空字符串,后跟您的字符类,但不会在字符串的开头之前。


考虑在正则表达式模式上分割字符串"ad%"情况 - "a(?=[dk+-])" 。 这将为您提供一个数组,其中第一个元素为空字符串。 这里唯一的变化是,空字符串被替换a

 "ad%".split("a(?=[dk+-])"); // Prints - `[, d%]` 

为什么? 那是因为匹配字符串的长度是1 。 所以第一次匹配后的索引值 - m.end()不会是0而是1 ,因此不会返回单个元素数组。

我很惊讶它不会发生在案例2和3中,所以真正的问题是

为什么“d20”和“d%”的开头没有空字符串?

正如Rohit Jain在他的详细分析中解释的那样,当在字符串的开头只找到一个匹配并且match.end索引为0时,会发生这种情况。(这只有在使用环绕声断言才能找到时才会发生。比赛)。

问题是, d%+3以你正在拆分的字符开头。 所以你的正则表达式在第一个字符之前匹配,你在开始时得到一个空字符串。

您可以添加一个lookbehind,以确保您的表达式在字符串的开头不匹配,以便它不会在那里拆分:

 String[] tokens = message.split("(? 

(?是一个lookbehind断言,当它不在字符串的开头时是真的。

我建议简单匹配而不是拆分:

 Matcher matcher = Pattern.compile("([1-9]*)(d[0-9%]+)([+-][0-9]+)?").matcher(string); if(matcher.matches()) { String first = matcher.group(1); // etc } 

不保证正则表达式,但我认为它会…