Java 8、为什么Arrays没有给Iterable的forEach方法?

In Java 8, why were Arrays not given the forEach method of Iterable?

我一定是漏掉了什么。

在 Java 5 中,"for-each loop" statement (also called the enhanced for loop) was introduced. It appears that it was introduced mainly to iterate through Collections. Any collection (or container) class that implements the Iterable 接口可以使用“for-each 循环”进行迭代。也许由于历史原因,Java 数组没有实现 Iterable 接口。但是由于数组 were/are 无处不在,javac 会接受在数组上使用 for-each 循环(生成等同于传统 for 循环的字节码)。

在 Java 8 中,forEach method 作为 default 方法添加到 Iterable 接口。这使得将 lambda 表达式传递给集合(在迭代时)成为可能(例如 list.forEach(System.out::println))。但同样,数组不享受这种待遇。 (我知道有解决方法)。

是否有技术原因导致 javac 无法增强为在 forEach 中接受数组,就像它在增强的 for 循环中接受数组一样?似乎无需数组实现 Iterable 即可生成代码。我是不是太天真了?

这对于语言新手来说尤其重要,因为数组在句法上很容易自然而然地使用数组。切换到列表并使用 Arrays.asList(1, 2, 3).

很难自然

假设将特殊代码添加到java 编译器中以处理forEach。 然后可以问很多类似的问题。 为什么我们不能写myArray.fill(0)?还是myArray.copyOfRange(from, to)?还是myArray.sort()myArray.binarySearch()myArray.stream()?实际上 Arrays 中的每个静态方法 接口可以转换成"array class"对应的方法。 JDK 开发人员为什么要停下来 myArray.forEach()?但是请注意,每个这样的方法不仅必须添加到 classlib 规范中, 但进入 Java 语言规范,它更加稳定和保守。这也意味着不仅 这些方法的实现将成为规范的一部分,而且 class 像 java.util.function.Consumer 应该在 JLS 中明确提及(这是提议的 forEach 方法的论点)。 另请注意,新消费者将有必要添加到 对应数组类型的标准库,如 FloatConsumerByteConsumer 等。 目前 JLS 很少引用 java.lang 包之外的类型(有一些明显的例外 像 java.util.Iterator)。这意味着一些稳定层。提议的更改对于 Java 语言来说过于激烈。

另请注意,目前我们有一种方法可以直接为数组调用(并且实现方式不同 来自 java.lang.Object):它是 clone() 方法。它实际上将一些脏部分添加到 javac 甚至 JVM 中 因为它必须在任何地方进行特殊处理。这会导致错误(例如,方法引用在 Java 8 JDK-8056051 中被错误处理)。在 javac 中添加更多类似的复杂性可能会引入 甚至更多类似的错误。

此类功能可能会在不久的将来作为 Arrays 2.0 initiative。 这个想法是为将位于 class 库中的数组引入一些 superclass ,所以 只需编写正常的 java 代码即可添加新方法,而无需调整 javac/JVM。然而, 这也是一个非常困难的特性,因为数组总是在 Java 中被特殊对待,并且, 据我所知,目前还不知道是否会实施以及何时实施。

在 Java 语言和数组的 JVM 中有很多特殊情况。数组有一个 API,但它几乎不可见。就好像数组被声明为:

  • implements Cloneable, Serializable
  • public final int length
  • public T[] clone() 其中 T 是数组的组件类型

但是,这些声明在任何地方的任何源代码中都不可见。有关解释,请参阅 JLS 4.10.3 and JLS 10.7CloneableSerializable 通过反射可见,并通过调用

返回
Object[].class.getInterfaces()

也许令人惊讶的是,length 字段和 clone() 方法在反射中不可见。 length 字段根本不是一个字段;使用它会变成一个特殊的 arraylength 字节码。对 clone() 的调用会导致实际的虚方法调用,但如果接收方是数组类型,则由 JVM 专门处理。

值得注意的是,数组 classes 没有实现 Iterable 接口。

当在 Java SE 5 中添加增强型 for 循环 ("for-each") 时,它支持右侧表达式的两种不同情况:Iterable 或数组类型 (JLS 14.14.2)。原因是 Iterable 实例和数组在 enhanced-for 语句中的处理方式完全不同。 JLS的那一段给出了完整的处理,不过说的简单点,情况如下

对于Iterable<T> iterable,代码

for (T t : iterable) {
    <loop body>
}

的语法糖
for (Iterator<T> iterator = iterable.iterator(); iterator.hasNext(); ) {
    t = iterator.next();
    <loop body>
}

对于数组T[],代码

for (T t : array) {
    <loop body>
}

的语法糖
int len = array.length;
for (int i = 0; i < len; i++) {
    t = array[i];
    <loop body>
}

那么,为什么要这样做呢?数组当然有可能实现 Iterable,因为它们已经实现了其他接口。编译器也可以合成一个由数组支持的 Iterator 实现。 (这有先例。编译器已经合成了自动添加到每个 enum class 的静态 values()valueOf() 方法,如 JLS 8.9.3 中所述.)

但是数组是一种非常低级的结构,通过 int 值访问数组预计是一种极其廉价的操作。 运行 从 0 到数组长度的循环索引,每次递增 1,这是非常惯用的。数组上的增强型 for 循环正是这样做的。如果数组上的增强型 for 循环是使用 Iterable 协议实现的,我想大多数人会惊讶地发现数组上的循环涉及初始方法调用和内存分配(创建 Iterator),每个循环迭代后跟两个方法调用。

因此,当在 Java 8 中将默认方法添加到 Iterable 时,这根本不会影响数组。

正如其他人所指出的,如果您有一个 intlongdouble 或引用类型的数组,则可以使用以下方法之一将其转换为流Arrays.stream() 电话。这提供了对 map()filter()forEach()

的访问

不过,如果 Java 语言和 JVM 中数组的特殊情况被 real 构造所取代(同时修复一堆其他与数组相关的问题,例如 2+ 维数组处理不当、2^31 长度限制等)。这是由 John Rose 领导的 "Arrays 2.0" 调查的主题。请参阅 John 在 JVMLS 2012 上的演讲 (video, slides)。与本次讨论相关的想法包括为数组引入实际接口,允许库插入元素访问,支持切片和复制等其他操作。

请注意,所有这些都是调查和未来的工作。在撰写本文时 (2016-02-23),任何版本的 Java 路线图中都没有承诺这些阵列增强功能。