如何让java.util.Scanner将标点分隔符作为令牌返回以解析算术表达式
这个需求我之前也碰到过,Scanner的分隔符逻辑有时候确实容易绕晕,咱们一步步理清楚问题,再给出可行的解决方案:
先分析你之前两种尝试的问题
第一种方法(吞掉标点):
useDelimiter("\\p{Space}+|\\p{Punct}")
这里你把标点符号也设成了分隔符,Scanner会直接丢弃所有分隔符,所以标点自然就消失了;另外当标点和其他字符紧密相连时(比如(ab里的(),分隔符会在(和ab之间拆分,但因为(本身是分隔符,拆分后就会产生空字符串,这就是结果里出现""的原因。第二种方法(部分令牌粘连):
useDelimiter("\\p{Space}+|(?=\\p{Punct})")
你用了正向预查(?=\\p{Punct}),这个规则只会在标点符号前面的位置触发拆分。比如ab和-之间会拆分,但-和bc不会——因为-后面没有预查规则,所以-会和bc粘在一起;同理(和ab也会粘在一起,因为(前面没有触发拆分的位置。
两种可行的解决方案
方案1:调整分隔符,结合正向+反向预查
我们需要让Scanner在标点符号的前后都触发拆分,这样既能保留标点作为独立令牌,又能丢弃空格。可以用以下正则作为分隔符:
List<String> tokens = new Scanner("12 + (ab-bc*3)") .useDelimiter("\\p{Space}+|(?<=\\p{Punct})|(?=\\p{Punct})") .tokens() .filter(token -> !token.isEmpty()) // 过滤分隔符相邻产生的空字符串 .collect(Collectors.toList());
正则规则解释:
\\p{Space}+:匹配任意数量的空格,作为分隔符直接丢弃(?<=\\p{Punct}):反向预查——匹配一个位置,这个位置的前一个字符是标点,这样标点后面的位置会被当作拆分点,标点就会被单独拆成一个令牌(?=\\p{Punct}):正向预查——匹配一个位置,这个位置的后一个字符是标点,这样标点前面的内容会被拆成一个令牌
运行后得到的结果完全符合你的预期:["12", "+", "(", "ab", "-", "bc", "*", "3", ")"]
方案2:直接用findAll匹配目标令牌(更推荐)
其实Scanner的findAll方法可以直接定义你想要的令牌类型,比调整分隔符逻辑更直观。我们只需要匹配两种令牌:
- 单个标点符号:
\\p{Punct} - 连续的非标点、非空格字符:
[^\\p{Punct}\\p{Space}]+
代码如下:
List<String> tokens = new Scanner("12 + (ab-bc*3)") .findAll("\\p{Punct}|[^\\p{Punct}\\p{Space}]+") .map(MatchResult::group) .collect(Collectors.toList());
这种方法不需要处理空字符串,因为findAll只会返回符合规则的匹配项,结果和方案1完全一致,逻辑也更清晰——直接告诉程序“我要找的就是单个标点,或者一串非标点非空格的字符”。
内容的提问来源于stack exchange,提问作者Danilo Piazzalunga

