1. 集合框架圖
Java中的集合是用于存儲對象的工具類容器挂据,它實現(xiàn)了常用的數(shù)據(jù)結(jié)構(gòu)以清,提供了一系列公開的方法用于增加、刪除崎逃、修改掷倔、查找和遍歷數(shù)據(jù),降低開發(fā)成本个绍。集合種類非常多勒葱,形成了一個比較經(jīng)典的繼承關系數(shù)勺像,稱為Java集合框架圖,如下圖所示错森∫骰拢框架圖主要分為兩類:第一類按照單個元素存儲的Collection,在繼承樹中Set和List都實現(xiàn)了Collection接口涩维;第二類是按照key-value村村的Map殃姓。以上兩類集合體系,無論在數(shù)據(jù)存儲還是遍歷瓦阐,都存在非常大的差別蜗侈。
??在集合框架圖中,紅色代表接口睡蟋,藍色代表抽象類踏幻,綠色代表并發(fā)包中的類,灰色代表早期線程安全的類(基本已棄用)戳杀「妹妫可以看到,與Collection相關的4條線分別是List信卡、Set隔缀、Queue、Map傍菇,它們的子類會映射到數(shù)據(jù)結(jié)構(gòu)中的表猾瘸、數(shù)、哈希等丢习。
List集合
??List集合是線性數(shù)據(jù)結(jié)構(gòu)的主要實現(xiàn)牵触,集合元素通常存在明確的上一個和下一個元素,也存在明確的第一個元素和最后一個元素咐低。List 集合的遍歷結(jié)果是穩(wěn)定的揽思。該體系最常用的是ArrayList 和 LinkedList 兩個集合類。
??ArrayList 是容量可以改變的非線程安全集合渊鞋。內(nèi)部實現(xiàn)使用數(shù)組進行存儲绰更,集合擴客時會創(chuàng)建更大的數(shù)組空間,把原有數(shù)據(jù)復制到新數(shù)組中锡宋。ArrayList 支持對元素的快速隨機訪問儡湾,但是插入與刪除時速度通常很慢,因為這個過程很有可能需要移動其它元素执俩。
??LinkedList 的本質(zhì)是雙向鏈表徐钠。與 ArrayList 相比,LinkedList 的插入和刪除速更快役首,但是隨機訪問速度則很慢尝丐。測試表明显拜,對于 10萬條的數(shù)據(jù),與 ArrayList相比隨機提取元素時存在數(shù)百倍的差距爹袁。除繼承 AbstractList 抽象類外远荠,LinkedList 還實現(xiàn)了另一個接口 Deque,即 double-ended queue失息。這個接口同時具有隊列和棧的性質(zhì)譬淳。LinkedList 包含3個重要的成員: size、first盹兢、last邻梆。size 是雙向鏈表中節(jié)點的個數(shù),first和last分別指向第一個和最后一個節(jié)點的引用绎秒。LinkedList 的優(yōu)點在于可以將零散的內(nèi)存單元通過附加引用的方式關聯(lián)起來扮叨,形成按鏈路順序查找的線性結(jié)構(gòu)卡骂,內(nèi)存利用率較高管毙。Queue集合
??Queue(隊列)是一種先進先出的數(shù)據(jù)結(jié)構(gòu)镜粤,隊列是一種特殊的線性表斋陪,它只許在表的一端進行獲取操作辈挂,在表的另一端進行插入操作抒痒。當隊列中沒有元素時飒责,稱為空隊列惠赫。自從BlockingQueue(阻塞隊列)問世以來把鉴,隊列的地位得到極大的提升在各種高并發(fā)編程場景中,由于其本身 FIFO的特性和阻塞操作的特點儿咱,經(jīng)常被作為Buffer(數(shù)據(jù)緩沖區(qū))使用庭砍。Map集合
??Map集合是以Key-Value鍵值對作為存儲元素實現(xiàn)的哈希結(jié)構(gòu),Key 按某種哈函數(shù)計算后是唯一的混埠,Value 則是可以重復的怠缸。Map 類提供三種 Collection 視圖,集合框架圖中钳宪,Map 指向 Collection 的箭頭僅表示兩個類之間的依賴關系揭北。可以使用keySet()查看所有的Key吏颖,使用 values()查看所有的Value搔体,使用entrySet()查看所的鍵值對。最早用于存儲鍵值對的 Hashtable 因為性能瓶頸已經(jīng)被淘頭半醉,而如今廣使用的 HashMap疚俱,線程是不安全的。ConcurrentHashMap 是線程安全的缩多,在JDK8中進行了鎖的大幅度優(yōu)化呆奕,體現(xiàn)出不錯的性能养晋。在多線程并發(fā)場景中,優(yōu)先推薦使用ConcurrentHashMap梁钾,而不是 HashMap绳泉。TreeMap 是 Key 有序的 Map 類集合。Set集合
??Set是不允許出現(xiàn)重復元素的集合類型姆泻。Set 體系最常用的是 HashSet圈纺、TreeSe和LinkedHashSet 三個集合類。HashSet 從源碼分析是使用HashMap 來實現(xiàn)的麦射,只是Value固定為一個靜態(tài)對象蛾娶,使用 Key 保證集合元素的唯一性,但它不保證集合元素的順序潜秋。TreeSet也是如此蛔琅,從源碼分析是使用 TreeMap 來實現(xiàn)的,底層為樹結(jié)構(gòu)峻呛,在添加新元素到集合中時罗售,按照某種比較規(guī)則將其插入合適的位置,保證插入后的人仍然是有序的钩述。LinkedHashSet 繼承自 HashSet寨躁,具有 HashSet 的優(yōu)點,內(nèi)部使用鏈表維護了元素插入順序牙勘。
2. 集合初始化
??集合初始化通常進行分配客量职恳、設置特定參數(shù)等相關工作。我們以使用頻率較高為ArayList 和 HashMap 為例方面,簡要說明初始化的相關工作放钦,并解釋為什么在任何情況下,都需要顯式地設定集合容量的初始大小恭金。ArayList是存儲單個元素的順序表結(jié)構(gòu)操禀,HashMap 是存儲 KV 鍵值對的哈希式結(jié)構(gòu)。分析兩者的初始化相關源碼横腿,洞悉它們的容量分配颓屑、參數(shù)設定等相關邏輯,有助于更好地了解集合特性耿焊,提升代碼質(zhì)量揪惦。下面先從 ArrayList 源碼說起:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
// 空表的表示方法
private static final Object[] EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 值大于 0時,根據(jù)構(gòu)造方法的參數(shù)值搀别,忠實地創(chuàng)建一個多大的數(shù)組
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 公開的 ada 方法調(diào)用此內(nèi)部私有方法
private void add(E e, Object[] elementData, int s) {
// 當前數(shù)組能否容納 size+1 的元素丹擎,如果不夠,則調(diào)用grow來擴容
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//擴容的最小要求,必須容納剛才的元素個數(shù) +1蒂培,注意再愈,newCapacity()
// 方法才是擴容的重點!
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private Object[] grow() {
return grow(size + 1);
}
private int newCapacity(int minCapacity) {
// overflow-conscious code 防止擴容1.5 倍之后,超過 int 的表示范圍(第1處)
int oldCapacity = elementData.length;
// JDK6之前擴容 50%或50-1护戳,但是取ceil翎冲,而之后的版本取 Floor (第2處
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//無參數(shù)構(gòu)造方法,會在此時分配默認為10 的容量
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
}
??第1處說明:正數(shù)帶符號右移的值肯定是正值媳荒,所以oldCapacity+(oldCapacity>>l)的結(jié)果可能超過int可以表示的最大值抗悍,反而有可能比參數(shù)的 minCapacity 更小,則返回值為(size+1)的minCapacity钳枕。
??第2處說明:如果原始容量是 13缴渊,當新添加一個元素時,依據(jù)程序中的計算方法得出13的二進制數(shù)為 1101鱼炒,隨后右移1位操作后得到二進制數(shù) 110衔沼,即十進制數(shù)6最終擴容的大小計算結(jié)果為 oldCapacitiy +(oldCapacity>>1)= 13+6=19。使用位算主要是基于計算效率的考慮昔瞧。在JDK7之前的公式指蚁,擴容計算方式和結(jié)果為 oldCapacitiy x3÷2+1=13x3÷2+1=20。
??當ArrayList 使用無參構(gòu)造時自晰,默認大小為 10凝化,也就是說在第一次 add 的時候分配為10的容量,后續(xù)的每次擴容都會調(diào)用 Array.copyof方法酬荞,創(chuàng)建新數(shù)組再復制搓劫,可以想象,假如需要將 1000個元素放在 ArrayList中袜蚕,采用默認構(gòu)造方法糟把,需要被動擴容13次才可以究成存。反之牲剃,如果在初始化時便指定了容量new ArrayList(1000),那么在初始化 ArrayList對象的時候就直接分配 1000個儲空間而避免被動擴容和數(shù)組復制的額外開銷雄可。最后凿傅,進一步設想,如果這個值達到更大量級数苫,卻沒有注意初始的容量分配問題聪舒,那么無形中造成的性能損耗是非常大的,甚至導致 0OM 的風險虐急。
??再來看一下HashMap,如果它需要放置1000個元素箱残,同樣沒有設置初始容量大小隨著元素的不斷增加,則需要被動擴客7次才可以完成存儲。擴容時需要重建hash表非常影響性能被辑。在 HashMap 中有兩個比較重要的參數(shù) Capacity 和 Load Factor燎悍,其中Capacity 決定了存儲容量的大小,默認為 16;而 Lod Factor 決定了填充比例-般使用默認的0.75盼理√干剑基于這兩個參數(shù)的乘積,HashMap 內(nèi)部用 threshold 變量表示HashMap中能放入的元素個數(shù)宏怔。HashMap 容量并不會在 new 的時候分配奏路,而是在第一次put 的時候完成創(chuàng)建的,源碼如下(jdk1.7).
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
.........
}
/**
* Inflates the table. 第一次 put 時臊诊,調(diào)用如下方法鸽粉,初始化 table
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
// 找到大于參數(shù)值且最接近 2 的冪值,假如輸入?yún)?shù)是 27抓艳,則返回32
int capacity = roundUpToPowerOf2(toSize);
//threshold 在不超過限制最大值的前提下等于 capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
??為了提高運算速度潜叛,設定 HashMap 容量大小為2?,這樣的方式使計算落槽位置更快壶硅。如果初始化 HashMap 的時侯通過構(gòu)造器指定了 initialCapacity威兜,則會先計算出比 initialCapacity 大的2 的冪存入 threshold,在第一次 put 時會按照這個2的冪初始化數(shù)組大小庐椒,此后每次擴容都是增加2倍椒舵。如果沒有指定初始值,log?1000 =9.96,結(jié)合源碼分析可知约谈,如果想要容納 1000 個元素笔宿,必須經(jīng)過7次擴容。HashMap的擴容還是有不小的成本的棱诱,如果提前能夠預估出 HashMap 內(nèi)要放置的元素數(shù)量泼橘,就可在初始化時合理設置容量大小,避免不斷擴容帶來的性能損耗迈勋。
??綜上所述炬灭,集合初始化時,指定集合初始值大小靡菇。如果暫時無法確定集合大小那么指定相應的默認值重归,這也要求我們記得各種集合的默認值大小,ArrayList大小10厦凤,而 HashMap 默認值為 16鼻吮。
3. 數(shù)組與集合
??數(shù)組是一種順序表,在各種高級語言中较鼓,它是組織和處理數(shù)據(jù)的一種常見方式椎木,我們可以使用索引下標進行快速定位并獲取指定位置的元素。數(shù)組的下標從0開始,但這并不符合生活常識香椎,這源于BCPL 語言漱竖,它將指針設置在0的位置,用數(shù)組下標作為直接偏移量進行計算士鸥。為什么下標不從1 開始呢?如果是這樣闲孤,計算偏移量就要使用當前下標減1的操作。加減法運算對 CPU 來說是一種雙數(shù)運算烤礁,在數(shù)組下標使用頻率極高的場景下讼积,這種運算是十分耗時的。在Java 體系中脚仔,數(shù)組用以存儲同-類型的對象勤众,一旦分配內(nèi)存后則無法擴容。提倡類型與中括號緊挨相連來定義數(shù)組鲤脏,因為在Java的世界里们颜,萬物皆為對象。String[] 用來指代String數(shù)組對象猎醇,示例代碼如下.
String[] strings = {"a", "b"};//數(shù)組引用賦值給 Object
Object obj = strings;//使用類名string[]進行強制轉(zhuǎn)化窥突,并成功賦值,strings[0]的值由a變?yōu)?object
((String[]) obj)[0] = "object";
??聲明數(shù)組和賦值的方式示例代碼如下:
// 初始化完成硫嘶,容量的大小即等于大括號內(nèi)元素的個數(shù)阻问,使用頻率并不高
String[] args3 = {"a", "b"};
String[] args4 = new String[2];
args4[0] = "a";
args4[1] = "h";
??上述源碼中的 args3 是靜態(tài)初始化,而 args4 是動態(tài)初始化沦疾。無論靜態(tài)初始化還是動態(tài)初始化称近,數(shù)組是固定容量大小的。注意在數(shù)組動態(tài)初始化時哮塞,出現(xiàn)了 new刨秆,這意味著需要在 new String[]的方括號內(nèi)填寫一個整數(shù)忆畅。如果寫的是負數(shù),并不會編譯出錯眠屎,但運行時會拋出異帶:NegativeArraySizeException。對于動態(tài)大小的數(shù)組肆饶,集合提供了Vector和 AmayLsit 兩個類岖常,前者是線程安全,性能校差,基本棄用板惑,而后者是線程不安全橄镜,它是使用頻率最高的集合之一冯乘。
??數(shù)組的遍歷優(yōu)先推薦 JDK5引進的 foreach 方式,即 for(元素:數(shù)組名)的方式姊氓,可以在不使用下標的情況下遍歷數(shù)組喷好。如果需要使用數(shù)組下標,則使用for(int i=0;i<array.lengt;i++)的方式禾唁,注意 length 是數(shù)組對象的一個屬性荡短,而不是方法哆键。string類是使用 length()方法來獲取字符串長度的)洼哎。也可以使用JDK8 的函數(shù)式接口進行遍歷:
Arrays.asList(args3).stream().forEach(x-> System.out.println(x));
Arrays.asList(args3).stream().forEach(System.out::println);
??Arrays 是針對數(shù)組對象進行操作的工具類,包括數(shù)組的排序锭沟、查找族淮、對比凭涂、拷貝等操作切油。尤其是排序,在多個JDK 版本中在不斷地進化孕荠,比如原來的歸并排序改成Timsort,明顯地改善了集合的排序性能娩鹉。另外,通過這個工具類也可以把數(shù)組轉(zhuǎn)成集合稚伍。
??數(shù)組與集合都是用來存儲對象的容器,前者性質(zhì)單一锈嫩,方便易用;后者類型安全呼寸,功能強大悼沿,且兩者之間必然有互相轉(zhuǎn)換的方式糟趾。畢竟它們的性格迥異义郑,在轉(zhuǎn)換過程中,如果不注意轉(zhuǎn)換背后的實現(xiàn)方式交汤,很容易產(chǎn)生意料之外的問題芙扎。轉(zhuǎn)換分成兩種情況:數(shù)組轉(zhuǎn)集合和集合轉(zhuǎn)數(shù)組填大。在數(shù)組轉(zhuǎn)集合的過程中允华,注意是否使用了視圖方式直接返回數(shù)組中的數(shù)據(jù)靴寂。我們以Arrays.asList()為例,它把數(shù)組轉(zhuǎn)換成集合時褐隆,不能使用其修改集合相關的方法妓灌,它的add/remove/clear 方法會拋出UnsupportedOperationException 異常蜜宪。示例源碼如下:
public class ArraysAsList {
public static void main(String[] args) {
String[] stringArray = new String[3];
stringArray[0] = "one";
stringArray[1] = "two";
stringArray[2] = "three";
List<String> stringList = Arrays.asList(stringArray);// 修改轉(zhuǎn)換后的集合圃验,成功地把第一個元素“one”改成“oneList
stringList.set(0, "oneList");
// 運行結(jié)果是 oneList澳窑,數(shù)組的值隨之改變
System.out.println(stringArray[0]);
// 這是重點:以下三行編譯正確摊聋,但都會拋出運行時異常
stringList.add("four");
stringList.remove(2);
stringList.clear();
}
}
??事實證明,可以通過set()方法修改元素的值箍镜,原有數(shù)組相應位置的值同時也會被修改色迂,但是不能進行修改元素個數(shù)的任何操作手销,否則均會拋出UnsupportedOperationException 異常锋拖。Arays.asList 體現(xiàn)的是適配器模式兽埃,后臺的數(shù)據(jù)仍是原有數(shù)組,set()方法即間接對數(shù)組進行值的修改操作慕趴。asList 的返回對象是一個Arrays 的內(nèi)部類冕房,它并沒有實現(xiàn)集合個數(shù)的相關修改方法耙册,這也正是拋出異常的原因毫捣。Arrays.asList 的源碼如下:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
??返回的明明是ArrayList 對象,怎么就不可以隨心所欲地對此集合進行修改呢蹲诀?注意此ArrayList 非彼ArrayList脯爪,雖然Arrays 與ArrayList 同屬于一個包矿微,但是在Arrays類中還定義了一個ArrayList的內(nèi)部類(或許命名為InnerArrayList更容易識別)涌矢,根據(jù)作用域就近原則娜庇,此處的ArrayList是李鬼,即這是個內(nèi)部類俺叭。此李鬼十分簡單只提供了個別方法的實現(xiàn)熄守,如下所示:
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
// final修飾不準修改其引用 (第1處)
private final E[] a;
// 直接把數(shù)組引用賦值給 a裕照,而 objects 是 JDK7引入的工具包
// requireNonNul1 僅僅判斷是否為 null
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
// 實現(xiàn)了修改特定位置元素的方法
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
// 注意 set 成功返回的是此位置上的舊值
return oldValue;
}
}
??第1處的 final 引用晋南,用于存儲集合的數(shù)組引用始終被強制指向原有數(shù)組负间。這個內(nèi)部類并沒有實現(xiàn)任何修改集合元麥個數(shù)的相關方法姜凄,那這個UnspportedOperationException 異常 是 從哪里 出 來的呢? 是李鬼的父類AbstractList:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
// clear()方法調(diào)用 remove 方法态秧,依然拋出異常
public void clear() {
removeRange(0, size());
}
}
??如果李鬼Arrays.ArrayList 內(nèi)部類覆寫這些方法不拋出異常,避免使用者踩進這個坑會不會更好?數(shù)組具有不為五斗米折腰的氣節(jié)云头,傳遞的信息是“要么直接用我,要么小心異常!”數(shù)組轉(zhuǎn)集合引發(fā)的故障還是十分常見的溃槐。比如,某業(yè)務調(diào)用某接口時撮慨,對方以這樣的方式返回一個 List 類型的集合對象砌溺,本方獲取集合數(shù)據(jù)時规伐,99.9%是只讀操作猖闪,但在小概率情況下需要增加一個元素肌厨,從而引發(fā)故障柑爸。在使用數(shù)組轉(zhuǎn)集合時表鳍,需要使用李逵iava.util.ArrayList 直接創(chuàng)建一個新集合,參數(shù)就是ArraysasList返回的不可變集合瓮恭,源碼如下:
List<Object> objectList = new java.util.ArrayList<Object>(Arrays.asList(stringArray));
??相對于數(shù)組轉(zhuǎn)集合來說屯蹦,集合轉(zhuǎn)數(shù)組更加可控登澜,畢竟是從相對自由的集合容器轉(zhuǎn)為更加苛刻的數(shù)組帖渠。什么情況下集合需要轉(zhuǎn)成數(shù)組呢?適配別人的數(shù)組接口空郊,或者進行局部方法計算等。先看一個源碼锁摔,猜猜執(zhí)行結(jié)果
public class ListToArray {
public static void main(String[] args) {
List<String> list = new ArrayList<String>(3);
list.add("one");
list.add("two");
list.add("three");
//泛型丟失谐腰,無法使用 string[] 接收無參方法返回的結(jié)果 (第1處)
Object[] array1 = list.toArray();
// array2 數(shù)組長度小于元素個數(shù) (第2處)
String[] array2 = new String[2];
list.toArray(array2);
System.out.println(Arrays.asList(array2));
// array3 數(shù)組長度等于元素個數(shù) (第3處)
String[] array3 = new String[3];
list.toArray(array3);
System.out.println(Arrays.asList(array3));
}
}
執(zhí)行結(jié)果如下:
[null,null]
[one十气,two砸西, three]
第1處比較容易理解芹枷,不要用toArray()無參方法把集合轉(zhuǎn)換成數(shù)組莲趣,這樣會致泛型丟失;
在第2處執(zhí)行成功后喧伞,輸出卻為 null;
第3處正常執(zhí)行絮识,成功地把集合數(shù)據(jù)復制到array3數(shù)組中次舌。
第2處與第3處的區(qū)別在于即將復制進去的數(shù)組容量是否足夠。如果容量不夠挪圾,則棄用此數(shù)組哲思,另起爐灶棚赔,關于此方法的源碼如下.
// 注意入?yún)?shù)組的 length 大小是重中之重靠益,如果大于或等于集合的大小
// 則集合中的數(shù)據(jù)復制進入數(shù)組即可,如果空間不夠芋浮,入?yún)?shù)組 a 就會被無視
// 重新分配一個空間纸巷,復制完成后返回一個新的數(shù)組引用
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
// 如果數(shù)組長度小于集合 size瘤旨,那么執(zhí)行此語句裆站,直接 return黔夭。(第1處)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// 如果容量足夠本姥,則直接復制 (第2處)
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
// 只有在數(shù)組容量足夠的情況下婚惫,才返回傳入?yún)?shù)
return a;
}
??第1處和第 2 處均 復制 java.util.ArrayList 的 elementData到數(shù)組中先舷,這個elementData是 ArrayList 集合對象中真正用于存儲數(shù)據(jù)的數(shù)組蒋川,它的定義為:transient Object[] elementData
??這個存儲ArrayList 真正數(shù)據(jù)的數(shù)組由 transient 修飾捺球,表示此字段在類的序列化時將被忽略夕冲。因為集合序列化時系統(tǒng)會調(diào)用 writeObject 寫入流中歹鱼,在網(wǎng)絡客戶端反序列化的readObject 時,會重新賦值到新對象的 elementData 中掺涛。為什么多此一舉鸽照?因為 elementData 容量經(jīng)常會大于實際存儲元素的數(shù)量矮燎,所以只需發(fā)送真正有實際值的數(shù)組元素即可诞外≡制保回到剛才的場景刊苍,當入?yún)?shù)組客量小于集合大小時正什,使用Amsys.copy0f()方法婴氮,它的源碼如下
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
// 新創(chuàng)建一個數(shù)組 copy
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
如果數(shù)組初始大小設置不當主经,不僅會降低性能罩驻,還會浪費空間鉴腻。使用集合的toArray(T[] array)方法爽哎,轉(zhuǎn)換為數(shù)組時,注意需要傳入類型完全一樣的數(shù)組厨内,并且的容量大小為 list.size()雏胃。
4. 集合與泛型
??泛型與集合的聯(lián)合使用瞭亮,可以把泛型的功能發(fā)揮到極致统翩,很多小伙伴不清楚List厂汗、List<Object>娶桦、List<?> 三者的區(qū)別衷畦,更加不能區(qū)分<? extends T> 與<? super T>的使用場景祈争。List 完全沒有類型限制和賦值限定,如果天馬行空地亂用,遲早會遭類型轉(zhuǎn)換失敗的異常墨吓。很多程序員覺得 List<Object> 的用法完全等同于 List帖烘,但在接受其他泛型賦值時會編譯出錯秘症。List<?> 是一個泛型乡摹,在沒有賦值之前聪廉,表示它可以接受任何類型的集合賦值板熊,賦值之后就不能便往里添加元素了。下方的例子很好活明了三者的區(qū)別干签,以List為原型展開說明:
public class ListNoGeneric {
public static void main(String[] args) {
// 第一段:泛型出現(xiàn)之前的集合定義方式
List a1 = new ArrayList();
a1.add(new Object());
a1.add(new Integer(111));
a1.add(new String("hello alal"));
//第二段:把a1引用賦值給 a2津辩,注意 a2與al的區(qū)別是增加了泛型原制<opject>
List<Object> a2 = a1;
a2.add(new Object());
a2.add(new Integer(222));
a2.add(new String("hello a2a2"));
//第三段:把al引用賦值給 a3,注意a3與a1的區(qū)別是增加了泛型<Integer>
List<Integer> a3 = a1;
a3.add(new Integer(333));
//下方兩行編譯出錯容劳,不允許增加非 Integer 類型進入集合
a3.add(new Object());
a3.add(new String("hello a3a3"));
// 第四段:把a1 引用賦值給 a4喘沿,a1 與a4的區(qū)別是增加了通配符
List<?> a4 = a1;
// 允許刪除和清除元素
a1.remove(0);
a4.clear();
// 編譯出錯。不允許增加任何元素
a4.add(new Object());
}
}
??第一段說明:在定義 List 之后鸭蛙,毫不猶豫地往集合里裝入三種不同的對象:Object摹恨、Integer 和 String娶视,遍歷沒有問題晒哄,但是貿(mào)然以為里邊的元素都是 Integer,使用強制轉(zhuǎn)化肪获,則拋出 ClassCastException 異常寝凌。
??第二段說明:把 a1 賦值給 a2,a2 是 List<Objec> 類型的孝赫,也可以再往里裝入三種不同的對象较木。很多程序員認為 List 和 List<Object> 是完全相同的,至少從目前這兩段來看是這樣的青柄。
??第三段說明:由于泛型在JDK5 之后才出現(xiàn)伐债,考慮到向前兼客,因此歷史代碼有時需要賦值給新泛型代碼致开,從編譯器角度是允許的峰锁。這種代碼似乎有點反人類,在實際故障案例中經(jīng)常出現(xiàn)双戳,來看一段問題代碼虹蒋。
JsoNobject jsonobject = JSoNobject.fromobject ("(\"level\":[\"3 \"])"):
List<Integer> intList= new ArrayList<Integer>(10);
if (jsonObject != nul1) {
intList.addAll(jsonObject.getJSONArray("level"));
int amount=0;
for (Integer t : intList) (
//拋出classCastException異帶 : string cannot be cast to Integer
if (condition) {
amount = amount + t;
}
}
}
addAll的定義如下:
public boolean addAll(Collection<? extends E> c) {...]
進行了泛型限制,示例中addAll的實際參數(shù)是getJSONArray 返回的JSONArray對象飒货,它并非是List魄衅,更加不是Integer集合的子類,為何編譯不報錯?查看JSONArray 的定義:
public final class JSONArray extends AbstractJSON implements JSON, List {}
??JSONArray 實現(xiàn)了 List塘辅,是非泛型集合晃虫,可以賦值給任何泛型限制的集合。編譯可以通過莫辨,但在運行時報錯傲茄,這是一個隱藏得比較深的Bug毅访,最終導致發(fā)生線上故障。在JDK5 之后盘榨,應盡量使用泛型定義喻粹,以及使用類、集合草巡、參數(shù)等守呜。
??如果把al的定義從List a1修改為 List<Object>a1,那么第三段就會編譯出錯List<Objec> 賦值給 List<Integer> 是不允許的山憨,若是反過來賦值:
List<Integer> intList = new ArrayList<Integer>(3);
intList.add(111);
List<Object> objectlist = intList;
??事實上查乒,依然會編譯出錯,提示如下:
Error:(10, 26) java: incompatible types: java.util.List<java.lang.Integer> cannot be converted tojava.util.List<java.lang.Object>
??注意郁竟,數(shù)組可以這樣賦值玛迄,因為它是協(xié)變的,而集合不是棚亩。
??第四段說明:間號在正則表達式中可以匹配任何字符蓖议,List<?>稱為通配待集合可以接受任何類型的集合引用賦值,不能添加任何元素讥蟆,但可以remove和clear,并非 immutable 集合勒虾。List<?>一般作為參數(shù)來接收外部的集合,或者返回一個不知具體元素類型的集合瘸彤。
??List<T>最大的問題是只能放置一種類型修然,如果隨意轉(zhuǎn)換類型的話,就是“破窗像論”质况,泛型就失去了類型安全的意義愕宋。如果需要放置多種受泛型約束的類型呢?JDK 的開發(fā)者順應了民意,實現(xiàn)了 <? extends T>與<? super>兩種語法结榄,但是兩的區(qū)別非常微妙掏婶。簡單來說,<?extends T>是 Get First潭陪,適用于,消費集合元素為主的場景,<?super T>是 Put First最蕾,適用于依溯,生產(chǎn)集合元素為主的場景。
??<? extends T>可以賦值給任何T及T子類的集合瘟则,上界為T黎炉,取出來的類型帶有泛型限制,向上強制轉(zhuǎn)型為 T醋拧。null 可以表示任何類型慷嗜,所以除 ull外淀弹,任何元素都不得添加進<?extends T>集合內(nèi)。
??<? super T>可以賦值給任何T及T的父類集合庆械,下界為 T薇溃。在生活中,投票選舉類似于<?super T>的操作缭乘。選舉代表時沐序,你只能往里投選票,取數(shù)據(jù)時堕绩,根本不知道是誰的票策幼,相當于泛型丟失。有人說奴紧,這只是一種生活場景特姐,在系統(tǒng)設計中,很難有這樣的情形黍氮。再舉例說明一下唐含,我們在填寫對主管的年度評價時,提交后若想再次訪問之前的鏈接修改評價滤钱,就會被告之:“您已經(jīng)完成對主管的年度反饋觉壶,謝謝參與〖祝”extends的場景是put 功能受限铜靶,而 super的場景是get功能受限。
??下例中他炊,以加菲貓争剿、貓、動物為例痊末,說明 extends和super的詳細語法差異:
public class AnimalCatGarfield {
public static void main(String[] args) {
//第1段;聲明三個依次承的類的集合: Object>動物>貓>加菲貓
List<Animal> animal = new ArrayList<Animal>();
List<Cat> cat = new ArrayList<Cat>();
List<Garfield> garfield = new ArrayList<Garfield>();
animal.add(new Animal());
cat.add(new Cat());
garfield.add(new Garfield());
//第二段測試賦值操作
// 下行編譯出錯蚕苇。只能賦值 Cat 或 cat 子類的集合
List<? extends Cat> extendsCatFromAnimal = animal;
List<? super Cat> superCatFromAnimal = animal;
List<? extends Cat> extendsCatFromCat = cat;
List<? super Cat> superCatFromCat = cat;
List<? extends Cat> extendsCatFromGarfield = garfield;
//下行編譯出錯。只能制值Cat或Cat父類的集合
List<? super Cat> superCatFromGarfield = garfield;
//第3段:測試add 方法
// 下面三行中所有的<? extends T> 都無法進行add操作凿叠,編譯均出錯
extendsCatFromCat.add(new Animal());
extendsCatFromCat.add(new Cat());
extendsCatFromCat.add(new Garfield());
// 下行編譯出錯涩笤。只能添加 cat 或 Ca 子類的集合
superCatFromCat.add(new Animal());
superCatFromCat.add(new Cat());
superCatFromCat.add(new Garfield());
//第4段:測試get 方法
// 所有的 super 操作能夠返回元素,但是泛型丟失盒件,只能返回 object 對象
//以下extends 操作能夠返回元素
Object catExtends2 = extendsCatFromCat.get(0);
Cat catExtends1 = extendsCatFromCat.get(0);
// 下行編譯出錯蹬碧。雖然 Cat 集合從 Garfield 賦值而來,但類型擦除后炒刁,是不知道的
Garfield garfield1 = extendsCatFromGarfield.get(0);
}
}
??第1段恩沽,聲明三個泛型集合,可以理解為三個不同的籠子翔始,List<Anima>住的是動物(反正就是動物世界里的動物)罗心,List<Ca住的是貓(反正就是貓科動物)里伯,List<Garfield>住的是加菲貓(又懶又可愛的一種貓)。Garfield 繼承于Cat渤闷,而Ca繼承自Animal俐东。
??第2段享潜,以Cat 類為核心寨典,因為它有父類也有子類调鬓。定義類型限定集合,分別為 List<? extends Cat>和List<? super Cat>补憾。在理解這兩個概念時漫萄,暫時不要引入上界和下界,專注于代碼本身就好盈匾。
??把 List<Cat> 對象賦值給兩者都是可以的腾务。但是把 List<Animal> 賦值給 List<? extends Cat> 時會編譯出錯,因為能賦值給 <? extend Cat> 的類型削饵,只有 Cat 自己和它的子類集合岩瘦。盡管它是類型安全的,但依然有泛型信息窿撬,因而從籠子里取出來的必然是只貓,而List<Animal>里邊有可能住著毒蛇启昧、鱷魚蝙蝠等其他動物。把 List<Garfield> 賦值給 List<? super Cat> 時劈伴,也會編譯報錯密末。因為能賦值給<?super Cat>的類型,只有 Cat自己和它的父類跛璧。
??第3段严里,所有的 List<?extends T>都會編譯出錯,無法進行add 操作追城,這是因為除 null外刹碾,任何元素都不能被添加進<? extends T> 集合內(nèi)。List<? super Cat> 可以往里增加元素座柱,但只能添加Cat 自身及子類對象迷帜,假如放入一塊石頭,則明顯違背了Animal大類的性質(zhì)色洞。
??第4段瞬矩,所有 List<? super T> 集合可以執(zhí)行 get操作,雖然能夠返回元素锋玲,但是類型丟失,即只能返回Object 對象涵叮。List<?extends Cat>可以返回帶類型的元素惭蹂,但只能返回 Cat 自身及其父類對象伞插,因為子類類型被擦除了。
??對于一個籠子盾碗,如果只是不斷地向外取動物而不向里放的話媚污,則屬于 Get First,應采用<?extends T>;相反,如果經(jīng)常向里放動物的話廷雅,則應采用<? super T>耗美,屬于Put First。