Java進階--詳解Java中的泛型(Generics)

語言: CN / TW / HK

Java泛型是在JDK1.5中引進來的一個概念。泛型意為泛化的引數型別,英文為Generics ,翻譯過來其實就是通用型別的意思。泛型在平時開發中經常用到,例如常用的集合類、Class類等都是JDK給我們提供的泛型類,更多的時候我們還會使用自定義泛型。可見,泛型在Java體系中還是一個很重要的知識。那麼,本篇文章我們就來系統的學習一下Java的泛型。

一、為什麼要引入泛型

上邊已經提到,泛型是在JDK 1.5引進來的一個概念。我們知道,現在宣告一個List集合是需要指定List的泛型的,指定了List的泛型後,List就只能接受我們指定的型別。而在JDK 1.5之前,由於沒有泛型的概念,List集合接受的是一個Object型別。我們知道Object是Java中所有類的父類,那麼這也就意味著宣告一個List集合後這個集合可以用來存放任意型別的資料。

舉個例子,我們宣告一個沒有泛型的List集合,並嘗試新增不同的資料型別。程式碼如下:

		List list=new ArrayList();
        list.add(123);
        list.add("abc");
        list.add(new Object());
        for (Iterator iterator = list.iterator(); iterator.hasNext();) {
			Object object = (Object) iterator.next();
	        System.out.println(object);
		}
複製程式碼

在上邊程式碼中,我們在List的集合種添加了三種不同的資料型別,而這樣的寫法在IDE中是不會由任何錯誤提示的。並且可以正常執行程式並打印出資料。

123 abc java.lang.Object@4926097b

這樣的程式碼不用想也知道是非常危險的,假設我們期望在集合中存放的是Integer型別,但卻在集合中存入了String,那麼在使用集合資料的時候把資料都當成Integer處理,程式必然會崩潰。也就是在這樣的情況下,為了提高Java語言的安全性以及程式的健壯性,Java在1.5的版本種提供了泛型的支援。有了泛型之後,便可以確保集合中存放的一定是我們所期望的型別。

修改上面的程式碼,將List泛型指定為Integer,當我們新增非整數型別的引數時IDE就會提示相應的錯誤。並且在編譯時編譯器也會丟擲錯誤致使程式無法編譯成位元組碼檔案。如下圖所示。

IDE提示異常: IDE提示型別錯誤 編譯時編譯器丟擲異常: 編譯器編譯錯誤 通過這一個例子可以認識到泛型在Java中有著多麼重要的意義。當然,泛型的用途遠不止這一點,比如我們可以通過自定義的泛型類結合多型來提高程式的可可擴充套件性等。

二、泛型基本使用

既然泛型這麼重要,那麼先來學習一下泛型的使用。泛型可以定義在類、介面或者方法上。定義的地方不同,泛型的作用域也不同,比如將泛型定義在類上,那麼這個泛型可以作用於整個類。而如果將泛型定義在方法上,那麼這個泛型的作用域僅限於這個方法。

1.泛型類

首先,我們來看如何定義一個泛型類。

public class Response<T> {

	private T data;

	public T getData() {
		return data;
	}

	public void setData(T data) {
		this.data = data;
	}

}
複製程式碼

上述程式碼我們定義了一個Response類,併為其聲明瞭一個泛型引數T。那麼在這個Response類中,我們就可以將T作為一個數據型別來使用。比如可將T當作一個成員變數宣告在Response中;可以作為方法的返回值型別,也可以作為方法的引數。但是,至於這個T指代的是什麼型別,此時還並不能確定,因為這需要我們在使用的時候來自行指定T的型別。

如下,我們在宣告Response的時候將T指定為String型別,那麼此時Response中的T就確定了,一定是一個String型別。

Response<String> response = new Response<>();
response.setData("abc");
String data = response.getData();
複製程式碼

指定泛型的型別後,我們便可以理所當然的把T當作String進行set和get,且無需再進行型別轉換。

2.泛型介面

泛型除了可以指定在類上也可以指定在介面上,使用方式和類泛型是一模一樣的。

public interface IResponse<T> {
	
	T getData();
	
	void setData(T t);
}
複製程式碼

上邊程式碼中我們定義了一個IResponse的介面,併為其聲明瞭一個泛型,在介面中添加了兩個抽象方法,分別將T作為方法的返回值型別,和引數型別。接著我們來看一下泛型介面的實現類:

假如說實現IResponse介面的類已經確定了泛型的型別。比如,事先我們已經知道返回的型別是一個String。則可有如下程式碼:

public class StringResponse implements IResponse<String> {

	@Override
	public String getData() {
		return null;
	}

	@Override
	public void setData(String t) {
		
	}

}
複製程式碼

上邊程式碼我們定義了一個StringResponse 類並實現了IResponse介面,而IResponse指定了泛型為String。那麼在StringResponse 類中重寫getData和setData方法的返回值和引數型別都為String。

但是,假如我們現在不確定Response的是一個什麼型別的資料,那麼則可以繼續宣告一個Response的泛型類,並實現IResponse介面,並將介面的泛型指定為Response的泛型。程式碼如下:

public class Response<T> implements IResponse<T>{

	private T data;

	public T getData() {
		return data;
	}

	public void setData(T data) {
		this.data = data;
	}
}
複製程式碼

此時的程式碼其實就是聲明瞭一個Response的泛型類,並將Response的泛型T作為了IResponse的泛型。

3.泛型方法

除了在類和介面上可以宣告泛型外,在方法上也是可以宣告泛型的。前邊提到了類和介面的泛型都是作用於整個類的。而在方法上宣告泛型,泛型的作用於則只作用於這個方法。我們來看一個例子:

public class Model {	
	public <M> void setData(M data) {
		System.out.println(data.toString());
	}
}
複製程式碼

在Model中有一個setData的方法,由於該方法接受的引數型別不確定,因此我們將這個方法定義成了泛型方法。在呼叫Model的setData方法的時候需要指定這個方法接受的型別。如下:

Model model=new Model();
model.<String>setData("string");
複製程式碼

由於在Java8中對於泛型方法的呼叫做了優化,可以省略指定泛型的型別,直接傳入引數即可。

Model model=new Model();
model.setData("string");
複製程式碼

當然,這裡其實編譯器根據我們傳入的引數做了型別推斷。

有同學可能會有疑問,那我直接把泛型宣告到類上,然後在這個方法中使用不行嗎?當然是可以的,其實跟在方法上宣告泛型是一樣的效果。我們前文也已經提到了,方法上的泛型與類上的泛型只是作用於不同罷了。但是,如果這個泛型引數僅僅只在方法中使用了,我們是沒必要把泛型宣告到類上去的。

4.限定泛型的型別

我們仍以Response的場景為例,假如我希望Response類接受的引數不是任意型別的,而是希望Response接受的資料型別是BaseData或者BaseData的子類。這種情況下我們就需要在指定Response的泛型的時候對泛型引數做一個約束。

定義一個BaseData類,類中有一個token,如下:

public class BaseData {
	private String token;

	public String getToken() {
		return token;
	}
}
複製程式碼

將Response的泛型宣告為<T extends BaseData>

public class Response<T extends BaseData>{![在這裡插入圖片描述](http://img-blog.csdnimg.cn/20210116193503911.png)


	private T data;

	public String getToken() {
		if(data!=null) {
			return data.getToken();
		}
		return null;
	}
}
複製程式碼

那麼此時Response類中的成員變數T就只能是一個BaseData型別,並且T擁有BaseData的方法,如上程式碼可以直接通過T呼叫getToken方法。

接下來,我們來測試一下Response泛型的作用範圍,將String作為Response的泛型引數,IDE則會提示mismatch的錯誤,並且無法通過編譯。

在這裡插入圖片描述 只有指定Response的泛型為BaseData或者其子類才能正常編譯。

5.泛型與萬用字元

Java中的萬用字元大家應該都不陌生,在Java中可以使用"?"來表示萬用字元,用來指代未知型別。而萬用字元與泛型的搭配也是開發中經常用到的。

眾所周知,在Java中Object類是所有類的父類,例如String的頂級父類一定是Object。但是,並不能說List<String>的頂級父類是List<Object>。下面我們來看一個例子:

有Person、Student和Teacher三個類,它們的繼承關係如下:

//  Person類
public class Person {

}

// Student類
public class Student extends Person{![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210116200353115.png)


}

// Teacher類
public class Teacher extends Person {

}
複製程式碼

接下來分別例項化出Student與Teacher,並分別賦值給Person,程式碼如下:

Student student = new Student();
Teacher teacher = new Teacher();

Person person;	
person = student;
person = teacher;
複製程式碼

熟悉Java多型的同學都應該知道上邊的程式碼是沒有任何錯誤的。那麼再來看下邊的程式碼: 在這裡插入圖片描述

上述程式碼IDE卻提示了一個mismatch的錯誤。但是如果我們就是希望personList能夠接受studentList也能夠接受teacherList應該怎麼辦呢?這種情況在現實開發中可是非常常見的。此時,我們就可以用萬用字元來解決,將personList的泛型修改為<? extends Person>即可,程式碼如下:

List<? extends Person> personList;
List<Student> studentList = new ArrayList<>();
List<Teacher> teacherList = new ArrayList<>();

personList = studentList;
personList = teacherList;
複製程式碼

另外,我們還可以通過<? super Student>來指定接收Student或者Student的父類,程式碼如下: 在這裡插入圖片描述 可以看到上述程式碼中listSuper的泛型宣告為了<? super Student>,此時listSuper就只能接收Student以及其父類的集合。所以可以看到,程式碼中將studentList與personList以及ObjectList正常賦值給listSuper,但是teacherList賦值給listSuper則會報錯。

三、泛型的型別擦除

到這裡關於泛型的基礎知識差不多已經講完了。可以發現,上邊講到的內容都是程式編譯前的程式碼。程式中一旦有不符合規範的程式碼IDE都會提示錯誤,並且編譯器在編譯原始碼時就會丟擲異常。那接下來要講的泛型擦除就是程式碼編譯後的知識了。

前文已經提到,泛型是在JDK1.5中引入的概念,在JDK1.5之前的原始碼中像List這些泛型類都是使用Object來實現的。那問題來了,JDK1.5版本是否能夠相容JDK1.4或者之前的版本呢?答案是肯定的。之所以能夠實現JDK的向下相容就是因為在編譯期間編譯器進行了型別擦除。

我們應該怎麼理解型別擦除呢?其實就是編譯器在對原始碼進行編譯的時候將泛型換成了泛型指定的上限類,如果沒有指定泛型的上限,編譯器則會使用Object類替代。簡單的說在編譯完成後的位元組碼檔案中其實是沒有泛型的概念的,原始碼中的泛型被編譯器用Object或者泛型指定的類給替換掉了。這也是為什麼JDK1.5能夠向下相容的原因。

我們可以通過javap的反彙編來證明編譯期間的型別擦除。定一個Model類,程式碼如下:

public class Model {

