Java集合-Collection

喝水會長肉發表於2021-11-24

Java集合-Collection

一、Collection繼承關係

Collection繼承關係

圖片來源

由上圖可知Collection有三個子類,分別是Set、List、Queue。
特點:
Set:無序且值唯一
List:有序、值可重複
Queue:先進先出的線性表

二、Collection提供的方法

Collection提供的方法

Collection提供了對集合的通用操作

三、Collection子類

1、Set

無序且值唯一。

Set子類有:

HashSet

底層資料結構是雜湊表(實際是hashMap),從建構函式可以看出在建立例項時會建立一個HashMap,該HashMap就是用來實際儲存元素的,除此之外在建立HashSet例項時我們可以指定其內部HashMap的容量和載入因子(預設大小為16,載入因子為0.75)

    public HashSet() {
        map = new HashMap<>();
    }

再來看下增刪查資料是如何實現的:

  • add操作
    public boolean add(E e) {
       //add是呼叫HashMap的put操作
        return map.put(e, PRESENT)==null;
    }
  • remove操作
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
  • contains操作
public boolean contains(Object o) {
    return map.containsKey(o);
}

HashSet如何來保證元素唯一性? 1.依賴兩個方法:hashCode()和equals()。

可選連結

TreeSet

TreeSet是一個非同步的非執行緒安全的二叉樹,底層資料結構是紅黑樹。(唯一,排序),其add , remove和contains操作的時間複雜度為log(n)

來看下預設建構函式:

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    
    private transient NavigableMap<E,Object> m;
    private static final Object PRESENT = new Object();
    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

可以看出其內部預設是使用TreeMap儲存元素的,因為其內部元素是有序的,對於元素的排序有兩種方式自然排序和比較器排序,自然排序就是當comparator為空的時候,構建無參建構函式的時候預設的一種排序方式,比較器排序就是在建構函式中傳入comparator從而指定排序方式。

        treeSet = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length()-o2.length();
            }
        });

TreeSet保證元素唯一性的是通過比較的返回值是否是0來決定

可選閱讀連結

LinkedHashSet

Set介面的雜湊表和連結列表實現即保證插入順序,(FIFO插入有序,唯一)由連結串列保證元素有序由雜湊表保證元素唯一。linkedHashSet是一個非執行緒安全的集合。如果有多個執行緒同時訪問當前linkedhashset集合容器,並且有一個執行緒對當前容器中的元素做了修改,那麼必須要在外部實現同步

來看下其建構函式

    public LinkedHashSet() {
        super(16, .75f, true);
    }
    
     HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

LinkedHashSet父類為HashSet,然後在HashSet的建構函式中建立了LinkedHashMap例項,也就是說LinkedHashSet最終是使用LinkedHashMap來儲存元素。

Set小結

我們簡紹了三種Set在實際使用時可以根據需求選擇合適的,同時我們也看到這三種Set的實現最終都是通過Map來儲存元素的。

2、List

List連結串列是一種線性結構,其內部元素有序(插入有序)、不唯一,可以根據索引來查詢獲取資料。

ArrayList

底層通過陣列實現,查詢快增刪慢,執行緒不安全。來看下建構函式

    transient Object[] elementData;
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

可以看到儲存元素的是一個叫做elementData的陣列。

  • add操作
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

我們看到在增加元素前會先呼叫 ensureCapacityInternal來確保陣列elementData有足夠的空間,如果空間不足會進行擴容操作。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //判斷是否需要擴容
        ensureExplicitCapacity(minCapacity);
    }
    
        private void ensureExplicitCapacity(int minCapacity) {
        //list的size增加
        modCount++;

        // 需要擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
        private void grow(int minCapacity) {
        // 當前陣列大小
        int oldCapacity = elementData.length;
        //擴容為原來的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);     //擴容後還不滿足所需最小容量則把容量設定為所需最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //MAX_ARRAY_SIZE的值為Integer.MAX_VALUE - 8表示最大可設定的值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 真正擴容操作是通過Arrays.copyOf來完成的
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // 溢位
            throw new OutOfMemoryError();
            //所需最小容量大於MAX_ARRAY_SIZE則擴容為Integer.MAX_VALUE
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

再來看下在指定位置插入元素的操作

    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        //檢查是否需要擴容
        ensureCapacityInternal(size + 1);  
        //把插入位置後面所有元素後移一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
         //插入元素                
        elementData[index] = element;
        size++;
    }
  • remove操作
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

可以看到remove中是根據equals來判斷元素是否是要刪除的,具體移除操作是通過fastRemove來完成。

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //把移除位置之後所有元素向前移動一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

總體來說ArrayList底層採用陣列儲存元素在元素增刪時通過copy陣列來實現元素移動,其增刪操作的時間複雜度為O(n)。

可選閱讀連結

Vector

底層陣列實現,查詢快增刪慢,執行緒安全。建構函式

    public Vector() {
        this(10);
    }
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    //這裡的capacityIncrement是指擴容時增加的容量
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

因為Vector底層也是陣列實現,所以在增刪資料時會涉及到陣列容量的變化,這跟ArrayList類似下面是Vector擴容的核心內容,可以看出其在容量不足時會增加capacityIncrement的容量,如果capacityIncrement<0則直接增加一倍的容量。

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

Vector實現跟ArrayList類似最大的不同在於Vector是執行緒安全的。

可選閱讀連結

stack

先進後出的結構,stack中peek函式是檢視棧頂元素但並不移除,pop是彈出棧頂元素。

其建構函式是空實現

    public Stack() {
    }
  • push操作
    public E push(E item) {
        addElement(item);
        return item;
    }
    
        public synchronized void addElement(E obj) {
        modCount++;
        //檢查是否需要擴容
        ensureCapacityHelper(elementCount + 1);
        //存入資料
        elementData[elementCount++] = obj;
    }
  • pop操作
    public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);
        return obj;
    }
        public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
           //移動資料
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        //將刪除位置置空
        elementData[elementCount] = null; /* to let gc do its work */
    }
  • peek操作
    public synchronized E peek() {
        int     len = size();
        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }
    
    public synchronized E elementAt(int index) {
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
        }
        return elementData(index);
    }
    
        E elementData(int index) {
        return (E) elementData[index];
    }

LinkedList

底層雙連結串列實現,查詢慢增刪快,執行緒不安全,LinkedList同時實現了List, Deque兩個介面也就是說它既可以作為list也可作為deque使用。

既然是雙連結串列則會有節點的概念,我們來看下它的Node,這是LinkedList的一個內部類。

       private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
  • add操作
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    //在表尾插入一個Node
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }
  • add(index,obj)
    public void add(int index, E element) {
        //檢查插入位置是否合法
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    //插入連結串列
     void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
  • remove操作

        public boolean remove(Object o) {
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
               //先查詢到要刪除節點
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                       //移除節點
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }
        
         E unlink(Node<E> x) {
            // assert x != null;
            final E element = x.item;
            final Node<E> next = x.next;
            final Node<E> prev = x.prev;
            //修改前驅指標
            if (prev == null) {
                first = next;
            } else {
                prev.next = next;
                x.prev = null;
            }
            //修改後繼指標
            if (next == null) {
                last = prev;
            } else {
                next.prev = prev;
                x.next = null;
            }
    
            x.item = null;
            size--;
            modCount++;
            return element;
        }
    

    可以看出LinkedList的資料操作大多都是連結串列的操作所以其特點是增刪快查詢慢,在類內部LinkedList維護了first和last兩個指標,這也是其能實現deque功能的基礎。在作為deque時offer表示在隊尾入隊一個元素,poll是出隊隊首一個元素,peek是檢視隊首元素但並不出隊。在作為deque時無法呼叫list相關介面方法。

3、Queue

佇列是一種先進先出的線性結構,不支援隨機訪問資料。

PriorityQueue

優先佇列是基於堆實現的,對內元素是有序的,offer,poll,remove和add等方法提供了O(log(n))的時間複雜度 ,而remove(obj)和contains方法的時間複雜度是線性時間,peek時間複雜度為O(1)。排序是通過自然排序和比較器排序實現的,採用哪種排序是通過建構函式確定的,其中自然排序要求元素實現compare函式,比較排序則需要在建構函式中指明排序規則。

預設建構函式

private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

 public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        //可以看出內部採用陣列儲存
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
 }
  • offer
    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            //擴容
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            //入隊
            siftUp(i, e);
        return true;
    }
    
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            //這裡以分析siftUpComparable為例
            siftUpComparable(k, x);
    }
    
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            //找k位置的父節點的index
            int parent = (k - 1) >>> 1;
            //k位置的父節點
            Object e = queue[parent];
            //調整堆,大於父節點的就不動,小於父節點的就上浮
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
  • poll操作
    public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            //調整堆
            siftDown(0, x);
        return result;
    }
    
        private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }
    
        private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

可選閱讀連結

ArrayDeque

雙端佇列,底層陣列實現。

預設建構函式

    public ArrayDeque() {
        //陣列大小預設16
        elements = new Object[16];
    }

因為可以雙端運算元據所以其內部採用head和tail來儲存頭尾元素的index這樣就可以快鎖找到頭尾元素。ArrayDeque還規定elements的size必須是2的整數次冪,當我們設定容量大小不是2的整數次冪時會進行調整

    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }
    
        private void allocateElements(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
        // Find the best power of two to hold elements.
        // Tests "<=" because arrays aren't kept full.
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

            if (initialCapacity < 0)    // Too many elements, must back off
                initialCapacity >>>= 1; // Good luck allocating 2^30 elements
        }
        elements = new Object[initialCapacity];
    }

 allocateElements實現思路如下:

 1.要明確2整數次冪使用二進位制的表現形式如下:0...010...0,中間有一個1,其它的都是0。

  2.根據1的形式,計算使輸入任意的X,等式成立的Y。X的二進位制形式為????????,是一個未知數,這樣如何求得Y呢?方法很簡單,找到X最高位為1的位置:那麼X就是0..001???,這種形式了。那麼所求的Y就是0..010...0,其值就是比X最高位為1再高一位為1,其它位為0的值。

  3.X的最高為1的那一位是未知的,如何求更高一位為1的Y呢?直接求是沒有辦法的,但是可以通過將X最高位為1後面所有位都變成1,再加1進位的方式辦到。就是0..001???變成0.001..1,使用這個+1就會變成所要的Y:0.010...0了。

  4.如何保證X最高位為1後面都是1呢?這個就是上面位運算所實現的內容了。假設X是0..01???,左移一位就是0.001??,做或運算就變成了0..011??,是不是很巧妙,出現了兩位為1的就移動2位,獲得四位為1的值,這樣移動到16的時候就涵蓋了32位整數的所有範圍了。這個時候+1可能發生整數溢位,所以再左移一位保證在整數範圍內。

參考

  • addFirst
    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
         //(head - 1) & (elements.length - 1)的作用是確定head的index
        elements[head = (head - 1) & (elements.length - 1)] = e;
        //首尾指向同一位置 擴容至原先兩倍大小
        if (head == tail)
            doubleCapacity();
    }
    
       private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n << 1;
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);
        System.arraycopy(elements, 0, a, r, p);
        elements = a;
        head = 0;
        tail = n;
    }
  • pollFirst
    public E pollFirst() {
        final Object[] elements = this.elements;
        final int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result != null) {
            elements[h] = null; // Must null out slot
            head = (h + 1) & (elements.length - 1);
        }
        return result;
    }

相關文章