文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏
参与翻译: pauli (12)

译者注:截止文章发布,Java 9 还没有正式发布。

我收到 Anubhava Srivastava 赠送的书 “Java 9 Regular Expressions”,由 Packt 出版。这本书是很好的教程,从零开始介绍了什么是正则表达式。对于那些想知道怎么使用 RegEx 正则表达式的人,这本书也很有趣,讲解了基础知识,深入了解了复杂的特性,就像零长度断言、反向引用,类似选择符。

本文中,我会侧重一些 Java 9 特有的以前 JDK 版本没有的正则表达式特性。

第 1 段(可获 1.29 积分)

Java 9 正则表达式模块

JDK 在 Java 9 中被分成多个模块。有人觉得应该为正则表达式出一个新的模块来处理包和类。实际上并没有。java.base 模块是默认的,其他模块都默认依赖,它暴露出的包中的类在 Java 应用中永远可用。正则表达式包 java.util.regex 被这个模块暴露出来。开发变得简单:当我们在代码中使用正则表达式的时候,不需要明确“引入”一个模块。看得出正则表达式对于 Java 非常重要,从而包含在基础模块中。

第 2 段(可获 1.48 积分)

正则表达式类 Classes

包 java.util.regex 包含这些 Java 类:

  • MatchResult
  • Matcher
  • Pattern
  • PatternSyntaxException

唯一一个有 API 改动的就是 Matcher。

Matcher 类的改变

类 Matcher 添加了五个新方法,其中四个是旧方法的重载。

  • appendReplacement
  • appendTail
  • replaceAll
  • replaceFirst
  • results

前四个是之前版本存在的,只是参数类型的改变(毕竟这是重载)。

appendReplacement/Tail

针对 appendReplacement and appendTail,唯一的区别就是,参数可以是 StringBuilder,不只是 StringBuffer。考虑到 StringBuilder 是 Java 1.5 引入的,就像十三年以前的东西,没有人会说这是欠考虑的。(译者注:2004年9月30日,J2SE1.5发布,为了表示该版本的重要性,更名为Java SE 5.0,到现在2017年刚好十三年

第 3 段(可获 1.33 积分)

非常有趣的是,针对 StringBuilder 参数,JDK API 的当前在线版本 写到了 appendReplacement 行为。旧的 StringBuffer 参数方法明确的写到,被替换的字符串可以包含指定的引用,被相应的元组替换StringBuilder 参数的版本没有这些功能。文档看起来就像复制/粘贴,然后编辑过的。文字替换 “buffer” 到 “builder”,相似的,文档中指定引用参考特性被删除了。

我尝试了 Java 9 build 160 版本中的特性,这不应该是惊喜,既然两种方法的源码是一样的,除了参数类型不一样,都是相同的复制粘贴。

第 4 段(可获 1.59 积分)

你可以这样用:

@Test
public 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);
}

包括注释掉的代码行或它上面的一行代码。尽管如此,文档中说了只是编号的引用。

第 5 段(可获 0.29 积分)

replaceAll/First

这也是“老”方法,把匹配元组替换成新字符串。新旧方法的唯一区别就是,替换的字符串如何提供。旧版中,字符串由 String 提供,方法调用前已经被计算出来了。新版中,字符串由 Function<MatchResult,String>提供。每次匹配后,这个函数被调用,同时计算了替换字符串。

三年前 Java 8 时引入 Function 类,正则表达式中开始使用它有些草率。或者说,也许,我们可以看出这是一种暗示,从现在开始十年,当 Fuction 类 13 岁的时候,我们仍然会有 Java 9 吧?

第 6 段(可获 1.66 积分)

让我们深挖一下这两个方法。(实际上主要看 replaceAll 方法,因为 replaceFirst 方法有相同的行为,它只是替换第一次匹配的元组。)我来尝试写一些不那么复杂的例子。

第一个例子来自 JDK 文档:

@Test
public void demoReplaceAllFunction() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("zzzdogzzzdogzzz");
    String result = matcher.replaceAll(mr -> mr.group().toUpperCase());
    assertEquals("zzzDOGzzzDOGzzz", result);
}
第 7 段(可获 0.6 积分)

这个例子不复杂,但是说明了表达式的特性。使用 Lambda 表达式足够了。我想不到有更简单的方法,把常量字符串 “dog” 转成大写,也许只有直接写 “DOG” 字符串。我只是开个玩笑。但是实际上,这个例子也很简单。对于文档中的例子非常合适,也许更复杂的例子会分散读者的注意力,而不是文档中这个方法的特性。实际上,不要期望 Javadoc 中复杂的例子少一点。它只是描述了如何使用 API,而不是 API 创建的原因,或者设计方法。

第 8 段(可获 1.2 积分)

但是现在在这儿,我们将开始一些较复杂的例子。我们想替换字符串中的 # 号为数字 1,2,3 等。这个字符串包含数字项,如果我们插入一个新的到字符串中,我们不想再次手动更新。有时我们把两项放在一组,这时我写 ##,然后我们想跳过一个序列字符到下一个。既然我们有单元测试,多说无意,代码更能描述这些功能

@Test
public 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);
}
第 9 段(可获 1.19 积分)

 

传递到 replaceAll 中的 Lambda 表达式,得到计数器并计算下一个值。如果我们使用一次 #,它就会加1。我们使用两次,它就会在计数器中增加2,以此类推。因为 Lambda  表达式不能够改变周围环境中变量的值,变量不得不声明为常量(译者注,Java 中使用 final 来声明常量。匿名类或 Lambda 表达式使用外面的变量时,要求她们声明为常量),计数器不能是整数或者整型变量。我们需要一个对象来存储可变的值。AtomicInteger 正是这样的,即使我们没有使用它的原子特性(译者注:Java 5 之后,专门提供了用来进行单变量多线程并发安全访问的工具包 java.util.concurrent.atomic,原子量虽然可以保证单个变量在某一个操作过程的安全,但无法保证你整个代码块,或者整个程序的安全性。因此,通常还应该使用锁等同步机制来控制整个程序的安全性)。

下一个例子会更深入,做一些数学计算。它将字符串中的浮点数字替换为正弦值。那种方法,改变了我们的句了,既然 sin(pi)  和 pi 相差很远,这里不能精确地表达。这相当接近零:

第 10 段(可获 1.84 积分)
@Test
public 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 对象的数据流。在下面的例子中,我们用它来收集任何浮点数字的格式化的字符串,打印他们的正弦值,逗号分隔:

第 11 段(可获 0.93 积分)
@Test
public 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,不是吗?

全部代码,从我的 IDE 复制/粘贴,可以从下面 gist 找到和下载。

第 12 段(可获 1.18 积分)

文章评论