一、Java集合類框架的基本接口有哪些?二、Collection 和 Collections 有什麼區別?三、爲何集合類接口沒有實現 Cloneable 和 Serializable 接口?四、List、Set、Map 之間的區別是什麼?五、什麼是迭代器(Iterator)?六、Iterator 和 ListIterator 的區別是什麼?七、快速失敗(fail-fast)和安全失敗(fail-safe)的區別是什麼?八、HashMap 和 Hashtable 有什麼區別?九、ConcurrentHashMap 和 Hashtable 的區別十、ConcurrentHashMap 線程安全的具體實現方式/底層具體實現十一、Java 中的 HashMap 的工做原理是什麼?十二、hashmap 大小爲何是 2 的冪次方1三、hashCode()和 equals()方法的重要性體如今什麼地方?1四、數組(Array)和列表(ArrayList)有什麼區別?何時應該使用 Array 而不是 ArrayList?1五、ArrayList 和 LinkedList 有什麼區別?1六、Comparable 和 Comparator 接口是幹什麼的?列出它們的區別。1七、什麼是 Java 優先級隊列(Priority Queue)?1八、Java集合類框架的最佳實踐有哪些?1九、HashSet 和 TreeSet 有什麼區別?20、HashSet 的實現原理?如何實現數組和 List 之間的轉換?2一、Arrays.asList()使用指南Collection.toArray()方法使用的坑&如何反轉數組2二、ArrayList 和 Vector 的區別是什麼?2三、在 Queue 中 poll()和 remove()有什麼區別?php
總共有兩大接口:Collection 和 Map ,一個元素集合,一個是鍵值對集合; 其中 List 和 Set 接口繼承了 Collection 接口,一個是有序元素集合,一個是無序元素集合; 而 ArrayList 和 LinkedList 實現了 List 接口,HashSet 實現了 Set 接口,這幾個都比較經常使用; HashMap 和 HashTable 實現了 Map 接口,而且 HashTable 是線程安全的,可是 HashMap 性能更好;html
java.util.Collection 是一個集合接口(集合類的一個頂級接口)。它提供了對集合對象進行基本操做的通用接口方法。Collection 接口在 Java 類庫中有不少具體的實現。Collection 接口的意義是爲各類具體的集合提供了最大化的統一操做方式,其直接繼承接口有 List 與 Set。java
Collections 則是集合類的一個工具類/幫助類,其中提供了一系列靜態方法,用於對集合中元素進行排序、搜索以及線程安全等各類操做。git
克隆(cloning)或者是序列化(serialization)的語義和含義是跟具體的實現相關的。所以,應該由集合類的具體實現來決定如何被克隆或者是序列化。github
迭代器是一種設計模式,它是一個對象,它能夠遍歷並選擇序列中的對象,而開發人員不須要了解該序列的底層結構。迭代器一般被稱爲「輕量級」對象,由於建立它的代價小。web
Java 中的 Iterator 功能比較簡單,而且只能單向移動:設計模式
(1) 使用方法 iterator()要求容器返回一個 Iterator。第一次調用 Iterator 的 next()方法時,它返回序列的第一個元素。注意:iterator()方法是 java.lang.Iterable 接口,被 Collection 繼承。數組
(2) 使用 next()得到序列中的下一個元素。安全
(3) 使用 hasNext()檢查序列中是否還有元素。數據結構
(4) 使用 remove()將迭代器新返回的元素刪除。
Iterator 是 Java 迭代器最簡單的實現,爲 List 設計的 ListIterator 具備更多的功能,它能夠從兩個方向遍歷 List,也能夠從 List 中插入和刪除元素。
Iterator 可用來遍歷 Set 和 List 集合,可是 ListIterator 只能用來遍歷 List。
Iterator 對集合只能是前向遍歷,ListIterator 既能夠前向也能夠後向。
ListIterator 實現了 Iterator 接口,幷包含其餘的功能,好比:增長元素,替換元素,獲取前一個和後一個元素的索引等等。
快速失敗:當你在迭代一個集合的時候,若是有另外一個線程正在修改你正在訪問的那個集合時,就會拋出一個 ConcurrentModification 異常。
在 java.util 包下的都是快速失敗,不能在多線程下發生併發修改(迭代過程當中被修改)。
安全失敗:你在迭代的時候會去底層集合作一個拷貝,因此你在修改上層集合的時候是不會受影響的,不會拋出 ConcurrentModification 異常。
在 java.util.concurrent 包下的全是安全失敗的。能夠在多線程下併發使用,併發修改。
一、HashMap 是非線程安全的,HashTable 是線程安全的。
二、HashMap 的鍵和值都容許有 null 值存在,而 HashTable 則不行。
三、由於線程安全的問題,HashMap 效率比 HashTable 的要高。
四、Hashtable 是同步的,而 HashMap 不是。所以,HashMap 更適合於單線程環境,而 Hashtable 適合於多線程環境。
五、二者提供了可供應用迭代的鍵的集合,都是快速失敗的。此外 Hashtable 提供了對鍵的列舉(Enumeration)
六、初始容量大小和每次擴充容量大小的不一樣: ①建立時若是不指定容量初始值,Hashtable 默認的初始大小爲 11,以後每次擴充,容量變爲原來的 2n+1。HashMap 默認的初始化大小爲 16。以後每次擴充,容量變爲原來的 2 倍。②建立時若是給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充爲 2 的冪次方大小(HashMap 中的 tableSizeFor()方法保證,下面給出了源代碼)。也就是說 HashMap 老是使用 2 的冪做爲哈希表的大小。
通常如今不建議用 HashTable, ①是 HashTable 是遺留類,內部實現不少沒優化和冗餘。②即便在多線程環境下,如今也有同步的 ConcurrentHashMap 替代,沒有必要由於是多線程而用 HashTable。
HashMap 中帶有初始容量的構造函數:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製代碼
下面這個方法保證了 HashMap 老是使用2的冪做爲哈希表的大小。
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼
二者的對比圖:
圖片來源:http://www.cnblogs.com/chengxiao/p/6842045.html
HashTable:
JDK1.7 的 ConcurrentHashMap:
JDK1.7(上面有示意圖)
首先將數據分爲一段一段的存儲,而後給每一段數據配一把鎖,當一個線程訪問其中一個段數據時,只是佔用當前數據段的鎖,其餘段的數據也能被其餘線程訪問。
ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。
Segment 實現了 ReentrantLock,因此 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數據。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
複製代碼
一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和 HashMap 相似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每一個 HashEntry 是一個鏈表結構的元素,每一個 Segment 守護着一個HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment的鎖。
JDK1.8 (上面有示意圖)
ConcurrentHashMap 取消了Segment 分段鎖,採用 CAS 和 synchronized 來保證併發安全。數據結構跟 HashMap1.8 的結構相似,數組+鏈表/紅黑二叉樹。Java 8 在鏈表長度超過必定閾值(8)時將鏈表(尋址時間複雜度爲O(N))轉換爲紅黑樹(尋址時間複雜度爲 O(log(N)))
synchronized 只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要 hash 不衝突,就不會產生併發,效率又提高 N 倍。
推薦閱讀:
hashmap 是一個 key-value 鍵值對的數據結構,從結構上來說在 jdk1.8 以前是用數組加鏈表的方式實現,jdk1.8 加了紅黑樹,hashmap 數組的默認初始長度是 16,hashmap 數組只容許一個 key 爲 null,容許多個 value 爲 null。
hashmap 的內部實現,hashmap 是使用數組+鏈表+紅黑樹的形式實現的,其中數組是一個一個 Node[]數組,咱們叫他 hash 桶數組,它上面存放的是 key-value 鍵值對的節點。HashMap 是用 hash 表來存儲的,在 hashmap 裏爲解決 hash 衝突,使用鏈地址法,簡單來講就是數組加鏈表的形式來解決,當數據被 hash 後,獲得數組下標,把數據放在對應下標的鏈表中。
而後再說一下 hashmap 的方法實現
put 方法,put 方法的第一步,就是計算出要 put 元素在 hash 桶數組中的索引位置,獲得索引位置須要三步,計算要 put 元素 key 的 hashcode 值,高位運算,取模運算,高位運算就是用第一步獲得的值 h,用 h 的高 16 位和低 16 位進行異或操做,第三步爲了使 hash 桶數組元素分佈更均勻,採用取模運算,取模運算就是用第二步獲得的值和 hash 桶數組長度-1的值取與。這樣獲得的結果和傳統取模運算結果一致,並且效率比取模運算高。
jdk1.8 中 put 方法的具體步驟,先判斷 hashmap 是否爲空,爲空的話擴容,不爲空計算出 key 的 索引值 i,而後看 table[i]是否爲空,爲空就直接插入,不爲空判斷當前位置的 key 和 table[i] 是否相同,相同就覆蓋,不相同就查看 table[i] 是不是紅黑樹節點,若是是的話就用紅黑樹直接插入鍵值對,若是不是開始遍歷鏈表插入,若是遇到重複值就覆蓋,不然直接插入,若是鏈表長度大於 8,轉爲紅黑樹結構,執行完成後看 size 是否大於閾值 threshold,大於就擴容,不然直接結束。
get 方法就是計算出要獲取元素的 hash 值,去對應位置取便可。
HashMap 的兩個重要屬性是容量 capacity 和加載因子 loadfactor,默認值分佈爲 16 和 0.75,當容器中的元素個數大於 capacity*loadfactor 時,容器會進行擴容 resize 爲 2n,在初始化 Hashmap 時能夠對着兩個值進行修改,負載因子 0.75 被證實爲是性能比較好的取值,一般不會修改,那麼只有初始容量 capacity 會致使頻繁的擴容行爲,這是很是耗費資源的操做,因此,若是事先能估算出容器所要存儲的元素數量,最好在初始化時修改默認容量 capacity,以防止頻繁的 resize 操做影響性能。
擴容機制,hashmap 的擴容中主要進行兩步,第一步把數組長度變爲原來的兩倍,第二部把舊數組的元素從新計算 hash 插入到新數組中,在 jdk1.8 時,不用從新計算 hash,只用看看原來的 hash 值新增的一位是零仍是 1,若是是 1, 這個元素在新數組中的位置,是原數組的位置加原數組長度,若是是零就插入到原數組中。擴容過程第二步一個很是重要的方法是 transfer 方法,採用頭插法,把舊數組的元素插入到新數組中。
在計算插入元素在 hash 桶數組的索引時第三步,爲了使元素分佈的更加均勻,用取模操做,可是傳統取模操做效率低,而後優化成 h&(length-1),設置成 2 冪次方,是由於 2 的冪次方-1後的值每一位上都是 1,而後與第二步計算出的 h 值與的時候,最終的結果只和 key 的 hashcode 值自己有關,這樣不會形成空間浪費而且分佈均勻。
若是 length 不爲 2 的冪,好比 15。那麼 length-1 的 2 進制就會變成 1110。在 h 爲隨機數的狀況下,和 1110 作&操做。尾數永遠爲 0。那麼 000一、100一、1101 等尾數爲 1 的位置就永遠不可能被 entry 佔用。這樣會形成浪費,不隨機等問題。
HashMap 的不少函數要基於 equal()函數和 hashCode()函數。hashCode()用來定位要存放的位置,equal()用來判斷是否相等。hashcode 和 equals 組合在一塊兒肯定元素的惟一性。
查找元素時,若是單單使用 equals 來肯定一個元素,須要對集合內的元素逐個調用 equals 方法,效率過低。所以加入了 hashcode 方法,將元素映射到隨機的內存地址上,經過 hashcode 快速定位到元素(大體)所在的內存地址,再經過使用 equals 方法肯定元素的精確位置。
比較兩個元素時,先比較 hashcode,若是 hashcode 不一樣,則元素必定不相等;若是相同,再用 equals 判斷。
HashMap 採用這兩個方法實現散列存儲,提升鍵的索引性能。HashSet 是基於 HashMap 實現的。
適用場景:
當集合長度固定時,使用數組;當集合的長度不固定時,使用 ArrayList。
因爲 ArrayList 不支持基本數據類型,因此保存基本數據類型時須要裝箱處理,對比數組性能會降低。這種狀況儘可能使用數組。
數組支持的操做方法不多,但內存佔用少,若是隻需對集合進行隨機讀寫,選數組
ArrayList 和 LinkedList 都實現了 List 接口,他們有如下的不一樣點:
ArrayList 是基於索引的數據接口,它的底層是數組。它能夠以O(1)時間複雜度對元素進行隨機訪問,適合元素查找。與此對應,LinkedList 基於鏈表,爲雙向鏈表(JDK1.6 以前爲循環鏈表,JDK1.7 取消了循環), 每個元素都和它的前一個和後一個元素連接在一塊兒,在這種狀況下,查找某個元素的時間複雜度是 O(n)。
相對於 ArrayList,LinkedList 增刪操做速度更快,由於當元素被添加到集合任意位置的時候,不須要像數組那樣從新計算大小或者是更新索引。
LinkedList 比 ArrayList 更佔內存,由於 LinkedList 爲每個節點存儲了兩個引用,一個指向前一個元素,一個指向後一個元素。
Comparable & Comparator 都是用來實現集合中元素的比較、排序的。
Comparable 接口(內部比較器):
Comparator 接口(外部比較器):
PriorityQueue 的邏輯結構是一棵徹底二叉樹,存儲結構實際上是一個數組。邏輯結構層次遍歷的結果恰好是一個數組。
PriorityQueue 是一個基於優先級堆的無界隊列,它的元素是按照天然順序(natural order)排序的。在建立的時候,咱們能夠給它提供一個負責給元素排序的比較器。PriorityQueue 不容許 null 值,由於他們沒有天然順序,或者說他們沒有任何相關聯的比較器。最後,PriorityQueue 不是線程安全的,入隊和出隊的時間複雜度是 O(log(n))。
Hashset 的底層是由哈希表實現的,Hashset 中的元素是無序的。add(),remove(),contains()方法的時間複雜度是 O(1)。
Treeset 底層是由紅黑樹實現的。若是須要在 Treeset 中插入對象,須要實現 Comparable 接口,重寫 compareTo 方法。
HashSet 底層由 HashMap 實現
HashSet 的值存放於 HashMap 的 key 上
HashMap 的 value 統一爲 PRESENT
List 轉換成爲數組:調用 ArrayList 的 toArray 方法。
數組轉換成爲 List:調用 Arrays 的 asList 方法。
String[] myArray = { "Apple", "Banana", "Orange" };
List<String> myList = Arrays.asList(myArray);
//上面兩個語句等價於下面一條語句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");
複製代碼
注意:使用工具類 Arrays.asList()把數組轉換爲集合時,不能使用其修改集合相關的方法,它的 add/remove/clear 方法會拋出 UnsupportedOperationException 異常。
說明:asList 的返回對象是一個 Arrays 內部類,並無實現集合的修改方法。Arrays.asList 體現的是適配器模式,只是轉換接口,後臺的數據還是數組。
String[] str = new String[]{"and","you"};
List list = Arrays.asList(str);
System.out.println(list.get(1));//輸出you
list.add("yes");//拋出異常
str[0] = s"yes";
System.out.println(list);//list也會發生改變,輸出[yes, you]
複製代碼
Arrays.asList()是泛型方法,傳入的參數對象必須是對象數組。
int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//數組地址值
System.out.println(myList.get(1));//報錯:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[1]);//1
複製代碼
當傳入一個原生數據類型數組時,Arrays.asList() 的真正獲得的參數就不是數組中的元素,而是數組對象自己!此時List 的惟一元素就是這個數組,這也就解釋了上面的代碼。
轉換爲包裝數據類型便可。
Integer[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//數組長度3
System.out.println(myList.get(0));//1
System.out.println(myList.get(1));//2
複製代碼
使用集合的修改方法:add()、remove()、clear()會拋出異常。
Arrays.asList() 方法返回的並非 java.util.ArrayList ,而是 java.util.Arrays 的一個內部類,這個內部類並無實現集合的修改方法或者說並無重寫這些方法。
List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
複製代碼
如何正確的將數組轉換爲ArrayList?
一、自定義代碼
static <T> List<T> arraysToList(T[] array){
List<T> list = new ArrayList<T>(array.length);
for(T obj:array){
list.add(obj);
}
return list;
}
String[] str = new String[]{"and","you"};
List ll = arraysToList(str);
System.out.println(ll.getClass());//class java.util.ArrayList
int[] myArray = { 1, 2, 3 };
List ll2 = arraysToList(myArray);//編譯報錯
複製代碼
該方法暫不支持基本數據類型。
二、最簡便的方法(推薦)
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
複製代碼
三、使用 Java8 的 Stream(推薦)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本類型也能夠實現轉換(依賴boxed的裝箱操做)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
複製代碼
四、 使用 Guava(推薦)
對於可變集合,你可使用 Lists 類及其 newArrayList()工廠方法:
String[] str = {"acd","yes"};
List list = Lists.newArrayList(str);//class java.util.ArrayList
int[] myArray = { 1, 2, 3 };
list = Lists.newArrayList(myArray);//class java.util.ArrayList
複製代碼
五、使用 Apache Commons Collections
List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);
複製代碼
該方法是一個泛型方法: T[] toArray(T[] a); 若是 toArray 方法中沒有傳遞任何參數的話返回的是 Object 類型數組。
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);//class java.util.Arrays$ArrayList
Collections.reverse(list);
s=list.toArray(new String[0]);//沒有指定類型的話會報錯,s2類型爲String
Object[] s2 = list.toArray();//效果同上
list = Lists.newArrayList(s);//class java.util.ArrayList
Collections.reverse(list);
s = list.toArray(new String[0]);//編譯報錯
Object[] s2 = list.toArray(new String[0]);//s2類型爲String
複製代碼
綜上可知,當數組爲引用類型數組時,使用 Arrays.asList 轉換爲 List,反轉操做後,再轉換爲數組,不管 toArray()方法中是否有參數,聲明爲 Object 類型的數組實際類型都爲原引用類型。使用其餘數組轉 List 方法,好比 Lists.newArrayList,須要在 toArray()參數中傳入對應類型,最後獲得的數組類型也是原類型。
當數組爲基本數據數組時,最後轉換以後獲得的數組爲 Object 類型。
poll() 和 remove() 都是從隊列中取出一個元素,可是 poll() 在獲取元素失敗的時候會返回空,可是 remove() 失敗的時候會拋出異常。