Java9正则表达式新特性

来源:互联网 发布:淘宝买的酒是真的吗 编辑:程序博客网 时间:2024/06/06 02:26

最近我收到由Packt出版,Anubhava Srivastava编写的书《Java 9 Regular Expressions》。对于想从头开始学习正则表达式的人来说,这本书是个好教程。而对于已经熟悉正则表达式的读者来说,翻看此书,可以让你深入的了解更复杂些的功能,譬如零长度断言、反向引用等等。

在本篇文章中,我将只会关注Java9中才具有的正则表达式特性。放心,不会有很多的;)。

Java9正则表达式模块

众所周知,Java9引入了模块系统。也许有人会期待将会有相关的正则表达式模块。但是然并卵。Java9有个默认的模块java.base,其他所有的模块都会默认依赖于此模块,因此在Java应用中默认模块下的包及其里面的类都是可用的,而java.util.regex身处在此模块中。这使得开发及兼容工作简单了些,否则你得在代码中require相对应的模块了。从这里可以看出,Java开发者对正则表达式还是很重视的。

正则表达式相关类

java.util.regex中包含了以下几个类:

  • MatchResult
  • Matcher
  • Pattern
  • PatternSyntaxException

以上四个类中,唯一改变了API的是Matcher类。

Matcher类的改变

Matcher类新增了5个新方法,其中有4个是重载的。分别是:

  • appendReplacement
  • appendTail
  • replaceAll
  • replaceFirst
  • results

头4个方法在之前版本中有存在,Java9中仅仅是修改了参数类型进行重载。

appendReplacement/Tail

Java API文档描述:

Modifier and Type Method Description Matcher appendReplacement(StringBuffer sb, String replacement) Implements a non-terminal append-and-replace step Matcher appendReplacement(StringBuilder sb, String replacement) Implements a non-terminal append-and-replace step StringBuffer appendTail(StringBuffer sb) Implements a terminal append-and-replace step StringBuffer appendTail(StringBuilder sb) Implements a terminal append-and-replace step

appendReplacementappendTail的各两个方法唯一区别是参数既可以为StringBuilder,也可以为StringBufferStringBuilder早在Java 1.5(13年前)已被引入,所以这次修改没有什么特别重大意义。

然而有趣的是当前版本API文档针对appendReplacement方法中StringBuilder类型的描述。参数为StringBuffer的方法很明显地指出包含命名引用的字符将会被相应的替换掉,参数为StringBuilder的方法文档中则没有。文档看起来应该是复制、粘贴,然后再将"buffer"换为"builder",命名引用去掉进行编辑得到的。

译者注:此处JDK文档在翻译时好像已经有更改了。

基于Java9 build160版本,我编写用例测试了下,不出意料结果是相同的。毕竟两个方法的实现几乎相同,只是方法参数的类型不同而已。

你可以使用注释掉的两行或者未注释掉的。但是文档中仅仅只说了数字引用。

