[轉(zhuǎn)]Java 泛型增補一 -- 如何理解 Java 中的 <T extends Comparable <? super T>>

說明:CSDN 平臺上 Markdown 編輯器識別 < 和 > 為非法字符,導(dǎo)致文章無法發(fā)布,于是使用 [ 和 ] 來替代。說實話,轉(zhuǎn)載文章的排版真是能瞅死個眼啊。


Java 中類似 <T extends Comparable<? super T>> 這樣的類型參數(shù) (Type Parameter)在 JDK 中或工具類方法中經(jīng)常能看到。比如 java.util.Collections 類中的這個方法聲明:

public static <T extends Comparable<? super T>> void sort (List <T> list)

我知道 extendssuper 這樣的關(guān)鍵字在泛型中是干什么的,但對上面這樣復(fù)雜的類型參數(shù)聲明著實有點看不懂。

我覺得類型參數(shù)T 寫成這樣就足夠了:

<T extends Comparable<T>>

T 偏偏被聲明成這樣:

<T extends Comparable<? super T>>

搞這么復(fù)雜圖啥呢?難道 Java 只是高智商人士的玩具?

終于有一天,我覺得有點對不起 Java Developer 這個頭銜了,于是認(rèn)真看了看書,認(rèn)真 Google 了一下,終于搞明白了這樣的類型參數(shù)是怎么回事兒。

1、<T extends Comparable<T>><T extends Comparable<? super T>> 有什么不同

<T extends Comparable<T>>中類型 T必須實現(xiàn) Comparable 接口,并且這個接口的 參數(shù)類型T。只有這樣,T 的實例之間才能相互比較大小。例如,在實際調(diào)用時若使用的具體類是 Dog,那么 Dog 必須 implements Comparable<Dog>

<T extends Comparable<? super T>> 中類型 T必須實現(xiàn) Comparable接口,并且這個接口的 參數(shù)類型TT 的任一父類。這樣聲明后,T 的實例之間、T的實例和它的父類的實例之間,可以相互比較大小。例如,在實際調(diào)用時若使用的具體類是 Dog (假設(shè) Dog 有一個父類 Animal),Dog 可以從 Animal 那里繼承 Comparable<Animal>,或者自己 implements Comparable<Dog> 。

2、 我對 <T extends Comparable<? super T>>類型參數(shù)的理解

光看上面的定義除了摸不著頭腦,不會有其它感覺。下面用代碼來說明為什么要這樣聲明。

2.1 代碼運行環(huán)境

我使用的 JDK 版本是:1.8.0_60 ,在 Eclipse 中編譯運行。因為注釋用了中文,編碼采用 UTF-8。如果你要在命令行下編譯、運行,編譯時要使用 -encoding UTF-8 選項:

javac -encoding UTF-8 TypeParameterTest.java

另外,Eclipse 中的警告、錯誤信息跟命令行中的不一樣(個人感覺 Eclipse 中的信息要好懂一些)。以下的示例以 Eclipse 中的信息為準(zhǔn)。

2.2 示例代碼

 package generics3;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;

 public class TypeParameterTest {
    //第一種聲明:簡單,靈活性低
    public static <T extends Comparable<T>> void mySort1(List<T> list) {
        Collections.sort(list);
    }

    //第二種聲明:復(fù)雜,靈活性高
    public static <T extends Comparable<? super T>> void mySort2(List<T> list) {
        Collections.sort(list);
    }

    public static void main(String[] args) {
        //在這個方法中要創(chuàng)建一個 Animal List 和一個 Dog List,然后分別調(diào)用兩個排序方法。
    }
}

class Animal implements Comparable<Animal> {
    protected int age;

    public Animal(int age) {
        this.age = age;
    }

    //使用年齡與另一實例比較大小
    @Override
    public int compareTo(Animal other) {
        return this.age - other.age;
    }
}

class Dog extends Animal {
    public Dog(int age) {
        super(age);
    }
}

上面的代碼包括三個類:

Animal 實現(xiàn)了Comparable<Animal> 接口,通過年齡來比較實例的大小。Dog繼承自 Animal 。TypeParameterTest 類中提供了兩個排序方法和測試用的 main() 方法:

  • mySort1() 使用 <T extends Comparable<T>> 類型參數(shù)
  • mySort2() 使用 <T extends Comparable<? super T>> 類型參數(shù)
  • main() 測試方法。在這個方法中要創(chuàng)建一個Animal List 和一個Dog List ,然后分別調(diào)用兩個排序方法

2.3 測試 mySort1() 方法

 // 創(chuàng)建一個 Animal List
 List<Animal> animals = new ArrayList<Animal>();
 animals.add(new Animal(25));
 animals.add(new Dog(35));

 // 創(chuàng)建一個 Dog List
 List<Dog> dogs = new ArrayList<Dog>();
 dogs.add(new Dog(5));
 dogs.add(new Dog(18));

 // 測試  mySort1() 方法
 mySort1(animals);
 mySort1(dogs);

Line 13 出編譯錯誤了。Eclipse 說:

The method mySort1(List<T>) in the type TypeParameterTest is not applicable for the arguments (List<Dog>)

為什么會出錯誤呢? mySort1() 方法的類型參數(shù)是 <T extends Comparable<T>>,它要求的類型參數(shù)是類型為 TComparable 。如果傳入的是List<Animal> ,沒問題,因為Animal implements Comparable<Animal> 。但是,如果傳入的參數(shù)是 List<Dog> 時會有問題,**因為 Dog 沒有 implements Comparable<Dog> **,它只是從 Animal 繼承了一個 Comparable<Animal> 。

不知道大家注意到?jīng)]有,那個 animals list 中實際上是包含一個 Dog 實例的 。如果你碰上類似的情況(子類 list 不能傳入到一個方法中),可以考慮把子類實例放到一個父類 List 中,避免編譯錯誤。

2.4 測試 mySort2() 方法

  public static void main(String[] args) {
        // 創(chuàng)建一個 Animal List
        List<Animal> animals = new ArrayList<Animal>();
        animals.add(new Animal(25));
        animals.add(new Dog(35));

        // 創(chuàng)建一個 Dog List
        List<Dog> dogs = new ArrayList<Dog>();
        dogs.add(new Dog(5));
        dogs.add(new Dog(18));

        // 測試  mySort2() 方法
        mySort2(animals);
        mySort2(dogs);
    }

兩個方法調(diào)用都沒有問題。 第二個方法不但可以接受 Animal implements Comparable<Animal> 這樣的參數(shù),也可以接收:Dog implements Comparable<Animal> 這樣的參數(shù)。

2.5 Dog 可以 implements Comparable<Dog> 嗎?

如果讓 Dog implements Comparable<Dog> 不也可以解決前面的那個編譯錯誤嗎?

 class Dog extends Animal implements Comparable<Dog>
 {
    public Dog(int age)
   {
        super(age);
    }
  }

很不幸,出錯了。Eclipse 說:

The interface Comparable cannot be implemented more than once with different arguments: Comparable<Animal> andComparable<Dog>

就是說,Dog已經(jīng)從父類 Animal那里繼承了一個Comparable ,它不能再實現(xiàn)一個 Comparable 。
如果子類不喜歡父類的實現(xiàn)怎么辦? Override 父類的 public int compareTo(Animal other) 方法。

2.6 <T extends Comparable<? super T>> 類型參數(shù)聲明的好處

對 Animal/Dog 這兩個有父子關(guān)系的類來說: <T extends Comparable<? super T>>可以接受 List<Animal> ,也可以接收List<Dog> 。 而 <T extends Comparable<T>> 只可以接收List<Animal>。所以,<T extends Comparable<? super T>> 這樣的類型參數(shù)對所傳入的參數(shù)限制更少,提高了 API 的靈活性??偟膩碚f,在保證類型安全的前提下,要使用限制最少的類型參數(shù)。

3 其他

3.1 JDK 中的例子

JDK 中這樣的例子很多,比如 java.util.Date 和 java.sql.Date 這兩個類:

public class Date implements java.io.Serializable, Cloneable, Comparable<Date>
public class Date extends java.util.Date

  • java.sql.Date 是 java.util.Date 的子類。
  • java.util.Date 實現(xiàn)了 Comparable<java.util.Date>,所以 ~java.sql.Date 也擁有了 Comparable<java.util.Date> 類型。
  • java.sql.Date 不能再 implementsComparable<java.sql.Date> 。
  • 如果你有一個List<java.sql.Date> 并對它排序的話,只能傳給擁有<T extends Comparable<? super T>> 這種類型參數(shù)的方法。

3.2 《Effective Java》 一書對 <T extends Comparable<? super T>> 這種類型參數(shù)的解釋

這本書使用 Produce-Extends, Consume-Super (PESC) 原則來解釋。這個原則不但可以幫你理解復(fù)雜的聲明,而且可以指導(dǎo)你在定義類型參數(shù)時,何時使用 extends ,何時使用 super,有助于你寫出復(fù)雜的、適應(yīng)性強的類型參數(shù)來。

有興趣的同學(xué)可以看看這本書的 Item 28: Use bounded wildcards to increase API flexibility

3.3 泛型是個腦力活

簡單的泛型很好理解很好用,但稍微復(fù)雜一點,就變得很難理解。

3.3.1 腦子開竅開大了

在琢磨這個問題時,我腦洞一開,心想,T 這樣的東西太一般化,有點摸不著頭腦,不好理解。如果把 T 換成一個具體類,應(yīng)該會好理解。于是我就想出了這樣兩個聲明:

<Dog extends Comparable<Dog>>

Dog extends Comparable<? super Dog>>

我挺得意,覺得這樣先用具體的類理解,然后再換成一般的類型,由具體到一般,多符合邏輯?。『髞戆l(fā)現(xiàn)這樣的聲明有個大問題,Eclipse 給了個黃色警告:

