泛型是Java SE 5.0引入的一種Java功能,在其發布幾年后,我發誓那里的每個Java程序員不僅聽說過它,而且已經使用過它。 關于Java泛型,有很多免費和商業資源,而我使用的最佳資源是:
- Java教程
- Java泛型和集合 ,作者Maurice Naftalin和Philip Wadler
- 有效的Java(第二版) ,作者:約書亞·布洛赫(Joshua Bloch)。
盡管有大量的信息,但在我看來,有時候許多開發人員仍然不了解Java泛型的含義和含義。 這就是為什么我試圖以最簡單的方式總結開發人員需要的有關泛型的基本信息。
泛型的動機
考慮Java泛型的最簡單方法是考慮一種語法糖,它可能使您省去一些強制轉換操作:
List<Apple> box = ...; Apple apple = box.get( 0 );
前面的代碼是自說的:box是對Apple類型對象列表的引用。 get方法返回一個Apple實例,不需要強制轉換。 沒有泛型,此代碼將是:
List box = ...; Apple apple = (Apple) box.get( 0 );
不用說,泛型的主要優點是讓編譯器跟蹤類型參數,執行類型檢查和強制轉換操作:編譯器保證強制轉換永遠不會失敗。
編譯器現在不再依賴程序員來跟蹤對象類型并執行強制轉換,這可能導致運行時失敗,難以調試和解決,而現在可以幫助程序員執行更多類型檢查并在編譯時檢測更多失敗。
通用設施
泛型工具引入了類型變量的概念。 根據Java語言規范,類型變量是由以下項引入的不合格標識符:
- 通用類聲明
- 通用接口聲明
- 通用方法聲明
- 通用構造函數聲明。
通用類和接口
如果類或接口具有一個或多個類型變量,則它是通用的。 類型變量由尖括號分隔,并遵循類(或接口)的名稱:
public interface List<T> extends Collection<T> { ... }
粗略地說,類型變量充當參數,并提供編譯器進行檢查所需的信息。
Java庫中的許多類(例如整個Collections Framework)都被修改為通用的。 例如,我們在第一個代碼段中使用的List接口現在是一個通用類。 在該代碼段中,box是對List <Apple>對象的引用,該對象是使用一個類型變量:Apple實現List接口的類的實例。 類型變量是編譯器在將get方法的結果自動轉換為Apple引用時使用的參數。
實際上,新的通用簽名或接口List的get方法是:
T get( int index);
方法get確實返回了一個T類型的對象,其中T是List <T>聲明中指定的類型變量。
通用方法和構造函數
如果方法和構造函數聲明一個或多個類型變量,它們的方式幾乎相同。
public static <t> T getFirst(List<T> list)
此方法將接受對List <T>的引用,并將返回類型T的對象。
例子
您可以在自己的類或通用Java庫類中利用通用類。
書寫時輸入安全性…
例如,在下面的代碼片段中,我們創建了一個實例List <String>,其中填充了一些數據:
List<String> str = new ArrayList<String>(); str.add( "Hello " ); str.add( "World." );
如果我們嘗試將其他類型的對象放入List <String>,則編譯器將引發錯誤:
str.add( 1 ); // won't compile
…以及閱讀時
如果我們傳遞List <String>引用,則始終保證可以從中檢索String對象:
String myString = str.get( 0 );
反復進行
庫中的許多類(例如Iterator <T>)已得到增強并變得通用。 接口List <T>的iterator()方法現在返回一個Iterator <T>,可以輕松使用它,而無需轉換通過其T next()方法返回的對象。
for (Iterator<String> iter = str.iterator(); iter.hasNext();) { String s = iter.next(); System.out.print(s); }
使用foreach
for每種語法都利用了泛型。 先前的代碼片段可以寫成:
for (String s: str) { System.out.print(s); }
更容易閱讀和維護。
自動裝箱和自動拆箱
處理泛型時,將自動使用Java語言的自動裝箱/自動拆箱功能,如以下代碼片段所示:
List<Integer> ints = new ArrayList<Integer>(); ints.add( 0 ); ints.add( 1 ); int sum = 0 ; for ( int i : ints) { sum += i; }
但是請注意,裝箱和拆箱會降低性能,因此,通常會出現警告和警告。
亞型
與其他面向對象的類型化語言一樣,在Java中,可以構建類型的層次結構:

