泛型詳解


 1. 為什么使用泛型(Why Use Generics?)

  • 更強的編譯時類型檢查

  Java編譯器對泛型代碼應用強類型檢查,如果代碼違反了類型安全,將會提示錯誤。解決編譯時錯誤比運行時錯誤更容易,后者更難發現。

  • 消除類型轉換

  如下代碼未使用泛型,需要類型轉換:

1 List list = new ArrayList();
2 list.add("hello");
3 String s = (String) list.get(0);

當用泛型重寫后,不再需要類型轉換

1 List<String> list = new ArrayList<String>();
2 list.add("hello");
3 String s = list.get(0);   // no cast
  • 開發者可實現泛型機制

  通過使用泛型,開發者可以使用泛型機制,定制化不同類型的集合,同時也是類型安全和更容易閱讀。

1 public class Test {
2     public static void main(String[] args) {
3         List<String> list = new ArrayList<>();
4         String val = "str";
5         list.add(val);
6         String str = list.get(0);
7     }
8 }

反編譯Test.class文件可以看到,返回對象增加了類型cast

1 public class Test {
2   public static void main(String[] args) {
3     List<String> list = new ArrayList<String>();
4     String val = "str";
5     list.add(val);
6     String str = (String)list.get(0);
7   }
8 }

 

2. 泛型(Generic Types)

2.1 簡單類(A Simple Box Class)

 1 public class Box {
 2     private Object object;
 3 
 4     public void set(Object object) {
 5         this.object = object;
 6     }
 7 
 8     public Object get() {
 9         return object;
10     }
11 }

2.2 泛型類(A Generic Version of the Box Class)

泛型類的定義格式,T1,T2...可以是具體類型,也是是參數化類型

1 class name<T1, T2, ..., Tn> { /* ... */ }

 例如:

 1 /**
 2  * Generic version of the Box class.
 3  * @param <T> the type of the value being boxed
 4  */
 5 public class Box<T> {
 6     // T stands for "Type"
 7     private T t;
 8 
 9     public void set(T t) { this.t = t; }
10     public T get() { return t; }
11 }

類型參數命名管理(Type Parameter Naming Conventions)

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

 參數化的類型(Parameterized Types)

1 OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

2.3 原始類型(Raw Types)

參數化類型

1 Box<Integer> intBox = new Box<>();

原始類型

1 Box rawBox = new Box();

允許將參數化類型指向原始類型

1 Box<String> stringBox = new Box<>();
2 Box rawBox = stringBox;               // OK

將原始類型指向參數化類型,會引起警告

1 Box rawBox = new Box();           // rawBox is a raw type of Box<T>
2 Box<Integer> intBox = rawBox;     // warning: unchecked conversion

原始類型調用泛型方法,也會引起警告

1 Box<String> stringBox = new Box<>();
2 Box rawBox = stringBox;
3 rawBox.set(8);  // warning: unchecked invocation to set(T)

 

3. 泛型方法

可以是靜態方法和非靜態方法,也可以使泛型構造函數

3.1 靜態泛型方法:

1 public class Util {
2     public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
3         return p1.getKey().equals(p2.getKey()) &&
4                p1.getValue().equals(p2.getValue());
5     }
6 }

3.2 泛型構造函數:

 1 public class Pair<K, V> {
 2 
 3     private K key;
 4     private V value;
 5 
 6     public Pair(K key, V value) {
 7         this.key = key;
 8         this.value = value;
 9     }
10 
11     public void setKey(K key) { this.key = key; }
12     public void setValue(V value) { this.value = value; }
13     public K getKey()   { return key; }
14     public V getValue() { return value; }
15 }

完整的方法調用如下:

1 Pair<Integer, String> p1 = new Pair<>(1, "apple");
2 Pair<Integer, String> p2 = new Pair<>(2, "pear");
3 boolean same = Util.<Integer, String>compare(p1, p2);

同樣支持類型推斷,可簡寫如下

1 boolean same = Util.compare(p1, p2);

Java API 中典型的泛型方法有

1 public static <T> List<T> asList(T... a) {
2     return new ArrayList<>(a);
3 }
1 List<Integer> li = Arrays.asList(1, 2, 3);

 泛型的使用限制

  • 不能使用泛型的形參創建對象

實例化可傳入Class<T>類型

1     static <T> void testGeneric(Class<T> clazz) throws Exception {
2         T t = clazz.newInstance();
3     }
  • 不能在靜態環境中使用泛型類的類型參數

  • 不能初始化一個泛型數組,但是可以聲明泛型數組

 

 4. 有界類型參數(Bounded Type Parameters)

extends關鍵字限定上界

 4.1 有界方法

 1 public class Box<T> {
 2 
 3     private T t;          
 4 
 5     public void set(T t) {
 6         this.t = t;
 7     }
 8 
 9     public T get() {
10         return t;
11     }
12 
13     public <U extends Number> void inspect(U u){
14         System.out.println("T: " + t.getClass().getName());
15         System.out.println("U: " + u.getClass().getName());
16     }
17 
18     public static void main(String[] args) {
19         Box<Integer> integerBox = new Box<Integer>();
20         integerBox.set(new Integer(10));
21         integerBox.inspect("some text"); // error: this is still String!
22     }
23 }

 4.2 有界類

 1 public class NaturalNumber<T extends Integer> {
 2 
 3     private T n;
 4 
 5     public NaturalNumber(T n)  { this.n = n; }
 6 
 7     public boolean isEven() {
 8         return n.intValue() % 2 == 0;
 9     }
10 
11     // ...
12 }

4.3 多個限定參數

限定方式如下:

<T extends B1 & B2 & B3>

限定類型參數中,有類,需要將類放在第一位置,否則編譯錯誤

1 Class A { /* ... */ }
2 interface B { /* ... */ }
3 interface C { /* ... */ }
4 
5 class D <T extends A & B & C> { /* ... */ }

4.4 泛型方法和有界類型參數(Generic Methods and Bounded Type Parameters)

有界類型參數是實現泛型機制的關鍵。如下方法,對數組T[]中的數字元素計數,其中數字元素需要大於指定元素elem。

1 public static <T> int countGreaterThan(T[] anArray, T elem) {
2     int count = 0;
3     for (T e : anArray)
4         if (e > elem)  // compiler error
5             ++count;
6     return count;
7 }

因為大於操作符(>)只能應用於原始類型如,short, int, double, long, float, bytechar。不能直接用於對象的比較。為此需要用接口Comparable<T>,來限定類型參數。

1 public interface Comparable<T> {
2     public int compareTo(T o);
3 }

最終代碼如下:

1 public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
2     int count = 0;
3     for (T e : anArray)
4         if (e.compareTo(elem) > 0)
5             ++count;
6     return count;
7 }

5. 泛型、繼承和子類型(Generics, Inheritance, and Subtypes

如果類型兼容,可以將一個類型的對象指向另一個類型的對象。例如,Object是Integer的父類,可以將Integer指向Object。

1 Object someObject = new Object();
2 Integer someInteger = new Integer(10);
3 someObject = someInteger;   // OK

對於泛型也一樣。可以執行泛型類型調用,傳遞Number給類型參數,后續兼容Number類型的都被允許調用。

1 Box<Number> box = new Box<Number>();
2 box.add(new Integer(10));   // OK
3 box.add(new Double(10.1));  // OK

泛型類和子類型化(Generic Classes and Subtyping)

可以繼承一個泛型類或者實現一個泛型接口。一個類或接口的類型參數和另外一個的關系,通過extends和implems語句來實現。

下面使用Collections類為例。 ArrayList<E> implements List<E>, and List<E> extends Collection<E>. 因此 ArrayList<String> 是 List<String>的子類型, 也是 Collection<String>的子類型。只要不改變類型參數,類型指定了子類關系。  

Collection的層級關系樣例

假定,我們要定義自己的list接口,PayloadList,加入了另外一個泛型參數P,聲明如下:

1 interface PayloadList<E,P> extends List<E> {
2   void setPayload(int index, P val);
3   ...
4 }

如下的PayloadList參數化實例都是List<String>的子類型

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

 

PayloadList 層級樣例

6. 類型推斷

 類型推斷和泛型方法

 1 public class BoxDemo {
 2 
 3   public static <U> void addBox(U u,  java.util.List<Box<U>> boxes) {
 4     Box<U> box = new Box<>();
 5     box.set(u);
 6     boxes.add(box);
 7   }
 8 
 9   public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
10     int counter = 0;
11     for (Box<U> box: boxes) {
12       U boxContents = box.get();
13       System.out.println("Box #" + counter + " contains [" + boxContents.toString() + "]");
14       counter++;
15     }
16   }
17 
18   public static void main(String[] args) {
19     java.util.ArrayList<Box<Integer>> listOfIntegerBoxes = new java.util.ArrayList<>();
20     BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
21     BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
22     BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
23     BoxDemo.outputBoxes(listOfIntegerBoxes);
24   }
25 }

輸出如下:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

 7. 通配符(Wildcards)

泛型代碼中,問號(?)稱為通配符,代表未知類型。通配符用於以下場景:作為參數、字段和局部變量的類型;有時也可作為返回類型。通配符不能作為調用泛型方法、實例化泛型類和子類型的類型參數。

上界通配符

上界通配符可以縮小對變量的限制

1 public static void process(List<? extends Foo> list) {
2     for (Foo elem : list) {
3         // ...
4     }
5 }

 

1 public static double sumOfList(List<? extends Number> list) {
2     double s = 0.0;
3     for (Number n : list)
4         s += n.doubleValue();
5     return s;
6 }

Integer可以調用上述方法

1 List<Integer> li = Arrays.asList(1, 2, 3);
2 System.out.println("sum = " + sumOfList(li));

Double可以調用上述方法

1 List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
2 System.out.println("sum = " + sumOfList(ld));

無界通配符

問號(?)用來表示無界通配類型,如List<?>,叫做未知類型列表。主要使用場景有兩個:

  • 如果您正在編寫一個可以使用Object類中提供的功能來實現的方法。
  • 當代碼使用不依賴於類型參數的泛型類中的方法時。例如,List.size() 或者 List.clear()事實上,Class<?>經常使用,是因為Class<T>中的大多數方法都不依賴於T。

考慮如下方法

1 public static void printList(List<Object> list) {
2     for (Object elem : list)
3         System.out.println(elem + " ");
4     System.out.println();
5 }

上述方法不能接收List<Integer>, List<String>, List<Double>作為參數,因為他們不是List<Object>的子類型。泛型方法可寫成如下形式:

1 public static void printList(List<?> list) {
2     for (Object elem: list)
3         System.out.print(elem + " ");
4     System.out.println();
5 }

可以進行如下調用

1 List<Integer> li = Arrays.asList(1, 2, 3);
2 List<String>  ls = Arrays.asList("one", "two", "three");
3 printList(li);
4 printList(ls);

List<Object> 和 List<?>的區別

可以將Object或者他的子類插入List<Object>。但是不能將null插入List<?>。

思考:

當實際類型參數為?。它代表某種未知的類型。我們傳遞添加的任何參數都必須是這種未知類型的子類型。因為我們不知道那是什么類型,所以我們不能傳遞任何東西。唯一的例外是null,它是每種類型的成員。

思考2:

1 List <String> l1 = new ArrayList<String>();
2 List<Integer> l2 = new ArrayList<Integer>();
3 System.out.println(l1.getClass() == l2.getClass());

打印:true

下界通配符

1 public static void addNumbers(List<? super Integer> list) {
2     for (int i = 1; i <= 10; i++) {
3         list.add(i);
4     }
5 }

通配符和子類型

有如下普通類

1 class A { /* ... */ }
2 class B extends A { /* ... */ }

可以實例化如下:

1 B b = new B();
2 A a = b;

如下代碼編譯報錯

1 List<B> lb = new ArrayList<>();
2 List<A> la = lb;   // compile-time error

 

公共父類為List<?>

盡管Integer是Number的子類,List<Integer>卻不是List<Number>的子類。事實上,它們沒有任何關系。List<Integer>和List<Number>的公共父類為List<?>。

1 List<? extends Integer> intList = new ArrayList<>();
2 List<? extends Number>  numList = intList;  // OK. List<? extends Integer>是List<? extends Number>的子類型

因為Integer是Number的子類,numList和intList存在一定的關系

幾個泛型List聲明的層次結構。

 8. 類型擦除

泛型被引入java語言,以便在編譯時提供更嚴格的類型檢查,並支持泛型編程。為了實現泛型,java編譯器將類型擦除應用於:

  • 如果類型參數是無界的,則用它們的邊界或Object替換泛型類型中的所有類型參數。因此,生成的字節碼只包含普通的類、接口和方法。
  • 如有必要,插入類型轉換以保持類型安全。
  • 生成橋接方法以保留擴展泛型類型中的多態性。

 類型擦除確保不會為參數化類型創建新的類;因此,泛型不會產生運行時開銷。

8.1 泛型類型的擦除(Erasure of Generic Types)

在類型擦除過程中,Java編譯器擦除所有類型參數,如果類型參數是有界的,則用第一個邊界替換每個類型參數,如果類型參數是無界的,則用Object替換每個類型參數。

考慮以下表示單鏈接列表中節點的泛型類:

 1 public class Node<T> {
 2 
 3     private T data;
 4     private Node<T> next;
 5 
 6     public Node(T data, Node<T> next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public T getData() { return data; }
12     // ...
13 }

因為類型參數T是無界的,Java編譯器用Object替換它:

 1 public class Node {
 2 
 3     private Object data;
 4     private Node next;
 5 
 6     public Node(Object data, Node next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public Object getData() { return data; }
12     // ...
13 }

在以下示例中,泛型類Node使用有界類型參數:

 1 public class Node<T extends Comparable<T>> {
 2 
 3     private T data;
 4     private Node<T> next;
 5 
 6     public Node(T data, Node<T> next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public T getData() { return data; }
12     // ...
13 }

Java編譯器用第一個綁定類Comparable替換綁定類型參數T,類似於:

 1 public class Node {
 2 
 3     private Comparable data;
 4     private Node next;
 5 
 6     public Node(Comparable data, Node next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public Comparable getData() { return data; }
12     // ...
13 }

8.2 泛型方法的擦除(Erasure of Generic Methods)

Java編譯器還會擦除泛型方法參數中的類型參數。考慮以下泛型方法:

1 // Counts the number of occurrences of elem in anArray.
2 //
3 public static <T> int count(T[] anArray, T elem) {
4     int cnt = 0;
5     for (T e : anArray)
6         if (e.equals(elem))
7             ++cnt;
8         return cnt;
9 }

因為T是無界的,Java編譯器用Object替換它:

1 public static int count(Object[] anArray, Object elem) {
2     int cnt = 0;
3     for (Object e : anArray)
4         if (e.equals(elem))
5             ++cnt;
6         return cnt;
7 }

假設定義了以下類:

1 class Shape { /* ... */ }
2 class Circle extends Shape { /* ... */ }
3 class Rectangle extends Shape { /* ... */ }

您可以編寫一個泛型方法來繪制不同的形狀:

1 public static <T extends Shape> void draw(T shape) { /* ... */ }

Java編譯器用Shape替換T:

1 public static void draw(Shape shape) { /* ... */ }

8.3 類型擦除和橋接方法的影響(Effects of Type Erasure and Bridge Methods)

有時類型擦除會導致您可能沒有預料到的情況。以下示例顯示了這是如何發生的。這個例子(在Bridge Methods中描述)展示了編譯器有時如何創建一個合成方法,稱為bridge方法,作為類型擦除過程的一部分。

給定以下兩個類:

 1 public class Node<T> {
 2 
 3     public T data;
 4 
 5     public Node(T data) { this.data = data; }
 6 
 7     public void setData(T data) {
 8         System.out.println("Node.setData");
 9         this.data = data;
10     }
11 }
12 
13 public class MyNode extends Node<Integer> {
14     public MyNode(Integer data) { super(data); }
15 
16     public void setData(Integer data) {
17         System.out.println("MyNode.setData");
18         super.setData(data);
19     }
20 }

考慮以下代碼:

1 MyNode mn = new MyNode(5);
2 Node n = mn;            // A raw type - 編譯拋出未檢查警告
3 n.setData("Hello");     
4 Integer x = mn.data;    // 拋出異常ClassCastException

類型擦除后,該代碼變為:

1 MyNode mn = new MyNode(5);
2 Node n = (MyNode)mn;         // A raw type - 編譯拋出為檢查警告
3 n.setData("Hello");
4 Integer x = (String)mn.data; // 拋出異常ClassCastException

下面是代碼執行時發生的情況:

  • n.setData("Hello");導致方法setdata(object)在MyNode類的對象上執行。(MyNode從Node繼承了setData(Object)。)
  • 在setData(Object)的主體中,由n引用的對象的字段data被分配給一個String。
  • 通過mn引用的同一對象的字段data可以被訪問,並且預期是整數(因為mn是MyNode類型的,是Node<Integer>)。
  • 試圖將String分配給Integer會導致Java編譯器在分配時插入的強制轉換產生ClassCastException。

橋接方法(Bridge Methods)

當編譯繼承參數化類或實現參數化接口的類或接口時,編譯器可能需要創建一個合成方法,稱為橋接方法,作為類型擦除過程的一部分。您通常不需要擔心橋接方法,但是如果堆棧跟蹤中出現橋接方法,您可能會感到困惑。

類型擦除后,Node和MyNode類變為:

 1 public class Node {
 2 
 3     public Object data;
 4 
 5     public Node(Object data) { this.data = data; }
 6 
 7     public void setData(Object data) {
 8         System.out.println("Node.setData");
 9         this.data = data;
10     }
11 }
12 
13 public class MyNode extends Node {
14 
15     public MyNode(Integer data) { super(data); }
16 
17     public void setData(Integer data) {
18         System.out.println("MyNode.setData");
19         super.setData(data);
20     }
21 }

類型擦除后,方法簽名不匹配。Node的方法變成setData(Object),MyNode方法變成setData(Integer)。因此,MyNode setData方法不會重寫Node setData方法。

為了解決這個問題並在類型擦除后保持泛型類型的多態性,Java編譯器生成一個橋接方法來確保子類型按預期工作。對於MyNode類,編譯器為setdata生成以下橋接方法:

 1 class MyNode extends Node {
 2 
 3     // Bridge method generated by the compiler
 4     //
 5     public void setData(Object data) {
 6         setData((Integer) data);
 7     }
 8 
 9     public void setData(Integer data) {
10         System.out.println("MyNode.setData");
11         super.setData(data);
12     }
13 
14     // ...
15 }

如您所見,橋接方法在類型擦除后與類Node的setData方法具有相同的方法簽名,委托給原始的setData方法。

9. 泛型的高級用法

9.1 泛型類父類為子類定義公共方法

父類:

1 public class Parent<Sub extends Parent<Sub>> {
2 
3     public Sub get() {
4         return (Sub) this;
5     }
6 }

子類:

1 public class Children extends Parent<Children> {
2 
3 }

測試:

1 public class Test {
2     public static void main(String[] args) {
3         Children children = new Children();
4         String name = children.get().getClass().getName(); // Children
5     }
6 }

參考代碼如下:

MasterNodeRequest的子類

 

9.2 參數化類型作為泛型,編譯檢查強類型校驗

Action:

紅圈中的Request和Response為有界參數化類型。

GenericAction<Request, Response>中的泛型Request和Response取自有界參數化類型。

ActionRequestBuilder:

同理,紅圈中的Request和Response為有界參數化類型,參數化類型RequestBuilder為限定為自身的子類。

GenericAction:

GenericAction的參數化類型為ActionRequest和ActionResponse的子類。Action繼承GenericAction時,泛型符合限定條件。

ClusterAllocationExplainAction中的ClusterAllocationExplainRequest等均為對應的子類。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM