Java类型推断:Java 8中的引用不明确,但Java 7则不然

Roh*_*mar 26 java generics type-inference compiler-errors java-8

假设我们有2节课.一个空类Base,以及该类的子类Derived.

public class Base {}

public class Derived extends Base {}
Run Code Online (Sandbox Code Playgroud)

然后我们在另一个类中有几个方法:

import java.util.Collection

public class Consumer {

    public void test() {
        set(new Derived(), new Consumer().get());
    }

    public <T extends Base> T get() {
        return (T) new Derived();
    }

    public void set(Base i, Derived b) {
        System.out.println("base");
    }

    public void set(Derived d, Collection<? extends Consumer> o) {
        System.out.println("object");
    }

}
Run Code Online (Sandbox Code Playgroud)

这在Java 7中成功编译并运行,但不能在Java 8中编译.错误:

Error:(8, 9) java: reference to set is ambiguous
  both method set(Base,Derived) in Consumer and 
  method set(Derived,java.util.Collection) in Consumer match
Run Code Online (Sandbox Code Playgroud)

为什么在Java 7中工作,而不是Java 8?怎么可能<T extends Base> 永远匹配收集?

Hol*_*ger 23

问题是类型推断已得到改进.你有一个像这样的方法

public <T extends Base> T get() {
    return (T) new Derived();
}
Run Code Online (Sandbox Code Playgroud)

基本上说,"调用者可以决定Base我返回的子类",这显然是无稽之谈.每个编译器都应该(T)在这里给你一个关于你的类型转换的未经检查的警告.

现在你有一个方法调用:

set(new Derived(), new Consumer().get());
Run Code Online (Sandbox Code Playgroud)

回想一下,你的方法Consumer.get()说"调用者可以决定我的回报".因此,假设可能存在同时扩展Base和实现的类型是完全正确的Collection.所以编译器说"我不知道是否要打电话" set(Base i, Derived b)set(Derived d, Collection<? extends Consumer> o)".

您可以通过调用来"修复"它,set(new Derived(), new Consumer().<Derived>get());但为了说明您的方法的疯狂,请注意您也可以将其更改为

public <X extends Base&Collection<Consumer>> void test() {
    set(new Derived(), new Consumer().<X>get());
}
Run Code Online (Sandbox Code Playgroud)

现在调用时set(Derived d, Collection<? extends Consumer> o)不会发出任何编译器警告.实际的不安全操作发生在get方法内部.

所以正确的解决方法是从get方法中删除type参数并声明它真正返回的内容Derived.


顺便说一句,令我恼火的是,你声称这个代码可以在Java 7下编译.它的嵌套方法调用的有限类型推断导致get在嵌套调用上下文中处理方法,如返回Base无法传递给方法期待一个Derived.因此,尝试使用符合标准的Java 7编译器编译此代码也会失败,但出于不同的原因.


Jat*_*tin 5

好吧,它不像Java7很乐意运行它.它在给出错误之前给出了几个警告:

jatin@jatin-~$ javac -Xlint:unchecked -source 1.7 com/company/Main.java 
warning: [options] bootstrap class path not set in conjunction with -source 1.7
com/company/Main.java:19: error: no suitable method found for set(Derived,Base)
        set(new Derived(), new Consumer().get());
        ^
    method Consumer.set(Base,Derived) is not applicable
      (argument mismatch; Base cannot be converted to Derived)
    method Consumer.set(Derived,Collection<? extends Consumer>) is not applicable
      (argument mismatch; Base cannot be converted to Collection<? extends Consumer>)

com/company/Main.java:28: warning: [unchecked] unchecked cast
        return (T) new Derived();
                   ^
  required: T
  found:    Derived
  where T is a type-variable:
    T extends Base declared in method <T>get()
Run Code Online (Sandbox Code Playgroud)

问题是:

set(new Derived(), new Consumer().get());
Run Code Online (Sandbox Code Playgroud)

在做的时候new Consumer().get(),如果看的是签名get.它返回一个类型T.我们知道T不知何故延伸了Base.但我们不知道T具体是什么.它可以是任何东西.那么如果我们不能具体决定什么T,那么编译器怎么样?

告诉编译器的一种方法是通过硬编码并具体告诉它:set(new Derived(), new Consumer().<Derived>get());.

上面的原因非常极端(有意重复)危险,是当你尝试这样做时:

class NewDerived extends Base {
     public String getName(){return "name";};
}
NewDerived d = new Consumer().<NewDerived>get();
System.out.println(d.getName());
Run Code Online (Sandbox Code Playgroud)

在Java7(或任何Java版本)中,它将在运行时抛出异常:

线程"main"中的异常java.lang.ClassCastException:com.company.Derived无法强制转换为com.company.NewDerived

因为get返回了一个类型的对象,Derived但是你已经向编译器提到了它的类型NewDerived.并且它无法正确地将Derived转换为NewDerived.这就是它显示警告的原因.


根据错误,现在我们明白了什么是错的new Consumer().get().它的类型是something that extends base.这样做set(new Derived(), new Consumer().get());看起来对接受参数的方法Derived (or any super class of it), something that extends Base.

现在你的两个方法都有资格获得第一个参数.根据第二个参数something that extends base,两者都符合条件,因为某些东西可以派生或扩展派生或扩展集合.这就是Java8抛出错误的原因.

根据Java7,它的类型推断有点弱.所以它尝试做类似的事情,

Base base = new Consumer().get();
set(new Derived(), base);
Run Code Online (Sandbox Code Playgroud)

而且,它找不到合适的方法Derived, Base作为参数.因此它会抛出错误,但出于不同的原因:

    set(new Derived(), new Consumer().get());
    ^
method Consumer.set(Base,Derived) is not applicable
  (argument mismatch; Base cannot be converted to Derived)
method Consumer.set(Derived,Collection<? extends Consumer>) is not applicabl e
  (argument mismatch; Base cannot be converted to Collection<? extends Consu mer>)
Run Code Online (Sandbox Code Playgroud)

PS:感谢Holger,指出我答案的不完整性.

  • 实际上,Java 8编译器并不那么聪明.事实上,`new Consumer().<NewDerived> get()`可以毫无问题地编译.提问者的编译错误并没有说明未经检查的操作有多么错误,它只是说方法调用是*模糊*.这意味着删除两个`set`方法中的一个(无论哪一个!)将"修复"编译器错误,即使结果是将调用错误的方法(期望`Collection`).或者,好吧,*尝试*,因为从'Derived`到`Collection`的演员表将失败. (2认同)
  • 顺便说一句.Java 7编译器不提供警告,它给出*错误*.只有输出中的第一行是警告. (2认同)