在Java中,類型T的子類型可以是擴展T的類型,也可以是直接或間接實現T的類型(如果T是接口)。 由于“成為...的子類型”是傳遞關系,因此,如果類型A是B的子類型,而B是C的子類型,則A也將是C的子類型。 在上圖中:
- 富士蘋果是蘋果的子類型
- 蘋果是水果的一種
- FujiApple是Fruit的子類型。
每個Java類型也將是Object的子類型。
類型B的每個子類型A都可以分配給類型B的引用:
Apple a = ...; Fruit f = a;
通用類型的子類型化
如果可以將Apple實例的引用分配給Fruit的引用,如上所示,那么List <Apple>和List <Fruit>之間是什么關系? 哪一個是子類型? 更一般而言,如果類型A是類型B的子類型,則C <A>和C <B>如何相互關聯?
出乎意料的是,答案是:絕對沒有。 用更正式的詞來說,泛型類型之間的子類型關系是不變的。
這意味著以下代碼段無效:
List<Apple> apples = ...; List<Fruit> fruits = apples;
以下內容也是如此:
List<Apple> apples; List<Fruit> fruits = ...; apples = fruits;
但為什么? 是一個蘋果是一種水果,一盒蘋果(一個清單)也是一盒水果。
從某種意義上講是這樣,但是類型(類)封裝了狀態和操作。 如果一盒蘋果是一盒水果會怎樣?
List<Apple> apples = ...; List<Fruit> fruits = apples; fruits.add( new Strawberry());
如果是這樣,我們可以在列表中添加Fruit的其他不同子類型,并且必須禁止這樣做。
相反,更直觀:一盒水果不是一盒蘋果,因為它可能是其他種類(子類型)水果(水果)(例如草莓)的盒子(列表)。
真的有問題嗎?
不應該這樣。 Java開發人員感到驚訝的最強烈原因是數組的行為與泛型類型之間的不一致。 后者的子類型關系是不變的,而前者的子類型關系是協變的:如果類型A是類型B的子類型,則A []是B []的子類型:
Apple[] apples = ...; Fruit[] fruits = apples;
可是等等! 如果我們重復上一節中公開的參數,最終可能會在一系列蘋果中添加草莓:
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
該代碼確實可以編譯,但是在運行時會以ArrayStoreException的形式引發錯誤。 由于數組的這種行為,在存儲操作期間,Java運行時需要檢查類型是否兼容。 顯然,該檢查還會增加您應該意識到的性能損失。
同樣,泛型更安全地使用,并且可以“糾正” Java數組的這種類型的安全性弱點。
在這種情況下,您現在想知道為什么數組的子類型關系是協變的,我將為您提供Java Generics和Collections給出的答案:如果它是不變的,則無法將引用傳遞給對象數組類型未知(無需每次都復制到Object [])的方法,例如:
void sort(Object[] o);
隨著泛型的出現,數組的這種特性不再是必需的(我們將在本文的下一部分中看到),并且確實應該避免。
通配符
正如我們在本文前面的部分中所看到的,泛型類型的子類型關系是不變的。 不過,有時我們還是希望以與普通類型相同的方式使用通用類型:
- 縮小參考(協方差)
- 擴大參考(差異)
協方差
例如,假設我們有一組盒子,每個盒子都有不同種類的水果。 我們希望能夠編寫可以接受任何方法的方法。 更正式地說,給定類型B的子類型A,我們想找到一種方法來使用類型C <B>的引用(或方法參數),該引用可以接受C <A>的實例。
為了完成此任務,我們可以使用帶有擴展名的通配符,例如以下示例:
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples;
? 擴展重新引入了泛型類型的協變子類型:Apple是Fruit的子類型,而List <Apple>是List <?的子類型。 延伸水果>。
逆差
現在讓我們介紹另一個通配符: 超。 給定類型A的超類型B,則C <B>是C <?的子類型。 超級A>:
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
如何使用通配符?
現在有足夠的理論:我們如何利用這些新結構?
? 延伸
讓我們回到第二部分中介紹Java數組協方差的示例:
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
如我們所見,當嘗試通過對Fruit數組的引用將Strawberry添加到Apple數組時,此代碼可以編譯,但會導致運行時異常。
現在,我們可以使用通配符將此代碼轉換為與之對應的通用代碼:由于Apple是Fruit的子類型,因此我們將使用? 擴展通配符,以便能夠將List <Apple>的引用分配給List <?的引用 延伸水果>:
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples; fruits.add( new Strawberry());
這次,代碼將無法編譯! Java編譯器現在阻止我們將草莓添加到水果列表中。 我們將在編譯時檢測到錯誤,甚至不需要進行任何運行時檢查(例如在數組存儲的情況下),以確保將兼容類型添加到列表中。 即使我們嘗試將Fruit實例添加到列表中,代碼也不會編譯:
fruits.add( new Fruit());
沒門。 結果是,實際上,您不能將任何東西放入其類型使用?的結構中。 擴展通配符。
如果我們考慮一下,原因很簡單: 擴展T通配符告訴編譯器我們正在處理類型T的子類型,但是我們不知道是哪一個。 由于沒有辦法說出來,而且我們需要保證類型安全,因此不允許您在此類結構內放置任何內容。 另一方面,由于我們知道它可能是T的子類型,因此我們可以從結構中獲取數據,并保證它是T實例:
Fruit get = fruits.get( 0 );
? 超
使用類型的行為是什么? 超級通配符? 讓我們從這個開始:
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
我們知道水果是對Apple超類商品列表的引用。 同樣,我們不知道它是哪個超類型,但是我們知道Apple及其任何子類型都將與其分配兼容。 確實,由于這種未知類型將同時是Apple和GreenApple超類型,因此我們可以這樣寫:
fruits.add( new Apple()); fruits.add( new fruits.add( GreenApple());
如果我們嘗試添加任何Apple超類型,編譯器都會抱怨:
fruits.add( new Fruit()); fruits.add( new Object());
由于我們不知道它是哪個超類型,因此不允許添加任何實例。
如何從這種類型的數據中獲取數據呢? 事實證明,您唯一可以擺脫的是對象實例:由于我們無法知道它是哪個超類型,因此編譯器只能保證它將是對對象的引用,因為對象是任何對象的超類型。 Java類型。
獲取和放置原則或PECS規則
總結一下行為? 延伸和? 超級通配符,我們得出以下結論:
- 使用 ? 如果需要從數據結構中檢索對象,則擴展通配符
- 使用 ? 如果需要將對象放入數據結構,則使用超級通配符
- 如果您需要同時做這兩個事情,請不要使用任何通配符。
這就是Maurice Naftalin在他的Java泛型和集合中稱為“獲取和放置原理”,在Joshua Bloch的“ 有效Java ”中稱為PECS規則。
Bloch的助記符PECS來自“ Producer Extends,Consumer Super”,可能更容易記住和使用。
方法簽名中的通配符
如本系列第二部分中所見,在Java中(與許多其他類型化語言一樣),Substitution原則是:可以將子類型分配給其任何超類型的引用。
這適用于分配任何引用的過程,即,即使將參數傳遞給函數或存儲其結果也是如此。 因此,該原理的優點之一是,在定義類層次結構時,可以編寫“通用”方法來處理整個子層次結構,而與要處理特定對象實例的類無關。 到目前為止,在Fruit類的層次結構中,接受Fruit作為參數的函數將接受其任何子類型(例如Apple或Strawberry)。
從上一篇文章中可以看出,通配符可還原泛型類型的協變量和逆變量子類型:然后,使用通配符,使開發人員編寫可以利用到目前為止所展示的優點的函數。
例如,如果開發人員想要定義一個方法eat,該方法接受任何水果的列表,則可以使用以下簽名:
void eat(List<? extends Fruit> fruits);
由于水果類的任何子類型的列表都是List <? 擴展Fruit>,先前的方法將接受任何此類列表作為參數。 請注意,如上一節所述,“獲取和放置原則”(或PECS規則)將允許您從此類列表中檢索對象并將其分配給Fruit引用。
另一方面,如果要將實例放在作為參數傳遞的列表上,則應使用?。 超級通配符:
void store(List<? super Fruit> container);
這樣,可以將任何水果超類的列表傳遞給存儲功能,并且可以安全地將任何水果子類型放入其中。
有界類型變量
但是,泛型的靈活性比這更大。 類型變量可以有界,幾乎與通配符可以有界(就像我們在第二部分中看到的一樣)。 但是,類型變量不能以super為邊界,而只能以extends為邊界。 查看以下簽名:
public static <T extends I<T>> void name(Collection<T> t);
它接受類型受限制的對象的集合:它必須滿足T擴展I <T>條件。 起初,使用有界類型變量似乎并不比通配符更強大,但是稍后我們將詳細介紹這些差異。
讓我們假設層次結構中的一些(但不是全部)結果可能是多汁的,如下所示:
public interface Juicy<T> { Juice<T> squeeze(); }
多汁的水果將實現此接口并發布擠壓方法。
現在,您編寫一個使用一堆水果并將其全部榨干的庫方法。 您可以寫的第一個簽名可能是:
<T> List<Juice<T>> squeeze(List<Juicy<T>> fruits);
使用有界類型變量,您將編寫以下內容(實際上,它與以前的方法具有相同的擦除作用):
<T extends Juicy<T>> List<Juice<T>> squeeze(List<T> fruits);
到目前為止,一切都很好。 但有限。 我們可以使用相同帖子中使用的相同參數,然后發現squeeze方法不起作用,例如,在以下情況下使用紅色橘子列表:
class Orange extends Fruit implements Juicy<Orange>; RedOrange class extends Orange;
由于我們已經了解了PECS原理,因此我們將通過以下方式更改方法:
<T extends Juicy<? super T>> List<Juice<? super T>> squeezeSuperExtends(List<? extends T> fruits);
此方法接受類型擴展為Juicy <?的對象列表。 super T>,換句話說,必須存在類型S,使得T擴展Juicy <S> 和 S superT。
遞歸界
也許您想放松T延伸多汁<? 超級T>綁定。 這種綁定稱為遞歸綁定,因為類型T必須滿足的綁定取決于T。您可以在需要時使用遞歸綁定,也可以將它們與其他種類的綁定進行混合匹配。
因此,例如,您可以編寫具有以下界限的通用方法:
<A extends B<A,C>, C extends D<T>>
請記住,這些示例僅用于說明泛型可以做什么。 您將要使用的界限始終取決于要放入類型層次結構中的約束。
使用多個類型變量
假設您想放寬在最后一個squeeze方法上設置的遞歸范圍。 然后,讓我們假設類型T可以擴展Juicy <S>,盡管T本身不擴展S。方法簽名可以是:
<T extends Juicy<S>, S> List<Juice<S>> squeezeSuperExtendsWithFruit(List<? extends T> fruits);
此簽名與上一個簽名相當(因為我們僅在方法參數中使用T),但有一個小優點:由于我們聲明了通用類型S,因此該方法可以返回List <Juice <S>而不是List <? 超級T>,在某些情況下很有用,因為編譯器將根據您傳遞的方法參數幫助您確定S類型是哪種。 由于要返回列表,因此您可能希望調用者能夠從中獲取某些信息,并且如上一部分所述,您只能從列表中獲取Object實例,例如List <?。 超級T>。
如果需要,顯然可以為S添加更多界限,例如:
<T extends Juicy<S>, S extends Fruit> List<Juice<S>> squeezeSuperExtendsWithFruit(List<? extends T> fruits);
多界
如果要對同一類型變量應用多個范圍怎么辦? 事實證明,您只能為每個泛型類型變量編寫一個綁定。 因此,以下界限是非法的:
<T extends A, T extends B> // illegal
編譯器將失敗,并顯示以下消息:
T已在…中定義
必須使用不同的語法來表示多個界限,這是一個非常熟悉的表示法:
<T extends A & B>
先前的邊界意味著T擴展了 A和B。請注意,根據Java語言規范 第4.4章的規定,邊界是:
- 類型變量。
- 一類。
- 接口類型,然后是其他接口類型。
這意味著只能使用接口類型來表達多個界限。 無法在多重綁定中使用類型變量,并且編譯器將失敗,并顯示以下消息:
類型變量不能后面跟隨其他界限。
在我閱讀的文檔中,這并不總是很清楚。
參考文獻:
- The Gray Blog的 JCG合作伙伴Gray 撰寫的有關Java泛型的系列文章
編碼愉快! 不要忘記分享!
拜倫
相關文章:
- Java泛型示例
- Java最佳實踐系列
- 正確記錄應用程序的10個技巧
- 每個程序員都應該知道的事情
- 生存在狂野西部開發過程中的9條提示
- 軟件設計法則
- Java Fork / Join進行并行編程
翻譯自: https://www.javacodegeeks.com/2011/04/java-generics-quick-tutorial.html