Search code examples
javagenericsjava-8type-erasurefunctional-interface

Generic FunctionalInterface and Method Reference messed up by Type Erasure


I have the following generic FunctionalInterface:

@FunctionalInterface
public interface FooInterface<T> {
    void bar(T arg);
}

And this ArrayList descendant:

public class FooList<T> extends ArrayList<FooInterface<T>> {
    public void doFoo(T arg) {
        for(Iterator<FooInterface<T>> i = iterator(); i.hasNext(); ) {
            i.next().bar(arg);
        }
    }
}

Now, I write this code using method references and type erasure:

protected void doFoo(Object arg) { }

private void doStuff() {                                         
    FooInterface f = this::doFoo; 

    List<FooInterface> list = new ArrayList<>();
    list.add(f2);                 
    list.add(this::doFoo);        

    FooList list2 = new FooList();
    list2.add(f2);                
    list2.add(this::doFoo);    // <-- Compiler chokes here, complaining that this is not a FunctionalInterface
}                                        

This baffles me. Why would the compiler be fine with me assigning this::doFoo to a FooInterface variable, and calling List.add() in the first part of the code, only to reject calling the same add() method from the class that descends from ArrayList?

Seems like something funky is going on with type erasure in my descendant class, but what? Is this a bug? Have I done something not supported?


Solution

  • FooList (without a type argument) is called a raw type. 4.8. Raw Types says this:

    The superclasses (respectively, superinterfaces) of a raw type are the erasures of the superclasses (superinterfaces) of any of the parameterizations of the generic type.

    This means that a raw FooList is just a raw ArrayList and the method add accepts Object.

    Since Object is not a functional interface, it cannot be the target of a lambda. This would not work either:

    Object f = this::doFoo;
    

    The full compiler error more or less corroborates all this:

    error: no suitable method found for add(this::doFoo)
        list2.add(this::doFoo);    // <-- Compiler chokes here, complaining that this is not a FunctionalInterface
             ^
        method Collection.add(Object) is not applicable
          (argument mismatch; Object is not a functional interface)

    One way to "fix" it is by doing something tricky like the following:

    public class FooList<T> extends ArrayList<FooInterface<T>> {
        @Override
        public boolean add(FooInterface<T> e) {
            return super.add(e);
        }
        ...
    }
    

    Really the solution here is to not use raw types but since you mention 'erasure' it would seem you are aware of this to some extent. There's no reason to use raw types.