Java中的泛型與數組平時開發用的很多,除了偶爾遇到"NullPointerException"和"IndexOutOfBoundsException"一般也不會遇到太大問題。可是如果深入研究,可能會發現這兩種類型有很多奇怪的特點。我查了一些資料,發現包括《Java編程思想》在內對這些問題的解釋都含糊其辭(不排除是本人理解能力有限)。因此在大量實踐的基礎上,我只能提出自己的對這些問題的理解並總結下來。
一、數組轉型
准備工作
創建三個表示繼承關系的類,供測試使用:
class Fruit { } class Apple extends Fruit { } class RedApple extends Apple { }
數組能夠轉型嗎?
// 測試:數組轉型 public void arrayTransform() { Fruit[] fruits = new Apple[10]; Fruit[] newFruits = new Fruit[10]; newFruits[0] = new Apple(); newFruits[1] = new Apple(); Apple[] apples = (Apple[]) newFruits; // 拋出異常java.lang.ClassCastException }
編譯期不報錯,運行期拋異常。而且這個錯誤與newFruits所持有的實際對象無關。如果要強制轉型只能遍歷數組來完成。要理解這個問題本質,我們需要首先了解Java的數組內存模型。
數組的內存模型
數組轉型,JVM只會檢查它的直接類型。上面的例子中,對於Apple[]而言Fruit[]是它的父類,因此可以直接轉型。而向下轉型的時候,相當於Fruit轉型Apple。因此在運行期報錯。不過事情似乎不那么簡單,Fruit[]真的是Apple[]的父類嗎?
public void arraySuperclass() { Fruit[] fruits = new Fruit[10]; Apple[] apples = new Apple[10]; System.out.println(fruits.getClass()); System.out.println(apples.getClass()); System.out.println(fruits.getClass().getSuperclass()); System.out.println(apples.getClass().getSuperclass()); } /* Output: class [LFruit; class [LApple; class java.lang.Object class java.lang.Object */
從輸出看,似乎數組之間並沒有相互的繼承關系。那么這樣的轉型到底是如何實現的呢?根據JVM的規范,數組類型是由虛擬機在運行時創建的,控制台輸出的前兩行即代表數組類型。同時JVM規范也定義了,數組的父類型為數組元素的父類型。對於數組,我們的總結如下:
- 數組類型只和聲明類型相關與它的實際持有類型無關。
- 數組類的繼承關系與聲明類型的繼承關系保持一致。
二、泛型通配符
extends的含義
public void genericExtends(List<? extends Fruit> ls) { ls.add(new Apple()); // 編譯報錯 Fruit f = ls.get(0); }
使用extends修飾的泛型容器只能調用get()方法,並且genericExtends方法可以接受List<Fruit>、List<Apple>和List<RedApple>類型的參數。
Java泛型是一種妥協的產物,實際在泛型內部無法知曉實際類型。Java為了實現泛型采用一種被稱為“擦除”的機制,並且這種機制在編譯期完成。這樣做的目的是為了和老版本的.class文件保持兼容。extends本質是一種擦除的約束條件,表示List<? extends Fruit>對象將會被擦除成List<Fruit>,因此在方法內部開發人員可以調用在Fruit的所有方法。但是必須明確一點,List<? extends Fruit>與List<Fruit>絕不一樣。這一點和數組類型相似,任何以Fruit的子類為泛型的容器類(如:List<Apple>)並不是Fruit容器類(如:List<Fruit>)的子類。如果必須要說它們之間存在什么共性的話就是以Fruit的子類為泛型的容器類可以在編譯期被擦除為Fruit的容器類。要如何實現這一點呢?Java提出了在泛型中利用extends作為修飾符。因此,List<? extends Fruit>聲明的本質是告訴編譯期,請嘗試把方法的參數(如:List<Apple>)擦除成List<Fruit>而不要直接擦除成List<Object>,如果成立則編譯通過。
因此在genericExtends方法的內部,ls表示它可能為List<Fruit>、List<Apple>和List<RedApple>之中的任何一個類型。因此ls不能使用add方法想容器中添加元素。
super的含義
public void genericSuper(List<? super Apple> ls) { ls.add(new Apple()); ls.add(new RedApple()); } public void methInvok() { List<Fruit> fruits = new ArrayList<>(); this.genericSuper(fruits); }
如果希望在方法內部可以向容器中添加元素,則推薦的做法是使用super修飾泛型。以它作為泛型修飾符的方法形參,從外部看表示:我可以接收以泛型聲明類自身或所有父類為泛型的容器類,如(List<Apple>或List<Fruit>)。它相當於容器添加了一個下界。從內部看表示:參數ls能夠接收所有泛型聲明類自身或為基類的對象,如(Apple 或 RedApple)。
關於泛型通配符,我們總結如下:
- 包含通配符的泛型對象在編譯期會被擦除,利用extends和super能夠為為擦除行為增加約束。
- 通配符表示為某一種明確的類型,具體類型只有在運行期可知。