Java高階特性—泛型

飛馳的蝸牛發表於2019-11-17

一、什麼是泛型?

泛型支援在定義類、介面和方法時將型別作為引數。泛型引數與方法宣告中使用的形式引數非常相似,泛型引數使我們可以在不同的輸入中重用相同的程式碼。但與形式引數不同之處在於,形式引數的輸入是值,而型別引數的輸入是型別。 我們可以將泛型 < T > 理解為佔位符,在定義類時不清楚它會是什麼型別,但是類中的程式碼邏輯是通用的,無論類在使用時< T >會被什麼引數型別替代

比如我們經常使用的List集合,在建立時通常都要指定其型別:

//指定泛型型別為String
List<String> stringList = new ArrayList<>();
  stringList.add("hello");
  stringList.add("world");
  //試圖新增非String型別元素,編譯會報錯
  stringList.add(100);
複製程式碼

當然我們也可以不指定集合泛型E,但是它會帶來隱患:

//未指定元素泛型型別,可以儲存任何object型別
List frulist  = new ArrayList();
 frulist .add("apple");
 frulist .add("banana");
 //隱患1:可以加入其它型別元素
 frulist.add(100);
 //隱患2:取出元素時,必須進行型別轉換,容易出錯
 String str = (String)frulist.get(0);
複製程式碼

二、泛型的定義

上面已經提到,泛型支援在定義類、介面和方法時將型別作為引數。下面我們通過例子來看下具體泛型具體使用方式。

(1)泛型類

泛型類的定義方式如下,型別引數 T由< >包裹,緊跟在類名之後,型別引數可以有多個,以英文逗號分割。

class name<T1, T2, ..., Tn> { 
   /* ... */ 
}
複製程式碼

知道了泛型類的格式,我們來具體實踐下,先定義一個非泛型的類Box,它只有一個Object型別成員變數,同時提供簡單的set\get方法

class Box{
    private Object content;
    
    public void set(Object object) {
        this.content = object;
    }
    public Object get() {
        return content;
    }
}
複製程式碼

Box類的成員屬性content是Object型別,所以我們可以自由的存放任何型別資料,在使用時可能會像下面這樣:

       public static void main(String[] args) {
            //建立一個box類,來存放String型別資料
	        Box box = new Box();
	        box.set("hello world");
	        //取值時,都要進行強制型別轉換
	        String content = (String) box.get();
	     
	        //.....很多行程式碼後,不小心存了boolean型別
	        box.set(false);
	        
	        //...很多行程式碼後,又一個疏忽,帶來了ClassCastException
	        Integer count = (Integer) box.get();
    }
複製程式碼

可以看到,在使用非泛型的Box類時,雖然存放的元素型別非常自由,但也存在很多嚴重問題,比如我們建立Box類物件,本來是想存放String型別資料,卻可能不小心使用box的set()方法存了boolean型別,另外每次使用box.get()取值時,都要進行強制型別轉換,很容易遇見java.lang.ClassCastException

這時候我們就可以使用泛型類了,對Box類進行改造,在類的宣告時加入泛型引數 < T >,然後在Box類內部就可以使用泛型引數 T 代替原來的Object,set\get方法所使用的引數型別,也都使用 T 來代替,此時我們的Box類就是泛型類了。

class Box <T> {
    private T content;
    
    public void set(T object) {
        this.content = object;
    }
    public T get() {
        return content;
    }
}
複製程式碼

這時我們在使用泛型類Box時,指定型別引數< T >的實際型別即可

 public static void main(String[] args) {
        //建立Box類時,指定泛型引數為String
        Box<String> box = new Box();
        box.set("hello world");
        
        //由於指定了泛型,不需要在進行強制型別轉換
        String content = box.get();
      
        //不小心存了boolean型別,IDE在編譯時會報錯
        box.set(false);
    }
複製程式碼

到了這裡你是否聯想到,我們經常使用的集合List< T >,Map < K,V>,例如HashMap的原始碼中類宣告部分:

//HashMap類原始碼
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  
   //........../
}    
複製程式碼
(2)泛型方法

泛型方法是引入自己的型別引數的方法。類似於宣告泛型型別,但是型別引數的範圍僅限於宣告它的方法。允許使用靜態和非靜態泛型方法,以及泛型類建構函式。 泛型方法的語法包括尖括號< >內的型別引數列表,該列表出現在方法的返回型別之前。對於靜態泛型方法,型別引數部分必須出現在方法的返回型別之前。 泛型方法長什麼樣子呢,下面看個例子:

/**
*泛型類Pair,用於建立key-value型別鍵值對
*類似與JDK中Map內部Entry<K,V>
*/
public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

public class Util {
    /**
    * 泛型方法compare
    * 泛型引數列表<T>出現在必須位於方法的返回值之前。
    * 泛型引數<T>在宣告後,才能在方法內部使用       
    * 泛型類中的返回值為T的方法不是泛型方法
    **/
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
   
   /**
   *泛型方法,
   *計算陣列T[]中大於指定元素elem的元素數量
   * <T extends Comparable<T> > 是泛型的繼承,extends 限定了方法
   * 中使用的<T>必須是Comparable<T>的子類,這樣才能在方法裡使用compareTo方法
   */
   public static <T extends Comparable<T> > int countGreaterThan(T[] anArray, T elem) {
		    int count = 0;
		    for (T e : anArray)
		        if (e.compareTo(elem) > 0)
		            ++count;
		    return count;
}

