views:

151

answers:

3

Let's say I have this base class:

abstract public class Base {
    abstract public Map save();
    abstract public void load(Map data);
}

to my surprise I could do this in a derived class:

public class Derived extends Base {
    @Override
    public Map<String, String> save() {    //Works
        ...
    }
    ...
}

but I couldn't do this:

public class Derived extends Base {
    @Override
    public void load(Map<String, String> data) {    // Fails
        ...
    }
    ...
}

What is happening here? Why can I use a specialized return type but not a specialized parameter type?

What's even more confusing is that if I keep the original declaration of load, I can just assign it to the more special type:

public class Derived extends Base {
    @Override
    public void load(Map data) {
        Map<String, String> myData = data;   // Works without further casting
        ...
    }
    ...
}
+2  A: 

You can't change the parameters to load(Map<String, String> data) because you could easily violate this if you used the base class instead of the specialized class like so:

Base base = new Derived()
base.load(new HashMap<Integer, Integer>());

This would call the load-method of derived but in violation to the declaration. Because generics are only checked at compile-time it wouldn't be possible to detect that error.

Return values are no problem as long as they are more specialized in subclasses than in superclasses.

Benedikt Eger
+9  A: 

There's an implicit conversion from the specialized type to the raw type - that's always "safe" because someone using the raw type can't make any assumptions. So someone expecting to get a raw Map back from a method doesn't mind if they get a Map<String, String>.

There isn't an implicit conversion from the raw type to the specialized type - if someone passes a raw Map into load it may have non-string keys and values. That's completely legal by the base type declaration of load.

Leaving generics aside, your methods are a bit like this:

public abstract class Base
{
    public abstract Object save();
    public abstract void load(Object x);
}

public class Derived extends Base
{
    @Override
    public String save() { ... } // Valid

    @Override
    public void load(String x) // Not valid
}

With the generics removed, is it clear why the save call is okay here, but the load call isn't? Consider this:

Base b = new Derived();
Object x = b.save(); // Fine - it might return a string
b.load (new Integer(0)); // Has to compile - but the override wouldn't work!
Jon Skeet
Downvoters... please leave comments.
Jon Skeet
A: 

I believe the issue is that in general it is possible to have two methods that discriminated by argument types. Precisely one needs to be selected. This requires looking at unerased types. This means that the two load methods are distinguishable. So we have distinguishable methods but in erased form only one method exists.

This is not a problem with return types because (in Java) you can have covariant return types but not overloading by return types.

Tom Hawtin - tackline
Would the issue I gave in my answer also not be a problem though?
Jon Skeet
It's irrelevant.
Tom Hawtin - tackline