【问题标题】:Why is String.chars() a stream of ints in Java 8?为什么 String.chars() 在 Java 8 中是整数流?
【发布时间】:2014-04-21 13:32:36
【问题描述】:

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

【问题讨论】:

  • @RohitJain 我不是指任何特定的流。如果CharStream 不存在,添加它会有什么问题?
  • @AdamDyga:设计者明确选择通过将原始流限制为 3 种类型来避免类和方法的爆炸,因为其他类型(char、short、float)可以用它们更大的等价物来表示(int, double) 没有任何显着的性能损失。
  • @JBNizet 我明白了。但是为了节省几个新课程,它仍然感觉像是一个肮脏的解决方案。
  • @JB Nizet:在我看来,考虑到所有流重载以及all function interfaces...,我们已经拥有大量接口
  • 是的,已经发生了爆炸,即使只有三个原始流专业化。如果所有八个原语都有流专业化会怎样?一场大灾难? :-)

标签: java string java-8


【解决方案1】:

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

不过,我个人认为这是一个非常糟糕的决定,考虑到他们不想做出CharStream,这是合理的,我会想到不同的方法而不是chars():

  • Stream<Character> chars(),它给出了一个框字符流,这将有一些轻微的性能损失。
  • IntStream unboxedChars(),将用于性能代码。

然而,与其关注为什么目前这样做,我认为这个答案应该侧重于展示一种使用我们拥有的 API 的方法使用 Java 8。

在 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 -&gt; (char)i映射到一个对象,这会自动把它装箱成​​一个Stream&lt;Character&gt;,然后我们就可以做我们想做的事了,仍然使用方法引用作为加号。

注意虽然你必须使用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&lt;Character&gt;,而目前的解决方法是在IntStream 上使用mapToObj(i -&gt; (char)i) 能够与他们一起正常工作。

【讨论】:

  • 我的结论是:这部分 API 被设计破坏了。但是感谢您的广泛回答
  • +1,但我的建议是使用codePoints() 而不是chars(),你会发现很多库函数已经接受int 作为代码点除了char,例如java.lang.Character 以及 StringBuilder.appendCodePoint 等的所有方法。此支持从 jdk1.5 开始存在。
  • 关于代码点的要点。使用它们将处理补充字符,它们在String 或char[] 中表示为代理对。我敢打赌,大多数 char 处理代码错误地处理代理对。
  • @skiwi,定义void print(int ch) { System.out.println((char)ch); },然后你就可以使用方法引用了。
  • 查看我的回答,了解Stream&lt;Character&gt; 被拒绝的原因。
【解决方案2】:

answer from skiwi 已经涵盖了许多要点。我再补充一点背景。

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

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

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

在设计 Streams API 时,很明显我们必须支持原语。装箱/拆箱开销会扼杀并行性带来的任何性能优势。不过,我们不想支持所有原语,因为那样会给 API 带来大量混乱。 (你真的能看到ShortStream 的用途吗?)“全部”或“无”对于设计来说都是舒适的地方,但两者都不可接受。所以我们必须找到一个合理的“some”值。我们最终得到了int、long 和double 的原始特化。 (就我个人而言,我会忽略int,但这只是我。)

对于CharSequence.chars(),我们考虑返回Stream&lt;Character&gt;(早期的原型可能已经实现了这一点)但由于装箱开销而被拒绝。考虑到 String 将 char 值作为原语,当调用者可能只是对值进行一些处理并将其拆箱回字符串时,无条件强制装箱似乎是错误的。

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

这对调用者造成的惩罚是他们必须知道IntStream 包含表示为ints 的char 值,并且必须在正确的位置进行转换。这是双重混淆,因为有重载的 API 调用,如 PrintStream.print(char) 和 PrintStream.print(int),它们的行为明显不同。由于codePoints() 调用也返回IntStream,但它包含的值完全不同,因此可能会出现另一个混淆点。

因此,这归结为在几个备选方案中进行务实的选择:

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

  2. 我们可以提供一套完整的原始特化,但代价是使 API 变得混乱并给 JDK 开发人员带来维护负担;或

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

    李>

我们选择了最后一个。

【讨论】:

  • 不错的答案!但是它没有回答为什么chars() 不能有两种不同的方法,一种返回Stream&lt;Character&gt;(性能损失很小),另一种是IntStream,这是否也考虑过?如果人们认为便利性比性能损失更值得,他们很可能最终会将其映射到Stream&lt;Character&gt;。
  • 极简主义来了。如果已经有 chars() 方法返回 IntStream 中的 char 值,那么让另一个 API 调用获取相同的值但采用盒装形式并不会增加太多。调用者可以毫不费力地将值装箱。当然,在这种(可能很少见)情况下不必这样做会更方便,但代价是给 API 增加了混乱。
  • 感谢重复的问题,我注意到了这个问题。我同意chars() 返回IntStream 不是一个大问题,特别是考虑到它很少使用这种方法这一事实。但是,最好有一种内置方法将IntStream 转换回String。可以用.reduce(StringBuilder::new, (sb, c) -&gt; sb.append((char)c), StringBuilder::append).toString()完成,但是真的很长。
  • @TagirValeev 是的,这有点麻烦。使用代码点流(IntStream)还不错:collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()。我想它并不是真的更短,但是使用代码点可以避免 (char) 强制转换并允许使用方法引用。另外,它可以正确处理代理。
  • @IlyaBystrov 不幸的是,像IntStream 这样的原始流没有采用Collector 的collect() 方法。它们只有一个三参数collect() 方法,如之前的 cmets 中所述。
猜你喜欢
  • 2016-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-17
  • 1970-01-01
  • 2014-12-19
相关资源
最近更新 更多