一篇文章帶你搞定JAVA泛型

1、泛型的概念

泛型的作用就是把類型參數化,也就是我們常說的類型參數

平時我們接觸的普通方法的參數,比如public void fun(String s);參數的類型是String,是固定的

現在泛型的作用就是再將String定義為可變的參數,即定義一個類型參數T,比如public static <T> void fun(T t);這時參數的類型就是T的類型,是不固定的

泛型常見的字母有以下:

? 表示不確定的類型
T (type) 表示具體的一個java類型
K V (key value) 分別代表java鍵值中的Key Value
E (element) 代表Element

這些字母隨意使用,隻是代表類型,也可以用單詞。

2、泛型的使用

泛型有三種使用方式,分別為:泛型類、泛型接口、泛型方法。

類的使用地方是

方法的使用地方

  • Java泛型類
  • Java泛型方法
  • Java泛型接口
/**
* @author 香菜
*/
public class Player<T> {// 泛型類
  private T name;
  public T getName() {
      return name;
  }
  public void setName(T name) {
      this.name = name;
  }
}
 
public class Apple extends Fruit {
  public <T> void getInstance(T t){// 泛型方法
      System.out.println(t);
  }
}
 
public interface Generator<T> {
      public T next();
  }
 

3、泛型原理,泛型擦除

3.1 IDEA 查看字節碼

1、創建Java文件,並編譯,確認生成瞭class

圖片

2、idea ->選中Java 文件 ->View

圖片

3.2 泛型擦除原理

我們通過例子來看一下,先看一個非泛型的版本:

圖片

從字節碼可以看出,在取出對象的的時候我們做瞭強制類型轉換。

下面我們給出一個泛型的版本,從字節碼的角度來看看:

圖片

在編譯過程中,類型變量的信息是能拿到的。所以,set方法在編譯器可以做類型檢查,非法類型不能通過編譯。但是對於get方法,由於擦除機制,運行時的實際引用類型為Object類型。為瞭“還原”返回結果的類型,編譯器在get之後添加瞭類型轉換。所以,在Player.class文件main方法主體第18行有一處類型轉換的邏輯。它是編譯器自動幫我們加進去的。

所以在泛型類對象讀取和寫入的位置為我們做瞭處理,為代碼添加約束。

泛型參數將會被擦除到它的第一個邊界(邊界可以有多個,重用 extends 關鍵字,通過它能給與參數類型添加一個邊界)。編譯器事實上會把類型參數替換為它的第一個邊界的類型。如果沒有指明邊界,那麼類型參數將被擦除到Object。

4、?和 T 的區別

?使用場景 和Object一樣,和C++的Void 指針一樣,基本上就是不確定類型,可以指向任何對象。一般用在引用。

T 是泛型的定義類型,在運行時是確定的類型。

5、super extends

通配符限定:

<? extends T>:子類型的通配符限定,以查詢為主,比如消費者集合場景

<? super T>:超類型的通配符限定,以添加為主,比如生產者集合場景

super 下界通配符 ,向下兼容子類及其子孫類, T super Child 會被擦除為 Object

extends 上界通配符 ,向下兼容子類及其子孫類, T extends Parent 會被擦除為 Parent

class Fruit {}
class Apple extends Fruit {}
class FuShi extends Apple {}
class Orange extends Fruit {}
import java.util.ArrayList;
import java.util.List;
public class Aain {
 public static void main(String[] args) {
       //上界
       List<? extends Fruit> topList = new ArrayList<Apple>();
       topList.add(null);
       //add Fruit對象會報錯
       //topList.add(new Fruit());
       Fruit fruit1 = topList.get(0);
       //下界
       List<? super Apple> downList = new ArrayList<>();
       downList.add(new Apple());
       downList.add(new FuShi());
       //get Apple對象會報錯
       //Apple apple = downList.get(0);
}

上界 <? extend Fruit> ,表示所有繼承Fruit的子類,但是具體是哪個子類,但是肯定是Fruit

下界 <? super Apple>,表示Apple的所有父類,包括Fruit,一直可以追溯到老祖宗Object 。

歸根結底可以用一句話表示,那就是編譯器可以支持向上轉型,但不支持向下轉型。具體來講,我可以把Apple對象賦值給Fruit的引用,但是如果把Fruit對象賦值給Apple的引用就必須得用cast。

6、註意點

1、靜態方法無法訪問類的泛型

圖片

可以看到Idea 提示無法引用靜態上下文。

2、創建之後無法修改類型

List<Player> 無法插入其他的類型,已經確定類型的不可以修改類型

3、類型判斷問題

問題:因為類型在編譯完之後無法獲取具體的類型,所以在運行時是無法判斷類的類型。

我們可以通過下面的代碼來解決泛型的類型信息由於擦除無法進行類型判斷的問題:

/**
* 判斷類型
* @author 香菜
* @param <T>
*/
public class GenClass<T> {
   Class<?> classType;
   public GenClass(Class<?> classType) {
       this.classType = classType;
  }
   public boolean isInstance(Object object){
       return classType.isInstance(object);
  }
}

解決方案:我們通過在創建對象的時候在構造函數中傳入具體的class類型,然後通過這個Class對象進行類型判斷。

4、創建類型實例

問題:泛型代碼中不能new T()的原因有兩個,一是因為擦除,不能確定類型;而是無法確定T是否包含無參構造函數。

在之前的文章中,有一個需求是根據不同的節點配置實例化創建具體的執行節點,即根據IfNodeCfg 創建具體的IfNode.

/**
* 創建實例
* @author 香菜
*/
public abstract class AbsNodeCfg<T> {
   public abstract T getInstance();
}
public class IfNodeCfg extends AbsNodeCfg<IfNode>{
   @Override
   public IfNode getInstance() {
       return new IfNode();
  }
}
/**
* 創建實例
* @author 香菜
*/
public class IfNode {
}

解決方案:通過上面的方式可以根據具體的類型,創建具體的實例,擴展的時候直接繼承AbsNodeCfg,並且實現具體的節點就可以瞭。

7、總結

泛型相當於創建瞭一組的類,方法,虛擬機中沒有泛型類型對象的概念,在它眼裡所有對象都是普通對象

圖片

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: