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

60
我可能漏掉了什么。
在Java 5中,引入了"for-each loop"语句(也称增强型for循环)。看起来它主要是用于遍历Collections。任何实现Iterable接口的集合类(或容器)都可以使用“for-each loop”进行迭代。也许由于历史原因,Java数组没有实现Iterable接口。但由于数组是/仍然是无处不在的,javac会接受对数组使用for-each循环(生成等效于传统for循环的字节码)。
在Java 8中,forEach方法作为一个默认方法被添加到Iterable接口中。这使得将lambda表达式传递到集合(在迭代时)成为可能(例如:list.forEach(System.out::println))。但是,数组不享受此待遇。 (我知道有解决方法)。
是否存在技术原因导致javac不能增强以接受数组中的forEach,就像它接受增强for循环中的数组一样?看起来可以生成代码,而无需要求数组实现Iterable。我是不是过于天真?
这对于初学者特别重要,因为他们自然会使用数组,因为它们具有语法上的便利性。切换到列表并使用Arrays.asList(1, 2, 3)并不自然。

3
可能是因为这不值得——与数组相比,集合更易于操作,当你需要使用数组时,asList(myArray) 是一个非常简单的解决方法。 - Mick Mnemonic
2
你在开玩笑吗?他们甚至没有自己的 toString() - Bohemian
听起来像是一个Java 8团队的问题。 - ChiefTwoPencils
5
我们如何知道答案缺少参考资料,而不允许回答呢? - Mark Peters
4
一个数组可以转换成Java 8的流:Arrays.stream(a).forEach(...);。我不知道为什么这个问题被关闭了,可能有很好的答案。 - pp_
显示剩余5条评论
2个回答

55

Java语言和JVM中有许多关于数组的特殊情况。虽然数组有API,但它几乎不可见。就好像数组被声明为:

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

然而,这些声明在任何源代码中都不可见。请参见JLS 4.10.3JLS 10.7以获取解释。通过反射可以看到CloneableSerializable,并且它们是通过调用返回的。

Object[].class.getInterfaces()

也许令人惊讶的是,length字段和clone()方法在反射中不可见。 length字段根本不是字段;使用它会变成特殊的arraylength字节码。对clone()的调用会导致实际的虚拟方法调用,但如果接收器是数组类型,则JVM会特别处理。
值得注意的是,数组类不实现Iterable接口。
当增强型for循环(“for-each”)在Java SE 5中添加时,它支持右侧表达式的两种不同情况:一个Iterable或一个数组类型(JLS 14.14.2)。原因是Iterable实例和数组被增强型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实现。(这方面有先例。编译器已经合成了静态的values()valueOf()方法,自动添加到每个enum类中,如JLS 8.9.3所述。)
但是数组是非常低级的结构,通过一个int值访问数组被认为是极其廉价的操作。通常情况下,会将循环索引从0到数组长度,每次递增1。对于数组的增强for循环正是这样做的。如果使用Iterable协议实现数组的增强for循环,我认为大多数人会惊讶地发现,循环数组涉及初始方法调用和内存分配(创建迭代器),以及每个循环迭代两个方法调用。
因此,当Java 8中添加默认方法到Iterable时,这并不影响数组。
正如其他人所指出的,如果您有一个int、long、double或引用类型的数组,则可以使用Arrays.stream()之一将其转换为流。这提供了对map()、filter()、forEach()等的访问。
虽然Java语言和JVM中的数组特殊情况可以被替换为“真正”的构造(以及修复一堆其他与数组相关的问题,例如对2+维数组的处理不佳,2^31长度限制等),但如果这样做会很好。这就是由John Rose领导的“Arrays 2.0”调查的主题。请参阅John在JVMLS 2012的演讲(videoslides)。与此讨论相关的想法包括引入实际的数组接口,允许库插入元素访问,支持额外的操作,例如切片和复制等。
请注意,所有这些都是调查和未来工作。截至撰写本文(2016-02-23),Java路线图中没有任何版本承诺这些数组增强功能。

1
谢谢您提供的更或少官方的观点,参考文献和最重要的是抽出时间写下这一切!尽管我的观察主要关于让for-each和forEach对数组行为表现得(天真地)符合期望(而不是让数组实现Iterable),但是您的答案确实提供了一些有价值的见解。 - Kedar Mhaswade
哇,天才! - Tilak Madichetti

12

假设特殊代码将被添加到Java编译器中来处理forEach。然后会有很多类似的问题。为什么我们不能写myArray.fill(0)?或者myArray.copyOfRange(from, to)?或者myArray.sort()myArray.binarySearch()myArray.stream()?实际上,Arrays接口中的几乎每个静态方法都可以转换为相应的"数组类"方法。为什么JDK开发人员要停留在myArray.forEach()上呢?请注意,每个这样的方法不仅必须添加到类库规范中,还必须添加到Java语言规范中,后者更加稳定和保守。此外,这意味着不仅这些方法的实现将成为规范的一部分,像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计划的一部分实现。该想法是引入一些超类来定位数组,该超类将位于类库中,因此可以通过编写普通的Java代码来添加新方法,而无需调整javac/JVM。然而,这也是非常困难的功能,因为数组在Java中始终被特殊处理,并且据我所知,尚不清楚是否会实现以及何时实现。

感谢提供JDK开发人员的观点!数组上缺少那些其他的集合方法并不困扰我,因为只有在查看集合(甚至是java.lang.Object)的Javadoc时才能看到它们,然后会想为什么不能将它们“移植”到数组中(有点类似于反向工程)。而增强型for循环和forEach则是数组可用性的自然候选项,因此引出了这个问题。 - Kedar Mhaswade
1
@KedarMhaswade,虽然我是JDK开发人员,但我实际上是一个新手,所以我更像是一个外部人员,我的答案不应被视为官方答案。也许Stuart Marks在AMA会议期间会对此发表评论(实际上我是通过virtualJUG网站找到这个问题的)。 - Tagir Valeev
1
@KedarMhaswade,很遗憾这个问题在VirtualJUG AMA中没有得到讨论。我也是通过你在VirtualJUG论坛上的发帖找到了这个问题。 - Stuart Marks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接