为什么String.chars()是Java 8中的一个int流?

Ada*_*yga 171 java string java-8

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

ski*_*iwi 195

正如其他人已经提到的那样,这背后的设计决策是为了防止方法和类的爆炸.

不过,我个人认为这是一个非常糟糕的决定,并且鉴于他们不想制作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));
}
Run Code Online (Sandbox Code Playgroud)

我认为在Java 8中使用它的合理方法如下:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

在这里,我获得了一个IntStream并通过lambda i -> (char)i将它映射到一个对象,这将自动将其打包成一个Stream<Character>,然后我们可以做我们想要的,并仍然使用方法引用作为加号.

请注意,你必须这样做mapToObj,如果你忘记并使用map,那么没有什么会抱怨,但你仍然IntStream会得到一个,你可能会想知道为什么它打印整数值而不是代表字符的字符串.

Java 8的其他丑陋替代品:

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

hello.chars()
        .forEach(i -> System.out.println((char)i));
Run Code Online (Sandbox Code Playgroud)

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

private void print(char c) {
    System.out.println(c);
}
Run Code Online (Sandbox Code Playgroud)

然后

hello.chars()
        .forEach(this::print);
Run Code Online (Sandbox Code Playgroud)

这将产生编译错误,因为可能存在有损转换.

结论:

API是这样设计的,因为不想添加CharStream,我个人认为该方法应该返回一个Stream<Character>,并且当前的解决方法是使用mapToObj(i -> (char)i)on IntStream来能够正常使用它们.

  • +1,但我的建议是使用`codePoints()`而不是`chars()`你会发现很多库函数已经接受`int`除了`char`之外的代码点,例如`的所有方法java.lang.Character`以及`StringBuilder.appendCodePoint`等.这个支持从`jdk1.5`开始存在. (24认同)
  • 我的结论:这部分API被设计破坏了.但感谢广泛的答复 (6认同)
  • 关于代码点的好处.使用它们将处理补充字符,它们在`String`或`char []`中表示为代理对.我敢打赌,大多数`char`处理代码错误处理了代理对. (6认同)
  • @skiwi,定义`void print(int ch){System.out.println((char)ch); 然后你可以使用方法引用. (2认同)
  • 请参阅我的答案,了解为什么`Stream <Character>`被拒绝了. (2认同)

Stu*_*rks 79

来自skiwi答案已经涵盖了许多重点.我会填写更多背景知识.

任何API的设计都是一系列的权衡.在Java中,其中一个难题是处理很久以前制定的设计决策.

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

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

在设计Streams API时,很明显我们必须支持原语.装箱/拆箱开销会破坏并行性带来的任何性能优势.但是,我们不想支持所有原语,因为这会给API增加大量的混乱.(你能真正看到一个用途ShortStream?)"全部"或"无"是设计的舒适场所,但都不可接受.所以我们必须找到合理的"一些"价值.我们结束了与原始的专长int,longdouble.(我个人原谅了,int但那只是我.)

因为CharSequence.chars()我们考虑返回Stream<Character>(早期的原型可能实现了这个),但由于拳击开销而被拒绝.考虑到String将char值作为基元,当调用者可能只对该值进行一些处理并将其反转回字符串时,无条件地强加拳击似乎是错误的.

我们还考虑了一种CharStream原始的特化,但与它添加到API的批量相比,它的使用似乎相当狭窄.添加它似乎不值得.

对调用者施加的惩罚是他们必须知道IntStream包含的char值表示为ints和必须在适当的位置进行转换.这是令人困惑的,因为有过多的API调用,PrintStream.print(char)并且PrintStream.print(int)它们的行为明显不同.可能会出现另一个混乱点,因为codePoints()调用也返回一个IntStream但它包含的值非常不同.

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

  1. 我们不能提供原始的特化,从而产生一个简单,优雅,一致的API,但它会带来高性能和GC开销;

  2. 我们可以提供一整套原始专业化,代价是混乱API并给JDK开发人员带来维护负担; 要么

  3. 我们可以提供一个原始特化的子集,给出一个中等大小,高性能的API,在相当窄范围的用例(字符处理)中给调用者带来相对较小的负担.

我们选择了最后一个.

  • @TagirValeev是的,它有点麻烦.使用代码点流(IntStream)也不错:`collect(StringBuilder :: new,StringBuilder :: appendCodePoint,StringBuilder :: append).toString()`.我猜它不是很短,但使用代码点避免了`(char)`强制转换,并允许使用方法引用.此外,它正确处理代理人. (7认同)
  • 多亏了重复的问题,我注意到了这个.我同意`chars()`返回`IntStream`并不是一个大问题,特别是考虑到它很少使用这种方法的事实.然而,有一个内置的方法将`IntStream`转换回`String`会很好.它可以用`.reduce(StringBuilder :: new,(sb,c) - > sb.append((char)c),StringBuilder :: append).toString()`完成,但它确实很长. (5认同)
  • 极简主义在这里出现.如果已经有`chars()`方法返回`IntStream`中的char值,那么为了获得相同的值但是以盒装形式的另一个API调用,它并没有增加太多.呼叫者可以毫不费力地打包值.当然,不必在这种(可能是罕见的)情况下执行此操作会更方便,但代价是为API添加混乱. (3认同)
  • 不错的答案!然而,它没有回答为什么`chars()` 不能有两种不同的方法,一种返回`Stream&lt;Character&gt;`(性能损失很小),另一种是`IntStream`,这是否也被考虑过?人们很可能最终将它映射到一个 `Stream&lt;Character&gt;`,如果他们认为方便是值得的而不是性能损失。 (2认同)
  • @IlyaBystrov不幸的是,诸如`IntStream`之类的原始流没有一个带有`Collector`的`collect()`方法.它们只有前面评论中提到的三参数`collect()`方法. (2认同)
  • 非常不幸的是,这将花费开发人员数千小时的时间,因为像 Arrays.stream(char[]) 这样的自然构造不起作用,开发人员将通过 Google 寻找替代方案。至少 Arrays.stream(char/byte/short[]) 应该存在并返回 IntStream。 (2认同)