Java入門(二)——泛型

若是你寫過前端,可能會常常寫一下關於變量類型的判斷,好比:typeof fn === 'function'之類的代碼。由於JavaScript做爲一門弱類型語言,類型的判斷每每須要開發人員本身去檢查。html

Java做爲一門強類型語言,它的強就強在類型的約束比較嚴格,大多都是在編譯器(IDEA、eclipse...)裏就作了檢查,也就是說你剛敲完一個字符,若是有類型錯誤,下一秒就能提示你哪錯了。這也避免了運行時的錯誤,讓你的代碼更加的嚴謹。下面就來了解一下爲類型約束作出卓越貢獻的人物——泛型。前端

Java泛型爲什麼而生

Java泛型(generics)是JDK 1.5中引入的一個新特性,泛型提供了編譯時類型安全檢測機制,該機制容許程序員在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說操做的數據類型被指定爲一個參數。java

我知道,上面那些乾巴巴的概念對於初學者看了也是一頭霧水。下面讓咱們穿越回到JDK 1.5以前的時代,當初尚未泛型的存在,咱們是怎麼生活的呢?git

泛型解決了哪些痛點

ArrayList能夠看作「可變長度」的數組,用起來比數組方便。實際上,ArrayList內部就是一個Object[]數組,配合存儲一個當前分配的長度,就能夠充當「可變數組」:程序員

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

若是有上面的ArrayList存儲String類型,會有這麼幾個缺點:github

  • 需求強制手動轉型
  • 不方便,易出錯

例如,代碼必須這麼寫:數組

ArrayList list = new ArrayList();
list.add("Hello");
// 獲取到Object,必須強制轉型爲String:
String first = (String) list.get(0);

很容易出現ClassCastException,由於容易「誤轉型」:安全

list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);

要解決上面的問題,咱們能夠爲String單獨編寫一種ArrayListdom

public class StringArrayList {
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...} // 注意這個特地作了處理
}

這樣一來,存入的必須是String,取出的也必定是String,不須要強制轉型,由於編譯器會強制檢查放入的類型:eclipse

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 編譯錯誤: 不容許放入非String類型:
list.add(new Integer(123));

好了,雖然沒有用泛型,可是藉助勞動人民的智慧結晶,咱們也能把這個問題解決掉🤲 。

然而,新的問題又來了,若是要存儲Integer,還須要爲Integer單獨編寫一種ArrayList

public class IntegerArrayList {
    private Integer[] array;
    private int size;
    public void add(Integer e) {...}
    public void remove(int index) {...}
    public Integer get(int index) {...} // 此處單獨處理
}

實際上,還須要爲其餘全部class單獨編寫一種ArrayListLongArrayListDoubleArrayListPersonArrayList...想到這些,確定奔潰了。

好的,爲了解決新的問題,咱們必須把ArrayList變成一種模板:ArrayList<T>,代碼以下:

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}  // 注意,T就是參數類型變量
}

所以,泛型就是定義了一種模板,例如ArrayList,而後在代碼中爲用到的類建立對應的ArrayList<類型>

ArrayList<String> strList = new ArrayList<String>(); // new 後面的String能夠省略不寫

這樣,即實現了編寫一次,萬能匹配,又經過編譯器保證了類型安全:這就是泛型的做用。

Java泛型定義

泛型就是參數化類型,也就是說所操做的數據類型被指定爲一個參數

這是你第二遍看到這句話了,是否是有了新的認知,若是沒有,請把上面泛型的由來再看一遍。

通俗點講,泛型,看表面的意思,泛型就是指普遍的、普通的類型。在java中是指把類型明確的工做推遲到建立對象或調用方法的時候纔去明確的特殊的類型

解釋一下上面的話:咱們以前執行一個函數的時候,參數的類型提早定義好,參數的值在調用時傳入。有了泛型以後,參數的類型也能夠不用提早定義好,先給個變量(e.g.:T、E、V)把參數類型存起來,直到運行時再傳入。總之一句話,把類型參數當作形參傳進去用。來看一個簡單的例子:

ArrayList<String> strings = new ArrayList<String>();
strings.add("a String");
String aString = strings.get(0);

能夠看到,經過菱形語法<>能夠將ArrayList內的元素的類型限定爲String類型。

注意:<>內的類型只能是引用類型。固然,對於基本類型,可使用對應的包裝類型

Java泛型的使用

Java泛型的使用有如下幾種類型:

  • 泛型類
  • 泛型接口
  • 類型通配符
  • 泛型方法

Java泛型類

泛型類型用於類的定義中,被稱爲泛型類。類結構是面向對象中最基本的元素,若是咱們的類須要有很好的擴展性,那麼咱們能夠將其設置成泛型的。假設咱們須要一個數據的包裝類,經過傳入不一樣類型的數據,能夠存儲相應類型的數據。

經過泛型能夠完成對一組類的操做對外開放相同的接口。最典型的就是各類容器類,如:List、Set、Map。

下面來定義一個普通的泛型類:

// 此處T能夠隨便寫爲任意標識,常見的如T、E、K、V等形式的參數經常使用於表示泛型
// 在實例化泛型類時,必須指定T的具體類型
public class Generic<T>{ 
    // key這個成員變量的類型爲T,T的類型由外部指定  
    private T key;

    public Generic(T key) { // 泛型構造方法形參key的類型也爲T,T的類型由外部指定
        this.key = key;
    }

    public T getKey(){ // 泛型方法getKey的返回值類型爲T,T的類型由外部指定
        return key;
    }
}

下面來調用這個泛型類:

// 泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型
// 傳入的實參類型需與泛型的類型參數類型相同,即爲Integer.
Generic<Integer> genericInteger = new Generic<Integer>(100);

//傳入的實參類型需與泛型的類型參數類型相同,即爲String.
Generic<String> genericString = new Generic<String>("key_string");
Log.d("泛型測試","key is " + genericInteger.getKey()); // 泛型測試: key is 100
Log.d("泛型測試","key is " + genericString.getKey()); // 泛型測試: key is key_string

定義的泛型類,就必定要傳入泛型類型實參麼?

並非這樣,在使用泛型的時候若是傳入泛型實參,則會根據傳入的泛型實參作相應的限制,此時泛型纔會起到本應起到的限制做用。若是不傳入泛型類型實參的話,在泛型類中使用泛型的方法或成員變量定義的類型能夠爲任何的類型。

泛型接口

泛型接口與泛型類的定義及使用基本相同。泛型接口常被用在各類類的生產器中,能夠看一個例子:

//定義一個泛型接口
public interface Generator<T> {
    public T next();
}

下面有兩點須要注意:

  1. 當實現泛型接口的類,未傳入泛型實參時
/**
 * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一塊兒加到類中
 * 即:class FruitGenerator<T> implements Generator<T>{}
 *
 * 若是不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
  1. 當實現泛型接口的類,傳入泛型實參時
/**
 * 傳入泛型實參時:
 * 定義一個生產器實現這個接口,雖然咱們只建立了一個泛型接口 Generator<T>
 * 可是咱們能夠爲 T 傳入無數個實參,造成無數種類型的 Generator 接口。
 * 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則全部使用泛型的地方都要替換成傳入的實參類型
 * 即:Generator<T>,public T next();中的的T都要替換成傳入的 String 類型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型通配符

類型通配符通常是使用?代替具體的類型參數。例如List在邏輯上是List<String>,List<Integer>等全部List<具體類型實參>的父類 。Java泛型的通配符分3種:無界、上界和下界通配符。下面一一介紹:

無界通配符

無界通配符做爲最基礎的一種通配符, 它和下面要講的泛型方法差很少,只是不用在使用前進行定義,例子以下:

import java.util.*;
 
public class GenericTest {
     
    public static void main(String[] args) {
        List<String> name = new ArrayList<String>();
        List<Integer> age = new ArrayList<Integer>();
        List<Number> number = new ArrayList<Number>();
        
        name.add("icon");
        age.add(18);
        number.add(314);
 
        getData(name);
        getData(age);
        getData(number);
       
   }
   // public <T> void getData(List<T> data) {} 這是個泛型方法
   public void getData(List<?> data) {
      System.out.println("data :" + data.get(0));
   }
}

輸入結果爲:

data :icon
data :18
data :314

由於getData()方法的參數是List類型的,因此 name,age,number 均可以做爲這個方法的實參,這就是通配符的做用。

?是類型實參,而不是類型形參

上界通配符

上界通配符使用<? extends T>來定義,能夠看出是出現?T存在繼承關係的。咱們先來看一個有繼承關係的兩個類:

class Fruit {}
class Apple extends Fruit {}

如今咱們定義一個盤子類:

class Plate<T>{
    T item;
    public Plate(T t){
        item = t;
    }
    
    public void set(T t) {
        item=t;
    }
    
    public T get() {
        return item;
    }
}

上面咱們定義了一個盤子類,下面,咱們實例化一個水果盤子:

Plate<Fruit> p=new Plate<Apple>(new Apple()); 
// 編譯報錯:cannot convert from Plate<Apple> to Plate<Fruit>

裝蘋果的盤子沒法轉化成裝水果的盤子。咱們知道了,就算容器中的類型存在繼承關係,可是PlatePlate兩個容器直接是不存在繼承關係的。在這種狀況下, Java就設計成Plate<? extend Fruit>來讓兩個容器之間存在繼承關係。咱們上面的代碼就能夠進行賦值了 :

Plate<? extends Fruit> p = new Plate<Apple>(new Apple());

Plate<? extends Fruit>覆蓋下面的藍色部分:

上界通配符

下界通配符

下界通配符的意思是容器中只能存放T及其T的基類類型的數據。咱們仍是以上面類層次的來看,<? super Fruit>覆蓋下面的紅色部分:

下界通配符

下界通配符<? super T>不影響往裏面存儲,可是讀取出來的數據只能是Object類型。

上界和下界的對比

泛型通配符< ? extends T >來接收返回的數據,此寫法的泛型集合不能使用add方 法, 而< ? super T >不能使用get方法,做爲接口調用賦值時易出錯。

這個怎麼來理解呢?當咱們使用extends時,咱們能夠讀元素,由於元素都是Fruit類或子類,能夠放心的用Fruit類拿出。當使用super時,能夠添加元素,由於都是Fruit類或父類,那麼就能夠安全的插入Fruit類。

上界<? extends T>不能往裏存,只能往外取,適合頻繁往外面讀取內容的場景。

下界<? super T>不影響往裏存,但往外取只能放在Object對象裏,適合常常往裏面插入數據的場景。

下界<? super T>不影響往裏存,但往外取只能放在Object對象裏,適合常常往裏面插入數據的場景。

泛型方法

前面咱們介紹的泛型是做用於整個類的,如今咱們來介紹泛型方法。泛型方法既能夠存在於泛型類中,也能夠存在於普通的類中。若是使用泛型方法能夠解決問題,那麼應該儘可能使用泛型方法。

咱們見到的大多數泛型類中的成員方法也都使用了泛型,有的甚至泛型類中也包含着泛型方法,這樣在初學者中很是容易將泛型方法理解錯了。記住一點:泛型類,是在實例化類的時候指明泛型的具體類型;泛型方法,是在調用方法的時候指明泛型的具體類型

泛型方法的基本使用

在泛型類中定義泛型方法:

class DataHolder<T>{
    T item;
    public void setData(T t) {
        this.item=t;
    }
    public T getData() {  // 這個不是泛型方法!
        return this.item;
    }
    
    /**
     * 泛型方法
     * @param e
     */
     public <E> void PrintInfo(E e) {
        System.out.println(e);
     }
}

從上面的例子中,咱們看到咱們是在一個泛型類裏面定義了一個泛型方法printInfo。經過傳入不一樣的數據類型,咱們均可以打印出來。在這個方法裏面,咱們定義了類型參數 E。這個 E 和泛型類裏面的 T 二者之間是沒有關係的。哪怕咱們將泛型方法設置成這樣:

// 注意這個T是一種全新的類型,能夠與泛型類中聲明的T不是同一種類型。
public <T> void PrinterInfo(T e) {
    System.out.println(e);
}

// 調用方法
DataHolder<String> dataHolder=new DataHolder<>();
dataHolder.PrinterInfo(1);
dataHolder.PrinterInfo("AAAAA");
dataHolder.PrinterInfo(8.88f);

運行結果以下:

1
AAAAA
8.88

這個泛型方法依然能夠傳入DoubleFloat等類型的數據。泛型方法裏面的類型參數T和泛型類裏面的類型參數是不同的類型,從上面的調用方式,咱們也能夠看出,泛型方法printInfo不受咱們DataHolder中泛型類型參數是String的影響。

泛型方法與可變參數

再看一個泛型方法和可變參數的例子:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型測試","t is " + t);
    }
}

調用:

printMsg("111",222,"aaaa","2323.4",55.55);
靜態方法與泛型

靜態方法有一種狀況須要注意一下,那就是在類中的靜態方法使用泛型:靜態方法沒法訪問類上定義的泛型;若是靜態方法操做的引用數據類型不肯定的時候,必需要將泛型定義在方法上。

即:若是靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法

public class StaticGenerator<T> {
    /**
     * 若是在類中定義使用泛型的靜態方法,須要添加額外的泛型聲明(將這個方法定義成泛型方法)
     * 即便靜態方法要使用泛型類中已經聲明過的泛型也不能夠。
     * 如:public static void show(T t){..},此時編譯器會提示錯誤信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){
    }
}
泛型方法總結

泛型方法能使方法獨立於類而產生變化,如下是一個基本的指導原則:

  • 若是使用泛型方法能夠解決問題,那麼應該儘可能使用泛型方法
  • 對於一個static的方法,沒法訪問類泛型定義的泛型參數。因此若是static方法要使用泛型能力,就必須使其成爲泛型方法

參考:

深刻理解Java泛型

java 泛型詳解

大白話說Java泛型:入門、使用、原理

相關文章
相關標籤/搜索