程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> [java學習]java容器源碼初探(1)

[java學習]java容器源碼初探(1)

編輯:JAVA綜合教程

[java學習]java容器源碼初探(1)


一、動態數組ArrayList

在我們開發者眼中,這就是一個“動態數組”,可以“動態”地調整數組的大小,雖然說數組從定義了長度後,就不能改變大小。
實現“動態”調整的基本原理就是:按照某個調整策略,重新創建一個調整後一樣大小的數組,然後將原來的數組賦值回去。
下面我們來解析一下幾個與數組不一樣的方法。

看看ArrayList中主要的幾個字段(源碼剖析):

    // 默認的初始數組大小
    private static final int DEFAULT_CAPACITY = 10;

    // 空數組的表示
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 數據保存的數組,主體就是它
    private transient Object[] elementData;

    // 數組元素個數(不一定等於數組的length)
    private int size;

添加元素(源碼剖析):

public boolean add(E e) {
    // 判斷數組需不需要擴容,需要就擴容(動態調整數組大小的關鍵方法)
    ensureCapacityInternal(size + 1);
    // 數組元素賦值,數組元素個數+1
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    // 監測是否是數組越界
    rangeCheckForAdd(index);
    // 判斷數組需不需要擴容,需要就擴容(動態調整數組大小的關鍵方法)
    ensureCapacityInternal(size + 1);
    // index和index的元素向後移動
     System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // index位置上賦值
    elementData[index] = element;
    // 數組元素+1
    size++;
}

刪除元素,這裡注意,並沒有真正的縮小數組的大小,只是修改size的值(源碼剖析):

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);
    // 移除引用,方便GC回收
    elementData[--size] = null;
    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {
        // 遍歷數組,查找到第一個為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;
}
// 與E remove(int index)的差不多,我就不多說了。
// 這裡我就不明白,remove(int index)和fastRemove(int index)明明可以代碼復用,為何不復用代碼呢?
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // 置null,讓GC可以回收它
    elementData[--size] = null;
}

