Java中的數組和List集合以及類型強轉

在java中,集合操作有兩種方式——容器、數組;
容器相比較於數組,多了可擴展性,這裏僅以容器的代表List,來對比和數組的關係。

都知道在java引入的泛型和自動拆裝箱等語法糖後,集合操作也變得簡單安全。
也都知道其實泛型在到字節碼層面上時,會被擦除,雖然字節碼中還會保留泛型參數(可以利用反射看到),但對真實的的類並不產生多大影響。

那麼,對於List來說,如果泛型存在繼承關係,是否可以強行轉換呢?

一、List泛型類型轉換

先看一個關於泛型的代碼:

List<Number> numbers=new ArrayList<>();

//無法直接賦值 :Incompatible types
//List<Object> objects=numbers;

//無法進行類型轉換:Inconvertible types; cannot cast 'java.util.List<java.lang.Number>' to 'java.util.List<java.lang.Object>
//List<Object> objectss=(List<Object>)numbers;

如果放開註釋,編譯器會直接報錯;錯誤警告在註釋中有標出;

可以看到,雖然泛型參數不會改變List類的類型,但在有了泛型之後,無法直接賦值,也無法進行類型的強行轉換

那怎麼樣才能賦值成功呢?

List<Number> numbers=new ArrayList<>();
List presenter =numbers;
List<Object> objects=presenter;

只有這樣:先消除泛型處理,然後再直接賦值

雖然說換了一種方式,但其實代碼邏輯與之前錯誤的那種並無不同,那爲什麼編譯器對上一種操作方式不認可呢?

這個需要繼續看接下來的代碼:

List<Number> numbers=new ArrayList<>();
numbers.add(new Integer(200));
List presenter =numbers;
List<Date> objects=presenter;
Date date = objects.get(0);

這裏的操作是這樣的:

  1. 新建List容器,存儲Number類型
  2. 將一個Integer類型對象放入
  3. 通過去泛型操作將存儲Number類型的List容器引用賦值給存儲Date類型的List容器
  4. 通過get方法獲取容器中第一個元素(真實類型爲Number),然後強轉爲Date類型

這段代碼完全符合Java語法,但真正運行時就會出現異常:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.util.Date

由此可見,最開始一段代碼中,進行非相同類型相互賦值是很有風險的,因此編譯器對此做了特殊處理,Java是一門基於安全的語言,這種很危險的事情當然會有相應的警告,但要是人爲使壞(比如通過反射向容器中添加元素),那就沒辦法了。

同樣的,除了容器泛型外,數組有時候也需要進行一些類型轉換操作,那麼數組會和泛型容器一樣出現相同的問題麼?

二、類型數組

Java中枚舉類型enum繼承自Enum類,但數組類可不繼承自Array類,對於容器類來說不允許的操作,數組類確是允許的:

Integer[] integers=new Integer[2];
Number[] numbers=integers;

如上代碼所示,一個Integer類型數組,可以直接賦值給Number類型數組

與集合不同的是,泛型不同的List所對應的class類相同類型數據對應的class類不同

因此說到底,泛型集合根本不是強制類型轉換,因爲表面類型(和靜態分派是所說的類型相似)並沒有發生變化。這點可以通過編譯器調試模式REPL來進行分析;

1、基本類型對應的數組

java中雖然說基本類型(如int)可以和包裝類型(如Integer)自動拆裝箱,彷彿隨時隨地可以相互轉化,但其實只是表面語法糖而已,同樣的,基本類型數組和對象數組也存在不同之處。

先寫出一些實例代碼用於調試:

int[] a=new int[3];

然後進入調試模式查看變量a到底是什麼類型:

① 數組類型

這裏寫圖片描述

這裏寫圖片描述

