Java通用子類型如何工作?

泛型類是任何編程語言中的強大工具,但它們也可能帶來很多混亂。例如,爲什麼List不是一個子類List,即使Double是一個亞型Number?在本文中,我們將探討圍繞子類化泛型類型的各種規則,並構建Java提供的泛型繼承機制的內聚視圖。但是,在深入研究這個重要主題之前,我們將定義各種不同的技術來定義泛型類型和泛型類參數。
瞭解泛型
面嚮對象語言中泛型的目的是允許將任意聚合類型提供給類,而不必爲每種提供的類型編寫新的類。例如,如果我們想寫一個列表類來存儲對象,不使用泛型我們將被迫要麼創建爲每種類型的通過了新的類別(例如IntegerList,DoubleList等)或有列表類店的內部結構ObjectS作爲如下清單所示:
public class MyList {
private Object[] elements;
public void addElement(Object element) {
// … add to the elements array …
}
public Object getElementAtIndex(int index) {
// … retrieve the element at the given index …
}
}

通用參數
儘管使用Object確實解決了存儲Object源自的任何類型的問題Object,但它仍然具有重要的缺陷。其中最重要的是編譯時類型安全性的損失。例如,如果我們addElement 使用參數 調用 Integer,則提供 Integer 的不再被視爲其實際類型,而是被視爲Object。這意味着我們必須Integer在檢索時強制轉換,並且使用此類的控件之外的代碼MyList可能沒有足夠的信息來知道將檢索的元素強制轉換爲哪種類型。
如果我們添加另一個元素,那麼這個問題就複雜了,但這一次是type Double。如果我們的MyList類的使用者需要Integer對象列表,Integer則將ClassCastException在運行時對檢索到的元素執行強制轉換。另一方面,如果我們期望MyList該類包含類型的值Number(其中的Integer和Double都是子類),則我們尚未傳輸此信息以確保在編譯時類型安全。從本質上講,從編譯器的角度來看,我們的列表同時包含Integer和Double對象這一事實是任意的。由於它不瞭解我們的意圖,因此它無法執行檢查以確保我們確實遵守我們聲明的意圖。
爲了解決這個問題,Java開發工具包(JDK)5向Java引入了通用類的概念,該類允許在類名之後的方括號內指定類型參數。例如,我們現在可以List 如下重寫我們的 類:
public class MyList {
private T[] elements;
public void addElement(T element) {
// … add to the elements array …
}
public T getElementAtIndex(int index) {
// … retrieve the element at the given index …
}
}

現在,我們可以創建MyList的 Integer 對象:
MyList listOfIntegers = new MyList();

請注意,如果我們想創建另一個MyList存儲Double對象,則不必創建另一個類:我們可以簡單地實例化一個MyList。我們還可以Number 通過類似的方式創建對象列表 :
MyList listOfNumbers = new MyList<>();
listOfNumbers.addElement(new Integer(1));
listOfNumbers.addElement(new Double(3.41));

上界通用參數
在設計泛型類時,我們可能還希望限制可以作爲泛型參數提供給類的值的類型(實例化泛型類時映射到泛型參數的類型)。例如,如果我們創建一個ListOfNumbers類,我們可能要附帶的通用參數限制是數種或延長Number(注意,鑽石經營者,<>,是在JDK 7中引入並允許類型推斷,在一般的參數上假定右側恰好是賦值左側的泛型參數):
public class ListOfNumber {
public Number sum() {
// … sum all values and return computed value …
}

在這種情況下,sum方法假定所有存儲的元素都是 Number 對象或從派生的對象Number,從而允許計算數值。如果我們不包括該泛型類型的上限,則客戶端可以實例化一個Object或其他非數字類型的列表,並且我們將被期望計算總和(從域或問題的角度來看這是沒有意義的)。請注意,上限的擴展部分可用於指定通用參數必須實現的接口或指定多個接口(或一個類和多個接口)。有關更多信息,請參見Oracle 的“ 綁定類型參數”文章。
通配符
當我們實例化泛型類型時,可能在某些情況下我們並不關心列表的實際泛型參數。例如,如果我們想要從中求和ListOfNumbers,但又不希望從列表中添加或檢索任何元素,則可以使用通配符(以問號表示爲通用)來忽略列表的實際通用參數類型。參數):
public class ListOfNumberFactory {
public static ListOfNumber getList() {
return new ListOfNumber();
}
}
ListOfNumber<?> list = ListOfNumberFactory.getList();
System.out.println(list.sum());

在繼續之前,必須對命名的通用參數(例如T和通配符)進行重要區分:
定義通用類或方法 時,將使用命名通用參數 來表示實例化該類或使用該方法時的實際通用參數。當採用通用類或方法表示實際的通用參數(或對通用參數不關心)時,使用通配符 。
這意味着我們不能創建具有公共類類型的泛型類,MyIncorrectList<?> {}也不能實例化該形式的泛型類,new MyList();除非它包含在另一個泛型類的定義中,例如以下情況:
public class OuterGeneric {
private MyList list;
// … other fields and methods …
}

泛型參數和通配符之間的區別是一個重要的區別,當我們處理泛型子類型時,泛型參數和通配符將合併到同一類型層次結構中,這將變得更加重要。
上界通配符
就像上限泛型參數一樣,在某些情況下,我們不關心泛型參數的類型,只是它是指定類型的子類或實現指定的接口。例如,假設我們要Number在循環中處理對象列表。在這種情況下,我們需要指定期望列表的上限爲Number,如下所示:
public class ListOfNumber implements Iterable {
public Number sum() {
// … compute the sum …
}
@Override
public Iterator iterator() {
// … return an iterator …
}
}
ListOfNumber<? extends Number> list = ListOfNumberFactory.getList();
for (Number number: list) {
// … do something with the Number …
}

僅將list的類型設置爲ListOfNumber,這很誘人,但是這會將我們的用法限制爲完全ListOfNumber對象。例如,我們將無法返回ListOfNumber從ListOfNumberFactory.getList()和執行相同的操作。在稍後討論泛型類層次結構時,我們將更清楚地看到這種區別的重要性。
請注意,ListOfNumber由於在使用上限通配符時我們不知道列表的實際泛型參數,因此從實用上限制了將任何對象添加到類中:我們僅知道其實際實現類型是的子類型Number。例如,可能很容易想到我們可以將Integer對象插入ListOfNumber<? extends Number>,但是如果我們這樣做,編譯器將拋出錯誤,因爲它不能保證此類插入的類型安全。通用參數可能是 Double,在這種情況下,我們無法將Integer 對象添加 到的列表中Double。有關更多信息,請參見StackOverflow說明。
下界通配符
與命名的通用參數不同,通配符還可以指定下限。例如,假設我們要向Integer列表添加一個。自然的傾向是指定列表的類型爲MyList,但這會任意限制我們可以操作的列表的類型。難道我們不能添加Integer對象列表Number或列表Object中呢?在這種情況下,我們可以指定通配符的下限,從而允許使用相同或下限類型的超類的任何泛型參數類型:
public class IntegerListFactory {
public static MyList<? super Integer> getList() {
// … return MyList, MyList, MyList, etc…
}
}
MyList<? super Integer> integerList = IntegerListFactory.getList();
integerList.addElement(new Integer(42));
儘管下限通配符不如無界或上限通配符流行,但它們仍在泛型類型的子類化中發揮重要作用,正如我們將在下一節中看到的那樣。
子類化通用類
通常,泛型子類可分爲兩類:(1)泛型參數子類型和(2)通配符泛型子類型。在與之前我們分別使用泛型參數和泛型通配符看到的泛型的定義和用法之間的劃分相似的意義上,這兩個類別中的每一個都有自己的細微差別和重要的繼承規則。
通用參數子類型
瞭解了泛型及其用途之後,我們現在可以開始研究可以使用泛型建立的繼承層次結構。從泛型開始時最常見的錯誤名詞之一是多態泛型參數隱含了多態泛型類。實際上,泛型參數的多態性與泛型類的多態性之間不存在任何關係:
多態通用參數 並不意味着多態通用類
例如,如果我們有一個List,List (其中Double的子類型Number)不是的子類型List。實際上,List和之間的唯一關係List是它們都繼承自Object(並且我們將很快看到List<?>)。爲了說明這種情況,我們可以定義以下類集:
public class MyList {}
public class MySpecializedList extends MyList {}
public class My2ParamList<T, S> extends MySpecializedList {}

這組類導致以下繼承層次結構:

從頂部開始,我們可以看到所有泛型類仍從Object該類繼承。當我們進入到一個新的水平,我們可以看到,雖然Double是一個亞型Number,MyList 是不是一個亞型MyList。爲了理解這種區別,我們必須看一個具體的例子。如果要實例化a MyList,則可以按以下方式插入a Number或subtype的任何對象Number:
public class MyList {
public void insert(T value) {
// … insert the value …
}
}
MyList numberList = new MyList<>();
numberList.insert(new Integer(7));
numberList.insert(new Double(5.72));

爲了MyList確實成爲的子類型MyList,MyList根據Liskov替換原理,它必須可以替換爲的任何實例(即,MyList可以在使用a的任何地方,MyList都必須提供相同的行爲,例如 在列表中添加 Integer 或。 Double)。如果實例化a MyList,我們很快就會發現該原理不成立,因爲我們不能將一個Integer對象添加到我們的對象中MyList,因爲Integer它不是的子類型Double。因此,MyList並非在所有情況下都可以替代,因此不是的子類型MyList。
當我們繼續向下的層次結構時,我們可以自然地看到,通用類(例如)MySpecializedList是的子類型MyList,只要Tmatch 的通用參數即可。例如,MySpecializedList是的子類型MyList,但MySpecializedList不是的子類型MyList。同樣地,MySpecializedList不是的子類型MyList爲同樣的原因MyList不是的子類型MyList。
最後,只要前一個類擴展了後一個類並且共享的通用參數匹配,則包含其他通用參數的通用類就是另一個通用類的子類型。例如,My2ParamList<T, S>子類型是MySpecializedList,只要T是同一類型(因爲它是共享的通用參數)。如果未共享通用參數(例如)S,則它可以獨立變化而不會影響通用層次結構。例如,My2ParamList<Number, Integer>和和My2ParamList<Number, Double>都是MySpecializedList共享的通用參數匹配的子類型。
通配符子類型
儘管通用參數層次結構相對簡單明瞭,但通用通配符引入的層次結構卻細微得多。在通配符方案中,我們必須考慮三種不同的情況:(1)無界通配符,(2)上界通配符,和(3)下界通配符。我們可以在下圖中看到這些各種情況之間的關係:
爲了理解此層次結構,我們必須關注呈現的每個通配符中的約束(請注意,與處理有關的類Double和退出處理的箭頭Double是綠色,與處理有關的類和與處理有關的Number箭頭退出的類Number是藍色)。從頂部開始,MyList<?>繼承自Object,因爲該MyList對象可以包含任何引用類型的泛型參數(此列表中可以包含任何對象)。實際上,從概念上講,此列表可以認爲僅包含type的對象Object。
建立層次結構的頂部之後,我們將移至底部並關注MyList(左下角的綠色類)。有兩個類別是該類別的直接父母:(1)MyList<? extends Double>和MyList<? super Double>。前一種情況簡單地指出MyList是的子類,MyList其中包含任何Double對象或的子類型的對象Double。從另一種角度來看,我們說的MyList是僅包含的Double是的特例MyList,其中包含的Double對象或任何其他子類Double。如果我們要替換MyList其中MyList<? extends Double>預期,我們知道,我們MyList會含有Double或亞型Double(實際上,它將包含只Double,但仍足以滿足的要求Double或子類型Double)。
後一種情況(MyList<? super Double>作爲父項)只是在相反的方向上陳述了同一件事:如果我們期望MyList包含Double或父類的Double,則提供a MyList就足夠了。與前一種情況類似,MyList僅包含Double對象可以被視爲是的特例MyList,其中包含Double或是的子類型的對象Double。實際上,MyList是的更受限版本MyList<? super Double>。因此,MyList<? super Double>只要提供a MyList,就可以在邏輯上滿足任何期望的a ,根據定義,該成爲MyLista的子類型MyList<? super Double>。
完成Double層次結構的一部分後,我們看到它MyList是的子類MyList<? extends Number>。要了解這種血統,我們必須考慮這個上限的含義。簡而言之,我們要求MyList包含type的對象Number或type的任何子類Number。因此,MyList僅包含類型的對象Double(是類型的子類Number)的,是MyList包含的Number對象或子類型的約束更嚴格的版本Number。根據定義,這是MyList的子類型MyList<? extends Number>。
Number 層次結構的 一部分只是Double已經討論過的部分的反映。從底部取出,任何地方MyList的Number或超類型Number預期(MyList<? super Number>),一個MyList可以是足夠的。同樣, Number 是的超型Double,因此,在任何預期的Double或超型Double(MyList<? super Double>)處,a MyList就足夠了。最後,在需要MyList包含Number或任何子類型Number(MyList<? extends Number>)的任何地方,MyList包含 Number 就足夠了,因爲它只是此要求的特例。
推論主題
儘管我們涵蓋了該層次結構的大部分內容,但仍然存在三個必然的主題:(1)MyList<? super Number>是的子類型MyList<? super Double>,(2)MyList<? extends Double>是的子類型MyList<? extends Number>,以及(3)MyList和之間的公共超類型MyList。
1.在第一種情況下,MyList<? super Double>簡單地指出,我們預計MyList含有Double或任何超類型Double,其中Number之一。因此,由於Number它將作爲的超型就足夠了Double,提供MyList<? super Number>是的更受限制的版本MyList<? super Double>,從而使前者成爲後者的子類型。
2.在第二種情況下,情況恰恰相反。如果我們預期MyList含有Number或任何亞型Number,被認爲Double是一個亞型Number,MyList含有Double或亞型Double可以被看作是一種特殊的情況下MyList含Number或亞型Number。
3.在後一種情況下,只MyList<?>充當之間的共同超MyList和MyList。如上所述,Double和之間的多態關係Number並不構成MyList和之間的多態關係MyList。因此,兩種類型之間唯一的共同祖先是a MyList,其中包含任何引用類型(對象)。
結論
泛型爲面向對象的語言添加了一些非常強大的功能,但是它們也可能給新手和有經驗的開發人員在語言的概念模型中帶來深深的困惑。在這些混亂中,最重要的是各種通用案例之間的繼承關係。在本文中,我們探討了泛型背後的目的和思考過程,並介紹了命名泛型參數和通配符泛型的正確繼承方案。
最後,開發這麼多年我也總結了一套學習Java的資料與面試題,如果你在技術上面想提升自己的話,可以關注我,私信發送領取資料或者在評論區留下自己的聯繫方式,有時間記得幫我點下轉發讓跟多的人看到哦。在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章