1. 程式人生 > >泛型通配符詳解

泛型通配符詳解

object 指向 string類型 類型參數 tlist i++ 泛型 通配符 如果

一、基本概念:
在學習Java泛型的過程中, 通配符是較難理解的一部分. 主要有以下三類:
1. 無邊界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>.
  無邊界的通配符的主要作用就是讓泛型能夠接受未知類型的數據.
2. 固定上邊界的通配符(Upper Bounded Wildcards):
  使用固定上邊界的通配符的泛型, 就能夠接受指定類及其子類類型的數據. 要聲明使用該類通配符, 采用<? extends E>的形式, 這裏的E就是該泛型的上邊界. 註意: 這裏雖然用的是extends關鍵字, 卻不僅限於繼承了父類E的子類, 也可以代指顯現了接口E的類.
3. 固定下邊界的通配符(Lower Bounded Wildcards):
  使用固定下邊界的通配符的泛型, 就能夠接受指定類及其父類類型的數據. 要聲明使用該類通配符, 采用<? super E>的形式, 這裏的E就是該泛型的下邊界. 註意: 你可以為一個泛型指定上邊界或下邊界, 但是不能同時指定上下邊界.

二、基本使用方法:
1. 無邊界的通配符的使用, 我們以在集合List中使用<?>為例. 如:

技術分享
 1 public static void printList(List<?> list) {
 2     for (Object o : list) {
 3         System.out.println(o);
 4     }
 5 }
 6 
 7 public static void main(String[] args) {
 8     List<String> l1 = new ArrayList<>();
 9     l1.add("aa");
10     l1.add("bb");
11     l1.add("cc");
12     printList(l1);
13     List<Integer> l2 = new ArrayList<>();
14     l2.add(11);
15     l2.add(22);
16     l2.add(33);
17     printList(l2);
18     
19 }
技術分享

這種使用List<?>的方式就是父類引用指向子類對象. 註意, 這裏的printList方法不能寫成public static void printList(List<Object> list)的形式, 原因我在上一篇博文中已經講過, 雖然Object類是所有類的父類, 但是List<Object>跟其他泛型的List如List<String>, List<Integer>不存在繼承關系, 因此會報錯.
有一點我們必須明確, 我們不能對List<?>使用add方法, 僅有一個例外, 就是add(null). 為什麽呢? 因為我們不確定該List的類型, 不知道add什麽類型的數據才對, 只有null是所有引用數據類型都具有的元素. 請看下面代碼:

技術分享
1 public static void addTest(List<?> list) {
2     Object o = new Object();
3     // list.add(o); // 編譯報錯
4     // list.add(1); // 編譯報錯
5     // list.add("ABC"); // 編譯報錯
6     list.add(null);
7 }
技術分享

由於我們根本不知道list會接受到具有什麽樣的泛型List, 所以除了null之外什麽也不能add.
還有, List<?>也不能使用get方法, 只有Object類型是個例外. 原因也很簡單, 因為我們不知道傳入的List是什麽泛型的, 所以無法接受得到的get, 但是Object是所有數據類型的父類, 所以只有接受他可以, 請看下面代碼:

技術分享
1 public static void getTest(List<?> list) {
2     // String s = list.get(0); // 編譯報錯
3     // Integer i = list.get(1); // 編譯報錯
4     Object o = list.get(2);
5 }
技術分享

那位說了, 不是有強制類型轉換麽? 是有, 但是我們不知道會傳入什麽類型, 比如我們將其強轉為String, 編譯是通過了, 但是如果傳入個Integer泛型的List, 一運行還會出錯. 那位又說了, 那麽保證傳入的String類型的數據不就好了麽? 那樣是沒問題了, 但是那還用<?>幹嘛呀? 直接List<String>不就行了.

2. 固定上邊界的通配符的使用, 我仍舊以List為例來說明:

技術分享
 1 public static double sumOfList(List<? extends Number> list) {
 2     double s = 0.0;
 3     for (Number n : list) {
 4         // 註意這裏得到的n是其上邊界類型的, 也就是Number, 需要將其轉換為double.
 5         s += n.doubleValue();
 6     }
 7     return s;
 8 }
 9 
10 public static void main(String[] args) {
11     List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
12     System.out.println(sumOfList(list1));
13     List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
14     System.out.println(sumOfList(list2));
15 }
技術分享

有一點我們需要記住的是, List<? extends E>不能使用add方法, 請看如下代碼:

技術分享
1 public static void addTest2(List<? extends Number> l) {
2     // l.add(1); // 編譯報錯
3     // l.add(1.1); //編譯報錯
4     l.add(null);
5 }
技術分享

原因很簡單, 泛型<? extends E>指的是E及其子類, 這裏傳入的可能是Integer, 也可能是Double, 我們在寫這個方法時不能確定傳入的什麽類型的數據, 如果我們調用:

