[Java基礎篇]對Java泛型的剖析

在這裏插入圖片描述

一、泛型詳解

1.1 泛型的定義以及存在意義

☞ 泛型的定義:

泛型(Generic type或 generics),即參數化類型,是JavaSE 1.5新特性,對Java語言的類型系統的一種擴展,以支持創建可以按類型進行參數化的類。就是將類型由原來的具體的類型參數化,類似於方法中的變量參數,此時類型也定義成參數形式(類型形參),然後在使用/調用時傳入具體的類型(類型實參)。
泛型的本質是爲了參數化類型(在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是在泛型使用過程中,操作的數據類型被指定爲一個參數,這種參數類型可以用在類、接口和方法中,分別被稱爲泛型類、泛型接口、泛型方法。

☞ 爲什麼使用泛型?

爲了解決數據在裝入集合時的類型都被當做Object對待,從而失去本身特有的類型,從集合讀取時,還要強制轉換,java是所謂的靜態類型語言,意思是在運行前,或者在編譯期間,就能夠確定一個對象的類型,這樣做的好處是減少運行時由於類型不對引發的錯誤。但是強制類型轉換是鑽了一個空子,在編譯期間不會有問題,而在運行期間,就有可能由於錯誤的強制類型轉換,導致錯誤。這個編譯器無法檢查到。有了泛型,就可以用不着強制類型轉換,在編譯期間,編譯器就能對類型進行檢查,杜絕運行時由於強制類型轉換導致的錯誤。

☞泛型的好處:

Java語言引入泛型是一個較大的功能增強。不僅語言、類型系統和編譯器有了較大的變化,以支持泛型,而且類庫也進行了大翻修,所以許多重要的類,比如集合框架,都已經稱爲泛型化的了。
1.類型安全。 泛型的主要目標是提高 Java 程序的類型安全。通過知道使用泛型定義的變量的類型限制,編譯器可以在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就只存在於程序員的頭腦中(或者如果幸運的話,還存在於代碼註釋中)。
2.消除強制類型轉換。 泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,並且減少了出錯機會。
3.潛在的性能收益。 泛型爲較大的優化帶來可能。在泛型的初始實現中,編譯器將強制類型轉換(沒有泛型的話,程序員會指定這些強制類型轉換)插入生成的字節碼中。但是更多類型信息可用於編譯器這一事實,爲未來版本的 JVM 的優化帶來可能。由於泛型的實現方式,支持泛型(幾乎)不需要 JVM 或類文件更改。所有工作都在編譯器中完成,編譯器生成類似於沒有泛型(和強制類型轉換)時所寫的代碼,只是更能確保類型安全而已。
簡單地說,泛型簡單易用、消除強制類型轉換、保證類型安全。

1.2 泛型類、泛型方法、泛型接口的使用

1.2.1 泛型類的定義與使用

定義格式:修飾符 class 類名<代表泛型的變量> { }
如:class ArrayList < Iteger > { }

///在創建對象時確定泛型
ArrayList <String> list = new ArrayList<String>();
class ArrayList<String>{
  public boolean add(String e){  }
  
  public String get(int index){  }
  ....
  }

///自定義泛型
public class MyGenericsClass<MVP>{
   //沒有MVP類型,在這裏代表未知地一種數據類型未來傳遞什麼就是什麼類型
}

class ArrayList<E>{
  public boolean add(E e){  }
  
  public E get(int index){  }
  ....
  }
1.2.2 泛型方法的定義與使用

定義格式:修飾符 <代表泛型地變量> 返回值類型 方法名(參數) { }
如:public < MVP > void Method( ){…}

class MyGenericsMethod{
    public <MVP> void show(MVP mvp){
        System.out.println(mvp.getClass());///getClass()方法是獲得調用該方法的對象的類
    }
}
public class Main{
    public static void main(String [] args){
      MyGenericsMethod my = new MyGenericsMethod();
      my.show("珠穆朗瑪峯");
      my.show(8844.43);
    }
}

控制檯結果輸出:
在這裏插入圖片描述

1.2.3 泛型接口的定義與使用

定義格式:修飾符 interface 接口名 <代表泛型的變量> {…}
public interface MyGenericsInterrface < E >{…}

public interface MyGenericsInterface <E>{
   public abstract void add(E e);
   public abstract E getE();
}

///定義接口實現類時確定泛型的類型
public class Main implements MyGenericsInterface <String>{
    public void add (String e){
     ...
    }
    public String getE(){
      return null;
    }
}
///始終不確定泛型的類型
public class Main <E> implements MyGenericsInterface <E>{
    public void add (E e){
     ///省略...
    }
    public E getE(){
      return null;
    }
}
///創建對象時,確定泛型的類型
public class Main{
   public static  void main(String [] args){
        Main<String> my = new Main<String>();
        my.add("華山");
   }
}

1.3 泛型通配符類型和通配符的使用

當使用泛型類或者接口時,傳遞的數據中,泛型類型不確定,可以通過**<?>** 表示。但是一旦使用通配符後,只能使用Object類中的共性方法,集合中元素自身方法無法使用。

1.3.1 通配符基本使用(不受限泛型)

不知道使用什麼類型來接收的時候,此時可以使用?,?表示未知通配符。此時只能接受數據,不能往集合中存儲數據

import java.util.Collection;
import java.util.ArrayList;
public class Main{
    public static void main(String [] args){
      Collection<Integer> list1 = new ArrayList<Integer>();
      getElement(list1);

      Collection<String> list2 = new ArrayList<String>();
      getElement(list2);
    }
    public static void getElement(Collection <?> collection){
        ///?代表可以接收任意類型
    }
}
1.3.2 通配符高級使用(受限泛型)

設置泛型的時候,是可以任意設置的,只要是類就可以設置。但是在Java的泛型中可以指定一個泛型的上限和下限。

在引用傳遞中,泛型操作中也可以設置一個泛型對象的範圍上限範圍下限。範圍上限使用extends關鍵字聲明,表示參數化的類型可能是所指定的類型,或者是此類型的子類,而範圍下限使用super關鍵字進行聲明,表示參數化的類型可能是所指定的類型,或者是此類型的父類型,直至Object類。

☞ 泛型的上限格式:類型名稱 <? extends 類> 對象名稱 (只能接收該類型及其子類)
☞ 泛型的下限格式:類型名稱<? super 類> 對象名稱(只能接收該類型及其父類型)

import java.util.Collection;
import java.util.ArrayList;
public class Main{
    public static void main(String [] args){
      Collection<Integer> list1 = new ArrayList<Integer>();///Integer類是Number類的子類
      Collection<String> list2 = new ArrayList<String>();
      Collection<Number> list3 = new ArrayList<Number>();
      Collection<Object> list4 = new ArrayList<>();///所有類的父類
      
        ///泛型上限的實例
      getElement1(list1);
      ///getElement1(list2);報錯,不是Number類或其子類
      getElement1(list3);
      ///getElement1(list4);報錯,不是Number類或其子類
      
      ///泛型下限的實例
      ///getElement2(list1);報錯,不是Number類或其父類
      ///getElement2(list2);報錯,Number類或其父類
      getElement2(list3);
      getElement2(list4);
    }
    public static void getElement1(Collection <? extends Number> coll){
        ///泛型上限:此時的泛型?,必須是Number類型或Number類的子類
    }

    public static void getElement2(Collection <? super Number> coll){
        ///泛型下限:此時的泛型?,必須是Number類型或Number類的父類
    }
}

1.4 泛型的實現方法:類型擦除

Java的泛型是僞泛型,這是因爲Java在編譯期間,所有的泛型信息都會被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候去掉,這個過程稱爲類型擦除

import java.util.ArrayList;
public class Main{
    public static void main(String [] args) {
    ArrayList <String>  arrayList1 = new ArrayList<String>();
     arrayList1.add("華爲");
     ArrayList <Integer>  arrayList2 = new ArrayList<Integer>();
        arrayList2.add(123);
        System.out.println(arrayList1.getClass()==arrayList2.getClass());///獲取類的信息true
        ///說明String和Integer類型都擦除掉了,只剩下了原始類型
    }
}

無論何時定義一個泛型類型,都自動提供了一個相應的原始類型。原始類型的名字就是刪除類型參數後的泛型類型名。擦除類型變量,並替換爲限定類型(無限定變量用Object)。
類型擦除後保留的原始類型:

class Pair<T>{
    private T first;
    private T second;
    public T getFirst(){
        return first;
    }
    public void setFirst(T first){
        this.first=first;
    }
}

///Pair<T>的原始類型
class Pair{
    private Object first;
    private Object second;
    public Object getFirst(){
        return first;
    }
    public void setFirst(Object first){
        this.first=first;
    }
    ...
}

如果類型變量有限定,那麼原始類型就用第一個邊界的類型變量來替換:

public class Pair <T extends Comparable&Serializable> {
}
///原始類型爲Comparable

1.5 泛型中的約束及其侷限性

這一小節將闡述Java泛型時需要考慮的一些限制。大多數限制都是由類型擦除引起的。

1.5.1 不能用基本類型實例化類型參數

不能用類型參數代替基本類型。因此,沒有Pair< double >,只有Pair< Double >

1.5.2 運行時類型查詢只適用於原始類型

虛擬機中的對象總有一個特定的非泛型類型。因此,所有的類型查詢只產生原始類型。
查詢一個對象是否屬於某個泛型類型時:

if(a instanceof Pair<String>)///編譯錯誤
if(a instanceof Pair<T>)///編譯錯誤

Pair<String> p = (Pair<String>)a;///warning,強制類型轉換

Pair<String> stringPair = ...;
Pair<E> ePair = ...;
if(stringPair.getClass()==ePair.getClass())///編譯通過,getClass()方法總是返回原始類型
1.5.3 不能創建參數化類型的數組
Pair<String>[] table = new Pair<String>[10];///編譯錯誤

擦除之後,table的類型是Pair[ ],需要說明的是,只是不允許創建這些數組,而聲明類型Pair< String >[ ]的變量仍是合法的,不過不能用new Pair< String >[10]初始化這個變量。
如果需要收集參數化類型對象,只有一種安全而有效的方法:使用
ArrayList:ArrayList<Pair< String >>

1.5.4 Varargs警告

向參數個數可變的方法傳遞一個泛型類型的實例。它的參數個數是可變的。

public static <T> void addAll(Collection<T> coll,T...ts){
    for(t: ts) ///枚舉
        coll.add(t);///ts是一個數組,包含提供的所有實參。
}
///調用
Collection<pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table,pair1,pair2);

這種情況會得到一個警告。可以用以下兩種方法抑制這個警告。
爲包含addAll調用的方法增加註釋 @Suppress Warns(“unchecked”)
在Java SE 7中,可以用 @SafeVarargs 直接標註addAll方法。

@SafeVarargs
public static <T> void addAll(Collection<T> coll,T...ts){
 ...
}
1.5.5 不能實例化類型變量

不能使用像 new T(…),new T[…]或T.class這樣的表達式中的類型變量。下面的Pair構造器就是非法的

public Pair(){
    first = new T();
    second = new T();
}

類型擦除將T改變成Object,本意肯定不希望調用new Object()。在JavaSE8後,最好的解決辦法是然調用者提供一個構造器表達方式:

Pair<String> p = Pair.makePair(String::new);

makePair方法接收一個Supplier,這是一個函數式接口,表示一個無參而且返回類型爲T的函數:

public static <T> Pair<T> makePair(Supplier<T> const){
 return new Pair<>(const.get(),constr.get());
}
1.5.6 不能構造泛型數組

就像不能實例化一個泛型實例一樣,也不能實例化數組。不過原因有所不同,畢竟數組會填充null值,構造時看上去是安全的。不過,數組本身也有類型,用來監控在虛擬機中的數組。這個類型會被擦除。

