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.
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 parametersA1,...,An
, and letC<T1,...,Tn>
be an invocation ofC
, 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>
, isT[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 ofC<T1,...,Tn>
that corresponds to D. Then the type of m inC<T1,...,Tn>
is the type of m inD<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,
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With