泛型程式設計意味著撰寫的代碼可以被很對不同型別的物件所重用,
簡單使用
簡單泛型類
一個泛型類是具有一個或多個型別變數的類,
型別變數常使用大寫形式,并且一般較短,通常使用E表示集合的元素型別,使用K和V分別表示關鍵字與值的型別,使用T表示任意型別,
例如:
public class SimpleGenericClass<T> {
private T first;
private T second;
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
@Override
public String toString() {
return "SimpleGenericClass{" +
"first=" + first +
", second=" + second +
'}';
}
public static void main(String[] args) {
SimpleGenericClass<String> StringGen = new SimpleGenericClass<>();
StringGen.setFirst("first");
StringGen.setSecond("second");
System.out.println(StringGen);
}
}
泛型方法
型別變數放在修飾符的后面,回傳型別的前面,泛型方法可以定義在普通類中,也可以定義在泛型類中,呼叫泛型方法是時在方法名前的尖括號中放入具體的型別
例如:
public class SimpleGenericMethod {
public static <T> T getData(T t1, T t2) {
return t2;
}
public static void main(String[] args) {
System.out.println(SimpleGenericMethod.<String>getData("Str1", "Str2"));
}
}
型別變數的限定
我們可以對型別變數加一些限定,比如需要實作指定的介面或者繼承自指定的類,統一使用extends關鍵字限定型別變數,如果有多個限定應使用&分隔,如果限定型別使用了類,那他必須放在第一個,
例如:
//定義介面interA
interface interA {
public String Print1();
}
//定義介面interB
interface interB {
public String Print2();
}
//定義類A,實作了A介面
class A implements interA {
String str;
public A(String str) {
this.str = str;
}
public String Print1() {
return "print1 " + str;
}
}
//定義類B,實作了B介面
class B implements interB {
String str;
public B(String str) {
this.str = str;
}
@Override
public String Print2() {
return "print2 " + str;
}
}
//定義類AB,實作了A介面和B介面
class AB implements interA, interB {
String str;
public AB(String str) {
this.str = str;
}
@Override
public String Print1() {
return "print1 " + str;
}
@Override
public String Print2() {
return "print2 " + str;
}
}
public class GenericRestrict {
//為型別變數加了限定,只有同時實作了A介面和B介面的類才可以使用該泛型方法
public static <T extends interA & interB> void fun(T t1) {
System.out.println(t1.Print1());
System.out.println(t1.Print2());
}
public static void main(String[] args) {
AB ab = new AB("ab");
GenericRestrict.<AB>fun(ab);
}
}
泛型與虛擬機
Java虛擬機并不存在泛型的概念,Java泛型只存在于原始碼中,編譯后的位元組碼檔案中的全部泛型都被替換為原始型別,
型別擦除
對于一個泛型型別,虛擬機都自動提供一個相應的原始型別,擦除型別變數,并替換為限定型別(沒有限定就替換為Object)
| 泛型型別 | 原始型別 |
|---|---|
| List |
List |
| T | Object |
| T extends Person & Comparable | Person |
翻譯泛型
如果虛擬機對回傳型別進行了擦除,就需要加上合適的強制型別轉換,
型別擦除還會帶來一個問題,例如對于一個泛型類:
public class Person<T> {
private T information;
public void setInformation(T information) {
this.information = information;
}
public T getInformation() {
return information;
}
}
定義一個MyPerson類,繼承了Person<String>
public class MyPerson extends Person<String> {
@Override
public String getInformation() {
return super.getInformation();
}
@Override
public void setInformation(String information) {
super.setInformation(information);
}
}
MyPerson重寫了setInformation(String)方法,但是經過虛擬機擦除后,Person類有一個需要Object引數的setInformation方法,顯然,MyPerson中的setInformation(String)方法與setInformation(Object)是兩個不一樣的方法,為了保持泛型類的多型性,編譯器會自動生成橋方法,確保MyPerson物件呼叫正確的方法,
編譯器自動生成的橋方法如下:
public void setInformation(Object information) {
setInformation((String) information);
}
還有一個問題,類似的MyPerson中的setInformation方法也會有兩個,但是只是回傳值不一樣,Java無法通過回傳值型別區分不同的方法,但是在虛擬機中實際是通過引數型別和回傳值型別來確定一個方法的,因此仍可以利用橋方法實作多型,
public String getInformation() {...}
public Object getInformation() {return getInformation()}
橋方法不僅用于泛型型別,在一個方法覆寫另一個方法時可以指定一個更嚴格的回傳值型別,這時就使用了橋方法保持了多型性,
總結
- 虛擬機沒有泛型,只有普通的類和方法
- 所有的型別引數都有他們的限定型別替換
- 橋方法被合成來保持多型
- 為保持型別安全性,必要時插入強制型別轉換
約束與限制
在使用Java泛型時有一些限制,主要是型別擦除引起的
-
不能用基本型別實體化型別引數
-
運行時型別查詢只適用于原始型別
由于虛擬機會進行型別擦除,所以型別查詢只能查詢到原始型別,在Java中不能使用instanceof查詢泛型型別,使用getClass也只會得到原始型別,
- 不能創建引數化型別的陣列
只是不允許創建這些陣列,而聲名型別為Pair
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
但是這樣做并不安全,
如果需要收集引數化型別物件,只有一種安全有效的方法:使用ArrayList:ArrayList<Pair
- Varargs警告
當使用可變數量的引數化型別引數時,Java虛擬機會自動創建一個引數型別的陣列,這違反了前面的規定,但是此時規則有所放松,你只會得到一個警告,
可以采用兩種方法抑制這個警告,一是呼叫方法前增加注解@SuppressWarnings("unchecked"),二是在定義方法前使用注解@SafeVarargs,
- 不能實體化型別變數
不能使用像new T(...),new T[...]或T.class這樣的運算式中的型別變數,對于下面的一個類Pair
public Pair<T> {
T first;
T second;
}
下面的構造器是非法的
public Pair() {first = new T(); second = new T();}
在Java8之后,最好的方法就是利用Lambda運算式
Pair(T first, T second) {
this.first = first;
this.second = second;
}
public static <T> Pair<T> makePair(Supplier<T> constr) {
//傳入T型別的建構式,創建兩個T型別的物件
//再利用Pair的拷貝構造器構造出一個Pair<T>型別的物件
return new Pair<>(constr.get(), constr.get());
}
比較老式的做法是使用反射
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.newInstance(), cl.newInstance());}
}
catch(Execption e) {
...
}
}
- 不能構造泛型陣列
考慮下面的例子
public static <T extends Comparable> T[] minmax(T[] a) {
T[] mm = new T[2];
...
}
由于型別擦除,該方法會永遠構造Comparable[2]陣列,
如果陣列僅僅作為一個類的私有域,就可以將這個陣列宣告為Object[],并且在獲取元素是進行型別轉換,例如ArrayList類可以這樣實作:但是如果回傳E[]型別的陣列就會有一些問題,
public class ArrayList<E> {
private E[] elements;
public ArrayList() {
elements = (E[])new Object[10];
}
}
最好讓用戶提供一個陣列構造其運算式,例如:
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)
T[] mm = constr.apply(2);
...
}
使用反射的話也可以
public static <T extends Comparable> T[] minmax(T... a)
T[] mm = (T[])Array.newInstance(a.getClass.getComponentType(), 2);
...
}
-
不能在靜態域或方法中參考型別變數
-
不能拋出或捕獲泛型類的實體
-
可以消除對受查例外的檢查
-
注意擦除后的沖突
通配符
在之前使用泛型時,如果給定了型別,那就固定了,Java中還允許引數型別變化,就需要用到通配符型別,
例如:
Pair<? extends Employee>
表示一系列的泛型Pair型別,但型別引數必須是Employee或其子類,比如Pair<Employee>、Pair<Manager>,但是Pair<String>并不屬于這種型別,
泛型協變與逆變
Java支持向上轉型(協變)和向下轉型(逆變),例如:
Employee manager = new Manager();
但是泛型卻和想象中的可能不同,下面這樣寫是錯誤的,
List<Employee> manager = new ArrayList<Manager>(); //error,無法通過編譯
為了獲得泛型類的“協變”,可以將參考型別設定為 ? extends 型別
為了獲得泛型類的“逆變”,可以將參考型別設定為 ? super 型別
如果將參考的泛型設為<? extends Apple>,此時這個參考可以接受Apple及其子類的容器
如果將參考的泛型設為<? super Apple>,此時這個參考可以接受Apple及其父類的容器
讀寫限制
雖然使用通配符可以實作協變逆變,但是也帶來了一些影響,主要是讀寫操作的限制,
這里所謂的讀指的是get之類的操作,將泛型型別作為函式的回傳值,
寫指的是set之類的操作,將泛型型別作為函式的引數,
-
對于型別為
List<? super Apple>,合法的行為是將something extends Apple型別賦值給? super Apple,而? super Apple型別只能賦值給Object -
對于型別為
List<? extends Apple>,合法的行為是將? extends Apple賦值給something super Apple,而只能將null型別賦值給? extends Apple,
Java中top type為Object,bottom type為null,
帶通配符的參考之間賦值必須相容
- 使用通配符的參考,可以把這種參考看作一個范圍,比如
List<?>看作從null到Object的范圍,而如果通配符帶了邊界,就只是將這個范圍縮小了, - 兩種參考
List(raw type)和List<?>(unbounded type)之間相互賦值,編譯器不會有警告, - 帶有通配符的參考,只能夠賦值給
List(raw type)或者相容的帶通配符的參考,不能賦值給帶有具體型別的參考
自己的疑惑與思考
-
對通配符的理解
首先型別變數(T)指代某一個型別,不確定是哪種型別,但是只是某一個,
通配符指代一系列的型別,表示了繼承樹上某一個范圍內的型別,
-
對代碼的理解
class MyList<T> { T first; public T getFirst() { return first; } public void setFirst(T first) { this.first = first; } public static void main(String[] args) { MyList<? extends Integer> l1; MyList<Integer> l2 = new MyList<Integer>(); l2.setFirst(111); l1 = l2; //l2 = l1; error Integer num = l1.getFirst(); } }其中的
l1 = l2;OK此處l1被定義為了
MyList<? extends Integer>型別,按照我的理解l1就是被定義為了一系列的型別,例如MyList<Integer>、MyList<Class1ExtendsInteger>、MyList<Class2ExtendsClass1>......,這里l1是MyList<Integer>型別的變數,所以可以被l1參考,但是如果想執行
l2 = l1;ERROR由于
l1的型別是MyList<Integer>、MyList<Class1ExtendsInteger>、MyList<Class2ExtendsClass1>......的,l2只是MyList<Integer>型別,無法參考MyList<Class1ExtendsInteger>、MyList<Class2ExtendsClass1>這樣型別的變數(應該知道Integer和Class1ExtendsInteger有繼承關系,但是MyList<Integer>與MyList<Class1ExtendsInteger>是沒有繼承關系的),所以編譯不會通過,而對于
Integer num = l1.getFirst();OK為什么可以這樣賦值呢?
l1的getFirst()回傳值型別為<? extends Integer>,表示了一些列的型別:Integer、Class1ExtendsInteger、Class2ExtendsClass1......與上面不同的是這些型別之前是確實存在繼承關系的,所以這一系列的物件都可以被Integer型別的變數參考,
參考資料
Java泛型 通配符詳解
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/467951.html
標籤:其他
上一篇:發現一款超牛逼的資料庫工具,IDEA 公司出品,功能超多,吊炸天。。
下一篇:SpringCloud Gateway 整合Springfox/SwaggerUI3 之后呼叫某一個服務的介面時,請求路徑不會加上對應的服務名問題
