Java泛型與數組深入研究


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規范也定義了,數組的父類型為數組元素的父類型。對於數組,我們的總結如下:

  1. 數組類型只和聲明類型相關與它的實際持有類型無關。
  2. 數組類的繼承關系與聲明類型的繼承關系保持一致。

二、泛型通配符

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)。

關於泛型通配符,我們總結如下:

  1. 包含通配符的泛型對象在編譯期會被擦除,利用extends和super能夠為為擦除行為增加約束。
  2. 通配符表示為某一種明確的類型,具體類型只有在運行期可知。

 


免責聲明!

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



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