集合

一、集合框架,list,map,set都有哪些具體的實現類,區別都是什麼?

Java集合裏使用接口來定義功能,是一套完善的繼承體系。Iterator是全部集合的總接口,其餘全部接口都繼承於它,該接口定義了集合的遍歷操做,Collection接口繼承於Iterator,是集合的次級接口(Map獨立存在),定義了集合的一些通用操做。java

Java集合的類結構圖以下所示:算法

List:有序、可重複;索引查詢速度快;插入、刪除伴隨數據移動,速度慢;數據庫

Set:無序,不可重複;設計模式

Map:鍵值對,鍵惟一,值多個;數組

1.List,Set都是繼承自Collection接口,Map則不是;緩存

2.List特色:元素有放入順序,元素可重複;安全

Set特色:元素無放入順序,元素不可重複,重複元素會蓋掉,(注意:元素雖然無放入順序,可是元素在set中位置是由該元素的HashCode決定的,其位置實際上是固定,加入Set 的Object必須定義equals()方法;bash

另外list支持for循環,也就是經過下標來遍歷,也可使用迭代器,可是set只能用迭代,由於他無序,沒法用下標取得想要的值)。數據結構

3.Set和List對比:多線程

Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引發元素位置改變。

List:和數組相似,List能夠動態增加,查找元素效率高,插入刪除元素效率低,由於會引發其餘元素位置改變。

4.Map適合儲存鍵值對的數據。

5.線程安全集合類與非線程安全集合類

LinkedList、ArrayList、HashSet是非線程安全的,Vector是線程安全的;

HashMap是非線程安全的,HashTable是線程安全的;

StringBuilder是非線程安全的,StringBuffer是線程安的。

ArrayList與LinkedList的區別和適用場景

Arraylist:

優勢:ArrayList是實現了基於動態數組的數據結構,因地址連續,一旦數據存儲好了,查詢操做效率會比較高(在內存裏是連着放的)。

缺點:由於地址連續,ArrayList要移動數據,因此插入和刪除操做效率比較低。

LinkedList:

優勢:LinkedList基於鏈表的數據結構,地址是任意的,其在開闢內存空間的時候不須要等一個連續的地址,對新增和刪除操做add和remove,LinedList比較佔優點。LikedList 適用於要頭尾操做或插入指定位置的場景。

缺點:由於LinkedList要移動指針,因此查詢操做性能比較低。

適用場景分析:

當須要對數據進行對此訪問的狀況下選用ArrayList,當要對數據進行屢次增長刪除修改時採用LinkedList。

ArrayList和LinkedList怎麼動態擴容的嗎?

ArrayList:

ArrayList 初始化大小是 10 (若是你知道你的arrayList 會達到多少容量,能夠在初始化的時候就指定,能節省擴容的性能開支) 擴容點規則是,新增的時候發現容量不夠用了,就去擴容 擴容大小規則是,擴容後的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,擴容後的大小就是 10 + 5+1 = 16)

LinkedList:

linkedList 是一個雙向鏈表,沒有初始化大小,也沒有擴容的機制,就是一直在前面或者後面新增就好。

ArrayList與Vector的區別和適用場景

ArrayList有三個構造方法:

public ArrayList(intinitialCapacity)// 構造一個具備指定初始容量的空列表。   
public ArrayList()// 構造一個初始容量爲10的空列表。
public ArrayList(Collection<? extends E> c)// 構造一個包含指定 collection 的元素的列表  
複製代碼

Vector有四個構造方法:

public Vector() // 使用指定的初始容量和等於零的容量增量構造一個空向量。    
public Vector(int initialCapacity) // 構造一個空向量,使其內部數據數組的大小,其標準容量增量爲零。    
public Vector(Collection<? extends E> c)// 構造一個包含指定 collection 中的元素的向量  
public Vector(int initialCapacity, int capacityIncrement)// 使用指定的初始容量和容量增量構造一個空的向量
複製代碼

ArrayList和Vector都是用數組實現的,主要有這麼四個區別:

1)Vector是多線程安全的,線程安全就是說多線程訪問代碼,不會產生不肯定的結果。而ArrayList不是,這能夠從源碼中看出,Vector類中的方法不少有synchronied進行修飾,這樣就致使了Vector在效率上沒法與ArrayLst相比;

2)兩個都是採用的線性連續空間存儲元素,可是當空間充足的時候,兩個類的增長方式是不一樣。

3)Vector能夠設置增加因子,而ArrayList不能夠。

4)Vector是一種老的動態數組,是線程同步的,效率很低,通常不同意使用。

適用場景:

1.Vector是線程同步的,因此它也是線程安全的,而ArraList是線程異步的,是不安全的。若是不考慮到線程的安全因素,通常用ArrayList效率比較高。

2.若是集合中的元素的數目大於目前集合數組的長度時,Vector增加率爲目前數組長度的100%,而ArrayList增加率爲目前數組長度的50%,在集合中使用數據量比較大的數據,用Vector有必定的優點。

HashSet與TreeSet的區別和適用場景

1.TreeSet 是二叉樹(紅黑樹的樹據結構)實現的,Treest中的數據是自動排好序的,不容許放入null值。

2.HashSet 是哈希表實現的,HashSet中的數據是無序的能夠放入null,但只能放入一個null,二者中的值都不重複,就如數據庫中惟一約束。

3.HashSet要求放入的對象必須實現HashCode()方法,放的對象,是以hashcode碼做爲標識的,而具備相同內容的String對象,hashcode是同樣,因此放入的內容不能重複可是同一個類的對象能夠放入不一樣的實例。

適用場景分析:

HashSet是基於Hash算法實現的,其性能一般都優於TreeSet。爲快速查找而設計的Set,咱們一般都應該使用HashSet,在咱們須要排序的功能時,咱們才使用TreeSet。

