3.6 字符串与流之间的转换
问题
用户希望通过惯用的流处理技术(而不是对 String 中的各个字符进行循环)实现字符串与流之间的转换。
方案
使用 java.lang.CharSequence 接口定义的默认方法 chars 和 codePoints,将 String 转换为 IntStream。为了将 IntStream 转换回 String,使用 java.util.stream.IntStream 接口定义的 collect 方法的重载形式。它传入三个参数,分别是 Supplier、表示累加器的 BiConsumer 以及表示组合器的 BiConsumer。
讨论
字符串是若干字符的集合。理论上说,将字符串转换为流并不困难,如同将字符串转换为集合或数组一样。遗憾的是,String 不属于集合框架(collections framework),因此无法实现 Iterable,不存在一种能将 String 转换为 Stream 的 stream 工厂方法。另一种方案是采用 java.util.Array 类定义的各种静态 stream 方法。然而,尽管 Arrays.stream 提供了用于处理 int[]、long[]、double[] 甚至 T[] 的方法,却并未定义用于处理 char[] 的方法。API 的设计者似乎不希望用户采用流技术处理字符串。
尽管如此,仍然有办法实现字符串与流之间的转换。String 类实现 CharSequence 接口,它引入了两种能生成 IntStream 的方法(chars 和 codePoints),它们都是接口中的默认方法,因此存在可用的实现。例 3-35 展示了两种方法的签名。
例 3-35
CharSequence接口定义的chars和codePoints方法
- default IntStream chars()
- default IntStream codePoints()
chars 和 codePoints 方法的不同之处在于,chars 方法用于处理 UTF-16 编码字符,而 codePoints 方法用于处理完整的 Unicode 代码点(code point)集。如果读者对两种方法之间的差异感兴趣,可以阅读 Javadoc 中有关 java.lang.Character 类的描述。就本范例而言,区别只在于返回的整数类型:chars 方法返回一个由序列中的 char 值构成的 IntStream,而 codePoints 方法返回一个由 Unicode 代码点构成的 IntStream。
那么,如何将字符流转换回字符串呢? Stream.collect 方法对流元素执行可变归约(mutable reduction)操作以生成集合。Collectors 工具类提供了大量可以生成所需 Collector 的静态方法(如本书讨论的 toList、toSet、toMap、joining 以及其他许多方法),因此传入 Collector 的 collect 方法在开发中最为常用。
然而,明显看出缺少的是 Collector 传入一个字符流并将其组装为字符串。好在代码并不复杂,可以使用 collect 的另一种重载形式,它传入一个 Supplier 以及两个分别作为累加器和组合器的 BiConsumer 参数。
听起来似乎比实际情况要复杂得多。接下来,我们编写 isPalindrome 方法,以检查某个字符串是否属于回文(palindrome)。回文检查器不区分大小写,它首先删除结果字符串中存在的标点符号,再检查字符串是否正读和反读都一样。用于测试字符串的 isPalindrome 方法如例 3-36 所示,这是 Java 7 及之前版本的实现。
例 3-36 检查字符串是否属于回文(Java 7 及之前)
- public boolean isPalindrome(String s) {
- StringBuilder sb = new StringBuilder();
- for (char c : s.toCharArray()) {
- if (Character.isLetterOrDigit(c)) {
- sb.append(c);
- }
- }
- String forward = sb.toString().toLowerCase();
- String backward = sb.reverse().toString().toLowerCase();
- return forward.equals(backward);
- }
以上代码具有典型的非函数式编程风格。isPalindrome 方法首先声明一个具有可变状态的单独对象(StringBuilder 实例),然后对集合进行迭代(由 String 类定义的 toCharArray 方法返回的 char[]),并利用 if 条件语句决定是否将值附加到缓冲区。StringBuilder 类还定义了一个能更容易实现回文检查的 reverse 方法,String 类则没有类似的方法。这种可变状态、迭代、决策语句的组合迫切需要一种基于流的替代方案,如例 3-37 所示,这是 Java 8 的实现。
例 3-37 检查字符串是否属于回文(Java 8)
- public boolean isPalindrome(String s) {
- String forward = s.toLowerCase().codePoints() ➊
- .filter(Character::isLetterOrDigit)
- .collect(StringBuilder::new,
- StringBuilder::appendCodePoint,
- StringBuilder::append)
- .toString();
- String backward = new StringBuilder(forward).reverse().toString();
- return forward.equals(backward);
- }
➊ 返回 IntStream
在本例中,codePoints 方法返回 IntStream,之后可以使用与例 3-37 相同的条件进行筛选。有意思的是 collect 方法,其签名为:
- <R> R collect(Supplier<R> supplier,
- BiConsumer<R,? super T> accumulator,
- BiConsumer<R,R> combiner)
这三个参数的用途如下。
Supplier生成经过归约的对象(本例为StringBuilder)。- 第一个
BiConsumer将流的各个元素累加至所生成的数据结构,本例使用appendCodePoint方法。 - 第二个
BiConsumer表示组合器,它是一个“无干扰的无状态函数”(non-interfering, stateless function),用于将两个必须与累加器兼容的值组合在一起(本例为append方法)。注意,组合器仅在并行操作时使用。
collect 方法的参数略多,不过其优点在于代码不必区分字符和整数,而这是处理字符串元素时经常遇到的问题。
例 3-38 显示了针对回文检查器的简单测试。
例 3-38 测试回文检查器
- private PalindromeEvaluator demo = new PalindromeEvaluator();
- @Test
- public void isPalindrome() throws Exception {
- assertTrue(
- Stream.of("Madam, in Eden, I'm Adam",
- "Go hang a salami; I'm a lasagna hog",
- "Flee to me, remote elf!",
- "A Santa pets rats as Pat taps a star step at NASA")
- .allMatch(demo::isPalindrome));
- assertFalse(demo.isPalindrome("This is NOT a palindrome"));
- }
将字符串视为一种字符数组不太符合 Java 8 倡导的函数式习惯用法,但希望本范例讨论的机制能对读者有所启发。
另见
有关收集器的讨论请参见第 4 章,有关实现自定义收集器的讨论请参见范例 4.9,有关 allMatch 方法的讨论请参见范例 3.10。