The type parameter Dog is hiding the type Dog

上面這句話翻譯過來就是: 類型參數(shù) Dog 掩蓋了 類型 Dog 。

<Dog extends Comparable<Dog>>這個聲明中,extends 前面的部分必須是類型參數(shù)。類型參數(shù)一般用T,E這樣的大寫字母,但也可以是小寫或者一個單詞(只要是個標(biāo)識符就行)。所以,Dog在這里是一個類型參數(shù),不是一個具體類。但我已經(jīng)創(chuàng)建過一個具體的Dog類了。怎么辦?類型參數(shù) Dog 贏了,具體類 Dog 暫時靠邊站。類似于你有一個實例變量x 。然后你在一個方法中又聲明了一個局部變量也叫x。在執(zhí)行這個方法時,方法中的這個局部變量 x 就暫時掩蓋了(shadow) 實例變量x。

3.3.2 腦子一點也不開竅

有時候想得多了,腦子就糊涂了,一點兒也不開竅,連簡單問題也不明白了。 比如,我可以這樣定義一個方法:

public static <T extends Animal> void mySort3(List<T> list)
{
    Collections.sort(list);
}

也可以這樣定義一個方法:

public static void mySort4(List<? extends Animal> list)
{
    Collections.sort(list);
}

第二個方法沒有T ,也能實現(xiàn)跟第一個方法同樣的功能,我為什么非得要一個T 呢?在腦子思慮過度的情況下,進死胡同了。在我準(zhǔn)備放狗搜之前,總算想明白了。

第二個方法中,參數(shù)是: List<? extends Animal> list 。 這個方法可以接收 List<Animal> ,也可以接收 List<Dog> 。這里沒有使用類型參數(shù),只是使用泛型的限定符對所傳入的 List 的類型做了一個限定。

而在第一個方法中,使用了一個類型參數(shù) T。這個 T 可以是 Animal ,也可以是 Animal的子類 Dog。

在第一個方法中,看不出定義一個類型參數(shù)有什么作用。但是,類型參數(shù)不但可以在方法參數(shù)中使用,也可以在方法返回值和方法體內(nèi)使用。比如下面這個方法:

 public <T extends Comparable<? super T>> T test1(T t, List<T> list)
{
    for (T element : list)
     {
         if (element.equals(t))
            return t;
     }
     return null;
 }

你定義了一個類型參數(shù) T ,這個 T 定義成 :<T extends Comparable<? super T>>。定義好之后,你就可以在參數(shù)中,返回值中,以及方法體內(nèi)使用這個 T 了。如果不使用類型參數(shù),是達不到這種效果的。

你也可以定義多個類型參數(shù),并讓這些參數(shù)之間有關(guān)聯(lián):

    public <T, S extends T> T test2(T t, S s)
    {
     return s;
     }

3.3.3 多練習(xí)練習(xí)

我從 JDK 中找了兩段程序,看看能不能看明白。

程序一
TreeSet 類是這樣聲明的:

public class TreeSet<E> extends AbstractSet<E>
     implements NavigableSet<E>, Cloneable, java.io.Serializable
它有個 constructor 是這樣定義的:

 public TreeSet(Comparator<? super E> comparator) {
   this(new TreeMap<>(comparator));
 }

程序二
Collections 類的 max() 方法:

public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) {
 if (comp==null) return (T)max((Collection) coll);

 Iterator<? extends T> i = coll.iterator();
 T candidate = i.next();

 while (i.hasNext()) {
     T next = i.next();
    if (comp.compare(next, candidate) > 0)
         candidate = next;
}
 return candidate;
}

這段程序也展示了類型參數(shù)的另一種用法:類型參數(shù)本身只是簡簡單單的 <T> ,但在方法參數(shù)和方法體中,卻以 T 為基礎(chǔ),向上向下進行了擴展:<? super T>,<? extends T>

4 推薦一本書:《SCJP Sun Certified Programmer for Java 6 Exam 310-065》

這本書的作者是:Kathy Sierra 和 Bert Bates,所以此書也被稱為“KB書”。從書名上就可以看出這本書是為 SCJP 6 認(rèn)證考試而寫的。因為是認(rèn)證考試教材,這本書講的不是很深,有很多方面也沒有涉及到。但這本書對 Java 基本概念的講解非常透徹而又不啰嗦。它不但是認(rèn)證寶典中的寶典,而且也非常適合已經(jīng)入門的 Java 程序員閱讀。這本書現(xiàn)在有 Java 7 的版本,不知道以后會不會出 Java 8 版本。如果出的話,我會去買一本。

5、參考書目和鏈接


本文原鏈接
What is <? super T> syntax?
What is the difference between these class declarations with Comparable?
Java:泛型
Effective Java (2nd Edition)
SCJP Sun Certified Programmer for Java 6 Exam 310-065

最后編輯于
?著作權(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)容