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

混江龙づ霸主 提交于 2019-12-06 05:46:00
Sotirios Delimanolis

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.

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.

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

BevynQ

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!