Java集合框架主要包括兩種類型的容器,一種是集合(Collection),存儲一個元素集合 ,另外一種是圖(Map),存儲鍵/值對映射。java
ArrayList
、LinkedList
、Vector
及Stack
。HashSet
、TreeSet
、LinkedHashSet
等HashMap
、TreeMap
、Hashtable
、LinkedHashMap
、ConcurrentHashMap
以及Properties
等ArrayList
底層使用的是動態數組,而 LinkedList
底層使用的是雙向鏈表。雙向鏈表ArrayList
比 LinkedList
在隨機訪問的時候效率要高,ArrayList
實現了 RandomAccess
接口,而LinkedList
是線性的數據存儲方式,因此須要移動指針從前日後依次查找。ArrayList
底層採用數組存儲,因此插入和刪除元素 的時間複雜度受元素位置的影響。 好比:執行add(E e)方法的時候, ArrayList
會默認在將指定的元素追加到此列表的末尾,這種狀況時間複雜度就是O(1)。可是若是要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間複雜度就爲 O(n-i)。由於在進 行上述操做的時候集合中第 i 和第 i 個元素以後的(n-i)個元素都要執行向後位/向前移一位的 操做。 LinkedList
底層採用鏈表存儲,因此對於add(E e)方法的插入,刪除元素時間複雜 度不受元素位置的影響,近似 O(1),若是是要在指定位置i插入和刪除元素的話 add(int index, E element)時間複雜度近似爲o(n)由於須要先移動到指定位置 再插入。LinkedList
比 ArrayList
更佔內存,由於 LinkedList 的節點除了存儲數據,還存儲了兩個引用,一個指向前一個元素,一個指向後一個元素。補充內容:RandomAccess接口node
public interface RandomAccess { }
查看源碼咱們發現實際上 RandomAccess 接口中什麼都沒有定義。因此,在我看來 RandomAccess 接口不過是一個標識罷了。標識什麼? 標識實現這個接口的類具備隨機訪問功能。面試
在 binarySearch( )方法中,它要判斷傳入的list 是否 RamdomAccess 的實例,若是是,調用 indexedBinarySearch() 方法,若是不是,那麼調用 iteratorBinarySearch() 方法算法
public static <T> int binarySearch(List<? extends Comparable<? super Tjk list, T key) { if (list instanceof RandomAccess || list.size() <BINARYSEARCH_THRESHOLD) return Collections.indexedBinarySearch(list, key); else } return Collections.iteratorBinarySearch(list, key);
ArrayList 實現了 RandomAccess 接口, 而 LinkedList 沒有實現。爲何呢?我以爲仍是 和底層數據結構有關! ArrayList 底層是數組,而 LinkedList 底層是鏈表。數組自然支持隨機 訪問,時間複雜度爲 O(1),因此稱爲快速隨機訪問。鏈表須要遍歷到特定位置才能訪問特定位置的 元素,時間複雜度爲 O(n),因此不支持快速隨機訪問。, ArrayList 實現了 RandomAccess 接 口,就代表了他具備快速隨機訪問功能。 RandomAccess 接口只是標識,並非說 ArrayList 實 現 RandomAccess 接口才具備快速隨機訪問功能的!編程
下面再總結一下 list 的遍歷方式選擇:數組
Vector
使用了 Synchronized 來實現線程同步,是線程安全的,而 ArrayList 是非線程安全的。ArrayList
在性能方面要優於 Vector
。ArrayList
和Vector
都會根據實際的須要動態的調整容量,只不過在 Vector 擴容每次會增長 1 倍,而 ArrayList 只會增長 50%。綜上所述:安全
補充說明:爲何 ArrayList 的 elementData 加上 transient 修飾數據結構
ArrayList 中的數組定義以下:多線程
private transient Object[] elementData;
再看一下 ArrayList 的定義:併發
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
能夠看到 ArrayList 實現了 Serializable 接口,這意味着 ArrayList 支持序列化。transient 的做用是說不但願 elementData 數組被序列化,重寫了 writeObject 實現:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ *// Write out element count, and any hidden stuff* int expectedModCount = modCount; s.defaultWriteObject(); *// Write out array length* s.writeInt(elementData.length); *// Write out all elements in the proper order.* for (int i=0; i<size; i++) s.writeObject(elementData[i]); if (modCount != expectedModCount) { throw new ConcurrentModificationException(); }
每次序列化時,先調用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,而後遍歷 elementData,只序列化已存入的元素,這樣既加快了序列化的速度,又減少了序列化以後的文件大小。
向HashSet 中add ()元素時,判斷元素是否存在的依據,不只要比較hash值,同時還要結合equles 方法比較。HashSet 中的add ()方法會使用HashMap 的put()方法。
HashMap 的 key 是惟一的,由源碼能夠看出 HashSet 添加進去的值就是做爲HashMap 的key,而且在HashMap中若是K/V相同時,會用新的V覆蓋掉舊的V,而後返回舊的V。因此不會重複( HashMap 比較key是否相等是先比較hashcode 再比較equals )。
如下是HashSet 部分源碼:
private static final Object PRESENT = new Object(); private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); } public boolean add(E e) { // 調用HashMap的put方法,PRESENT是一個至始至終都相同的虛值 return map.put(e, PRESENT)==null; }
hashCode()與equals()的相關規定:
深刻理解可參考:Java中hashCode() 和 equals()的問題解答
若是你看過 HashSet 源碼的話就應該知道:HashSet 底層就是基於 HashMap 實現的。(HashSet 的 源碼很是很是少,由於除了 clone() 、 writeObject() 、 readObject() 是 HashSet 本身不得不 實現以外,其餘方法都是直接調用 HashMap 中的方法。
HashMap | HashSet |
---|---|
實現了Map接口 | 實現Set接口 |
存儲鍵值對 | 僅存儲對象 |
調用put()向map中添加元素 | 調用add()方法向Set中添加元素 |
HashMap使用鍵(Key)計算Hashcode | HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false |
HashMap相對於HashSet較快,由於它是使用惟一的鍵獲取對象 | HashSet較HashMap來講比較慢 |
在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。「數組的特色是:尋址容易,插入和刪除困難;鏈表的特色是:尋址困難,但插入和刪除容易;*因此咱們將數組和鏈表結合在一塊兒,發揮二者各自的優點,使用一種叫作**拉鍊法」的方式能夠解決哈希衝突。
JDK1.8以前
JDK1.8 以前 HashMap 底層是 數組和鏈表 結合在一塊兒使用也就是 鏈表散列即採用的是拉鍊法。拉鍊法:將鏈表和數組相結合。也就是說建立一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希衝突,則將衝突的值加到鏈表中便可。
jdk1.7中HashMap數據結構
JDK1.8以後
相比於以前的版本,jdk1.8在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。
jdk1.8中HashMap數據結構
JDK1.7 VS JDK1.8 比較
JDK1.8主要解決或優化了一下問題:
當咱們put的時候,首先計算 key的hash值,這裏調用了 hash方法,hash方法實際是讓key.hashCode()與key.hashCode()>>>16進行異或操做,高16bit補0,一個數和0異或不變,因此 hash 函數大概的做用就是:高16bit不變,低16bit和高16bit作了一個異或,目的是減小碰撞。按照函數註釋,由於bucket數組大小是2的冪,計算下標index = (table.length - 1) & hash,若是不作 hash 處理,至關於散列生效的只有幾個低 bit 位,爲了減小散列的碰撞,設計者綜合考慮了速度、做用、質量以後,使用高16bit和低16bit異或來簡單處理減小碰撞,並且JDK8中用了複雜度 O(logn)的樹結構來提高碰撞下的性能。
putVal方法執行流程圖
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //實現Map.put和相關方法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 步驟①:tab爲空則建立 // table未初始化或者長度爲0,進行擴容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步驟②:計算index,並對null作處理 // (n - 1) & hash 肯定元素存放在哪一個桶中,桶爲空,新生成結點放入桶中(此時,這個結點是放在數組中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已經存在元素 else { Node<K,V> e; K k; // 步驟③:節點key存在,直接覆蓋value // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 將第一個元素賦值給e,用e來記錄 e = p; // 步驟④:判斷該鏈爲紅黑樹 // hash值不相等,即key不相等;爲紅黑樹結點 // 若是當前元素類型爲TreeNode,表示爲紅黑樹,putTreeVal返回待存放的node, e可能爲null else if (p instanceof TreeNode) // 放入樹中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 步驟⑤:該鏈爲鏈表 // 爲鏈表結點 else { // 在鏈表最末插入結點 for (int binCount = 0; ; ++binCount) { // 到達鏈表的尾部 //判斷該鏈表尾部指針是否是空的 if ((e = p.next) == null) { // 在尾部插入新結點 p.next = newNode(hash, key, value, null); //判斷鏈表的長度是否達到轉化紅黑樹的臨界值,臨界值爲8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //鏈表結構轉樹形結構 treeifyBin(tab, hash); // 跳出循環 break; } // 判斷鏈表中結點的key值與插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環 break; // 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表 p = e; } } //判斷當前的key已經存在的狀況下,再來一個相同的hash值、key值時,返回新來的value這個值 if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent爲false或者舊值爲null if (!onlyIfAbsent || oldValue == null) //用新值替換舊值 e.value = value; // 訪問後回調 afterNodeAccess(e); // 返回舊值 return oldValue; } } // 結構性修改 ++modCount; // 步驟⑥:超過最大容量就擴容 // 實際大小大於閾值則擴容 if (++size > threshold) resize(); // 插入後回調 afterNodeInsertion(evict); return null; }
①.判斷鍵值對數組table[i]是否爲空或爲null,不然執行resize()進行擴容;
②.根據鍵值key計算hash值獲得插入的數組索引i,若是table[i]==null,直接新建節點添加,轉向⑥,若是table[i]不爲空,轉向③;
③.判斷table[i]的首個元素是否和key同樣,若是相同直接覆蓋value,不然轉向④,這裏的相同指的是hashCode以及equals;
④.判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對,不然轉向⑤;
⑤.遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操做,不然進行鏈表的插入操做;遍歷過程當中若發現key已經存在直接覆蓋value便可;
⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,若是超過,進行擴容。
①.在jdk1.8中,resize方法是在hashmap中的鍵值對大於閥值時或者初始化時,就調用resize方法進行擴容;
②.每次擴展的時候,都是擴展2倍;
③.擴展後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。
在putVal()中,咱們看到在這個函數裏面使用到了2次resize()方法,resize()方法表示的在進行第一次初始化時會對其進行擴容,或者當該數組的實際大小大於其臨界值值(第一次爲12),這個時候在擴容的同時也會伴隨的桶上面的元素進行從新分發,這也是JDK1.8版本的一個優化的地方,在1.7中,擴容以後須要從新去計算其Hash值,根據Hash值對其進行分發,但在1.8版本中,則是根據在同一個桶的位置中進行判斷(e.hash & oldCap)是否爲0,從新進行hash分配後,該元素的位置要麼停留在原始位置,要麼移動到原始位置+增長的數組大小這個位置上
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//oldTab指向hash桶數組 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) {//若是oldCap不爲空的話,就是hash桶數組不爲空 if (oldCap >= MAXIMUM_CAPACITY) {//若是大於最大容量了,就賦值爲整數最大的閥值 threshold = Integer.MAX_VALUE; return oldTab;//返回 }//若是當前hash桶數組的長度在擴容後仍然小於最大容量 而且oldCap大於默認值16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 雙倍擴容閥值threshold } // 舊的容量爲0,但threshold大於零,表明有參構造有cap傳入,threshold已經被初始化成最小2的n次冪 // 直接將該值賦給新的容量 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 無參構造建立的map,給出默認容量和threshold 16, 16*0.75 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新的threshold = 新的cap * 0.75 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 計算出新的數組長度後賦給當前成員變量table @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶數組 table = newTab;//將新數組的值複製給舊的hash桶數組 // 若是原先的數組沒有初始化,那麼resize的初始化工做到此結束,不然進入擴容元素重排邏輯,使其均勻的分散 if (oldTab != null) { // 遍歷新數組的全部桶下標 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 舊數組的桶下標賦給臨時變量e,而且解除舊數組中的引用,不然就數組沒法被GC回收 oldTab[j] = null; // 若是e.next==null,表明桶中就一個元素,不存在鏈表或者紅黑樹 if (e.next == null) // 用一樣的hash映射算法把該元素加入新的數組 newTab[e.hash & (newCap - 1)] = e; // 若是e是TreeNode而且e.next!=null,那麼處理樹中元素的重排 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // e是鏈表的頭而且e.next!=null,那麼處理鏈表中元素重排 else { // preserve order // loHead,loTail 表明擴容後不用變換下標,見注1 Node<K,V> loHead = null, loTail = null; // hiHead,hiTail 表明擴容後變換下標,見注1 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 遍歷鏈表 do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) // 初始化head指向鏈表當前元素e,e不必定是鏈表的第一個元素,初始化後loHead // 表明下標保持不變的鏈表的頭元素 loHead = e; else // loTail.next指向當前e loTail.next = e; // loTail指向當前的元素e // 初始化後,loTail和loHead指向相同的內存,因此當loTail.next指向下一個元素時, // 底層數組中的元素的next引用也相應發生變化,形成lowHead.next.next..... // 跟隨loTail同步,使得lowHead能夠連接到全部屬於該鏈表的元素。 loTail = e; } else { if (hiTail == null) // 初始化head指向鏈表當前元素e, 初始化後hiHead表明下標更改的鏈表頭元素 hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 遍歷結束, 將tail指向null,並把鏈表頭放入新數組的相應下標,造成新的映射。 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
什麼是哈希?
Hash,通常翻譯爲「散列」,也有直接音譯爲「哈希」的,這就是把任意長度的輸入經過散列算法,變換成固定長度的輸出,該輸出就是散列值(哈希值);這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來惟一的肯定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。
全部散列函數都有以下一個基本特性:根據同一散列函數計算出的散列值若是不一樣,那麼輸入值確定也不一樣。可是,根據同一散列函數計算出的散列值若是相同,輸入值不必定相同。
什麼是哈希衝突?
當兩個不一樣的輸入值,根據同一散列函數計算出相同的散列值的現象,咱們就把它叫作碰撞(哈希碰撞)。
這樣咱們就能夠將擁有相同哈希值的對象組織成一個鏈表放在hash值所對應的bucket下,但相比於hashCode返回的int類型,咱們HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要遠小於int類型的範圍,因此咱們若是隻是單純的用hashCode取餘來獲取對應的bucket這將會大大增長哈希碰撞的機率,而且最壞狀況下還會將HashMap變成一個單鏈表,因此咱們還須要對hashCode做必定的優化。
hash()函數
上面提到的問題,主要是由於若是使用hashCode取餘,那麼至關於參與運算的只有hashCode的低位,高位是沒有起到任何做用的,因此咱們的思路就是讓hashCode取值出的高位也參與運算,進一步下降hash碰撞的機率,使得數據分佈更平均,咱們把這樣的操做稱爲擾動,在JDK 1.8中的hash()函數以下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 與本身右移16位進行異或運算(高低位異或) }
這比在JDK 1.7中,更爲簡潔,相比在1.7中的4次位運算,5次異或運算(9次擾動),在1.8中,只進行了1次位運算和1次異或運算(2次擾動);
「JDK1.8新增紅黑樹」
經過上面的鏈地址法(使用散列表)和擾動函數咱們成功讓咱們的數據分佈更平均,哈希碰撞減小,可是當咱們的HashMap中存在大量數據時,加入咱們某個bucket下對應的鏈表有n個元素,那麼遍歷時間複雜度就爲O(n),爲了針對這個問題,JDK1.8在HashMap中新增了紅黑樹的數據結構,進一步使得遍歷複雜度下降至O(logn);總結 簡單總結一下HashMap是使用了哪些方法來有效解決哈希衝突的:
答:hashCode()方法返回的是int整數類型,其範圍爲-(2 ^ 31)~(2 ^ 31 - 1),約有40億個映射空間,而HashMap的容量範圍是在16(初始化默認值)~2 ^ 30,HashMap一般狀況下是取不到最大值的,而且設備上也難以提供這麼多的存儲空間,從而致使經過hashCode()計算出的哈希值可能不在數組大小範圍內,進而沒法匹配存儲位置;
那怎麼解決呢?
爲了能讓 HashMap 存取高效,儘可能較少碰撞,也就是要儘可能把數據分配均勻,每一個鏈表/紅黑樹長度大體相同。這個實現就是把數據存到哪一個鏈表/紅黑樹中的算法。
這個算法應該如何設計呢?
咱們首先可能會想到採用%取餘的操做來實現。可是,重點來了:「取餘(%)操做中若是除數是2的冪次則等價於與其除數減一的與(&)操做(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。」 而且 採用二進制位操做 &,相對於%可以提升運算效率,這就解釋了 HashMap 的長度爲何是2的冪次方。
那爲何是兩次擾動呢?
答:這樣就是加大哈希值低位的隨s機性,使得分佈更均勻,從而提升對應數組存儲下標位置的隨機性&均勻性,最終減小Hash衝突,兩次就夠了,已經達到了高位低位同時參與運算的目的;
HashMap
是非線程安全的,HashTable
是線程安全的;HashTable 內部的方法基本都通過 synchronized 修飾。(多線程可使用 ConcurrentHashMap 吧!);ConcurrentHashMap 和 Hashtable 的區別主要體如今實現線程安全的方式上不一樣。
「二者的對比圖」:
HashTable:
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 鏈表節點):
答:ConcurrentHashMap 結合了 HashMap 和 HashTable 兩者的優點。HashMap 沒有考慮同步,HashTable 考慮了同步的問題。可是 HashTable 在每次同步執行時都要鎖住整個結構。ConcurrentHashMap 鎖的方式是稍微細粒度的。
JDK1.7
首先將數據分爲一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其餘段的數據也能被其餘線程訪問。
在JDK1.7中,ConcurrentHashMap採用Segment + HashEntry的方式進行實現,結構以下:
一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和HashMap相似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每一個 HashEntry 是一個鏈表結構的元素,每一個 Segment 守護着一個HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment的鎖。
JDK1.8
在JDK1.8中,放棄了Segment臃腫的設計,取而代之的是採用Node + CAS + Synchronized來保證併發安全進行實現,synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提高N倍。
結構以下:
對於基本類型數據,集合使用自動裝箱來減小編碼工做量。可是,當處理固定大小的基本數據類型的時候,這種方式相對比較慢。
通常咱們須要對一個集合使用自定義排序時,咱們就要重寫compareTo方法或compare方法,當咱們須要對某一個集合實現兩種排序方式,好比一個song對象中的歌名和歌手名分別採用一種排序方法的話,咱們能夠重寫compareTo方法和使用自制的Comparator方法或者以兩個Comparator來實現歌名排序和歌星名排序,第二種表明咱們只能使用兩個參數版的Collections.sort()。
Comparator定製排序
ArrayList<Integer> arrayList = new ArrayList<Integer>(); arrayList.add(-1); arrayList.add(3); arrayList.add(3); arrayList.add(-5); arrayList.add(7); arrayList.add(4); arrayList.add(-9); arrayList.add(-7); System.out.println("原始數組:"); System.out.println(arrayList); // void reverse(List list):反轉 Collections.reverse(arrayList); System.out.println("Collections.reverse(arrayList):"); System.out.println(arrayList); // void sort(List list),按天然排序的升序排序 Collections.sort(arrayList); System.out.println("Collections.sort(arrayList):"); System.out.println(arrayList); // 定製排序的用法 Collections.sort(arrayList, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } }); System.out.println("定製排序後:"); System.out.println(arrayList);
原始數組: [-1, 3, 3, -5, 7, 4, -9, -7] Collections.reverse(arrayList): [-7, -9, 4, 7, -5, 3, 3, -1] Collections.sort(arrayList): [-9, -7, -5, -1, 3, 3, 4, 7] 定製排序後: [7, 4, 3, 3, -1, -5, -7, -9]
重寫compareTo方法實現按年齡來排序
// person對象沒有實現Comparable接口,因此必須實現,這樣纔不會出錯,纔可 以使treemap中的數據按順序排列 // 前面一個例子的String類已經默認實現了Comparable接口,詳細能夠查看 String類的API文檔,另外其餘 // 像Integer類等都已經實現了Comparable接口,因此不須要另外實現了 public class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } /** * TODO重寫compareTo方法實現按年齡來排序 */ @Override public int compareTo(Person o) { if (this.age > o.getAge()) { return 1; } else if (this.age < o.getAge()) { return -1; } return age; } }
public static void main(String[] args) { TreeMap<Person, String> pdata = new TreeMap<Person, String>(); pdata.put(new Person("張三", 30), "zhangsan"); pdata.put(new Person("李四", 20), "lisi"); pdata.put(new Person("王五", 10), "wangwu"); pdata.put(new Person("小紅", 5), "xiaohong"); // 獲得key的值的同時獲得key所對應的值 Set<Person> keys = pdata.keySet(); for (Person key : keys) { System.out.println(key.getAge() + "-" + key.getName()); } }
5-小紅 10-王五 20-李四 30-張三
TreeSet 要求存放的對象所屬的類必須實現 Comparable 接口,該接口提供了比較元素的 compareTo()方法,當插入元素時會回調該方法比較元素的大小。TreeMap 要求存放的鍵值對映射的鍵必須實現 Comparable 接口從而根據鍵對元素進 行排 序。
Collections 工具類的 sort 方法有兩種重載的形式,
第一種要求傳入的待排序容器中存放的對象比較實現 Comparable 接口以實現元素的比較;
第二種不強制性的要求容器中的元素必須可比較,可是要求傳入第二個參數,參數是Comparator 接口的子類型(須要重寫 compare 方法實現元素的比較),至關於一個臨時定義的排序規則,其實就是經過接口注入比較元素大小的算法,也是對回調模式的應用(Java 中對函數式編程的支持)。
文章參考:
看到這裏今天的分享就結束了,若是以爲這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~
歡迎關注我的公衆號 「JavaClub」,按期爲你分享一些面試乾貨。