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, byte, char。不能直接用於對象的比較。為此需要用接口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等均為對應的子類。