前言
整理一下Java泛型的相關知識,算是比較基礎的,希望大家一起學習進步。
一、什麼是Java泛型
Java 泛型(generics)是 JDK 5 中引入的一個新特性,其本質是參數化類型,解決不確定具體對象類型的問題。其所操作的數據類型被指定爲一個參數(type parameter)這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。
泛型類
泛型類(generic class) 就是具有一個或多個類型變量的類。一個泛型類的簡單例子如下:
//常見的如T、E、K、V等形式的參數常用於表示泛型,編譯時無法知道它們類型,實例化時需要指定。
public class Pair <K,V>{
private K first;
private V second;
public Pair(K first, V second) {
this.first = first;
this.second = second;
}
public K getFirst() {
return first;
}
public void setFirst(K first) {
this.first = first;
}
public V getSecond() {
return second;
}
public void setSecond(V second) {
this.second = second;
}
public static void main(String[] args) {
// 此處K傳入了Integer,V傳入String類型
Pair<Integer,String> pairInteger = new Pair<>(1, "第二");
System.out.println("泛型測試,first is " + pairInteger.getFirst()
+ " ,second is " + pairInteger.getSecond());
}
}
運行結果如下:
泛型測試,first is 1 ,second is 第二
泛型接口
泛型也可以應用於接口。
public interface Generator<T> {
T next();
}
實現類去實現這個接口的時候,可以指定泛型T的具體類型。
指定具體類型爲Integer的實現類:
public class NumberGenerator implements Generator<Integer> {
@Override
public Integer next() {
return new Random().nextInt();
}
}
指定具體類型爲String的實現類:
public class StringGenerator implements Generator<String> {
@Override
public String next() {
return "測試泛型接口";
}
}
泛型方法
具有一個或多個類型變量的方法,稱之爲泛型方法。
public class GenericMethods {
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("字符串");
gm.f(666);
}
}
運行結果:
java.lang.String
java.lang.Integer
二、泛型的好處
Java語言引入泛型的好處是安全簡單。泛型的好處是在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,提高代碼的重用率。
我們先來看看一個只能持有單個對象的類。
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) {
this.a = a;
}
public Automobile getA() {
return a;
}
}
我們可以發現,這個類的重用性不怎樣。要使它持有其他類型的任何對象,在jdk1.5泛型之前,可以把類型設置爲Object,如下:
public class Holder2 {
private Object a;
public Holder2(Object a) {
this.a = a;
}
public Object getA() {
return a;
}
public void setA(Object a) {
this.a = a;
}
public static void main(String[] args) {
Holder2 holder2 = new Holder2(new Automobile());
//強制轉換
Automobile automobile = (Automobile) holder2.getA();
holder2.setA("測試泛型");
String s = (String) holder2.getA();
}
}
我們引入泛型,實現功能那個跟Holder2類一致的Holder3,如下:
public class Holder3<T> {
private T a;
public T getA() {
return a;
}
public void setA(T a) {
this.a = a;
}
public Holder3(T a) {
this.a = a;
}
public static void main(String[] args) {
Holder3<Automobile> holder3 = new Holder3<>(new Automobile());
holder3.setA("測試泛型");
Automobile automobile = holder3.getA();
}
}
因此,泛型的好處很明顯了:
-
不用強制轉換,因此代碼比較簡潔;(簡潔性)
-
代替Object來表示其他類型對象,與ClassCastException異常劃清界限。(安全性)
-
泛型使代碼可讀性增強。(可讀性)
三、泛型通配符
我們定義泛型時,經常碰見T,E,K,V,?等通配符。本質上這些都是通配符,是編碼時一種約定俗成的東西。當然,你換個A-Z中另一個字母表示沒有關係,但是爲了可讀性,一般有以下定義:
-
? 表示不確定的 java 類型
-
T (type) 表示具體的一個java類型
-
K V (key value) 分別代表java鍵值中的Key Value
-
E (element) 代表Element
爲什麼需要引入通配符呢,我們先來看一個例子:
class Fruit{
public int getWeigth(){
return 0;
}
}
//Apple是水果Fruit類的子類
class Apple extends Fruit {
public int getWeigth(){
return 5;
}
}
public class GenericTest {
//數組的傳參
static int sumWeigth(Fruit[] fruits) {
int weight = 0;
for (Fruit fruit : fruits) {
weight += fruit.getWeigth();
}
return weight;
}
static int sumWeight1(List<? extends Fruit> fruits) {
int weight = 0;
for (Fruit fruit : fruits) {
weight += fruit.getWeigth();
}
return weight;
}
static int sumWeigth2(List<Fruit> fruits){
int weight = 0;
for (Fruit fruit : fruits) {
weight += fruit.getWeigth();
}
return weight;
}
public static void main(String[] args) {
Fruit[] fruits = new Apple[10];
sumWeigth(fruits);
List<Apple> apples = new ArrayList<>();
sumWeight1(apples);
//報錯
sumWeigth2(apples);
}
}
我們可以發現,Fruit[]與Apple[]是兼容的。 List<Fruit>
與 List<Apple>
不兼容的,集合List是不能協變的,會報錯,而 List<Fruit>
與 List<?extendsFruits>
是OK的,這就是通配符的魅力所在。通配符通常分三類:
-
無邊界通配符,如
<?>
-
上邊界限定通配符,如
<?extendsE>
; -
下邊界通配符,如
<?superE>
;
?無邊界通配符
無邊界通配符,它的使用形式是一個單獨的問號: List<?>
,也就是沒有任何限定。
看個例子:
public class GenericTest {
public static void printList(List<?> list) {
for (Object object : list) {
System.out.println(object);
}
}
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("B");
List<Integer> list2 = new ArrayList<>();
list2.add(100);
list2.add(666);
//報錯,List<?>不能添加任何類型
List<?> list3 = new ArrayList<>();
list3.add(666);
}
}
無界通配符 (<?>)
可以適配任何引用類型,看起來與原生類型等價,但與原生類型還是有區別,使用無界通配符則表明在使用泛型 。同時, List<?>list
不可以添加任何類型,因爲並不知道實際是哪種類型。但是List list因爲持有的是Object類型對象,所以可以add任何類型的對象。
上邊界限定通配符 < ? extends E>
使用 <?extendsFruit>
形式的通配符,就是上邊界限定通配符。 extends關鍵字表示這個泛型中的參數必須是 E 或者 E 的子類,請看demo:
class apple extends Fruit{}
static int sumWeight1(List<? extends Fruit> fruits) {
int weight = 0;
for (Fruit fruit : fruits) {
weight += fruit.getWeigth();
}
return weight;
}
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
sumWeight1(apples);
}
但是,以下這段代碼是不可行的:
static int sumWeight1(List<? extends Fruit> fruits){
//報錯
fruits.add(new Fruit());
//報錯
fruits.add(new Apple());
}
-
在
List<Fruit>
裏只能添加Fruit類對象及其子類對象(如Apple對象,Oragne對象),在List<Apple>
裏只能添加Apple類和其子類對象。 -
我們知道
List<Fruit>、List<Apple>
等都是List<? extends Fruit>的子類型。假設一開始傳參是List<Fruit>list
,兩個添加沒問題,那如果傳來List<Apple>list
,添加就失敗了,編譯器爲了保護自己,直接禁用添加功能了。 -
實際上,不能往
List<?extendsE>
添加任意對象,除了null。
下邊界限定通配符 < ? super E>
使用 <?superE>
形式的通配符,就是下邊界限定通配符。 super關鍵字表示這個泛型中的參數必須是所指定的類型E,或者是此類型的父類型,直至 Object。
public class GenericTest {
private static <T> void test(List<? super T> dst, List<T> src){
for (T t : src) {
dst.add(t);
}
}
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();
test(fruits, apples);
}
}
可以發現, List<?superE>
添加是沒有問題的,因爲子類是可以指向父類的,它添加並不像 List<?extendsE>
會出現安全性問題,所以可行。
四、泛型擦除
什麼是類型擦除
什麼是Java泛型擦除呢? 先來看demo:
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
/* Output
true
*/
日常開發中, ArrayList<Integer>
和 ArrayList<String>
很容易被認爲是不同的類型。但是這裏輸出結果是true,這是因爲Java泛型是使用擦除實現的,不管是 ArrayList<Integer>()
還是 newArrayList<String>()
,在編譯生成的字節碼中都不包含泛型中的類型參數,即都擦除成了ArrayList,也就是被擦除成“原生類型”,這就是泛型擦除。
類型擦除底層
Java泛型在編譯期完成,它是依賴編譯器實現的。其實,編譯器主要做了這些工作:
-
set()方法的類型檢驗
-
get()處的類型轉換,編譯器插入了一個checkcast語句,
再看個例子:
public class GenericTest<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
public static void main(String[] args) {
GenericTest<String> test = new GenericTest<String>();
test.set("jay@huaxiao");
String s = test.get();
System.out.println(s);
}
}
/* Output
jay@huaxiao
*/
javap -c GenericTest.class反編譯GenericTest類可得
public class generic.GenericTest<T> {
public generic.GenericTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public T get();
Code:
0: aload_0
1: getfield #2 // Field t:Ljava/lang/Object;
4: areturn
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field t:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class generic/GenericTest
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String jay@huaxiao
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
25: aload_2
26: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
29: return
}
-
看第11,set進去的是原始類型Object(#6);
-
看第15,get方法獲得也是Object類型(#7),說明類型被擦出了。
-
再看第18,它做了一個checkcast操作,是一個String類型,強轉。
五、泛型的限制與侷限
使用Java泛型需要考慮以下一些約束與限制,其實幾乎都跟泛型擦除有關。
不能用基本類型實例化類型化參數
不能用類型參數代替基本類型。因此, 沒有 Pair<double>
, 只 有 Pair<Double>
。 當然, 其原因是類型擦除。擦除之後, Pair 類含有 Object 類型的域, 而 Object 不能存儲 double值。
運行時類型查詢只適用於原始類型
如,getClass()方法等只返回原始類型,因爲JVM根本就不知道泛型這回事,它只知道原始類型。
if(a instanceof Pair<String>) //ERROR,僅測試了a是否是任意類型的一個Pair,會看到編譯器ERROR警告
if(a instanceof Pair<T>) //ERROR
Pair<String> p = (Pair<String>) a;//WARNING,僅測試a是否是一個Pair
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass() == employeePair.getClass()) //會得到true,因爲兩次調用getClass都將返回Pair.class
不能創建參數化類型的數組
不能實例化參數化類型的數組, 例如:
Pair<String>[] table = new Pair<String>[10]; // Error
不能實例化類型變量
不能使用像 new T(...),newT[...] 或 T.class 這樣的表達式中的類型變量。例如, 下面的 Pair<T>
構造器就是非法的:
public Pair() { first = new T(); second = new T(); } // Error
使用泛型接口時,需要避免重複實現同一個接口
interface Swim<T> {}
class Duck implements Swim<Duck> {}
class UglyDuck extends Duck implements Swim<UglyDuck> {}
可以消除對受查異常的檢查
@SuppressWamings("unchecked")
public static <T extends Throwable〉void throwAs(Throwable e) throws T { throw (T) e; }
定義API返回報文時,儘量使用泛型;
public class Response<T> extends BaseResponse {
private static final long serialVersionUID = -xxx;
private T data;
private String code;
public Response() {
}
public T getData() {
return this.data;
}
public void setData(T data,String code ) {
this.data = data;
this.code = code;
}
}
六、Java泛型常見面試題
Java泛型常見幾道面試題
-
Java中的泛型是什麼 ? 使用泛型的好處是什麼?(第一,第二小節可答)
-
Java的泛型是如何工作的 ? 什麼是類型擦除 ? (第四小節可答)
-
什麼是泛型中的限定通配符和非限定通配符 ? (第三小節可答)
-
List list和List <?>之間有什麼區別 (第三小節可答)
-
你瞭解泛型通配符與上下界嗎?(第三小節可答)
個人公衆號
-
如果你是個愛學習的好孩子,可以關注我公衆號,一起學習討論。
-
如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公衆號,私聊我,大家一起學習進步哈。