Whimusical
Whimusical

Reputation: 6629

Why can't I extend an interface "generic method" and narrow its type to my inherited interface "class generic"?

I show an example of what I mean which, is easier. Imagine the generic type C means Color type: So for visual simplification assume C is C extends Color

interface Screen {
   <C> Background<C> render(Plane<C> plane);
}

interface MonochromeScreen<C> extends Screen{
       @Override
       Background<C> render(Plane<C> plane);  
}

This would throw a name clash compilation error explaining that both have the same type erasure but are not overridable.

But I cannot understand why we could not simply allow overriding the signature as long as it is more restrictive. I mean, after all, the only difference is the scope of the generic type, in Screen is method-wide and in MonochromeScreen is class-wide.

It would not make sense to allow a child method to override as a method-scoped generic when its parent enforces coherence at class level, but I think it does otherwise: My parent interface could have 20 methods with unrelated generic types, but my child class would force them all to be the same as a non-incompatible extra specification/contract (which is what any extended interface does), After all, a monochrome sccreen is still an screen, as it can be painted with any color, I am just enforcing that color, whichever it is, to be it consistent accross the other functions of the child, Just narrowing the possibilities at class level, not method level.

Is there any fundamentally wrong assumption for considering the feature?

EDIT: I accepted Sotirios Delimanolis answer for him spotted the right trouble very cleverly and I was not asking for a solution, but for those who want to know how to overcome the situation there is a trick explained in my own answered answer

Upvotes: 3

Views: 1175

Answers (4)

BevynQ
BevynQ

Reputation: 8259

I do not think your code is what you want, which is why you are getting errors.

I think this is what you want

public interface Screen<C> {
    Background<C> render(Plane<C> plane);
}

and

public interface MonochromeScreen<C> extends Screen<C> {

  Background<C> render(Plane<C> plane);
}

What you may be mistaken in thinking is that because <C> is both interfaces it is the same thing. It is not.

this

public interface MonochromeScreen<HI> extends Screen<HI> {

  Background<HI> render(Plane<HI> plane);
}

is exactly the same as the code above. C and HI are just names for the generic placeholders. by extending Screen<C> with extends Screen<HI> we tell java that C is the same placeHolder as HI so it will do it's magic.

In your code

<C> Background<C> render(Plane<C> plane);

we have declared a brand new place holder that only has context in that method. so we could write this code

MonochromeScreen<String> ms;
ms.render(new Plane<Banana>());

<C> Background<C> render(Plane<C> plane);

is redefined as

Background<Banana> render(Plane<Banana> plane);

but

Background<C> render(Plane<C> plane);

is redefined as

Background<String> render(Plane<String> plane);

which conflict and so java gives you an error.

Upvotes: 0

Whimusical
Whimusical

Reputation: 6629

FYI: That is the only way I found to solve the case and pass the overriding of the method (If the design pattern has a name, I'd appreciate to know ir! It's in the end the way to extend an interface with generic methods to make it class-generic. And still you can type it with the parent type (aka Color) to use it as the raw general type old interface.):

Screen.java

public interface Screen {
    public interface Color {}
    public class Red  implements Color {}
    public class Blue implements Color {}

    static Screen getScreen(){
        return new Screen(){};
    }
    default <C extends Color> Background<C> render(Plane<C> plane){
        return new Background<C>(plane.getColor());
    }
}

MonochromeScreen.java

interface MonochromeScreen<C extends Color> extends Screen{

    static <C extends Color> MonochromeScreen<C> getScreen(final Class<C> colorClass){
        return new MonochromeScreen<C>(){
            @Override public Class<C> getColor() { return colorClass; };
        };
    }

    public Class<C> getColor();

    @Override
    @SuppressWarnings("unchecked")
    default Background<C> render(@SuppressWarnings("rawtypes") Plane plane){
        try{
            C planeColor = (C) this.getColor().cast(plane.getColor());
            return new Background<C>(planeColor);
        } catch (ClassCastException e){
            throw new UnsupportedOperationException("Current screen implementation is based in mono color '" 
                                                   + this.getColor().getSimpleName() + "' but was asked to render a '"
                                                   + plane.getColor().getClass().getSimpleName() + "' colored plane" );
        }
    }
}

Plane.java

public class Plane<C extends Color> {   
    private final C color;
    public Plane(C color) {this.color = color;}
    public C getColor()   {return this.color;}
}

Background.java

public class Background<C extends Color> {  
    private final C color;
    public Background(C color) {this.color = color;}
    public C getColor()        {return this.color;}
}

MainTest.java

public class MainTest<C> {

    public static void main(String[] args) {

        Plane<Red> redPlane   = new Plane<>(new Red());
        Plane<Blue> bluePlane = new Plane<>(new Blue());

        Screen coloredScreen = Screen.getScreen();
        MonochromeScreen<Red> redMonoScreen = MonochromeScreen.getScreen(Red.class);
        MonochromeScreen<Color> xMonoScreen = MonochromeScreen.getScreen(Color.class);

        Screen redScreenAsScreen = (Screen) redMonoScreen;

        coloredScreen.render(redPlane);
        coloredScreen.render(bluePlane);
        redMonoScreen.render(redPlane);
        //redMonoScreen.render(bluePlane); --> This throws UnsupportedOperationException*
        redScreenAsScreen.render(redPlane);
        //redScreenAsScreen.render(bluePlane); --> This throws UnsupportedOperationException*
        xMonoScreen.render(new Plane<>(new Color(){})); //--> And still I can define a Monochrome screen as of type Color so  
        System.out.println("Test Finished!");           //still would have a wildcard to make it work as a raw screen (not useful 
                                                        //in my physical model but it is in other abstract models where this problem arises

    } 
}
  • Exception thrown when adding blue plane in redScreen:

    java.lang.UnsupportedOperationException: Current screen implementation is based in mono color 'Red' but was asked to render a 'Blue' colored plane

Upvotes: 0

biziclop
biziclop

Reputation: 49724

The reason this is not allowed is that it violates the Liskov substitution principle.

interface Screen {
   <C> Background<C> render(Plane<C> plane);
}

What this means is that you can call render() at any time with an arbitrary type as C.

You can do this for example:

Screen s = ...;
Background<Red> b1 = s.render(new Plane<Red>());
Background<Blue> b2 = s.render(new Plane<Blue>());

Now if we look at MonochromeScreen:

interface MonochromeScreen<C> extends Screen{
   Background<C> render(Plane<C> plane);  
}

What this declaration says is: you must choose exactly one type as C when you create an instance of this object and you can only use that for the whole life of that object.

MonochromeScreen<Red> s = ...;
Background<Red>  b1 = s.render(new Plane<Red>());
Background<Blue> b2 = s.render(new Plane<Blue>()); // this won't compile because you declared that s only works with Red type.

Therefore it follows that Screen s = new MonochromeScreen<Red>(); is not a valid cast, MonochromeScreen cannot be a subclass of Screen.


Okay, let's turn this around a bit. Let's assume that all colors are instances of a single Color class and not separate classes. What would our code look like then?

interface Plane {
    Color getColor();
}

interface Background {
    Color getColor();
}

interface Screen {
   Background render(Plane plane);
}

So far, so good. Now we define a monochrome screen:

class MonochromeScreen implements Screen {
    private final Color color; // this is the only colour we have
    public Background render(Plane plane) {
        if (!plane.getColor().equals(color))
           throw new IllegalArgumentException( "I can't render this colour.");
        return new Background() {...}; 
    }
}

This would compile fine and would have more or less the same semantics.

The question is: would this be good code? After all, you can still do this:

public void renderPrimaryPlanes(Screen s) { //this looks like a good method
    s.render(new Plane(Color.RED));
    s.render(new Plane(Color.GREEN));
    s.render(new Plane(Color.BLUE));
}

...
Screen s = new MonochromeScreen(Color.RED);
renderPrimaryPlanes(s); //this would throw an IAE

Well, no. That's definitely not what you'd expect from an innocent renderPrimaryPlanes() method. Things would break in unexpected ways. Why is that?

It's because despite it being formally valid and compileable, this code too breaks the LSP in exactly the same way the original did. The problem is not with the language but with the model: the entity you called Screen can do more things than the one called MonochromeScreen, therefore it can't be a superclass of it.

Upvotes: 3

Sotirios Delimanolis
Sotirios Delimanolis

Reputation: 279920

Here's where this breaks:

MonochromeScreen<Red> redScreen = ...;
Screen redScreenJustAScreen = redScreen;
Plane<Blue> bluePlane = null;
redScreenJustAScreen.<Blue>render(bluePlane);

If what you suggested worked at compile time, the snippet above would presumably have to fail at runtime with a ClassCastException because the object referenced by redScreenJustAScreen expects a Plane<Red> but received a Plane<Blue>.

Generics, used appropriately, are supposed to prevent the above from happening. If such overriding rules were allowed, generics would fail.

I don't know enough about your use case, but it doesn't seem like generics are really needed.

Upvotes: 4

Related Questions