深入理解什么是Java泛型?泛型怎么使用?【純轉】


本篇文章給大家帶來的內容是介紹深入理解什么是Java泛型?泛型怎么使用?有一定的參考價值,有需要的朋友可以參考一下,希望對你們有所助。

 

一、什么是泛型

“泛型” 意味着編寫的代碼可以被不同類型的對象所重用。泛型的提出是為了編寫重用性更好的代碼。泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數

比如常見的集合類 LinkedList:

1

2

3

4

5

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>,Deque<E>,Cloneable,Serializable{

 //.....

 transient Link<E> voidLink;

//.....   

}

可以看到,LinkedList<E> 類名及其實現的接口名后有個特殊的部分<E>,而且它的成員的類型 Link<E> 也包含一個<E>,這個符號的就是類型參數,它使得在運行中,創建一個 LinkedList 時可以傳入不同的類型。

二、為什么引入泛型

在引入泛型之前,要想實現一個通用的、可以處理不同類型的方法,你需要使用 Object 作為屬性和方法參數,比如這樣:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class Generic{

    private Object[] mData;

 

    public Generic(int capacity){

        mData = new Object[capacity];

    }

     

    public Object getData(int index){

       //.....

       return mData[index];

    }

      

    public void add(int index,Object item){

       //.....

       mData[index] = item;

    }  

 

}

它使用一個 Object 數組來保存數據,這樣在使用時可以添加不同類型的對象:

1

2

3

Generic generic = new Generic(10);

generic.add(0,"fangxing");

generic.add(1,23);

Object 是所有類的父類,所有的類都可以作為成員被添加到上述類中;當需要使用的時候,必須進行強制轉換,而且這個強轉很有可能出現轉換異常:

1

2

String item1 = (String) generic.getData(0);

String item2 = (String) generic.getData(1);

第二行代碼將一個 Integer 強轉成 String,運行時會報錯 :

1.png

可以看到,使用 Object 來實現通用、不同類型的處理,有這么兩個缺點:

  1. 每次使用時都需要強制轉換成想要的類型

  2. 在編譯時編譯器並不知道類型轉換是否正常,運行時才知道,不安全

根據《Java 編程思想》中的描述,泛型出現的動機在於:

有許多原因促成了泛型的出現,而最引人注意的一個原因,就是為了創建容器類。

在 JDK 1.5 出現泛型以后,許多集合類都使用泛型來保存不同類型的元素,比如 Collection:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public interface Collection<E> extends Iterable<E>{

    

     Iterator<E> iterator();

    

     Object[] toArray();

      

     <T> T[] toArray(T[] a);

 

     boolean add(E e);

      

     boolean remove(Object o);

  

     boolean  containsAll(Collecion<?> c);

 

     boolean addAll(Collection<? extends E> c);

 

     boolean removeAll(Collection<?> c);

    

}

實際上引入泛型的主要目標有以下幾點:

類型安全

  • 泛型的主要目標是提高 Java 程序的類型安全

  • 編譯時期就可以檢查出因 Java 類型不正確導致的 ClassCastException 異常

  • 符合越早出錯代價越小原則

消除強制類型轉換

  • 泛型的一個附帶好處是,使用時直接得到目標類型,消除許多強制類型轉換

  • 所得即所需,這使得代碼更加可讀,並且減少了出錯機會

潛在的性能收益

  • 由於泛型的實現方式,支持泛型(幾乎)不需要 JVM 或類文件更改

  • 所有工作都在編譯器中完成

  • 編譯器生成的代碼跟不使用泛型(和強制類型轉換)時所寫的代碼幾乎一致,只是更能確保類型安全而已

三、泛型的使用方式

泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。

類型參數的意義是告訴編譯器這個集合中要存放實例的類型,從而在添加其他類型時做出提示,在編譯時就為類型安全做了保證。

參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public class GenericClass<F>{

    private F mContent;

     

    public GenericClass(F content){

      mContent = content;

    }

   

    /*

      泛型方法

   */

    public F getContent(){

      return mContent;

    }

     

    public void setContent(F content){

      mcontent = content;

    }

    

    /*

       泛型接口

    */

    public interface GenericInterface<T>{

        void  doSomething(T t);

    }

 

}

泛型類

泛型類和普通類的區別就是類名后有類型參數列表 <E>,既然叫“列表”了,當然這里的類型參數可以有多個,比如 public class HashMap<K, V>,參數名稱由開發者決定。

