程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA編程入門知識 >> 如何理解 Java 中的 <T extends Comparable<? super T>>

如何理解 Java 中的 <T extends Comparable<? super T>>

編輯:JAVA編程入門知識

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

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

我知道 extendssuper 這樣的關鍵字在泛型中是干什麼的,但對上面這樣復雜的類型參數聲明著實有點看不懂。

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

<T extends Comparable<T>>

可 T 偏偏被聲明成這樣:

<T extends Comparable<? super T>>

搞這麼復雜圖啥呢?難道 Java 只是高智商人士的玩具?

平常寫工具類的機會比較少,上面的方法參數類型看不懂或寫不出來問題倒也不大。只要知道怎麼調用這些方法,日子就能混過去。我估計像我這樣混日子的程序員不少吧。

終於有一天,我覺得有點對不起 Java Developer 這個頭銜了,於是認真看了看書,認真 Google 了一下,終於搞明白了這樣的類型參數是怎麼回事兒。

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

<T extends Comparable<T>>
類型 T 必須實現 Comparable 接口,並且這個接口的類型是 T。只有這樣,T 的實例之間才能相互比較大小。例如,在實際調用時若使用的具體類是 Dog,那麼 Dog 必須 implements Comparable<Dog>
<T extends Comparable<? super T>>
類型 T 必須實現 Comparable 接口,並且這個接口的類型是 T 或 T 的任一父類。這樣聲明後,T 的實例之間,T 的實例和它的父類的實例之間,可以相互比較大小。例如,在實際調用時若使用的具體類是 Dog (假設 Dog 有一個父類 Animal),Dog 可以從 Animal 那裡繼承 Comparable<Animal> ,或者自己 implements Comparable<Dog>

2 我對 <T extends Comparable<? super T>> 類型參數的理解

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

2.1 代碼運行環境

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

javac -encoding UTF-8 TypeParameterTest.java

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

2.2 示例代碼

 1: package generics3;
 2: 
 3: import java.util.ArrayList;
 4: import java.util.Collections;
 5: import java.util.List;
 6: 
 7: public class TypeParameterTest
 8: {
 9:     //第一種聲明:簡單,靈活性低
10:     public static <T extends Comparable<T>> void mySort1(List<T> list)
11:     {
12:         Collections.sort(list);
13:     }
14: 
15:     //第二種聲明:復雜,靈活性高
16:     public static <T extends Comparable<? super T>> void mySort2(List<T> list)
17:     {
18:         Collections.sort(list);
19:     }
20: 
21:     public static void main(String[] args)
22:     {
23:         //在這個方法中要創建一個 Animal List 和一個 Dog List,然後分別調用兩個排序方法。
24:     }
25: }
26: 
27: class Animal implements Comparable<Animal>
28: {
29:     protected int age;
30: 
31:     public Animal(int age)
32: 
33:     {
34:         this.age = age;
35:     }
36: 
37:     //使用年齡與另一實例比較大小
38:     @Override
39:     public int compareTo(Animal other)
40:     {
41:         return this.age - other.age;
42:     }
43: }
44: 
45: class Dog extends Animal
46: {
47:     public Dog(int age)
48:     {
49:         super(age);
50:     }
51: }

上面的代碼包括三個類:

  1. Animal 實現了 Comparable<Animal> 接口,通過年齡來比較實例的大小
  2. Dog 繼承自 Animal
  3. TypeParameterTest 類中提供了兩個排序方法和測試用的 main() 方法:
    • mySort1() 使用 <T extends Comparable<T>> 類型參數
    • mySort2() 使用 <T extends Comparable<? super T>> 類型參數
    • main() 測試方法。在這個方法中要創建一個 Animal List 和一個 Dog List ,然後分別調用兩個排序方法

2.3 測試 mySort1() 方法

 1: // 創建一個 Animal List
 2: List<Animal> animals = new ArrayList<Animal>();
 3: animals.add(new Animal(25));
 4: animals.add(new Dog(35));
 5: 
 6: // 創建一個 Dog List
 7: List<Dog> dogs = new ArrayList<Dog>();
 8: dogs.add(new Dog(5));
 9: dogs.add(new Dog(18));
10: 
11: // 測試  mySort1() 方法
12: mySort1(animals);
13: mySort1(dogs);

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

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

為什麼會出錯誤呢? mySort1() 方法的類型參數是 <T extends Comparable<T>> ,它要求的類型參數是類型為 T 的 Comparable

