为什么实现这个通用接口会产生不明确的引用?

Jef*_*rey 7 java eclipse generics javac ambiguous

假设我有以下内容:

public interface Filter<E> {
     public boolean accept(E obj);
}
Run Code Online (Sandbox Code Playgroud)

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

就目前而言,您可以使用javac进行编译CombiningFileFilter.但是,如果你还决定实施Filter<File>CombiningFileFilter,你会得到以下错误:

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
Run Code Online (Sandbox Code Playgroud)

但是,如果我做第三节课:

import java.io.File;

public abstract class AnotherFileFilter extends CombiningFileFilter implements
        Filter<File> {
}
Run Code Online (Sandbox Code Playgroud)

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

public interface Filter {
    public boolean accept(File obj);
}
Run Code Online (Sandbox Code Playgroud)

为什么编译器无法弄清楚自从类实现以来Filter<File>,该accept方法实际上应该accept(File)是没有歧义?另外,为什么这个错误只发生在javac上?(它与Eclipse的编译器一起工作正常.)

/ edit
这个编译器问题的一个更简洁的解决方法比创建第三个类将是添加public abstract boolean accept(File)方法CombiningFileFilter.这消除了模糊性.

/ e2
我使用的是JDK 1.7.0_02.

mer*_*ike 9

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

让我们C成为一个带有形式类型参数的类或接口声明A1,...,An,并让C<T1,...,Tn>它们调用C,其中,对于1in,Ti是类型(而不是通配符).然后:

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

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

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

CombiningFilterFilter 继承这两种方法.

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

一个类可以使用override-equivalent(§8.4.2)签名继承多个方法.

如果类C继承了一个具体方法,那么这是一个编译时错误,该方法的签名是C继承的另一个具体方法的子签名.

(这不适用,因为这两种方法都不具体.)

否则,有两种可能的情况:

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

因此,只有当其中一个是具体的时,才会将覆盖等效的继承方法"合并"到一个方法中,如果它们都是抽象的,则它们保持独立,因此所有这些方法都是可访问的,并且可以应用于方法调用.

Java语言规范定义了将要发生的事情,如下所示:

如果多个成员方法都可访问并适用于方法调用,则必须选择一个为运行时方法调度提供描述符.Java编程语言使用选择最具体方法的规则.

非正式的直觉是,如果第一个方法处理的任何调用都可以传递给另一个没有编译时类型错误的调用,那么一个方法比另一个方法更具体.

然后它正式定义更具体.我会饶恕您的定义,但值得注意的是,更具体的不是偏序,因为每种方法都比其本身更具体.然后写道:

当且仅当m1 比m2 更具特异性且m2 不比m1 更具特异性时,方法m1 严格地比另一方法m2 更具特异性.

因此,在我们的情况下,我们有几个具有相同签名的方法,每个方法都比另一个更具体,但两者都没有严格比另一个更具体.

如果方法可访问且适用,并且没有其他适用且可访问的方法严格更具体,则称该方法对于方法调用是最大特定的.

所以在我们的例子中,所有继承的accept方法都是最具体的.

如果只有一个最大特定方法,那么该方法实际上是最具体的方法; 它必须比适用的任何其他可访问方法更具体.然后进行一些进一步的编译时检查,如第15.12.3节所述.

可悲的是,这不是这种情况.

可能没有方法是最具体的,因为有两种或更多种方法是最具体的.在这种情况下:

  • 如果所有最大特定方法都具有覆盖等效(§8.4.2)签名,则:
    • 如果其中一个最大特定方法未被声明为抽象,则它是最具体的方法.
    • 否则,如果所有最大特定方法都被声明为抽象,并且所有最大特定方法的签名具有相同的擦除(第4.6节),则在具有最大特定方法的子集中任意选择最具体的方法.最具体的退货类型.但是,当且仅当在每个最大特定方法的throws子句中声明该异常或其擦除时,才会考虑使用最具体的方法抛出已检查的异常.
  • 否则,我们说方法调用是不明确的,并且发生编译时错误.

最后,这是重点:所有继承的方法都具有相同的,因此覆盖等效的签名.但是,从通用接口继承的方法Filter与其他接口没有相同的擦除.

因此,

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

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

总而言之,javac的行为虽然远非直观,但却受Java语言规范的约束,而eclipse未能通过兼容性测试.