簡體   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