Think In Java 第11章 持有對象 & 第17章 容器深入研究

本文發表于KuTear's Blog,轉載請注明

基本功能

Arrays & Collections

常用的方法

//Arrays.java
public static <T> List<T> asList(T... a) {
   return new ArrayList<>(a);
}

注意這里返回的ArrayList不同于我們平時使用的ArrayList,根據該ArrayLsit的源碼

private static class ArrayList<E> extends AbstractList<E>

知道其繼承至AbstractList,但是沒有實現它的add()delete()方法,因此若調用會拋出UnsupportedOperationException的提示,這是由于該List的底層就是一個數組,而且不會擴容,所以不支持添加等操作,在使用的時候要特別注意。

List list = ...;
List lists1 = new ArrayList(list);
List lists2 = Collections.addAll(list);

上面代碼,相比lists1,lists2更為高效。

集合類基本介紹

  • List 以特定順序保存的一組元素
  • Set 以特定順序保存的不重復的一組元素
  • Queue 同數據結構隊列
  • Map 使用KV保存兩組值

具體介紹

集合類圖

List

相比Collection,多 了一些方法,如listIterator()等.

ArrayList

ArrayList包含函數

概述

根據類圖可以知道ArrayList的繼承結構,RandomAccess是一個說明性接口,沒有任何的方法實現.ArrayList的底層實現任然是數組,當容量達到一定時,會新建一個數組,再把原來的數據拷貝過去,所以性能并不是太好.下面詳細的看看.

準備

由于ArrayList的底層是由數組實現的,并且ArrayList是動態大小,因此修改擴容,這里用到Arrays.copyOf(...)方法

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
          ? (T[]) new Object[newLength]
          : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                   Math.min(original.length, newLength));  //native
    return copy;
}

源碼閱讀

transient Object[] elementData; // 為什么是Object而不是泛型E?
private int size; //實際大小 size()函數就是返回該值

它的三個構造函數的作用都是初始化上面兩個參數的值.都是非常的簡單,不多說,下面看看最常用的add()函數.

public boolean add(E e) {
   ensureCapacityInternal(size + 1);  // 判斷數組容量是否夠,不夠就擴容
   elementData[size++] = e;
   return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index); //index > size || index < 0拋異常
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);//index后的往后移一位
    elementData[index] = element;
    size++;
}
    
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    int numMoved = size - index;
    if (numMoved > 0)
       System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

本身這段代碼是非常容易理解的,下面看看它擴容的實現.

private void ensureCapacityInternal(int minCapacity) {
      if (elementData == EMPTY_ELEMENTDATA) { //還沒有數據時
          minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
      }
      ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
     modCount++;
     // overflow-conscious code
     //注意elementData.length只是表示現有的容量,不是size
     if (minCapacity - elementData.length > 0)
         grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);//增加1.5倍容量,位操作效率遠遠高于做除法
    if (newCapacity - minCapacity < 0) //容量還沒有達到申請的量
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) //Integer.MAX_VALUE - 8
        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) // int 溢出變為負數了
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
         Integer.MAX_VALUE :
         MAX_ARRAY_SIZE;
}

通過上面的代碼,可以看到ArrayList的最大容量為Integer.MAX_VALUE,接下來就看看同等常用的get()函數.

public E get(int index) {
     rangeCheck(index);  //檢查index是否在[0,size)范圍內,具體實現就這一個條件判斷
     return elementData(index); //取得元素elementData[index]
}

根據他的名稱我們很容易的了解他的功能,并且對于數組的隨機存取,這個實現太簡單了,就不必多說.下面看看set()方法.

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

臥槽,我都不想多說什么了,就是簡單的判斷index的范圍,然后就是對數組操作.函數indexOf()/contains()lastIndexOf()都是簡單的對數組的遍歷過程,也跳過.下面看看remove()相關的方法.

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    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
    return oldValue;
}

/**
 * 先遍歷查找到index,在移除
 */
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;
}
    
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
}