    public static void main(String[] args) {
         Pair<Integer, String> p1 = new Pair<>(1, "apple");
		 Pair<Integer, String> p2 = new Pair<>(2, "pear");
		 //使用泛型方法,並指定引數型別
		 boolean same = Util.<Integer, String>compare(p1, p2);
    }
}
複製程式碼

三、泛型規範

(1)型別引數命名約定

按照慣例,型別引數名稱是單個大寫字母。便於區分型別變數和普通類或介面名稱。最常用的型別引數名有:

  • T 型別,常用在泛型類、泛型介面上,如java 中的 Comparable< T >
  • E 元素,在Java集合類中廣泛使用,如List< E > , Set< E >
  • N 數值型別,主要用在數字相關的
  • K 鍵,典型的就是Map< K ,V>
  • V 值,同Map< K ,V>

以上是官方推薦的幾種,除此之外,還可以使用S、U等等

(2)泛型萬用字元與泛型限定

在泛型裡程式碼中,使用 <?> 作為萬用字元,表示一個未知的型別。 那為什麼要使用萬用字元呢?主要是因為在java中,陣列是可以協變的,比如Cat extends Animal,那麼Animal[] 與Cat[]是相容的。而集合是不能協變的,也就是說List < Animal >不是List< Cat >的父類,二者不能相互賦值,這就導致了一個邏輯上問——能夠存放父類Animal元素的集合,卻不能存放它的子類Cat。

abstract class Animal {
    public abstract void eat();
}
class Cat extends Animal{
    @Override
    public void eat() {}
}
public class TestC{
    public static void main(String[] args) {
        //Animal是Cat父類,陣列可以賦值
        Animal[] animal = new Cat[5];
        
        //Animal是Cat父類,但是不意味著List<Animal>集合是List<Cat>父類,
        List<Animal> animals = new ArrayList<>();
        List<Cat> cats = new ArrayList<>();
        
        //下面這行程式碼會編譯失敗,編譯器無法推斷出List<Cat>是List<Animal>的子類
        animals = cats; // incompatible types
    }
}
複製程式碼

為了解決上面描述的問題,泛型的萬用字元就派上用途了。泛型萬用字元分為三種型別:

  • 無邊界配符(Unbounded Wildcards) < ? >就是無邊界萬用字元,比如List<?> list表示持有某種特定型別物件的List,但是不知道是哪種型別,所以不能add任何型別的物件。它與List list並不相同,List list是表示持有Object型別物件的List,可以add任何型別的物件。

  • 上邊界限定的萬用字元(Upper Bounded wildcards)

    , E指是就是該泛型的上邊界,表示泛型的型別只能是E類或者E類的子類 ,這裡雖然用的是extends關鍵字, 卻不僅限於繼承了E的子類, 也可以代指介面E的實現類。
   public static void main(String[] args) {

        // <? extends Animal>限定了泛型類只能是Animal或其子類物件的List
        List<? extends Animal> animals = new ArrayList<>();
        List<Cat> cats = new ArrayList<>();
        //下面程式碼不會報錯,二者引用可以賦值
        animals = cats;
        //但是集合中無法add元素,因為無法確定持有的實際型別,
        animals.add(new Cat());//error
        //從集合中獲取物件是是可以的,因為在這個List中,不管實際型別是什麼,肯定都能轉型為Animal
        Animal animal = animals.get(0);
    }
複製程式碼
  • 下邊界限定的萬用字元(Lower Bounded wildcards)

    ,表示泛型的型別只能是E類或者E類的父類,List list表示某種特定型別(Integer或者Integer的父類)物件的List。可以確定這個List持有的物件型別肯定是Integer或者其父類。
        //某種特定型別(Integer或者Integer的父類)物件的List
        List<? super Integer> list = new ArrayList<>();
        //往list裡面add一個Integer或者其子類的物件是安全的,
        //因為Integer或者其子類的物件,都可以向上轉型為Integer的父類物件
        list.add(new Integer(1));
        //下面程式碼編譯錯誤,因為無法確定實際型別,所以往list裡面add一個Integer的父類物件是不被允許的
        list.add(new Object());
複製程式碼

所以從上面上邊界限定的萬用字元和下邊界限定的萬用字元的特性,可以知道:

  • 對於上邊界限定的萬用字元 <? extends E>,無法向其中加入任何物件,但是可以從中正常取出物件。
  • 對於下邊界限定的萬用字元 <? super E>,,可以存入subclass物件或者subclass的子類物件,但是取出時只能用Object型別變數指向取出的物件。

四、型別擦除

Java 中的的泛型是偽泛型,這是因為泛型資訊只存在於程式碼編譯階段,編譯後與泛型相關的資訊會被擦除掉,稱為型別擦除(type erasure)。 編譯器在編譯期,會將泛型轉化為原生型別。並在相應的地方插入強制轉型的程式碼。什麼是原生型別呢?原生型別就是刪去型別引數後泛型類的型別名,比如:

   List<String> stringList = new ArrayList<String>();
   List<Integer> integerList = new ArrayList<Integer>();
   //下面的結果為true,因為型別擦除後,二者原生型別都是List
   System.out.println(stringList.getClass() == integerList.getClass());
複製程式碼

如果泛型引數中,有限定符則會使用 第一個限定符的型別來替換,比如

class Box <T extends Number> {
    private T content;
   
}
複製程式碼

型別擦除後的原生型別變為其限定符型別:

class Box{
    private Number content;
}
複製程式碼

總結

泛型所涵蓋內容不止這麼多,希望通過本文對你瞭解JAVA中泛型有所幫助。

相關文章