Java中關於泛型集合類存儲的總結


集合類存儲在任何編程語言中都是很重要的內容,只因有這樣的存儲數據結構才讓我們可以在內存中輕易的操作數據,那么在Java中這些存儲類集合結構都有哪些?內部實現是怎么樣?有什么用途呢?下面分享一些我的總結

集合類存儲結構的種類及其繼承關系圖

圖中只列出了比較關鍵的繼承關系,在Java中所有的集合類都實現Collection 接口,在直接的繼承關系中主要分為兩大接口:一個是列表實現的List 接口,另一個是集合實現的Set 接口。在列表中最為常用的實現類是ArrayList 和LinkedList 。在集合中最為常用的實現類則是HashSet 和LinkedHashSet 。雖然這些具體的實現有所不同,但所包含的操作卻大致相同。Collection 又擴展了Iterator 接口為各個實現類提供遍歷功能。下面我們分別描述各個實現類實現原理和用途。
注: 只能是引用類型,要想存儲基本的數據類型需要使用對應的引用類型結構

列表和集合的區別

實現了List 接口的列表與於實現了Set 接口的集合之間對比如下:

  1. 列表中允許存儲重復元素而集合則不允許存儲元素。
  2. 元素加入列表中的順序是固定的而集合則是無序的,所以集合在遍歷的時候並不是按照添加順序輸出的。
  3. 列表中的元素可以通過索引進行訪問而集合不能。

ArrayList

ArrayList可以說是在Java開發的過程中是常用的存儲結構了,通過名字大致可以猜到它的內部實現其實是通過數組來存儲的。那究竟是不是這么回事呢?我們來一探究竟。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // non-private to simplify nested class access

    private int size;

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    

查看源碼我們發現這樣一行代碼transient Object[] elementData; 並且在對ArrayList進行初始化的時候也對這個屬性進行了賦值操作。內部果然是采用Object數組存儲的。既然內部是數組實現的,操作也和數組差不多,為什么不直接用數組呢?在Java中數組一旦定義長度既不可更改,而在ArrayList中數組的元素是可以隨意添加的,在ArrayList內部默認使用的數組長度為10,當對List添加的元素個數超過10之后,會對數組進行擴容和對數據復制。每次在添加元素的時候,如果數組滿了,就會觸發擴容操作計算出一個新的數組容量並使用Arrays.copyOf操作(內部是通過System.arraycopy來操作的)對數據進行整體的復制

ArrayList既然內部是使用數組來實現的,也就繼承了數組的特性:支持快速查找,但是對於添加和刪除操作來說數組的性能會慢一些,在需要頻繁進行添加和刪除元素的場景下,會引起頻繁的數組擴容和數據移動,降低性能。所以在讀多寫少的場景下非常合適。

LinkedList

和ArrayList同屬於List 的一種實現方式,區別於ArrayList,但是內部的實現卻和ArrayList從名字上能猜測出來一樣,是使用鏈表來實現內部存儲的。下面來看下源碼

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient Node<E> first;

    transient Node<E> last;

    public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }

    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

    public void addFirst(E e) {
        linkFirst(e);
    }

    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;
        }
    }

查看源碼我們發現在LinkedList的內部維護了一個內部實現類Node結構用於存儲列表中的元素,查看Node的代碼不難看出,Node實現了是一個雙向鏈表。既然是鏈表,那么LinkedList也就繼承了鏈表的特性:查詢性能低,但支持快速的添加和刪除操作。故在需要進行頻繁添加和刪除操作的場景下,更為適用。與ArrayList是互斥的。

對列表中的元素進行判重操作

有時候我們需要判斷列表中是否包含一個元素,我們會調用相應類型的Contains方法,而在Contains的實現內部則是使用存儲的數據類型的equals方法來進行判等操作的。我們以ArrayList的源碼為例

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
public int indexOf(Object o) {
        return indexOfRange(o, 0, size);
    }

    int indexOfRange(Object o, int start, int end) {
        Object[] es = elementData;
        if (o == null) {
            for (int i = start; i < end; i++) {
                if (es[i] == null) {
                    return i;
                }
            }
        } else {
            for (int i = start; i < end; i++) {
                if (o.equals(es[i])) {
                    return i;
                }
            }
        }
        return -1;
    }

當被查找的元素不為null時,會調用元素的equals方法進行判等操作。在存儲自定義類型的時候,比如自定義類Person,在判斷元素是否存在的時候會調用Person的equals方法,默認情況下會比較兩個元素的地址,對於不同的Person類實例,地址也不相同,這是沒有意義的。所以我們需要進行重寫equals方法來實現對Person的判等操作。

HashMap

在我們開始講集合的實現類之前,先來看一下HashMap這個結構,在集合實現類中無論是HashSet和LinkedHashSet內部的實現方式均是依賴於HashMap的。

//HashSet的內部實現部分代碼
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map;

    private static final Object PRESENT = new Object();

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

    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

//LinkedHashSet內部實現部分代碼
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

HashMap實現Map<K,V>接口,存儲的是鍵值對的映射關系,並不屬於Collection 接口的實現類,在HashMap的內部使用的Hash表來存儲數據的,具體Hash表怎么一回事,還是通過源碼來研究研究吧

//HashMap的主要源碼
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    transient Node<K,V>[] table;

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final float loadFactor;
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

源碼中我們可以看到在HashMap的內部存儲着一個內部類實例數組Node<K,V>[],默認情況下,這個數組的長度為16。除此之外還有一個Hash函數和一個裝填因子。他們是做什么用的呢?我們先着重看一下Put方法,每次我們向HashMap的實例中添加元素的時候,都會對Key使用Hash函數計算出來一個整數hash值,然后和數組的長度進行運算得出一個索引值,這個索引值就是該元素應該在數組中的位置。如果這個位置並沒有元素存在,則直接放置在該位置,如果該位置的元素已經存在,也被稱為哈希碰撞,則使用單向鏈表的方式將元素連接起來。內部類Node<K,V>就是一個單向鏈表。當數組的容量超過(填裝因子*容量)的時候,意味着hash表的存儲非常臃腫,哈希碰撞會增多,會降低程序的性能(這里hash函數計算出hash值並且運算得到位置時間復雜度為O(1),如果在相同位置出現碰撞的次數越多就需要在鏈表中進行查找元素了,鏈表查找元素的時間復雜度是O(N),這會大大降低程序的性能),這個時候就需要對數組進行擴容,對所有元素進行遷移,這個過程也叫reHash。

我們在初始化HashMap的時候可以指定容量和填裝因子,容量一定要是2的冪,填裝因子的默認值為0.75。但是這里我不建議初始化的時候主動去設置這些值。因為這些值設置的是否合理直接影響到程序的性能,容量設置的大,浪費空間,容量設置的小,會導致哈希碰撞的次數增多,而且一旦超過了閾值(容量*填裝因子)還會導致擴容和數據遷移,這對程序的性能會大打折扣。

HashMap給我們遍歷它存儲的元素暴露出一些有用的方法,最為常用的則為:entrySet() 方法返回鍵值對作為值的集合;keySet() 方法返回鍵的集合;並且在HashMap中的Key和Value都允許為null。

HashSet

上面描述了List及其實現類的實現方式和用途,接下來我們對比看一下集合及其實現類的原理及用途。在某些場景下,我們存儲的元素中不需要有重復,這個時候集合就派上了用場,例如維護爬蟲爬取的鏈接。
我們先來看下集合的第一個主要實現類HashSet。
前面講HashMap原理的時候,我們說過HashSet的內部存儲就是靠HashMap來實現的,HashMap<K,V>是鍵值對的形式,而集合實現類並不存在這樣的關系,所以在使用HashMap的過程中對於集合類而言,Value是不存儲值的,默認情況下是個Object類型的null值。

private static final Object PRESENT = new Object();

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashSet在使用方式上除了和列表對比的那幾點不同之外沒有任何區別,具體的用途也可以根據它的特點來選擇合適的使用場景。

LinkedHashSet

LinkedHashSet繼承自HashSet,只不過LinkedHashSet可以保證存入的順序和取出的順序是一樣,是一個有序的集合。它是如何在HashSet的基礎上實現的呢?老規矩,源碼走起

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
transient LinkedHashMap.Entry<K,V> head;

transient LinkedHashMap.Entry<K,V> tail;

static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

原來是這樣:LinkedHashSet重寫了newNode方法並且在內部維護了一個新的Entry類和一個雙向鏈表,在每次創建新節點的時候都會對head,tail指針進行更新,就是這個雙向鏈表保證集合元素在遍歷的時候輸出的結果就是插入時的順序。除了這一點之外,用法和HashSet並無不同。

對集合中的元素進行判重操作

當我們需要判斷一個元素是否存在於集合中或者是向集合中添加重復元素時,除了需要像列表一樣重寫equals方法外,還需要重寫hashCode方法。在HashMap的內部,首先比對HashCode,如果這個值相等才會去比較equals。默認情況下HashCode是對地址的編碼,和equals一樣都和地址有關系,不重寫的話這種比較是沒有意義的。

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

總結

本節介紹了關於Java泛型集合存儲類一些常見的實現類及其原理和應用,上面介紹的所有的實現類均是線程不安全的。所以在多線程模式下訪問需要注意這一點,並且需要對其操作進行額外的防護。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM