Java 字符串与流之间的转换,用户希望通过惯用的流处理技术(而不是对 String
中的各个字符进行循环)实现字符串与流之间的转换。使用 java.lang.CharSequence
接口定义的默认方法 chars
和 codePoints
,将 String
转换为 IntStream
。为了将 IntStream
转换回 String
,使用 java.util.stream.IntStream
接口定义的 collect
方法的重载形式。
Java 字符串与流之间的转换 问题描述
用户希望通过惯用的流处理技术(而不是对 String
中的各个字符进行循环)实现字符串与流之间的转换。
Java 字符串与流之间的转换 解决方案
使用 java.lang.CharSequence
接口定义的默认方法 chars
和 codePoints
,将 String
转换为 IntStream
。为了将 IntStream
转换回 String
,使用 java.util.stream.IntStream
接口定义的 collect
方法的重载形式。它传入三个参数,分别是 Supplier
、表示累加器的 BiConsumer
以及表示组合器的 BiConsumer
。
Java 字符串与流之间的转换 具体实例
字符串是若干字符的集合。理论上说,将字符串转换为流并不困难,如同将字符串转换为集合或数组一样。遗憾的是,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 倡导的函数式习惯用法,但希望本范例讨论的机制能对读者有所启发。