1. 程式人生 > >Java集合(偏向結構解析,完善中)

Java集合(偏向結構解析,完善中)

前言:這是我在面試的時候發現面試官很喜歡考Java集合底層結構,故而總結的,至此我的秋招仍沒有結束,只是將我理解的知識寫上去,許多地方閱讀可能有小問題。只能等招聘結束再修改了。

Java集合

fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。例如:當某一個執行緒A通過iterator去遍歷某集合的過程中,若該集合的內容被其他執行緒所改變了;那麼執行緒A訪問集合時,就會丟擲ConcurrentModificationException異常,產生fail-fast事件。

陣列和集合的區別:

Java集合按照儲存結構可以分為兩大類,即單列集合Collection和雙列集合Map

1) 陣列長度固定

集合長度可變

2) 內容的區別

陣列只能儲存一種型別的元素

集合,可以儲存多種元素

3) 陣列可以儲存引用型別,也可以儲存基本型別

集合:只能儲存引用型別(若有基本型別,將基本型別變成包裝類)

Collection單列集合類的根介面,其中有兩個重要子介面List和Set.

List介面的主要介面有ArrayList和LinkList Vector

Set介面的主要類有HashMap和TreeMap 以及 LinkedHashSet

Map雙列集合的主要實現類喲HashMap和TreeMap;

List和Set的區別

1.set介面例項儲存是無序的,不重複的資料,List介面實現儲存是有序的,可以重複的元素.

2.set檢索效率idxia(因為檢索依靠Hash值,需要進行計算),刪除和插入效率高(因為插入和刪除的時候其他元素的位置不會發生變化),刪除和插入如並不會引起元素位置的改變,實現類有HashSet和TreeSeti以及LinkHashSet

3.List和陣列lsi,可以動態增長,根據實際儲存的資料長度自增長List的長度.查詢元素效率高(依靠下標查詢).插入刪除效率低(因為插入的時候其他位置的下標和陣列整個長度會變化),因為會引起其他元素位置的改變,實現類有ArrayList,LinkedLIst,Vector

總結:1 .set儲存無序無重複,LIst有序有重複

2.set檢索按,List檢索塊

3.set插入如和刪除快,因為並不會影響原有元素位置的改變,List插入和刪除慢,會引起其他位置的變化.

List集合子類的特點:

 ArrayList(面試題中沒有告訴具體的集合:預設都使用ArrayList)

內部是一種陣列實現,查詢快,增刪慢

執行緒不安全,--à不同步-à執行效率高

Vector:底層是一種陣列實現,查詢快,增刪慢

執行緒安全的類,同步-à執行效率低

StringBuffer:執行緒安全的類

LinkedList:底層是一種連結列表實現,查詢慢,增刪快,

執行緒不安全,不同步-à執行效率高

ArrayList和和LinkedList區別:

1 : ListkedList 插入刪除快 (是連結串列結構,因為插入刪除的時候只需要功能上下兩個元素的索引,即可)

而ArrayList是連續儲存,插入刪除會引起整個陣列的改變,和下標索引的改變.

2 :查詢的是ArrayList比較快,因為依據下標索引即可查詢到相應的元素,而LinkedList需要一個額往後查詢,因為上一個元素的最後就是下一個元素的索引.

3.結構不同,ArrayList是基於陣列的動態的資料結構,而LinkedList是基於連結串列的結構.

ArrayList和Vector區別

相同點:都是基於索引,內部結構是陣列 ,兩者元素存取都允許null

不同點: 1 Vector是安全,效率慢 ,對應的LIst是不安全的,效率高

2.初始容量都是10,ArrayList都是預設增長百分之五十,(Vector增長一倍.可以設定的)

3.ArrayList更通用.

Set集合:  Set集合的元素不重複,能保證唯一性(不能保證元素的迭代的順序恆久不變)

HashSet存放元素的過程:獲取元素的hashCode()值,再通過雜湊(hash)演算法找到要存放的位置;一般我們在定義類時,重寫equals()方法也要重寫hashCode()方法,要保證兩個物件equals()返回值為true時,它們的hashCode()返回值也相同;但是有時候會出現以下兩種情況,那元素是如何儲存的呢?

1:兩個物件通過equals()返回值為true,但這兩個物件的hashCode()返回值不同時,會導致HashSet將這兩個物件存放在不同的位置,但是這和HashSet的不可重複規則衝突;

