在Java 8中,有一个新的方法String.chars(),它返回一个表示字符代码的int流(IntStream)。我猜很多人会期待这里会有一连串的字符。这样设计API的动机是什么?
当前回答
skiwi的回答已经涵盖了许多主要问题。我会补充一些背景知识。
任何API的设计都是一系列的权衡。在Java中,一个困难的问题是处理很久以前做出的设计决策。
Java从1.0开始就有了原语。它们使Java成为一种“不纯的”面向对象语言,因为原语不是对象。我相信,添加原语是以牺牲面向对象的纯洁性为代价来提高性能的务实决定。
这是我们在近20年后的今天仍然面临的一个权衡。Java 5中添加的自动装箱特性基本上消除了用装箱和拆箱方法调用使源代码混乱的需要,但开销仍然存在。在很多情况下,这是不明显的。但是,如果要在一个内循环中执行装箱或拆箱操作,就会发现这会增加大量的CPU和垃圾收集开销。
When designing the Streams API, it was clear that we had to support primitives. The boxing/unboxing overhead would kill any performance benefit from parallelism. We didn't want to support all of the primitives, though, since that would have added a huge amount of clutter to the API. (Can you really see a use for a ShortStream?) "All" or "none" are comfortable places for a design to be, yet neither was acceptable. So we had to find a reasonable value of "some". We ended up with primitive specializations for int, long, and double. (Personally I would have left out int but that's just me.)
对于CharSequence.chars(),我们考虑返回Stream<Character>(早期原型可能已经实现了这一点),但由于装箱开销而被拒绝。考虑到String具有char值作为原语,当调用者可能只对值进行一些处理并将其解箱为字符串时,无条件地进行装箱似乎是错误的。
我们还考虑了CharStream原语专门化,但与它将添加到API的批量数量相比,它的用途似乎相当狭窄。似乎不值得再加进去。
这给调用者带来的惩罚是,他们必须知道IntStream包含以int表示的char值,并且强制转换必须在适当的位置进行。这是双重混乱,因为有重载API调用,如PrintStream.print(char)和PrintStream.print(int),它们的行为明显不同。另一个可能引起混淆的地方是,codePoints()调用也返回一个IntStream,但它包含的值完全不同。
所以,这可以归结为在几个选项中进行务实的选择:
We could provide no primitive specializations, resulting in a simple, elegant, consistent API, but which imposes a high performance and GC overhead; we could provide a complete set of primitive specializations, at the cost of cluttering up the API and imposing a maintenance burden on JDK developers; or we could provide a subset of primitive specializations, giving a moderately sized, high performing API that imposes a relatively small burden on callers in a fairly narrow range of use cases (char processing).
我们选择了最后一个。
其他回答
skiwi的回答已经涵盖了许多主要问题。我会补充一些背景知识。
任何API的设计都是一系列的权衡。在Java中,一个困难的问题是处理很久以前做出的设计决策。
Java从1.0开始就有了原语。它们使Java成为一种“不纯的”面向对象语言,因为原语不是对象。我相信,添加原语是以牺牲面向对象的纯洁性为代价来提高性能的务实决定。
这是我们在近20年后的今天仍然面临的一个权衡。Java 5中添加的自动装箱特性基本上消除了用装箱和拆箱方法调用使源代码混乱的需要,但开销仍然存在。在很多情况下,这是不明显的。但是,如果要在一个内循环中执行装箱或拆箱操作,就会发现这会增加大量的CPU和垃圾收集开销。
When designing the Streams API, it was clear that we had to support primitives. The boxing/unboxing overhead would kill any performance benefit from parallelism. We didn't want to support all of the primitives, though, since that would have added a huge amount of clutter to the API. (Can you really see a use for a ShortStream?) "All" or "none" are comfortable places for a design to be, yet neither was acceptable. So we had to find a reasonable value of "some". We ended up with primitive specializations for int, long, and double. (Personally I would have left out int but that's just me.)
对于CharSequence.chars(),我们考虑返回Stream<Character>(早期原型可能已经实现了这一点),但由于装箱开销而被拒绝。考虑到String具有char值作为原语,当调用者可能只对值进行一些处理并将其解箱为字符串时,无条件地进行装箱似乎是错误的。
我们还考虑了CharStream原语专门化,但与它将添加到API的批量数量相比,它的用途似乎相当狭窄。似乎不值得再加进去。
这给调用者带来的惩罚是,他们必须知道IntStream包含以int表示的char值,并且强制转换必须在适当的位置进行。这是双重混乱,因为有重载API调用,如PrintStream.print(char)和PrintStream.print(int),它们的行为明显不同。另一个可能引起混淆的地方是,codePoints()调用也返回一个IntStream,但它包含的值完全不同。
所以,这可以归结为在几个选项中进行务实的选择:
We could provide no primitive specializations, resulting in a simple, elegant, consistent API, but which imposes a high performance and GC overhead; we could provide a complete set of primitive specializations, at the cost of cluttering up the API and imposing a maintenance burden on JDK developers; or we could provide a subset of primitive specializations, giving a moderately sized, high performing API that imposes a relatively small burden on callers in a fairly narrow range of use cases (char processing).
我们选择了最后一个。
正如其他人已经提到的,这背后的设计决策是为了防止方法和类的爆炸。
尽管如此,我个人认为这是一个非常糟糕的决定,并且应该有,考虑到他们不想做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将它映射到一个对象,这将自动将它放入一个流<字符>,然后我们可以做我们想做的事情,仍然使用方法引用作为一个加号。
请注意,您必须执行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,我个人认为该方法应该返回一个流<字符>,目前的解决方案是在IntStream上使用mapToObj(I -> (char) I),以便能够正确地与它们一起工作。
推荐文章
- 在Java中切换布尔变量的最干净的方法?
- 为什么Oracle 9i将空字符串视为NULL?
- 为什么在Java 8中String.chars()是整数流?
- 无法创建Android虚拟设备
- 在Java中数组是按值传递还是按引用传递?
- PHP中的多行字符串
- 如何在Spring中以编程方式获取当前活动/默认环境概要文件?
- equals vs Arrays。Java中的等号
- 为什么我们通常用|| / |?有什么不同?
- 如何在Android中获得一个RadioGroup的选定索引
- 如何大写一个字的第一个字母在字符串使用Java?
- 在Python中检查一个单词是否在字符串中
- 禁用IntelliJ星(包)导入?
- Python csv字符串到数组
- 面试问题:检查一个字符串是否是另一个字符串的旋转