本文內容如下:
1、 什麼是型別擦除
2、常用的 ?, T, E, K, V, N的含義
3、上界萬用字元 < ?extends E>
4、下界萬用字元 < ?super E>
5、什麼是PECS原則
6、通過一個案例來理解 ?和 T 和 Object 的區別
一、什麼是型別擦除?
我們說Java的泛型是偽泛型,那是因為泛型資訊只存在於程式碼編譯階段,在生成的位元組碼中是不包含泛型中的型別資訊的,使用泛型的時候加上型別引數,在編譯器編譯的時候會去掉,這個過程為型別擦除。
泛型是Java 1.5版本才引進的概念,在這之前是沒有泛型的,但是因為型別擦除特性,讓泛型程式碼能夠很好地和之前版本的程式碼相容。
我們來看個案例
(圖1)
因為這裡泛型定義為Integer型別集合,所以新增String的時候在編譯時期就會直接報錯。
那是不是就一定不能新增了呢?答案是否定的,我們可以通過Java泛型中的型別擦除特點及反射機制實現。
如下
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList();
list.add(6);
//反射機制實現
Class<? extends ArrayList> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
add.invoke(list, "歡迎關注:後端元宇宙");
System.out.println("list = " + list);
}
執行結果
list = [6, 歡迎關注:後端元宇宙]
二、案例實體準備
這裡先建幾個實體,為後面舉例用
Animal類
@Data
@AllArgsConstructor
public class Animal {
/**
* 動物名稱
*/
private String name;
/**
* 動物毛色
*/
private String color;
}
Pig類
:Pig是Animal的子類
public class Pig extends Animal{
public Pig(String name,String color){
super(name,color);
}
}
Dog類
: Dog也是Animal的子類
public class Dog extends Animal {
public Dog(String name,String color){
super(name,color);
}
}
三、常用的 ?, T, E, K, V, N的含義
我們在泛型中使用萬用字元經常看到T、F、U、E,K,V其實這些並沒有啥區別,我們可以選 A-Z 之間的任何一個字母都可以,並不會影響程式的正常執行。
只不過大家心照不宣的在命名上有些約定:
- T (Type) 具體的Java類
- E (Element)在集合中使用,因為集合中存放的是元素
- K V (key value) 分別代表java鍵值中的Key Value
- N (Number)數值型別
- ? 表示不確定的 Java 型別
四、上界萬用字元 < ? extends E>
語法:<? extends E>
舉例:<? extends Animal> 可以傳入的實參型別是Animal或者Animal的子類
兩大原則
add
:除了null之外,不允許加入任何元素!get
:可以獲取元素,可以通過E或者Object接受元素!因為不管存入什麼資料型別都是E的子型別
示例
public static void method(List<? extends Animal> lists){
//正確 因為傳入的一定是Animal的子類
Animal animal = lists.get(0);
//正確 當然也可以用Object類接收,因為Object是頂層父類
Object object = lists.get(1);
//錯誤 不能用?接收
? t = lists.get(2);
// 錯誤
lists.add(new Animal());
//錯誤
lists.add(new Dog());
//錯誤
lists.add(object);
//正確 除了null之外,不允許加入任何元素!
lists.add(null);
}
五、下界萬用字元 < ? super E>
語法: <? super E>
舉例 :<? super Dog> 可以傳入的實參的型別是Dog或者Dog的父類型別
兩大原則
add
:允許新增E和E的子類元素!get
:可以獲取元素,但傳入的型別可能是E到Object之間的任何型別,也就無法確定接收到資料型別,所以返回只能使用Object引用來接受!如果需要自己的型別則需要強制型別轉換。
示例
public static void method(List<? super Dog> lists){
//錯誤 因為你不知道?到底啥型別
Animal animal = lists.get(0);
//正確 只能用Object類接收
Object object = lists.get(1);
//錯誤 不能用?接收
? t = lists.get(2);
//錯誤
lists.add(object);
//錯誤
lists.add(new Animal());
//正確
lists.add(new Dog());
//正確 可以存放null元素
lists.add(null);
}
六、什麼是PECS原則?
PECS原則:生產者(Producer)使用extends,消費者(Consumer)使用super。
原則
- 如果想要獲取,而不需要寫值則使用" ? extends T "作為資料結構泛型。
- 如果想要寫值,而不需要取值則使用" ? super T "作為資料結構泛型。
示例-
public class PESC {
ArrayList<? extends Animal> exdentAnimal;
ArrayList<? super Animal> superAnimal;
Dog dog = new Dog("小黑", "黑色");
private void test() {
//正確
Animal a1 = exdentAnimal.get(0);
//錯誤
Animal a2 = superAnimal.get(0);
//錯誤
exdentAnimal.add(dog);
//正確
superAnimal.add(dog);
}
}
示例二
Collections集合工具類有個copy方法,我們可以看下原始碼,就是PECS原則。
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
我們按照這個原始碼簡單改造下
public class CollectionsTest {
/**
* 將源集合資料拷貝到目標集合
*
* @param dest 目標集合
* @param src 源集合
* @return 目標集合
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
for (int i = 0; i < srcSize; i++) {
dest.add(src.get(i));
}
}
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList();
ArrayList<Pig> pigs = new ArrayList();
pigs.add(new Pig("黑豬", "黑色"));
pigs.add(new Pig("花豬", "花色"));
CollectionsTest.copy(animals, pigs);
System.out.println("dest = " + animals);
}
}
執行結果
dest = [Animal(name=黑豬, color=黑色), Animal(name=花豬, color=花色)]
七、通過一個案例來理解 ?和 T 和 Object 的區別
1、實體轉換
我們在實際開發中,經常進行實體轉換,比如SO轉DTO,DTO轉DO等等,所以需要一個轉換工具類。
如下示例
/**
* 實體轉換工具類
*
* TODO 說明該工具類不能直接用於生產,因為為了程式碼看去清爽點,我少了一些必要檢驗,所以如果直接拿來使用可以會在某些場景下會報錯。
*/
public class EntityUtil {
/**
* 集合實體轉換
*
* @param target 目標實體類
* @param list 源集合
* @return 裝有目標實體的集合
*/
public static <T> List<T> changeEntityList(Class<T> target, List<?> list) throws Exception {
if (list == null || list.size() == 0) {
return null;
}
List<T> resultList = new ArrayList<T>();
//用Object接收
for (Object obj : list) {
resultList.add(changeEntityNew(target, obj));
}
return resultList;
}
/**
* 實體轉換
*
* @param target 目標實體class物件
* @param baseTO 源實體
* @return 目標實體
*/
public static <T> T changeEntity(Class<T> target, Object baseTO) throws Exception{
T obj = target.newInstance();
if (baseTO == null) {
return null;
}
BeanUtils.copyProperties(baseTO, obj);
return obj;
}
}
使用工具類示例
private void changeTest() throws Exception {
ArrayList<Pig> pigs = new ArrayList();
pigs.add(new Pig("黑豬", "黑色"));
pigs.add(new Pig("花豬", "花色"));
//實體轉換
List<Animal> animals = EntityUtil.changeEntityList(Animal.class, pigs);
}
這是一個很好的例子,從這個例子中我們可以去理解 ?和 T 和 Object的使用場景。
我們先以集合轉換
來說
public static <T> List<T> changeEntityListNew(Class<T> target, List<?> list);
首先其實我們並不關心傳進來的集合內是什麼物件,我們只關係我們需要轉換的集合內是什麼物件,所以我們傳進來的集合就可以用List<?>
表示任何物件的集合都可以。
返回呢,這裡指定的是Class<T>,也就是返回最終是List<T>
集合。
再以實體轉換
方法為例
public static <T> T changeEntityNew(Class<T> target, Object baseTO)
同樣的,我們並不關心源物件是什麼,我們只關心需要轉換的物件,只需關心需要轉換的物件為T
。
那為什麼這裡用Object上面用?呢,其實上面也可以改成List<Object> list
,效果是一樣的,上面List<?> list
在遍歷的時候最終不就是用Object接收的嗎
?和Object的區別
?型別不確定和Object作用差不多,好多場景下可以通用,但?可以縮小泛型的範圍,如:List<? extends Animal>,指定了範圍只能是Animal的子類,但是用List<Object>
,沒法做到縮小範圍。
總結
- 只用於讀功能時,泛型結構使用<? extends T>
- 只用於寫功能時,泛型結構使用<? super T>
- 如果既用於寫,又用於讀操作,那麼直接使用<T>
- 如果操作與泛型型別無關,那麼使用<?>
宣告: 公眾號如需轉載該篇文章,發表文章的頭部一定要 告知是轉至公眾號: 後端元宇宙。同時也可以問本人要markdown原稿和原圖片。其它情況一律禁止轉載!