为什么实现这个泛型接口会创建一个不明确的引用



假设我有以下内容:

public interface Filter<E> {
public boolean accept(E obj);
}

import java.io.File;
import java.io.FilenameFilter;
public abstract class CombiningFileFilter extends javax.swing.filechooser.FileFilter
implements java.io.FileFilter, FilenameFilter {
@Override
public boolean accept(File dir, String name) {
return accept(new File(dir, name));
}
}

目前,您可以使用javac来编译CombiningFileFilter。但是,如果您还决定在CombiningFileFilter中实现Filter<File>,则会得到以下错误:

CombiningFileFilter.java:9: error: reference to accept is ambiguous, 
both method accept(File) in FileFilter and method accept(E) in Filter match
return accept(new File(dir, name));
^
where E is a type-variable:
E extends Object declared in interface Filter
1 error

然而,如果我创建第三个类:

import java.io.File;
public abstract class AnotherFileFilter extends CombiningFileFilter implements
Filter<File> {
}

不再有编译错误。如果Filter不是通用的,编译错误也会消失:

public interface Filter {
public boolean accept(File obj);
}

为什么编译器不能看出,既然类实现了Filter<File>,那么accept方法实际上应该是accept(File),并且没有歧义?另外,为什么这个错误只发生在javac上?(它可以很好地与Eclipse的编译器一起工作。)

/edit
对于这个编译器问题,一个比创建第三个类更干净的解决方案是在CombiningFileFilter中添加public abstract boolean accept(File)方法。这就消除了歧义。

/e2
我使用JDK 1.7.0_02.

据我所知,编译错误是由Java语言规范强制规定的,它写道:

C为具有正式类型参数A1,...,An的类或接口声明,设C<T1,...,Tn>C的调用,其中,对于1in, Ti是类型(而不是通配符)。然后:

  • 设m为C语言中的成员或构造函数声明,其声明的类型为t,则类型C<T1,...,Tn>中的m(§8.2,§8.8.6)的类型为T[A1 := T1, ..., An := Tn]
  • 设m为D中的成员或构造函数声明,其中D为C扩展的类或C实现的接口。设D<U1,...,Uk>为与D对应的C<T1,...,Tn>的超类型,则C<T1,...,Tn>中的m的类型就是D<U1,...,Uk>中的m的类型。

如果参数化类型的任何类型参数都是通配符,则其成员和构造函数的类型未定义。

也就是说,Filter<File>声明的方法的类型是boolean accept(File)FileFilter也声明了一个方法boolean accept(File)

CombiningFilterFilter继承了这两个方法。

那是什么意思?Java语言规范写道:

一个类可以继承多个具有覆盖等效(§8.4.2)签名的方法。

如果类C继承的具体方法的签名是C继承的另一个具体方法的子签名,则会产生编译时错误。

(这并不适用,因为这两种方法都是具体的。)

否则有两种可能的情况:

  • 如果其中一个继承的方法不是抽象的,那么有两个子案例:
    • 如果非抽象方法是静态的,则会发生编译时错误。
    • 否则,非抽象的方法被认为是覆盖的,因此要实现代表继承它的类的所有其他方法。如果非抽象方法的签名不是其他继承方法的子签名,则必须发出未检查的警告(除非被抑制(§9.6.1.5))。如果非抽象方法的返回类型不是其他继承方法的可替换返回类型(第8.4.5节),也会发生编译时错误。如果非抽象方法的返回类型不是任何其他继承方法的返回类型的子类型,则必须发出未检查的警告。此外,如果继承的非抽象方法有一个与任何其他继承方法冲突的抛出子句(§8.4.6),则会发生编译时错误。
  • 如果所有继承的方法都是抽象的,那么该类必然是一个抽象类,并且被认为继承了所有的抽象方法。如果对于任何两个这样的继承方法,其中一个方法的返回类型不能替代另一个方法,则会发生编译时错误(在这种情况下,抛出子句不会导致错误)。

所以"合并"只有当其中一个是具体的继承方法时,才会发生重写等效继承方法到一个方法的情况,如果所有继承方法都是抽象的,则它们保持分离,因此所有继承方法都可以访问并适用于方法调用。

Java语言规范定义了接下来要发生的事情:

如果不止一个成员方法既可访问又适用于方法调用,则有必要选择一个来为运行时方法分派提供描述符。Java编程语言使用的规则是选择最具体的方法。

非正式的直觉是,如果由第一个方法处理的任何调用可以传递给另一个方法而不会出现编译时类型错误,则一个方法比另一个方法更特定。

然后,它正式定义了更具体的。我将省去定义,但值得注意的是,更具体的不是偏序,因为每个方法比其本身更具体。然后写入:

方法m1严格地比另一个方法m2更专一当且仅当m1比m2更专一且m2不比m1更专一

所以在我们的例子中,我们有几个具有相同签名的方法,每个都比另一个更具体,但是都没有严格地比另一个更具体。

如果一个方法是可访问和适用的,并且没有其他适用和可访问的方法是严格特定的,则该方法被称为最特定于方法调用的方法。

所以在我们的例子中,所有继承的accept方法都是最大特异性

如果只有一个最特定的方法,那么这个方法实际上是最特定的方法;它必然比任何其他适用的可访问方法更具体。然后,按照§15.12.3的描述,对它进行进一步的编译时检查。

遗憾的是,这里的情况并非如此。

可能没有方法是最特定的,因为有两个或更多的方法是最大特定的。在本例中:

  • 如果所有的最大化特定方法都有覆盖等效(§8.4.2)签名,那么:
    • 如果其中一个最具体的方法没有被声明为抽象,那么它就是最具体的方法。
    • 否则,如果所有的最大特定方法都被声明为抽象的,并且所有最大特定方法的签名都有相同的擦除(§4.6),那么最特定的方法是在具有最特定返回类型的最大特定方法的子集中任意选择的。然而,当且仅当该异常或其擦除在每个最特定方法的抛出子句中声明时,最特定的方法被认为抛出检查异常。
  • 否则,我们说方法调用是不明确的,并且发生编译时错误。

最后,这是最重要的一点:所有继承的方法都有相同的,因此覆盖等效的签名。但是,从泛型接口Filter继承的方法不具有与其他方法相同的擦除功能。

因此,

  1. 第一个例子可以编译,因为所有的方法都是抽象的,覆盖等效的,并且具有相同的擦除。
  2. 第二个例子不能编译,因为所有的方法都是抽象的,重写是等价的,但是它们的擦除是不一样的。第三个示例可以编译,因为所有候选方法都是抽象的,覆盖等效的,并且具有相同的擦除。(具有不同擦除的方法在子类中声明,因此不是候选方法)
  3. 第四个示例可以编译,因为所有方法都是抽象的,覆盖等效的,并且具有相同的擦除。
  4. 最后一个示例(在CombiningFileFilter中重复抽象方法)将编译,因为该方法与所有继承的accept方法重写等效,因此重写它们(注意,重写不需要相同的擦除!)因此,只有一个可应用且可访问的方法,因此是最具体的方法。

我只能推测为什么规范除了覆盖等价之外还需要相同的擦除。这可能是因为,为了保持与非泛型代码的向后兼容性,当方法声明引用类型参数时,编译器需要发出带有擦除签名的合成方法。在这个被删除的世界中,编译器可以使用什么方法作为方法调用表达式的目标?Java语言规范通过要求提供一个合适的、共享的、擦除的方法声明来回避这个问题。

总而言之,javac的行为虽然远非直观,但却是由Java语言规范强制要求的,eclipse无法通过兼容性测试。

FileFilter接口中有一个方法与您的具体接口Filter<File>具有相同的签名。它们的签名都是accept(File f)

这是一个模棱两可的引用,因为编译器无法知道在覆盖的accept(File f, String name )方法调用中调用这些方法中的哪一个。

相关内容

最新更新