@Testpublic void testAppendReplacement() {    Pattern p = Pattern.compile("cat(?<plural>z?s?)");    //Pattern p = Pattern.compile("cat(z?s?)");    Matcher m = p.matcher("one catz two cats in the yard");    StringBuilder sb = new StringBuilder();    while (m.find()) {        m.appendReplacement(sb, "dog${plural}");        //m.appendReplacement(sb, "dog$001");    }    m.appendTail(sb);    String result = sb.toString();    assertEquals("one dogz two dogs in the yard", result);}

replaceAll/First

Java API文档描述:

Modifier and Type Method Description String replaceAll(String replacement) Replaces every subsequence of the input sequence that matches the pattern with the given replacement string. String replaceAll(Function replacer) Replaces every subsequence of the input sequence that matches the pattern with the result of applying the given replacer function to the match result of this matcher corresponding to that subsequence. String replaceFirst(String replacement) Replaces the first subsequence of the input sequence that matches the pattern with the given replacement string. String replaceFirst(Function replacer) Replaces the first subsequence of the input sequence that matches the pattern with the result of applying the given replacer function to the match result of this matcher corresponding to that subsequence.

两方法的作用是将匹配组替换为新的字符串。两方法新旧两版本的唯一区别是替换字符串是如何提供的。老版本的方法中,方法参数中的字符串是String类型;而新版本的方法里,方法参数是由Function<MatchResult, String>提供,每次匹配的结果都将调用此方法并进行字符串替换。

Function在3年前的Java 8版本中引入,但是其在正则表达式中的使用有点草率。或者,我们可以将其看作10年后的提示吧,可当类Function已有13年,我们还会在使用Java9吗?

为了更深入的了解以上两个方法,我编写了一些简单的示例来说明。

示例一:

@Testpublic void demoReplaceAllFunction() {    Pattern pattern = Pattern.compile("dog");    Matcher matcher = pattern.matcher("zzzdogzzzdogzzz");    String result = matcher.replaceAll(mr -> mr.group().toUpperCase());    assertEquals("zzzDOGzzzDOGzzz", result);}

示例一很简单,lambda表达式的使用使得程序看起来特别简洁。对于文档来说示例一已足够说明方法的使用了,不然会影响读者对于方法的理解。

接下来我们看下稍复杂些的示例。示例二将字符串中的#替换为数字1,2,3等等。这些字符串包含编号的项目,如果我们在字符串中插入新字符串,我们不想手动重新编号。另外有时会匹配到两个##,这时我们会跳过第二个#。直接看示例:

@Testpublic void countSampleReplaceAllFunction() {    AtomicInteger counter = new AtomicInteger(0);    Pattern pattern = Pattern.compile("#+");    Matcher matcher = pattern.matcher("# first item\n" +            "# second item\n" +            "## third and fourth\n" +            "## item 5 and 6\n" +            "# item 7");    String result = matcher.replaceAll(mr -> "" + counter.addAndGet(mr.group().length()));    assertEquals("1 first item\n" +            "2 second item\n" +            "4 third and fourth\n" +            "6 item 5 and 6\n" +            "7 item 7", result);}

replaceAll方法中的lambda表达式可以获取计数并计算下一个值。如果字符串有一个#,则加1,如果有两则加2。因为lambda表达式不能改变周围环境中变量的值(变量必须是final),所以计数器不能是int或Integer变量。我们需要一个对象来存储int值并修改,AtomicInteger是个很好的选择。

接下来的示例三会涉及到数学运算,主要是将字符串中的浮点数替换为sine的值。如:

@Testpublic void calculateSampleReplaceAllFunction() {    Pattern pattern = Pattern.compile("\\d+(?:\\.\\d+)?(?:[Ee][+-]?\\d{1,2})?");    Matcher matcher = pattern.matcher("The sin(pi) is 3.1415926");    String result = matcher.replaceAll(mr -> "" + (Math.sin(Double.parseDouble(mr.group()))));    assertEquals("The sin(pi) is 5.3589793170057245E-8", result);}

我们还将用这个计算方法来演示最后一个方法的演示,这是Matcher类中全新的方法。

Stream results()

results()方法返回匹配结果的流,更确切的说返回的是MatchResult对象的Stream。接下来的示例我们采用results方法来搜集字符串中的浮点数,并输出他们的sine值,采用逗号隔开。

@Testpublic void resultsTest() {    Pattern pattern = Pattern.compile("\\d+(?:\\.\\d+)?(?:[Ee][+-]?\\d{1,2})?");    Matcher matcher = pattern.matcher("Pi is around 3.1415926 and not 3.2 even in Indiana");    String result = String.join(",",            matcher                    .results()                    .map(mr -> "" + (Math.sin(Double.parseDouble(mr.group()))))                    .collect(Collectors.toList()));    assertEquals("5.3589793170057245E-8,-0.058374143427580086", result);}

总结

在Java 9 JDK中引入的新的正则表达式方法与已经可用的没有本质上的区别。它们整齐、方便,在某些情况下,可以减轻编程。没有什么内容在早起版本是没有介绍的。这只是Java对JDK进行缓慢而深思熟虑的改变的方式。 毕竟这就是为什么我们喜欢Java,不是吗?

文章中的代码可以从此处下载。

原创粉丝点击