背景 在沒有泛型前,一旦把一個對象丟進集合中,集合就會忘記對象的類型,把所有的對象都當成 Object 類型處理。當程式從集合中取出對象後,就需要進行強制類型轉換,這種轉換很容易引起 ClassCastException 異常。 定義 程式在創建集合時指定集合元素的類型。增加了泛型支持後的集合,可以 ...
背景
在沒有泛型前,一旦把一個對象丟進集合中,集合就會忘記對象的類型,把所有的對象都當成 Object 類型處理。當程式從集合中取出對象後,就需要進行強制類型轉換,這種轉換很容易引起 ClassCastException 異常。
定義
程式在創建集合時指定集合元素的類型。增加了泛型支持後的集合,可以記住集合中元素的類型,並可以在編譯時檢查集合中元素的類型,如果試圖向集合中添加不滿足類型要求的對象,編譯器就會報錯。
示例
集合使用泛型
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DiamondTest {
public static void main(String[] args) {
List<String> books = new ArrayList<>();
books.add("learn");
books.add("java");
books.forEach(book -> System.out.println(book.length()));
Map<String, List<String>> schoolsInfo = new HashMap<>();
List<String> schools = new ArrayList<>();
schools.add("i");
schools.add("love");
schoolsInfo.put("java", schools);
schoolsInfo.forEach((key, value) -> System.out.println(key + "--->" + value));
}
}
類、介面使用泛型
public class Apple<T> {
private T info;
public Apple() {}
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getinfo() {
return this.info;
}
public static void main(String[] args) {
Apple<String> a1 = new Apple<>("Apple");
System.out.println(a1.getinfo());
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getinfo());
}
}
類型通配符
需求分析
public void test(List<Object> c) {
for (int i = 0; i < c.size(); i++) {
System.out.prinln(c.get(i));
}
}
這個方法聲明沒有任何問題,但是調用該方法實際傳入的參數值,可能會出錯。考慮如下代碼:
List<String> strList = new ArrayList<>();
test(strList); // 編譯出錯,表明 List<String> 對象不能被當成 List<Object> 對象使用,也就是說 List<String> 並不是 List<Object> 的子類。
問題解決
為了表示各種泛型 List 的父類,可以使用類型通配符。List<?>
表示元素類型未知的 List。這個 ?
號被稱為通配符,它可以匹配任何類型。將上面的代碼,改為如下形式:
public void test(List<?> c) {
for (int i = 0; i < c.size(); i++) {
System.out.prinln(c.get(i));
}
}
現在傳入任何類型的 List,程式可以正常列印集合 c 中的元素。
不過集合中元素的類型會被當成 Object 類型對待。
類型通配符的上限
需求分析
當使用 List<?>
時,表明它可以是任何泛型 List 的父類。如果我們只希望它代表某一類泛型 List 的父類,java 提供了被限制的泛型通配符。
看如下代碼:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
@Override
public void draw(Canvas c) {
System.out.println("在畫布上" + c + "上畫一個圓");
}
}
public class Rectangle extends Shape {
@Override
public void draw(Canvas c) {
System.out.println("把一個矩形畫在畫布" + c + "上");
}
}
import java.util.List;
public class Canvas {
public void drawAll(List<Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
}
下麵代碼將引起編譯錯誤,因為 List<Circle>
並不是 List<Shape>
的子類型,所以不能把 List<Circle>
對象當成 List<Shape>
類用。
// 錯誤示範
List<Circle> circleList = new ArrayList<>();
Canvas c = new Canvas();
c.drawAll(circleList);
問題解決
方法一:通過類型通配符解決,即 List<?>
方式。
需要進行強制類型轉換,因為
List<?>
中元素預設為 Object 類型
import java.util.List;
public class Canvas {
public void drawAll(List<?> shapes) {
for (Object obj : shapes) {
Shape s = (Shape)obj // 但是需要進行強制類型轉換,因為前面提到過 List<?> 中元素預設為 Object 類型
s.draw(this);
}
}
}
方法二:使用被限制的泛型通配符
List<? extends Shape>
可以表示List<Circle>
和List<Rectangle>
的父類。只要 List 尖括弧里的類型是 Shape 的子類即可。
import java.util.List;
public class Canvas {
public void drawAll(List<? extends Shape> shapes) { // 使用被限制的泛型通配符
for (Shape s : shapes) {
s.draw(this);
}
}
}
形參類型上的應用
在定義類型形參時設定類型通配符上限。以此來表示傳遞給該類型形參的實際類型必須是該上限類型或者其子類。
public class Apple<T extends Number> {
T col;
public static void main(String[] args) {
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
Apple<String> as = new Apple<>(); // 編譯出錯,試圖將 String 類型傳給 T 形參,但是 String 不是 Number 的子類型
}
}
泛型方法
定義
泛型方法就是在聲明方法時定義一個或多個類型形參。多個類型參數之間用逗號隔開。
修飾符 <T, S> 返回值類型 方法名(形參列表) {方法體}
需求分析
泛型方法解決了什麼問題?
static void fromArrayToCollection(Object[] a, Collection<Object> c) {
for (Object o : a) {
c.add(o)
}
}
上面定義的方法沒有任何問題,關鍵在於方法中的 c 形參,它的數據類型是 Collection<Object>
。假設傳入的實際參數的類型是 Collection<String>
,因為 Collection<String>
並不是 Collection<Object>
的子類,所以這個方法的功能非常有限,它只能將 Object[] 數組的元素複製到元素為 Object 類型(Object 的子類不行)的 Collection 集合中。
如果使用通配符 Collection<?>
是否可行呢?顯然也不行,Collection 集合提供的的 add() 方法中有類型參數 E,而如果使用類型通配符,這樣程式無法確定 c 集合中的元素類型,所以無法正確調用 add 方法。
問題解決
泛型方法。
import java.util.ArrayList;
import java.util.Collection;
public class GenericMethodTest {
// 聲明一個泛型方法
static <T> void fromArryToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
public static void main(String[] args) {
Object [] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
fromArryToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
fromArryToCollection(sa, cs);
}
}
進一步改造,如下
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class RightTest {
static <T> void test(Collection<? extends T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}
public static void main(String[] args) {
List<String> as = new ArrayList<>();
List<Object> ao = new ArrayList<>();
test(as, ao);
}
}
泛型構造器
和泛型方法類似,Java 也允許在構造器簽名中聲明類型形參,這就產生了所謂的泛型構造器
public class Foo {
// 泛型構造器
public <T> Foo(T t) {
System.out.println(t);
}
}
public class GenericConstructor {
public static void main(String[] args) {
new <String> Foo("crazy");
new Foo("crazy"); // 與上面等價
new <Sting> Foo(12.3) // 出錯
}
}
類型通配符下限
需求分析
實現一個方法,將 src 集合里的元素複製到 dest 集合里,且返回最後一個被覆制的元素的功能。
因為 dest 集合可以保存 src 集合里的所有元素,所以 dest 集合元素的類型應該是 src 集合元素類型的父類。
為了表示兩個參數間的類型依賴,考慮同時使用之前介紹過的通配符、泛型參數來實現該方法,代碼如下:
public static <T> T copy(Collection<T> dest, Collection<? extends T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
// 下麵代碼會引起編譯錯誤
Integer last = copy(ln, li);
上面的代碼有一個問題,ln 的類型是 List<Number>
,那麼 T 的實際類型就是 Number,即返回值 last 類型是 Number 類型。但實際上最後一個複製元素的類型一定是 Integer。也就是說,程式在複製集合元素的過程中,丟失了 src 集合元素的類型。
問題解決
為瞭解決這個問題,引入通配符下限,<? super Type>
。表示必須是 Type 本身或者 Type 的父類。改寫後的完整代碼,如下:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class MyUtils {
// 使用通配符下限
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
// 此時可以準確知道最後一個被覆制的元素是 Integer 類型,而不是籠統的 Number 類型
Integer last = copy(ln, li);
System.out.println(last);
System.out.println(ln);
}
}
泛型擦除
泛型基本上都是在編譯器這個層次來實現的。在生成的 Java 位元組代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數, 會被編譯器在編譯的時候去掉。這個過程就稱為泛型擦除。如在代碼中定義的 List<Object>
和 List<String>
等類型, 在編譯之後都會變成 List, JVM 看到的只是 List, 泛型附加的類型信息對 JVM 來說是不可見的。
歡迎關註我的公眾號