類名中聲明參數類型后,內部成員、方法就可以使用這個參數類型,比如上面的 GenericClass<F> 就是一個泛型類,它在類名后聲明了類型 F,它的成員、方法就可以使用 F 表示成員類型、方法參數/返回值都是 F 類型。

泛型類最常見的用途就是作為容納不同類型數據的容器類,比如 Java 集合容器類。

泛型接口

和泛型類一樣,泛型接口在接口名后添加類型參數,比如以下 GenericInterface<T>,接口聲明類型后,接口方法就可以直接使用這個類型。

    /*

       泛型接口

    */

    public interface GenericInterface<T>{

        void  doSomething(T t);

    }

 

實現類在實現泛型接口時需要指明具體的參數類型,不然默認類型是 Object,這就失去了泛型接口的意義。

未指明類型的實現類,默認是 Object 類型:

1

2

3

4

5

6

public class Generic implements GenericInterface{

    @Override

    public void doSomething(Object o){

      //...

    }

}

指明了類型的實現:

1

2

3

4

5

6

public class Generic implements GericInterface<String>{

     @Override

     public void doSomething(String s){

       //.....

     }

}

泛型接口比較實用的使用場景就是用作策略模式的公共策略, Comparator就是一個泛型接口:

1

2

3

4

public interface Comparator<T>{

    public int compare(T lhs, Trhs);

    public bollean equals(Object object);

}

泛型接口定義基本的規則,然后作為引用傳遞給客戶端,這樣在運行時就能傳入不同的策略實現類。

泛型方法

泛型方法是指使用泛型的方法,如果它所在的類是一個泛型類,那就很簡單了,直接使用類聲明的參數。

如果一個方法所在的類不是泛型類,或者他想要處理不同於泛型類聲明類型的數據,那它就需要自己聲明類型。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/*

    傳統的方法,會有unchecked ... raw type 的警告

*/

public Set union(Set s1, Set s2){

    Set result = new HashSet(s1);

    result.addAll(s2);

    return result;

}

 

/*

    泛型方法,介於方法修飾符和返回值之間的稱作 類型參數列表<A,V,F,E....>(可以有多個)

    類型參數列表 指定參數、返回值中泛型的參數類型范圍,命名慣例與泛型相同。

*/

public <E> Set<E> union2(Set<E> s1, Set<E> s2){

    Set<E> result = new HashSet<>(s1);

    result.addAll(s2);

    return result;

}

四、泛型的通配符

通配符:傳入的類型有一個指定的范圍,從而可以進行一些特定的操作

泛型中有三種通配符形式:

1.<?>無限制通配符

2.<? extends E> extends 關鍵字聲明了類型的上界,表示參數化的類型可能是所指定的類型,或者是此類型的子類。

3.<? super E> super 關鍵字聲明了類型的下界,表示參數化類型可能是指定類型,或者是此類型的父類。

無限制通配符 < ?>

要使用泛型,但是不確定或者不關心實際要操作的類型,可以使用無限制通配符(尖括號里一個問號,即 <?> ),表示可以持有任何類型。

? 和 Object 不一樣,List<?> 表示未知類型的列表,而 List<Object> 表示任意類型的列表。

如傳入個 List<String> ,這時 List 的元素類型就是 String,想要往 List 里添加一個 Object,這當然是不可以的。

上界通配符 < ? extends E>

在類型參數中使用 extends 表示這個泛型中的參數必須是 E 或者 E 的子類,這樣有兩個好處:

  • 如果傳入的類型不是 E 或者 E 的子類,編輯不成功

  • 泛型中可以使用 E 的方法,要不然還得強轉成 E 才能使用

下界通配符 < ? super E>

在類型參數中使用 super 表示這個泛型中的參數必須是 E 或者 E 的父類。

1

2

3

4

5

private <E> void add(List<? super E> dst, List<E> Src){

    for (E e : src){

    dst.add(e);

   }

}

上面的 dst 類型 “大於等於” src 的類型,這里的“大於等於”是指 dst 表示的范圍比 src 要大,因此裝得下 dst 的容器也就能裝 src。

通配符比較

無限制通配符 < ?> 和 Object 有些相似,用於表示無限制或者不確定范圍的場景。

< ? super E> 用於靈活寫入或比較,使得對象可以寫入父類型的容器,使得父類型的比較方法可以應用於子類對象。

< ? extends E> 用於靈活讀取,使得方法可以讀取 E 或 E 的任意子類型的容器對象。

2.png

因此使用通配符的基本原則:

  • 如果參數化類型表示一個 T 的生產者,使用 < ? extends T>;(T 的子類)

  • 如果它表示一個 T 的消費者,就使用 < ? super T>;(T 的父類)

  • 如果既是生產又是消費,那使用通配符就沒什么意義了,因為你需要的是精確的參數類型。

小總結一下:

  • T 的生產者的意思就是結果會返回 T,這就要求返回一個具體的類型,必須有上限才夠具體;

  • T 的消費者的意思是要操作 T,這就要求操作的容器要夠大,所以容器需要是 T 的父類,即 super T;

舉個例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

private <E extends Comparable<? super E>> E max(List<? extends E> e1){

 

    if(e1 == null){

        return null;

    }

 

    //迭代器返回的元素屬於 E 的某個子類型

    

    Iterator<? extends E> iterator = e1.iterator();

    E result = iterator.next();

    while (iterator.hasNext()){

     E next = iterator.next();

      if(next.compareTo(result)>0){

       result = next;

      }

    }

    return result;

}

1.要進行比較,所以 E 需要是可比較的類,因此需要 extends Comparable<…>(注意這里不要和繼承的 extends 搞混了,不一樣)

2.Comparable< ? super E> 要對 E 進行比較,即 E 的消費者,所以需要用 super

3.而參數 List< ? extends E> 表示要操作的數據是 E 的子類的列表,指定上限,這樣容器才夠大

五、泛型的類型擦除

Java 中的泛型和 C++ 中的模板有一個很大的不同:

  • C++ 中模板的實例化會為每一種類型都產生一套不同的代碼,這就是所謂的代碼膨脹。

  • Java 中並不會產生這個問題。虛擬機中並沒有泛型類型對象,所有的對象都是普通類。

在 Java 中,泛型是 Java 編譯器的概念,用泛型編寫的 Java 程序和普通的 Java 程序基本相同,只是多了一些參數化的類型同時少了一些類型轉換。

實際上泛型程序也是首先被轉化成一般的、不帶泛型的 Java 程序后再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯,Java 虛擬機運行時對泛型基本一無所知。

當編譯器對帶有泛型的java代碼進行編譯時,它會去執行類型檢查和類型推斷,然后生成普通的不帶泛型的字節碼,這種普通的字節碼可以被一般的 Java 虛擬機接收並執行,這在就叫做 類型擦除(type erasure)。

實際上無論你是否使用泛型,集合框架中存放對象的數據類型都是 Object,這一點不僅僅從源碼中可以看到,通過反射也可以看到。

1

2

3

List<String> strings = new ArrayList<>();

List<Integer> integers = new ArrayList<>();

System.out.println(Strings.getClass()==integers.getClass());//true

上面代碼輸出結果並不是預期的false,而是true。其原因就是泛型的擦除。

六、擦除的實現原理

一直有個疑問,Java 編譯器在編譯期間擦除了泛型的信息,那運行中怎么保證添加、取出的類型就是擦除前聲明的呢?

Java 編輯器會將泛型代碼中的類型完全擦除,使其變成原始類型。當然,這時的代碼類型和我們想要的還有距離,接着 Java 編譯器會在這些代碼中加入類型轉換,將原始類型轉換成想要的類型。這些操作都是編譯器后台進行,可以保證類型安全。總之泛型就是一個語法糖,它運行時沒有存儲任何類型信息。

擦除導致的泛型不可變性

泛型中沒有邏輯上的父子關系,如 List 並不是 List 的父類。兩者擦除之后都是List,所以形如下面的代碼,編譯器會報錯:

1

2

3

4

5

6

7

8

/*

    兩者並不是方法的重載,擦除之后就是同一方法,所以編譯不會通過。

    擦除之后:

    void m(List numbers){}

    void m(List Strings){}   //編譯不通過,已經存在形同方法簽名

*/

   void method(List<Object> numbers){}

   void method(List<String> strings){}

泛型的這種情況稱為 不可變性,與之對應的概念是 協變、逆變:

  • 協變:如果 A 是 B 的父類,並且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父類,則稱之為協變的(父子關系保持一致)

  • 逆變:如果 A 是 B 的父類,但是 A 的容器 是 B 的容器的子類,則稱之為逆變(放入容器就篡位了)

  • 不可變:不論 A B 有什么關系,A 的容器和 B 的容器都沒有父子關系,稱之為不可變

Java 中數組是協變的,泛型是不可變的。

擦除的拯救者:邊界

我們知道,泛型運行時被擦除成原始類型,這使得很多操作無法進行.

如果沒有指明邊界,類型參數將被擦除為 Object。

如果我們想要讓參數保留一個邊界,可以給參數設置一個邊界,泛型參數將會被擦除到它的第一個邊界(邊界可以有多個),這樣即使運行時擦除后也會有范圍。

比如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class GenericErasure {

    interface Game{

      void play();

    }

    interface Program{

      void code();

   }

   public static calss People<T extends Program & Game>{

       private T mPeople;

        

       public People(T people){

         mPeople = people;

       }

       public void habit(){

         mPeople.code();

         mPeople.play();

        }

   }

    

 

}

上述代碼中, People 的類型參數 T 有兩個邊界,編譯器事實上會把類型參數替換為它的第一個邊界的類型。

七、泛型的規則

  • 泛型的參數類型只能是類(包括自定義類),不能是簡單類型。

  • 同一種泛型可以對應多個版本(因為參數類型是不確定的),不同版本的泛型類實例是不兼容的。

  • 泛型的類型參數可以有多個

  • 泛型的參數類型可以使用 extends 語句,習慣上稱為“有界類型”

  • 泛型的參數類型還可以是通配符類型,例如 Class

泛型的使用場景

當類中要操作的引用數據類型不確定的時候,過去使用 Object 來完成擴展,JDK 1.5后推薦使用泛型來完成擴展,同時保證安全性。

八、總結

1.上面說到使用 Object 來達到復用,會失去泛型在安全性和直觀表達性上的優勢,那為什么 ArrayList 等源碼中的還能看到使用 Object 作為類型?

泛型出現時,Java 平台即將進入它的第二個十年,在此之前已經存在了大量沒有使用泛型的 Java 代碼。人們認為讓這些代碼全部保持合法,並且能夠與使用泛型的新代碼互用,非常重要。

這樣都是為了兼容,新代碼里要使用泛型而不是原始類型。

2.泛型是通過擦除來實現的。因此泛型只在編譯時強化它的類型信息,而在運行時丟棄(或者擦除)它的元素類型信息。擦除使得使用泛型的代碼可以和沒有使用泛型的代碼隨意互用。

3.如果類型參數在方法聲明中只出現一次,可以用通配符代替它。

1

2

3

private <E> void swap(List<E> list, int i, int j){

    //....

}

只出現了一次 類型參數,沒有必要聲明,完全可以用通配符代替:

1

2

3

private void swap(List<?> list, int i, int j){

    //...

}

對比一下,第二種更加簡單清晰吧。

4.數組中不能使用泛型

Array 事實上並不支持泛型,這也是為什么 Joshua Bloch 在 《Effective Java》一書中建議使用 List 來代替 Array,因為 List 可以提供編譯期的類型安全保證,而 Array 卻不能。

5.Java 中 List<Object> 和原始類型 List 之間的區別?

  • 在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查

  • 通過使用 Object 作為類型,可以告知編譯器該方法可以接受任何類型的對象,比如String 或 Integer

  • 你可以把任何帶參數的類型傳遞給原始類型 List,但 卻不能把 List< String> 傳遞給接受 List< Object> 的方法,因為泛型的不可變性,會產生編譯錯誤。

九、補充

靜態資源不認識泛型

接上一個話題,如果把<T>去掉,那么:

1

2

3

private static T ifThenElse(boolean b, T first, T second){

    return b ? first : second;

}

報錯,T未定義。但是如果我們再把static去掉:

1

2

3

4

5

6

7

8

public class TestMain<T>{

    public static void main(String[] args){}

     

   @SuppressWarnings("unused")

   private List<T> ifThenElse(boolean b,T first, T second){

     return null;

   }

}

這並不會有任何問題。兩相對比下,可以看出static方法並不認識泛型,所以我們要加上一個<T>,告訴static方法,后面的T是一個泛型。既然static方法不認識泛型,那我們看一下static變量是否認識泛型:

1

2

3

4

public class TestMain<T>{

    private List<T> notStaticList;

    private static List<T> staticList;

}

這證明了,static變量也不認識泛型,其實不僅僅是staic方法、static變量、static塊,也不認識泛型,可以自己試一下。總結起來就是一句話:靜態資源不認識泛型。

總結:以上就是本篇文的全部內容,希望能對大家的學習有所幫助。更多相關教程請訪問Java視頻教程java開發圖文教程bootstrap視頻教程

以上就是深入理解什么是Java泛型?泛型怎么使用?的詳細內容,更多請關注php中文網其它相關文章

 

參考

本文完全參考自: 深入理解什么是Java泛型?泛型怎么使用?==>http://m.php.cn/article/411947.html?tdsourcetag=s_pctim_aiomsg


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM