Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does implementing this generic interface create an ambiguous reference?

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.

like image 802
Jeffrey Avatar asked Jan 23 '12 01:01

Jeffrey


2 Answers

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.

like image 142
meriton Avatar answered Nov 08 '22 09:11

meriton


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.

like image 43
Hunter McMillen Avatar answered Nov 08 '22 09:11

Hunter McMillen