Java數組協變與范型不變性的案例?這個問題可能是我們日常學習或工作經常見到的。希望通過這個問題能讓你收獲頗深。下面是小編給大家帶來的參考內容,讓我們一起來看看吧!
成都創新互聯公司堅持“要么做到,要么別承諾”的工作理念,服務領域包括:成都網站建設、成都網站制作、企業官網、英文網站、手機端網站、網站推廣等服務,滿足客戶于互聯網時代的新榮網站設計、移動媒體設計的需求,幫助企業找到有效的互聯網解決方案。努力成為您成熟可靠的網絡建設合作伙伴!變性是OOP語言不變的大坑,Java的數組協變就是其中的一口老坑。因為最近踩到了,便做一個記錄。順便也提一下范型的變性。
解釋數組協變之前,先明確三個相關的概念,協變、不變和逆變。
一、協變、不變、逆變
假設,我為一家餐館寫了這樣一段代碼
class Soup<T> { public void add(T t) {} } class Vegetable { } class Carrot extends Vegetable { }
有一個范型類Soup<T>,表示用食材T做的湯,它的方法add(T t)表示向湯中添加食材T。類Vegetable表示蔬菜,類Carrot表示胡蘿卜。當然,Carrot是Vegetable的子類。
那么問題來了,Soup<Vegetable>和Soup<Carrot>之間是什么關系呢?
第一反應,Soup<Carrot>應該是Soup<Vegetable>的子類,因為胡蘿卜湯顯然是一種蔬菜湯。如果真是這樣,那就看看下面的代碼。其中Tomato表示西紅柿,是Vegetable的另一個子類
Soup<Vegetable> soup = new Soup<Carrot>(); soup.add(new Tomato());
第一句沒問題,Soup<Carrot>是Soup<Vegetable>的子類,所以可以將Soup<Carrot>的實例賦給變量soup。第二句也沒問題,因為soup聲明為Soup<Vegetable>類型,它的add方法接收一個Vegetable類型的參數,而Tomato是Vegetable,類型正確。
但是,兩句放在一起卻有了問題。soup的實際類型是Soup<Carrot>,而我們給它的add方法傳遞了一個Tomato的實例!換言之,我們在用西紅柿做胡蘿卜湯,肯定做不出來。所以,把Soup<Carrot>視為Soup<Vegetable>的子類在邏輯上雖然是通順的,在使用過程中卻是有缺陷的。
那么,Soup<Carrot>和Soup<Vegetable>究竟應該是什么關系呢?不同的語言有不同的理解和實現。總結起來,有三種情況。
(1)如果Soup<Carrot>是Soup<Vegetable>的子類,則稱泛型Soup<T>是協變的
(2)如果Soup<Carrot>和Soup<Vegetable>是無關的兩個類,則稱泛型Soup<T>是不變的
(3)如果Soup<Carrot>是Soup<Vegetable>的父類,則稱泛型Soup<T>是逆變的。(不過逆變不常見)
理解了協變、不變和逆變的概念,再看Java的實現。Java的一般泛型是不變的,也就是說Soup<Vegetable>和Soup<Carrot>是毫無關系的兩個類,不能將一個類的實例賦值給另一個類的變量。所以,上面那段用西紅柿做胡蘿卜湯的代碼,其實根本無法通過編譯。
二、數組協變
Java中,數組是基本類型,不是泛型,不存在Array<T>這樣的東西。但它和泛型很像,都是用另一個類型構建的類型。所以,數組也是要考慮變性的。
與泛型的不變性不同,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是一個泛型類,它是不變的。所以,ArrayList<Carrot>和ArrayList<Vegetable>之間并無繼承關系,這段代碼在編譯期就會報錯。
兩段代碼雖然都會報錯,但通常情況下,編譯期錯誤總比運行期錯誤好處理一些。
三、當泛型也想要協變、逆變
泛型是不變的,但某些場景里我們還是希望它能協變起來。比如,有一個天天喝蔬菜湯減肥的小姐姐
class Girl { public void drink(Soup<Vegetable> soup) {} }
我們希望drink方法可以接受各種不同的蔬菜湯,包括Soup<Carrot>和Soup<Tomato>。但受到不變性的限制,它們無法作為drink的參數。
要實現這一點,應該采用一種類似于協變性的寫法
public void drink(Soup<? extends Vegetable> soup) {}
意思是,參數soup的類型是泛型類Soup<T>,而T是Vegetable的子類(也包括Vegetable自己)。這時,小姐姐終于可以愉快地喝上胡蘿卜湯和西紅柿湯了。
但是,這種方法有一個限制。編譯器只知道泛型參數是Vegetable的子類,卻不知道它具體是什么。所以,所有非null的泛型類型參數均被視為不安全的。說起來很拗口,其實很簡單。直接上代碼
public void drink(Soup<? extends Vegetable> soup) { soup.add(new Tomato()); // 錯誤 soup.add(null); // 正確}
方法內的第一句會在編譯期報錯。因為編譯器只知道add方法的參數是Vegetable的子類,卻不知道它具體是Carrot、Tomato、或者其他的什么類型。這時,傳遞一個具體類型的實例一律被視為不安全的。即使soup真的是Soup<Tomato>類型也不行,因為soup的具體類型信息是在運行期才能知道的,編譯期并不知道。
但是方法內的第二句是正確的。因為參數是null,它可以是任何合法的類型。編譯器認為它是安全的。
同樣,也有一種類似于逆變的方法
public void drink(Soup<? super Vegetable> soup) {}
這時,Soup<T>中的T必須是Vegetable的父類。
這種情況就不存在上面的限制了,下面的代碼毫無問題
public void drink(Soup<? super Vegetable> soup) { soup.add(new Tomato()); }
Tomato是Vegetable的子類,自然也是Vegetable父類的子類。所以,編譯期就可以確定類型是安全的。
感謝各位的閱讀!看完上述內容,你們對Java數組協變與范型不變性的案例大概了解了嗎?希望文章內容對大家有所幫助。如果想了解更多相關文章內容,歡迎關注創新互聯行業資訊頻道。
文章題目:Java數組協變與范型不變性的案例-創新互聯
本文路徑:http://vcdvsql.cn/article12/ddhcgc.html
成都網站建設公司_創新互聯,為您提供網站設計公司、自適應網站、品牌網站建設、外貿建站、營銷型網站建設、域名注冊
聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