瘋狂Java講義-泛型

泛型

本章思惟導圖

泛型

泛型入門

Java集合有個缺點——把一個對象「丟進」集合裏以後,集合就會「忘記」這個對象的數據類型,當再次取出該對象時,該對象的編譯類型就變成了Object類型(其運行時類型沒變)。java

編譯時不檢查類型的異常

下面代碼將會看到編譯時不檢查類型所致使的異常。web

import java.util.ArrayList;
import java.util.List;

public class ListErr {

	public static void main(String[] args) {
		// 建立一個只想保存字符串的List集合
		List strList = new ArrayList();
		strList.add("十年寒窗無人問");
		strList.add("縱使相逢應不識");
		// "不當心"把一個Integer對象"丟進"了集合
		strList.add(5);
		strList.forEach(str -> System.out.println(((String) str).length()));
	}

}

上面程序建立了一個List集合,並且只但願該List集合保存字符串對象——但程序不能進行任何限制,上面程序將引起ClassCastException異常。數組

使用泛型

從Java5之後,Java引入了參數化類型(parameterized type)的概念,容許程序在建立集合時指定集合元素的類型。Java參數化類型被稱爲泛型(Generic)。安全

建立這種特殊集合的方法是:在集合接口 、類後增長尖括號,尖括號裏放一個數據類型,即代表這個集合接口、集合類只能保存特定類型的對象。從而使集合自動記住全部集合元素的數據類型,從而無須對集合元素進行強制類型轉換。框架

Java9加強的」菱形「語法

在Java7之前,若是使用帶泛型的接口、類定義變量,那麼調用構造器建立對象時構造器的後面也必須帶泛型,這顯得有些多餘了。例如以下兩條語句。svg

List<String> strList = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();

上面兩條語句中的構造器後面的尖括號部分徹底是多餘的,在Java7之前這是必需的,不能省略。從Java7開始,Java容許在構造器後不需帶完整的泛型信息,只要給出一對尖括號(<>)便可,Java能夠推斷尖括號裏應該是什麼泛型信息。上面兩條代碼能夠改寫爲以下形式。學習

List<String> strList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();

Java9再次加強了」菱形「語法,它甚至容許在建立匿名內部類時使用菱形語法,Java可根據上下文來推斷匿名內部類中泛型的類型。下面代碼示範了在匿名內部類中使用菱形語法。this

interface Foo<T> {
	void test(T t);
}

public class AnnoymousDiamond {

	public static void main(String[] args) {
		// 指定Foo類中泛型爲String
		Foo<String> f = new Foo<>() {
			// test()方法的參數類型爲String
			public void test(String t) {
				System.out.println("test方法的t參數爲: " + t);
			}
		};
		// 使用泛型通配符,此時至關於通配符的上限爲Object
		Foo<?> fo = new Foo<>() {
			// tes()方法的參數類型爲Object
			public void test(Object t) {
				System.out.println("test方法的Object參數爲: " + t);
			}

		};
		// 使用泛型通配符,通配符的上限爲Number
		Foo<? extends Number> fn = new Foo<>() {
			// 此時test()方法的參數類型爲Number
			public void test(Number t) {
				System.out.println("test方法的Number參數爲: " + t);
			}
		};
	}

}

上面的代碼定義了帶泛型聲明的接口。spa

深刻泛型

所謂泛型,就是容許在定義類、接口、方法時使用類型的形參,這個類型形參(或叫泛型)將在聲明變量、建立對象、調用方法時動態地指定(即傳入實際的類型參數,也可稱爲類型實參)。Java5改寫了集合框架中的所有接口,爲這些接口、類增長了泛型支持,從而能夠在聲明集合變量、建立集合對象時傳入類型實參。設計

定義泛型接口、類

下面是Java5改寫後List接口、Iterator接口、Map的代碼片斷。

// 定義接口時制定了一個泛型形參,該形參名爲E
public interface List<E> {
    // 在該接口裏,E可做爲類型使用
    // 下面方法可使用E做爲參數類型
    void add(E x);
    Iterator<E> iterator();
    ...
}
// 定義接口時指定了一個泛型形參,該形參名爲E
public interface Iterator<E> {
    // 在該接口裏E徹底可做爲類型使用
    E next();
    boolean hasNext();
    ...
}
// 定義接口時指定了一個泛型形參,該形參名爲E
public interface Map<K, V> {
    // 在該接口裏K、V徹底可做爲類型使用
    Set<K, V> keySet();
    V put(K key, V value);
    ...
}

容許在定義接口、類時聲明泛型形參,泛型形參在整個接口、類體內可當成類型使用,幾乎全部可以使用普通類型的地方均可以使用這種泛型形參。

能夠爲任何類、接口增長泛型聲明,並非只有集合類纔可使用泛型聲明。

從泛型類派生子類

當建立了帶泛型聲明的接口、父類以後,能夠爲該接口建立實現類,或從該父類派生子類,當使用這些接口、父類時不能再包含泛型形參。

方法中的形參表明變量、常量、表達式等數據。定義方法時能夠聲明數據形參,調用方法時必須爲這些數據形參傳入實際的數據;與此相似的是,定義類、接口、方法時能夠聲明泛型形參,使用類、接口、方法時應該爲泛型形參傳入實際的類型。

若是想從Apple類派生一個子類,以下代碼。

// 使用Apple類時,爲T形參傳入String類型
public class A extends Apple<String>

調用方法時必須爲全部的數據形參傳入參數值,與調用方法不一樣的是,使用類、接口時也能夠不爲泛型形參傳入實際的類型參數,下面代碼也是正確的。

// 使用Apple類時,沒有爲T形參傳入實際的類型參數
public class A extends Apple

像這種使用Apple類時省略泛型的形式被稱爲原始類型(raw type)。

若是使用原始類型的形式繼承父類,Java編譯器可能發出警告:使用了未經檢查或不安全的操做——這就是泛型檢查的警告,若是但願看到該警告提示的更詳細信息,則能夠經過爲javac命令增長-Xlint:unchecked選項來實現。

並不存在泛型類

當一個類使用了泛型,系統並無爲該類生成新的class文件,並且也不會將該類當成新類來處理。無論泛型的實際類型參數是什麼,它們在運行時總有一樣的類。

無論泛型形參傳入哪種類型實參,對於Java來講,它們依然被當成同一個類處理,在內存中也只佔用一塊,所以在靜態方法、靜態初始化塊或者靜態變量的聲明和初始化中不容許使用泛型形參。下面程序演示了這種錯誤。

public class R<T> {
    // 下面代碼錯誤,不能在靜態變量聲明中使用泛型形參
    static T info;
    T age;
    public void foo(T msg) {}
    // 下面代碼錯誤,不能在靜態方法聲明中使用泛型形參
    public static void bar(T msg) {}

因爲系統中並不會真正生成泛型類,因此instanceof運算符後面不能使用泛型類。下面代碼是錯誤的。

Collection<String> cs = new ArrayList<>();
// 下面代碼編譯時引起錯誤:instanceof運算符後不能使用泛型
if (cs instanceof ArrayList<String>) {
    ...
}

類型通配符

當使用一個泛型類時(包括聲明變量和建立對象兩種狀況),都應該爲這個泛型類傳入一個類型實參。若是沒有傳入類型實際參數,編譯器就會提出泛型警告。

若是Foo是Bar的一個子類型(子類或者子接口),而G是具備泛型聲明的類或接口,G並非G的子類型。

在數組中,程序能夠直接把一個Integer[]數組賦給一個Number[]變量。若是試圖把一個Double對象保存到該Number[]數組中,編譯能夠經過,但在運行時拋出ArrayStoreException異常。

Java在泛型設計時進行了改進,再也不容許把List對象賦值給List變量。

Java泛型設計原則是,只要代碼在編譯時沒有出現警告,就不會遇到運行時ClassCastException異常。

數組和泛型有所不一樣,假設Foo是Bar的一個子類型(子類或者子接口),那麼Foo[]依然是Bar[]的子類型;但G不是G的子類型。Foo[]自動向上轉型爲Bar[]的方式被稱爲型變。Java數組支持型變,但Java集合並不支持型變。

使用類型通配符

爲了表示各類泛型List的父類,可使用類型通配符,類型通配符是一個問號(?),將一個問號做爲類型實參傳給List集合,寫做:List<?>(意思是元素類型未知的List)。這個問號(?)被稱爲通配符,它的元素類型能夠匹配任何類型。例如代碼。

public void test(List<?> c) {
    for (int i = 0; i < c.size(); i++) {
        System.out.println(c.get(i));
    }
}

如今使用任何類型的List來調用它,程序依然能夠訪問集合c中的元素,其類型是Object,這永遠是安全的,由於無論List的真實類型是什麼,它包含的都是Object。

但這種帶通配符的List僅表示它是各類泛型List的父類,並不能把元素加入到其中。例以下代碼,將會引發編譯錯誤。

List<?> c = new ArrayList<String>();
// 下面代碼引發編譯錯誤
c.add(new Object());

由於程序沒法肯定c集合中的元素類型,因此不能向其中添加對象。

程序能夠調用get()方法來返回List<?>集合指定索引處的元素,其返回值是一個未知類型,但能夠確定,它老是一個Object。

設定類型通配符的上限

爲了表示List集合的全部元素是一個類F的子類,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示以下:

// 它表示泛型形參必須是F子類的List
List<? extends F>

List<? extends F>是受限制通配符的例子,此處的問號(?)表明一個未知類型,可是必定是F類的子類型(也能夠是F自己),所以能夠把F稱爲這個通配符的上限(upper bound)。

相似地,因爲程序沒法肯定這個受限制的通配符的具體類型,因此不能把F對象或其子類的對象加入這個泛型集合中。例以下代碼是錯誤的。

public void addFs(List<? extends F> fs) {
    // 下面代碼引發編譯錯誤
    fs.add(0, new S());
}

這種指定通配符上限的集合,只能從集合中取元素(取出的元素老是上限的類型),不能向集合中添加元素(由於編譯器無法肯定集合元素實際是哪一種子類型)。

對於更普遍的泛型來講,指定通配符上限就是爲了支持類型型變。好比Foo是Bar的子類,這樣A就至關於A<? extends Foo>的子類,能夠將A賦值給A<? extends Foo>類型的變量,這種型變方法被稱爲協變

對於協變的泛型類來講,它只能調用泛型類型做爲返回值類型的方法(編譯器會將該方法返回值當成通配符上限的類型);而不能調用泛型類型做爲參數的方法。口訣是:協變只出不進。

對於指定通配符上限的類型類,至關於通配符上限是Object。

設定類型通配符的下限

通配符下限用<? super 類型>的方式來指定,通配符下限的做用與通配符上限的做用剛好相反。

指定通配符的下限就是爲了支持類型型變。好比Foo是Bar的子類,當程序須要一個A<? super Bar>變量時,程序可將A、A賦值給A<? super Bar>類型的變量,這種型變方法被稱爲逆變

對於逆變的泛型集合來講,編譯器只知道集合元素是下限的父類型,但具體是哪一種父類型則不肯定。所以,這種逆變的泛型集合能向其中添加元素(由於實際賦值的集合元素老是逆變聲明的父類),從集合中取元素時只能被當成Object類型處理(編譯器沒法肯定取出的究竟是哪一個父類的對象)。

設定泛型形參的上限

Java泛型不只容許在使用通配符形參時設定上限,並且能夠在定義泛型形參時設定上限,用於表示傳給該泛型形參的實際類型要麼是該上限類型,要麼是該上限類型的子類。

public class Apple<T extends Number> {
	T col;
	public static void main(String[] args) {
		Apple<Integer> ai = new Apple<>();
		Apple<Double> ad = new Apple<>();
		// 下面代碼將引起異常 下面代碼試圖把String類型傳給T形參
		// 但String不是Number的子類型 因此引發編譯錯誤
		Apple<String> as = new Apple<>();
	}
}

上面代碼定義了一個Apple泛型類,該Apple類的泛型形參的上限是Number類,這代表Apple類是爲T形參傳入的實際類型只能是Number類或Number類的子類。

在一種更極端的狀況下,程序須要爲泛型形參設定多個上限(至多一個父類上限,能夠有多個接口上限),代表該泛型形參必須是其父類的子類(父類自己也行),而且實現多個上限接口。

// 代表T類型必須是Number類或其子類,並必須實現java.io.Serializable接口
public class Apple<T extends Number & java.io.Serializable> {
    ...
}

與類同時繼承父類、實現接口相似的是,爲泛型形參指定多個上限時,因此的接口上限必須位於類上限以後(類上限位於第一位)。

泛型方法

定義泛型方法

Java5提供的泛型方法(Generic Method),在聲明方法時定義一個或多個泛型形參。泛型方法的語法格式以下。

修飾符 <T, S> 返回值類型 方法名(形參列表) {
    // 方法體...
}

泛型形參聲明以尖括號括起來,多個泛型形參之間以逗號(,)隔開,全部的泛型形參聲明放在方法修飾符和方法返回值類型之間。

與接口、類聲明中定義的泛型不一樣的是,方法聲明中定義的泛型只能在該方法裏使用,而接口、類聲明中定義的泛型則能夠在整個接口、類中使用。

與類、接口中使用泛型參數不一樣的是,方法的泛型參數無須顯示傳入實際類型參數,系統能夠知道爲泛型實際傳入的類型,由於編譯器根據實參推斷出泛型所表明的類型,它一般推斷出最直接的類型。

爲了讓編譯器能準確地推斷出泛型方法中泛型的類型,不要製做迷惑。以下程序。

public class ErrorTest {
	// 聲明一個泛型方法,該泛型方法中帶一個T泛型形參
	static <T> void test(Collection<T> from, Collection<T> to) {
		for (T ele : from) {
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代碼將產生編譯錯誤
		test(as, ao);
	}
}

該方法中的兩個形參from、to的類型都是Collection<T>,這要求調用該方法時的兩個集合實參中的泛型類型相同,不然編譯器沒法準確地推斷出泛型方法中泛型形參的類型。。

可將代碼改爲以下。

public class RightTest {
	// 聲明一個泛型方法,該泛型方法中帶一個T泛型形參
	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<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代碼徹底正常
		test(as, ao);
	}
}

上面代碼改變了test()方法簽名,將該方法的前一個形參類型改成Collection<? extends T>,這種採用類型通配符的表示方式,只要test()方法的前一個Collection集合裏的元素類型是後一個Collection集合裏元素類型的子類便可。

泛型方法和類型通配符的區別

大多數的時候均可以使用泛型方法來代替類型通配符。例如,對於Java的Colletion接口中兩個方法定義:

public interface Collection<E> {
    boolean containsAll(Collection<?> c);
    boolean addAll(Colletion<? extends E> c);
    ...
}

上面集合中兩個方法的形參都採用了類型通配符的形式,也能夠採用泛型方法的形式,以下所示。

public interface Collection<E> {
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
    ...
}

上面方法使用了<T extends E>泛型形式,這時定義泛型形參時設定上限。

上面兩個方法中泛型形參T只使用了一次,泛型形參T產生的惟一效果是能夠在不一樣的調用點傳入不一樣的實際類型。對於這種狀況,應該使用通配符:通配符就是被設計用來支持靈活的子類化的。

泛型方法容許泛型形參被用來表示方法的一個或多個參數之間的類型依賴,或者方法返回值與參數之間的類型依賴關係。若是沒有這樣的類型依賴關係,就不該該使用泛型方法。

若是某個方法中一個形參(a)的類型或返回值的類型依賴於另外一個形參(b)的類型,則形參(b)的類型聲明不該該使用通配符——由於形參(a)或返回值的類型依賴於該形參(b)的類型,若是形參(b)的類型沒法肯定,程序就沒法定義形參(a)的類型。在這種狀況下,只能考慮使用在方法簽名中聲明泛型——也就是泛型方法。

若是有須要,也能夠同時使用泛型方法和通配符,如Java的Colletions.copy()方法。

public class Colletions {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
        ...
    }
}

上面的copy方法中的dest和src存在明顯的依賴關係,從源List中複製出來的元素,必須能夠存放在目標List中,因此源List集合元素的類型只能是目標集合元素的類型的子類型或者它自己。但JDK定義src形參類型時使用的是類型匹配符,而不是泛型方法。這是由於:該方法無須向src集合中添加元素,也無須修改src集合裏的元素,因此可使用類型通配符,無須使用泛型方法。

簡而言之,指定上限的類型通配符支持協變,所以這種協變的集合能夠安全地取出元素(協變只出不進),所以無須使用泛型方法。

固然,也能夠將上面的方法簽名改成使用泛型方法,不使用類型通配符,以下所示。

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
        ...
    }
}

這個方法簽名能夠代替前面的方法簽名。但注意上面的泛型形參S,它僅使用了一次,其餘參數的類型、方法返回值類型都不依賴與它,那泛型形參S就沒有存在的必要,便可以用通配符來代替S。使用通配符比使用泛型方法(在方法簽名中顯式聲明泛型形參)更加清晰和準確,所以Java設計該方法時採用了通配符,而不是泛型方法。

類型通配符和泛型方法還有一個顯著的區別:類型通配符既能夠在方法簽名中定義形參的類型,也能夠用於定義變量的類型;但泛型方法中的泛型形參必須在對應方法顯式聲明。

Java7的菱形語法與泛型構造器

Java也容許在構造器簽名中聲明泛型形參。一旦定義了泛型構造器,在調用構造器時,不只可讓Java根據數據參數的類型來推斷泛型形參的類型,並且也能夠顯式地爲構造器中的泛型形參指定實際的類型。以下程序。

class Foo {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}
public class GenericConstructor {
    public static void main(String[] args) {
        // 泛型構造器中的T類型爲String
		new Foo("好好學習");
		// 泛型構造器中的T類型爲Integer
		new Foo(1024);
		// 顯式指定泛型構造器中T類型爲String
		// 傳給Foo構造器的實參也是String對象 徹底正確
		new <String>Foo("每天向上");
		// 顯式指定泛型構造器中T類型爲String
		// 傳給Foo構造器的實參是Double對象 下面代碼出錯
		new <String>Foo(3.14);
    }
}

Java7新增的菱形語法,容許調用構造器時在構造器後使用一對尖括號來表明泛型信息。但若是程序顯式指定了泛型構造器中聲明的泛型形參的實際類型,則不可使用菱形語法。以下程序所示。

class MyClass<E> {
	public <T> MyClass(T t) {
		System.out.println("t參數值爲: " + t);
	}
}
public class GenericDiamondTest {
	public static void main(String[] args) {
		// MyClass類聲明中的E形參是String類型
		// 泛型構造器中聲明的T形參是Integer類型
		MyClass<String> mc1 = new MyClass<>(9);
        
		// 顯式指定泛型構造器中聲明的T形參是Integer類型
		MyClass<String> mc2 = new <Integer>MyClass<String>(9);
        
		// MyClass類聲明中E形參是String類型
		// 若是顯式指定泛型構造器中聲明的T形參是Integer類型
		// 此時就不能使用菱形語法 下面代碼是錯的
		MyClass<String> mc3 = new <Integer>MyClass<>(9);
	}
}

泛型方法與方法重載

由於泛型即容許設定通配符上限,也容許設定通配符的下限,從而容許在一個類裏包含以下兩個方法定義。

public class MyUtils {
    public static <T> void copy(Collection<T> dest, Collection<? extends T> src) {
        ...
    }
    public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
        ...
    }
}

這兩個方法參數都是Collection對象,前一個集合裏的集合元素類型是後一個集合裏集合元素類型的父類。若是隻是在該類中定義這兩個方法不會有任何錯誤,但只要調用這個方法就會引發編譯錯誤。例以下代碼。

List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln, li);

調用copy()方法既能夠匹配第一個copy方法也能夠匹配第二個copy方法,編譯器沒法肯定想調用哪一個copy方法,因此引發編譯錯誤。

Java8改進的類型推斷

Java8改進了泛型方法的類型推斷能力,類型推斷主要有以下兩方面。

  • 可經過調用方法的上下文來推斷泛型的目標類型。
  • 可在方法調用鏈中,將推斷獲得的泛型傳遞到最後一個方法。
class MyUtil<E> {
	public static <Z> MyUtil<Z> nil() {
		return null;
	}
	public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail) {
		return null;
	}
	E head() {
		return null;
	}
}
public class InferenceTest {
	public static void main(String[] args) {
		// 能夠經過方法賦值的目標參數來推斷泛型爲String
		MyUtil<String> ls = MyUtil.nil();
		// 無須使用下面語句在調用nil()方法時指定泛型的類型
		MyUtil<String> mu = MyUtil.<String>nil();

		// 可調用cons()方法所需的參數類型來推斷泛型爲Integer
		MyUtil.cons(42, MyUtil.nil());
		// 無須使用下面語句在調用nil()方法時指定泛型的類型
		MyUtil.cons(42, MyUtil.<Integer>nil());
	}
}

前兩個調用nil()類方法做用徹底相同,第一個無須在調用nil()方法時顯式指定泛型參數爲String,這是由於程序須要將該方法返回值賦值給MyUtil<String>類型,所以系統能夠自動推斷出此處的泛型參數爲String類型。

後兩個調用cons()方法做用也徹底相同,第一個無須再調用cons()方法時顯式指定泛型參數爲Integer,這是由於程序將nil()方法返回值做爲了cons()方法的第二個參數,而程序可根據cons()方法的第一個參數(42)推斷出此處的泛型參數爲Integer類型。

雖然Java8加強了泛型推斷的能力,但泛型推斷不是萬能的,以下代碼就是錯誤的。

// 但願系統能推斷出調用nil()方法時泛型爲String類型
// 但實際上Java8依然推斷不出來,因此下面代碼報錯
String s = MyUtil.nil().head();

所以,上面這行代碼必須顯式指定泛型的實際類型,即將代碼改成以下形式:

String s = MyUtil.<String>nil().head();

擦除和轉換

容許再使用帶泛型聲明的類時不指定實際的類型。若是沒有爲這個泛型類指定實際的類型,此時被稱做raw type(原始類型),默認是聲明該泛型形參時指定的第一個上限類型。

當把一個具備泛型信息的對象賦給另外一個沒有泛型信息的變量時,全部在尖括號之間的類型信息都將被扔掉。好比一個List<String>類型被轉換爲List,則該List對集合元素的類型檢查變成了泛型參數的上限(即Object)。下面程序示範了這種擦除。

class Milk<T extends Number> {
	T size;
	public Milk() {}
	public Milk(T size) {
		this.size = size;
	}
	public T getSize() {
		return size;
	}
	public void setSize(T size) {
		this.size = size;
	}
}

public class ErasureTest {
	public static void main(String[] args) {
		Milk<Integer> a = new Milk<>(6);
		// a的getSize()方法返回Integer
		Integer as = a.getSize();
		// 把a對象賦給Milk變量,丟失尖括號裏的類型信息
		Milk b = a;
		// b只知道size的類型是Number類
		Number size1 = b.getSize();
		// 下面代碼引發編譯錯誤
		Integer size2 = b.getSize();		
	}
}

當把a賦給一個不帶泛型信息的b變量時,編譯器就會丟失a對象的泛型信息,即全部尖括號裏的信息都會丟失——由於Milk的泛型形參上限是Number類,因此編譯器依然知道b的getSize()方法返回Number類型,但具體是Number的哪一個子類就不清楚了。

從邏輯上看,List<String>是List的子類,若是直接把一個List對象賦給一個List<String>對象應該引發編譯錯誤,但實際上不會。對泛型而言,能夠直接把一個List對象賦給一個List<String>對象,編譯器僅僅提示「未檢查的轉換」,以下程序。

public class ErasureTest2 {
	public static void main(String[] args) {
		List<Integer> li = new ArrayList<>();
		li.add(6);
		li.add(9);
		List list = li;
		// 下面代碼引發「爲經檢查的轉換」警告,編譯、運行時徹底正常
		List<String> ls = list;
		// 但只要訪問ls裏的元素,以下面代碼將引發運行時異常
		System.out.println(ls.get(0));
	}
}

當把這個List<Integer>對象賦給一個List類型後,編譯器就會丟失前者的泛型信息,即丟失集合裏元素的類型信息,這就是典型的擦除。當試圖把該集合裏的元素當成String類型對象取出時,將引起ClassCastException異常。

泛型與數組

Java泛型有一個很重要的設計原則——若是一段代碼在編譯時沒有提出「[unchecked]未經檢查的轉換」警告,則程序在運行時不會引起ClassCastException異常。正是基於這個緣由,因此數組元素的類型不能包含泛型變量或泛型形參,除非是無上限的類型通配符。但能夠聲明元素類型包含泛型變量或泛型形參的數組。也就是說,只能聲明List<String>[]形式的數組,但不能建立ArrayList<String>[10]這樣的數組對象。

加入Java支持建立ArrayList[10]這樣的數組對象,則有以下程序:

// 下面代碼其實是不容許的
List<String>[] lsa = new ArrayList<String>[10];
// 將lsa向上轉型位Object[]類型的變量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
// 將List<Integer>對象做爲oa的第二個元素
// 下面代碼沒有任何警告
oa[1] = li;
// 下面代碼也不會有任何警告,但將引起ClassCastException異常
String s = lsa[1].get(0);

若是第一行代碼是合法的,勢必在最後一行引起運行時異常,這就違背了Java泛型的設計原則。

若是將程序改爲以下形式:

// 下面代碼編譯時有「[unchecked] 未經檢查的轉換」警告
List<String>[] lsa = new ArrayList[10];
// 將lsa向上轉型位Object[]類型的變量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
// 下面代碼引發ClassCastException異常
String s = lsa[1].get(0);

不容許建立List<String>[]類型的對象,但能夠建立一個類型爲ArrayList[10]的數組對象。只是在第一行會有編譯警告。

Java容許建立無上限的通配符泛型數組,例如new ArrayList<?>[10],在這種狀況下,程序不得不進行強制類型轉換。在進行強制類型轉換以前應經過instanceof運算符來保證它的數據類型。以下代碼。

List<?>[] lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String) {
    // 下面代碼安全
    String s = (String)target;
}

與此相似的是,建立元素類型是泛型類型的數組對象也將致使編譯錯誤。以下所示。

<T> T[] makeArray(Collection<T> coll) {
    // 下面代碼致使編譯錯誤
    return new T[coll.size()];
}

因爲類型變量在運行時並不存在,而編譯器沒法肯定實際類型是什麼,所以編譯器報錯。

相關文章
相關標籤/搜索