1、List,Set,Map三者的區別?
List:用於存儲一個有序元素的集合。
Set:用於存儲一組不重復的元素。
Map:使用鍵值對存儲。Map會維護與Key有關聯的值。兩個Key可以引用相同的對象,但Key不能重復,典型的Key是String類型,但也可以是任何對象。
補充:
Stack用於存儲采用后進先出方式處理的對象。
Queue用於存儲采用先進先出方式處理的對象。
PriorityQueue用於存儲按照優先級順序處理的對象。
2、Arraylist 與 LinkedList 區別?
-
(相同點)是否保證線程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保證線程安全; -
1. 底層數據結構:
Arraylist
底層使用的是Object
數組;LinkedList
底層使用的是雙向鏈表(JDK1.6之前為循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別,下面有介紹到!) -
2. 插入和刪除是否受元素位置的影響: ①
ArrayList
采用數組存儲,所以插入和刪除元素的時間復雜度受元素位置的影響。 比如:執行add(E e)
方法的時候,ArrayList
會默認在將指定的元素追加到此列表的末尾,這種情況時間復雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element)
)時間復雜度就為 O(n-i)。因為在進行上述操作的時候集合中第 i 和第 i 個元素之后的(n-i)個元素都要執行向后位/向前移一位的操作。 ②LinkedList
采用鏈表存儲,所以插入、刪除元素時間復雜度不受元素位置的影響,都是近似 O(1) 而數組為近似 O(n)。 -
3. 是否支持快速隨機訪問:
LinkedList
不支持高效的隨機元素訪問,而ArrayList
支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index)
方法)。 -
4. 內存空間占用: ArrayList的空間浪費主要體現在在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因為要存放直接后繼和直接前驅以及數據)。
雙向鏈表和雙向循環鏈表:
雙向鏈表: 包含兩個指針,一個prev指向前一個節點,一個next指向后一個節點。
雙向循環鏈表: 最后一個節點的 next 指向head,而 head 的prev指向最后一個節點,構成一個環。
3、ArrayList 與 Vector的區別?為什么要用Arraylist取代Vector呢?
Vector
類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。
Arraylist
不是同步的,所以在不需要保證線程安全時建議使用Arraylist。
4、說一說 ArrayList 的擴容機制
( ArrayList擴容發生在add(E e)方法調用的時候)
1. 以無參數構造方法創建 ArrayList 時,實際上初始化賦值的是一個空數組。當真正對數組進行添加元素操作時(add),才真正分配容量。即向數組中添加第一個元素時,數組容量擴為10。(默認初始容量DEFAULT_CAPACITY = 10)
2. 當數組首次擴容的10個空間用完需要擴容后,會走grow方法來擴容(每次擴容為1.5倍)
private void grow(int minCapacity) { // 獲取到ArrayList中elementData數組的內存空間長度 int oldCapacity = elementData.length; // 擴容至原來的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //然后檢查新容量是否大於最小需要容量,若還是小於最小需要容量,那么就把最小需要容量當作數組的新容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //若預設值大於默認的最大值檢查是否溢出 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 調用Arrays.copyOf方法將elementData數組指向新的內存空間時newCapacity的連續空間,並將elementData的數據復制到新的內存空間 elementData = Arrays.copyOf(elementData, newCapacity); }
從此方法中我們可以清晰的看出其實ArrayList擴容的本質就是計算出新的擴容數組的size后實例化,並將原有數組內容復制到新數組中去。
3. 如果新容量大於 MAX_ARRAY_SIZE,執行)hugeCapacity()方法來比較 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大於最大容量,則新容量則為Integer.MAX_VALUE,否則,新容量大小則為 MAX_ARRAY_SIZE 即為 Integer.MAX_VALUE - 8。
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md
5、HashMap的底層實現?怎么put、get?put中的resize?(重要!)
JDK1.8 之前 HashMap
底層是數組+鏈表結合在一起實現的(HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表), jdk8 對於鏈表長度超過 8 的鏈表將轉儲為紅黑樹。
當新建一個HashMap的時候,就會初始化一個數組。Entry就是數組中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。
transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; ……
HashMap通過key的hashCode得到hash值,然后計算當前元素存放的位置((n - 1) & hash,n指的是數組的長度),如果當前位置存在元素的話,就利用equals方法判斷該元素與要存入的元素的key是否相等,如果相等就覆蓋其value值,不相等就通過拉鏈法解決沖突(就是將當前元素與該位置原有元素形成鏈表,並且當前元素位於鏈表的頭部)。
對於存值和取值,使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。
(1)put(key, value):
- 對key求hash值,然后找到該hash值對應數組中的存儲位置。
- 如果當前位置存在元素的話(不為空),就遍歷已存在的元素 判斷該元素與要存入的元素的key值是否相等,如果相等就覆蓋value值;如果不相等就將當前元素添加到到該位置的鏈表中(當前元素位於鏈表頭部)。
- 如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,當前位置元素長度低於6,就把紅黑樹轉回鏈表;
- 如果桶滿了(容量16*加載因子0.75),就需要 resize(擴容2倍后重排);
public V put(K key, V value) { // HashMap允許存放null鍵和null值。 // 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。 if (key == null) return putForNullKey(value); // 根據key的keyCode重新計算hash值。 int hash = hash(key.hashCode()); // 搜索指定hash值在對應table中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引處的 Entry 不為 null,通過循環不斷遍歷 e 元素的下一個元素。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 如果發現已有該鍵值,則存儲新的值,並返回原始值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果i索引處的Entry為null,表明此處還沒有Entry。 modCount++; // 將key、value添加到i索引處。 addEntry(hash, key, value, i); return null; }
為什么鏈表長度超過8,就把鏈表轉換為紅黑樹;桶中元素小於等於6,就將樹結構還原為鏈表?
紅黑樹的平均查找長度是log(n),當長度為8時,查找長度為log(8)=3;鏈表的平均查找長度為n/2,當長度為8時,平均查找長度為8/2=4,這才有將鏈表轉換為紅黑樹的必要。
還有選擇6和8的原因是:中間有個差值7可以防止鏈表和樹之間頻繁地轉換。假設鏈表個數超過8就將鏈表轉換為樹結構、小於8就將樹結構轉換成鏈表,那么如果一個HashMap不停地插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁地發生樹轉鏈表、鏈表轉樹,效率會很低。
(2)get(key):
當調用get()方法,首先計算key的hashcode來找到桶位置(數組中對應位置),找到桶位置之后,調用key的equals()方法在對應位置的鏈表中找到需要的元素。
(3)put中的resize(擴容):
當HashMap中的元素個數超過 數組大小*負載因子(loadFactor)時,就會進行數組擴容(把數組大小擴大一倍),然后重新計算元素在數組中的位置。
Entry<K,V>[] table的初始化長度length(數組大小默認值是16),loadFactor的默認值為0.75。
HashMap不是無限擴容的,當達到了實現預定的MAXIMUM_CAPACITY,就不再進行擴容。
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果當前的數組長度已經達到最大值,則不在進行調整 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //根據傳入參數的長度定義新的數組 Entry[] newTable = new Entry[newCapacity]; //按照新的規則,將舊數組中的元素轉移到新數組中 transfer(newTable); table = newTable; //更新臨界值 threshold = (int)(newCapacity * loadFactor); } //舊數組中元素往新數組中遷移 void transfer(Entry[] newTable) { //舊數組 Entry[] src = table; //新數組長度 int newCapacity = newTable.length; //遍歷舊數組 for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity);//放在新數組中的index位置 e.next = newTable[i];//實現鏈表結構,新加入的放在鏈頭,之前的的數據放在鏈尾 newTable[i] = e; e = next; } while (e != null); } }
6、 HashMap、Hashtable、ConcurrentHashMap 的區別(重要!)
(1)HashMap
實現了Map接口,實現了將唯一鍵隱射到特定值上。允許一個NULL鍵和多個NULL值。非線程安全。
(2)HashTable
類似於HashMap,但是不允許NULL鍵和NULL值,比HashMap慢,因為它是同步的。HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即每次鎖住整張表讓線程獨占。
(3)ConcurrentHashMap
ConcurrentHashMap允許多個修改操作並發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的Hashtable,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以並發進行。
Hashtable 和 HashMap不同點?
- Hashtable和HashMap都實現了Map接口,但是Hashtable的實現是基於Dictionary抽象類。
- 在HashMap中,null可以作為鍵,這樣的鍵只有一個,但可以有一個或多個鍵所對應的值為null;Hashtable不允許NULL鍵和NULL值。
當get()方法返回null值時,即可以表示HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。而在Hashtable中,無論是key還是value都不能為null。
- 這兩個類最大的不同在於Hashtable是線程安全的,它的方法是同步了的,可以直接用在多線程環境中。而HashMap則不是線程安全的。在多線程環境中,需要手動實現同步機制。
Hashtable 和ConcurrentHashMap的不同?
(1)HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。(所有訪問HashTable的線程都必須競爭同一把鎖。)因為當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法可能會進入阻塞或輪詢狀態。
(2)ConcurrentHashMap使用鎖分段技術,首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。從而有效地提高了並發訪問效率。
Hashtable中采用的鎖機制是一次鎖住整個hash表,從而同一時刻只能由一個線程對其進行操作;而ConcurrentHashMap中則是 一次鎖住一個桶。ConcurrentHashMap默認將hash表分為16個桶,諸如get,put,remove等常用操作只鎖當前需要用到的桶。 這樣,原來只能一個線程進入,現在卻能同時有16個寫線程執行,並發性能的提升是顯而易見的。(16個線程指的是寫線程,而讀操作大部分時候都不需要用到鎖。)
- 底層數據結構: JDK1.7的 ConcurrentHashMap 底層采用 分段的數組+鏈表 實現,JDK1.8 采用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是采用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的;
- 實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器里不同數據段的數據,就不會存在鎖競爭,提高並發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,並發控制使用 synchronized 和 CAS 來操作。(JDK1.6以后 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是為了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。
HashTable:
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 鏈表節點):
ConcurrentHashMap線程安全的具體實現方式/底層具體實現?
JDK1.7(上面有示意圖):
首先將數據分為一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。
ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。
一個 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倍。
7、HashMap 和 HashSet區別
HashSet 底層就是基於HashMap實現的。(HashSet 的源碼非常非常少,因為除了 clone()
、writeObject()
、readObject()
是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法。
HashMap | HashSet |
---|---|
實現了Map接口 | 實現Set接口 |
存儲鍵值對 | 僅存儲對象 |
調用 put() 向map中添加元素 |
調用 add() 方法向Set中添加元素 |
HashMap使用鍵(Key)計算Hashcode | HashSet使用成員對象來計算hashcode值,對於兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性, |
8、HashSet如何檢查重復
當你把對象加入HashSet
時,HashSet會先計算對象的hashcode
值來判斷對象加入的位置,同時也會與其他加入的對象的hashcode值作比較,如果沒有相符的hashcode,HashSet會假設對象沒有重復出現。但是如果發現有相同hashcode值的對象,這時會調用equals()
方法來檢查hashcode相等的對象是否真的相同。如果兩者相同,HashSet就不會讓加入操作成功。
hashCode() 與equals() 的相關規定:
- 如果兩個對象相等,則hashcode一定也是相同的;
- 兩個對象相等,對兩個equals方法返回true;
- 兩個對象有相同的hashcode值,它們也不一定是相等的;
- 綜上,equals方法被覆蓋過,則hashCode方法也必須被覆蓋
- hashCode()的默認行為是對堆上的對象產生獨特值。如果沒有重寫hashCode(),則該class的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數據)。
==與equals的區別:
- ==是判斷兩個變量或實例是不是指向同一個內存空間,equals是判斷兩個變量或實例所指向的內存空間的值是不是相同。
- ==是指對內存地址進行比較,equals()是對字符串的內容進行比較。
- ==指引用是否相同,equals()指的是值是否相同。
9、comparable 和 Comparator的區別
- comparable接口實際上是出自java.lang包,它有一個
compareTo(Object obj)
方法用來排序 - comparator接口實際上是出自 java.util 包,它有一個
compare(Object obj1, Object obj2)
方法用來排序
內置比較器接口:Comparable<T>----------compareTo(Object obj)
方法用來排序:
compareTo(Object obj):該方法用於比較此對象與指定對象的順序。如果該對象小於、等於或大於指定對象,則分別返回負整數、零或正整數。 根據不同類的實現返回不同,大部分返回1,0和-1三個數。
public class Student implements Comparable<Student>{ private int id; private String name; private double score; @Override public int compareTo(Student o) { //指定排序規則 return +(this.id - o.id); } } -------------------------------------------------------------------- public void Test() { List<Student> studentList = new ArrayList<Student>(); studentList.add(new Student(1, "劉文傑", 100)); studentList.add(new Student(2, "劉潔", 80)); studentList.add(new Student(3, "李雪", 90)); studentList.add(new Student(4, "黃育航", 59.4)); Collections.sort(studentList); for (Student student : studentList) { System.out.println(student.getId() + ":" + student.getName() + ":"+ student.getScore()); } }
外置比較器接口:Comparator<T>接口-----------compare(T o1,T o2)
方法用來排序:
compare(T o1,T o2):
比較用來排序的兩個參數。返回如果是正整數,則交換兩個對象的位置。
public class StudentScoreDescComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { if(o1.getScore() > o2.getScore()){ return -1; }else{ return 1; } } } --------------------------------------------------------------------- public void Test() { List<Student> studentList = new ArrayList<Student>(); studentList.add(new Student(1, "劉文傑", 100)); studentList.add(new Student(2, "劉潔", 80)); studentList.add(new Student(3, "李雪", 90)); studentList.add(new Student(4, "諸葛山珍", 59.4)); Collections.sort(studentList, new StudentScoreDescComparator()); for (Student student : stuList) { System.out.println(student.getId() + ":" + student.getName() + ":"+ student.getScore()); } }
10、如何選用集合?
當我們需要根據鍵值獲取到元素值時就選用Map接口下的集合:
- 需要排序時選擇TreeMap;
- 不需要排序時就選擇HashMap;
- 需要保證線程安全就選用ConcurrentHashMap。
當我們只需要存放元素值時,就選擇實現Collection接口的集合:
- 需要保證元素唯一時選擇實現Set接口的集合比如TreeSet或HashSet;
- 不需要就選擇實現List接口的比如ArrayList或LinkedList,然后再根據實現這些接口的集合的特點來選用。