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

发布于 2024-12-28 07:30:17 字数 1627 浏览 0 评论 0原文

假设我有以下内容:

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 ,则会出现以下错误:

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,则 accept 方法实际上应该是 accept(File) 并且没有歧义?另外,为什么这个错误只发生在 javac 上? (它与 Eclipse 的编译器一起工作得很好。)

/edit
与创建第三个类相比,解决此编译器问题的更简洁的解决方法是在 CombiningFileFilter 中添加 public abstract boolean Accept(File) 方法。这消除了歧义。

/e2
我使用的是 JDK 1.7.0_02。

Let's say I have the following:

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

and

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));
    }
}

As it stands, you can use javac to compile CombiningFileFilter. But, if you also decide to implement Filter<File> in CombiningFileFilter, you get the following error:

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

However, if I make a third class:

import java.io.File;

public abstract class AnotherFileFilter extends CombiningFileFilter implements
        Filter<File> {
}

There is no longer a compilation error. The compilation error also goes away if Filter isn't generic:

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

Why can't the compiler figure out that since the class implements Filter<File>, the accept method should actually be accept(File) and that there is no ambiguity? Also, why does this error only happen with javac? (It works fine with Eclipse's compiler.)

/edit
A cleaner workaround to this compiler issue than creating the third class would be to add the public abstract boolean accept(File) method in CombiningFileFilter. That erases the ambiguity.

/e2
I am using JDK 1.7.0_02.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

怎会甘心 2025-01-04 07:30:17

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

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

  • 设 m 为 C 中的成员或构造函数声明,其声明的类型为 T。则 m (§8.2, §8.8.6) 的类型为 C类型;,是T[A1 := T1, ..., An := Tn]
  • 设 m 为 D 中的成员或构造函数声明,其中 D 是由 C 扩展的类或由 C 实现的接口。设 D 为超类型的C对应于D。那么C中m的类型就是米在D

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

也就是说,Filter 声明的方法的类型为 boolean Accept(File)FileFilter 还声明了一个方法 boolean Accept(File)

CombiningFilterFilter 继承了这两个方法。

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

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

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

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

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

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

因此重写等效继承的“合并”仅当其中一个方法是具体的时,才会将方法转换为一种方法,如果所有方法都是抽象的,则它们保持独立,因此所有方法都可以访问并适用于方法调用。

Java 语言规范定义什么是然后发生如下:

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

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

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

当且仅当 m1 比 m2 更具体并且 m2 不更具体时,方法 m1 比另一个方法 m2 严格更具体 > 比 m1。

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

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

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

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

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

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

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

最后,这是要点:所有继承的方法都具有相同的、因此覆盖等效的签名。然而,从通用接口Filter继承的方法并不具有与其他方法相同的擦除功能。

因此,

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

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

总而言之,javac 的行为虽然远非直观,但却是 Java 语言规范所规定的,并且 eclipse 未能通过兼容性测试。

As far as I can tell, the compilation error is mandated by the Java Language Specification, which writes:

Let C be a class or interface declaration with formal type parameters A1,...,An, and let C<T1,...,Tn> be an invocation of C, where, for 1in, Ti are types (rather than wildcards). Then:

  • Let m be a member or constructor declaration in C, whose type as declared is T. Then the type of m (§8.2, §8.8.6) in the type C<T1,...,Tn>, is T[A1 := T1, ..., An := Tn].
  • Let m be a member or constructor declaration in D, where D is a class extended by C or an interface implemented by C. Let D<U1,...,Uk> be the supertype of C<T1,...,Tn> that corresponds to D. Then the type of m in C<T1,...,Tn> is the type of m in D<U1,...,Uk>.

If any of the type arguments to a parameterized type are wildcards, the type of its members and constructors is undefined.

That is, the method declared by Filter<File> has type boolean accept(File). FileFilter also declares a method boolean accept(File).

CombiningFilterFilter inherits both these methods.

What does that mean? The Java Language Specification writes:

It is possible for a class to inherit multiple methods with override-equivalent (§8.4.2) signatures.

It is a compile time error if a class C inherits a concrete method whose signatures is a subsignature of another concrete method inherited by C.

(That doesn't apply, as neither method is concrete.)

Otherwise, there are two possible cases:

  • If one of the inherited methods is not abstract, then there are two subcases:
    • If the method that is not abstract is static, a compile-time error occurs.
    • Otherwise, the method that is not abstract is considered to override, and therefore to implement, all the other methods on behalf of the class that inherits it. If the signature of the non-abstract method is not a subsignature of each of the other inherited methods an unchecked warning must be issued (unless suppressed (§9.6.1.5)). A compile-time error also occurs if the return type of the non-abstract method is not return type substitutable (§8.4.5) for each of the other inherited methods. If the return type of the non-abstract method is not a subtype of the return type of any of the other inherited methods, an unchecked warning must be issued. Moreover, a compile-time error occurs if the inherited method that is not abstract has a throws clause that conflicts (§8.4.6) with that of any other of the inherited methods.
  • If all the inherited methods are abstract, then the class is necessarily an abstract class and is considered to inherit all the abstract methods. A compile-time error occurs if, for any two such inherited methods, one of the methods is not return type substitutable for the other (The throws clauses do not cause errors in this case.)

So the "merging" of override-equivalent inherited methods into one method only occurs if one of them is concrete, if all are abstract they remain separate, so all of them are accessible and appliccable to the method invocation.

The Java Language Specification defines what is to happen then as follows:

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time type error.

It then defines more specific formally. I'll spare you the definition, but it is worth noting that more specific is not a partial order, as each method is more specific than itself. It then writes:

A method m1 is strictly more specific than another method m2 if and only if m1 is more specific than m2 and m2 is not more specific than m1.

So in our case, where we have several methods with identical signatures, each is more specific than the other, but neither is strictly more specific than the other.

A method is said to be maximally specific for a method invocation if it is accessible and applicable and there is no other method that is applicable and accessible that is strictly more specific.

So in our case, all inherited accept methods are maximally specific.

If there is exactly one maximally specific method, then that method is in fact the most specific method; it is necessarily more specific than any other accessible method that is applicable. It is then subjected to some further compile-time checks as described in §15.12.3.

Sadly, that's not the case here.

It is possible that no method is the most specific, because there are two or more methods that are maximally specific. In this case:

  • If all the maximally specific methods have override-equivalent (§8.4.2) signatures, then:
    • If exactly one of the maximally specific methods is not declared abstract, it is the most specific method.
    • Otherwise, if all the maximally specific methods are declared abstract, and the signatures of all of the maximally specific methods have the same erasure (§4.6), then the most specific method is chosen arbitrarily among the subset of the maximally specific methods that have the most specific return type. However, the most specific method is considered to throw a checked exception if and only if that exception or its erasure is declared in the throws clauses of each of the maximally specific methods.
  • Otherwise, we say that the method invocation is ambiguous, and a compile-time error occurs.

And that, finally, is the salient point: All inherited methods have identical, and therefore override-equivalent signatures. However, the method inherited from the generic interface Filter doesn't have the same erasure as the other ones.

Therefore,

  1. The first example will compile because all methods are abstract, override-equivalent, and have the same erasure.
  2. The second example will not compile, because all methods are abstract, override-equivalent, but their erasure isn't the same.
  3. The third example will compile, because all candicate methods are abstract, override-equivalent, and have the same erasure. (The method with a different erasure is declared in a subclass, and hence not a candidate)
  4. The fourth example will compile, because all methods are abstract, override-equivalent, and have the same erasure.
  5. The last example (repeat abstract method in CombiningFileFilter) will compile, because that method is override-equivalent with all inherited accept methods, and therefore overrides them (note that same erasure is not required for overriding!). So there is only a single appliccable and accessible method, which is therefore the most-specific one.

I can only speculate why the spec requires same erasures in addition to override-equivalence. It might be because, to retain backwards compatibility with non-generic code, the compiler is required to emit a synthetic method with erased signature when a method declaration refers to type parameters. In this erased world, what method can the compiler use as target for the method invocation expression? The Java Language Specification side-steps this issue by requiring that a suitable, shared, erased method declaration is present.

To conclude, javac's behaviour, though far from intuitive, is mandated by the Java Language Specification, and eclipse fails the compatibility test.

木森分化 2025-01-04 07:30:17

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

这是一个不明确的引用,因为编译器无法知道在重写的 accept(File f, String name ) 方法调用中要调用这些方法中的哪一个。

There is a method in the FileFilter interface that has the same signature as the one from your concrete interface Filter<File>. They both have the signature accept(File f).

It is an ambiguous reference because the compiler has no way of knowing which of these methods to call in your overridden accept(File f, String name ) method call.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文