這篇文章帶給大家的內容是關於Java數組協變與範型不變性的知識介紹(附代碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
變性是OOP語言不變的大坑,Java的陣列協變就是其中的一個老坑。因為最近踩到了,便做一個記錄。順便也提一下範式的變性。
解釋數組協變之前,先明確三個相關的概念,協變、不變和逆變。
一、協變、不變、逆變
#假設,我為一家餐廳寫了這樣一段程式碼
#
class Soup<T> { public void add(T t) {} } class Vegetable { } class Carrot extends Vegetable { }
有一個範式類別Soup8742468051c85b06f0a0af9e3e506b5c,表示用食材T做的湯,它的方法add(T t)表示向湯中添加食材T。類Vegetable表示蔬菜,類Carrot表示胡蘿蔔。當然,Carrot是Vegetable的子類別。
那麼問題來了,Soupf154463d24280b51e227a9971c008850和Soup48d44884ab571fbfd69a92f91c198045之間是什麼關係呢?
第一個反應,Soup48d44884ab571fbfd69a92f91c198045應該是Soupf154463d24280b51e227a9971c008850的子類,因為胡蘿蔔湯顯然是一種蔬菜湯。如果真是這樣,那就看看下面的程式碼。其中Tomato表示西紅柿,是Vegetable的另一個子類
Soup<Vegetable> soup = new Soup<Carrot>(); soup.add(new Tomato());
第一句沒問題,Soup48d44884ab571fbfd69a92f91c198045是Soupf154463d24280b51e227a9971c008850的子類,Soup48d44884ab571fbfd69a92f91c198045是Soupf154463d24280b51e227a9971c008850的子類,所以可以將Soup48d44884ab571fbfd69a92f91c198045的實例賦給變數soup。第二句也沒問題,因為soup宣告為Soupf154463d24280b51e227a9971c008850型,它的add方法接收一個Vegetable類型的參數,而Tomato是Vegetable,型別正確。
但是,兩句放在一起卻有了問題。 soup的實際類型是Soup48d44884ab571fbfd69a92f91c198045,而我們給它的add方法傳遞了一個Tomato的實例!換言之,我們在用番茄做胡蘿蔔湯,肯定做不出來。所以,把Soup48d44884ab571fbfd69a92f91c198045視為Soupf154463d24280b51e227a9971c008850的子類別在邏輯上雖然是通順的,在使用過程中卻是有缺陷的。
那麼,Soup48d44884ab571fbfd69a92f91c198045和Soupf154463d24280b51e227a9971c008850究竟應該是什麼關係呢?不同的語言有不同的理解和實現。總結起來,有三種情況。
(1)如果Soup48d44884ab571fbfd69a92f91c198045是Soupf154463d24280b51e227a9971c008850的子類,則稱泛型Soup8742468051c85b06f0a0af9e3e506b5c是協變的
(2)如果Soup48d44884ab571fbfd69a92f91c198045和Soupf154463d24280b51e227a9971c008850 ;是無關的兩個類,則稱泛型Soup8742468051c85b06f0a0af9e3e506b5c是不變的
(3)如果Soup48d44884ab571fbfd69a92f91c198045是Soupf154463d24280b51e227a9971c008850的父類,則稱泛型Soup8742468051c85b06f0a0af9e3e506b5c是逆變的。 (不過逆變不常見)
理解了協變、不變和逆變的概念,再看Java的實作。 Java的一般泛型是不變的,也就是說Soupf154463d24280b51e227a9971c008850和Soup48d44884ab571fbfd69a92f91c198045是毫無關係的兩個類,不能將一個類別的實例賦值給另一個類別的變數。所以,上面那段用番茄做胡蘿蔔湯的程式碼,其實根本無法通過編譯。
二、陣列協變
Java中,陣列是基本型,不是泛型,不存在Array8742468051c85b06f0a0af9e3e506b5c這樣的東西。但它和泛型很像,都是用另一個型別建構的型別。所以,數組也是要考慮變性的。
與泛型的不變性不同,Java的陣列是協變的。也就是說,Carrot[]是Vegetable[]的子類別。而上一節的例子已經表明,協變有時會引發問題。例如下面這段程式碼
Vegetable[] vegetables = new Carrot[10]; vegetables[0] = new Tomato(); // 运行期错误
因為數組是協變的,編譯器允許把Carrot[10]賦值給Vegetable[]類型的變量,所以這段程式碼可以順利通過編譯。只有在運行期,JVM真的試圖往一堆胡蘿蔔中插入一個西紅柿的時候,才發現大事不好。所以,上面的程式碼在運行期會拋出一個java.lang.ArrayStoreException類型的例外。
陣列協變性,是Java的著名歷史包袱之一。使用數組時,千萬要小心!
如果把範例中的陣列替換為List,情況就不同了。就像這樣
ArrayList<Vegetable> vegetables = new ArrayList<Carrot>(); // 编译期错误 vegetables.add(new Tomato());
ArrayList是一個泛型類,它是不變的。所以,ArrayList48d44884ab571fbfd69a92f91c198045和ArrayListf154463d24280b51e227a9971c008850之間並無繼承關係,這段程式碼在編譯期就會報錯。
兩段程式碼雖然都會報錯,但通常情況下,編譯期錯誤總比運行期錯誤好處理一些。
三、當泛型也想要協變、逆變
#泛型是不變的,但某些場景裡我們還是希望它能協變。例如,有一個天天喝蔬菜湯減肥的小姐姐
class Girl { public void drink(Soup<Vegetable> soup) {} }#
我们希望drink方法可以接受各种不同的蔬菜汤,包括Soup48d44884ab571fbfd69a92f91c198045和Soup8ad9f592c4c8b60fa486d2a1b7041744。但受到不变性的限制,它们无法作为drink的参数。
要实现这一点,应该采用一种类似于协变性的写法
public void drink(Soup<? extends Vegetable> soup) {}
意思是,参数soup的类型是泛型类Soup8742468051c85b06f0a0af9e3e506b5c,而T是Vegetable的子类(也包括Vegetable自己)。这时,小姐姐终于可以愉快地喝上胡萝卜汤和西红柿汤了。
但是,这种方法有一个限制。编译器只知道泛型参数是Vegetable的子类,却不知道它具体是什么。所以,所有非null的泛型类型参数均被视为不安全的。说起来很拗口,其实很简单。直接上代码
public void drink(Soup<? extends Vegetable> soup) { soup.add(new Tomato()); // 错误 soup.add(null); // 正确}
方法内的第一句会在编译期报错。因为编译器只知道add方法的参数是Vegetable的子类,却不知道它具体是Carrot、Tomato、或者其他的什么类型。这时,传递一个具体类型的实例一律被视为不安全的。即使soup真的是Soup8ad9f592c4c8b60fa486d2a1b7041744类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。
但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型。编译器认为它是安全的。
同样,也有一种类似于逆变的方法
public void drink(Soup<? super Vegetable> soup) {}
这时,Soup8742468051c85b06f0a0af9e3e506b5c中的T必须是Vegetable的父类。
这种情况就不存在上面的限制了,下面的代码毫无问题
public void drink(Soup<? super Vegetable> soup) { soup.add(new Tomato()); }
Tomato是Vegetable的子类,自然也是Vegetable父类的子类。所以,编译期就可以确定类型是安全的。
以上是Java數組協變與範型不變性的知識介紹(附程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!