1. 程式人生 > 實用技巧 >java後端知識點梳理——java集合

java後端知識點梳理——java集合

集合概覽

Java中的集合,從上層介面上看分為了兩類,Map和Collection。Map是和Collection並列的集合上層介面,沒有繼承關係。

Java中的常見集合可以概括如下。

  • Map介面和Collection介面是所有集合框架的父介面
  • Collection介面的子介面包括:Set介面和List介面
  • Map介面的實現類主要有:HashMap、TreeMap、HashtableLinkedHashMap、ConcurrentHashMap以及Properties等
  • Set介面的實現類主要有:HashSet、TreeSet、LinkedHashSet等
  • List介面的實現類主要有:ArrayList、LinkedList、Stack以及Vector等

HashMap和Hashtable的區別有哪些?

  • HashMap沒有考慮同步,是執行緒不安全的;Hashtable使用了synchronized關鍵字,是執行緒安全的;
  • HashMap允許null作為Key;Hashtable不允許null作為Key,Hashtable的value也不可以為null

HashMap是執行緒不安全的是吧?你可以舉一個例子嗎?

先別說快速失敗機制

  • HashMap執行緒不安全主要是考慮到了多執行緒環境下進行擴容可能會出現HashMap死迴圈
  • Hashtable執行緒安全是由於其內部實現在put和remove等方法上使用synchronized進行了同步,所以對單個方法的使用是執行緒安全的。但是對多個方法進行復合操作時,執行緒安全性無法保證。 比如一個執行緒在進行get然後put更新的操作,這就是兩個複合操作,在兩個操作之間,可能別的執行緒已經對這個key做了改動,所以,你接下來的put操作可能會不符合預期。

快速失敗(fast-fail)機制

快速失敗是Java集合的一種錯誤檢測機制,當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast。

例如

假設存在兩個執行緒(執行緒1 、執行緒2) , 執行緒1通過Iterator 在遍歷集合 A 中的元素, 在某個時候執行緒2修改了集合A 的結構(是結構上面的修改,而 不是簡單的修改集合元素的內容), 那麼這個時候程式就會丟擲異常從而產生快速失敗機制。

原因

迭代器在遍歷時直接訪問集合中的內容, 並且在遍歷過程中使用—個 modCount 變數。集合在被遍歷期間如果內容發生變化, 就會改變modCount 的值。每當迭代器使用hashNext()/next()遍歷下一個元素之前, 都會檢測 modCount 變數是否為expectedmodCount 值,是的話就返回遍歷; 否則丟擲異常, 終止遍歷。

解決方法

  • 在遍歷過程中, 所有涉及到改變modCount 值得地方全部加上synchronized。
  • 使用執行緒安全的集合

hashmap

hashmap推薦看我的另一篇文章:

hashmap專題

這裡簡單補充一下一致性Hash演算法

一致性Hash:

​客戶端分片:雜湊+順時針(優化取餘)
節點伸縮:隻影響鄰近節點,但是還是有資料遷移
​翻倍伸縮:保證最小遷移資料和負載均衡

​一致性Hash可以很好的解決穩定問題,可以將所有的儲存節點排列在收尾相接的Hash環上,每個key在計算Hash後會順時針找到先遇到的一組儲存節點存放。而當有節點加入或退出時,僅影響該節點在Hash環上順時針相鄰的後續節點,將資料從該節點接收或者給予。但這有帶來均勻性的問題,即使可以將儲存節點等距排列,也會在儲存節點個數變化時帶來資料的不均勻。而這種可能成倍數的不均勻在實際工程中是不可接受的。

【未完待續】

ConcurrentHashMap和Hashtable的區別?

ConcurrentHashMap結合了HashMap和Hashtable二者的優勢。HashMap沒有考慮同步,Hashtable考慮了同步的問題。但是Hashtable在每次同步執行時都要鎖住整個結構。

ConcurrentHashMap鎖的方式是稍微細粒度的,ConcurrentHashMap將hash表分為16個桶(預設值),諸如get,put,remove等常用操作只鎖上當前需要用到的桶。

ConcurrentHashMap的具體實現方式(分段鎖)

該類包含兩個靜態內部類MapEntry和Segment,前者用來封裝對映表的鍵值對,後者用來充當鎖的角色。

segment

Segment是一種可重入的鎖ReentrantLock,每個Segment守護一個HashEntry數組裡得元素,當對HashEntry陣列的資料進行修改時,必須首先獲得對應的Segment鎖。

在JDK1.7及其之前ConcurrentHashMap實現執行緒安全的方法相對比較簡單:

  • 其內部將資料分為數個“段(Segment)”,其數量和併發級別有關係,具體是“大於等於併發級別的最小的2的冪次”。
  • 每個segment使用單獨的ReentrantLock(分段鎖)。
  • 如果操作涉及不同segment,則可以併發執行,如果是同一個segment則會進行鎖的競爭和等待。
  • 此設計的效率是高於synchronized的。

不過JDK8之後,ConcurrentHashMap捨棄了ReentrantLock,而重新使用了synchronized。其原因大致有一下幾點:

  • 加入多個分段鎖浪費記憶體空間。
  • 生產環境中, map 在放入時競爭同一個鎖的概率非常小,分段鎖反而會造成更新等操作的長時間等待。
  • 為了提高 GC 的效率

新的ConcurrentHashMap中使用synchronized關鍵字+CAS操作保證了執行緒安全。

TreeMap有哪些特性?

TreeMap底層使用紅黑樹實現,TreeMap中儲存的鍵值對按照鍵來排序。

  • 如果Key存入的是字串等型別,那麼會按照字典預設順序排序
  • 如果傳入的是自定義引用型別,比如說User,那麼該物件必須實現Comparable介面,並且覆蓋其compareTo方法;或者在建立TreeMap的時候,我們必須指定使用的比較器。

如下所示:

// 方式一:定義該類的時候,就指定比較規則
class User implements Comparable{
    @Override
    public int compareTo(Object o) {
        // 在這裡邊定義其比較規則
        return 0;
    }
}
public static void main(String[] args) {
    // 方式二:建立TreeMap的時候,可以指定比較規則
    new TreeMap<User, Integer>(new Comparator<User>() {
        @Override
        public int compare(User o1, User o2) {
            // 在這裡邊定義其比較規則
            return 0;
        }
    });
}

引申:那麼Comparable介面和Comparator介面有哪些區別呢?

  • Comparable實現比較簡單,但是當需要重新定義比較規則的時候,必須修改原始碼,即修改User類裡邊的compareTo方法
  • Comparator介面不需要修改原始碼,只需要在建立TreeMap的時候重新傳入一個具有指定規則的比較器即可。

補充:comparable 和comparator的區別?

comparable接口出自java.lang包,它有一個compareTo(Object obj)方法用來排序
comparator接口出自java.util包,它有一個compare(Object obj1 Object obj2) 方法用來排序

ArrayList和LinkedList有哪些區別?

  • ArrayList底層使用了動態陣列實現,實質上是一個動態陣列
  • LinkedList底層使用了雙向連結串列實現,可當作堆疊、佇列、雙端佇列使用
  • ArrayList在隨機存取方面效率高於LinkedList
  • LinkedList在節點的增刪方面效率高於ArrayList
  • ArrayList必須預留一定的空間,當空間不足的時候,會進行擴容操作
  • LinkedList的開銷是必須儲存節點的資訊以及節點的指標資訊

其實還有一個集合Vector,它是執行緒安全的ArrayList,但是已經被廢棄,不推薦使用了。多執行緒環境下,我們可以使用CopyOnWriteArrayList替代ArrayList來保證執行緒安全。

HashSet和TreeSet有哪些區別?

  • HashSet底層使用了Hash表實現。
    • 保證元素唯一性的原理:判斷元素的hashCode值是否相同。如果相同,還會繼續判斷元素的equals方法,是否為true
  • TreeSet底層使用了紅黑樹來實現。
    • 保證元素唯一性是通過Comparable或者Comparator介面實現

HashSet和HashMap

其實,HashSet的底層實現還是HashMap,只不過其只使用了其中的Key,具體如下所示:

  • HashSet的add方法底層使用HashMap的put方法將key = e,value=PRESENT構建成key-value鍵值對,當此e存在於HashMap的key中,則value將會覆蓋原有value,但是key保持不變,所以如果將一個已經存在的e元素新增中HashSet中,新新增的元素是不會儲存到HashMap中,所以這就滿足了HashSet中元素不會重複的特性。
  • HashSet的contains方法使用HashMap得containsKey方法實現

LinkedHashMap和LinkedHashSet有了解嗎?

LinkedHashMap內部的Entry繼承於HashMap.Node,這兩個類都實現了Map.Entry<K,V>
LinkedHashMap的Entry不光有value,next,還有before和after屬性,這樣通過一個雙向連結串列,保證了各個元素的插入順序
通過構造方法public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder), accessOrder傳入true可以實現LRU快取演算法(訪問順序)
LinkedHashSet 底層使用LinkedHashMap實現,兩者的關係類似與HashMap和HashSet的關係,大家可以自行類比。

什麼是LRU演算法?LinkedHashMap如何實現LRU演算法?

LRU(Least recently used,最近最少使用)演算法根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是“如果資料最近被訪問過,那麼將來被訪問的機率也更高”。思路如下

  1. 新資料插入到連結串列頭部;
  2. 每當快取命中(即快取資料被訪問),則將資料移到連結串列頭部;
  3. 當連結串列滿的時候,將連結串列尾部的資料丟棄。

關於【命中率】

當存在熱點資料時,LRU的效率很好,但偶發性的、週期性的批量操作會導致LRU命中率急劇下降,快取汙染情況比較嚴重。

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUTest {

    private static int size = 5;

    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<String, String>(size, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > size;
            }
        };
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");
        map.put("5", "5");
        System.out.println(map.toString());

        map.put("6", "6");
        System.out.println(map.toString());
        map.get("3");
        System.out.println(map.toString());
        map.put("7", "7");
        System.out.println(map.toString());
        map.get("5");
        System.out.println(map.toString());
    }
}

List和Set的區別?

  • List是有序的並且元素是可以重複的
  • Set是無序(LinkedHashSet除外)的,並且元素是不可以重複的

說明: 此處的有序和無序是指放入順序和取出順序是否保持一致

Iterator和ListIterator的區別是什麼?

  • Iterator可以遍歷list和set集合;ListIterator只能用來遍歷list集合
  • Iterator前者只能前向遍歷集合;ListIterator可以前向和後向遍歷集合
  • ListIterator其實就是實現了前者,並且增加了一些新的功能。

Iterato參考程式碼:

ArrayList<String> list =  new ArrayList<>();
list.add("zhangsan");
list.add("lisi");
list.add("yangwenqiang");
// 建立迭代器實現遍歷集合
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}

Collection和Collections有什麼關係?

Collection和Collections的關係: Collection是一個頂層集合介面,其子介面包括List和Set;而Collections是一個集合工具類,可以操作集合,比如說排序,二分查詢,拷貝集合,尋找最大最小值等。 總而言之:帶s的大都是工具類。

說在最後

附上集合介面和實現類的關係

如果感興趣,可以配合我的java集合面試題來看。