Java 字符串与流之间的转换

Java 字符串与流之间的转换,用户希望通过惯用的流处理技术(而不是对 String 中的各个字符进行循环)实现字符串与流之间的转换。使用 java.lang.CharSequence 接口定义的默认方法 charscodePoints,将 String 转换为 IntStream。为了将 IntStream 转换回 String,使用 java.util.stream.IntStream 接口定义的 collect 方法的重载形式。

Java 字符串与流之间的转换 问题描述

用户希望通过惯用的流处理技术(而不是对 String 中的各个字符进行循环)实现字符串与流之间的转换。

Java 字符串与流之间的转换 解决方案

使用 java.lang.CharSequence 接口定义的默认方法 charscodePoints,将 String 转换为 IntStream。为了将 IntStream 转换回 String,使用 java.util.stream.IntStream 接口定义的 collect 方法的重载形式。它传入三个参数,分别是 Supplier、表示累加器的 BiConsumer 以及表示组合器的 BiConsumer

Java 字符串与流之间的转换 具体实例

字符串是若干字符的集合。理论上说,将字符串转换为流并不困难,如同将字符串转换为集合或数组一样。遗憾的是,String 不属于集合框架(collections framework),因此无法实现 Iterable,不存在一种能将 String 转换为 Streamstream 工厂方法。另一种方案是采用 java.util.Array 类定义的各种静态 stream 方法。然而,尽管 Arrays.stream 提供了用于处理 int[]long[]double[] 甚至 T[] 的方法,却并未定义用于处理 char[] 的方法。API 的设计者似乎不希望用户采用流技术处理字符串。
尽管如此,仍然有办法实现字符串与流之间的转换。String 类实现 CharSequence 接口,它引入了两种能生成 IntStream 的方法(charscodePoints),它们都是接口中的默认方法,因此存在可用的实现。例 3-35 展示了两种方法的签名。

例 3-35 CharSequence 接口定义的 charscodePoints 方法

default IntStream chars()
default IntStream codePoints()

charscodePoints 方法的不同之处在于,chars 方法用于处理 UTF-16 编码字符,而 codePoints 方法用于处理完整的 Unicode 代码点(code point)集。如果读者对两种方法之间的差异感兴趣,可以阅读 Javadoc 中有关 java.lang.Character 类的描述。就本范例而言,区别只在于返回的整数类型:chars 方法返回一个由序列中的 char 值构成的 IntStream,而 codePoints 方法返回一个由 Unicode 代码点构成的 IntStream
那么,如何将字符流转换回字符串呢? Stream.collect 方法对流元素执行可变归约(mutable reduction)操作以生成集合。Collectors 工具类提供了大量可以生成所需 Collector 的静态方法(如本书讨论的 toListtoSettoMapjoining 以及其他许多方法),因此传入 Collectorcollect 方法在开发中最为常用。
然而,明显看出缺少的是 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 倡导的函数式习惯用法,但希望本范例讨论的机制能对读者有所启发。

赞(1)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

Java 实例