-
- 2.1. 再談Hsah
- 2.2. 聊聊Hashcode
- 2.3. HashMap深入理解
- 2.4. HashTable深入理解
- 2.5. HashMap與Redis底層的Dict的區別
-
- 3.1. ArrayList深入理解
- 3.2. LinkedList深入理解
JAVA基礎複習(一):集合類
1. 迭代器
- 迭代器是一種設計模式,可以通過統一的方式來遍歷不同的容器。
- 爲什麼要採用這種設計模式?本質上是爲了解耦。比如你訪問數組的時候可以取下標遍歷,訪問鏈表的時候可以while next!=null遍歷,但是你必須熟知這些集合的數據結構纔行,而且不利於代碼的複用,比如你用List這個接口指向兩個對象,一個動態數組,一個鏈表,然後發現糟了,遍歷方式不同,如果要從動態數組轉爲鏈表的話,所有遍歷語句都要重寫。怎麼解決呢?採用迭代器,迭代器作爲這些容器的內部類,實現Iterator接口,內部抽象出方法。無論你訪問什麼數據結構,都遵循1.先判斷有沒有下一個2.取出下一個這樣的操作,實現了遍歷與具體容器解耦。
- 方法解釋
- hashNext 判斷是否有下一個
- next 取出容器中的下一個值
- remove 刪除容器中的某個元素
- HashMap中的樣例分析
- 分析語句Iterator map1it=map.entrySet().iterator();while(map1it.hasNext());
- entrySet.iterator()返回值是EntrySetIterator對象,而這個迭代器對象是繼承與HashIterator的。HashIterator作爲父類並未完全實現Iterator中的方法,而是實現了hasNext,remove這樣的方法,而具體的next()方法交給子類來完成。比如EntryIterator類就是HashIterator的子類,通過繼承父類的hasNext與remove,自己實現next()的方式,徹底實現了Iterator接口。爲什麼next方法不在hashmapIterator中實現呢,因爲還有KeyIterator和ValueIterator,他們分別繼承自HashIterator,實現Iterator接口並重寫next方法,這樣既公用了hashmap內的remove和hasNext,同時還針對不同類型有不同的next()以返回不同值。
- fast-fail
- 如果exceptedmodCnt與modCnt不同,則說明數據數量已經發生修改,則立即發出異常。
- 爲什麼在迭代器遍歷的過程中不允許元素數量有變動?爲了防止併發情況下一個線程遍歷集合的同時另一個線程進行增刪操作,無法保證數據一致性。而且隨意添加刪除可能導致數組越界、重複遍歷、遍歷不到新添加的數據等一系列複雜的錯誤發生。
2. HashMap與HashTable
2.1. 再談Hsah
- Hash本質上就是一種映射,將任意對象映射成數字,以便於存儲。
- 哈希函數構造方法
- 直接定址法,比如以年齡爲關鍵字定址
- 餘數法,對散列表長度取餘
- 平方取中,平方後選擇數值中間的數字
- 摺疊法,將關鍵詞分爲幾部分,然後用疊加和作爲散列地址
- 哈希衝突?
- 影響衝突產生的三個因素
- 哈希函數是否均勻
- 處理衝突的方法
- 哈希表的加載因子,即哈希表有32個空位,但是現在分配了64個,加載因子就是2,多了顯然不好,因爲必然衝突。默認爲0.75.
- 解決方法:拉鍊法。數組拉鍊表,即發生衝突的時候,數據保存頭指針,鏈表保存元素值。好處是數組的隨機查找爲O1,鏈表爲On的順序查找,但是鏈表不需要連續空間。
- 影響衝突產生的三個因素
2.2. 聊聊Hashcode
- 有的Hashcode()返回的是對象的存儲地址,有些只是和存儲地址有一定的關聯,以地址爲核心進行一系列的位預算可以獲得hashcode。因此不同的對象可能具有同樣的hashcode,但是如果hashcode不相等就一定不是相同的對象。
- ==表示左右兩邊指向的內存空間的東西是否相同,因爲相同內容字符串指向堆中不同的對象,共享同一個堆中常量池的字符常量。
- 重寫equals必須重寫hashcode。因爲equals表示內容相等,但是兩個實例的hashcode如果不相同會導致HashMAP中無法針對key取道正確的值。比如A a1和A a2的內容一樣,但是指向兩個對象,此時a1!=a2,但是a1.equals(a2)。map.put(a1,1),但是map.get(a2)就拿不到值了,因此他們是不一樣的對象。一般可以用String的hashcode,因爲重寫過了。
2.3. HashMap深入理解
- 特點概覽
- 底層是鏈表數組,即數組存頭指針,鏈表存數據。
- key用set存放,所以重寫equals和hashcode後可以保證key不重複。
- 允許空鍵和空值,但是空鍵只能有一個,且放在第一位。
- 元素無序,且不定時改變。LinkedHashmap是有序的,以後再說。
- 插入獲取是O1。
- 遍歷Map需要的時間和數組長度成正比。
- 關鍵因子:初始容量,加載因子。
- HashTable的特點
- 多線程安全,不允許null
- HashMap的初始容量和加載因子
- 爲什麼這兩個因子影響很大。
- 因爲HashMap擴容需需要重新創建數組,重新哈希再分配,開銷很大。
- 加載因子默認爲0.75,太大則哈希衝突增多,插敘效率降低。太小的話頻繁rehash開銷太大。
- 初始容量默認爲16,可以自己設置,但是必須是2的整數次方。
- TODO:爲什麼必須是2的整數次方?
- 爲什麼這兩個因子影響很大。
- 方法解釋
- 4種構造函數
- HashMap(int initialCapacity,float loadFactor)。你任意傳入的initialCapacity都可以被轉化成與之最接近的2^n,是通過tableSizeFor()方法實現,此方法的核心算法是對給定容量-1後,將該值的每一位都變爲1,然後+1,就必然是2的整數倍。
- **transient Node<K,V>[] table;**就是鏈表數組。可以看到,其中的每個元素的類型是Node<K,V>
- Node是Map.Entry的一個實現類,沒想到吧,Entry其實是接口Map的內部接口。Node是一個鏈表的元素,包含常見next指針,value值,作爲哈希表中的元素,還包括key和hash。Node的hashcode是key的hashcode^value的hashcode
- put()。
- 4中構造函數裏面的無參構造方法在put第一個元素的時候才設定容量。如果table[]的對應位置沒有鏈表,則在該位置新建一個節點,換而言之,其實數組存放的是頭結點,頭結點中也是有值的。如果table[]位置已經有了,查看目前的數據結構是紅黑樹還是鏈表,然後插入。插入鏈表的時候注意,如果已經超過閾值(8)則轉換數據結構爲紅黑樹。如果超過閾值就擴容。
- hash()。
- 規定了key爲null的在table[]中的下標爲0且始終爲0.hash值的獲取通過將(h=key.hashcode)^(h>>>16)形成。這一步的目的是,將高16爲和低16位異或。爲了講解這個,我們不得不回到put中,put方法中如何通過hash值插入呢?插入的位置是hash&(n-1),n就是table的長度,即capacity。n一定是2的整數次方,那n-1的二進制表示一定全是1,hash&(n-1),我們知道&操作對1來說是保持自身,那相當於是hash的低x位(比如n-1是1111,那x=4)被保留下來了。好了,計算hash值的時候不做高16位和低16位的異或,我們知道一般table不會特別特別大, 那相當於hash的高位幾乎不會被用到,也就是說hash會不會被碰撞完全只受hash低x位的影響,這很不科學,丟失了hash高位的特徵。因此用高16位^低16位,保全他們獲得了全部的32位特徵。
- resize().
- 返回值爲新table[]。有可能是put引起的resize操作,此時一切默認創建一個table[]即可。如果確實是不夠了再擴充的,就變爲原來的兩倍。擴充的過程要遍歷老的table[],然後爲新的table[]複製,只有一個的時候利用hash值和enwCap確定j,直接放入table[j],有多個的時候就有講究了。
- 這是個騷操作,聚焦在兩點,1.e.hash&oldCap 2.newTab[j]=loHead,newTab[j+oldCap]=hihead.對第1點來說,明確如果oldCap-1是x個1,你家麼newCap必然是x+1個1,因爲newCap是oldCap的兩倍。因此hash&(oldCap-1)與hash&(newCap-1)的區別僅僅在多出來的那一位1.oldCap本題就是10…0,有x個90.即1位第x+1位,因此用e.hash&oldCap直接可以獲得它是否會呆在原來的地方。如果是0,表示還呆在原來的地方,即原來是j,現在還是j。如果是1,說明不在原來的地方了,應該重寫定位,用hash&(newCap-1)。但是開發JDK的人多騷啊,hash&(newCap-1)與hash&(oldCap-1)的區別就在x+1那一位上,反映到數組定位上,相當於偏移量多了oldcap。講解完畢。
- get().
- 雖然我們用get方法傳入的參數是key,但是其實table中定位的下標是hash(key),如果桶裏第一個就是要找的就返回,否則遍歷單鏈表或者二分查找紅黑樹。
- remove.
- 刪除對應節點,分從樹裏刪除和從鏈表刪除。
- 新增加方法:
- putIfAbsent,如果當前map不存在key或者key關聯的值爲null,就執行put(key,value)
- compute,可採用Lamdba表達啊是,將value計算後賦新值。
- containsKey和containsValue
- 看上去一樣的,但複雜度完全不同。key可以直接定位查詢,而value需要遍歷所有元素然後查詢。
- readObject和writeObject
- 這是爲了序列化。你想想看,如果採用默認的序列化,相當於把hash也序列化了,此時就糟了,因爲在不同的jvm裏面同一個key,同一個value對應的hashcode是不同的,那key就不同。所以每序列化一次都必須重新計算hash,這也是爲什麼要重寫這兩個方法。
- merge、replace、forEach()都在爲了Lambda表達式設計的,之後聊Lambda的時候再說具體。這些方法都用default關鍵詞修飾Map接口中的方法,接口的方法有方法體你敢信? 這是1.8新特性,如果接口中方法用default修飾則可以寫方法體,如果實現類不重寫則直接繼承該方法體。
- 4種構造函數
- HashMap中的內部類結構
- KeySet,繼承自AbstractSet。由此引出第一種遍歷方式。
- for(K key:map.keySet())。
- EntrySet,繼承自Map.Entry,記住住在table[]裏的Node也是繼承自Map.Entry。
- Iterator map1it=map.entrySet().iterator();while(map1it.hasNext());
- for(Map.Entry<K, V> entry: map.entrySet())
- 上下兩句等價,JVM會自動把第二句翻譯成第一句然後運行。
- values,繼承自abstractcollection。
- for(V v:map.values())
- 以上都是通過modCount來實現fast-fail。即讀取過程中不可以add或remove,適用於單線程、多線程情況下,但是隻用於參考。modCount不是volatile的,因此可見性得不到保障,線程不安全。
- KeySet,繼承自AbstractSet。由此引出第一種遍歷方式。
2.4. HashTable深入理解
- 概述
- 雖然HashTable是併發安全的,但總覺得是過時的,過時的原因是併發安全通過synchronized實現過於重量,沒有迭代器而是Enumeration,不夠強大。源碼中考慮的沒有HashMap那麼細,比如沒有紅黑樹,就是比較簡陋。在併發中還是使用concurrentHashMap吧。
- 方法分析
- 構造函數,默認初始容量爲11,加載因子0.75,table[]中的元素是Entry<K,V>而不是Node。hashcode()也不是key和value的hashcode()做異或,而是hash與value的hashcode()做異或。
- 沒有hash(),注意它沒有hash()。他是字節用key的hashcode對table[]的長度取餘得到的定位下標。
- rehash()用來擴容,將容量擴大到原來的2n+1倍,沒有樹也沒有鏈表兩開花的操作方式,就是單純的遍歷數據然後重哈希重放置,從table[] A放到table[] B中。太簡陋了8.
- contains()。HashMap中沒有這個方法,
- 爲什麼HashMap可以key可以有一個爲null呢,因爲其不需要null的hashcode,在hash()方法中如果爲null就是0,而在HashTabl中沒有hash()這個說法,直接就是key.hashcode(),如果key==null,就會報錯。
- 如何實現線程同步、併發安全
- 對方法加synchronized。
- 對內部類獲得的集合用Collections.synchronizedSet()返回線程安全的集合類。
- 將內部keySet,entrySet,values用volatile做修飾,保證可見性。
2.5. HashMap與Redis底層的Dict的區別
- Redis底層使用漸進式擴容,而hashmap是直接擴容。
3. ArrayList與LinkedList
3.1. ArrayList深入理解
- 特點概述
- 動態數組
- 有序,輸入和輸出順序一致。這裏的有序並不指大小順序。
- 元素可以爲null
- 讀寫操作爲O1,但是add操作爲On
- 屬性概覽
- Object[] elementData 動態數組裏的數組
- size 容量,默認容量爲10
- static final Object[] EMPTY_ELEMENTDATA 類變量,所有創建的空ArrayList共用這個空數組。
- static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};雖然大家都是空數組,但是EMPTY_ELEMENTDATA != DEFAULTCAPACITY_EMPTY_ELEMENTDATA,因爲他們指向的對象不同,因此就出現了騷操作。
- EMPTY_ELEMENTDATA是賦值給有參爲0的構造函數,或者複製構造函數參數的容量爲空的默認空數組,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是賦值給無參構造函數的空數組。
- 方法分析
- 三種構造函數。給定一個初始容量,新建一個對應容量的數組,注意size沒在這裏賦值,因爲capacity是容量而size是實際存儲數據的大小。
- add().
- 添加之前先判斷是否數組越界。如果是無參構造函數創建的ArrayList,第一次添加的時候就使用默認數組長度10或者添加的數量,取更大的那個,不然如果是EMPTY_ELEMENTDATA就容量就取需要添加的數量。如果發現數據長度比需要的容量要小,就調用grow()方法擴容
- grow().
- 是否要擴容,要看size+numNew和capacity的關係。擴容過程是沒超過1.5倍就擴充成1.5倍,超過了就擴充成需要的容量。比如目前容量是10,但是需求是11,就擴充到15,如果需求是20,那就擴充到20.
- ensureCapacity()手動擴容
- 這個方法是暴露出來讓你手動擴容的,指定你需要的容量。這個擴容之前也會判斷是無參構造器生成的還是有參構造器生成的。
- rangeCheck()與rangeCheckForAdd()
- 用於add(index,E e)進行越界警告
- readObject和writeObject
- ArrayList又不涉及到HashCode,爲什麼也要重寫呢?是因爲ArrayList中容量是大於size的,即容量爲10的數組採用默認序列化會全部序列化到文件中,哪怕數組只用了第一位,後面全是null,重寫的目的是爲了省空間
- 迭代器分析
- itr implements Iterator
- 常規迭代器
- Listitr extends itr implements ListIterator
- 雙向迭代器,還具有hasPrevious,previousIndex
- itr implements Iterator
- 與Vector的區別
- vector是併發安全的
- vector基礎大小爲10,構造函數與ArrayList不同
- vector擴容大小可以指定,如果是0,擴容後就是2倍,如果兩倍還不夠,就選擇當前需要的容量
- 沒有重寫readObject
- Stack是利用Vector的方法實現了Stack,但是現在應該用LinkedList來實現棧,而不是Stack
3.2. LinkedList深入理解
- 特點概述
- 本質上是雙向鏈表,可以當做隊列,雙端隊列或者棧使用。
- 繼承自AbstractSequentialList接口,同時實現了Deque,Queue接口
- 成員變量,均爲transient
- first
- last
- size
- 方法分析
- 兩種構造函數。無參構造函數爲空,有參構造函數將集合內全部通過addAll添加。
- add()
- 方法中調用linkLast,即通過last字段將數據放置到雙鏈表末尾
- add(int index,E e)
- 先要通過checkPositonIndex()判斷index是否合法,然後通過node(index)定位到index下標內的元素,這個定位算法有個騷操作,通過index與size/2的大小判斷在前一半還是後一半,前一半用頭指針找,後一半用尾指針找。找到node(index)後,將新的e插入到它前面去。
- get(index)
- 就是用之前的node(index).item即可
- readObject和WriteObject
- 其他沒什麼好說的,就是雙鏈表數據結構而已。
- 迭代器分析
- Listitr
4. LinkedHashMap深入理解
-
- 本質上就是HashMap然後將每個Node node通過before和after指針聯繫起來。
- 特點,實現了LRU算法。當你插入元素時,他會將節點插入雙向鏈表的鏈尾,如果key重複,也會將節點移動至鏈尾,當用get方法獲取value是也會將節點移動至鏈尾。
- 節點
- Entry extends HashMap.Entry,新增before和after指針。繼承了next指針,hash值,key和value值。
- 成員變量
- LinkedHashMap.Entry<K,V> head、tail。整個鏈表的頭尾指針,
- final boolean accessOrder。true按照access-order訪問順序,false按照insertion-order插入順序迭代linkedHashMap。
- 什麼是insertion-order呢?就是hashmap會像linkenlist一樣的順序保存數據,數據變成有序的,這個有序指的是按照插入順序,你迭代也是插入順序。
- 什麼是access-order呢?就是你put的和get的都會導致節點防止到鏈尾。所以只有配置access-order才能是LRU的,默認不開啓。
- 方法分析
- hashmap中定位的空方法,是留給ListedHashMap實現的。
- put。直接繼承自HashMap方法,那如何體現出構建雙鏈表的過程呢?put中要將新節點放入桶中,通過NewNode方法創建新節點,在LinkedHashMap中重寫了newNode方法,除了創建一個LinkedHashMap.Entry外,還用了linkNodeLast方法以維護每次插入過程都在鏈表尾部。那如何體現出更新節點也放置到鏈尾呢?put方法中會檢測key是否存在,如果存在就更改value,但是在linkedhashmap中更進一步,採用了afterNodeAccess方法,在HashMap中是空方法,但是在LinkedHashmap中被重寫。
- linkNodeLast(Node p)。一開始head和tail均爲null,當第一次put的時候,head和tail都指向p,否則讓tail.after=p;p.before=tail;tail=p.即更新尾節點。
- afterNodeAccess(Node p).從鏈表中取出p,重建p前,p後節點的關係,重建p與tail的關係即可。
- get。get操作在access-order爲真的情況下,執行afterNodeAccess方法,以放置末尾。
- removeEldestEntry。如果想實現LRU算法,重寫此函數即可。因爲在put方法的最後,調用的afterNodeInsertion()方法,在此方法中有removeEldestEntry方法。此方法默認false,即不調整鏈表長度,如果你重寫爲當大於某一值是爲真,那麼就相當於給定了LRU緩存的長度。
- remove,不僅要從hashmap裏面拿走,更重要的是從linkedlist裏面拿走。
- hashmap中定位的空方法,是留給ListedHashMap實現的。
- 疑問:HashMap中的紅黑樹怎麼解決呢?
- 不需要額外的解決方式,在newTreeNode方法的基礎上使用linkNodeLast即可,因爲雙向鏈表其實相當於是一個額外的數據結構,只要把這些節點串聯起來即可。
5. ashSet深入理解
- 基本要求
- key必須重寫hashcode和equals方法,並且equals返回ture,則hashcode必須相等,這個可以借鑑String,hashcode是用31進制對每個字母求和。
- 如果key已經存在,會直接覆蓋。
- 如果equals返回true但是hashcode不相等,因爲存儲是根據俄hashcode來的,因此還是會存放兩個key。
- 成員變量
- HashMap<E,Object> map;其中Object是一個私有類變量,因此可以理解成所有HashSet的實例共享一個object作爲value以節省空間。
- 利用HashMap的key的性質實現Set
- 採用HashSet(initialCapacity,loadFactor,boolean dummy)的方法,dummy只是爲了重載而已,可以返回一個linkedHashMap,實現有序的Set
6. PriorityQueue深入理解
- 基於堆數據結構實現的無界隊列。元素的優先順序基於實現Comparable接口或者構造函數中傳入Comparator比較器。默認是最小堆,且不接受null值,非線程安全。
- 成員變量
- DEFAULT_INITIAL_CAPACITY=11,默認隊列容量
- Object[] queue,用來實現最小堆
- size 元素個數
- Comparator comparator,比較器,可以通過構造函數傳入。如果構造函數不傳入,則採用數組元素自帶的大小順序,若無實現Comparable則報錯。
- 方法概覽
- 七種構造器,主要是指定排序方法,指定初始容量等。
- add,offer方法是同樣的,通過siftUp方法,插入到隊列的末尾,然後上浮到應該在的位置。
- siftUp又分爲利用比較器的和實現比較接口兩種,從下向上,如果比父親小,就交換,直到比父親大。(最小堆)
- peek,返回首元素。
- poll,返回並刪除首元素。先首尾元素交換,刪除微元素,然後將首元素從上向下下沉自應該存在的位置。
- 如果對方法有不理解,請參考我寫的通過java實現堆排序的博客
https://blog.csdn.net/w8253497062015/article/details/89496516
7. TreeMap與TreeSet深入理解
7.1. TreeMap
- 底層由紅黑樹實現,每個節點都是一個紅黑樹節點,自動排序,但是添加和查找的效率低於HashMap,優勢是根據排序規則保存有序狀態。
7.2. TreeSet
- TreeSet通過NavigableMap實現,其實底層用的還是TreeMap