7

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.

Jeffrey
  • 44,417
  • 8
  • 90
  • 141
  • @HunterMcMillen You still get the error – Jeffrey Jan 23 '12 at 01:49
  • There is an interface collision between `javax.swing.filechooser.FileFilter#accept(File)` and your own `Filter#accept(File)`. See here for a detailed explanation: http://stackoverflow.com/a/2832651/406984 – earldouglas Jan 23 '12 at 02:01
  • @James That answer supports that this example should be compileable as all of the `accept` methods *should* have the same signature. – Jeffrey Jan 23 '12 at 02:12
  • 1
    Except that when the run-time dispatcher is choosing a method to call for a given method invocation it tries to pick the MOST specific method that it can find to match the invocation, if it is choosing from `accept(File)` and `accept(File)` Which one will it choose? – Hunter McMillen Jan 23 '12 at 02:14
  • @HunterMcMillen James' link explains that point. `If a type implements two interfaces, and each interface define a method that has identical signature, then in effect there is only one method` – Jeffrey Jan 23 '12 at 02:16
  • That is only the case when an interface inherits from the two interfaces with the same method signature. That doesn't seem to be the case here. – Hunter McMillen Jan 23 '12 at 02:17
  • @HunterMcMillen That's exactly my question: Why? Why doesn't the compiler recognize that the signature of `Filter` should be `accept(File)` instead of `accept(E)` and why does this compile with Eclipse but not javac? (I know that Eclipse has its own compiler, what did they do differently to overcome this?) – Jeffrey Jan 23 '12 at 02:20
  • @Jeffrey hmm, no idea. Sorry. – Hunter McMillen Jan 23 '12 at 02:24
  • I actually don't get the compile error with my 1.6.0_26 JDK. What version of the JDK are you using to compile? Also, here's a link that will shed a bit of light on the topic: http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#Why%20doesn%27t%20method%20overriding%20work%20as%20I%20expect%20it? – Mark Peters Jan 23 '12 at 04:42
  • @MarkPeters I'm using 1.7.0_02 – Jeffrey Jan 23 '12 at 04:43
  • @Jeffrey: Whoops, false alarm. IntelliJ wasn't properly building at the time so the error wasn't showing up. I do indeed get the same error. Also, StackOverflow mangled the link; [here it is again](http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ051) – Mark Peters Jan 23 '12 at 04:48
  • @MarkPeters If I take that link for granted it would explain why `CombiningFileFilter` fails to compile (if the `accept(E)` method was overloading instead of overriding). But then why is `AnotherFileFilter` compiling? Shouldn't it run into the same pitfall? – Jeffrey Jan 23 '12 at 05:15
  • @Jeffrey: I'm as baffled as you are on this; I was just trying to expand the list of terms you could Google for :-). – Mark Peters Jan 23 '12 at 05:21
  • Please post answers, even partial ones, as answers rather than comments, so they can be voted and commented on. It'll get real messy if we start discussing answers here. For the record, I think the FAQ example MarkPeters linked to is different, as the type parameter in question is declared on the class, and hence its value not fixed as in the example from this question. – meriton Jan 23 '12 at 19:42

2 Answers2

9

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.

Community
  • 1
  • 1
meriton
  • 68,356
  • 14
  • 108
  • 175
  • This answers the question as to why the original doesn't compile. I assume the Eclipse compiler either combines all of the methods into one or just arbitrarily chooses one because as you said, they're all merged together in the concrete class anyway. – Jeffrey Jan 23 '12 at 04:11
  • Wait, shouldn't this mean that `CombiningFileFilter extends javax.swing.filechooser.FileFilter implements java.io.FileFilter, FilenameFilter` shouldn't compile either? Both `java.io.FileFilter` and `javax.swing.filechooser.FileFilter` have abstract methods with the signature `boolean accept(File)`. – Jeffrey Jan 23 '12 at 04:19
  • Digging deeper, I finally found the paragraph that explains it all, and have revised the answer accordingly. – meriton Jan 23 '12 at 20:52
  • 1
    Simply put, that is one thorough and impressive answer. – rfeak Jan 23 '12 at 20:58
  • @meriton Wow, thanks for sifting through all of the JLS articles. – Jeffrey Jan 23 '12 at 21:04
1

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.

Hunter McMillen
  • 59,865
  • 24
  • 119
  • 170
  • Oops, I forgot to mention that this only happens when the `Filter` interface is generic. If `Filter` has the method `accept(File)` instead of `accept(E)`, there is no longer an ambiguous reference. And according to the compiler, the ambiguous signatures are `accept(File)` and `accept(E)`. Not `accept(File)` and `accept(File)`. – Jeffrey Jan 23 '12 at 02:01