Java 基礎 - 各項集合實現

[toc]html

Java 類庫中的集合接口和迭代器

集合接口及迭代器

  • 集合類的基本接口是:Collection
public interface Collection<E>{
        // 集合改變返回 true,不然返回 false
        boolean add();
        boolean addAll();
        // 返回一個迭代器
        Iterator<E> iterator();
        int size();
        boolean isEmpty();
        // 集合中包含了和 obj 相等的對象,那麼返回 true
        boolean contains(Object obj);
        // 若是集合中包含 other 集合中的全部元素,那麼返回 true
        boolean containsAll(Collect<?> other);
        // 從這個集合中刪除等於 obj 的對象,若是有匹配的對象,返回 true
        boolean remove(Object obj);
        // 從這個集合中刪除 other 中存在的元素,若是這個調用改變了集合,那麼返回 true
        boolean removeAll(Collect<?> other);
        void clear();
        // 從這個集合中刪除全部與 other 這個集合中的元素不一樣的元素,若是這個調用改變了集合,那麼返回 true
        boolean retainAll(Collection<?> other);
        Object[] toArray();
        <T> T[] toArray(T[] a);
        
    }
複製代碼
  • 迭代器
public interface Iterator<E>{
        // 反覆調用,能夠逐個訪問集合中的每一個元素(配合 hasNext()  這個方法)
        E next();
        boolean hasNext();
        // 刪除上次調用 next() 返回的元素,沒有調用 next() 方法,調用 remove() 則會報 IllegalStateException 異常
        void remove();
    }
複製代碼
  • 迭代器的用法
    • 用法 1
    Collection<String> c = ....;
        Iterator<String> iterator = c.iterator();
        while(iterator.hasNext()){
            String element = iterator.next();
            iterator.remove();
            // todo something
        }
    複製代碼
    • 用法 2:java SE 5.0 以後的寫法,for each 循環操做
    Collection<String> c = ....;
        for(String element : c){
            // todo something
        }
    複製代碼

    「for each」 循環能夠與任何實現了 Iterable 接口的對象一塊兒工做java


集合概覽圖

具體的集合實現

ArrayList

簡介

  • 繼承於 AbstractList,實現了 List,是一個數組隊列,提供添加、刪除、修改、遍歷的功能
  • 實現了 RandomAccess 接口,提供隨機訪問的功能
  • 實現了 Cloneable 接口,提供了克隆功能
  • 實現了 java.io.Serializable 接口,提供序列化功能

定義

java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.ArrayList<E>

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
複製代碼

特性

  • 關於 ArrayList 是線程不安全的,那麼 ArrayList 只能在單線程中使用,若是須要多線程使用的話,那麼可使用 Vector。或者是如下方式
// 將其包裝成線程安全
    List list = Collections.synchronizedList(new ArrayList());
複製代碼
  • ArrayList 是一個動態數組隊列,它能高效的隨機訪問元素和順序遍歷,但對於插入和刪除效率會比較低,由於須要涉及到數組的移動。

擴容

  • ArrayList 是一個動態的數組,那麼一開始數組的大小是固定的(默認的話爲 10),當向 ArrayList 中插入某個數組時,size 的值恰好爲容量的大小,那麼就會觸發擴容的操做。擴容的方式是從新建立一個新的數組,拷貝原來的數據到新的數組中,並將新的元素插入到新的數組中,舊的數組則會被垃圾回收。
  • 默認容量:10
  • 擴容規則
    • JDK 1.6 及以前數組

      int newCapacity = (oldCapacity * 3)/2 + 1;
      複製代碼
    • JDK 1.7 及以後安全

      int newCapacity = oldCapacity + (oldCapacity >> 1);
      複製代碼
    • JDK 1.8bash

      private void grow(int minCapacity) {
          // overflow-conscious code
          int oldCapacity = elementData.length;
          int newCapacity = oldCapacity + (oldCapacity >> 1);
          if (newCapacity - minCapacity < 0)
           newCapacity = minCapacity;
          if (newCapacity - MAX_ARRAY_SIZE > 0)
          newCapacity = hugeCapacity(minCapacity);
          // minCapacity is usually close to size, so this is a win:
          elementData = Arrays.copyOf(elementData, newCapacity);
      }
      
      private static int hugeCapacity(int minCapacity) {
       if (minCapacity < 0) // overflow
              throw new OutOfMemoryError();
          return (minCapacity > MAX_ARRAY_SIZE) ?
          Integer.MAX_VALUE :
          MAX_ARRAY_SIZE;
      }
      複製代碼

toArray()

  • 2 種實現
Object[] toArray()
        <T> T[] toArray(T[] contents)
複製代碼
  • 關於 「java.lang.ClassCastException」異常 toArray() 會拋出異常是由於 toArray() 返回的是 Object[] 數組,將 Object[] 轉換爲其它類型(如如,將Object[]轉換爲的Integer[])則會拋出「java.lang.ClassCastException」異常,由於Java不支持向下轉型。
  • 關於轉換爲數組的方式
// toArray(T[] contents)調用方式一
public static Integer[] vectorToArray1(ArrayList<Integer> v) {
    Integer[] newText = new Integer[v.size()];
    v.toArray(newText);
    return newText;
}
// toArray(T[] contents)調用方式二。最經常使用!
public static Integer[] vectorToArray2(ArrayList<Integer> v) {
    Integer[] newText = (Integer[])v.toArray(new Integer[0]);
    return newText;
}
// toArray(T[] contents)調用方式三
public static Integer[] vectorToArray3(ArrayList<Integer> v) {
    Integer[] newText = new Integer[v.size()];
    Integer[] newStrings = (Integer[])v.toArray(newText);
    return newStrings;
}
複製代碼

注意點

  • 多線程的話不使用 ArrayList,而是使用 Vector。

LinkedList

一種能夠在任意位置進行高效插入及刪除的操做的有序序列數據結構

簡介

  • 繼承了 AbstractSequentialList 的雙向鏈表,所以 LinkedList 是能夠被當作堆棧、列表和雙端列表進行操做
  • 實現 List 接口,進行隊列的操做
  • 實現 Cloneable 接口,能夠進行克隆操做
  • 實現 Deque 接口,能夠進行雙端隊列操做
  • 實現 java.io.Serializable 接口,能夠實現序列化
  • 非同步的

定義

java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.AbstractSequentialList<E>
                     ↳     java.util.LinkedList<E>
                     
public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
複製代碼

特性

  • 順序訪問的效率高,可是隨機訪問的效率比較低
  • 刪除及添加的操做效率高
  • 不一樣步(線程不安全)

將LinkedList看成 LIFO(後進先出)的堆棧示例

public static void useLinkedListAsLIFO() {
        System.out.println("\nuseLinkedListAsLIFO");
        // 新建一個LinkedList
        LinkedList stack = new LinkedList();

        // 將1,2,3,4添加到堆棧中
        stack.push("1");
        stack.push("2");
        stack.push("3");
        stack.push("4");
        // 打印「棧」
        System.out.println("stack:"+stack);

        // 刪除「棧頂元素」
        System.out.println("stack.pop():"+stack.pop());
        
        // 取出「棧頂元素」
        System.out.println("stack.peek():"+stack.peek());

        // 打印「棧」
        System.out.println("stack:"+stack);
    }
複製代碼

將LinkedList看成 FIFO(先進先出)的隊列

public static void useLinkedListAsFIFO() {
        System.out.println("\nuseLinkedListAsFIFO");
        // 新建一個LinkedList
        LinkedList queue = new LinkedList();

        // 將10,20,30,40添加到隊列。每次都是插入到末尾
        queue.add("10");
        queue.add("20");
        queue.add("30");
        queue.add("40");
        // 打印「隊列」
        System.out.println("queue:"+queue);

        // 刪除(隊列的第一個元素)
        System.out.println("queue.remove():"+queue.remove());
    
        // 讀取(隊列的第一個元素)
        System.out.println("queue.element():"+queue.element());

        // 打印「隊列」
        System.out.println("queue:"+queue);
    }
複製代碼

HashMap(JDK 1.7 及以前)

簡介

HashMap 它是基於 hash 表的 Map 接口實現,以 key-value 的形式存在的,HashMap 老是以 key-value 的形式存在的,系統會經過計算 key 的 hash 值來定位 key-value 的存儲位置的,咱們能夠快速的經過 key 來存取 value;多線程

定義

public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable
複製代碼

數據結構

關於 HashMap 的數據結構,底層的話仍是數組的,只不過數組的每一項就是一個鏈表dom

構造函數的源碼函數

public HashMap(int initialCapacity, float loadFactor) {
            //初始容量不能<0
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: "
                        + initialCapacity);
            //初始容量不能 > 最大容量值,HashMap的最大容量值爲2^30
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            //負載因子不能 < 0
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: "
                        + loadFactor);

            // 計算出大於 initialCapacity 的最小的 2 的 n 次方值。
            int capacity = 1;
            while (capacity < initialCapacity)
                capacity <<= 1;

            this.loadFactor = loadFactor;
            //設置HashMap的容量極限,當HashMap的容量達到該極限時就會進行擴容操做
            threshold = (int) (capacity * loadFactor);
            //初始化table數組
            table = new Entry[capacity];
            init();
        }
複製代碼

Entry 的源碼post

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

            /**
             * Creates new entry.
             */
            Entry(int h, K k, V v, Entry<K,V> n) {
                value = v;
                next = n;
                key = k;
                hash = h;
            }
            .......
        }
複製代碼

Entry 是 HashMap 的內部類,其中包含了 key,value 和 下一個 Entry,以及 hash 值,正由於有這下才構成了數組的項爲一個列表。

容量、加載因子、臨界值及哈希衝突

  • 容量:table 數組的大小,通常默認爲 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
複製代碼
  • 加載因子:表示 table 數組的飽和程度
    • 加載因子越大,填滿的元素越多,空間利用率越高,但衝突的機會加大了。 反之;
    • 加載因子越小,填滿的元素越少,衝突的機會減少,但空間浪費多了。
  • 臨界值
    • 爲了不形成哈希衝突率,那麼當 HashMap 的數組長度達到一個臨界值的時候就會觸發擴容,把全部的元素從新計算 hash 值,再放到擴容後的容器中,這是一個比較耗時的操做。
      • 臨界值由加載因子及當前的容量來決定,默認狀況下 16*0.75=12 就會觸發擴容
    DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR
    複製代碼

哈希衝突

在關鍵字的 hash 地址上已經有了記錄,那麼這就是哈希衝突
複製代碼
  • 解決衝突的方法
    • 開放定址法
    • 再哈希法
    • 創建一個公共溢出區
    • 鏈地址法(拉鍊法)

存儲實現:put(key,value)

public V put(K key, V value) {
            //當key爲null,調用putForNullKey方法,保存null與table第一個位置中,這是HashMap容許爲null的緣由
            if (key == null)
                return putForNullKey(value);
            //計算key的hash值
            int hash = hash(key.hashCode());                   ------(1)
            //計算key hash 值在 table 數組中的位置
            int i = indexFor(hash, table.length);             ------(2)
            //從i出開始迭代 e,找到 key 保存的位置
            for (Entry<K, V> e = table[i]; e != null; e = e.next) {
                Object k;
                //判斷該條鏈上是否有hash值相同的(key相同)
                //若存在相同,則直接覆蓋value,返回舊value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;    //舊值 = 新值
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;     //返回舊值
                }
            }
            //修改次數增長1
            modCount++;
            //將 key、value 添加至i位置處
            addEntry(hash, key, value, i);
            return null;
        }
複製代碼

(1)處代碼實現:技術 hash 值

static int hash(int h) {
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
複製代碼

(2)處代碼實現:根據 hash 值計算出 key 在 table 數組中所對應的位置

static int indexFor(int h, int length) {
            return h & (length-1);
        }
複製代碼

(3)將節點插入表頭

void addEntry(int hash, K key, V value, int bucketIndex) {
            //獲取bucketIndex處的Entry
            Entry<K, V> e = table[bucketIndex];
            //將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
            table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
            //若HashMap中元素的個數超過極限了,則容量擴大兩倍
            if (size++ >= threshold)
                resize(2 * table.length);
        }
複製代碼

存儲步驟:

  • step 1:判斷 key 是否爲 null,若爲 null,那麼直接調用 putForNullKey 方法(table[0] 的數組項),不然進入 step2;
  • step 2:計算 key 的 hash 值
  • step 3:計算 key 的 hash 值在 table 數組中的位置 index
  • step 4:在 table[index] 項中迭代,找出 key 的存儲位置,若是存在則替換就的值,並將舊的值返回,若是不存在對應的 key 的存儲位置,則進入 step5;
  • step 5:將 key-value 放在 table[index] 的鏈表頭

擴容問題

隨着 HashMap 中的元素愈來愈多,發生 hash 衝突的機率愈來愈大,鏈表的長度愈來愈長,查找的效率就愈來愈低;這樣咱們就必須在 HashMap 的某個臨界值進行擴容處理。擴容的方式:從新建立一個新的 table 數組,從新計算 key 的 hash 值,並放入新的 table 數組中,這樣的操做是比較耗時的,若是咱們可以預知 HashMap 中的大小時,咱們能夠指定 HashMap 中的元素個數。

  • 讀取實現:get(key) 經過 key 的 hash 值找到在 table 數組中的索引處的 Entry,而後返回該 key 對應的 value 便可。
public V get(Object key) {
            // 若爲null,調用getForNullKey方法返回相對應的value
            if (key == null)
                return getForNullKey();
            // 根據該 key 的 hashCode 值計算它的 hash 碼  
            int hash = hash(key.hashCode());
            // 取出 table 數組中指定索引處的值
            for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
                Object k;
                //若搜索的key與查找的key相同,則返回相對應的value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                    return e.value;
            }
            return null;
        }
複製代碼

HashMap 非同步

HashMap 是線程不安全的,咱們能夠經過 Collections 的靜態方法 SynchronizedMap 來獲取線程安全的 HashMap

Map map = Collections.SynchronizedMap(new HashMap<>();
複製代碼

LinkedHashMap

介紹

  • LinkedHashMap 是 HashMap 的子類,所以 LinkedHashMap 擁有 HashMap 中的全部特性,可是 HashMap 的迭代是沒有順序的。LinkedHashMap 經過維護一個雙鏈表來保證迭代的順序(插入順序或者訪問順序),可是同時也增長了時間和空間的開銷。

數據結構

  • HashMap(數組+鏈表)+雙鏈表

雙鏈表

```
/**
 * HashMap.Node subclass for normal LinkedHashMap entries.
 */
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after;
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

```
複製代碼

重要變量

  • head:雙鏈表頭部,保存最先插入的元素。
  • tail:雙鏈表的尾部,保存最近插入的元素。
  • accessOrder:訪問順序(true:訪問順序迭代;false:插入順序迭代)

重要函數

// Callbacks to allow LinkedHashMap post-actions
    // 訪問元素以後
    void afterNodeAccess(Node<K,V> p) { }
    // 插入節點以後
    void afterNodeInsertion(boolean evict) { }
    // 刪除節點以後
    void afterNodeRemoval(Node<K,V> p) { }
複製代碼

HashMap 和 HashTable 的區別

HashTable 和 HashMap 都實現了 Map 接口,他們的主要區別在於線程安全、速度。

  • HashMap 能夠接受 key 爲 null,HashTable 不能夠接受 key 爲 null
  • HashMap 是線程不安全(非 synchronize),HashTable 是線程安全的(synchronize)。synchronize 表明着每一次在一個線程中修改 HashTable 中的數據時,都須要得到同步鎖,其餘的線程要修改 HashTable 中的數據時,須要等待同步鎖被釋放才能進行。
  • HashMap 的迭代器是 Iterator,HashTable 的迭代器是 enumerator。
  • 在單線程的操做中,HashMap 的操做速度要比 HashTable 快,由於 HashTable 是 synchronize 的,因此會有同步鎖的獲取和釋放過程。

HashSet

  • 介紹
  • HashSet 是基於 HashMap 實現的,底層是使用 HashMap 來保存數組的

參考資料

數據結構之紅黑樹

LinkedHashSet

徹頭徹尾理解 LinkedHashMap

LinkedList 的詳細介紹

相關文章
相關標籤/搜索