Java泛型

1、基本概念和原理

之前我們一直強調(diào)數(shù)據(jù)類型的概念,Java有8中基本數(shù)據(jù)類型??梢远x類,類相當(dāng)于自定義數(shù)據(jù)類型,類之間還可以有組合和繼承。我們也介紹了接口,其中提到,很多時候我們關(guān)心的不是類型,而是能力,針對接口和能力編程,不僅可以復(fù)用代碼,還可以降低耦合度,提高靈活性。

泛型將接口的概念進一步延伸,泛型字面上的意思是廣泛的類型。類,接口和方法代碼可以應(yīng)用于非常廣泛的類型,代碼與他們能否操作的數(shù)據(jù)類型不再綁定在一起,同一套代碼可以應(yīng)用與多種數(shù)據(jù)類型,這樣,不僅可以復(fù)用代碼,降低耦合,而且可以提高代碼的可讀性和安全性。

  • 1.1 一個簡單泛型類

    • 基本概念

        public class Pair<T> {  
             T first;  
             T second;  
             public Pair(T first, T second){  
                this.first = first;  
                this.second = second;  
             }  
             public T getFirst() {  
                return first;  
             }  
             public T getSecond() {  
                return second;  
             }  
        }  
      

      T表示類型參數(shù),泛型就是類型參數(shù)化,處理的數(shù)據(jù)類型不再是固定的,而是可以作為參數(shù)傳遞。

        public class Pair<U, V> {  
             U first;
             V second;
             public Pair(U first, V second){
             this.first = first;
             this.second = second;
             }
             public U getFirst() {
             return first;
             }
             public V getSecond() {
             return second;
             }
        }  
      

      類型參數(shù)可以有多個。

    • 基本原理

      定義普通類,使用Object。

        public class Pair {
             Object first;
             Object second;
             public Pair(Object first, Object second){
                 this.first = first;
                 this.second = second;
             }
             public Object getFirst() {
                return first;
             }
             public Object getSecond() {
                return second;
            }
        }
      
      
        Pair minmax = new Pair(1,100);
        Integer min = (Integer)minmax.getFirst();
        Integer max = (Integer)minmax.getSecond();
      

      實際上,Java泛型的內(nèi)部原理就是這樣的。
      我們知道,Java有Java編譯器和Java虛擬機,編譯器將Java源代碼轉(zhuǎn)換成.class文件,虛擬機加載并運行.class文件。對于泛型類,Java編譯器會將泛型代碼轉(zhuǎn)換成普通的非泛型代碼,就像上面的Pair,將類型T擦掉,替換成Object,插入必要的強制類型轉(zhuǎn)換。Java虛擬機實際執(zhí)行的時候,它是不知道泛型這回事,只知道普通的類及代碼。
      再強調(diào)一下,Java泛型是通過擦除實現(xiàn)的,類定義的類型參數(shù)T會被替換成Object。認識這一點是非常重要的,它有助于我們理解Java泛型的很多限制。

    • 泛型的好處

      • 更好的安全性
      • 更好的閱讀性

      語言和程序設(shè)計的一個重要目標是將bug盡量消滅在搖籃里,能消滅在寫代碼的時候,就不要等到代碼寫完程序運行的時候。只使用Object,代碼寫錯的時候,開發(fā)和編譯器都不能幫我們發(fā)現(xiàn)問題,運行的時候拋出類型轉(zhuǎn)換異常ClassCastException。如果使用泛型,就不可能犯這個錯誤。
      通過使用泛型,為程序多設(shè)置一道安全防護網(wǎng)。使用泛型,還可以省去繁瑣的強制類型轉(zhuǎn)換,再加上明確的類型信息,代碼的可讀性也會更好。

  • 1.2 容器類

    泛型類最常用的用途是作為容器類。所謂容器類,簡單來說,就是容納并管理多項數(shù)據(jù)的類。計算機技術(shù)有一門專業(yè)的課程,叫數(shù)據(jù)結(jié)構(gòu),專門討論管理數(shù)據(jù)的各種方式。
    這些數(shù)據(jù)結(jié)構(gòu)在Java中的實現(xiàn)主要就是Java中的各種容器類,甚至Java泛型的引入主要也是為了更好的支持Java容器。
    我們先來實現(xiàn)一個簡單的動態(tài)數(shù)組容器。

      public class DynamicArray<E> {  
    
           private static final int DEFAULT_CAPACITY = 10;
           private int size;
           private Object[] elementData;
           public DynamicArray() {
              this.elementData = new Object[DEFAULT_CAPACITY];
           }
           private void ensureCapacity(int minCapacity) {
               int oldCapacity = elementData.length;
               if(oldCapacity >= minCapacity){
               return;
               }
               int newCapacity = oldCapacity * 2;
               if(newCapacity < minCapacity)
               newCapacity = minCapacity;
               elementData = Arrays.copyOf(elementData, newCapacity);
           }
           public void add(E e) {
               ensureCapacity(size + 1);
               elementData[size++] = e;
           }
           public E get(int index) {
              return (E)elementData[index];
           }
           public int size() {
              return size;
           }
           public E set(int index, E element) {
               E oldValue = get(index);
               elementData[index] = element;
               return oldValue;
           }
      }
    

    使用容器時,如果不指定泛型,可以添加任何元素,但是如果指定了泛型,就只能添加指定類型元素。
    通常我們在使用容器時,都會指定泛型。這樣放進去的元素都是固定類型,取出來的也是固定類型。
    試想一下,假如不指定泛型,可以放入任何類型元素。取出來時不知道是什么類型,更不知道如何轉(zhuǎn)換。

  • 1.3 泛型方法

    除了泛型類,方法也可以是泛型,而且,一個方法的是不是泛型的,與它所在類是不是泛型的沒有關(guān)系。

      public static <T> int indexOf(T[] arr, T elm){
           for(int i=0; i<arr.length; i++){
               if(arr[i].equals(elm)){
                  return i;
               }
           }
           return -1;
      }
    
  • 1.4 泛型接口

    接口也可以是泛型的,例如Comparable和Comparator接口都是泛型的。

  • 1.5 類型參數(shù)的限定

    Java支持限定這個類型參數(shù)的一個上界,也就是說,類型參數(shù)必須為給定的上界類型或者其子類型,這個限定是通過extends關(guān)鍵字來表示的。這個上界可以是某個具體的類或者某個具體的接口,也可以是其他的類型參數(shù),我們逐個介紹其應(yīng)用。

    • 上界為某個具體的類

        public class NumberPair<U extends Number, V extends Number>  extends Pair<U, V> {  
             public NumberPair(U first, V second) {  
                super(first, second);  
             }  
        }  
      

      指定邊界后,類型擦除時就不會轉(zhuǎn)換成Object了,而是轉(zhuǎn)換成它的邊界類型。

    • 上界為某個接口

        public static <T extends Comparable> T max(T[] arr){
             T max = arr[0];
             for(int i=1; i<arr.length; i++){
                 if(arr[i].compareTo(max)>0){
                    max = arr[i];
                 }
             }
             return max;
        }
      

      max方法計算泛型數(shù)組元素的最大值。計算最大值需要元素之間進行比較,要求元素實現(xiàn)Comparable接口,所以給類型參數(shù)設(shè)置了一個上邊界Comparable ,T必須實現(xiàn)Comparable接口。
      不過直接這樣編寫代碼,Java會有一個警告信息,因為Comparable是一個泛型接口,它也需要一個類型參數(shù),所以完整的聲明應(yīng)該是:

        public static <T extends Comparable<T>> T max(T[] arr){
             
        }    
      

      這種形式成為遞歸類型限制,可以這么解讀:T表示一種數(shù)據(jù)類型,必須實現(xiàn)Comparable接口,且必須可以與相同類型的元素進行比較。

    • 上界為其他類型參數(shù)

      Java支持一個類型參數(shù)以另一個類型參數(shù)作為上界。為什么需要這個呢?我們看個例子,給上面的DynamicArray添加一個addAll。

        public void addAll(DynamicArray<E> c) {
             for(int i=0; i<c.size; i++){
                add(c.get(i));
             }
        }
      

      但是這么寫有一些局限性,我們看它的使用代碼

        DynamicArray<Number> numbers = new DynamicArray<>();
        DynamicArray<Integer> ints = new DynamicArray<>();
        ints.add(100);
        ints.add(34);
        numbers.addAll(ints); //編譯錯誤
      

      numbers是Number類型的容器,ints是Integer類型的容器,我們希望將ints假如numbers中,因為Integer是Number的子類,應(yīng)該說這是一個合理的需求和操作。
      但是Java會在numbers.addAll(ints)這行代碼上提示編譯錯誤:addAll需要類型為DynamicArray<Number>,而提供的是DynamicArray<Integer>,不適用。
      Integer是Number的子類,怎么會不適用呢?
      事實就是這樣,確實不適用,而且是很有道理的,假設(shè)適用,我們看下會發(fā)生什么。

        DynamicArray<Integer> ints = new DynamicArray<>();
        DynamicArray<Number> numbers = ints; //假設(shè)合法
        numbers.add(new Double(12.34));
      

      那最后一行就是合法的,這時,DynamicArray<Integer>就會出現(xiàn)Double類型的值,而這顯示破壞了Java泛型關(guān)于泛型安全的保證。
      我們強調(diào)一下,雖然Integer是Number的子類,但是DynamicArray<Integer>并不是DynamicArray<Number>的子類,DynamicArray<Integer>對象也不能賦值給DynamicArray<Number>的變量,這一點初看上去是違反直覺的,但這是事實,必須要理解這一點。

      不過我們的需求是合理的,將Integer類型的容器添加到Number類型的容器沒有問題,這個問題可以通過類型限定為其他類型參數(shù)來解決:

        public <T extends E> void addAll(DynamicArray<T> c) {
             for(int i=0; i<c.size; i++){
                add(c.get(i));
             }
        }
      

      E是DynamicArray的類型參數(shù),T是addAll的類型參數(shù),T的上界限定為E,這樣,下面得代碼就沒有問題。

        DynamicArray<Number> numbers = new DynamicArray<>();
        DynamicArray<Integer> ints = new DynamicArray<>();
        ints.add(100);
        ints.add(34);
        numbers.addAll(ints)
      
  • 1.6 通配符

    • 1.6.1 更簡潔的類型參數(shù)限定

      我們之前提到的例子,為了將Integer類型容器添加到Number類型容器,我們的類型參數(shù)使用了其他類型參數(shù)作為上界,我們提到這種寫法過于繁瑣,它可以替換成下面簡潔的通配符形式:

        public void addAll(DynamicArray<? extends E> c) {
             for(int i=0; i<c.size; i++){
                add(c.get(i));
             }
        }
      

      ?表示通配符,<? extends E>表示匹配E或E的某個子類,具體是什么子類是未知的。適用這個方法的代碼可以不做任何改動,還可以是:

        DynamicArray<Number> numbers = new DynamicArray<>();
        DynamicArray<Integer> ints = new DynamicArray<>();
        ints.add(100);
        ints.add(34);
        numbers.addAll(ints);
      

      這里E是Number類型,DynamicArray<? extends E>可以匹配DynamicArray<Integer>。

      那么問題來了,同樣是extends關(guān)鍵字,同樣應(yīng)用于泛型,<T extends E>和<? extends E>有什么區(qū)別?
      它們用的地方不一樣:
      1.<T extends E>用于定義類型參數(shù),它聲明了一個類型參數(shù)T,可以放在泛型類定義中類名后面、泛型方法返回值前面。
      2.<? extends E>用于實例化類型參數(shù),它用于實例化泛型變量的類型參數(shù),只是具體這個類型是未知的,只知道它是E或者E的子類。
      雖然它們不一樣,但兩種寫法可以達到相同的目標。
      那么我們到底應(yīng)該用那種形式呢?我們先進一步了解通配符,然后再解釋。

    • 1.6.2 理解通配符

      • 無限定通配符

          public static int indexOf(DynamicArray<?> arr, Object elm){  
               for(int i=0; i<arr.size(); i++){  
                   if(arr.get(i).equals(elm)){  
                      return i;  
                   }  
               }  
               return -1;  
          }  
        

        這種無限定通配符形式也可以改為使用類型參數(shù):

          public static <T> int indexOf(DynamicArray<T> arr, Object elm)  
        

        不過,通配符形式更加簡潔。

      • 通配符限制

        但是上面那種形式通配符都有一個重要的限制:只能讀,不能寫。怎么理解,看下面的例子:

          DynamicArray<Integer> ints = new DynamicArray<>();
          DynamicArray<? extends Number> numbers = ints;
          Integer a = 200;
          numbers.add(a); //錯誤
          numbers.add((Number)a); //錯誤
          numbers.add((Object)a); //錯誤
        

        三種add方法都是非法的,無論是Integer、還是Number或Object。編譯器都會報錯。為什么呢?問號就是表示類型安全未知,? extends Number表示是Number的某個子類,但是不知道具體子類,如果允許寫入,Java就無法確保類型安全性,所以干脆禁止。我們來看個例子,假如允許寫入會發(fā)生什么:

          DynamicArray<Integer> ints = new DynamicArray<>();
          DynamicArray<? extends Number> numbers = ints;
          Number n = new Double(23.0);
          Object o = new String("hello world");
          numbers.add(n);
          numbers.add(o);
        

        如果允許寫入Object或者Number類型,則最后兩行編譯就是正確的,也就是說,Java允許將Double或者String對象放入Integer容器,顯然這是違背了Java關(guān)于類型安全的承諾。
        大部分情況下,這種限制是好的,但是使得一些理應(yīng)正確的基本操作無法完成,比如交換兩個元素的位置,看如下代碼

          public static void swap(DynamicArray<?> arr, int i, int j){
               Object tmp = arr.get(i);
               arr.set(i, arr.get(j));
               arr.set(j, tmp);
          }
        

        這個代碼看上去應(yīng)該是正確的,但是Java會提示編譯錯誤,兩行set語句是不合法的。不過,借助帶類型參數(shù)的限定方法,這個問題可以解決:

          private static <T> void swapInternal(DynamicArray<T> arr, int i, int j){
               T tmp = arr.get(i);
               arr.set(i, arr.get(j));
               arr.set(j, tmp);
          }
        
          public static void swap(DynamicArray<?> arr, int i, int j){
              swapInternal(arr, i, j);
          }
        

        swap可以調(diào)用swapInternal,而帶類型參數(shù)的swapInternal可以寫入。Java容器中就有類型這樣的用法,公共的API是通配符形式,但內(nèi)部調(diào)用帶類型參數(shù)的方法。

        如果類型參數(shù)之前有依賴關(guān)系,也只能用類型參數(shù)形式。

          public static <D,S extends D> void copy(DynamicArray<D> dest,
               DynamicArray<S> src){
               for(int i=0; i<src.size(); i++){
                  dest.add(src.get(i));
               }
          }
        

        不過上面的聲明可以使用通配符簡化,兩個參數(shù)合并為一個參數(shù)

          public static <D> void copy(DynamicArray<D> dest,
               DynamicArray<? extends D> src){
               for(int i=0; i<src.size(); i++){
                  dest.add(src.get(i));
               }
          }
        

        如果返回值依賴類型參數(shù),也只能使用類型參數(shù)。

          public static <T extends Comparable<T>> T max(DynamicArray<T> arr){
               T max = arr.get(0);
               for(int i=1; i<arr.size(); i++){
                   if(arr.get(i).compareTo(max)>0){
                      max = arr.get(i);
                   }
              }
               return max;
          }
        
      • 通配符與類型參數(shù)形式總結(jié):

        • 通配符形式都可以用類型參數(shù)形式替代,通配符能做的,用類型參數(shù)都能做。
        • 通配符形式可以減少類型參數(shù),形式上往往更為簡單,可讀性也更好,所以,能使用通配符的地方就使用通配符。
        • 如果類型參數(shù)之前有依賴關(guān)系、或者返回值依賴類型參數(shù),或者需要寫操作,則只能使用類型參數(shù)。
    • 1.6.3 超類型通配符

      還有一種通配符,與形式<? extends E>正好相反,它的形式為<? super E>,稱為超類型通配符。表示E的某個父類型。它有什么作用呢?有了它,我們就可以更靈活的寫入了。
      如果沒有這些寫法,寫入會有一些限制。來看個例子,我們給DynamicArray添加一個方法:

        public void copyTo(DynamicArray<E> dest){
             for(int i=0; i<size; i++){
                dest.add(get(i));
             }
        }
      

      這個方法很簡單,將當(dāng)前容器中的元素添加到傳入的容器中。我們希望可以這么使用:

        DynamicArray<Integer> ints = new DynamicArray<Integer>();
        ints.add(100);
        ints.add(34);
        DynamicArray<Number> numbers = new DynamicArray<Number>();
        ints.copyTo(numbers);
      

      Integer是number的子類,將Integer對象拷貝如Number容器,這種用法應(yīng)該合情合理的,但是Java會提示編譯錯誤,理由我們之前說過,期望類型是DynamicArray<Integer>,提供的是DynamicArray<Number>不適用。
      如之前所說,不能將DynamicArray<Integer>看作DynamicArray<Number>。
      Java解決這個問題的方法就是超類型通配符,可以將copyTo代碼改成:

        public void copyTo(DynamicArray<? super E> dest){
             for(int i=0; i<size; i++){
                dest.add(get(i));
             }
        }
      

      這樣就沒有問題了。

    • 1.6.4 超類型通配符在Comparable、Comparator的使用

      Comparable、Comparator接口不使用超類型通配符的限制,以前面計算最大值的方法為例,它的聲明如下:

        public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
      

      舉個例子,有兩個類Base、Child。Base代碼如下:

        class Base implements Comparable<Base>{
             private int sortOrder;
             public Base(int sortOrder) {
                this.sortOrder = sortOrder;
             }
             @Override
             public int compareTo(Base o) {
                 if(sortOrder < o.sortOrder){
                    return -1;
                 }else if(sortOrder > o.sortOrder){
                    return 1;
                 }else{
                    return 0;
                 }
             }
        }
      

      Child代碼如下:

        class Child extends Base {  
             public Child(int sortOrder) {  
                super(sortOrder);  
             }  
        }  
      

      注意:Child沒有重新實現(xiàn)Comparable接口,因此child的比較規(guī)則跟Base的比較規(guī)則是一致的。我們希望可以使用前面的max方法操作Child容器,如下所示:

        DynamicArray<Child> childs = new DynamicArray<Child>();
        childs.add(new Child(20));
        childs.add(new Child(80));
        Child maxChild = max(childs);
      

      遺憾的是,Java會提示編譯錯誤,類型不匹配。為什么會不匹配呢?我們可能認為,Java會將max方法的類型參數(shù)T推斷為Child,但類型T的要求是extends Comparable<T>,而Child并沒有實現(xiàn)Comparable<Child>,它實現(xiàn)的是Comparable<Base>。
      但我們的需求是合理的,Java為了解決這個問題,修改max方法的聲明,使用超類型通配符,如下所示:

        public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)
      

      <? super Child>可以匹配Base,所以整體是匹配的。

    • 1.6.5 超類型通配符與類型參數(shù)限定。

      類型參數(shù)限定只有extends形式,沒有super形式。如果之前copyTo方法:

        public void copyTo(DynamicArray<? super E> dest)
      

      如果類型參數(shù)限定支持super形式,則應(yīng)該是:

        public <T super E> void copyTo(DynamicArray<T> dest)
      

      事實是,Java不支持這種語法。
      前面我們說過,對于限定的通配符形式<? extends E>,可以用類型參數(shù)限定替代,但是對于上面的超類型通配符,則無法使用類型參數(shù)限定替代。

    • 1.6.6 通配符總結(jié)

      本文介紹了泛型中三種通配符形式<?>、<? extends E>、<? super E>,并分析了與類型參數(shù)形式的區(qū)別和聯(lián)系,他們比較容易混淆,我們總結(jié)比較如下:

      • 它們的目的都是為了使方法、接口更靈活,可以接收更為廣泛的類型。
      • <? super E>用于靈活寫入或比較,使得對象可以寫入父類型的容器,使得父類型的比較方法可以應(yīng)用于子類對象,它不能被類型參數(shù)形式替代。
      • <?>、<? extends E>用于靈活讀取,使得方法可以讀取E或者E的任意子類型的容器對象,它們可以用類型參數(shù)形式替代,但通配符形式更為簡潔。通常情況下他們交叉使用,公共的API是通配符形式,但內(nèi)部調(diào)用帶類型參數(shù)的方法。

2、參考文獻

Java編程的邏輯 第8章 泛型

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容