繁体   English   中英

为什么不能扩展接口“泛型方法”并将其类型缩小到继承的接口“类通用”?

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

我展示了一个我的意思的例子,这很容易。 想象一下,通用类型C表示颜色类型:因此,为了简化视觉,假设C是C扩展颜色

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

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

这将引发名称冲突编译错误,解释为两者具有相同的类型擦除,但不可覆盖。

但是我不明白为什么我们不能简单地允许覆盖签名,只要它更具限制性。 我的意思是,毕竟,唯一的区别是泛型类型的范围,在Screen中是方法范围的,而在MonochromeScreen是类范围的。

当父方法在类级别强制执行一致性时,允许将子方法覆盖为方法范围的泛型是没有意义的,但是我认为是这样的:我的父接口可能有20个具有不相关泛型类型的方法,但是我的子类会迫使它们都与不兼容的额外规范/合同相同(这是任何扩展接口的作用),毕竟,单色滤网仍然是屏幕,因为它可以用任何颜色绘制,我只是强制使用该颜色(无论是哪种颜色),使其与孩子的其他功能保持一致,只是缩小类级别而不是方法级别的可能性。

考虑该功能是否存在根本错误的假设?

编辑:我接受了Sotirios Delimanolis的回答,他非常聪明地发现了正确的问题,我不是在寻求解决方案,但是对于那些想知道如何克服这种情况的人,我自己的回答中有一个窍门

这是中断的地方:

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

如果您建议的内容在编译时有效,则上面的代码段可能会在运行时因ClassCastException而失败,因为redScreenJustAScreen引用的对象期望使用Plane<Red>却收到Plane<Blue>

适当地使用泛型应该可以防止上述情况的发生。 如果允许此类覆盖规则,则泛型将失败。

对于您的用例,我还不太了解,但似乎并不是真的需要泛型。

不允许这样做的原因是它违反了Liskov替换原则

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

这意味着您可以随时使用任意类型的C调用render()

您可以这样做,例如:

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

现在,如果我们看一下MonochromeScreen

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

该声明的意思是:创建此对象的实例时,必须选择与C完全相同的一种类型,并且只能在该对象的整个生命周期中使用该类型。

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.

因此,可以得出Screen s = new MonochromeScreen<Red>(); 不是有效的类型转换, MonochromeScreen不能是Screen的子类。


好吧,让我们稍微扭转一下。 让我们假设所有颜色都是单个Color类的实例,而不是单独的类。 那么我们的代码是什么样的?

interface Plane {
    Color getColor();
}

interface Background {
    Color getColor();
}

interface Screen {
   Background render(Plane plane);
}

到现在为止还挺好。 现在我们定义一个单色屏幕:

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() {...}; 
    }
}

这样可以很好地编译,并具有或多或少相同的语义。

问题是: 这将是好的代码吗? 毕竟,您仍然可以这样做:

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

好吧,不。 这绝对不是您从无辜的renderPrimaryPlanes()方法中获得的期望。 事情会以意想不到的方式破裂。 这是为什么?

这是因为尽管它在形式上有效且可编译,但它也以与原始方式完全相同的方式破坏了LSP。 这个问题是不是与语言,但与模型:您呼叫的实体Screen可以做更多的事情比一个叫MonochromeScreen ,因此它不能成为它的超类。

我认为您的代码不是您想要的,这就是为什么您会出错的原因。

我想这就是你想要的

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

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

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

您可能会误以为,因为<C>是两个接口,所以是同一回事。 它不是。

这个

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

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

与上面的代码完全相同。 C和HI只是通用占位符的名称。 通过用extends Screen<HI>扩展Screen<C> ,我们告诉Java C与HI是相同的placeHolder,所以这将是神奇的。

在你的代码中

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

我们已经声明了一个全新的占位符,该占位符仅在该方法中具有上下文。 所以我们可以写这段代码

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

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

重新定义为

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

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

重新定义为

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

哪个冲突,所以java给您一个错误。

仅供参考:这是我找到的解决案例并传递方法的重写的唯一方法(如果设计模式有名称,我很高兴知道ir!这是使用通用方法扩展接口的最终方法使其成为类通用的。您仍然可以使用父类型(也称为Color)键入它,以将其用作原始的常规类型旧接口。):

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

    } 
}
  • 在redScreen中添加蓝色平面时引发异常:

    java.lang.UnsupportedOperationException:当前的屏幕实现基于单色“红色”,但被要求渲染“蓝色”彩色平面

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM