String.chars()
的新方法,它返回一个表示字符编码的int
流(IntStream
)。我猜很多人会期望这里返回一个char
流。那么设计API这种方式的动机是什么呢?String.chars()
的新方法,它返回一个表示字符编码的int
流(IntStream
)。我猜很多人会期望这里返回一个char
流。那么设计API这种方式的动机是什么呢?正如其他人已经提到的,这个设计决定是为了防止方法和类的爆炸。
尽管如此,我个人认为这是一个非常糟糕的决定,考虑到他们不想创建一个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>
,目前的解决方法是在 IntStream
上使用 mapToObj(i -> (char)i)
,以便能够正确地处理它们。
codePoints()
而不是chars()
,你会发现许多库函数已经接受了一个代表代码点的int
,除了char
之外。例如:java.lang.Character
的所有方法以及StringBuilder.appendCodePoint
等。此支持从jdk1.5
开始存在。 - Holgervoid print(int ch) { System.out.println((char)ch); }
然后你就可以使用方法引用了。 - Stuart MarksStream<Character>
被拒绝,请查看我的答案。 - Stuart Marksskiwi的回答已经涵盖了许多主要观点。我将提供更多背景信息。
任何API的设计都是一系列权衡。在Java中,其中一个棘手的问题是处理早期设计决策。
原始类型从Java 1.0开始就存在。它们使得Java成为了一个“不纯”的面向对象语言,因为原始类型并不是对象。添加原始类型是一种实用的决策,可以在牺牲面向对象纯度的前提下提高性能。
这是一个我们今天仍在使用的权衡,已经快20年了。Java 5中添加的自动装箱特性基本上消除了在源代码中添加装箱和拆箱方法调用的需要,但是开销仍然存在。在许多情况下,这并不明显。但是,如果您在内部循环中执行装箱或拆箱,您将看到它会施加重大的CPU和垃圾收集开销。
设计Streams API时,清楚地意识到我们必须支持原始类型。装箱/拆箱开销将抵消并行性带来的任何性能优势。然而,我们并不想支持所有的原始类型,因为那样会给API添加大量的混乱。 (你真的能看到ShortStream
的用法吗?)"全部"或"无"都是设计中舒适的位置,但两者都不可接受。所以我们必须找到一个合理的"一些"的值。最终我们选择了int
,long
和double
的原始类型专业化。(个人认为应该把int
省略掉,但这只是我的想法。)
对于CharSequence.chars()
,我们考虑返回Stream<Character>
(早期原型可能已实现此功能),但由于装箱开销而被拒绝。考虑到字符串具有char
作为原始类型的值,强制进行装箱并不明智,因为调用方可能只会对该值进行一些处理,并将其迅速解压回字符串中。
我们可以不提供原始特化,从而获得简单、优雅、一致的API,但这会带来高性能和GC开销;
我们可以提供完整的原始特化集,代价是使API变得混乱,并对JDK开发人员施加维护负担;或者
我们可以提供子集的原始特化,提供一个相当小范围的使用情况(字符处理)下给调用者带来较小负担的高性能API。
chars()
不能有两种不同的方法,一种返回一个 Stream<Character>
(性能较差),另一种是 IntStream
,这也被考虑过吗?如果人们认为方便的价值超过了性能损失,他们很可能会将其映射到 Stream<Character>
上。 - skiwiIntStream
中char值的chars()
方法,那么再添加另一个API调用在装箱形式下获取相同的值并没有太多意义。调用者可以轻松地将这些值装箱。当然,在这种(可能很少见)情况下,不需要这样做会更加方便,但代价是给API添加混乱的元素。 - Stuart Markschars()
返回 IntStream
并不是一个大问题,特别是考虑到这个方法很少被使用。然而,最好有一种内置的方法将 IntStream
转换回 String
。可以通过 .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString()
实现,但这样做实在太长了。 - Tagir Valeevcollect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()
。我想它并没有变得更短,但使用代码点避免了(char)
强制转换,并允许使用方法引用。此外,它可以正确处理代理项。 - Stuart MarksIntStream
这样的原始流没有接受Collector
的collect()
方法。正如之前的评论所提到的那样,它们只有一个三个参数的collect()
方法。 - Stuart Marks
CharStream
不存在,添加它会有什么问题? - Adam DygaString.codePoints()
而不是.chars()
——后者处理Unicode字符的方式不如人意(它会分割代理对)。除非您确定字符串绝不包含高代码点字符,否则应避免使用.chars()
。 - dimo414