HashMap與TreeMap、HashTable的區別及適用場景

HashMap 非線程安全

HashMap:基於哈希表(散列表)實現。使用HashMap要求的鍵類明肯定義了hashCode()和equals()[能夠重寫hasCode()和equals()],爲了優化HashMap空間的使用,您能夠調優初始容量和負載因子。其中散列表的衝突處理主分兩種,一種是開放定址法,另外一種是鏈表法。HashMap實現中採用的是鏈表法。

TreeMap:非線程安全基於紅黑樹實現。TreeMap沒有調優選項,由於該樹總處於平衡狀態。

適用場景分析:

HashMap和HashTable:HashMap去掉了HashTable的contain方法,可是加上了containsValue()和containsKey()方法。HashTable是同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap容許空鍵值,而HashTable不容許。

HashMap:適用於Map中插入、刪除和定位元素。

Treemap:適用於按天然順序或自定義順序遍歷鍵(key)。

二、set集合從原理上如何保證不重複?

1)在往set中添加元素時,若是指定元素不存在,則添加成功。

2)具體來說:當向HashSet中添加元素的時候,首先計算元素的hashcode值,而後用這個(元素的hashcode)%(HashMap集合的大小)+1計算出這個元素的存儲位置,若是這個位置爲空,就將元素添加進去;若是不爲空,則用equals方法比較元素是否相等,相等就不添加,不然找一個空位添加。

三、HashMap和HashTable的主要區別是什麼?,二者底層實現的數據結構是什麼?

HashMap和HashTable的區別:

兩者都實現了Map 接口,是將惟一的鍵映射到特定的值上,主要區別在於:

1)HashMap 沒有排序,容許一個null 鍵和多個null 值,而Hashtable 不容許;

2)HashMap 把Hashtable 的contains 方法去掉了,改爲containsvalue 和containsKey, 由於contains 方法容易讓人引發誤解;

3)Hashtable 繼承自Dictionary 類,HashMap 是Java1.2 引進的Map 接口的實現;

4)Hashtable 的方法是Synchronized 的,而HashMap 不是,在多個線程訪問Hashtable 時,不須要本身爲它的方法實現同步,而HashMap 就必須爲之提供額外的同步。Hashtable 和HashMap 採用的hash/rehash 算法大體同樣,因此性能不會有很大的差別。

HashMap和HashTable的底層實現數據結構:

HashMap和Hashtable的底層實現都是數組 + 鏈表結構實現的(jdk8之前)

四、HashMap、ConcurrentHashMap、hash()相關原理解析?

hash()原理:

把任意長度的輸入,經過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來惟一的肯定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。

全部散列函數都有以下一個基本特性:根據同一散列函數計算出的散列值若是不一樣,那麼輸入值確定也不一樣。可是,根據同一散列函數計算出的散列值若是相同,輸入值不必定相同。

兩個不一樣的輸入值,根據同一散列函數計算出的散列值相同的現象叫作碰撞。

常見的Hash函數有如下幾個:

直接定址法:直接以關鍵字k或者k加上某個常數(k+c)做爲哈希地址。

數字分析法:提取關鍵字中取值比較均勻的數字做爲哈希地址。

除留餘數法:用關鍵字k除以某個不大於哈希表長度m的數p,將所得餘數做爲哈希表地址。

分段疊加法:按照哈希表地址位數將關鍵字分紅位數相等的幾部分,其中最後一部分能夠比較短。而後將這幾部分相加,捨棄最高進位後的結果就是該關鍵字的哈希地址。

平方取中法:若是關鍵字各個部分分佈都不均勻的話,能夠先求出它的平方值,而後按照需求取中間的幾位做爲哈希地址。

僞隨機數法:採用一個僞隨機數看成哈希函數。

上面介紹過碰撞。衡量一個哈希函數的好壞的重要指標就是發生碰撞的機率以及發生碰撞的解決方案。任何哈希函數基本都沒法完全避免碰撞,常見的解決碰撞的方法有如下幾種:

  • 開放定址法:
    • 開放定址法就是一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
  • 鏈地址法
    • 將哈希表的每一個單元做爲鏈表的頭結點,全部哈希地址爲i的元素構成一個同義詞鏈表。即發生衝突時就把該關鍵字鏈在以該單元爲頭結點的鏈表的尾部。
  • 再哈希法
    • 當哈希地址發生衝突用其餘的函數計算另外一個哈希函數地址,直到衝突再也不產生爲止。
  • 創建公共溢出區
    • 將哈希表分爲基本表和溢出表兩部分,發生衝突的元素都放入溢出表中。

HashMap 的數據結構

在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。數組的特色是:尋址容易,插入和刪除困難;而鏈表的特色是:尋址困難,插入和刪除容易。上面咱們提到過,經常使用的哈希函數的衝突解決辦法中有一種方法叫作鏈地址法,其實就是將數組和鏈表組合在一塊兒,發揮了二者的優點,咱們能夠將其理解爲鏈表的數組。

咱們能夠從上圖看到,左邊很明顯是個數組,數組的每一個成員是一個鏈表。該數據結構所容納的全部元素均包含一個指針,用於元素間的連接。咱們根據元素的自身特徵把元素分配到不一樣的鏈表中去,反過來咱們也正是經過這些特徵找到正確的鏈表,再從鏈表中找出正確的元素。其中,根據元素特徵計算元素數組下標的方法就是哈希算法,即本文的主角hash()函數(固然,還包括indexOf()函數)。

hash方法 咱們拿JDK 1.7的HashMap爲例,其中定義了一個final int hash(Object k) 方法,其主要被如下方法引用。

上面的方法主要都是增長和刪除方法,這不難理解,當咱們要對一個鏈表數組中的某個元素進行增刪的時候,首先要知道他應該保存在這個鏈表數組中的哪一個位置,即他在這個數組中的下標。而hash()方法的功能就是根據Key來定位其在HashMap中的位置。HashTable、ConcurrentHashMap同理。

hash方法的功能是根據Key來定位這個K-V在鏈表數組中的位置的。也就是hash方法的輸入應該是個Object類型的Key,輸出應該是個int類型的數組下標。

調用Object對象的hashCode()方法,該方法會返回一個整數,而後用這個數對HashMap或者HashTable的容量進行取模就好了。沒錯,其實基本原理就是這個,只不過,在具體實現上,由兩個方法int hash(Object k)和int indexFor(int h, int length)來實現。可是考慮到效率等問題,HashMap的實現會稍微複雜一點。

hash :該方法主要是將Object轉換成一個整型。

indexFor :該方法主要是將hash生成的整型轉換成鏈表數組中的下標。

HashMap In Java 7

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

static int indexFor(int h, int length) {
    return h & (length-1);
}
複製代碼

indexFor方法其實主要是將hash生成的整型轉換成鏈表數組中的下標。那麼return h & (length-1);是什麼意思呢?其實,他就是取模。Java之全部使用位運算(&)來代替取模運算(%),最主要的考慮就是效率。位運算(&)效率要比代替取模運算(%)高不少,主要緣由是位運算直接對內存數據進行操做,不須要轉成十進制,所以處理速度很是快。

爲何可使用位運算(&)來實現取模運算(%)呢?這實現的原理以下:

X % 2^n = X & (2^n – 1)

2^n表示2的n次方,也就是說,一個數對2^n取模 == 一個數和(2^n – 1)作按位與運算 。

假設n爲3,則2^3 = 8,表示成2進制就是1000。2^3 = 7 ,即0111。

此時X & (2^3 – 1) 就至關於取X的2進制的最後三位數。

從2進制角度來看,X / 8至關於 X >> 3,即把X右移3位,此時獲得了X / 8的商,而被移掉的部分(後三位),則是X % 8,也就是餘數。
複製代碼

因此,return h & (length-1);只要保證length的長度是2^n的話,就能夠實現取模運算了。而HashMap中的length也確實是2的倍數,初始值是16,以後每次擴充爲原來的2倍。

HashMap的數據是存儲在鏈表數組裏面的。在對HashMap進行插入/刪除等操做時,都須要根據K-V對的鍵值定位到他應該保存在數組的哪一個下標中。而這個經過鍵值求取下標的操做就叫作哈希。HashMap的數組是有長度的,Java中規定這個長度只能是2的倍數,初始值爲16。簡單的作法是先求取出鍵值的hashcode,而後在將hashcode獲得的int值對數組長度進行取模。爲了考慮性能,Java總採用按位與操做實現取模操做。

兩個不一樣的鍵值,在對數組長度進行按位與運算後獲得的結果相同,這不就發生了衝突嗎。來看下Java是如何作的。

其中的主要代碼部分以下:

h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
複製代碼

這段代碼是爲了對key的hashCode進行擾動計算,防止不一樣hashCode的高位不一樣但低位相同致使的hash衝突。簡單點說,就是爲了把高位的特徵和低位的特徵組合起來,下降哈希衝突的機率,也就是說,儘可能作到任何一位的變化都能對最終獲得的結果產生影響。

其實,使用位運算代替取模運算,除了性能以外,還有一個好處就是能夠很好的解決負數的問題。由於咱們知道,hashcode的結果是int類型,而int的取值範圍是-2^31 ~ 2^31 – 1,即[ -2147483648, 2147483647];這裏面是包含負數的,咱們知道,對於一個負數取模仍是有些麻煩的。若是使用二進制的位運算的話就能夠很好的避免這個問題。首先,無論hashcode的值是正數仍是負數。length-1這個值必定是個正數。那麼,他的二進制的第一位必定是0(有符號數用最高位做爲符號位,「0」表明「+」,「1」表明「-」),這樣裏兩個數作按位與運算以後,第一位必定是個0,也就是,獲得的結果必定是個正數。

HashTable In Java 7

private int hash(Object k) {
    // hashSeed will be zero if alternative hashing is disabled.
    return hashSeed ^ k.hashCode();
}
複製代碼

至關於只是對k作了個簡單的hash,取了一下其hashCode。而HashTable中也沒有indexOf方法,取而代之的是這段代碼:int index = (hash & 0x7FFFFFFF) % tab.length;。也就是說,HashMap和HashTable對於計算數組下標這件事,採用了兩種方法。HashMap採用的是位運算,而HashTable採用的是直接取模。

爲啥要把hash值和0x7FFFFFFF作一次按位與操做呢,主要是爲了保證獲得的index的第一位爲0,也就是爲了獲得一個正數。由於有符號數第一位0表明正數,1表明負數。

HashTable默認的初始大小爲11,以後每次擴充爲原來的2n+1。

也就是說,HashTable的鏈表數組的默認大小是一個素數、奇數。以後的每次擴充結果也都是奇數。

因爲HashTable會盡可能使用素數、奇數做爲容量的大小。當哈希表的大小爲素數時,簡單的取模哈希的結果會更加均勻。

  • HashMap默認的初始化大小爲16,以後每次擴充爲原來的2倍。
  • HashTable默認的初始大小爲11,以後每次擴充爲原來的2n+1。
  • 當哈希表的大小爲素數時,簡單的取模哈希的結果會更加均勻,因此單從這一點上看,HashTable的哈希表大小選擇,彷佛更高明些。由於hash結果越分散效果越好。
  • 在取模計算時,若是模數是2的冪,那麼咱們能夠直接使用位運算來獲得結果,效率要大大高於作除法。因此從hash計算的效率上,又是HashMap更勝一籌。
  • 可是,HashMap爲了提升效率使用位運算代替哈希,這又引入了哈希分佈不均勻的問題,因此HashMap爲解決這問題,又對hash算法作了一些改進,進行了擾動計算。

ConcurrentHashMap In Java 7

private int hash(Object k) {
    int h = hashSeed;

    if ((0 != h) && (k instanceof String)) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // Spread bits to regularize both segment and index locations,
    // using variant of single-word Wang/Jenkins hash.
    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

int j = (hash >>> segmentShift) & segmentMask;
複製代碼

上面這段關於ConcurrentHashMap的hash實現其實和HashMap一模一樣。都是經過位運算代替取模,而後再對hashcode進行擾動。區別在於,ConcurrentHashMap 使用了一種變種的Wang/Jenkins 哈希算法,其主要母的也是爲了把高位和低位組合在一塊兒,避免發生衝突。

HashMap In Java 8

在Java 8 以前,HashMap和其餘基於map的類都是經過鏈地址法解決衝突,它們使用單向鏈表來存儲相同索引值的元素。在最壞的狀況下,這種方式會將HashMap的get方法的性能從O(1)下降到O(n)。爲了解決在頻繁衝突時hashmap性能下降的問題,Java 8中使用平衡樹來替代鏈表存儲衝突的元素。這意味着咱們能夠將最壞狀況下的性能從O(n)提升到O(logn)。

關於Java 8中的hash函數,原理和Java 7中基本相似。Java 8中這一步作了優化,只作一次16位右位移異或混合,而不是四次,但原理是不變的。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼

在JDK1.8的實現中,優化了高位運算的算法,經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的。以上方法獲得的int的hash值,而後再經過h & (table.length -1)來獲得該對象在數據中保存的位置。

HashTable In Java 8

在Java 8的HashTable中,已經不在有hash方法了。可是哈希的操做仍是在的,好比在put方法中就有以下實現:

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
複製代碼

ConcurrentHashMap In Java 8

Java 8 裏面的求hash的方法從hash改成了spread。實現方式以下:

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
複製代碼

Java 8的ConcurrentHashMap一樣是經過Key的哈希值與數組長度取模肯定該Key在數組中的索引。一樣爲了不不太好的Key的hashCode設計,它經過以下方法計算獲得Key的最終哈希值。不一樣的是,Java 8的ConcurrentHashMap做者認爲引入紅黑樹後,即便哈希衝突比較嚴重,尋址效率也足夠高,因此做者並未在哈希值的計算上作過多設計,只是將Key的hashCode值與其高16位做異或並保證最高位爲0(從而保證最終結果爲正整數)。

crossoverjie.top/2018/07/23/…

HashMap什麼時候擴容:

當向容器添加元素的時候,會判斷當前容器的元素個數,若是大於等於閾值---即大於當前數組的長度乘以加載因子的值的時候,就要自動擴容。

擴容的算法是什麼:

擴容(resize)就是從新計算容量,向HashMap對象裏不停的添加元素,而HashMap對象內部的數組沒法裝載更多的元素時,對象就須要擴大數組的長度,以便能裝入更多的元素。固然Java裏的數組是沒法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組。

五、ArrayMap跟SparseArray在HashMap上面的改進?

HashMap要存儲完這些數據將要不斷的擴容,並且在此過程當中也須要不斷的作hash運算,這將對咱們的內存空間形成很大消耗和浪費。

SparseArray比HashMap更省內存,在某些條件下性能更好,主要是由於它避免了對key的自動裝箱(int轉爲Integer類型),它內部則是經過兩個數組來進行數據存儲的,一個存儲key,另一個存儲value,爲了優化性能,它內部對數據還採起了壓縮的方式來表示稀疏數組的數據,從而節約內存空間,咱們從源碼中能夠看到key和value分別是用數組表示:

private int[] mKeys;
private Object[] mValues;
複製代碼

同時,SparseArray在存儲和讀取數據時候,使用的是二分查找法。也就是在put添加數據的時候,會使用二分查找法和以前的key比較當前咱們添加的元素的key的大小,而後按照從小到大的順序排列好,因此,SparseArray存儲的元素都是按元素的key值從小到大排列好的。 而在獲取數據的時候,也是使用二分查找法判斷元素的位置,因此,在獲取數據的時候很是快,比HashMap快的多。

ArrayMap利用兩個數組,mHashes用來保存每個key的hash值,mArrray大小爲mHashes的2倍,依次保存key和value。

mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
複製代碼

當插入時,根據key的hashcode()方法獲得hash值,計算出在mArrays的index位置,而後利用二分查找找到對應的位置進行插入,當出現哈希衝突時,會在index的相鄰位置插入。

假設數據量都在千級之內的狀況下:

一、若是key的類型已經肯定爲int類型,那麼使用SparseArray,由於它避免了自動裝箱的過程,若是key爲long類型,它還提供了一個LongSparseArray來確保key爲long類型時的使用

二、若是key類型爲其它的類型,則使用ArrayMap。

6.LinkedHashMap與HashMap的區別

LinkedHashMap類與HashMap很是類似。 可是,連接的哈希映射基於哈希表和鏈表,以加強哈希映射的功能。

除了默認大小爲16的基礎數組外,它還維護一個運行全部條目的雙向鏈表。

爲了維護元素的順序,連接HashMap經過添加指向下一個和前一個條目的指針來修改HashMap的Map.Entry類:

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
複製代碼

請注意,Entry類只添加了兩個指針; before和after使其可以將本身掛鉤到鏈表。 除此以外,它使用HashMap的Entry類實現。

最後,請記住,此連接列表定義了迭代的順序,默認狀況下是元素的插入順序(插入順序)。

@Test
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
    LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
 
    Set<Integer> keys = map.keySet();
    Integer[] arr = keys.toArray(new Integer[0]);
 
    for (int i = 0; i < arr.length; i++) {
        assertEquals(new Integer(i + 1), arr[i]);
    }
}
複製代碼

咱們能夠保證此測試將始終經過,由於始終會保持插入順序。 咱們沒法爲HashMap提供相同的保證。

若是客戶端須要在調用API以前以相同的方式對返回的映射進行排序,那麼連接的散列映射是可行的方法。

若是將鍵從新插入映射,插入順序不會受到影響。

LinkedHashMap提供了一個特殊的構造函數,使咱們可以在自定義加載因子(LF)和初始容量之間指定一個稱爲訪問順序的不一樣排序機制/策略:

LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, .75f, true);
複製代碼

第一個參數是初始容量,後跟負載因子,最後一個參數是排序模式。 所以,經過傳入true,咱們生成了訪問順序,而默認值是insert-order。

此機制確保元素的迭代順序是上次訪問元素的順序,從最近訪問到最近訪問。

所以,使用某種映射構建最近最少使用(LRU)緩存很是簡單實用。 成功的put或get操做會致使訪問該條目:

@Test
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
    LinkedHashMap<Integer, String> map 
      = new LinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
 
    Set<Integer> keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());
  
    map.get(4);
    assertEquals("[1, 2, 3, 5, 4]", keys.toString());
  
    map.get(1);
    assertEquals("[2, 3, 5, 4, 1]", keys.toString());
  
    map.get(3);
    assertEquals("[2, 5, 4, 1, 3]", keys.toString());
}
複製代碼

請注意,當咱們在映射上執行訪問操做時,如何轉換鍵集中元素的順序。

簡單地說,映射上的任何訪問操做都會產生一個順序,使得若是要當即執行迭代,則訪問的元素將顯示在最後。

在上面的例子以後,很明顯putAll操做爲指定映射中的每一個映射生成一個條目訪問。

固然,對映射視圖的迭代不會影響支持映射的迭代順序; 只有映射上的顯式訪問操做纔會影響排序。

LinkedHashMap還提供了一種機制,用於維護固定數量的映射,並在須要添加新映射時不斷刪除最舊的條目。

能夠重寫removeEldestEntry方法以強制執行此策略以自動刪除過期映射。

public class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
 
    private static final int MAX_ENTRIES = 5;
 
    public MyLinkedHashMap(
      int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }
 
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
}
複製代碼

咱們上面的覆蓋將容許映射增加到最大大小爲5個條目。 當大小超過該大小時,將插入每一個新條目,代價是丟失映射中的最舊條目,即最後訪問時間在全部其餘條目以前的條目:

@Test
public void givenLinkedHashMap_whenRemovesEldestEntry_thenCorrect() {
    LinkedHashMap<Integer, String> map
      = new MyLinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
    Set<Integer> keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());
  
    map.put(6, null);
    assertEquals("[2, 3, 4, 5, 6]", keys.toString());
  
    map.put(7, null);
    assertEquals("[3, 4, 5, 6, 7]", keys.toString());
  
    map.put(8, null);
    assertEquals("[4, 5, 6, 7, 8]", keys.toString());
}
複製代碼

就像HashMap同樣,只要哈希函數的尺寸合適,LinkedHashMap就會在常量時間內執行添加,刪除和包含的基本Map操做。 它還接受null鍵和null值。

可是,因爲維護雙向鏈表的額外開銷,LinkedHashMap的這種常量性能可能比HashMap的常量時間稍差。

迭代LinkedHashMap的集合視圖也須要線性時間O(n)相似於HashMap的線性時間。 另外一方面,LinkedHashMap在迭代期間的線性時間性能優於HashMap的線性時間。

這是由於,對於LinkedHashMap,O(n)中的n只是映射中的條目數,而與容量無關。 然而,對於HashMap,n是容量和大小總和,O(大小+容量)。

負載因子和初始容量與HashMap精肯定義。 但請注意,對於LinkedHashMap而言,爲初始容量選擇太高的值的懲罰不如HashMap嚴重,由於此類的迭代次數不受容量的影響。

就像HashMap同樣,LinkedHashMap實現不一樣步。 所以,若是要從多個線程訪問它,而且這些線程中至少有一個可能在結構上進行更改,那麼它必須在外部進行同步。

Map m = Collections.synchronizedMap(new LinkedHashMap());
複製代碼

與HashMap的不一樣之處在於須要進行結構修改。 在訪問順序連接的哈希映射中,僅調用get API會致使結構修改。 除此以外,還有像put和remove這樣的操做。

7.Iterator

JDK提供的迭代接口進行Java集合的迭代。

Iterator iterator = list.iterator();
while(iterator.hasNext()){
    iterator.next();
    //do something
}
複製代碼

迭代其實咱們能夠簡單地理解爲遍歷,是一個標準化遍歷各種容器裏面的全部對象的方法類,它是一個很典型的設計模式。Iterator模式是用於遍歷集合類的標準訪問方法。

它能夠把訪問邏輯從不一樣類型的集合類中抽象出來,從而避免向客戶端暴露集合的內部結構。 在沒有迭代器時咱們都是這麼進行處理的。以下:

int[] arrays = new int[10];
for(int i = 0 ; i < arrays.length ; i++){
   int a = arrays[i];
   //do something
}
複製代碼
List<String> list = new ArrayList<String>();
for(int i = 0 ; i < list.size() ;  i++){
  String string = list.get(i);
  //do something
}
複製代碼

對於這兩種方式,咱們老是都事先知道集合的內部結構,訪問代碼和集合自己是緊密耦合的,沒法將訪問邏輯從集合類和客戶端代碼中分離出來。同時每一種集合對應一種遍歷方法,客戶端代碼沒法複用。

在實際應用中如何須要將上面將兩個集合進行整合是至關麻煩的。因此爲了解決以上問題,Iterator模式騰空出世,它老是用同一種邏輯來遍歷集合。

使得客戶端自身不須要來維護集合的內部結構,全部的內部狀態都由Iterator來維護。客戶端從不直接和集合類打交道,它老是控制Iterator,向它發送」向前」,」向後」,」取當前元素」的命令,就能夠間接遍歷整個集合。

java.util.Iterator

在Java中Iterator爲一個接口,它只提供了迭代了基本規則,在JDK中他是這樣定義的:對 collection 進行迭代的迭代器。迭代器取代了 Java Collections Framework 中的 Enumeration。迭代器與枚舉有兩點不一樣:

一、迭代器容許調用者利用定義良好的語義在迭代期間從迭代器所指向的 collection 移除元素。

二、方法名稱獲得了改進。
複製代碼
public interface Iterator {
&emsp;&emsp;boolean hasNext();
&emsp;&emsp;Object next();
&emsp;&emsp;void remove();
}
複製代碼
Object next():返回迭代器剛越過的元素的引用,返回值是Object,須要強制轉換成本身須要的類型

boolean hasNext():判斷容器內是否還有可供訪問的元素

void remove():刪除迭代器剛越過的元素
複製代碼

對於咱們而言,咱們只通常只需使用next()、hasNext()兩個方法便可完成迭代。以下:

for(Iterator it = c.iterator(); it.hasNext(); ) {
&emsp;&emsp;Object o = it.next();
&emsp;&emsp; //do something
}
複製代碼

前面闡述了Iterator有一個很大的優勢,就是咱們沒必要知道集合的內部結果,集合的內部結構、狀態由Iterator來維持,經過統一的方法hasNext()、next()來判斷、獲取下一個元素,至於具體的內部實現咱們就不用關心了。

各個集合的Iterator的實現

ArrayList的Iterator實現

在ArrayList內部首先是定義一個內部類Itr,該內部類實現Iterator接口,以下:

private class Itr implements Iterator<E> {
    //do something
}

而ArrayList的iterator()方法實現:
public Iterator<E> iterator() {
    return new Itr();
}
複製代碼

在Itr內部定義了三個int型的變量:cursor、lastRet、expectedModCount。其中cursor表示下一個元素的索引位置,lastRet表示上一個元素的索引位置

int cursor;             
int lastRet = -1;     
int expectedModCount = modCount;
複製代碼
public boolean hasNext() {
    return cursor != size;
}
複製代碼
public E next() {
    checkForComodification();
    int i = cursor;    //記錄索引位置
    if (i >= size)    //若是獲取元素大於集合元素個數,則拋出異常
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;      //cursor + 1
    return (E) elementData[lastRet = i];  //lastRet + 1 且返回cursor處元素
}
複製代碼

checkForComodification()主要用來判斷集合的修改次數是否合法,即用來判斷遍歷過程當中集合是否被修改過。

。modCount用於記錄ArrayList集合的修改次數,初始化爲0,,每當集合被修改一次(結構上面的修改,內部update不算),如add、remove等方法,modCount + 1,因此若是modCount不變,則表示集合內容沒有被修改。

該機制主要是用於實現ArrayList集合的快速失敗機制,在Java的集合中,較大一部分集合是存在快速失敗機制的。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
對於remove()方法的是實現,它是調用ArrayList自己的remove()方法刪除lastRet位置元素,而後修改modCount便可。

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
複製代碼

FastFail機制

「快速失敗」也就是fail-fast,它是Java集合的一種錯誤檢測機制。當多個線程對集合進行結構上的改變的操做時,有可能會產生fail-fast機制。

記住是有可能,而不是必定。例如:假設存在兩個線程(線程一、線程2),線程1經過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會拋出 ConcurrentModificationException異常,從而產生fail-fast機制。

public class FailFastTest {
    private static List<Integer> list = new ArrayList<>();

    /**
     * @desc:線程one迭代list
     * @Project:test
     * @file:FailFastTest.java
     * @Authro:chenssy
     * @data:2014年7月26日
     */
    private static class threadOne extends Thread{
        public void run() {
            Iterator<Integer> iterator = list.iterator();
            while(iterator.hasNext()){
                int i = iterator.next();
                System.out.println("ThreadOne 遍歷:" + i);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * @desc:當i == 3時,修改list
     * @Project:test
     * @file:FailFastTest.java
     * @Authro:chenssy
     * @data:2014年7月26日
     */
    private static class threadTwo extends Thread{
        public void run(){
            int i = 0 ; 
            while(i < 6){
                System.out.println("ThreadTwo run:" + i);
                if(i == 3){
                    list.remove(i);
                }
                i++;
            }
        }
    }

    public static void main(String[] args) {
        for(int i = 0 ; i < 10;i++){
            list.add(i);
        }
        new threadOne().start();
        new threadTwo().start();
    }
}
複製代碼
ThreadOne 遍歷:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
ThreadTwo run:4
ThreadTwo run:5
Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util.ArrayList$Itr.next(Unknown Source)
    at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)
複製代碼

經過上面的示例和講解,我初步知道fail-fast產生的緣由就在於程序在對 collection 進行迭代時,某個線程對該 collection 在結構上對其作了修改,這時迭代器就會拋出 ConcurrentModificationException 異常信息,從而產生 fail-fast。

迭代器在調用next()、remove()方法時都是調用checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount ? 若不等則拋出ConcurrentModificationException 異常,從而產生fail-fast機制。

expectedModCount 是在Itr中定義的:int expectedModCount = ArrayList.this.modCount;因此他的值是不可能會修改的,因此會變的就是modCount。modCount是在 AbstractList 中定義的,爲全局變量:

protected transient int modCount = 0; 那麼他何時由於什麼緣由而發生改變呢?

ArrayList中不管add、remove、clear方法只要是涉及了改變ArrayList元素的個數的方法都會致使modCount的改變。

解決方案:

方案一:在遍歷過程當中全部涉及到改變modCount值得地方所有加上synchronized或者直接使用Collections.synchronizedList,這樣就能夠解決。可是不推薦,由於增刪形成的同步鎖可能會阻塞遍歷操做。

方案二:使用CopyOnWriteArrayList來替換ArrayList。推薦使用該方案。

CopyOnWriteArrayList爲什麼物?ArrayList 的一個線程安全的變體,其中全部可變操做(add、set 等等)都是經過對底層數組進行一次新的複製來實現的。 該類產生的開銷比較大,可是在兩種狀況下,它很是適合使用。

1:在不能或不想進行同步遍歷,但又須要從併發線程中排除衝突時。

2:當遍歷操做的數量大大超過可變操做的數量時。遇到這兩種狀況使用CopyOnWriteArrayList來替代ArrayList再適合不過了。那麼爲何CopyOnWriterArrayList能夠替代ArrayList呢?
複製代碼

第1、CopyOnWriterArrayList的不管是從數據結構、定義都和ArrayList同樣。它和ArrayList同樣,一樣是實現List接口,底層使用數組實現。在方法上也包含add、remove、clear、iterator等方法。

第2、CopyOnWriterArrayList根本就不會產生ConcurrentModificationException異常,也就是它使用迭代器徹底不會產生fail-fast機制。請看:

private static class COWIterator implements ListIterator {
    /* 省略此處代碼 /
    public E next() {
        if (!(hasNext()))
            throw new NoSuchElementException();
        return this.snapshot[(this.cursor++)];
    }

    /** 省略此處代碼 */
}
複製代碼

CopyOnWriterArrayList的方法根本就沒有像ArrayList中使用checkForComodification方法來判斷expectedModCount 與 modCount 是否相等。它爲何會這麼作,憑什麼能夠這麼作呢?咱們以add方法爲例:

public boolean add(E paramE) {
    ReentrantLock localReentrantLock = this.lock;
    localReentrantLock.lock();
    try {
        Object[] arrayOfObject1 = getArray();
        int i = arrayOfObject1.length;
        Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
        arrayOfObject2[i] = paramE;
        setArray(arrayOfObject2);
        int j = 1;
        return j;
    } finally {
        localReentrantLock.unlock();
    }
}

final void setArray(Object[] paramArrayOfObject) {
    this.array = paramArrayOfObject;
}
複製代碼

CopyOnWriterArrayList的add方法與ArrayList的add方法有一個最大的不一樣點就在於,下面三句代碼:

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);
複製代碼

就是這三句代碼使得CopyOnWriterArrayList不會拋ConcurrentModificationException異常。他們所展示的魅力就在於copy原來的array,再在copy數組上進行add操做,這樣作就徹底不會影響COWIterator中的array了。

因此CopyOnWriterArrayList所表明的核心概念就是:任何對array在結構上有所改變的操做(add、remove、clear等),CopyOnWriterArrayList都會copy現有的數據,再在copy的數據上修改,這樣就不會影響COWIterator中的數據了,修改完成以後改變原有數據的引用便可。同時這樣形成的代價就是產生大量的對象,同時數組的copy也是至關有損耗的。

Collections類

java中的Collections類是一個有用的實用程序類,用於處理java中的集合。 java.util.Collections類直接擴展Object類,而且只包含對Collections進行操做或返回它們的靜態方法。

Collections類包含對集合和「包裝器」進行操做的多態算法 - 它返回由指定集合支持的新集合。

Collections類包含3個字段:EMPTY_LIST,EMPTY_SET,EMPTY_MAP,它們可分別用於獲取不可變的空List,Map和Set。

boolean addAll(Collection c, T... elements)此方法一次將全部提供的元素添加到指定的集合。 元素能夠以逗號分隔的列表的形式提供。

List fruits = new ArrayList();
Collections.addAll(fruits, "Apples", "Oranges", "Banana");
fruits.forEach(System.out::println);
複製代碼
Apples
Oranges
Banana
複製代碼

void sort(List list, Comparator c)此方法根據天然順序對提供的列表進行排序。 若是咱們想要一些自定義排序,咱們也能夠傳入Comparator。

Collections.sort(fruits);
System.out.println("Sorted according to natural ordering:");
fruits.forEach(System.out::println);

Collections.sort(fruits, Comparator.reverseOrder());
System.out.println("Sorted according to reverse of natural ordering:");
fruits.forEach(System.out::println);
複製代碼
Sorted according to natural ordering:
Apples
Banana
Oranges
Sorted according to reverse of natural ordering:
Oranges
Banana
Apples
複製代碼

Queue asLifoQueue(Deque deque) 此方法返回Deque視圖做爲後進先出(Lifo)隊列。添加和刪除方法分別映射到push,pop等。 當咱們想要使用須要Queue的方法但咱們須要Lifo排序時,這可能頗有用。

Deque deque = new LinkedList();
deque.addFirst("Apples");
deque.add("Oranges");
deque.addLast("Bananas");

Queue queue = Collections.asLifoQueue(deque);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
複製代碼
Apples
Oranges
Banana
複製代碼

int binarySearch(List<? extends Comparable> list, T key) 此方法使用指定列表中的二進制搜索來搜索關鍵字。 在調用此方法以前,列表應按天然順序排序,不然,結果將是未定義的。 若是找到元素,它返回排序列表中元素的索引,在其餘狀況下,它返回( - (插入點)-1)。 其中,插入點定義爲關鍵字將插入列表的點,即第一個元素的索引大於關鍵字,或者list.size(),若是列表中的全部元素都小於指定的 關鍵字。 請注意,若是找到關鍵字,則能夠保證返回值> = 0。

Collections.sort(fruits);
System.out.println(Collections.binarySearch(fruits, "Banana"));
System.out.println(Collections.binarySearch(fruits, "Grapes"));
複製代碼
1
-3
複製代碼

咱們還能夠傳入一個Comparator,它表示列表按指定比較器引起的順序排序。

Collection checkedCollection(Collection c, Class type) 此方法提供所提供集合的動態類型安全視圖。 注意集合是頗有用的,任何錯誤輸入的元素都不會插入其中。

List list = new ArrayList();
Collections.addAll(list, "one", "two", "three", "four");
Collection checkedList = Collections.checkedCollection(list, String.class);
System.out.println("Checked list content: " + checkedList);
//we can add any type of element to list
list.add(10);
//we cannot add any type of elements to chkList, doing so
//throws ClassCastException
checkedList.add(10); 
複製代碼

一樣,咱們有特定集合的檢查方法,如List,Map,Set等。

void copy(List dest, List src)此方法將全部元素從源列表複製到目標列表。 執行此操做後,目標列表中每一個複製元素的索引將與源列表中的索引相同。 在前面的方法示例中,咱們建立了一個包含5個元素的列表。 讓咱們將「fruits」列表複製到此列表中,看看會發生什麼:

Collections.copy(list, fruits);
list.forEach(System.out::println);
複製代碼
Oranges
Banana
Apples
four
10
複製代碼

目標列表必須至少與源列表同樣長。 在咱們的示例中,它比源列表長,所以在這種狀況下,目標列表中的其他元素不受影響(此處爲「four」和「10」)。

boolean disjoint(Collection c1, Collection c2) 若是兩個指定的集合沒有共同的元素,則此方法返回true。 在前面的例子中,咱們將水果複製到列表中,因此如今它們不是不相交的。 因此,當咱們執行時:

System.out.println(Collections.disjoint(list, fruits));
複製代碼
false
複製代碼

讓咱們爲「vegetables」建立另外一個列表,並檢查它是否與「fruits」不相交。

List vegetables = new ArrayList();
Collections.addAll(vegetables, "Potato", "Cabbage");
System.out.println(Collections.disjoint(vegetables, fruits));
複製代碼
true
複製代碼

若是咱們在兩個參數中傳遞相同的集合,咱們會獲得false,除非它們是空的:

System.out.println(Collections.disjoint(vegetables, vegetables));
System.out.println(Collections.disjoint(new ArrayList(), new ArrayList()));
複製代碼
false
true
複製代碼

void fill(List list, T obj)此方法用指定的元素替換指定列表的全部元素。 若是咱們填寫「list」列表,它的全部五個元素都將被替換:

Collections.fill(list, "filled with dummy data");
list.forEach(System.out::println);
複製代碼
filled with dummy data
filled with dummy data
filled with dummy data
filled with dummy data
filled with dummy data
複製代碼

int frequency(Collection c, Object o) 此方法返回指定集合中等於指定對象的元素數。

System.out.println(Collections.frequency(list, "filled with dummy data"));
複製代碼
5
複製代碼

int indexOfSubList(List source, List target) 此方法返回指定源列表中指定目標列表第一次出現的起始位置,若是不存在,則返回-1。

List fruitsSubList1 = new ArrayList();
Collections.addAll(fruitsSubList1, "Oranges", "Banana");
System.out.println(Collections.indexOfSubList(fruits, fruitsSubList1));
複製代碼
1
複製代碼

由於該子列表從fruits的索引1開始。 如今,若是咱們嘗試對另外一個不存在的子列表執行相同操做:

List fruitsSubList2 = new ArrayList();
Collections.addAll(fruitsSubList2, "Kiwi", "Pinapple");
System.out.println(Collections.indexOfSubList(fruits, fruitsSubList2));
複製代碼
-1
複製代碼

還要注意,若是子列表的大小>列表的大小,咱們獲得-1。 咱們有另外一個方法int lastIndexOfSubList(List source,List target),它只返回指定子列表的最後一個索引,不然產生與此一個相同的輸出。

static ArrayList list(Enumeration e)Enumeration enumeration(Collection c)這些方法分別從枚舉返回一個列表和從列表返回枚舉,以便在返回枚舉的舊API和須要集合的新API之間提供互操做性。

T max(Collection coll, Comparator comp) 此方法根據元素的天然順序返回集合中的最大元素。

System.out.println(Collections.max(fruits));
複製代碼
Oranges
複製代碼

若是咱們想要自定義排序,咱們也能夠在此方法中傳遞Comparator。 相似地,也可使用min方法,它也能夠與Comparator一塊兒使用或不與Comparator一塊兒使用。

Collection<T> synchronizedCollection(Collection<T> c) 此方法返回由提供的集合支持的同步(線程安全)集合。 在須要時從任何集合對象獲取同步集合很方便。 API還爲咱們提供了synchronizedList方法,該方法返回參數中由提供的列表支持的線程安全列表。

Collection<String> synchronizedCollection = 
  Collections.synchronizedCollection(fruits);
List<String> synchronizedList = Collections.synchronizedList(fruits);
複製代碼

此外,還有synchronizedMap,synchronizedSet,synchronizedSortedSet以及可用於執行相似工做的synchronizedSortedMap方法。

這些是經常使用的方法,除此以外咱們還有其餘方法,如newSetFromMap,replaceAll,swap,reverse等。

請注意,若是提供給它們的集合或類對象爲null,則此類的全部方法都會拋出NullPointerException。

相關文章
相關標籤/搜索