可以看到,int[] 對應的類型爲 [I

② 類方法、類成員

這裏寫圖片描述

這裏寫圖片描述

這個類型其實是JVM內部自動生成的,在外部無法訪問到。

這裏我們可以發現,竟然沒有任何成員,也沒有任何方法。我們平時使用數組的時候,明明是使用了length屬性的,這裏爲什麼沒有顯示呢?

看這個代碼:

int[] a = new int[3];
int length = a.length;

生成的字節碼是這樣的:

...
5 arraylength
...

對於字節碼來說,如果length真的是屬性,那麼應該是通過 getfield 命令來獲取,這裏顯然不是這樣。

對於數組元素的提取,則是使用了其他的字節碼指令(如aastore等)來完成的。因此確實沒有length這個成員域。

關於數組爲何要使用length屬性來獲取長度,可以參閱:爲什麼使用length獲取Java數組的長度

③ 接口

這裏寫圖片描述

這裏實現了Cloneable和Serializable,可以進行序列化和克隆。

④ 父類

這裏寫圖片描述

這裏看到,基本類型數組的父類爲Object,這也符合情況,因爲一般來說基本類型數組對象最多也只是將引用賦值給Object類型引用而已。

2、引用類型對應的數組

首先來思考一下,引用類型數組和基本類型數組是否基本類似?

使用一下之前的代碼:

Integer[] integers = new Integer[5];
Number[] numbers = integers;
Integer[] is = (Integer[]) numbers;

這裏所有的操作都是有效的,一般而言:

  1. 子類實例同樣是父類的實例
  2. 只有類型見存在繼承關係,才能進行強制類型轉換,並且只有真實類型爲子類時,強轉纔不會出錯

那麼我們有理由推理,Integer[] 是Number[] 的子類。

和基本類型一樣,進行調試測試一下類型到底是什麼:

① 引用數組類型

這裏寫圖片描述

改數組類型爲: [java.lang.Integer
格式和基本類型都一致,中括號表示一維,Integer表示類型

② 接口

這裏寫圖片描述

和基本數組類型一樣,實現了克隆和序列化接口

③ 父類

這裏寫圖片描述

看到這裏,我們發現與我們的推理有些不符合了,Integer[] 的父類竟然直接就是Object。

是猜想有問題麼?但如果真的 Integer[] 和 Number[] 都是Object的直接子類,那兩者強轉肯定會類型轉型轉換異常的吧。

因此這裏我們直接使用java代碼去進行測試。

通常我們一般會使用 instanceof 操作符來判斷繼承關係

integers instanceof [java.lang.Integer

這樣很明顯是不行的,我們根本無法直接表示出數組類來,所以還得使用一般的方式:

integers instanceof Number[]

執行後可以看到,結果爲 true ,這就很好了,我們完全可以通過 Integer 類的繼承關係,找出 Integer[] 類的繼承關係:

這裏寫圖片描述

3、多維數組類型

通過調試模式我們可以驗證 一維數組繼承關係,但如果現在是多維數組,那麼該如何處理呢?

這裏我們可以直接看類繼承是如何來判斷的。

通用的 instanceof 操作符是java語法自帶的,根本沒法看到具體邏輯,因此我們需要查看繼承判斷方式的源碼:

  • Class.isInstance()
  • Class.isAssignableFrom()

兩者的判斷邏輯其實是一樣的:

public boolean isInstance(Object obj) {
    if (obj == null) {
        return false;
    }
    return isAssignableFrom(obj.getClass());
}

Class.isInstance() 方法會先判斷是否爲null,爲null的話直接返回false;否則調用Class.isAssignableFrom()

public boolean isAssignableFrom(Class<?> cls) {
    if (this == cls) {
        return true;  // Can always assign to things of the same type.
    } else if (this == Object.class) {
        return !cls.isPrimitive();  // Can assign any reference to java.lang.Object.
    } else if (isArray()) {
        return cls.isArray() && componentType.isAssignableFrom(cls.componentType);
    } else if (isInterface()) {
        // Search iftable which has a flattened and uniqued list of interfaces.
        Object[] iftable = cls.ifTable;
        if (iftable != null) {
            for (int i = 0; i < iftable.length; i += 2) {
                if (iftable[i] == this) {
                    return true;
                }
            }
        }
        return false;
    } else {
        if (!cls.isInterface()) {
            for (cls = cls.superClass; cls != null; cls = cls.superClass) {
                if (cls == this) {
                    return true;
                }
            }
        }
        return false;
    }
}

這裏進行的判斷邏輯如下:這裏用本類和目標類表示this與cls

  1. 如果本類和目標類相同,那麼返回true
  2. 如果本類爲Object,目標類不是基本數據類型,返回true
  3. 如果本類爲數組,目標類也爲數組,則使用本類和目標類的componentType來調用isAssignableFrom進行判斷
  4. 如果本類爲接口,則獲取目標類所有實現的接口,然後逐個判斷是否與 自身相同
  5. 如果本來爲其他一般類,則逐級獲取目標類的父類,直到Object,看是否有某一級父類會與自身相同。

這個邏輯很簡單,這裏直接看第三步對於數組類型的判斷邏輯,其中有提到componentType,那麼這個是什麼存在呢?

/**
 * For array classes, the component class object for instanceof/checkcast (for String[][][],
 * this will be String[][]). null for non-array classes.
 */
private transient Class<?> componentType;

翻譯過來大概意思是:

對於數組類型,component 用於在 instanceof 操作或者類型轉換時進行判斷;對String[][][]類型來說,其component爲 String[][]
對於非數組類型,改字段爲null

也就是說,對於多維數組 A[ ][ ][ ][ ]… 和 B[ ][ ][ ][ ]… ,只要維度相同,那麼兩者之間的繼承關係可以直接通過 A 和 B 類來判斷。

至於多維數組內存存儲方式,可以參考:Java中數組在內存中的存放原理

三、List和數組,應該如何選擇

在一般寫代碼時,都習慣性的使用容器類,因爲它可以 動態擴容,很方便,那數組還有沒有 什麼用處呢?

1、數組使用場景

對於固定數量的對象,一般使用數組是比較好的,一方面數組佔用內存空間肯定要少的多。
另外一方面,由於多維數組的存在,在操縱一些矩陣 類數據時,總會直觀一些。

2、ArrayList、LinkList、數組

這個其實 很多人都 已經知道了,ArrayList底層使用的數組 ,在生成實例時,可以傳遞容量參數。

transient Object[] elementData;
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

elementData 變量用於存儲元素值,Object類型表示ArrayList真實存在的其實類型很寬,使用泛型也只是在語法 層面上減少使用出錯率。
transient變量則保證了集合中的元素不會存在線程同步的問題。

LinkList 則在底層使用了鏈表,

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

從代碼中可以看到,這還是個雙向鏈表

jdk1.5添加了增強for循環功能,對於數組來說,因爲無法看到源碼,暫時不考慮效率問題。

但從ArrayList和LinkList來看,很明顯的LinkList使用增強for循環會更快一些,ArrayList由於內部爲數組,因此普通的for循環訪問速度會更快。

總的來說,由於容器List的出現,數組類使用場景已經沒有以前那麼多了,但由於容器類的特殊性,是通過JVM自動生成的,因此至少安全和效率會有很大的保證。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章