一、基本概念:
在學習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類中的方法訪問時, 使用無邊界通配符;
- 當你需要一個既能讀又能寫的對象時, 就不要使用通配符了.
P.S. 泛型的通配符感覺好麻煩, 中英文資料研究了一整天搞了個大概其. 自己能力有限也不知表達清楚了沒有.
References:
[1] The Java™ Tutorials - Upper Bounded Wildcards - https://docs.oracle.com/javase/tutorial/java/generics/upperBounded.html
[2] The Java™ Tutorials - Lower Bounded Wildcards - https://docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html
[3] The Java™ Tutorials - Guidelines for Wildcard Use - https://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html
[4] stackoverflow - Difference between <? super T> and <? extends T> in Java - http://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java