如果傳入的是 List<Animal> ,沒問題,因為 Animal implements Comparable<Animal>

但是,如果傳入的參數是 List<Dog> 有問題,因為 Dog 沒有 implements Comparable<Dog> ,它只是從 Animal 繼承了一個 Comparable<Animal>

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

2.4 測試 mySort2() 方法

 1: public static void main(String[] args)
 2: {
 3:     // 創建一個 Animal List
 4:     List<Animal> animals = new ArrayList<Animal>();
 5:     animals.add(new Animal(25));
 6:     animals.add(new Dog(35));
 7: 
 8:     // 創建一個 Dog List
 9:     List<Dog> dogs = new ArrayList<Dog>();
10:     dogs.add(new Dog(5));
11:     dogs.add(new Dog(18));
12: 
13:     // 測試  mySort2() 方法
14:     mySort2(animals);
15:     mySort2(dogs);
16: }

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

2.5 Dog 可以 implements Comparable<Dog> 嗎?

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

1: class Dog extends Animal implements Comparable<Dog>
2:  {
3:      public Dog(int age)
4:      {
5:          super(age);
6:      }
7:  }

很不幸,出錯了。Eclipse 說:

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

就是說,Dog 已經從父類 Animal 那裡繼承了一個 Comparable ,它不能再實現一個 Comparable

如果子類不喜歡父類的實現怎麼辦? Override 父類的 public int compareTo(Animal other) 方法。

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

對 Animal/Dog 這兩個有父子關系的類來說: <T extends Comparable<? super T>> 可以接受 List<Animal> ,也可以接收 List<Dog> 。 而 <T extends Comparable<T>> 只可以接收 List<Animal>

所以,<T extends Comparable<? super T>> 這樣的類型參數對所傳入的參數限制更少,提高了 API 的靈活性。總的來說,在保證類型安全的前提下,要使用限制最少的類型參數。

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.Datejava.util.Date 的子類。
  • java.util.Date 實現了 Comparable<java.util.Date>~,所以 ~java.sql.Date 也擁有了 Comparable<java.util.Date> 類型。
  • java.sql.Date 不能再 implements Comparable<java.sql.Date>
  • 如果你有一個 List<java.sql.Date> 並對它排序的話,只能傳給擁有 <T extends Comparable<? super T>> 這種類型參數的方法。

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

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

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

3.3 泛型是個腦力活

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

3.3.1 腦子開竅開大了

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

<Dog extends Comparable<Dog>>
<Dog extends Comparable<? super Dog>>

我挺得意,覺得這樣先用具體的類理解,然後再換成一般的類型,由具體到一般,多符合邏輯啊!後來發現這樣的聲明有個大問題,Eclipse 給了個黃色警告:

The type parameter Dog is hiding the type Dog

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

<Dog extends Comparable<Dog>> 這個聲明中,extends 前面的部分必須是類型參數。類型參數一般用 T,E 這樣的大寫字母,但也可以是小寫或者一個單詞(只要是個標識符就行)。所以,Dog 在這裡是一個類型參數,不是一個具體類。但我已經創建過一個具體的 Dog 類了。怎麼辦?類型參數 Dog 贏了,具體類 Dog 暫時靠邊站。類似於你有一個實例變量 x 。然後你在一個方法中又聲明了一個局部變量也叫 x 。在執行這個方法時,方法中的這個局部變量 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 ,也能實現跟第一個方法同樣的功能,我為什麼非得要一個 T 呢?在腦子思慮過度的情況下,進死胡同了。在我准備放狗搜之前,總算想明白了。

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

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

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

1: public <T extends Comparable<? super T>> T test1(T t, List<T> list)
2: {
3:     for (T element : list)
4:     {
5:         if (element.equals(t))
6:             return t;
7:     }
8:     return null;
9: }

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

你也可以定義多個類型參數,並讓這些參數之間有關聯:

1: public <T, S extends T> T test2(T t, S s)
2: {
3:     return s;
4: }

3.3.3 多練習練習

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

  1. What is <? super T> syntax?
  2. What is the difference between these class declarations with Comparable?
  3. Java:泛型
  4. Effective Java (2nd Edition)
  5. SCJP Sun Certified Programmer for Java 6 Exam 310-065

Created: 2016-04-13 Wed 21:03

Emacs 24.5.1 (Org mode 8.2.10)

Validate

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved