在咱們的實際工做中 泛型(Generics) 是無處不在的,咱們也寫過很多,看到的更多,如,源碼、開源框架... 隨處可見,可是,咱們真正理解泛型嗎?理解多少呢?例如:Box
、Box<Object>
、Box<?>
、Box<T>
、Box<? extends T>
、Box<? super T>
之間的區別是什麼?本篇文章將會對 泛型(Generics) 進行全面的解析,讓咱們對泛型有更深刻的理解。html
本篇文章的示例代碼放在 Github 上,全部知識點,如圖:java
首先,經過一個盤子裝水果小故事來打開咱們的泛型探索之旅(咱們爲何要使用泛型),故事場景以下:android
Lucy 到 James 家作客,James 須要招待客人,且知道 Lucy 喜歡吃橘子🍊,因而使用水果盤裝滿了🍊來招待客人
這個場景怎麼用代碼表現呢,咱們來新建幾個類,以下:git
Fruit:水果類github
package entity; public class Fruit { @Override public String toString() { return "This is Fruit"; } }
Apple:蘋果類,繼承水果類數組
package entity; public class Apple extends Fruit { @Override public String toString() { return " Apple 🍎"; } }
Orange:橘子類,繼承水果類安全
package entity; public class Orange extends Fruit { @Override public String toString() { return " Orange 🍊"; } }
Plate:水果盤接口bash
package entity; public interface Plate<T> { public void set(T t); public T get(); }
FruitPlate:水果盤類,實現水果盤接口app
package entity; import java.util.ArrayList; import java.util.List; public class FruitPlate implements Plate { private List items = new ArrayList(6); @Override public void set(Object o) { items.add(o); } @Override public Fruit get() { int index = items.size() - 1; if(index >= 0) return (Fruit) items.get(index); return null; } }
AiFruitPlate:智能水果盤,實現水果盤接口框架
package entity; import java.util.ArrayList; import java.util.List; /** * 使用泛型類定義 * @param <T> */ public class AiFruitPlate<T> implements Plate<T> { private List<T> fruits = new ArrayList<T>(6); @Override public void set(T t) { fruits.add(t); } @Override public T get() { int index = fruits.size() - 1; if(index >= 0) return fruits.get(index); return null; } }
Person:人類
package entity; public class Person { }
Lucy:Lucy類,繼承 Person 類,她擁有吃橘子的能力 eat
import entity.Orange; import entity.Person; public class Lucy extends Person { public void eat(Orange orange) { System.out.println("Lucy like eat" + orange); } }
James:James類,繼承 Person 類,他擁有獲取水果盤的能力 getAiFruitPlate
import entity.*; public class James extends Person { public FruitPlate getPlate() { return new FruitPlate(); } public AiFruitPlate getAiFruitPlate() { return new AiFruitPlate(); } public void addFruit(FruitPlate fruitPlate, Fruit fruit) { fruitPlate.set(fruit); } public void add(AiFruitPlate<Orange> aiFruitPlate, Orange orange) { aiFruitPlate.set(orange); } }
Scenario:測試類
import entity.*; public class Scenario { public static void main(String[] args) { scenario1(); scenario2(); } //沒有使用泛型 private static void scenario1() { James james = new James(); Lucy lucy = new Lucy(); FruitPlate fruitPlate = james.getPlate(); // James 拿出水果盤 james.addFruit(fruitPlate,new Orange()); // James 往水果盤裏裝橘子 lucy.eat((Orange) fruitPlate.get()); // 須要轉型爲 Orange } //使用了泛型 private static void scenario2() { James james = new James(); Lucy lucy = new Lucy(); AiFruitPlate<Orange> aiFruitPlate = james.getAiFruitPlate(); // James 拿出智能水果盤(知道你須要裝橘子) james.add(aiFruitPlate, new Orange()); // James 往水果盤裏裝橘子(若是,裝的不是橘子會提醒) lucy.eat(aiFruitPlate.get()); // 不須要轉型 } }
運行結果,以下:
Lucy like eat Orange 🍊 Lucy like eat Orange 🍊 Process finished with exit code 0
咱們能夠很明顯的看出,使用了泛型以後,不須要類型轉換,若是,咱們把 scenario1()
方法,稍微改下,以下:
private static void scenario1() { James james = new James(); Lucy lucy = new Lucy(); FruitPlate fruitPlate = james.getPlate(); james.addFruit(fruitPlate,new Apple()); //new Orange() 改爲 new Apple() lucy.eat((Orange) fruitPlate.get()); }
編譯器不會提示有問題,可是運行以後報錯,以下:
Exception in thread "main" java.lang.ClassCastException: entity.Apple cannot be cast to entity.Orange at Scenario.scenario1(Scenario.java:21) at Scenario.main(Scenario.java:7) Process finished with exit code 1
而,咱們把 scenario2()
(使用了泛型)作出一樣的修改,以下:
private static void scenario2() { James james = new James(); Lucy lucy = new Lucy(); AiFruitPlate<Orange> aiFruitPlate = james.getAiFruitPlate(); james.add(aiFruitPlate, new Apple()); lucy.eat(aiFruitPlate.get()); }
編譯器,會提示咱們有錯誤,如圖:
經過以上案例,很清晰的知道咱們爲何要使用泛型,以下:
泛型類是經過類型進行參數化的類,這樣說可能不是很好理解,以後咱們用代碼演示。
首先,咱們來定義一個普通的類,以下:
package definegeneric; public class SimpleClass { private Object object; public Object getObject() { return object; } public void setObject(Object object) { this.object = object; } }
它的 get
、set
方法接受和返回一個 Object
,因此,咱們能夠隨意的傳遞任何類型。在編譯時沒法檢查類型的使用,咱們能夠傳入 Integer
且取出 Integer
,也能夠傳入 String
,從而容易致使運行時錯誤。
泛型類的定義格式以下:
class name<T1,T2,...,Tn>{ ... }
在類名以後的 <>
尖括號,稱之爲類型參數(類型變量),定義一個泛型類就是使用 <>
給它定義類型參數:T一、T2 ... Tn。
而後,咱們把 SimpleClass
改爲泛型類,以下:
package definegeneric; public class GenericClass<T> { private T t; public T getT() { return t; } public void setT(T t) { this.t = t; } }
因此的 object
都替換成爲 T
,類型參數能夠定義爲任何的非基本類型,如:class類型、interface類型、數組類型、甚至是另外一個類型參數。
要想使用泛型類,必須執行泛型類調用,如:
GenericClass<String> genericClass;
泛型類的調用相似於方法的調用(傳遞了一個參數),可是,咱們沒有將參數傳遞給方法,而是,將類型參數(String)傳遞給了 GenericClass
類自己。
此代碼不會建立新的 GenericClass
對象,它只是聲明瞭 genericClass
將保存對 String
的引用
要實例化此類,要使用 new
關鍵字,如:
GenericClass<String> genericClass = new GenericClass<String>();
或者
GenericClass<String> genericClass = new GenericClass<>();
在 Java SE 7 或更高的版本中,編譯器能夠從上下文推斷出類型參數,所以,可使用 <>
替換泛型類的構造函數所需的類型參數
咱們的類型參數是否必定要寫成 T
呢,按照規範,類型參數名稱是單個大寫字母。
經常使用的類型參數名稱有,如:
類型參數 | 含義 |
---|---|
E | Element |
K | Key |
N | Number |
V | Value |
S,U,V... | 2nd, 3rd, 4th type |
泛型類能夠有多個類型參數,如:
public interface MultipleGeneric<K,V> { public K getKey(); public V getValue(); } public class ImplMultipleGeneric<K, V> implements MultipleGeneric<K, V> { private K key; private V value; public ImplMultipleGeneric(K key, V value) { this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } public static void main(String[] args) { MultipleGeneric<String, Integer> m1 = new ImplMultipleGeneric<String, Integer>("per",6); System.out.println("key:" + m1.getKey() + ", value:" + m1.getValue()); MultipleGeneric<String,String> m2 = new ImplMultipleGeneric<String, String>("per","lsy"); System.out.println("key:" + m2.getKey() + ", value:" + m2.getValue()); } }
輸出結果:
key:per, value:6 key:per, value:lsy Process finished with exit code 0
如上代碼,new ImplMultipleGeneric
將 K
實例化爲 String
,將 V
實例化爲 Integer
,所以, ImplMultipleGeneric
構造函數參數類型分別爲 String
和 Integer
,在編寫 new ImplMultipleGeneric
代碼時,編輯器會自動填寫 <>
的值
因爲,Java 編譯器會從聲明 ImplMultipleGeneric
推斷出 K
和 V
的類型,所以咱們能夠簡寫爲,以下:
MultipleGeneric<String, Integer> m1 = new ImplMultipleGeneric<>("per",6); System.out.println("key:" + m1.getKey() + ", value:" + m1.getValue()); MultipleGeneric<String,String> m2 = new ImplMultipleGeneric<>("per","lsy"); System.out.println("key:" + m2.getKey() + ", value:" + m2.getValue());
定義泛型接口和定義泛型類類似(泛型類的技術可同用於泛型接口),以下:
interface name<T1,T2,...,Tn>{ ... }
咱們來定義一個泛型接口,以下:
package definegeneric; public interface Genertor<T> { public T next(); }
那麼,如何實現一個泛型接口呢,咱們使用兩種方式來實現泛型接口,以下:
使用泛型類,實現泛型接口,且不指定確切的類型參數,因此,實現的 next()
返回值自動變成 T
package definegeneric.impl; import definegeneric.Genertor; public class ImplGenertor<T> implements Genertor<T> { @Override public T next() { return null; } }
使用普通類,實現泛型接口,且指定確切的類型參數爲 String
,因此,實現的 next()
返回值自動變成 String
package definegeneric.impl; import definegeneric.Genertor; public class ImplGenertor2 implements Genertor<String> { @Override public String next() { return null; } }
泛型方法使用了類型參數的方法,泛型方法比較獨立,能夠聲明在 普通類、泛型類、普通接口、泛型接口中。
泛型方法定義格式,以下:
public <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2)
泛型方法的類型參數列表,在 <>
內,該列表必須在方法返回類型以前;對於靜態的泛型方法,類型參數必須在 static
以後,方法返回類型以前。
咱們在普通類中定義泛型方法,以下:
package methodgeneric; public class MethodGeneric { //定義一個泛型方法 public <T> T genericMethod(T...t) { return t[t.length/2]; } public static void main(String[] args) { MethodGeneric methodGeneric = new MethodGeneric(); System.out.println(methodGeneric.<String>genericMethod("java","dart","kotlin")); } }
methodGeneric.<String>genericMethod("java","dart","kotlin")
一般能夠省略掉 <>
的內容,編譯器將推斷出所需的類型,和調用普通方法同樣,如:
methodGeneric.genericMethod("java","dart","kotlin")
咱們在泛型類中定義泛型方法,以下:
package methodgeneric; public class MethodGeneric2 { static class Fruit{ @Override public String toString() { return "fruit"; } } static class Apple extends Fruit { @Override public String toString() { return "Apple"; } } static class Person{ @Override public String toString() { return "person"; } } //定義了泛型類 static class ShowClass<T> { //定義了普通方法 public void show1(T t){ System.out.println(t.toString()); } //定義了泛型方法 public <E> void show2(E e) { System.out.println(e.toString()); } //定義了泛型方法 public <T> void show3(T t) { System.out.println(t.toString()); } } public static void main(String[] args) { Apple apple = new Apple(); Person person = new Person(); ShowClass<Fruit> showClass = new ShowClass<>(); showClass.show1(apple); //能夠放入 apple,由於 apple 是 fruit 的子類 showClass.show1(person); //此時,編譯器會報錯,由於 ShowClass<Fruit> 已經限定類型 showClass.show2(apple); //能夠放入,泛型方法 <E> 能夠是任何非基本類型 showClass.show2(person);//能夠放入,泛型方法 <E> 能夠是任何非基本類型 showClass.show3(apple); //能夠放入,泛型方法 <T> 和泛型類中的 <T> 不是同一條 T,能夠是任何非基本類型 showClass.show3(person); //能夠放入,泛型方法 <T> 和泛型類中的 <T> 不是同一條 T,能夠是任何非基本類型 } }
在泛型類中定義泛型方法時,須要注意,泛型類裏的泛型參數 <T>
和泛型方法裏的泛型參數 <T>
不是同一個。
咱們常常看到相似 public <U extends Number> void inspect(U u)
的代碼,<U extends Number>
就是限制類型參數,只對數字進行操做且只接受 Number
或其子類。
要聲明一個限定的類型參數,須要在參數類型後加上 extends
關鍵字,而後是其上限類型(類或接口)。
泛型類也可使用限定類型參數,以下:
package boundedgeneric; public class BoundedClass<T extends Comparable> { private T t; public void setT(T t) { this.t = t; } public T min(T outter){ if(this.t.compareTo(outter) > 0) return outter; else return this.t; } public static void main(String[] args) { BoundedClass<String> boundedClass = new BoundedClass<>(); //只能傳入實現了 Comparable 接口的類型 boundedClass.setT("iOS"); System.out.println(boundedClass.min("android")); } }
泛型方法也可使用限定類型參數,以下:
package boundedgeneric; public class BoundedGeneric { public static <T extends Comparable> T min(T a, T b) { if (a.compareTo(b) < 0) return a; else return b; } public static void main(String[] args) { System.out.println(BoundedGeneric.min(66,666)); } }
限定類型參數,也能夠爲多個限定,如:
<T extends B1 & B2 & B3>
多個限定參數,若是其中有類,類必須放在第一個位置,例如:
interface A { ... } interface B { ... } class C { ... } class D <T extends C & A & B>
在前面的盤子裝水果小故事裏咱們已經建立好了一些水果類,以下:
public class Fruit { @Override public String toString() { return "This is Fruit"; } } public class Apple extends Fruit { @Override public String toString() { return " Apple 🍎"; } } public class Orange extends Fruit { @Override public String toString() { return " Orange 🍊"; } } public class QIOrange extends Orange { @Override public String toString() { return "qi Orange 🍊"; } }
他們的繼承關係,如圖:
衆所周知,咱們能夠把子類賦值給父類,例如:
Apple apple = new Apple(); Fruit fruit = new Fruit(); fruit = apple;
泛型也是如此,咱們定義一個水果盤子的泛型類,以下:
public class FruitPlateGen<Fruit> implements Plate<Fruit> { private List<Fruit> fruits = new ArrayList<>(6); @Override public void set(Fruit fruit) { fruits.add(fruit); } @Override public Fruit get() { int index = fruits.size() - 1; if(index >= 0) return fruits.get(index); return null; } }
因此,是 Fruit
的子類均可以放入水果盤裏,以下:
FruitPlateGen<Fruit> fruitPlate = new FruitPlateGen<Fruit>(); fruitPlate.set(new Apple()); fruitPlate.set(new Orange());
如今,James 能夠獲取盤子,以下:
public class James extends Person { public FruitPlateGen getAiFruitPlateGen(FruitPlateGen<Fruit> plate) { return new FruitPlateGen(); } }
如是,James 想獲取放橘子的盤子,以下:
James james = new James(); james.getAiFruitPlateGen(new FruitPlateGen<Fruit>()); //獲取成功 james.getAiFruitPlateGen(new FruitPlateGen<Orange>()); //編譯器報錯
雖然,Orange
是 Fruit
的子類,可是,FruitPlateGen<Orange>
不是 FruitPlateGen<Fruit>
的子類,因此,不能傳遞產生繼承關係。
咱們能夠經過繼承(extends)或實現(implements)泛型類或接口,例如:
private static class ExtendFruitPlate<Orange> extends FruitPlateGen<Fruit> { }
此時,ExtendFruitPlate<Orange>
就是 FruitPlateGen<Fruit>
的子類,James 再去拿盤子,就不會有錯誤提示:
james.getAiFruitPlateGen(new ExtendFruitPlate<Orange>());
咱們常常看到相似 List<? extends Number>
的代碼,?
就是通配符,表示未知類型。
咱們可使用上限通配符來放寬對變量的限制,例如,上文提到的 FruitPlateGen<Fruit>
和 FruitPlateGen<Orange>()
就可使用上限通配符。
咱們來改寫一下 getAiFruitPlateGen
方法,以下:
public FruitPlateGen getAiFruitPlateGen2(FruitPlateGen<? extends Fruit> plate) { return new FruitPlateGen(); }
這時候,James 想獲取放橘子的盤子,以下:
James james = new James(); james.getAiFruitPlateGen2(new FruitPlateGen<Fruit>()); //獲取成功 james.getAiFruitPlateGen2(new FruitPlateGen<Orange>()); //獲取成功
上限通配符 FruitPlateGen<? extends Fruit>
匹配 Fruit
和 Fruit
的任何子類型,因此,咱們能夠傳入 Apple
、Orange
都沒有問題。
上限通配符將未知類型限定爲該類型或其子類型,使用 extends
關鍵字,而下限通配符將未知類型限定爲該類型或其父類型,使用 super
關鍵字。
咱們再來寬展一下 getAiFruitPlateGen
方法,以下:
public FruitPlateGen getAiFruitPlateGen3(FruitPlateGen<? super Apple> plate) { return new FruitPlateGen(); }
這時候,James 只能獲取 FruitPlateGen<Fruit>
和 FruitPlateGen<Apple>
的盤子,以下:
James james = new James(); james.getAiFruitPlateGen3(new FruitPlateGen<Apple>()); james.getAiFruitPlateGen3(new FruitPlateGen<Fruit>());
下限通配符 FruitPlateGen<? super Apple>
匹配 Apple
和 Apple
的任何父類型,因此,咱們能夠傳入 Apple
、Fruit
。
在 泛型,繼承和子類型 章節有講到,雖然,Orange
是 Fruit
的子類,可是,FruitPlateGen<Orange>
不是 FruitPlateGen<Fruit>
的子類。可是,你可使用通配符在泛型類或接口之間建立關係。
咱們再來回顧下 Fruit
的繼承關係,如圖:
代碼,以下:
Apple apple = new Apple(); Fruit fruit = apple;
這個代碼是沒有問題的,Fruit
是 Apple
的父類,因此,能夠把子類賦值給父類。
代碼以下:
List<Apple> apples = new ArrayList<>(); List<Fruit> fruits = apples; // 編輯器報錯
由於,List<Apple>
不是 List<Fruit>
的子類,實際上這二者無關,那麼,它們的關係是什麼?如圖:
List<Apple>
和 List<Fruit>
的公共父級是 List<?>
。
咱們可使用上下限通配符,在這些類之間建立關係,以下:
List<Apple> apples = new ArrayList<>(); List<? extends Fruit> fruits1 = apples; // OK List<? super Apple> fruits2 = apples; // OK
下圖展現了上下限通配符聲明的幾個類的關係,如圖:
在上文中有 FruitPlateGen
水果盤子的類,咱們嘗試使用上下限通配符來實例化水果盤,代碼以下:
Apple apple = new Apple(); Orange orange = new Orange(); Fruit fruit = new Fruit(); FruitPlateGen<? extends Fruit> fruitPlateGen = new FruitPlateGen<>(); fruitPlateGen.set(apple); // error fruitPlateGen.set(orange); // error fruitPlateGen.set(fruit); // error Fruit fruit1 = fruitPlateGen.get(); // OK Orange orange1 = fruitPlateGen.get(); // error Apple apple1 = fruitPlateGen.get(); // error
上限通配符沒法 set
數據,可是,能夠 get
數據且只能 get
到其上限 Fruit
,因此,上限通配符能夠安全的訪問數據。
在來看一下代碼,以下:
FruitPlateGen<? super Apple> fruitPlateGen1 = new FruitPlateGen<>(); fruitPlateGen1.set(apple); // OK fruitPlateGen1.set(orange); // error fruitPlateGen1.set(fruit); // error Object object = fruitPlateGen1.get(); // OK Fruit fruit2 = fruitPlateGen1.get(); // error Apple apple2 = fruitPlateGen1.get(); // error Orange orange2 = fruitPlateGen1.get(); // error
下限通配符能夠且只能 set
其下限 Apple
,也能夠 get
數據,但只能用 Object
接收(由於Object是全部類型的父類,這是一個特例),因此,下限通配符能夠安全的寫入數據。
因此,在使用上下限通配符時,能夠遵循如下準則:
Java 語言使用類型擦除機制實現了泛型,類型擦除機制,以下:
Java 編譯器在擦除過程當中,會擦除全部類型參數,若是類型參數是有界的,則替換爲第一個邊界,若是是無界的,則替換爲 Object。
咱們定義了一個泛型類,代碼以下:
public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } ... }
因爲類型參數 T
是無界的,所以,Java 編譯器將其替換爲 Object,以下:
public class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } ... }
咱們再來定義一個有界的泛型類,代碼以下:
public class Node<T extends Comparable<T>> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } ... }
Java 編譯器其替換爲第一個邊界 Comparable
,以下:
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data; } ... }
Java 編譯器一樣會擦除泛型方法中的類型參數,例如:
public static <T> int count(T[] anArray, T elem) { int cnt = 0; for (T e : anArray) }
因爲 T
是無界的,所以,Java 編譯器將其替換爲 Object,以下:
public static int count(Object[] anArray, Object elem) { int cnt = 0; for (Object e : anArray) if (e.equals(elem)) }
以下代碼:
class Shape { ... } class Circle extends Shape { ... } class Rectangle extends Shape { ... }
有一個泛型方法,以下:
public static<T extends Shape> void draw(T shape){ ... }
Java 編譯器將用第一個邊界 Shape
替換 T
,以下:
public static void draw(Shape shape){ ... }
有時類型擦除會致使沒法預料的狀況,以下:
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
類型擦除後,代碼以下:
public class Node { public Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
此時,Node 的方法變爲 setData(Object data)
和 MyNode 的 setData(Integer data)
不會覆蓋。
爲了解決此問題並保留泛型類型的多態性,Java 編譯器會生成一個橋接方法,以下:
class MyNode extends Node { // 生成的橋接方法 public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } ... }
這樣 Node 的方法 setData(Object data)
和 MyNode 生成的橋接方法 setData(Object data)
能夠完成方法的覆蓋。
爲了有效的使用泛型,須要考慮如下限制:
代碼以下:
class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } ... }
建立對象時,不能使用基本類型替換參數類型:
Pair<int, char> p = new Pair<>(8, 'a'); // error
代碼以下:
public static <E> void append(List<E> list) { E elem = new E(); // error list.add(elem); }
代碼以下:
public class MobileDevice<T> { private static T os; // error ... }
類的靜態字段是全部非靜態對象共享的變量,所以,不容許使用類型參數的靜態字段。
代碼以下:
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // error ... } }
Java 編譯器會擦除全部類型參數,全部,沒法驗證在運行時使用的參數化類型。
代碼以下:
List<Integer>[] arrayOfLists = new List<Integer>[2]; // error
代碼以下:
class MathException<T> extends Exception { ... } // error class QueueFullException<T> extends Throwable{ ... } // error
代碼以下:
public class Example { public void print(Set<String> strSet) { } public void print(Set<Integer> intSet) { } }
print(Set<String> strSet)
和 print(Set<Integer> intSet)
在類型擦除後是徹底相同的類型,因此,沒法重載。
最後,附上本身的博客和GitHub地址:以下
博客地址:https://h.lishaoy.net
GitHub地址:https://github.com/persilee