年后跑路第一戰,從 Java 泛型學起!
文末本文轉載自微信公眾號「愛寫Bug的麥洛」,作者麥洛 。轉載本文請聯系愛寫Bug的麥洛公眾號。
概述
大家好,我是麥洛,今天來復習一下泛型。JDK 5.0 引入了 Java 泛型,允許設計者詳細地描述變量和方法的類型要如何變化,使得代碼具有更好的可讀性。本文章是對 Java 中泛型的快速介紹,包含泛型背后的目標以及使用泛型如何提高我們代碼的質量。
為什么要引入泛型?
在沒有泛型的背景下,讓我們想象一個場景,我們要在 Java 中創建一個List來存儲Integer。
代碼如下:
- List list = new LinkedList();
- list.add(new Integer(1));
- Integer i = list.iterator().next();
果不其然,IDEA會直接提醒需要強制轉換。
我們對代碼進行修改,如下所示:
- Integer i = (Integer) list.iterator.next();
在沒有泛型的前提下,定義的List可以保存任何對象,當我們遍歷時候,根據上下文進行判斷,只能保證它是一個Object,所以需要我們顯示轉換。
我們知道List中的數據類型是Integer,可以直接強制轉換,如果我們不知道或者強制轉換時候寫錯類型,就會導致報錯,一場災難就這樣發生了。
這時候,就有人想了,我能不能在使用List時候就指定保存的類型,編譯階段來幫我保證類型的正確性,那就可以完全避免讓人討厭的強制轉換,所以,泛型就因運而生了。
讓我們修改前面代碼片段的第一行:
- List<Integer> list = new LinkedList<>();
通過添加包含類型的菱形運算符 <>,我們將List能保存的類型限制到只有Integer類型,編譯器可以在編譯時強制執行類型。
泛型方法
對于泛型方法,我們可以用不同類型的參數調用它們。編譯器將確保我們使用的任何類型的正確性。
泛型方法屬性:
- 泛型方法在方法聲明的返回類型之前有一個類型參數(包含類型的菱形運算符)。
- 類型參數可以是有界的(我們將在本文后面解釋邊界)。
- 泛型方法可以在方法簽名中具有用逗號分隔的不同類型參數。
- 泛型方法的方法體就像普通方法一樣。
這是定義將數組轉換為List的泛型方法的示例:
- public <T> List<T> fromArrayToList(T[] a) {
- return Arrays.stream(a).collect(Collectors.toList());
- }
方法簽名中的
如前所述,該方法可以處理多個泛型類型。在這種情況下,我們必須將所有泛型類型添加到方法簽名中。
以下是我們如何修改上述方法以處理類型T和類型G:
- public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
- return Arrays.stream(a)
- .map(mapperFunction)
- .collect(Collectors.toList());
- }
我們正在傳遞一個函數,該函數將具有T類型元素的數組轉換為具有G類型元素的列表。
一個例子是將Integer轉換為它的String表示:
- @Test
- public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
- Integer[] intArray = {1, 2, 3, 4, 5};
- List<String> stringList
- = Generics.fromArrayToList(intArray, Object::toString);
- assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
- }
請注意,Oracle 建議使用大寫字母來表示泛型類型,并選擇更具描述性的字母來表示正式類型。在 Java 集合中,我們使用T表示類型,K表示鍵,V表示值。
有界泛型
類型參數可以有界,我們可以限制方法接受的類型。例如,我們可以指定一個方法接受一個類型及其所有子類(上限)或一個類型及其所有超類(下限)。要聲明上界類型,我們在類型后使用關鍵字extends,要聲明下界類型,我們在類型后使用關鍵字super。
例子:
- public <T extends Number> List<T> fromArrayToList(T[] a) {
- ...
- }
我們在這里使用關鍵字 extends 表示類型 T 在類的情況下擴展上限或在接口的情況下實現上限。
多重邊界
一個類型也可以有多個上限:
如果T擴展的類型之一是一個類(例如Number),我們必須將它放在邊界列表中的第一個。否則會導致編譯時錯誤。
在泛型中使用通配符
在Java中,通配符由?表示,我們使用它們來指代未知類型。通配符對泛型特別有用,可以用作參數類型。
首先,我們知道Object是所有 Java 類的超類。但是,Object的集合不是任何集合的超類型。所以,一個List 不是List
例子:
- public static void paintAllBuildings(List<Building> buildings) {
- buildings.forEach(Building::paint);
- }
假如現在有一個Building 的子類型,叫House,我們不能將這個方法用于 House 的列表,即使 House 是 Building 的一個子類型。
如果我們需要將此方法與類型 Building 及其所有子類型一起使用,則有界通配符可以發揮作用:
- public static void paintAllBuildings(List<? extends Building> buildings) {
- ...
- }
現在此方法將適用于類型 Building 及其所有子類型。這稱為上限通配符,其中類型 Building 是上限。
我們還可以指定具有下限的通配符,其中未知類型必須是指定類型的超類型。可以使用 super 關鍵字后跟特定類型來指定下限。例如, 表示未知類型,它是 T 的超類(= T 及其所有父類)。
類型擦除
Java 中添加了泛型以確保類型安全。并且為了確保泛型不會在運行時造成開銷,編譯器在編譯時對泛型應用了一個稱為類型擦除的過程。
如果類型參數是無界的,則類型擦除會刪除所有類型參數并用它們的邊界或Object替換它們。這樣,編譯后的字節碼只包含正常的類、接口和方法,確保不會產生新的類型。在編譯時也將正確的轉換應用于 Object 類型。
這是類型擦除的示例:
- public <T> List<T> genericMethod(List<T> list) {
- return list.stream().collect(Collectors.toList());
- }
使用類型擦除,無界類型T被替換為Object:
- public List<Object> withErasure(List<Object> list) {
- return list.stream().collect(Collectors.toList());
- }
- public List withErasure(List list) {
- return list.stream().collect(Collectors.toList());
- }
如果類型是有界的,則在編譯時該類型將被邊界替換:
- public <T extends Building> void genericMethod(T t) {
- ...
- }
編譯后:
- public void genericMethod(Building t) {
- ...
- }
泛型和原始數據類型
Java 中泛型的一個限制是類型參數不能是基本類型。
例如,以下不能編譯:
- List<int> list = new ArrayList<>();
- list.add(17);
要理解基本類型為什么不起作用,讓我們記住泛型是一個編譯時特性,這意味著類型參數被刪除并且所有泛型類型都實現為類型Object。
我們來看 一個列表的add方法:
- List<Integer> list = new ArrayList<>();
- list.add(17);
add方法的簽名是:
- boolean add(E e);
并將被編譯為:
- boolean add(Object e);
因此,類型參數必須可轉換為Object。由于基本類型不擴展Object,我們不能將它們用作類型參數。
然而,Java 為原語提供了裝箱類型,以及自動裝箱和拆箱來解包它們:
- Integer a = 17;
- int b = a;
所以,如果我們想創建一個可以容納整數的列表,我們可以使用這個包裝器:
- List<Integer> list = new ArrayList<>();
- list.add(17);
- int first = list.get(0);
編譯后的代碼將等效于以下內容:
- List list = new ArrayList<>();
- list.add(Integer.valueOf(17));
- int first = ((Integer) list.get(0)).intValue();
結論
Java 泛型是對 Java 語言的強大補充,因為它使程序員的工作更輕松且不易出錯。泛型在編譯時強制類型正確,最重要的是,可以實現泛型算法而不會對我們的應用程序造成任何額外開銷。