1.5.7 泛型類的靜態上下文中類型變量無效

不能在靜態域或方法中引用類型變量。

public class Pair<T>{
   private static T first;///編譯錯誤
   public static T getFirst(){///編譯錯誤
     if(first==null) return first;
   }
}
1.5.8 不能拋出或捕獲泛型類的實例

既不能拋出也不能捕獲泛型對象。實際上,甚至泛型擴展Throwable都是不合法的。

1.5.9 可以消除對受查異常的檢查

Java異常處理的一個基本原則是,必須爲所有受查異常提供一個處理器。不過可以利用泛型消除這個限制。通過使用泛型類、擦除和@Suppress Warning註解,就能消除Java類型系統的部分基本限制。

1.5.10 注意擦除後的衝突

當泛型類型被擦除時,無法創建引發衝突的條件。

1.6 泛型類型的繼承規則

在使用泛型類時,需要了解一些有關繼承和子類型的準則。

泛型類型的繼承原則:要麼同時擦除,要麼子類大於等於父類類型

☞屬性類型: 在父類中,隨父類而定;在子類中,隨子類而定。
☞方法重寫: 隨父類而定。
1.子類聲明爲具體類型(具體類型大於泛型):

public abstract class Father<T>{
   T name;
   public abstract void test(T name);
}
class Child extends Father<Integer>{
 public void test(Integer name){
  }
}

2. 子類聲明爲泛型(子類有兩個,大於父類一個):

class Child<T,T1>  extends Father<T>{
  public void test(T name){///重寫的方法隨父類
  }
}

3. 子類爲泛型,父類不指定,使用Object替代(子類一個,大於父類沒有):

class Child<T> extends Father{
   public void test(Object name){
   }
}

4.子類與父類同時擦除,用Object替代:

class Child extends Father{
  public void test(Object name){
  }
}

有待更新…

二、參考資料

1.Java泛型詳解
2.全網Java泛型最詳解
3.猿問
4.Java中泛型的作用和意義
5.受限泛型
6.《Java核心技術 卷Ⅰ 基礎知識》–泛型程序設計
7.類型擦除以及類型擦除帶來的問題
8.3.泛型類型的繼承規則

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章