Java泛型詳解(完整版)必看

Java泛型詳解(完整版)必看

泛型是什麼?

用來規定一個類、接口或方法所能接受的數據的類型. 就像在聲明方法時指定參數一樣, 我們在聲明一個類, 接口或方法時, 也可以指定其"類型參數", 也就是泛型.

泛型的好處

  1. 提高安全性: 將運行期的錯誤轉換到編譯期. 如果我們在對一個對象所賦的值不符合其泛型的規定, 就會編譯報錯.避免強轉: 比如我們在使用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等)之後,返回值聲明之前。方法參數列表,以及方法體中用到的所有泛型變量,都必須聲明。使用方式:
  • 太簡單,不說了。
Java泛型詳解(完整版)必看

泛型中的通配符

1. 作用:規定只允許某一部分類作為泛型;

2. 分類:

  1. 無邊界通配符(>):
  2. 無邊界的通配符的主要作用就是讓泛型能夠接受未知類型的數據。固定上邊界通配符( extends E>):
  3. 使用固定上邊界的通配符的泛型, 就能夠接受指定類及其子類類型的數據。
  4. 要聲明使用該類通配符, 採用 extends E>的形式, 這裡的E就是該泛型的上邊界. 注意: 這裡雖然用的是extends關鍵字, 卻不僅限於繼承了父類E的子類, 也可以代指顯現了接口E的類.固定下邊界通配符( super E>):
  5. 使用固定下邊界的通配符的泛型, 就能夠接受指定類及其父類類型的數據。
  6. 要聲明使用該類通配符, 採用 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")的時候,系統肯定會拋出異常,使得後面的代碼無法執行。

所以,編譯器其實是把運行時可能出現的異常放在編譯階段來檢查,提高了代碼的健壯性以及安全性。

Java泛型詳解(完整版)必看

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。

Java泛型詳解(完整版)必看

總結:

有人將上面的原則總結了一下,寫作"in out"原則, 歸納起來就是:

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


分享到:


相關文章: