基礎篇:JAVA集合,面試專用

潛行前行發表於2021-10-30

沒啥好說的,在座的各位都是靚仔

  • List 陣列
  • Vector 向量
  • Stack 棧
  • Map 對映字典
  • Set 集合
  • Queue 佇列
  • Deque 雙向佇列

關注公眾號,一起交流,微信搜一搜: 潛行前行

  • 一般佇列的通用方法
操作方法 丟擲異常 阻塞執行緒 返回特殊值 超時退出
插入元素 add(e) put(e) offer(e) offer(e, timeout, unit)
移除元素 remove() take() poll() pull(timeout, unit)
檢查 element() peek()

1 List 陣列

  • 元素按進入先後有序儲存,可重複
  • List有兩種底層實現,一種是陣列,一種是連結串列,而連結串列的實現繼承了 Collection。陣列和集合的區別:
    • 陣列大小是固定,集合是可變的
    • 陣列的元素可以基本型別也可以是引用型別,而集合只能是引用型別

ArrayList

  • ArrayList底層是使用一個可動態擴容的陣列,與普通陣列的區別就是它是沒有固定大小的限制,可以新增或刪除元素
  • 它的特點就是讀速度、更新快,增刪慢;記憶體相鄰,根據Index讀取的時間複雜度是O(1);可以儲存重複元素,但執行緒不安全
  • ArrayList 的擴容機制
//ArrayList openJDK 13
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) //放不下了
        elementData = grow(); // 擴容
    elementData[s] = e;
    size = s + 1;
}
private Object[] grow() {
    return grow(size + 1);
}
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity,  // minCapacity - oldCapacity == 1
                oldCapacity >> 1 ); // oldCapacity == 1/2 oldCapacity
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}
  • 如果當前元素放不下,則擴容至 1.5 倍,且大於等於 1
// ArraysSupport.newLength
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    //prefGrowth 是 oldLength 的 1/2,minGrowth 是 1。因此 newLength = 1.5 oldLength
    int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
    if (newLength - MAX_ARRAY_LENGTH <= 0) { // MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8
        return newLength;
    }
    return hugeLength(oldLength, minGrowth);
}

LinkedList

  • LinkedList的節點並不是儲存真實的資料,而是存下資料的引用物件,而且節點之間使用引用相關聯
  • LinkedList實現了Queue、Deque介面,可作為佇列使用;查詢慢,增刪快,可以儲存重複元素,但執行緒不安全
  • 使用 LinkedList 實現LRU
public static class LRU<T> {
    //預設的快取大小
    private  int CAPACITY = 0;
    //引用一個雙向連結表
    private LinkedList<T> list;
    //建構函式
    public LRU(int capacity) {
        this.CAPACITY = capacity;
        list = new LinkedList();
    }
    //新增一個元素
    public synchronized void put(T object) {
        if (list != null && list.contains(object)) {
            list.remove(object);
        }
        removeLeastVisitElement();
        list.addFirst(object);
    }
    //移除最近訪問次數最少的元素
    private void removeLeastVisitElement() {
        int size = list.size();
        //注意,這兒必須得是CAPACITY - 1否則所獲的size比原來大1
        if (size > (CAPACITY - 1)) {
            Object object = list.removeLast();
        }
    }
    //獲取第N個索引下面的元素
    public  T get(int index) {
        return list.get(index);
    }
}
  • LinkedList 的 API
public E getFirst() //獲取第一個元素
public E getLast()  //獲取最後一個元素
public E removeFirst() // 移除第一個元素,並返回
public E removeLast() // 移除最後一個元素,並返回
public void addFirst(E e) //加入頭部
public void addLast(E e)  //加入尾部
public void add(E e) //加入尾部
public boolean contains(Object o) //是否包含 元素 o
public E peek() //獲取頭部第一個元素
public E element()  // 獲取頭部第一個元素,不存在則報錯
public E poll() //獲取頭部第一個元素,並移除
public boolean offer(E e) // 呼叫 add(E e)
public boolean offerFirst(E e) // 呼叫 addFirst
public boolean offerLast(E e) // 呼叫 addLast
public void push(E e) //在頭部壓入一個元素
public E pop()  //彈出第一個元素,並移除。不存在則報錯
  • ArrayList 和 LinkedList 使用場景
    • 頻繁訪問列表中的某一個元素,或者需要在列表末尾進行新增和刪除元素操作,用ArrayList
    • 頻繁的在列表開頭、中間、末尾等位置進行新增和刪除元素操作,用LinkedList

Iterator 和 fast-fail、fail-safe機制

  • Java Iterator(迭代器)不是一個集合,它是一種用於訪問集合的方法,可用於迭代 List 和 Set 等集合,主要有hashNext(),next(),remove()三種方法
  • fail-fast 是Java集合(Collection)的一種錯誤機制。當多個執行緒對同一個集合進行修改結構操作,使用集合的迭代器iterator,會首先檢測是否有對集合的併發修改,進而產生ConcurrentModificationException 異常提示
  • fail-safe:保證在對任何集合結構的修改操作都基於 《複製-修改》 進行的,即先copy一個新的集合物件,然後對新的集合物件進行修改,最後將新的集合物件替換掉老的集合物件(老的集合物件的地址指向新的集合物件)。java.util.concurrent包下采用的是fail-safe機制。
    • 缺點1-對集合的複製copy會產生大量的物件,造成記憶體空間的浪費。
    • 缺點2-無法保證集合迭代過程中獲取的集合資料是最新的內容

CopyOnWriteArrayList

  • CopyOnWriteArrayList 的執行緒安全:CopyOnWriteArrayList 在寫的時候會加鎖,為了保證寫安全,會在寫操作時複製一個新陣列來操作,然後覆蓋舊的陣列;不會影響讀的效能
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    //可重入鎖
    final transient ReentrantLock lock = new ReentrantLock();
    //陣列,僅通過get和set方法操作
    private transient volatile Object[] array; 
    ....
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//拿到當前陣列
            int len = elements.length;//獲得長度
            //拷貝當前陣列
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements); //呼叫set方法將新陣列設定為當前陣列
            return true;
        } finally {
            lock.unlock();//解鎖
        }
    }
  • CopyOnWriteArrayList 的缺點
    • CopyOnWrite 在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體,導致記憶體的浪費
    • CopyOnWrite 容器只能保證資料的最終一致性,不能保證資料的實時一致性。如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器,沒有阻塞等待的概念
  • CopyOnWriteArrayList 和 Collections.synchronizedList 區別
    • CopyOnWriteArrayList 的寫操作效能較差,而多執行緒的讀操作效能較好
    • Collections.synchronizedList的寫操作效能比CopyOnWriteArrayList在多執行緒操作的情況下要好很多,而讀操作因為是採用了 synchronized關鍵字的方式,其讀操作效能並不如CopyOnWriteArrayList

執行緒安全的List

  • A:使用 Vector;B:使用 Collections.synchronized() 返回執行緒安全的 List;C:使用 CopyOnWriteArrayList

List的API示例

boolean contains(Object o) // 是否包含 o
boolean isEmpty(); // 是否為空
int size(); //集合元素
Iterator<E> iterator(); // 返回迭代器
Object[] toArray(); // 轉為 Object陣列
<T> T[] toArray(T[] a); // 轉為具體型別陣列
boolean add(E e); // 加入尾部
boolean remove(Object o); // 移除 o
boolean containsAll(Collection<?> c); //是否報考 集合 c
boolean addAll(Collection<? extends E> c);// 合併 c 
boolean retainAll(Collection<?> c);//保留只存在集合 c 的元素
void clear(); // 清除集合元素
void sort(Comparator<? super E> c) //根據 Comparator 排序
E get(int index); // 根據下標獲取 元素
E set(int index, E element); // 設定第 index 的元素
E remove(int index); // 移除 第 index 的元素
<E> List<E> of(E e1.....) // jdk 9
List<E> copyOf(Collection<? extends E> coll)  // 複製

2 Vector(向量)

ArrayList 和 Vector、LinkedList 的區別

  • Vector 相當於是 ArrayList 執行緒安全的翻版
  • Vector 繼承實現List 特點: 底層資料結構是陣列,查詢快,執行緒安全

Vector 的API示例

boolean synchronized contains(Object o);
boolean synchronized isEmpty();
boolean synchronized containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
public synchronized boolean add(E e)
public synchronized E get(int index);
public synchronized E set(int index, E element);
public synchronized E firstElement()
public synchronized void removeElementAt(int index)
public synchronized E lastElement()
public synchronized void setElementAt(E obj, int index)
public synchronized E remove(int index)
public void clear()
Iterator<E> iterator();

3 Stack(棧)

  • Stack 是 Vector提供的一個子類,用於模擬"棧"這種資料結構(LIFO後進先出)
  • 執行緒安全,允許 null 值

Stack 的API示例

public E push(E item) //推入棧頂
public synchronized E pop() // 彈出棧頂元素,不存在則報錯
public synchronized E peek() // 獲取棧頂元素,不移除
public boolean empty()  // 棧是否為空
public synchronized int search(Object o) // 搜尋元素 

4 Map

  • Map 用於儲存具有對映關係的資料,Map裡儲存著兩種對映的資料:key和value,它們都可以使任何引用型別的資料,但key不能重複。所以通過指定的key就可以取出對應的value
  • 請注意!!!Map 沒有繼承 Collection 介面

TreeMap(1.8JDK)

  • 繼承 AbstractMap,TreeMap 是基於紅黑樹實現,可保證在log(n)時間複雜度內完成 containsKey,get,put 和 remove 操作,效率很高。(紅黑數的原理這裡不展開講,後面會專門寫一篇)
  • 另一方面,由於 TreeMap 基於紅黑樹實現,因此 TreeMap 的鍵是有序的

