1. 程式人生 > >Java中的陣列和List集合以及型別強轉

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這個成員域。

③ 介面

這裡寫圖片描述

這裡實現了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 類來判斷。

三、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自動生成的,因此至少安全和效率會有很大的保證。