    public void test() {
    	List<String> list=new ArrayList();
    	 list.add("abc");
    }
    
    public void test2() {
    	List list=new ArrayList();
    	list.add("abc");
    }
   
}
複製程式碼

可以看到,這個類中有兩個方法,這兩個方法不同的地方在於test方法中指定了List的泛型為String,而test2方法中未指定List的泛型。我們先將Model.java通過javac工具編譯成Model.class檔案,然後通過javap反彙編Model.class檔案,得到結果如下:

$ javap -c Model.class
Compiled from "Model.java"
public class com.test.reflection.Model {
  public com.test.reflection.Model();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void test();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String abc
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: return

  public void test2();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String abc
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: return
}
複製程式碼

通過反彙編指令可以看到test1方法與test2方法並無任何區別,並且可以看到第18和30行的註釋:

// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

都是向List集合中添加了一個Object物件。

另外,我們也可以通過反射證實型別擦除的存在。上篇文章《Java進階--深入理解Java的反射機制》我們學習了Java的反射,知道Class物件是在類載入時候生成的,並且反射是在程式執行時的操作。那我們來通過反射List的Class物件,並嘗試新增向List中新增不同的型別看是否能夠成功。程式碼如下:

		List<String> list=new ArrayList<>();
		list.add("abc");
		
		Class<List> listClass=List.class;
		
		try {
			Method addMethod=listClass.getDeclaredMethod("add", Object.class);
			addMethod.invoke(list, 123);
			
		} catch (NoSuchMethodException | SecurityException|IllegalAccessException
				| IllegalArgumentException | InvocationTargetException e) {
			e.printStackTrace();
		}
		
		System.out.println("list.size() = "+list.size());
		
		for (Object obj : list) {
			System.out.println(obj.toString());
		}
複製程式碼

輸出結果:

list.size() = 2
abc
123
複製程式碼

上述程式碼我們聲明瞭一個泛型為String的List集合,並向集合中添加了一個字串“abc",接著通過反射向集合list中添加了一個整數型別123,通過輸出結果可以看到兩個值都被新增到了集合中。這一結果也印證了泛型的型別擦除。

四、總結

泛型是開發中使用頻率非常高的一個技術點,泛型的引入使得Java語言更加安全,也增強程式的健壯性。通過本篇文章我們系統的學習Java的泛型。同時也明白了Java的泛型僅僅是在編譯期間由IDE和編譯器來進行檢查和校驗的。在編譯後的位元組碼檔案以及執行期間的JVM中是沒有泛型的概念的。也正是因為這一原因,其實我們可以通過編輯位元組碼檔案或者在執行時通過反射來繞過泛型的校驗,完成程式碼編寫期間不能實現的操作。