看看最重要的數組擴容方法(源碼剖析):

    private void ensureCapacityInternal(int minCapacity) {
        // 判斷數組是否為空數組
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        // 修改次數+1
        modCount++;
        // 判斷需不需要擴容:比較數組大小和需要擴容的大小
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    // 增加容量開始
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // x1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            // 擴容的大小太小了
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 擴容的大小不會超過最大容量Integer.MAX_VALUE-8
            newCapacity = hugeCapacity(minCapacity);
        // 數組復制
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

ArrayList被稱為“動態數組的原理”,大致就如源碼解析中注釋。裡面還有其他數組操作的方法,這裡就不多講,其實思路也是差不多的,一切以源碼為准。

二、鏈表隊列LinkedList

(雙向)鏈表+隊列(Deque)。以前我一直以為LinkedList只是個鏈表而已,沒想到它可以當隊列用。

看看結點代碼,一看就知道是雙向的結點(源碼):

    private static class Node {
        E item;
        Node next; // 後繼節點
        Node prev; // 前置節點

        Node(Node prev, E element, Node next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

幾個重要字段(源碼解析):

    // transient:不被序列化
    // 結點個數
    transient int size = 0;
    // 第一個結點
    transient Node first;
    // 最後一個結點
    transient Node last;

簡單的看看鏈表的代碼(源碼解析):

    // 添加結點
    public void add(int index, E element) {
        // 檢查index是否越界
        checkPositionIndex(index);
        if (index == size)
            // 添加最後一個
            linkLast(element);
        else
            // 添加到原index結點前面
            linkBefore(element, node(index));
    }
    // 刪除結點
    public E remove(int index) {
        // 檢查index是否越界
        checkElementIndex(index);
        // 解除結點鏈接
        return unlink(node(index));
    }
    // 查找結點
    public E get(int index) {
        // 檢查越界了沒
        checkElementIndex(index);
        // 開始查找
        return node(index).item;
    }
    // “真正”是使用這個方法來查找
    Node node(int index) {
        // 折半查找~,果然是會利用雙向鏈表的優點
        if (index < (size >> 1)) { // 從前面找找前半邊
            Node x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else { // 從後面找找後半邊
            Node x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

簡單的看看隊列的代碼(源碼解析):

    // 入隊
    public boolean offer(E e) {
        return add(e);
    }
    // 也是入隊
    public boolean add(E e) {
        // 接到最後一個結點
        linkLast(e);
        // 個人覺得這個return true沒必要,完全可以無返回值void,以為如果入隊失敗linkLast會拋異常。既然是拋異常了,就不會return false了,那就沒必要return true。
        return true;
    }
    // “溫柔地”取隊頭元素,隊為空不會拋出異常,只會返回null
    public E peek() {
        final Node f = first;
        return (f == null) ? null : f.item;
    }
    // “粗暴地”取隊頭元素,隊為空拋出NoSuchElementException異常
    public E element() {
        return getFirst();
    }
    // “溫柔地”出隊,隊為空不會拋出異常,只會返回null
    public E poll() {
        final Node f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
    // “粗暴地”出隊,隊為空拋出NoSuchElementException異常
    public E remove() {
        return removeFirst();
    }

三、安全的動態數組Vector

加鎖版的ArrayList(線程安全)。方法實現與ArrayList差不多,不同的是線程不安全的方法都加上鎖(synchronized)了,這裡就不講啦。
一般不使用Vector而使用ArrayList,除非在多線程環境下(萬不得已)才使用Vector。

四、棧Stack

棧,父類是Vector,所以你懂的(加鎖版動態數組)。既然父類“安全”了,作為子類的棧也要“安全”了(加鎖!),這麼說棧也不是很快嘛~。
Stack規定數組最後一個元素為棧頂元素。

入棧(源碼解析):

    // 不加鎖?父類Vector的addElement已經加鎖了,所以不加多余的鎖。
    public E push(E item) {
        // 添加元素到數組最後一個元素的後面
        addElement(item);
        // 返回壓入棧的元素
        return item;
    }

出棧(源碼解析):

    // 出棧
    public synchronized E pop() {
        E obj;
        // 棧中元素個數
        int len = size();
        // 取棧頂元素
        obj = peek();
        // 移除最後的一個元素(就是移除棧頂元素啦)
        removeElementAt(len - 1);
        // 返回移除的棧頂元素
        return obj;
    }
    // 取棧頂元素
    public synchronized E peek() {
        int len = size();
        if (len == 0)
            // 空棧異常
            throw new EmptyStackException();
            // 取數組最後一個元素
        return elementAt(len - 1);
    }

棧中元素查找,從棧頂(最後一個數組元素)到棧底(第一個數組元素)(源碼解析):

    public synchronized int search(Object o) {
        // 找到最後一個符合要求的元素
        int i = lastIndexOf(o);
        if (i >= 0) {
            // 找到了
            return size() - i;
        }
        // 沒找到
        return -1;
    }

五、優先隊列PriorityQueue

PriorityQueue實現了優先隊列(默認最小堆)。
實現優先隊列的難點:需要調整堆。
調整堆的方法有:上濾和下濾。

幾個重要字段(源碼解析):

    // 默認的初始化容器大小
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    // 元素數組
    private transient Object[] queue;
    // 元素個數
    private int size = 0;
    // 比較器(可拓展),可以為空,為空的話E就必須實現Comparable接口。
    private final Comparator comparator;
    // 修改次數
    private transient int modCount = 0;

添加元素(入隊)(源碼解析):

    public boolean add(E e) {
        return offer(e);
    }

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        // 修改次數+1
        modCount++;
        int i = size;
        // 判斷是否應該擴充數組
        if (i >= queue.length)
        // 數組擴容
            grow(i + 1);
        // 數組元素增加1
        size = i + 1;
        if (i == 0)
        // 第一個入隊的元素
            queue[0] = e;
        else
        // 因為有元素加入,需要調整堆(從下到上):上濾
            siftUp(i, e);
        return true;
    }

我們來看看調整堆的方法(源碼解析):

上濾(源碼解析):

    // 選擇比較器上濾還是Comparable對象的上濾
    private void siftUp(int k, E x) {
        if (comparator != null)
            // 優先選擇有比較器的進行上濾
            siftUpUsingComparator(k, x);
        else
            // Comparable對象的上濾
            siftUpComparable(k, x);
    }

    // Comparable對象的上濾
    private void siftUpComparable(int k, E x) {
        // 強制轉換成Comparable對象,這樣才能有下面的比較
        Comparable key = (Comparable) x;
        while (k > 0) {
            // k是子結點在數組中的下標,(k-1)/2可以求出父結點下標
            int parent = (k - 1) >>> 1;
            // 取得父結點
            Object e = queue[parent];
            // 如果key大於或等於父節點
            if (key.compareTo((E) e) >= 0)
                break;
            // key小於父結點
            // 父結點與子結點交換值
            queue[k] = e;
            // k跑到父結點的位置
            k = parent;
        }
        // 賦值到合適位置
        queue[k] = key;
    }
    // 使用比較器的上濾(解析和siftUpComparable一樣)
    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 siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }
    // Comparable對象的下濾
    private void siftDownComparable(int k, E x) {
        Comparable key = (Comparable)x;
        // 計算出非葉子結點個數
        int half = size >>> 1;
        while (k < half) {
            // 計算出左孩子的位置
            int child = (k << 1) + 1;
            // 取得左孩子的對象
            Object c = queue[child];
            // 右孩子的位置
            int right = child + 1;
            // 如果右孩子存在,並且左孩子大於右孩子
            if (right < size &&
                ((Comparable) 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;
    }
    // 使用比較器的下濾(解析和siftDownComparable一樣)
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

總結:閱讀一下java容器包中的類後,對數據結構的了解又加深了一層,確實源碼裡的都是“好東西”。這裡的講的類還不全面,未完持續!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved