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中是沒有泛型的概念的。也正是因為這一原因,其實我們可以通過編輯字節碼文件或者在運行時通過反射來繞過泛型的校驗,完成代碼編寫期間不能實現的操作。