一尘不染

为什么在Java 8中String.chars()是一个整数流?

java

在Java 8中,有一个新方法String.chars()可返回代表字符代码的ints(IntStream)流。我想很多人会期待char这里有s 流。这样设计API的动机是什么?


阅读 1576

收藏
2020-03-18

共2个答案

一尘不染

正如其他人已经提到的那样,其背后的设计决策是防止方法和类的爆炸式增长。

尽管如此,我个人还是认为这是一个非常糟糕的决定,并且鉴于他们不想做出CharStream合理的替代方法chars(),我应该考虑:

  • Stream<Character> chars(),它会提供一系列字符,这会降低性能。
  • IntStream unboxedChars(),该代码将用于性能代码。
    但是,我认为,这个答案应该集中在展示一种使用Java 8中获得的API来完成此操作的方法,而不是着眼于目前为什么这样做的原因。

在Java 7中,我应该这样做:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

而且我认为在Java 8中执行此操作的合理方法如下:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

在这里,我获得一个,IntStream并通过lambda i -> (char)i将其映射到一个对象,这会将其自动装箱到Stream<Character>,然后我们可以做我们想做的事情,并且仍然使用方法引用作为加号。

请注意,尽管您必须这样做mapToObj,但是如果忘记并使用它map,那么什么也不会抱怨,但是最终还是会出现一个IntStream,并且您可能会怀疑为什么它会打印整数值而不是表示字符的字符串。

Java 8的其他替代品:

通过保留在IntStream并希望最终打印它们,您将无法再使用方法引用进行打印:

hello.chars()
        .forEach(i -> System.out.println((char)i));

而且,使用对您自己方法的方法引用不再起作用!考虑以下:

private void print(char c) {
    System.out.println(c);
}

接着

hello.chars()
        .forEach(this::print);

这可能会导致编译错误,因为可能会导致有损转换。

结论:

API的这种设计方式是因为不想添加CharStream,我个人认为该方法应返回Stream<Character>,并且当前的解决方法是在mapToObj(i -> (char)i)上使用IntStream以便能够与之正常工作。

2020-03-18
一尘不染

92

Skiwi的答案已经涵盖了许多要点。我会再填一些背景。

任何API的设计都是一系列折衷。在Java中,困难的问题之一是处理很久以前做出的设计决策。

从1.0开始,基元就已经在Java中使用。它们使Java成为“不纯的”面向对象的语言,因为基元不是对象。我相信添加原语是一个务实的决定,它以牺牲面向对象的纯度为代价来提高性能。

这是将近20年后的今天,我们仍然需要权衡的问题。Java 5中添加的自动装箱功能主要消除了使用装箱和拆箱方法调用使源代码混乱的问题,但开销仍然存在。在许多情况下,它并不明显。但是,如果要在一个内部循环中执行装箱或拆箱,您会发现它会带来大量的CPU和垃圾回收开销。

在设计Streams API时,很明显,我们必须支持原语。装箱/拆箱的开销会扼杀并行性带来的任何性能优势。但是,我们并不想支持所有的原语,因为那样会给API增加很多麻烦。(您真的看到了“ a”的用法ShortStream吗?)“全部”或“无”是设计的理想位置,但都不可接受。因此,我们必须找到“ some”的合理值。我们结束了与原始的专长int,long和double。(就我个人而言,我会被排除在外,int但那只是我。)

因为CharSequence.chars()我们考虑过退货Stream(早期的原型可能已经实现了),但由于装箱费用而被拒绝。考虑到字符串具有char作为原始值的字符串,当调用者可能只是对该​​值进行一点处理并将其重新装箱回到字符串中时,无条件施加装箱似乎是一个错误。

我们还考虑了CharStream原始的专业化,但是与它添加到API的批量数量相比,它的使用似乎非常狭窄。似乎没有必要添加它。

这给调用者带来的代价是,他们必须知道IntStream包含char表示为的值,ints并且必须在正确的位置进行转换。这使人们倍感困惑,因为有大量的API调用,例如PrintStream.print(char)和PrintStream.print(int),其行为明显不同。可能会引起另一个混乱点,因为该codePoints()调用还会返回一个,IntStream但其中包含的值却大不相同。

因此,这归结为在几种选择中务实地选择:

  1. 我们无法提供原始的专业知识,因此会产生简单,优雅,一致的API,但会带来高性能和GC开销;

  2. 我们可以提供一套完整的原始专业知识,但代价是使API混乱不堪,并给JDK开发人员带来了维护负担;要么

  3. 我们可以提供原始专业化的子集,从而提供中等大小的高性能API,从而在相当狭窄的用例范围(字符处理)中为调用者带来相对较小的负担。

我们选择了最后一个。

2020-11-06