泛型是什麼?
用來規定一個類、接口或方法所能接受的數據的類型. 就像在聲明方法時指定參數一樣, 我們在聲明一個類, 接口或方法時, 也可以指定其"類型參數", 也就是泛型.
泛型的好處
- 提高安全性: 將運行期的錯誤轉換到編譯期. 如果我們在對一個對象所賦的值不符合其泛型的規定, 就會編譯報錯.避免強轉: 比如我們在使用List時, 如果我們不使用泛型, 當從List中取出元素時, 其類型會是默認的Object, 我們必須將其向下轉型為String才能使用。比如:
List l = new ArrayList(); l.add("abc"); String s = (String) l.get(0);
而使用泛型,就可以保證存入和取出的都是String類型, 不必在進行cast了。比如:
List l = new ArrayList<>(); l.add("abc"); String s = l.get(0);
泛型的使用
1. 定義類/接口:
public class Test { private T obj; public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } }
- 使用方式:
- List l = new ArrayList<>( );重點說明:變量類型中的泛型,和實例類型中的泛型,必須保證相同(不支持繼承關係)。既然有了這個規定, 因此在JDK1.7時就推出了一個新特性叫菱形泛型(The Diamond), 就是說後面的泛型可以省略直接寫成<>, 反正前後一致。
2. 定義方法:
public void print(Q q) { System.out.println(q); }
- 說明:泛型的聲明,必須在方法的修飾符(public,static,final,abstract等)之後,返回值聲明之前。方法參數列表,以及方法體中用到的所有泛型變量,都必須聲明。使用方式:
- 太簡單,不說了。
泛型中的通配符
1. 作用:規定只允許某一部分類作為泛型;
2. 分類:
- 無邊界通配符(>):
- 無邊界的通配符的主要作用就是讓泛型能夠接受未知類型的數據。固定上邊界通配符( extends E>):
- 使用固定上邊界的通配符的泛型, 就能夠接受指定類及其子類類型的數據。
- 要聲明使用該類通配符, 採用 extends E>的形式, 這裡的E就是該泛型的上邊界. 注意: 這裡雖然用的是extends關鍵字, 卻不僅限於繼承了父類E的子類, 也可以代指顯現了接口E的類.固定下邊界通配符( super E>):
- 使用固定下邊界的通配符的泛型, 就能夠接受指定類及其父類類型的數據。
- 要聲明使用該類通配符, 採用 super E>的形式, 這裡的E就是該泛型的下邊界.
注意: 你可以為一個泛型指定上邊界或下邊界, 但是不能同時指定上下邊界。
3. 使用方法:
3.1 無邊界通配符:
public static void printList(List> list) { for (Object o : list) { System.out.println(o); } } public static void main(String[] args) { List l1 = new ArrayList<>(); l1.add("aa"); l1.add("bb"); l1.add("cc"); printList(l1); List l2 = new ArrayList<>(); l2.add(11); l2.add(22); l2.add(33); printList(l2);
注意:這裡的printList方法不能寫成public static void printList(List list)的形式。原因在上文提到過,變量類型中的泛型,和實例類型中的泛型,必須保證相同。兩者之間不支持繼承關係。
- 重點說明:我們不能對List>使用add,get以及List擁有的其他方法。
- 原因是,我們不確定該List的類型, 也就不知道add,或者get方法的參數類型。
- 但是也有特例。
- 請看下面代碼:
public static void addTest(List> list) { Object o = new Object(); // list.add(o); // 編譯報錯 // list.add(1); // 編譯報錯 // list.add("ABC"); // 編譯報錯 list.add(null); // 特例 // String s = list.get(0); // 編譯報錯 // Integer i = list.get(1); // 編譯報錯 Object o = list.get(2); // 特例 } 這個地方有點不好理解。
我們可以假設:使用這些方法編譯不報錯。
以上面的代碼為例,並且取消上面的註釋。
由於參數的泛型不確定,調用者可能會傳List,也可能傳List。
當調用者傳過來的參數是List,執行到list.add(o)以及list.("ABC")的時候,系統肯定會拋出異常,使得後面的代碼無法執行。
所以,編譯器其實是把運行時可能出現的異常放在編譯階段來檢查,提高了代碼的健壯性以及安全性。
2. 固定上邊界通配符:
public static double sumOfList(List extends Number> list) { double s = 0.0; for (Number n : list) { // 注意這裡得到的n是其上邊界類型的, 也就是Number,需要將其轉換為double. s += n.doubleValue(); } return s; } public static void main(String[] args) { List list1 = Arrays.asList(1, 2, 3, 4); System.out.println(sumOfList(list1)); List list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4); System.out.println(sumOfList(list2)); }
- 重點說明:我們不能對List extends E>使用add方法。
- 原因是,我們不確定該List的類型, 也就不知道add方法的參數類型。
- 但是也有特例。
- 請看下面代碼:
public static void addTest2(List extends Number> l) { // l.add(1); // 編譯報錯 // l.add(1.1); // 編譯報錯 l.add(null); Number number = l.get(1); // 正常 }
目的跟第一種通配符類似,就是編譯器其實是把運行時可能出現的異常放在編譯階段來檢查。
但是,我們可以保證不管參數是什麼泛型,裡面的元素肯定是Number或者其子類,所以,從List中獲取一個Number元素的get()方法是允許的。
3. 固定下邊界通配符:
public static void addNumbers(List super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } public static void main(String[] args) { List list1 = new ArrayList<>(); addNumbers(list1); System.out.println(list1); List list2 = new ArrayList<>(); addNumbers(list2); System.out.println(list2); List list3 = new ArrayList<>(); // addNumbers(list3); // 編譯報錯 }
- 重點說明:我們不能對List super E>使用get方法。
- 原因是,我們不確定該List的類型, 也就不知道get方法的參數類型。
- 但是也有特例。
- 請看下面代碼:
public static void getTest2(List super Integer> list) { // Integer i = list.get(0); //編譯報錯 Object o = list.get(1); }
目的跟第一種通配符類似,就是編譯器其實是把運行時可能出現的異常放在編譯階段來檢查。
但是,我們可以保證不管參數是什麼泛型,裡面的元素肯定是Integer,所以,從List中add一個Integer元素的add()方法是允許的。
- 典型使用場景:
- 使用 super E>有個常見的場景就是Comparator。
- TreeSet有這麼一個構造方法:TreeSet(Comparator super E> comparator) ,就是使用Comparator來創建TreeSet。
- 請看下面的代碼:
import java.util.Comparator; import java.util.TreeSet; class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } class Student extends Person { public Student(String name, int age) { super(name, age); } } class comparatorTest1 implements Comparator { @Override public int compare(Person s1, Person s2) { int num = s1.getAge() - s2.getAge(); return num == 0 ? s1.getName().compareTo(s2.getName()) : num; } } public class Test { public static void main(String[] args) { TreeSet ts2 = new TreeSet<>(new comparatorTest1()); ts2.add(new Student("Susan", 23)); ts2.add(new Student("Rose", 27)); ts2.add(new Student("Jane", 19)); for (Student stu : ts2) { System.out.println(stu.getName() + ":" + stu.getAge()); } } }
注意:通過查看TreeSet源碼得知,構造方法TreeSet(Comparator super E> comparator)中的E,來源於泛型類 TreeSet,在這裡就是變量ts2的類型TreeSet 中的Student 。因為泛型限定是 super E>,即 super Student>,所以Comparator的泛型必須是Student的父類,即Person。
總結:
有人將上面的原則總結了一下,寫作"in out"原則, 歸納起來就是:
in或者producer就是你要讀取出數據以供隨後使用(想象一下List的get), 這時使用extends關鍵字, 固定上邊界的通配符. 你可以將該對象當做一個只讀對象;out或者consumer就是你要將已有的數據寫入對象(想象一下List的add), 這時使用super關鍵字, 固定下邊界的通配符. 你可以將該對象當做一個只能寫入的對象;當你希望in或producer的數據能夠使用Object類中的方法訪問時, 使用無邊界通配符;當你需要一個既能讀又能寫的對象時, 就不要使用通配符了.