1. 程式人生 > >Java 集合之 Collection

Java 集合之 Collection

face exception ima oci 實體 try ransient seq 對象的引用

集合就是一組數的集合,就像是一個容器,但是我們應該清楚的是集合中存放的都是對象的引用,而不是真正的實體。而我們常說的集合中的對象其實指的就是對象的引用。

我們可以把集合理解為一個小型數據庫,用於存放數據,我們對集合的操作也就是數據的增刪改查,在 Java 中有兩個頂層接口 Collection 和 Map 用於定義和規範集合的相關操作。這篇文章主要說一下集合框架中的 Collection 部分。

Collection 表示一組對象,這些對象可以是有序也可以是無序的,它提供了不同的子接口滿足我們的需求。我們主要看看 List 和 Set 。

技術分享圖片

List 整體的特征就是有序可重復。我們需要研究的是上圖中具體的實現類都有什麽特性。底層的實現原理是什麽,首先來看一看 List 的古老的實現類 Vector ,說是古老是因為在 JDK 1.0 的時候就出現了,都走開,我要開始看源碼了!這些源碼來自於 JDK 1.7。

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    /**
     * The array buffer into which the components of the vector are
     * stored.
     */
    protected Object[] elementData;

    /**
     * The number of valid components in this {
@code Vector} object. */ protected int elementCount; /** * The amount by which the capacity of the vector is automatically * incremented when its size becomes greater than its capacity. If * the capacity increment is less than or equal to zero, the capacity * of the vector is doubled each time it needs to grow.
*/ protected int capacityIncrement; public Vector() { this(10); } // 添加元素 public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } // 刪除元素 public synchronized E remove(int index) { modCount++; if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); E oldValue = elementData(index); int numMoved = elementCount - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--elementCount] = null; // Let gc do its work return oldValue; } // 修改元素 public synchronized E set(int index, E element) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } // 查找元素 public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); } ...

就以上源碼分析就可以知道關於 Vector 的特征。1 底層實現是使用數組來存儲數據,所以相應的查找元素和添加元素速度較快,刪除和插入元素較慢。2 數組的初始長度為 10 ,當長度不夠時,增長量也為 10 使用變量 capacityIncrement 來表示。3 方法的聲明中都加入了 synchronized 關鍵字,線程安全的,所以效率相應降低了。 4 沒分析出來的再看一遍。

下面開始看 ArrayList 的源碼。

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

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * DEFAULT_CAPACITY when the first element is added.
     */
    private transient Object[] elementData;

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

    // 添加元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    // 增加數組的長度
    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);
    }
...

因為源碼和 Vector 類似,所以有些就不貼了,但是不耽誤我們繼續分析 ArrayList 。1 底層存儲數據使用的還是數組,長度依舊為 10 ,但是進步了,沒有在剛開始創建的時候就初始化,而是在添加第一個元素的時候才初始化的。2 方法的聲明少了 synchronized 關鍵字,線程不安全,但性能提高了。3 數組長度不夠時,會自動增加為原長度的 1.5 倍。

以上分析也能體現出 Vector 和 ArrayList 的差別。主要就是想說 Vector 已經不用了。使用 ArrayList 即可,關於線程安全問題,後面再說。

接著看 LinkedList 的實現,上源碼 ~

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     */
    transient Node<E> last;

    /**
     * Constructs an empty list.
     */
    public 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;
        }
    }

    // 添加元素
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

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

    // 刪除某個節點的邏輯
    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 的底層實現是雙鏈表。這樣就會有以下特性,1 查找元素較慢,但是添加和刪除較快。2 占內存,因為每一個節點都要維護兩個索引。3 線程不安全 。4 對集合長度沒有限制。

以上,List 的幾個實現已經分析完成,以後再談到 Vector ,ArrayList ,LinkedList 之間的區別應該不會不知所雲了吧!還要接著看 Collection 的另一個子接口 Set 。首先有個大前提,Set 中存儲的元素是無序不可重復的。然後我們再來看實現類是如何實現的。下面開始 HashSet 的表演。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

    // 添加元素,其實就是像 Map 中添加主鍵,所以添加的元素不能重復
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    // 實現 Iterable 接口中的 Iterator 方法。
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
...

看了源碼才發現,1 原來 HashSet 就是對 HashMap 的封裝啊,底層實現是基於 hash 表的,回頭有必要再好好的介紹一下 hash 相關的知識。2 Set 集合中的值,都會以 key 的形式存放在 Map 中,所以說 Set 中的元素不能重復。3 線程不安全。4 允許存放空值,因為 Map 的鍵允許為空。

今天要說的最後一個實現類終於出現啦,他就是 TreeSet ,這個實現類中的元素是有序的!註意這裏說的有序是指按照一定的規則排序,而我們說 Set 集合中的元素無序是因為添加進集合的順序和輸出的順序不保證一致。TreeSet 是怎麽保證有序我們待會再說,還是一樣的套路,是對 TreeMap 的封裝,線程依舊不安全。

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    /**
     * The backing map.
     */
    private transient NavigableMap<E,Object> m;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * Constructs a set backed by the specified navigable map.
     */
    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

    /**
     * Constructs a new, empty tree set, sorted according to the
     * natural ordering of its elements.  All elements inserted into
     * the set must implement the Comparable interface.
     */
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

    /**
     * Constructs a new, empty tree set, sorted according to the specified
     * comparator.  All elements inserted into the set must be mutually
     * comparable by the specified comparator
     * If the Comparable natural ordering of the elements will be used.
     */
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    
    // 添加元素方法
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }    
...

我們可以看到 TreeSet 中構造函數上方的註釋,TreeSet 要保證元素有序,保證有序的思路是在添加元素的時候進行比較。

... // 這是 TreeMap 的 put 方法的節選,為了看到比較的過程。
    public V put(K key, V value) {
    ...
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        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
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            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);
        }
    ...
    }

Java 中提供了兩種方式,第一種方法,需要我們所添加對象的類實現 Comparable 接口,進而實現 compareTo 方法,這種方式也叫自然排序,我們並沒有傳入什麽排序規則。這種方式對應 TreeSet 的空參構造器。而另一種方式就是定制排序,即我們自己定義兩個元素的排序規則,在實例化 TreeSet 的時候傳入對應的排序規則即可,對應於 TreeSet 中帶有 Comparator 接口的構造器,這裏面需要實現 compare 方法 。有點迷糊了是吧,舉個例子看看 ~

public class Person implements Comparable<Person>{

    public String name;
    public Integer age;

    public Person(String name,Integer age) {
        this.name = name;
        this.age = age;
    }
    /* 自定義的比較的邏輯:
     *      首先按照對象的 name 屬性排序
     *      其次按照 age 屬性排序
     * 方法的返回值為 0 ,大於 0,小於 0 ,分別對應於 相等,大於,和小於
     */
    @Override
    public int compareTo(Person o) {
        int i = this.name.compareTo(o.name);
        if(i == 0){
            return this.age.compareTo(o.age);
        }else {
            return i;
        }
    }
    @Override
    public String toString() {
        return "[Person] name:"+this.name+" age:"+this.age;
    }
}

    // 以下是測試代碼
    public static void main(String[] args) {
        TreeSet<Person> set = new TreeSet<>();
        set.add(new Person("AJK923",20));
        set.add(new Person("BJK923",20));
        set.add(new Person("AJK923",21));
        set.add(new Person("BJK923",21));

        for (Person person : set) {
            System.out.println(person.toString());
        }
        /*
        [Person] name:AJK923 age:20
        [Person] name:AJK923 age:21
        [Person] name:BJK923 age:20
        [Person] name:BJK923 age:21
        */
                
        ----以下為定制排序的部分----匿名內部類實現 Comparator 接口----        
        TreeSet<Person> set2 = new TreeSet<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                int i = o1.age.compareTo(o2.age);
                if(i == 0){
                    return o1.name.compareTo(o2.name);
                }else {
                    return i;
                }
            }
        });

        set2.add(new Person("AJK923",20));
        set2.add(new Person("BJK923",20));
        set2.add(new Person("AJK923",21));
        set2.add(new Person("BJK923",21));

        for (Person person : set2) {
            System.out.println(person.toString());
        }
        /*
            [Person] name:AJK923 age:20
            [Person] name:BJK923 age:20
            [Person] name:AJK923 age:21
            [Person] name:BJK923 age:21
         */
    }

上面就是自然排序和定制排序的應用。要說明幾點:

1 定制排序和自然排序同時存在的話,優先執行定制排序。可以看看上面的 put 方法的實現 。

2 自然排序對應 Comparable 接口,定制排序對應 Comparator 接口。

3 String ,包裝類,日期類都已經實現了 Comparable 接口的 comparaTo 方法,所以我上面的例子中都偷懶了,沒有自己實現具體比較,而是直接調用現成的。

看到這裏,也算是對 Collection 有了整體的認識,但是這裏我並沒有說到具體的 API ,我現在日常也用不到幾個方法,就放一張 Collection 接口的結構圖吧,方法也比較簡單,看個名字就大概知道什麽意思了。

技術分享圖片

還是要重點說一下 iterator 方法。這個方法定義在 Iterable 接口中(Collection 繼承了 Iterable 接口),方法返回的是 iterator 接口對象。iterator 中定義了叠代器的操作規則,而 Collection 的實現類中是具體的實現。Iterator 接口中定義了 next ,hasNext 和 remove 方法。看一下在ArrayList 中是如何實現的吧。

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;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

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


應用起來還是比較簡單的,使用一個 while 循環即可。看下面這個例子。



       public static void main(String[] args) {

        List<Integer> list = new ArrayList<>(); 
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> it = list.iterator();
        while (it.hasNext()) {
            Integer i = it.next();
            System.out.println(i);
        }
    }

不知道你有沒有發現,除了 Vector 以外,保存集合元素的那個變量都定義為 transient 不管你是數組,Node 或是 Map ,不由得我又要想一想為什麽這樣設計?

先看一下 transient 的作用吧!Java 的 serialization 提供了一種持久化對象實例的機制。當持久化對象時,可能有一個特殊的對象數據成員,我們不想用 serialization 機制來保存它。為了在一個特定對象的一個域上關閉 serialization,可以在這個域前加上關鍵字 transient 。當一個對象被序列化的時候,transient 型變量的值不包括在序列化的表示中,非 transient 型的變量是被包括進去的。

那麽為什麽要這麽做呢,好好的標準序列化不用,原因如下:

對於 ArrayList 來說,底層實現是數組,而數組的長度和容量不一定相等,可能容量為 10 ,而我們的元素只有 5 。此時序列化的時候就有點浪費資源,序列化和反序列化還是要的,故 ArrayList 自己實現了兩個方法,分別是 writeObject 和 readObject ,用於序列化和反序列化。

對於 HashSet 和 HashMap 來說,底層實現都是依賴於 hash 表,而不同的 JVM 可能算出的 hashCode 值不一致,這樣在跨平臺的時候就會導致序列化紊亂。故也重寫了那兩個方法。借用一句似懂非懂的話:

當一個對象的物理表示方法與它的邏輯數據內容有實質性差別時,使用默認序列化形式有 N 種缺陷,所以應該盡可能的根據實際情況重寫序列化方法。

對應於 Collection ,有一個 Collections 工具類,其中提供很多方法,比如說集合的排序,求子集合,最大值,最小值,交換,填充,打亂集合等等,還記得上面說到的實現類中存在線程不安全的情況吧,這個工具類中提供很多對應的 synchronized 的方法。

後記 :不知不覺中擴展了這麽多知識點,實話說,肯定還有遺漏的地方,就我現在能想到的依然還有很多,除去 Hash 表和 Hash 算法這一部分之外,我還沒有對底層的數據結構進一步分析,數組,鏈表,二叉樹等等,現在是分析不動了,本文已經很長了。

Java 集合之 Collection