HashMap

  • HashMap 繼承AbstractMap類實現了Map,是一個雜湊表,它儲存的內容是鍵值對(key-value)對映。HashMap 實現了 Map 介面,根據鍵的 HashCode 值儲存資料,具有很快的訪問速度,最多允許一條記錄的鍵為 null,不支援執行緒同步。HashMap 是無序的,即不會記錄插入的順序
  • HashMap如何處理hash衝突,hash衝突的幾種解決方法
    • 開放定址法
      • 線性探查在雜湊的時候,如果當前計算出的位置已經被儲存,那麼就順序的向後查詢,知道找到空位置或則是所有位置均不為空失敗
      • 二次探查使用一個輔助雜湊函式,使得後續的探查位置在前一個探查位置上加上一個偏移量,該偏移量以二次方的形式依賴於探查號i。二次探查的雜湊函式形式為:h(k,i)=(h'(k,i)+c1*i + c2 * i^2) mod m
      • 雙重雜湊使用兩個輔助雜湊函式h1和h2,初始的雜湊位置是h1(k),後續的雜湊位置在此基礎上增加一個偏移量h2(k)*i mod m
    • 鏈地址法
      • 鏈地址法-如果存在 hash 碰撞,則建立一連結串列儲存相同的元素
      • 開放定址法容易導致 hash 碰撞,查詢慢
  • HashMap 底層實現是陣列+連結串列+紅黑樹。空參的HashMap初始容量是16,預設載入因子為0.75。取值0.75是因為 0.5 容易浪費空間,取值 1 則需要填滿每一個桶,實際情況很難達到,會產生大量的雜湊碰撞。因此取中間值
  • HashMap 的容量一般是 2 的冪次方,可直接使用“位與”計算 hash 值,相對取模計算 hash 快

Hashtable

  • 繼承於Dictionary,現在基本已被淘汰
  • HashTable的操作幾乎和HashMap一致,主要的區別在於HashTable為了實現多執行緒安全,在幾乎所有的方法上都加上了synchronized鎖,而加鎖的結果就是HashTable操作的效率十分低下
  • HashMap允許有一個鍵為null,允許多個值為null;但HashTable不允許鍵或值為null
  • Hash對映:HashMap的hash演算法通過非常規設計,將底層table長度設計為2的冪,使用位與運算代替取模運算,減少運算消耗;而HashTable的hash演算法首先使得hash值小於整型數最大值,再通過取模進行散射運算

LinkedHashMap

  • LinkedHashMap的元素存取過程基本與HashMap基本類似,只是在細節實現上稍有不同。當然,這是由LinkedHashMap本身的特性所決定的,因為它額外維護了一個雙向連結串列用於保持迭代順序。此外,LinkedHashMap可以很好的支援LRU演算法。HashMap和雙向連結串列合二為一即是LinkedHashMap
    image.png

WeakHashMap

  • WeakHashMap 也是一個雜湊表,它儲存的內容也是鍵值對(key-value)對映,而且鍵和值都可以是 null
  • WeakHashMap的鍵是“弱鍵”。在 WeakHashMap 中,當某個 key 不再被強引用使用時,會被從WeakHashMap中被 JVM 自動移除,然後它對應的鍵值對也會被從WeakHashMap中移除。JAVA引用型別和ThreadLocal

ConcurrentHashMap(1.8JDK)

  • ConcurrentHashMap 是 HashMap 的多執行緒安全版本。它使用了細粒度鎖 和 cas 提高了在多執行緒環境的安全性和高併發
  • 底層資料結構是 陣列 + 連結串列/紅黑樹(後面專門寫一篇介紹)

ConcurrentSkipListMap 瞭解一波

  • ConcurrentSkipListMap 則是基於跳躍連結串列的實現的 map,使用了 cas 技術實現執行緒安全性,高併發
  • ConcurrentSkipListMap 相比 ConcurrentHashMap 的優點
    • ConcurrentSkipListMap 的key是有序的。
    • ConcurrentSkipListMap 支援更高的併發。ConcurrentSkipListMap的存取時間是log(N),和執行緒數幾乎無關。也就是說在資料量一定的情況下,併發的執行緒越多,ConcurrentSkipListMap 越能體現出它的優勢
  • TreeMap 實現了 NavigableMap 。ConcurrentNavigableMap 高併發執行緒安全版的 TreeMap
  • NavigableMap 提供了針對給定搜尋目標返回最接近匹配項的導航方法。直接看API
K lowerKey(K key)  // 找到第一個比指定的key小的值
K floorKey(K key)  // 找到第一個比指定的key小於或等於的key
K ceilingKey(K key) // 找到第一個大於或等於指定key的值
K higherKey(K key) // 找到第一個大於指定key的值
Map.Entry<K,V> firstEntry() // 獲取最小值
Map.Entry<K,V> lastEntry() // 獲取最大值
Map.Entry<K,V> pollFirstEntry() // 刪除最小的元素
Map.Entry<K,V> pollLastEntry() // 刪除最大的元素
NavigableMap<K,V> descendingMap() //返回一個倒序的Map 
// 返回值小於 toKey 的 NavigableMap
NavigableMap<K,V> headMap(K toKey, boolean inclusive)
// 返回值大於 fromKey 的 NavigableMap
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive)
// 返回值小於 toKey 大於 的 fromKey NavigableMap
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey,   boolean toInclusive)

5 Set(集合)

  • Set特點:元素無放入順序,元素不可重複,如果加入重複元素,會保留最先加入的物件。存取速度快

Set 的幾種實現子類和各自特點

  • TreeSet:底層資料結構採用二叉樹來實現,元素唯一且已經排好序;唯一性同樣需要重寫 hashCode 和 equals()方法,二叉樹結構保證了元素的有序
    • 根據構造方法不同,分為自然排序(無參構造)和比較器排序(有參構造),自然排序要求元素必須實現Compareable介面,並重寫裡面的compareTo()方法
  • HashSet:是雜湊表實現的,HashSet中的資料是無序的,可以放入 null,但只能放入一個null,兩者中的值都不能重複,就如資料庫中唯一約束
    • HashSet 是基於 HashMap 演算法實現的,其效能通常都優於TreeSet
    • 為快速查詢而設計的Set,我們通常都會用到HashSet,若需要排序的功能時,才使用TreeSet
  • LinkedHashSet:底層資料結構採用連結串列和雜湊表共同實現,連結串列保證了元素的順序與儲存順序一致,雜湊表保證了元素的唯一性,效率高。但執行緒不安全

ConcurrentSkipListSet

  • 基於 ConcurrentSkipListMap 實現

CopyOnWriteArraySet

  • 基於 CopyOnWriteArrayList 實現

BitSet

  • BitSet是位操作的物件,值只有 0 或 1 即false和true,內部維護了一個long陣列,初始只有一個long,所以BitSet最小的size是64,當隨著儲存的元素越來越多,BitSet內部會動態擴充,最終內部是由N個long來儲存
  • 如統計40億個資料中沒有出現的資料,將40億個不同資料進行排序等。\
  • 現在有1千萬個隨機數,隨機數的範圍在1到1億之間。現在要求寫出一種演算法,將1到1億之間沒有在隨機數中的數求出來
void and(BitSet set) // 兩個BitSet 做與操作,結果並存入當前 BitSet
void andNot(BitSet set) //  兩個BitSet 與非操作
void flip(int index) // 反轉某一個指定 index 
boolean intersects(BitSet bitSet) // 是否有交集
int cardinality() //返回 true/1 的個數
void clear() // 重置
void clear(int startIndex, int endIndex) // startIndex~endIndex 重置
int nextSetBit(int startIndex) //檢索在startIndex之後出現為1的第一位的索引
int nextClearBit(int startIndex) //檢索在startIndex之後出現為0的第一位的索引

6 Queue(佇列)

  • Queue的概念 佇列是一種特殊的線性表,只允許元素從佇列一端入隊,而另一端出隊(獲取元素),就像我們平時排隊結算一樣(懂文明講禮貌不插隊)。Queue 的資料結構和 List 一樣,可以基於陣列,連結串列實現,佇列通常都是一端進(offer),另一端出(poll),有序性

PriorityQueue

  • PriorityQueue是按優先順序排序的佇列,也就是說 vip 可以插隊。優先佇列要求使用 Java Comparable 和 Comparator 介面給物件排序,並且在排序時會按照優先順序處理其中的元素
  • PriorityBlockingQueue 是執行緒安全的PriorityQueue

BlockingQueue

  • BlockingQueue很好的解決了多執行緒中,如何高效安全“傳輸”資料的問題。通過這些高效並且執行緒安全的佇列類,為我們快速搭建高質量的多執行緒程式帶來極大的便利。常用於執行緒的任務佇列
  • DelayQueue
    • DelayQueue是一個沒有邊界BlockingQueue實現,加入元素必需實現Delayed介面。當生產者執行緒呼叫put之類的方法加入元素時,會觸發 Delayed 介面中的compareTo方法進行排序
    • 消費者執行緒檢視佇列頭部的元素,注意是檢視不是取出。然後呼叫元素的getDelay方法,如果此方法返回的值小0或者等於0,則消費者執行緒會從佇列中取出此元素,並進行處理。如果getDelay方法返回的值大於0,則消費者執行緒阻塞到第一元素過期

Queue 的 API

boolean add(E e); //加入佇列尾部
boolean offer(E e); // 加入佇列尾部,並返回結果
E remove(); //移除頭部元素
E poll();  // 獲取頭部元素,並移除
E element(); // 獲取頭部元素,不存在則報錯
E peek(); // 獲取頭部元素,不移除

7 Deque(雙向佇列)

  • Deque介面代表一個"雙端佇列",雙端佇列可以同時從兩端來新增、刪除元素,因此Deque的實現類既可以當成佇列使用、也可以當成棧使用
  • Deque 的子類 LinkedList,ArrayDeque,LinkedBlockingDeque

Deque的 API

void addFirst(E e); //加入頭部
void addLast(E e);  //加入尾部
boolean offerFirst(E e); //加入頭部,並返回結果
boolean offerLast(E e); //加入尾部,並返回結果
E removeFirst(); // 移除第一個元素
E removeLast(); // 移除最後一個元素
E getFirst(); //獲取第一個元素,不存在則報錯
E getLast();  //獲取最後一個元素,不存在則報錯
E pollFirst(); //獲取第一個元素,並移除
E pollLast(); //獲取最後一個元素,並移除
E peekFirst(); //獲取第一個元素
E peekLast(); // 獲取最後一個元素
void push(E e); //加入頭部
E pop(); //彈出頭部元素

歡迎指正文中錯誤

參考文章

相關文章