在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);
這裏的操作是這樣的:
- 新建List容器,存儲Number類型
- 將一個Integer類型對象放入
- 通過去泛型操作將存儲Number類型的List容器引用賦值給存儲Date類型的List容器
- 通過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;
這裏所有的操作都是有效的,一般而言:
- 子類實例同樣是父類的實例
- 只有類型見存在繼承關係,才能進行強制類型轉換,並且只有真實類型爲子類時,強轉纔不會出錯
那麼我們有理由推理,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
- 如果本類和目標類相同,那麼返回true
- 如果本類爲Object,目標類不是基本數據類型,返回true
- 如果本類爲數組,目標類也爲數組,則使用本類和目標類的componentType來調用isAssignableFrom進行判斷
- 如果本類爲接口,則獲取目標類所有實現的接口,然後逐個判斷是否與 自身相同
- 如果本來爲其他一般類,則逐級獲取目標類的父類,直到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自動生成的,因此至少安全和效率會有很大的保證。