2:兩個物件通過equals()返回值為false,但這兩個物件的hashCode()返回值相同,因為兩個物件的hashCode()返回值一樣,所以HashSet將試圖把它們儲存在同一個位置,但是一個位置不允許存放兩個元素,所以在這個位置用連結串列來儲存多個元素

因為它們新增元素呼叫的是Map的put方法,說明儲存結構和map類似

HashSet   TreeSet(主要條件 :對自定義的屬性進行規制排序)

HashSet:   -àhashtable:雜湊表 --à依賴於兩個方法 HashCode()雜湊碼值   equals()方法

如果使用HashSet集合儲存自定義物件並遍歷(如果自定義物件的成員變數的值一樣,把它同一個物件):在類中重寫上面兩個方法

 LinkedHashSet:保證元素唯一性(雜湊表)添加了雙列連結串列保證新增的順序性

                           保證元素的有序性(儲存和取出一致):由連結列表保證

TreeSet:基於TreeMap的紅黑樹結構(內部是一種自平衡的二叉樹結構…)

儲存方法依賴於TreeMap

底層原始碼:有兩種排序(根據構造方式不一樣)

TreeSet() ;   自然排序 自定義的類,必須實現Comparable介面,重寫裡面的compareTo(Object O)方法

TreeSet(Comparator<T>  comparator):比較器排序:  採用匿名內部類的方式進行排序(主要條件)

疑問:為何LinkedHashSet能保證元素唯一性,和元素有序性,這兩者如何不矛盾,

因為它們儲存也是依靠hashCode值,但是地址值存在連結串列中,該連結串列是以存取順序存放的.

HashSet和TreeSet

1.HashSet不能保證元素的排列順序,TreeSet是SortedSet介面的唯一實現類,可以確保集合元素處於元素處於排序狀態.

2.HashSet底層用的是雜湊表,TreeSet用的結構是紅黑樹.

3.HashSet中元素可以是null,TreeSet不允許NULL

HashSet和LinkedHashSet區別:

1.HashSet不能保證存取一致性,Linked...可以.

2.前者查詢慢,但是插入刪除快

Map-----

TreeMap HashMap LinkedHashMap

相同點: 1都是介面

關係:HashSet依賴於HashMap

treeset 依賴於treeMap

集合中collection和Map集合的區別:

1.map是一種鍵和值的對映關係,是種雙列集合 Collection是單列集合,只能儲存一種型別元素

Map<K,V>:是雙列集合,裡面 元素一一對映(Mapping)的關係     框架部分:對映檔案(在xml配置一些實體類的資訊)

一個鍵對應一個值

Map集合只針對鍵有效,鍵唯一的,如果重複,那麼後面鍵對應的值

會將前面鍵對應的值覆蓋掉,返回的是前面的鍵對應的值

子實現類:HashMap<K,V>

Map<k,V> map = new HashMap<k,v>();

Map集合遍歷的方式:

1) 通過keySet()獲取所有的鍵的集合 :這種方式使用最多

2) 通過獲取鍵值對物件entrySet() ;

子實現類有HashMap TreeMap HashTable

實現:

HashMap基於

LinkedHashMap 基於雜湊表和.連結串列實現,能夠保證鍵的唯一性並且元素有序.

TreeMap能夠實現排序.基於紅黑樹

HashMap工作原理---------

結構:是以拉鍊法來儲存的,就是以陣列+連結串列形式儲存.

基於雜湊原理,通過put和get方法儲存和獲取元素,put方法是通過Hashcode()計算出值,找到對應的bucket位置來儲存資料,當HashCode值相同的時候,再進行判斷鍵物件是否相同,如果相同,則只更新value值,如果只是hashcode相同,則儲存在該bucket位置連結串列的下一個節點.獲取put方法是算出hashcode,找到bucket位置,然後遍歷連結串列,通過key的equals方法,找到鍵值對

H

好處:作為快取十分方便,能間接實現資料庫的功能 ,實現一些簡單的編號對應資料的儲存 (例如撲克牌).

有HashMap和HashTable是子實現類:

相同:都是基於hash實現的,每個元素都是Key-value的鍵值對,內部通過單向連結串列解決衝突,容量都會自動增長

區別:1.父類不同HashMap父類是AbstractMap類,而HashTable是Dictionary類

2.執行緒同步,和不同步 安全和不安全,效率高低

3.HashTable初始容量是11,擴容是增加一倍加1 HashMap key 初始容量是16擴容方式是增加一倍

4.是否有contains方法,HashMap中將Contains變為Containkey和Containvalue方法,而Hashtable中有全部三個方法,而contains和containvalue效果相同

5.內部遍歷的方式不同,現在都是用Iterator的遍歷,但是之前HashTable還用過Enumeration方式

6..HashMap key 和value允許null值,而HashTable則不允許.

7.Hash值不同,HashTable是使用HashCode方法獲取Hash值,而HashMap是重新計算Hash值(重新Hash(key.HashCode())) // 根據key的keyCode重新計算hash值。 7 int hash = hash(key.hashCode());

HashMap儲存過程深度講解:

對於HashMap及其子類而言,它們採用Hash演算法來決定集合中儲存元素的儲存位置。當系統開始初始化HashMap時,系統會建立一個長度為capacity的Entry陣列,這個陣列可以儲存元素的位置稱為“桶”(bucket),每個bucket都有其指定索引,系統可以根據其索引快速訪問該bucket裡儲存的元素。

無論何時,HashMap的每個“桶之儲存一個元素”(也就是一個Entry),由於Entry物件可以包含一個引用變數(就是Entry構造器的最後一個引數,用於指向下一個Entry),因此可能出現的情況是:HashMap的bucket中只有一個Entry,但是這個Entry指向另一個Entry ----這就形成了一個Entry鏈,圖解

困惑:那麼它們是如何讀取資料的,它們既然是通過陣列儲存,但是hash值明顯不是按順序的,這樣儲存,不符合陣列儲存規則:

解答:它們並不是一個一個按順序存的,而是一開始就建立好了一個有一定長度的陣列,這時候,我們會根據hash值(hash(key.hashcode()))去儲存元素。

那它們是如何讀取資料的呢:

如果兩個鍵的Hash值相同,你如何獲取值物件?

計算鍵的Hash值,找到相應bucket位置,遍歷連結串列,呼叫keys.equals()方法去找到LinkedList中正確的將,最終找到要存的值。

如果HashMap的大小從了負載因子定義的容量,怎麼辦?

這道題就需要我們真正知道HashMap的工作原理,否則你將回答不出這道題。預設的負載銀子大小是0.75,也就是說,當一個map填滿了百分之75的bucket的時候,和其他集合類(如ArrayList等一樣),將會建立原來HashMap大小的兩倍bucket陣列,來重新調整map的大小,並將原來的物件放入buckey陣列這個。這個過程叫做rehashing,因為它呼叫hash方法找到資訊的bucket位置,遍歷出隊尾

你瞭解重新調整HashMap大小存在什麼問題嗎?(擴容重雜湊的時候容易死迴圈,髒讀等)

在多執行緒中,可能產生條件競爭,當重新調整HashMap大小的是,確實存在條件競爭,因為兩個執行緒都發現了HashMap需要重新調整大小了,它們會同時試著調整大小了。在調整大小的過程中,儲存在LinkedLIst元素次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷(每次遍歷連結串列,找到尾部位置)。如果條件競爭發生了,那麼久會死迴圈了。這個時候,質問面試官,太奇怪了,為什麼不再多執行緒環境用ConcurrentHashMap。

為什麼String, Interger這樣的wrapper類適合作為鍵? 

因為這些類都是不可變類,都已經重寫了hashcode()方法和equals方法,表示他們是不可變的,不可變在多執行緒中意味著鍵值不會被改變,說明這是安全的,在

可以用自定義物件作為鍵嗎

可以的,可以使用任何物件作為鍵,只要它遵守equals方法和 Hashcode方法的定義規則,當物件插入到HashMap之後就不會再改變了。如果這個自定義物件不可改變時,說明該物件已經滿足了作為鍵的要求。

HashTable使用get方法加鎖

ConcurrentHashMap

ConcurrentHashMap和HashMap思路上是差不多的,但是因為它支援併發操作,所以要複雜一些。

整個ConcurrentHashMap由一個個segment組成,Segment代表“部分”或者“一段”的意思,所以很多地方都會將其描述為分段鎖。注意,很多“槽”代表一個segment

簡單的理解就是,ConcurrentHashMap是一個Segment陣列,Segment通過整合ReentrantLock來進行枷鎖,所以每次需要加鎖的操作鎖住的是一個Segment,這樣只要保證每個Segment是執行緒安全的,也就實現了全域性的執行緒安全。

有16個Segment,所以理論上,這個時候,最多可以同時指出i16個執行緒併發,只要它們的操作分別分佈在不同的Segment上。這個值可以在初始化的時候設定其他值,但是一旦初始化以後,它是不可以擴容的。

再具體到每個Segment內部,其實每個Segment很想之前介紹的HashMap,不過它要保證執行緒安全,所以處理起來要麻煩一些

初始化:

初始容量是指整個ConcurrentHashMap的初始容量,實際操作的時候需要平均分給每個Segment

負載因子是給每個Segment內部使用的

特點總結:(在HashTable效能太差的情況下,推出了既安全又快捷的ConcurrentHashMap)

Segment陣列長度為16,不可以擴容

Segment[i]的預設大小為2,負載因子是0.75,得出初始閾值為1.5,

也就是以後插入第一個元素不會觸發擴容,插入第二個元素會觸發擴容。

Put操作:

由於put方法需要對共享變數進行寫入操作,所以為了執行緒安全,在操作共享變數的時候必須得加鎖。Put方法首先定位到Segment,然後再Segment裡進行插入操作。插入操作經歷兩個步驟,第一判斷是否需要對Segment裡面的Entry陣列進行擴容,第二步定位新增元素的位置,然後放在HashEntry數組裡。

根據key的hash值的高n位就可以確定元素到底在哪一個Segment中。

其他操作和HashMap無區別。

效率的提高是因為:

HashTable本身是執行緒安全的,但是通過Syncharonized關鍵字實現執行緒安全,這樣對整張表實現同步的缺陷就在於使效率很低。ConcurrentMap將鎖加在Segment,這樣我們在對Segment1操作的時候,同時也可以對Segment2中的資料進行操作,這樣的效率就會高很多。

LinkedHashMap

概述:因為HashMap是無序的,也就是說,迭代HashMap所得到的元素並不是最初放的順序。所以我們需要一個保持插入順序的Map。慶幸的是,JDK為我們解決了這個問題,為HashMap提供了一個子類-----LinkHashMap(保持插入和訪問順序)

在這之間我們必須複習一下連結串列的相關知識:

單鏈表和雙鏈表的區別:

單鏈表只有一個指向下一節點的指標,也是說只能next.

雙鏈表除了有一個指向下一節點的指標外,還有一個指向前一節點的指標,可以通過prec()快速找到前一節點,單鏈表只能單向讀取,雙鏈表能夠雙向讀取

為什麼市面上用單鏈表更多:雖然雙鏈表在查詢,刪除的時候可以用二分法實現,效率更高,但是因為單鏈表使用的空間小,實際上上是用時間換空間,在效率要求不高的情況下經量使用單鏈表,

LinkedHashMap實現了Map介面,繼承於HashMap,於HashMap不同的是,它維持有一個雙鏈表,從而可以保證迭代時候的順序,不同的是HashMap維持的是單向連結串列

TreeMap:

紅黑樹是一種近似平衡的二叉查詢樹,它能夠保證任何一個的節點的左右子樹高度差不會超過二者中較低的那個的一倍。具體來說,共黑數是滿足如下條件的二叉查詢樹:

1.每個節點要麼是紅色,要麼是黑色。

2.根節點必須是黑色

3.紅色節點不能連續,(也即是,紅色節點的孩子和父親都不能是紅色)。

4.對於每個節點,從該點至null(樹尾端)的任何路徑,都含有相同個數的黑色節點

在樹的結構發生改變時,往往會破壞上述條件3或條件4,需要通過調整使得查詢樹重新滿足紅黑樹的條件。

總結:程式新增新節點的時候,總是從數的根節點開始比較,即將根節點當成當前節點。如果新增節點大於當前節點並且當前節點的右節點存在,則以右節點為當前節點。若是新增節點比當前節點小,並且左節點存在,則當前節點為做節點;若新增節點等於當前節點,則用新增界定啊覆蓋當前節點,並結束迴圈 。否則,知道某個節點的左右節點不存在,將新節點新增為該節點的子節點。如果新節點比該節點大,則新增為右子節點內,反之也可。