深入理解Java泛型
未完待續
一、引言
泛型(Generics)和面向對象、函數式編程一樣,也是一種程序設計的范式,泛型允許程序員在定義類、接口和方法時使用引用類型的類型形參代表一些以后才能確定下來的類型,在聲明變量、創建對象、調用方法時像調用函數傳參一樣將具體類型作為實參傳入來動態指明類型。
Java的泛型,是在jdk1.5中引入的一個特性,最主要應用是在jdk1.5的新集合框架中。作為Java語法層面的東西,本博客原本不打算介紹,但考慮到泛型理解和使用起來有一定的難度,應用的還很普遍,再加上自己工作多年好像也沒有能夠完全理解和靈活的運用泛型,因此還是決定看一些相關的書籍中與泛型有關的內容,并用一些篇幅總結下學習成果,介紹下我理解的泛型。
二、泛型類(接口)
2.1 創建泛型類
先來看兩個類
public class StringPrinter {
private String thingsToPrint;
public StringPrinter() {
}
public StringPrinter(String thing) {
thingsToPrint = thing;
}
public void setThingsToPrint(String thing) {
thingsToPrint = thing;
}
public void print() {
System.out.println(thingsToPrint);
}
}
public class IntegerPrinter {
private String thingsToPrint;
public IntegerPrinter() {
}
public IntegerPrinter(String thing) {
thingsToPrint = thing;
}
public void setThingsToPrint(String thing) {
thingsToPrint = thing;
}
public void print() {
System.out.println(thingsToPrint);
}
}
兩個類的作用相當,都是將傳進來的參數進行打印,類的功能幾乎完全相同,唯一的不同是參數的類型不一樣,假如要為很多類型實現這個打印功能,就會編寫很多的Printer類,如果要實現一個類統一實現這個功能,就可以采用泛型。
先來講講泛型語法,泛型用一個“菱形”<>聲明,<>中是類型形參列表,如有多個類型形參,使用英文逗號,隔開。
下面程序定義了一個帶有泛型聲明的Printer類,有一個類型形參T(Type),聲明了類的泛型參數后,就可以在類內部使用此泛型參數,構造函數名仍然是類名本身不需要加泛型
public class Printer<T> {
private T thingsToPrint;
public Printer() {
}
public void setThingsToPrint(T thing) {
thingsToPrint = thing;
}
public Printer(T thing) {
thingsToPrint = thing;
}
public void print() {
System.out.println(thingsToPrint);
}
}
Java還能在定義類型參數時設置限制條件,如下例定義了一個NumberPrinter類,通過extends指定T的類型上限只能是Number。
??注意:類型參數和第四章提到的類型通配符是不一樣的,類型參數上的限制不能用
super關鍵字,因為會造成不確定,使用extends指定T的類型上限,編譯器至少知道T是個Number,如果是super關鍵字,編譯器根本不知道T有哪些屬性和方法。
public class NumberPrinter<T extends Number> {
private T thingsToPrint;
public NumberPrinter() {
}
public void setThingsToPrint(T thing) {
thingsToPrint = thing;
}
public NumberPrinter(T thing) {
thingsToPrint = thing;
}
public void print() {
System.out.println(thingsToPrint);
}
public T get() {
return thingsToPrint;
}
}
還可以設置多個限制條件,extends后面只能有一個類但是可以有多個接口:
public class NumberPrinter<T extends Number & Comparable<T>> {
private T thingsToPrint;
}
創建泛型接口同理,例如jdk中的List實際上就是一個接口。
public interface List<E> extends Collection<E> {
}
并非任何類都能聲明為泛型類,Java規定:異常類(java.lang.Throwable)不得帶有泛型
public class MyException<T> extends Exception { //編譯出錯?,Generic class may not extend 'java.lang.Throwable'
T msg;
}
public class MyException<T> extends RuntimeException { //編譯出錯?,Generic class may not extend 'java.lang.Throwable'
T msg;
}
public class MyException<T> extends Throwable { //編譯出錯?,Generic class may not extend 'java.lang.Throwable'
T msg;
}
2.2 實例化泛型類
使用泛型類創建對象時就可以為類型形參T傳入具體類型,就可以生成類似Printer<String>,Printer<Double>的類型
public static void main(String[] args) {
// 構造器T形參是String,只能用String初始化
Printer<String> printer1 = new Printer<String>("apple");
printer1.print(); //apple
// 構造器T形參是Double,只能用Double初始化
Printer<Double> printer2 = new Printer<Double>(3.8);
printer2.print(); //3.8
}
jdk1.7以后,支持泛型類型推斷,可以簡寫為:
Printer<String> printer1 = new Printer<>("apple");
Printer<Double> printer2 = new Printer<>(3.8);
如不指定類型實參默認為Object類型,因為所有引用類型都能被Object代表,int、double、char等基本數據類型不能被Object代表,這就是類型實參必須是引用類型的原因,不過注意如果定義類型形參時通過entends指定了上限例如NumberPrinter<T extends Number>,則不傳遞類型實參時默認為上限類型Number
public static void main(String[] args) {
Printer printer1 = new Printer("apple");
printer1 = new Printer(12);
printer1 = new Printer(new Date());
NumberPrinter numberPrinter1 = new NumberPrinter(5);
NumberPrinter numberPrinter2 = new NumberPrinter(5.8);
NumberPrinter numberPrinter3 = new NumberPrinter(""); //編譯出錯?
}
2.3 派生泛型類
派生該類時,需要指定類型實參
public class HPPrinter extends Printer<Integer> {
}
通過entends指定了上限的類型需不超過上限類型,以下同理
public class SuperNumberPrinter extends NumberPrinter<Double> {
}
public class SuperNumberPrinter extends NumberPrinter<Date> { //編譯出錯?
}
如不使用泛型,不指定類型實參,則泛型轉換為Object類型或上限類型
public class HPPrinter extends Printer {
public static void main(String[] args) {
HPPrinter hpPrinter = new HPPrinter();
hpPrinter.setThingsToPrint(new Object());
hpPrinter.setThingsToPrint("hello");
hpPrinter.setThingsToPrint(12);
}
}
還可以子類和父類聲明同一個類型形參,子類中也不確定具體的類型,需要子類被實例化時將類型間接傳遞給父類,同時子類還可以一同定義自己的泛型
public class HPPrinter<T> extends Printer<T> {
}
public class HPPrinter<T, E> extends Printer<T> {
}
子類確定父類泛型類型的同時,又可以有自己的泛型
public class HPPrinter<E> extends Printer<Integer> {
}
使用泛型又不指定類型的寫法是錯誤的
public class HPPrinter extends Printer<T> { //編譯出錯?
}
三、泛型方法和泛型構造器
有時候,在類和接口上不需定義類型形參,只是具體方法中的某個類型不確定,需要在方法上面定義類型形參,這個也是支持的,jdk1.5提供了對于泛型方法的支持。
3.1 泛型方法
聲明方法時,在返回值前指明泛型的類型形參列表<>,類型形參僅作用于方法內,這個方法就聲明為了泛型方法。類型形參可以出現在參數和返回值中,調用方法時指定具體類型。泛型方法可以根據需要聲明為靜態。任何類中都可以存在泛型方法,而不是只有泛型類中才能聲明泛型方法。
??在返回值前面指明泛型的類型形參列表
<>是泛型方法的特征,沒有這個特征的都不是泛型方法,泛型類中使用類<>里面聲明的類型作為方法參數或返回值類型的方法,不屬于泛型方法,例如2.1中Printer類中的任何方法都不是泛型方法。
public class Demo {
public <T> E fun1(T e) {
return null;
}
public <T> void fun2(T e) {
}
public static <T> List<T> copyArray(T[] arr) {
List<T> list = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
list.add(arr[i]);
}
return list;
}
public <T> T getMiddle(T... a) {
return a[a.length / 2];
}
public static <T> T getMiddleStatic(T... a) {
return a[a.length / 2];
}
}
與類、接口中使用泛型參數不同的是,方法中的泛型參數無須顯式傳入實際類型參數,當程序調用copyArray()方法時,無須在調用該方法前傳入String、Obiect等類型,但系統依然可以知道類型形參的數據類型,因為編譯器根據實參推斷類型形參的值,它通常推斷出最直接的類型參數。例如,下面調用代碼:
public static void main(String[] args) {
List<Dog> dogs = copyArray(new Dog[]{});
}
如果要顯示指明類型實參,則需要和實際類型一致,而且必須在對象名.,this.或類名.之后指定,否則語法報錯。
public static void main(String[] args) {
Demo demo = new Demo();
Integer middle1 = getMiddleStatic(1, 2, 3);
String middle2 = Demo.<String>getMiddleStatic("a", "b", "c");
// 指定的和傳入的類型不匹配,編譯出錯?
Integer middle3 = Demo.<String> getMiddleStatic(1, 2, 3);
Double d = demo.<Double>getMiddleStatic(1.0, 2.0, 3.0);
}
當使用自動推斷時,如果涉及多個類型進行自動推斷則取多個類型的共同父類(接口)
public static void main(String[] args) {
Number n1 = getMiddleStatic(1, 2, 3.9);
Serializable n2 = getMiddleStatic(1, 2, "", new Date());
Serializable n3 = getMiddleStatic( 2, "", null);
}
3.2 泛型構造器
構造器也可能成為泛型方法,Java也允許在構造器簽名中聲明類型形參,這樣就產生了所謂的泛型構造器。例如:
public class Demo {
public <T> Demo(T obj) {
System.out.println(obj.getClass());
}
public static void main(String[] args) {
new Demo("hello"); //class java.lang.String
new Demo(12); //java.lang.Integer
new <String> Demo("hello"); //class java.lang.String
new <Integer> Demo(12); //java.lang.Integer
new <String> Demo(12); // 編譯出錯?
}
}
如果泛型構造器上指明了類型形參,則不可以在new后使用“菱形”語法又手動指定,否則會導致類型無法推斷。不過這種奇怪的寫法應該不常碰見
public class Demo<T> {
public <T> Demo(T obj) {
System.out.println(obj.getClass());
}
public static void main(String[] args) {
// 類指定String 方法顯示指定Integer
Demo<String> demo1 = new <Integer> Demo<String>(4);
//類指定Integer 方法隱式指定String
Demo<Integer> demo2 = new Demo<Integer>("4");
//類型推斷為Integer
Demo<Integer> demo3 = new Demo<>(3);
// 編譯出錯?,不能既要求自動類型推斷,又要手動指定
Demo<Integer> demo4 = new <String> Demo<>("4");
}
}
四、不存在泛型類
包含泛型聲明的類型可以在定義變量、創建對象時傳入一個類型實參,從而可以動態地生成無數多個邏輯上的子類,但這種子類在物理上并不存在。
即使加了不同泛型,運行時仍然是同一種類,并不會因為類型參數的不同,產生新的類
public static void main(String[] args) {
Fruit<String> fruit = new Fruit<>("apple");
Fruit<Double> fruit2 = new Fruit<>(3.8);
System.out.println(fruit2.getClass() == fruit.getClass()); //true
}
因此在泛型類中的靜態的代碼塊、靜態變量和靜態方法上,不能使用類型形參
public class Demo<T> {
public static T st; //編譯出錯?
static {
T a = st; //編譯出錯?
}
public static void fun1(T obj) { //編譯出錯?
st = obj;
}
}
由于并不存在真正的泛型類,因此instanceof關鍵字后不能接泛型類
if (new ArrayList<>() instanceof List<String>) { //編譯出錯?
}
if (new ArrayList<>() instanceof List) { //正確寫法?
}
事實上,泛型在編譯后會被擦除,運行時Java虛擬機中沒有泛型,只有普通類和普通方法,<T>會變為Object類型,<T extends Serializable>會變成Serializable類型
例如下面例子通過反射忽略了泛型,從而在運行時將一個List<String>中添加進去了一個Integer和Date類型的對象。
public class TestReflection {
public static void main(String[] args) {
List<String> strs = new ArrayList<>();
strs.add("hello");
strs.add("world");
Class clazz = strs.getClass();
try {
Method method = clazz.getMethod("add", Object.class);
method.invoke(strs, 1);
method.invoke(strs, new Date());
}
catch (Exception e) {
e.printStackTrace();
}
finally {
for (Object obj : strs) {
System.out.println(obj);
}
}
}
}
運行結果
hello
world
1
Fri Apr 25 22:30:10 CST 2025
之前提到Java規定異常類不得帶有泛型,原因就是異常在運行時是存在的,而泛型在運行時不存在,進行捕獲處理時根本不能區分出來。
因為泛型運行時被擦除,因此泛型會影響方法的重載。例如下例由于List的泛型被擦除,導致兩個方法不能重載。
public class Demo {
//編譯出錯? 'test(List<String>)' clashes with 'test(List<Integer>)'; both methods have same erasure
void test(List<String> list) {
}
void test(List<Integer> list) {
}
}
五、類型通配符
有時,如要實現一個遍歷打印list的方法,list中是哪一種元素都有可能,于是我們將泛型實參指定為Object類型,看似解決了問題,但是調用時卻會編譯報錯:無法將List<Object>用于List<String>。
public class Demo {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
test(strings); //編譯出錯?
}
public static void test(List<Object> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
在Java中,兩個類通過繼承和實現接口可以具有父子關系,但不能認為使用了父子類型的兩個泛型類具有父子關系,例如上面程序出現了編譯錯誤,說明List<String>不能被當成List<Object>的子類來用。
??泛型與數組不同,如果是兩個有父子關系的類各聲明一個數組,例如:
Object[]和String[],String[]是Object[]的子類型,是可以將String[]類型的變量賦值給Object[]的,這是一種Java語言早期不安全的設計,操作不當會引發ArrayStoreException,因此jdk1.5設計泛型時避免了這種設計。
為了表示任意類型,可以使用類型通配符,類型通配符是一個問號?,例如將一個問號作為類型實參傳給List集合,寫作List<?>,意思是元素類型未知的List。這個問號?被稱為通配符,它的元素類型可以匹配任何類型。
現在使用任何類型的List來調用它,程序依然可以訪問集合中的元素,其類型是Obiect,這永遠是安全的,因為不管List的真實類型是什么,它包含的都是Obiect。
public static void test(List<?> list) {
Object obj = list.get(i);
}
但是,如果調用add()方法向其中添加非null元素,又會發現編譯出錯
public static void test(List<?> list) {
list.add(new Date()); //編譯出錯?
list.add(null); //能通過編譯?
}
List.java
E get(int index);
boolean add(E e);
通過分析List的get()和add()兩個方法的源碼,可知get()方法是對泛型的讀取,返回為E,E類型雖然不確定,但肯定是一個Object類型,而add()方法需要為E類型賦值一個參數,是對泛型的寫入,而傳進來的?不能確定是什么類型,假如傳進來的List是個List<String>,在方法中又add(new Date())寫入Date類型,就會導致類型混亂,因此無法處理,但是null除外,它是任何引用類型的實例。
說白了,Java的泛型系統是類型安全優先的,不確定類型的泛型可讀不可寫。
類型通配符還能進行類型范圍的限制,例如如果不希望List<?>可以傳入任意一種類型,只希望傳入某一類具體的類型,在設置類型通配符時,可以添加extends和super限制條件,叫做受限制的通配符,extends代表某種類型及子類,super代表某種類型及父類。
例如有這樣的一些類:
/**
* 動物
*/
public class Animal {
}
/**
* 貓
*/
public class Cat extends Animal {
}
/**
* 狗
*/
public class Dog extends Animal {
}
/**
* 英短貓
*/
public class YingDuan extends Cat {
}
/**
* 布偶貓
*/
public class BuOu extends Cat {
}
首先看extends,extends設置的是類型的上限,保證傳入的類型不能超過某個類型,在使用時,如果只希望泛型參數類型是某個類型及其子類,List的類型參數就可以寫成? extends,例如:List<? extends Animal>就是只允許傳入的泛型類型是Animal及其子類,這樣修飾的泛型可讀,讀出為父類Animal類型,但不可寫
為什么不能寫入?因為允許寫入會導致類型混亂,只要泛型中限制某個類及其子類,那隨著類的不斷繼承就一定會出現更小的子類,當更小的子類作為類型參數時,比這個子類大一些的父類祖父類對象就不能寫到泛型修飾的變量中,因為沒有子類引用指向父類的道理。再者假如兩個兄弟類AB繼承自同一父類,AB甚至沒有父子關系,當A類作為類型形參,B類的對象更不能寫到A類的泛型中。
例如傳進來的list是個List<Dog>,方法中又去add(new Cat())會導致類型混亂,因為雖然Cat和Dog都繼承自Animal但是Cat不是Dog的子類,會破壞List<Dog>的類型一致性。再例如傳進來的是個List<Dog>,方法中又去add(new Animal())也會導致類型混亂,所以extends修飾的泛型禁止寫入任何一個非null實例
public class Demo {
public static void test(List<? extends Animal> list) {
Animal animal = list.get(1); //獲取返回值時,由父類Animal接收
list.add(new Cat()); //編譯出錯?
list.add(new YingDuan()); //編譯出錯?
list.add(new Dog()); //編譯出錯?
list.add(new Animal()); //同樣編譯出錯?
}
}
再來看super,super和extends的情況會略有不同,super代表限制類型為某種類型及父類,設置的是類型的下限,保證傳入的泛型類型不能低于某個類型,super修飾的泛型可讀,但只能讀出為Object,可寫,但只能寫入對應類型及其子類,例如? super Cat修飾的變量只允許賦值Cat類及其子類的對象,因為Cat類及其子類的對象肯定可以被Cat類及其父類的引用指向
public class Demo{
public static void test(List<? super Cat> list) {
Object object = list.get(1);
list.add(new Animal()); //編譯出錯?
list.add(new Cat()); //編譯通過?
list.add(new YingDuan()); //編譯通過?
list.add(new BuOu()); //編譯通過?
list.add(null); //編譯通過?
list.add(new Dog()); //編譯出錯?
}
}
舉例來講的話,可以傳給List<? super Cat> list的不是“List<貓>”就是“List<動物>”,所以首先不能add“動物”進去,因為有可能傳進來的是“List<貓>”,只有“動物”包含“貓”,沒有“貓”包含“動物”。同理,不能add“狗”進去,因為“狗”屬于“動物”但不屬于“貓”(廢話)。所以只有“貓”和“英短”以及“布偶”能add進去,因為無論傳進來的是“List<貓>”還是“List<動物>”,“英短”和“布偶”既直接繼承自“貓”,也間接繼承自“動物”,而“貓”本身就能添加進去
所以只有類及其子類(貓、英短、布偶)可寫入super修飾的泛型是因為這樣符合程序里面類的繼承關系,不會導致泛型中類型混亂。說白了就是:存放“動物”的List,存一只“貓”進去也行,存放“貓”的List,存進去“英短”以及“布偶”邏輯上都是正確的,都可以實現父類引用指向子類對象而不是顛倒過來。
上面例子中,List是一個帶有泛型,但是泛型參數沒有類型限制的類,如果定義一個泛型類,并限制類型參數的范圍,該怎樣和類型通配符搭配使用呢
此處定義一個限制類型參數的泛型類Pet,指定類型上限是Animal
public class Pet<T extends Animal> {
private T thing;
public T get() {
return thing;
}
public void set(T t) {
thing = t;
}
}
使用類型通配符?時,類型參數需嚴格按照定義泛型時指定的上限,除了讀取時返回的都是上限Animal類型,其他的和不加類型限制的泛型類沒有區別,都是可讀不可寫,extends和super修飾的類型通配符也類似,可直接看結論:
public static void main(String[] args) {
Pet<?> pet = new Pet<Object>(); //編譯出錯?
Pet<?> pet2 = new Pet<>();
pet2.set(new Animal()); //編譯出錯?
Animal animal = pet2.get();
}
public static void main(String[] args) {
Pet<? extends Cat> pet = new Pet<YingDuan>();
pet.set(new Cat()); //編譯出錯?
Cat cat = pet.get();
}
public static void main(String[] args) {
Pet<? super Cat> pet1 = new Pet<Animal>();
Pet<? super Dog> pet2 = new Pet<>();
Pet<? super Cat> pet3 = new Pet<Object>(); //編譯出錯?
Animal animal1 = pet1.get();
Animal animal2 = pet2.get();
}
??可直接記住結論:
? extends T→ “只能讀(讀出是T或類型上限)不能寫”
? super T→ “能寫(T和T的子類),讀出來只能是Object或者類型上限”
類型通配符和類型參數的區別:
| 用法 | 位置 | 意義 | 是否允許 |
|---|---|---|---|
? extends Cat |
通配符 | 接受某類或子類(只讀) | ? 允許 |
? super Cat |
通配符 | 接受某類或父類(只寫) | ? 允許 |
T extends Cat |
類型參數 | 限制上界,T至少是某類 | ? 允許 |
T super Cat |
類型參數 | 企圖限制下界(但 Java 不支持) | ? 不允許 |

浙公網安備 33010602011771號