Java 8编译器中重载方法的困扰

7

我在将一个应用程序升级到Java 8时,在几个地方遇到了与Google Guava的newArrayList相关的奇怪问题。

看看这个例子:

import com.google.common.collect.UnmodifiableIterator;

import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import java.util.ArrayList;

import static com.google.common.collect.Iterators.forEnumeration;
import static com.google.common.collect.Lists.newArrayList;

public class NewArrayListIssue {
    public static void main(String[] args) throws NamingException {

        UnmodifiableIterator<?> elements = forEnumeration(getEnumeration().getAll());
        System.out.println("declarefirst = " + newArrayList(elements)); // calls newArrayList(Iterator<? extends E> elements)

        ArrayList directCopy = newArrayList(forEnumeration(getEnumeration().getAll()));
        System.out.println("useDirectly = " + directCopy); //calls newArrayList(E... elements)
    }

    public static Attribute getEnumeration(){
        return new BasicAttribute("foo",1);
    }
}

在第一个例子中,当我将UnmodifiableIterator先存储到自己的变量中,然后调用newArrayList时,得到了我期望的结果,即将迭代器的值复制到新的List中。

在第二个例子中,当forEnumeration直接进入newArrayList方法时,返回的是一个包含迭代器(其中包含值)的List

根据Intellij的推测,两个方法调用都应该是newArrayList(Iterator<? extends E> elements),但我发现调试时第二个调用实际上进入了newArrayList(E... elements)

只有在使用针对Java8的Oracle JDK8编译时才会出现这种情况。如果我针对7进行编译,就能正常工作。

2个回答

7
问题在于编译器认为newArrayList(Iterator<? extends E>)不适用(可能是因为这个bug),然后默默地选择了泛型可变参数方法,该方法始终适用(这显示了这种重载的危险性),当您没有为结果列表使用特定元素类型时。

该bug出现在通配符类型中,即在您的代码中是Attribute.getAll()返回一个NamingEnumeration<?>,因此forEnumeration的结果是UnmodifiableIterator<?>,编译器拒绝将其分配给Iterable<? extends E>newArrayList的参数类型。如果您将内部调用的返回值强制转换为Enumeration的返回值,则问题消失,就像强制转换外部调用的返回值到Iterator的返回值一样。

我看不到这个问题的简单短期解决方案。毕竟,我不明白为什么您一开始没有使用List<?> directCopy=Collections.list(getEnumeration().getAll())

请注意,如果您想找到此问题的所有出现情况,可以使用已删除newArrayList(E...)的guava的修补版本,并检查所有编译器错误(假设您没有许多真正想要调用此重载的情况)。重写调用站点后,您可以转回原始的guava。


这是一个由许多作者编写的大型代码库。因此,我无法回答为什么选择了一种方法而不是另一种方法。我只需要找到它们所有的位置。感谢您提供有关修补guava的提示。我之前没有想过这个。我猜我知道今天早上要做什么了。 - ryber
唯一的问题在于移除newArrayList(E...)之后,编译器会开始使用正确的方式。因此,看起来唯一真正安全的方法是尽快评估它们所有。 - ryber
1
这很奇怪,因为不使用正确的方法的原因是编译器认为正确的方法不适用,而且当错误的方法被删除时,这种情况不应该突然改变。当我尝试使用 Netbeans 技巧时,它按预期工作了,但在 IntelliJ 上可能会有所不同,正如您所说,IntelliJ 会选择正确的调用目标。但我认为如果 IntelliJ 和底层 javac 不一致,IntelliJ 就不会隐藏 javac 的错误。它是否实际生成了类文件? - Holger
我只是在使用Maven进行编译,没有使用IntelliJ。 - ryber
2
似乎是版本问题。我刚用1.8.0_20测试了一下,它产生了一个错误,但在beta版的1.8.0_40中编译成功了。这是一致的:如果使用1.8.0_40进行编译而没有删除varargs方法,则仍会调用正确的方法。 - Holger

1
我曾经看到过这种情况出现在方法重载和泛型类型上。在这种情况下,当参数没有明确类型时,会选择更通用的newArrayList()版本。
我无法为您提供技术解释,但我建议您通过强制转换来使用所需的方法重载:
ArrayList directCopy = newArrayList((Iterator)forEnumeration(getEnumeration().getAll()));

啊,这意味着我必须先找到它们全部! - ryber
哈哈,有点糟糕 :/ 因为IntelliJ已经正确地确定了这些引用,所以应该只是一个“查找所有引用”吧? - gknicker
我本以为Intellij会很奇怪,但它却给了我所有的变化。而且newArrayList(Iterator)也很常见,因为它经常与Filter等一起使用。所以我需要浏览它们所有,并找出那些有歧义的泛型(大约有600个)。 - ryber

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