7

假设我有以下内容:

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. 但是,如果您还决定实现Filter<File>in 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

但是,如果我参加第三节课:

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
对于这个编译器问题,比创建第三个类更简洁的解决方法是将public abstract boolean accept(File)方法添加到CombiningFileFilter. 这消除了歧义。

/e2
我使用的是 JDK 1.7.0_02。

4

2 回答 2

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 实现的接口。令m 为对应于 DD<U1,...,Uk>的超类型。那么 m in的类型就是 m in 的类型。C<T1,...,Tn>C<T1,...,Tn>D<U1,...,Uk>

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

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

CombiningFilterFilter继承了这两种方法。

这意味着什么?Java 语言规范写道

一个类可以继承多个具有重写等效(第 8.4.2 节)签名的方法。

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

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

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

  • 如果继承的方法之一不是抽象的,则有两种子情况:
    • 如果非抽象方法是静态的,则会发生编译时错误。
    • 否则,非抽象的方法被认为覆盖并因此代表继承它的类实现所有其他方法。如果非抽象方法的签名不是每个其他继承方法的子签名,则必须发出未经检查的警告(除非被抑制(§9.6.1.5))。如果非抽象方法的返回类型对于每个其他继承的方法都不是可替换的返回类型(第 8.4.5 节),也会发生编译时错误。如果非抽象方法的返回类型不是任何其他继承方法的返回类型的子类型,则必须发出未经检查的警告。此外,如果非抽象继承方法的 throws 子句与任何其他继承方法的 throws 子句冲突(第 8.4.6 节),则会发生编译时错误。
  • 如果所有继承的方法都是抽象的,那么该类必然是抽象类,被认为继承了所有的抽象方法。如果对于任何两个这样的继承方法,其中一个方法的返回类型不可替代另一个,则会发生编译时错误(在这种情况下,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 未能通过兼容性测试。

于 2012-01-23T03:45:39.733 回答
1

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

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

于 2012-01-23T01:56:48.757 回答