目錄
引子
好處
泛型通配符
泛型位置
出現在類或者介面上
出現在方法中使用,包括回傳值,引數
出現在變數宣告中
注意事項
泛型邊界
分類
無邊界
上界
下界
作用時機
型別擦除
部分擦除
哪些被擦除了,哪些沒被擦除?
擦除的規則
泛型介面的多型實作
橋方法
可能的疑問
引子
Java在jdk 1.5引入泛型的概念,在1.5之前沒有泛型.
以 ArrayList 為例,它的底層是一個Object型別的陣列,確保該容器可以放任何型別的元素. 比如:
/**
* 無泛型
*/
@Test
public void test01() {
ArrayList list = new ArrayList();
list.add("123");
list.add(456);
Student student = new Student();
student.setAge(18);
student.setName("夜貓");
list.add(student);
for (Object o : list) {
System.out.println(o);
}
}
這段代碼的輸出是:
123
456
Student(age=18, name=夜貓)
到目前為止看起來還不錯,我們放置了多種型別的元素,與陣列只能放同一型別的元素,且必須提前指定陣列長度相比,是一個巨大的進步.
接下來你的同事告訴你:嘿,你要的學生資訊我給你放到list里了,你要讓他們開始寫代碼.
你看了一下Student的結構,正好有一個writeCode()方法
@Data
public class Student {
private Integer age;
private String name;
public void writeCode() {
System.out.println("寫代碼");
}
你心里想,這還不簡單,我直接呼叫就好了.
for (Object o : list) {
Student s = (Student) o;
s.writeCode();
}
完美.可是運行之后發生了ClassCastException,提示你String沒法轉成Student.
原來是你的同事把很多其它的資訊也放進去了.
java.lang.ClassCastException: java.lang.String cannot be cast to com.baoly.generics.Student
這當然難不倒你,你想那我判斷一下元素型別,是student我再呼叫這個方法就可以了,于是
for (Object o : list) {
if (o instanceof Student) {
Student s = (Student) o;
s.writeCode();
}
}
直到后來,你同事向list中放了Car,Teacher,File,City等型別,于是你每種型別都添加了手動的型別判斷...
這時你想:如果能在撰寫代碼時就限制住List中存放的資料型別,那么就不用這么麻煩了,
好處
后來你的同事把這段代碼加了泛型,指定只能放Student型別,這次你終于不用手動判斷型別然后再進行強轉了.
ArrayList<Student> list = new ArrayList();
Student student = new Student();
student.setAge(18);
student.setName("夜貓");
list.add(student);
for (Student student1 : list) {
student1.writeCode();
}
可是新的問題也產生了,你現在一個Person類,一個Student類,一個Teacher類,它們之間的關系是這樣的:

這個時候我想放Person Teacher Student三種型別的資料,引入泛型之后,我是不是需要宣告三種不同泛型的List來存放它們呢?如果宣告不同型別的List,那么我們的容器數量會變得很多,我們常常有這樣的需求,List中放置的是一類物件,以及它的子類或者父類,這時又該怎么做呢?
泛型通配符
泛型統配符實際上可以理解成一個占位符,它可以用任意大寫字母表示,在實際開發程序中約定俗成的常見以下泛型(只是習慣,不是強制要求)
<K,V> 表示key,value鍵值對,常見于宣告Map容器
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
<E> 表示element,比如ArrayList的add方法宣告中
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
<T> 表示Type 即類
泛型位置
出現在類或者介面上
public class Demo<K, V> {
public K getKey(K k, V v) {
return k;
}
}
出現在方法中使用,包括回傳值,引數
public <T> T getData(T t) {
return t;
}
如果你第一次見泛型方法,可能會對它的格式有疑問,T是回傳值型別,<T>是什么?
<T>是泛型宣告,泛型需要先宣告,再使用,而出現在類上的例子,方法中的K在哪里宣告的?
答案是在類中宣告的泛型.
出現在變數宣告中
public class Demo<K, V> {
public K data;
public K getData() {
return data;
}
public void setData(K data) {
this.data = data;
}
}
注意事項
對于在類上宣告的泛型,他們可以直接被使用在
- 成員變數型別
- 實體方法回傳值型別和引數型別中
- 對于靜態方法和靜態成員變數不能直接使用
對于靜態方法,我們可以用靜態方法自己宣告的泛型型別,如:
public static <E> E getData1() {
return null;
}
出現上面這種差異的原因是:當使用泛型類時,必須在創建物件時執行型別引數的值,對于靜態方法,我們沒有創建物件,它就生效了,所以靜態方法不能使用類上宣告的泛型.
而使用泛型方法時,我們通常不用指明引數型別,因為編譯器會型別推斷出具體的引數型別,所以雖然靜態方法不能使用類上宣告的泛型,但我們依然可以使用靜態方法自己宣告的泛型型別.
泛型邊界
了解了泛型通配符之后,回到我們最開始的問題:
我想宣告一個容器List,它里面既可以放Person,也可以放Person和Teacher,或者Person和Student,那又該怎么做呢?這就需要了解泛型邊界了.
分類
無邊界 <?>
無邊界的泛型,顧名思義,它不對型別做限制,它默認的上界就是Object.
看個例子:
public void receiveList(ArrayList<?> list) {
}
@Test
public void test() {
ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
receiveList(strList);
receiveList(intList);
}
receiveList()方法接收一個無邊界泛型的ArrayList,觀察發現,宣告的intList和strList都可以接收
上界<? extends >
就是對上邊界進行限制,你傳入的型別不能超過extends的型別
看個例子:
public void receiveList(ArrayList<? extends Number> list) {
}
@Test
public void test() {
ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
ArrayList<Double> doubleList = new ArrayList<>();
receiveList(strList); // 編譯錯誤
receiveList(intList);
receiveList(doubleList);
}
上面的代碼表示receiveList只能接收泛型為Number或Number的子類的ArrayList
下界<? super >
就是對下邊界進行限制,你傳入的型別不能低于super的型別
看個例子:
public void receiveList(ArrayList<? super Integer> list) {
}
@Test
public void test() {
ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
ArrayList<Number> numberList = new ArrayList<>();
receiveList(strList); //編譯錯誤
receiveList(intList);
receiveList(numberList);
}
作用時機
泛型約束只有在編譯期間有效,在運行期間都會被擦除,擦除規則請繼續閱讀.不過在此之前,我們先看一段代碼.
@Test
public void test() throws Exception {
List<Integer> intList = new ArrayList<>();
intList.add(123);
Class<? extends List> clazz = intList.getClass();
Method addMethod = clazz.getDeclaredMethod("add",Object.class);
addMethod.invoke(intList,"name");
System.out.println(intList); // [123,name]
}
在上面的程式中,第二行我們正常添加了一個int型別的123,然后在運行期間,我們通過反射,向"intList"中添加了一個字面值為name的字串.也就是說泛型Integer的限制并未在運行期生效.
型別擦除
先看以下的代碼:
@Test
public void test() {
ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // true
}
intList 和strList getClass()的結果都是java.util.ArrayList.
也就是說:泛型只有在靜態型別檢查時才生效,在此之后,部分泛型型別都會被擦除.即編譯擦除.
部分擦除
注意,Java中并不是所有用到泛型的地方都會進行泛型擦除,
我們先來看一個例子
public class Person<T> {
}
@Test
public void test() throws Exception {
Person<String> person = new Person<>();
TypeVariable<? extends Class<? extends Person>>[] typeParameters =
person.getClass().getTypeParameters();
System.out.println(Arrays.toString(typeParameters));//T
}
上面的代碼運行結果為T,這也就是說,雖然我們傳遞的是String,但實際上拿到的只是一個占位符.到目前為止,我們無法取得與T系結的泛型資訊.
再看一個例子
public class Student extends Person<String> {
}
@Test
public void test() throws Exception {
Type type = Student.class.getGenericSuperclass();
System.out.println(type.toString()); // com.baoly.Person<java.lang.String>
}
上面代碼的運行結果是 com.baoly.Person<java.lang.String>,我們拿到了傳遞給Person的型別資訊.
哪些被擦除了,哪些沒被擦除?
以下資訊將會被保留
-
泛型類和泛型介面上的宣告
-
泛型方法的引數和回傳值的宣告
其余的泛型資訊被擦除了.
這也就是為什么我們可以通過用非泛型類繼承泛型類之后取得泛型資訊的原因.
擦除的規則
- 對于未指定邊界的泛型,擦除為Object
- 對于指定邊界的泛型,將擦除為它的非泛型上界
比如List<T>擦除為List,List<T extends People> 擦除為People,等等.
泛型介面的多型實作
了解了泛型擦除,我們來思考一下它可能帶來的問題,先來看一下代碼
public interface Base<T> {
void setItem(T t);
T getItem();
}
public class BaseImpl implements Base<Integer> {
@Override
public void setItem(Integer integer) {
}
@Override
public Integer getItem() {
return null;
}
}
我們先定義了一個泛型介面T,在子類(實作類)實作的時候,不通的子類會傳遞自己需要的型別引數,觀察上面的BaseImpl宣告,我們發現,由于子類實作父介面時給定了引數型別,使得子類中的引數和方法回傳值都有了具體的型別,本例中是Integer.
我們知道,想要實作多型,其中一個必要條件是子類重寫父類方法.可由于型別擦除,父介面中的T會被擦除成Object,而子類的型別引數是Integer,泛型是不是就破壞了多型了.
橋方法
為了解決上面出現的問題,Java使用橋接方法去實作多型,它的思路是:
在編譯階段進行引數泛化,生成橋方法,既然介面T被擦除成Object,那么它就生成一個引數為Object 型別的setItem 方法,再生成一個回傳值為Object型別的getItem方法,在生成的橋接方法內部,呼叫子類實際重寫的setItem(Integer integer) 和 Integer getItem()來達到多型的目的,
看一下BaseImpl.class
public com.baoly.BaseImpl();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/baoly/BaseImpl;
public void setItem(java.lang.Integer);
descriptor: (Ljava/lang/Integer;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/baoly/BaseImpl;
0 1 1 integer Ljava/lang/Integer;
public java.lang.Integer getItem();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/baoly/BaseImpl;
public java.lang.Object getItem();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method getItem:()Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/baoly/BaseImpl;
public void setItem(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/Integer
5: invokevirtual #4 // Method setItem:(Ljava/lang/Integer;)V
8: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/baoly/BaseImpl;
}
Signature: #24 // Ljava/lang/Object;Lcom/baoly/Base<Ljava/lang/Integer;>;
SourceFile: "BaseImpl.java"
我們可以發現,BaseImpl自己寫了兩個方法,在編譯階段,又生成了兩個橋接方法
觀察
flags: ACC_BRIDGE
invokevirtual 呼叫的method是我們自己寫的方法
可能的疑問
生成橋接方法,不會導致類本身方法沖突么?
不會,因為在jvm層面,方法的簽名包含方法回傳值,從而避免了橋方法和本身方法的沖突問題.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/337623.html
標籤:其他
