xtrakBandit
xtrakBandit

Reputation: 133

Generic FunctionalInterface and type erasure

What does the following compilation error mean?

The method apply(Sample.Foo, capture#5-of ?) in the type FooSetter is not applicable for the arguments (Sample.Foo, capture#6-of ?)

It occurs in the code below (at the bottom where I've indicated with a comment) which tries to map setters of one bean to the getters of another bean without casting (yes I know there are utilities to do this but this is an example). I use the following two functional interfaces

@FunctionalInterface
public interface FooSetter<V> {
    void apply(Foo foo, V value);
} 


@FunctionalInterface
public interface BarGetter<T> {
   T apply(Bar from); 
}


public class Sample {

public static class Bar{
    public String description;
    public Integer num;

    public String getDecription(){
        return description;
    }
    public void setDescription(String desc){
        this.description = desc;
    }
    public Integer getNum() {
        return num;
    }
    public void setNum(int num) {
        this.num = num;
    }
}

public static class Foo{

    private String name;
    private Integer age;

    public Foo setName(String name){
        this.name = name;
        return this;
    }

    public Foo setAge(int age){
        this.age = age;
        return this;
    }

    @Override
    public String toString(){
        return "Name" + name + "  Age " + age;
    }
}

public static void main(String[] args) throws Exception  {

    HashMap<String, BarGetter<?>> barGetters= Maps.newHashMap();
    barGetters.put("description", (BarGetter<String>)(Bar b) -> b.getDecription());
    barGetters.put("num", (BarGetter<Integer>)(Bar b) -> b.getNum());

    HashMap<String, FooSetter<?>> fooSetters= Maps.newHashMap();
    fooSetters.put("name",   (Foo f, String name) -> f.setName(name));
    fooSetters.put("age",   (Foo f, Integer age) -> f.setAge(age));

    HashMap<String,String> fooFromBar = Maps.newHashMap();
                 // FOO      BAR
    fooFromBar.put("name", "description");
    fooFromBar.put("age", "num");

    Bar bar  = new Bar();
    bar.setDescription("bar desc");
    bar.setNum(5);

    Foo newFoo = new Foo();
    for (String key : fooFromBar.keySet()) {
       //COMPILATION ERROR  
       fooSetters.get(key).apply(newFoo, barGetters.get( fooFromBar.get(key) ).apply(bar) );
    }
 }
}

Is there something I'm not seeing here with respect to type erasure?

Upvotes: 0

Views: 293

Answers (2)

Holger
Holger

Reputation: 298153

If you have a collection of differently-typed generic elements, you need a helper class which wraps the actual element and guard it’s access with a runtime-check which replaces the compile-time check that doesn’t work in this case.

E.g.

final class FooSetterHolder<T> {
    final Class<T> guard;
    final FooSetter<T> setter;

    FooSetterHolder(Class<T> guard, FooSetter<T> setter) {
        this.guard = guard;
        this.setter = setter;
    }
    <U> FooSetterHolder<U> cast(Class<U> expectedType) {
        if(guard != expectedType) throw new ClassCastException(
            "cannot cast FooSetterHolder<"+guard.getName()
           +"> to FooSetterHolder<"+expectedType.getName()+'>');

        // now that we have checked the type
        @SuppressWarnings("unchecked") FooSetterHolder<U> h=(FooSetterHolder)this;

        return h;
    }
}

You can use this helper class in the following way:

HashMap<String, FooSetterHolder<?>> fooSetters= new HashMap<>();
fooSetters.put("name", new FooSetterHolder<>(String.class, Foo::setName));
fooSetters.put("age", new FooSetterHolder<>(Integer.class, Foo::setAge));

FooSetter<String> nameSetter=fooSetters.get("name").cast(String.class).setter;
FooSetter<Integer> ageSetter=fooSetters.get("age").cast(Integer.class).setter;

The cast method provides an operation which uses the unavoidable unchecked operation internally but performs a runtime comparison of the expected and actual type so that the code using it is type safe (assuming that there are no other unchecked operations).

So the following code won’t be rejected by the compiler

FooSetter<Integer> wrong=fooSetters.get("name").cast(Integer.class).setter;

but doesn’t create heap pollution but rather throws a ClassCastException with the message “cannot cast FooSetterHolder<java.lang.String> to FooSetterHolder<java.lang.Integer>” very similar to ordinary, non-generic type casts.

Upvotes: 1

Sotirios Delimanolis
Sotirios Delimanolis

Reputation: 279940

This

fooSetters.get(key)

returns a value of type FooSetter<?>, ie. a FooSetter that doesn't know about the type it sets. Its apply method therefore has the following (inferred) definition

void apply(Foo foo, ? value);

You can't use this method with anything (for its second parameter) but null (which can be used for any reference type) since you don't know what type it is.

This

barGetters.get( fooFromBar.get(key) )

returns a value of type BarGetter<?>, ie a BarGetter that doesn't know what it's getting. Its apply method therefore appears as

? apply(Bar from);

This ? can be something completely different than what fooSetter.apply is expecting.

The type system can therefore not allow it and gives you a compilation error.

Upvotes: 2

Related Questions