1 List<Integer> list = new ArrayList<>();
2 addTest(list);

那麽我們之前寫的add(1.1)就會出錯, 反之亦然, 所以除了null之外什麽也不能add. 但是get的時候是可以得到一個Number, 也就是上邊界類型的數據的, 因為不管存入什麽數據類型都是Number的子類型, 得到這些就是一個父類引用指向子類對象.

3. 固定下邊界通配符的使用. 這個較前面的兩個有點難理解, 首先仍以List為例:

技術分享
 1 public static void addNumbers(List<? super Integer> list) {
 2     for (int i = 1; i <= 10; i++) {
 3         list.add(i);
 4     }
 5 }
 6 
 7 public static void main(String[] args) {
 8     List<Object> list1 = new ArrayList<>();
 9     addNumbers(list1);
10     System.out.println(list1);
11     List<Number> list2 = new ArrayList<>();
12     addNumbers(list2);
13     System.out.println(list2);
14     List<Double> list3 = new ArrayList<>();
15     // addNumbers(list3); // 編譯報錯
16 }
技術分享

我們看到, List<? super E>是能夠調用add方法的, 因為我們在addNumbers所add的元素就是Integer類型的, 而傳入的list不管是什麽, 都一定是Integer或其父類泛型的List, 這時add一個Integer元素是沒有任何疑問的. 但是, 我們不能使用get方法, 請看如下代碼:

技術分享
1 public static void getTest2(List<? super Integer> list) {
2     // Integer i = list.get(0); //編譯報錯
3     Object o = list.get(1);
4 }
技術分享

這個原因也是很簡單的, 因為我們所傳入的類都是Integer的類或其父類, 所傳入的數據類型可能是Integer到Object之間的任何類型, 這是無法預料的, 也就無法接收. 唯一能確定的就是Object, 因為所有類型都是其子類型.
使用? super E還有個常見的場景就是Comparator. TreeSet有這麽一個構造方法:

1 TreeSet(Comparator<? super E> comparator) 

就是使用Comparator來創建TreeSet, 大家應該都清楚, 那麽請看下面的代碼:

技術分享
 1 public class Person {
 2     private String name;
 3     private int age;
 4     /*
 5      * 構造函數與getter, setter省略
 6      */
 7 }
 8 
 9 public class Student extends Person {
10     public Student() {}
11     
12     public Student(String name, int age) {
13         super(name, age);
14     }
15 }
16 
17 class comparatorTest implements Comparator<Person>{
18     @Override
19     public int compare(Student s1, Student s2) {
20         int num = s1.getAge() - s2.getAge();
21         return num == 0 ? s1.getName().compareTo(s2.getName()) :  num;
22     }
23 }
24 
25 public class GenericTest {
26     public static void main(String[] args) {
27         TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
28         ts1.add(new Person("Tom", 20));
29         ts1.add(new Person("Jack", 25));
30         ts1.add(new Person("John", 22));
31         System.out.println(ts1);
32         
33         TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
34         ts2.add(new Student("Susan", 23));
35         ts2.add(new Student("Rose", 27));
36         ts2.add(new Student("Jane", 19));
37         System.out.println(ts2);
38     }
39 }
技術分享

不知大家有想過沒有, 為什麽Comparator<Person>這裏用的是父類Person, 而不是子類Student. 初學時很容易困惑, ? super E不應該E是子類才對麽? 其實, 實現接口時我們所設定的類型參數不是E, 而是?; E是在創建TreeSet時設定的. 如:

1 TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
2 TreeSet<Student> ts2 = new TreeSet(new comparatorTest());

這裏實例化的comparatorTest的泛型就是<Student super Student>和<Person super Student>(我這麽寫只是為了說明白). 在實現接口時使用:

1 // 這是錯誤的
2 class comparatorTest implements Comparator<Student> {...}

那麽上面的結果就成了: <Student super Person>和<Person super Person>, <Student super Person>顯然是錯誤的.

三、總結:

我們要記住這麽幾個使用原則, 有人將其稱為PECS(即"Producer Extends, Consumer Super", 網上翻譯為"生產者使用extends, 消費者使用super", 我覺得還是不翻譯的好). 也有的地方寫作"in out"原則, 總的來說就是:

  • in或者producer就是你要讀取出數據以供隨後使用(想象一下List的get), 這時使用extends關鍵字, 固定上邊界的通配符. 你可以將該對象當做一個只讀對象;
  • out或者consumer就是你要將已有的數據寫入對象(想象一下List的add), 這時使用super關鍵字, 固定下邊界的通配符. 你可以將該對象當做一個只能寫入的對象;
  • 當你希望in或producer的數據能夠使用Object類中的方法訪問時, 使用無邊界通配符;
  • 當你需要一個既能讀又能寫的對象時, 就不要使用通配符了.

泛型通配符詳解