下面看看retainAll()removeAll()的實現函數batchRemove().

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
           if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r]; //保留相等/或者不等的部分
    } finally {
       // Preserve behavioral compatibility with AbstractCollection,
       // even if c.contains() throws.
       if (r != size) {
           System.arraycopy(elementData, r,
                            elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
     return modified;
}

下面看看排序函數sort()

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

由上面的代碼可以看出sort()在排序過程重中是不允許執行修改/添加等等操作的.subList()返回一個List,但是這個List是依附在原本的ArrayList的,也就是說subList()得到的List其實是ArrayList的鏡像,當ArrayList修改后,取得的subList也會顯示出修改后的狀態.這里可以看看它的一部分實現

//構造函數
SubList(AbstractList<E> parent,
        int offset, int fromIndex, int toIndex) {
   this.parent = parent;
   this.parentOffset = fromIndex;
   this.offset = offset + fromIndex;
   this.size = toIndex - fromIndex;
   this.modCount = ArrayList.this.modCount;
}

public E get(int index) {
   rangeCheck(index);
   checkForComodification();
   //從這里可以看出,它的數據是外部類ArrayList的.
   return ArrayList.this.elementData(offset + index);
}

最后看看函數listIterator()和函數iterator(),他們分別返回一個雙向迭代器和單向迭代器.本質他們的遍歷過程還是數組的遍歷,想要了解詳情可以去看看具體的源碼,這里就不介紹了.

LinkedList

LinkedList包含函數

概述

inkedList實現了List、Deque、Cloneable以及Serializable接口。其中Deque是雙端隊列接口,所以LinkedList可以當作是棧、隊列或者雙端隊隊列。在使用它的時候,通??梢园阉蛏限D型為List,Queue已達到縮小她的接口的功能(限制了不需要的方法).

源碼閱讀

由于是由鏈表實現,首先需要查看的就是結點了.

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的內部,保存著firstlast結點的引用,這樣就方便了兩端的插入刪除等操作.

    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;

下面看看它的關鍵實現函數,添加結點相關函數.

//尾部添加
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++;
}

//頭部添加
private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
}

//在某個結點前添加
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++;
}

刪除結點相關函數.

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;
 }
 
private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
}

private E unlinkLast(Node<E> l) {
        // assert l == last && l != null;
        final E element = l.item;
        final Node<E> prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
}

以上兩組函數實現都是非常的簡單,和數據結構書中講的都幾乎一樣,想必大家也可以看懂,就不多廢話了,而該類的其他方法多依賴于以上方法的實現.還有一個其他函數的依賴函數是node()

//獲取第index個結點
Node<E> node(int index) {
        // assert isElementIndex(index);
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
}

可以看出他是遍歷鏈表的操作,只是因為有firstlast的存在,可以稍微優化一下.

Stack

根據上面LinkedList的實現,其實使用LinkedList實現一個Stack是非常的容易的,可以看看實現方式.

public class Stack<T> {
    private LinkedList<T> storage = new LinkedList<T>();

     /** 入棧 */
    public void push(T v) {
       storage.addFirst(v);
    }

     /** 出棧,但不刪除 */
    public T peek() {
       return storage.getFirst();
    }

     /** 出棧 */
    public T pop() {
       return storage.removeFirst();
    }

     /** 棧是否為空 */
    public boolean empty() {
       return storage.isEmpty();
    }

     /** 打印棧元素 */
    public String toString() {
       return storage.toString();
    }
}

但是Java中任然提供了Stack類,而且實現方式與上面的完全不同,它的內部存儲結構是數組,基本的實現其實還是和ArrayList差不多,而且在<<Think In Java>>中并不建議使用它,因此這里不講了.

Map

HashMap

HasgMap包含函數

本文的代碼均來至于JDK1.8,HashMap與前面版本的變化比較大,Android SDK V23中的是舊版本的

源碼閱讀

    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    // HashMap的閾值,用于判斷是否需要調整HashMap的容量(threshold = 容量*加載因子)   
    int threshold;
    final float loadFactor; //加載因子,注意Android SDK寫死的0.75

在這里可以看看構造函數的參數

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;
        //tableSizeFor(n) Returns the smallest power of two >= its argument
        this.threshold = tableSizeFor(initialCapacity);
}

可以看出threshold在沒到達最大值之前是$2^n$.下面再看看常用的方法.

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
    
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,`
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //還沒有初始化數組
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //找到要添加的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果還沒有元素,就放入
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //已經存在了一個相同KEY的元素
                e = p;
                //紅黑樹,JDK1.8的優化點,當鏈表的長度大于8時,不再使用鏈表,轉為紅黑樹
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //鏈表結構
                for (int binCount = 0; ; ++binCount) {
                    //添加到鏈表尾部
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash); //鏈表長度為8了,轉紅黑樹
                        break;
                    }
                    //在鏈表中找到了同KEY值得元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key ,已經存在的KEY,修改原本的值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict); //空操作
        return null;
}

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            //到此得到一個雙向鏈表的格式.
            if ((tab[index] = hd) != null)
                hd.treeify(tab); //形成從該節點連接的節點的樹。實現有點復雜
        }
}

關于紅黑樹的操作本身是非常復雜的,可以參考Wiki,接下來看看get()操作.

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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) {
            //Fiest為鏈表的首節點或紅黑樹的根節點
            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;
}

之理也只是大體說明一下HashMap的結構,核心的東西就是紅黑樹,它的其他方法也是大體一致,都是對鏈表和紅黑樹的操作.entrySet()keySet()也是和List中的iterator一樣,內部的操作都是由HashMap本身來完成.

public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
}

這個函數大家可能也會有一點困惑,為什么這里就只討論了鏈表的情況,并根據next()遍歷整個鏈表?其實TreeNode是繼承至Node,并且在生成紅黑樹的時候并沒有修改next的指向,所以通過next()遍歷就沒問題了.

TreeMap

TreeMap包含函數

TreeMap的底層實現也是基于紅黑樹的.

源碼閱讀

還是老規矩,先看最常用的方法put().

public V put(K key, V value) {
        Entry<K,V> t = root;
        //紅黑樹為空,直接添加一個結點接OK
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        //優先使用主動提供的比較器,
        //在使用該類(KEY)自帶的比較器(繼承Comparable)
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    //找到key值相同的結點,覆蓋該值即可
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            //key不允許為NULL
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //到此找到了要插入到結點parent的子結點
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //插入完成,此時的紅黑樹結構可能已經被破壞,需要重新構建
        //過程和HasmMap的其實是一樣的.了解更多可以看文章后面的參考
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
}

接下來看看get()函數.

public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
}

final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            //自定義比較器的時候
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        //實現就是查找二叉樹查找問題
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
}
//這個函數的實現
final Entry<K,V> getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            Entry<K,V> p = root;
            while (p != null) {
                int cmp = cpr.compare(k, p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
}

遍歷的時候調用了一個函數

final Entry<K,V> nextEntry() {
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            next = successor(e);  //中序遍歷的E的后一節點
            lastReturned = e;
            return e;
}

這樣輸入的數據就是按照紅黑樹中序遍歷的數據,也就是有序數據.

LinkedHashMap

LinkedHashMap函數列表

根據最上面的繼承關系圖我們知道LinkedHashMap繼承至HashMap,所以重復型的東西我就不說了,LinkedHashMap的核心功能就是維持了原有的輸入順序或者指定為訪問順序(LRU).下面也是主要看看這個功能的實現.

源碼閱讀

LinkedHashMap中的字段

    // 頭部放舊結點(最久沒使用或最久放入)
    transient LinkedHashMap.Entry<K,V> head;
    // 尾部放新節點
    transient LinkedHashMap.Entry<K,V> tail;

通過上面的分析,我們知道HashMapput()時調用了函數newNode(),而LinkedHashMap就重寫了這個方法.

//結點,相比HashMap多了before和after
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);
        }
}

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        //創建一個結點
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
}
//內部保存了一個鏈表
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;
        }
}

這樣就要求在以后的插入刪除的工作中需要額外的維護這個鏈表.另外,如果開啟了按訪問順序排序的話,在每次get()或者put()了重復數據是都會要求把訪問的結點放到鏈表尾部.

//把E移動到雙向鏈表的尾部
void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null) //P本身為首節點
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else         //P本身為尾接點
                last = b;
            //到此P被移除了    
            if (last == null) //原本鏈表只有一個元素,移除光了
                head = p;
            else {
               //P放在最后
                p.before = last;
                last.after = p;
            }
            //修改指向末尾結點的指針
            tail = p;
            ++modCount;
        }
}

接下來就看看LinkedHashMap的遍歷.entrySet()返回的是一個LinkedEntrySet的實例,而LinkedEntrySet的迭代器是LinkedEntryIterator,LinkedEntryIteratornext()方法調用nextNode()函數.

//構造函數
LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;
            current = null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
 }

可以很明顯的看到,它的實現完全依賴于構建的鏈表,不像HashMap對組數和鏈表(紅黑樹)的遍歷.相比HashMap其實就是多了一個雙向鏈表而已.

Set

Set是一個不包含重復元素的Collection。更確切地講,Set 不包含滿足 e1.equals(e2) 的元素對 e1e2,并且最多包含一個 null 元素.

HashSet

HashSet函數列表

HashSet的底部是使用一個HashMap,把值存在HashMapKEY,HashMapVALUE字段為固定的值,根據HashMapKEY的唯一性,可以保證HashSet的值得唯一性.額外,有一個構造器使用的是LinkedHashMap,只有包權限,是后面要講的LinkedHashSet的實現.

//dummy 參數只是使用來區別構造函數
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

源碼閱讀

其實HashSet的源碼并沒有什么東西,都是調用HashMap的基本操作.下面隨便看兩個函數.

private transient HashMap<E,Object> map;

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

public Iterator<E> iterator() {
        return map.keySet().iterator();
}

public boolean contains(Object o) {
        return map.containsKey(o);
}

由于這些方法前面都已經說過了,這里就不說了.

TreeSet

TreeSet函數列表

有了上面HashSet的介紹,可能你已經猜到TreeSet的實現方式是基于TreeMap了.將值存放在TreeMapKEY中,保證了不會重復并且有序,最后只需要遍歷TreeSetKEY就行了.具體的操作可以看看TreeMap.

LinkedHashSet

它的實現是最簡單的,繼承至HashSet,調用前面說的特殊構造器,相當于把HashSetHashMap換成了LinkedHashMap,并且按照插入順序排序.

Queue

LinkedList

這個上面已經分析過了,這里跳過。

PriorityQueue

PriorityQueue函數

準備

在數據結構的課程中,我們都學過用數組表示完全二叉樹,這里有一些固定的公式

Index(parent) = (Index(Child)-1) >> 1   //索引0開始

而優先級隊列Priority就是使用了數組表示最小堆,每次插入刪除都會重新排列內部數據。

最小堆,是一種經過排序的完全二叉樹

源碼閱讀

有用的字段

priavte transient Object[] queue; //內部表示最小堆的數組
private int size = 0; //實際大小

常用方法add()的實現

public boolean add(E e) {
    return offer(e); // add方法內部調用offer方法
}

public boolean offer(E e) {
    if (e == null) // 元素為空的話,拋出NullPointerException異常
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length) // 如果當前用堆表示的數組已經滿了,調用grow方法擴容
        grow(i + 1); // 擴容
    size = i + 1; // 元素個數+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(k, x); // 使用元素自身的比較器調整
}

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) { // 一直循環直到父節點還存在
        int parent = (k - 1) >>> 1; // 找到父節點索引,依賴完全二叉樹性質
        Object e = queue[parent]; // 賦值父節點元素
        if (comparator.compare(x, (E) e) >= 0) // 新元素與父元素進行比較,如果滿足比較器結果,直接跳出,否則進行調整
            break;
        queue[k] = e; // 進行調整,新位置的元素變成了父元素
        k = parent; // 新位置索引變成父元素索引,進行遞歸操作
    }
    queue[k] = x; // 新添加的元素添加到堆中
}
private void siftUpComparable(int k, E x) {
 ...//同上面類似
}

下面看看函數remove()的實現

public boolean remove(Object o) {
        int i = indexOf(o); //按數組索引遍歷
        if (i == -1)
            return false;
        else {
            removeAt(i);
            return true;
        }
}

private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        if (s == i) // removed last element,移除最后的元素,該數組依舊是最小堆
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null; //數組最后一個位置置空
            siftDown(i, moved); 
            if (queue[i] == moved) {
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
}
    

private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
}

@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
        //為什么是一半??
        //因為大于half的元素必然是沒有葉子節點的,這是只需要用末節點X替換要刪除的節點index(K),然后重新排序。
        //而對于小于half的節點,由于存在(左)/(右)節點,用較小的節點替換要刪除的節點,這樣帶刪除節點的Index = (左)/(右)的索引,然后繼續遞歸執行,直到大于half,在用末節點替換她。
        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;
}

函數poll()remove()的實現基本一致。

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

其他的比如擴容函數和ArrayList的原理都是一樣的,這里就不說了。到此,基本的集合類的源碼大體上都說完了。

其他技術點

Java 8 default關鍵字

interface A {
     void doSomeThing();
}
static class B implements A {
     @Override
     public void doSomeThing() {
        System.out.println("B");
     }
 }
 static class C implements A {
     @Override
     public void doSomeThing() {
         System.out.println("C");
     }
 }

以上代碼如果想在A中添加一個函數,必然需要修改BC的實現,但是在Java 8支持為接口添加一個默認的實現,這樣和抽象類就很相似了。

interface A {
     void doSomeThing();
     default void doAction() {
         System.out.println("Default");
     }
}
static class B implements A {
     @Override
     public void doSomeThing() {
        System.out.println("B");
     }
 }
 static class C implements A {
     @Override
     public void doSomeThing() {
         System.out.println("C");
     }
 }

就向上面就OK了。

Integer比較問題

System.out.println(Integer.valueOf(127)==Integer.valueOf(127));
System.out.println(Integer.valueOf(128)==Integer.valueOf(128));
System.out.println(Integer.parseInt("128")==Integer.valueOf("128"));

輸出

true
false
true

為什么會有這問題?通過源代碼

public static Integer valueOf(int i) {
   if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

代碼中的IntegerCache.low為固定值-128,IntegerCache.high根據VM系統參數不同會有區別,默認127,因此在[-128,127]范圍內,實例化的時候是返回的同一個對象,必然相等。當Integer修改的時候,由于他是不可變對象(參考String,每次修改都是重新生成對象),也不會出現問題。對于第三個例子,parseInt()的返回是int,這時和Integer比較,Integer會拆包為int,當然也就相等。
另外補充一點,當我們調用

Integer i = 1;

其實也是執行

Integer i = Integer.valueOf(1);

可以從反編譯中看出

//源碼
public static void main(String[] args){
  Integer i = 1;
  int r = i + 1;
}
//反編譯結果
public static void main(java.lang.String[]);
  Code:
     0: iconst_1      
     1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     4: astore_1      
     5: aload_1       
     6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
     9: iconst_1      
    10: iadd          
    11: istore_2      
    12: return  

自動拆箱調用intValue(),自動裝箱調用valueOf()。

List&Set&Map在遍歷過程中刪除添加元素錯誤

for(int i:list){
  if(i == 2){
      list.remove(Integer.valueOf(2));
   }
}

以上這段代碼會拋出java.util.ConcurrentModificationException異常。這是因為foreach本質還是調list.iterator(),這里用ArrayList說明。

public Iterator<E> iterator() {
     return new Itr();
}

這里返回一個迭代器,其內部參數包括

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount; //修改次數,注意int為值傳遞
}

也就是說保存了修改的次數,在迭代器的next()中有檢測這個值是否被篡改(可以修改的地方包括ArrayListadd(...)remove())。

final void checkForComodification() {
   if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
}

解決方案是使用迭代器的remove(...)

Iterator iterator = list.iterator();
while (iterator.hasNext()){
   Integer i = (Integer) iterator.next();
   if(i == 2){
      iterator.remove();
   }
}

instanceof 關鍵字

Object obj = null;
if(obj instanceof Object){
   System.out.println("會輸出嗎?還是崩潰");
}

上面的例子不會輸出,也不會崩潰,instanceof會檢測左邊對象是否為null,若是,返回false.

HashMap的容量為什么為$ 2^n $

put()函數中,選取數組索引的方式為

tab[i = (n - 1) & hash]

重點關注(n - 1) & hash,這里的n是容量,若n=$2^n$,n-1的二進制形式為11...11,做&運算后只有hash的后幾位相關,保證足夠散列,而若其他情況,下n-101..01,運算后只有hash的后幾位中的某幾位相關,縮小了散列范圍,如n-1最末尾為0,這樣&之后始終是一個偶數,導致分布過于集中.

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容