1. 程式人生 > 實用技巧 >JavaSE學習筆記 - Collection集合

JavaSE學習筆記 - Collection集合

  • 集合是一種容器,可以用來儲存物件。在陣列中儲存物件是長度一旦確定是不能改變的,但是集合的長度是可變的。
  • 集合中儲存的都是 Java 物件的引用,集合不能儲存基本資料型別。

集合繼承結構圖

Collection

  • 單列集合類的父介面
public class Main {
    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<>();
        collection.add("abc");
        collection.add("abc");
        collection.add("bcd");
        Object[] objs = collection.toArray();
        for (int i = 0; i < objs.length; i++) {
            System.out.print(objs[i] + " ");//abc abc bcd 
        }
        System.out.println();
        System.out.println(collection.contains("abc"));//true
        System.out.println(collection.size());//3
        System.out.println(collection.isEmpty());//false
        System.out.println(collection.remove("abc"));//true
        System.out.println(collection);//[abc, bcd]
        collection.clear();
        System.out.println(collection.size());//0
    }
}

contains()

  • 通過下列原始碼可以看出,在判斷是否包含這個某一個物件的時候,底層使用了 equals 方法,由於 String 類重寫了 equals 方法,所以可以直接判斷出是否包含某一個字串,如果判斷自定義的 Java 物件的話,就需要重寫 equals 方法來按照自己寫的規則來判斷集合中是否存在某一個物件。equals 方法預設比較的是物件的引用,所以如果不重寫 equals 方法,該方法則返回 false。
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
               return i;
    }
    return -1;
}

remove()

  • 通過原始碼可以發現,remove 底層也是使用了 equals 方法,所以在刪除自定義型別的物件的時候如果使用此方法需要重寫 equals 方法,並且這個方法在刪除物件的時候只會刪除第一個出現的物件。
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;
}

Iterator

  • 遍歷集合中的元素,此介面在使用的時候需要 Colletion 通過 iterator 方法來獲取迭代器物件,使用這個介面的 hasNext 方法可以判斷是否還有可以迭代的物件,如果有則返回 true,否則返回 false。如果還有可以迭代的物件,我們就可以使用 next 方法迭代出下一個元素。如果我們想要刪除迭代器中的某一個物件,那麼就需要通過迭代器中的 remove 方法來刪除。在操作迭代器的時候,我們無法通過集合來刪除元素,因為一旦集合的結構發生了改變我們就需要獲取新的迭代器來迭代物件,否則就會發生異常。
  • 我們也可以使用增強 for 迴圈來遍歷迭代器中的元素,這個迴圈沒有下標
public class Main {
    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<>();
        collection.add("abc");
        collection.add("abc");
        collection.add("bcd");

        for (String string : collection) {
            System.out.print(string + " ");//abc abc bcd
        }
        System.out.println();
        
        Iterator<String> it = collection.iterator();
        while (it.hasNext()) {
            String string = it.next();
            //java.util.ConcurrentModificationException
            //通過集合來刪除了元素
            //集合結構一旦發生改變就需要重新獲取迭代器
            collection.remove(string);
            System.out.print(string + " ");
        }
        System.out.println();
        System.out.println(collection);

        while (it.hasNext()) {
            String string = it.next();
            it.remove();
            System.out.print(string + " ");//abc abc bcd
        }
        System.out.println();
        System.out.println(collection);//[]通過迭代器全部刪除
        
    }
}

List

  • List 集合中出現的元素是可以重複的,所有的元素都是以線性的方式儲存的,可以通過執行的索引來訪問其中的元素
  • 有序可重複:有序指的是存入的順序和取出的順序是相同的,允許儲存重複的元素
public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        System.out.println(list);//[a, a, b, b, c]

        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " ");//a a b b c
        }
        System.out.println();

        list.set(0, "d");
        System.out.println(list); //[d, a, b, b, c]

        list.remove(0);
        System.out.println(list);//[a, b, b, c]

        System.out.println(list.indexOf("b"));//1
        System.out.println(list.lastIndexOf("b"));//2
    }
}

Stack

  • 繼承自 Vector,擴容機制和 Vector 相同,這個類不經常使用。

Vector

  • 底層是陣列,方法上都有 synchronized,執行緒安全的,所以相比於 ArrayList 效率較低,用法和 ArrayList 一樣,所以這個類不經常使用。
  • 初始化容量為 10,建議使用預估計值給 Vector 一個初始容量,原因與 ArrayList 一樣,減少擴容的次數,提高程式的執行效率。
protected Object[] elementData;

public Vector() {
    this(10);
}

擴容

  • 從擴容的原始碼中可以找到如下程式碼, int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);,在 Vector 進行擴容時,新的容量是舊的容量的 2 倍。
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList

  • 底層是 final 修飾的 Object 陣列,查詢快,增刪慢。查詢快是因為在陣列中我們有了首地址和元素下標就可以通過表示式快速的計算出某一個位置的記憶體地址。
  • 使用較為頻繁,如果不指定容量,初始化容量為 10,但是我們在使用的時候最好給一個預估計的容量,這個可以減少擴容(下面解釋)的次數,增加程式的執行效率。
  • ArrayList 不能儲存大量的資料,因為在記憶體中不好找到很多連續的記憶體空間地址。
/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
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);
    }
}

擴容

  • 在擴容機制的原始碼中我們可以看到 int newCapacity = oldCapacity + (oldCapacity >> 1);,如果容量不夠的時候,新容量是是原來容量的 1.5 倍。底層通過建立一個新的 Object 的陣列,然後使用 System.arraycopy 將原來的陣列的元素拷貝到新的陣列中,原來的陣列會被垃圾回收機制銷燬。由於底層使用的是陣列拷貝,所以在使用這個集合的時候如果不指定容量,那麼擴容機制將會是程式的執行時間變長,會增加程式的執行效率,所以在使用這個集合的時候最好預估計一個容量。
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

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

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

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));
    return copy;
}

LinkedList

  • 底層是雙向連結串列,記憶體地址不連續,查詢慢,增刪快
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++;
}

雙向連結串列新增記憶體分析圖

  • 第一次呼叫 add 方法
  • 第二次呼叫 add 方法
  • 第三次呼叫 add 方法

常用方法

public class Main {
    public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<>();
        linkedList.add("a");
        linkedList.add("b");
        linkedList.add("c");
        System.out.println(linkedList);//[a, b, c]
        linkedList.addFirst("d");//[d, a, b, c]
        System.out.println(linkedList);//d
        System.out.println(linkedList.getFirst());//c
        System.out.println(linkedList.getLast());//d
        System.out.println(linkedList.remove());//d
        System.out.println(linkedList.removeFirst());//a
        System.out.println(linkedList);//[b, c]
        linkedList.pop();
        System.out.println(linkedList);//[c]
        linkedList.push("d");
        System.out.println(linkedList);//[d, c]
    }
}

Set

  • 無序不可重複:無序是指存入的順序和取出的順序不一致,且存入的元素是不可重複的。
  • 沒有索引,所以不能使用普通的 for 迴圈遍歷,使用迭代器遍歷或者增強 for 來遍歷
public class Main {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(2);
        set.add(4);
        set.add(3);
        System.out.println(set);//[1, 2, 3, 4]

        for (Integer integer : set) {
            System.out.print(integer + " ");//1 2 3 4 
        }
        System.out.println();

        Iterator<Integer> it = set.iterator();
        while (it.hasNext()) {
            Integer integer = it.next();
            System.out.print(integer + " ");//1 2 3 4 
        }
        System.out.println();

        List<Integer> list = new ArrayList<>(set);
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " ");//1 2 3 4 
        }
        System.out.println();
    }
}

HashSet

public HashSet() {
    map = new HashMap<>();
}
  • 在看完了 HashMap 之後我們知道,HashMap 的初始化容量為 16,預設載入因子為 0.75,在進行擴容的時候每次都在原來容量的基礎上乘 2,在指定容量的時候必須是 2 的倍數。HashSet 也是如此,在 HashSet 底層就是將元素放入了 HashMap 和 Key 的部分,如果把元素放入雜湊表的時候,首先需要將元素的雜湊值轉換為下標值,如果當前下標有元素,則使用當前元素和單向連結串列上面的結點的 Key 值進行比較,如果最終沒有相同的 Key 值就讓這個 Key 放入鏈尾,如果有則覆蓋 Key 值。所以使用 HashSet 儲存自定義的類時需要重寫 hashCode 和 equals 方法,這個才能讓 HashSet 的存取效率更加的高效。
public class Main {
    public static void main(String[] args) {
        Set<Person> set = new HashSet<>();
        set.add(new Person("abc"));
        set.add(new Person("abc"));
        set.add(new Person("abb"));
        set.add(new Person("acc"));
        System.out.println(set);//[Person{name='abc'}, Person{name='abb'}, Person{name='acc'}]
    }
}

TreeSet

  • HashMap 和 TreeMap 的介紹
  • 底層是 TreeMap,TreeSet 是將元素放入了 TreeMap 的 Key 部分。TreeMap 底層使用了平衡二叉樹的資料結構,我們可以對 TreeMap 的 Key 按照一定的規則排序,也就是說我們可以對 TreeSet 中的 Key 值進行排序,在儲存自定義類的時候我們必須要自定義一個排序的規則,否則程式就是丟擲異常。排序方式和 TreeMap 相同,有兩種方式對 TreeSet 中的元素進行排序。
  • 元素可以自動排序,需要重寫某些方法
public TreeSet() {
    this(new TreeMap<E,Object>());
}

自定義比較器

public class Person {
    String name;
    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
public class Main {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.name.compareTo(o2.name);
            }
        });
        set.add(new Person("abc"));
        set.add(new Person("abc"));
        set.add(new Person("abb"));
        set.add(new Person("acc"));
        System.out.println(set);//[Person{name='abb'}, Person{name='abc'}, Person{name='acc'}]
    }
}

自定義類實現 Comparable 介面

public class Person implements Comparable<Person>{
    String name;
    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public int compareTo(Person o) {
        return 0;
    }
}
public class Main {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        set.add(new Person("abc"));
        set.add(new Person("abc"));
        set.add(new Person("abb"));
        set.add(new Person("acc"));
        System.out.println(set);//[Person{name='abb'}, Person{name='abc'}, Person{name='acc'}]
    }
}

Queue

LinkedList

  • 上面已經做了敘述

ArrayDeque

  • 底層使用了可變的陣列,初始化容量為 16,也可以對容量進行預估計。由於和 LinkedList 一樣都是繼承自 Deque 介面,所以常用方法和 LinkedList 一樣。

擴容

  • int newCapacity = n << 1; 可以看出,在擴容時新的容量是原來容量的 2 倍。擴容的時候也是開了一個新的陣列,將舊的陣列的元素拷貝到新的陣列,垃圾回收器在一定的時間自動回收舊陣列。
public boolean add(E e) {
    addLast(e);
    return true;
}

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

PriorityQueue

  • 底層使用了陣列的形式來儲存資料,邏輯上是二叉堆,和 TreeMap 一樣,在使用 PriorityQueue 的時候需要實現 Comparable 介面方法。下面原始碼中使用了 Integer 這個類,它已經實現了 Comparable 介面,如果是我們自定義的類,在使用 PriorityQueue 時,我們需要實現 Comparable 來按照我們自己的規則進行排序。在使用自定義的比較器的時候我們不需要實現 Comparator 介面。
  • 在我們自定義物件的時候如果要按照我們使用的規則來排序,那麼在這個類上實現 Comparable 介面
  • 如果我們在定義 PriorityQueue 想要改變某種排序規則,那麼我們就可以使用 Comparator 的匿名內部類定義一個新的排序規則。
public class Main {
    public static void main(String[] args) {
        //在不指定排序規則的時候預設是小根堆
        Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });

        queue.add(1);
        queue.add(3);
        queue.add(2);

        System.out.println(queue.peek());
        while (!queue.isEmpty()) {
            System.out.print(queue.poll() + " "); //3 2 1
        }

    }
}
  • 初始化容量為 11
transient Object[] queue; // non-private to simplify nested class access

public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}
  • 擴容: int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));,從原始碼中我們可以看出,這個二叉堆在 6 層以內的時候,新的容量是舊的容量 + 2,在大於 6 層的時候,新的容量 = 舊容量 + 舊 容量 >> 1,也就是擴容為原來的 1.5 倍。
public boolean add(E e) {
   return offer(e);
}

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}

Collections工具類

常用方法

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("abc");
        list.add("bcd");
        list.add("acd");
        Collections.addAll(list, "a", "b", "c");
        System.out.println(list);//[abc, bcd, acd, a, b, c]

        Collections.shuffle(list);//打亂順序
        System.out.println(list);//[acd, b, bcd, a, c, abc]
    }
}

排序

  • 底層使用了快速排序演算法,Collections 不能對 Set 集合排序,如果想要排序需要把 Set 集合轉換為 List 集合,然後進行排序或者自定義排序即可。被排序的物件必須要實現 Comparable 介面。
public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("abc");
        list.add("bcd");
        list.add("acd");
        Collections.addAll(list, "a", "b", "c");

        Collections.sort(list);
        System.out.println(list);//[a, abc, acd, b, bcd, c]
    }
}

自定義排序

  • 實現 Comparable
public class Main {
    public static void main(String[] args) {
        List<Person> list = new ArrayList<>();
        Collections.addAll(list, new Person(1, "a"), new Person(3, "b"), new Person(2, "c"));
        System.out.println(list);//[Person{no=1, name='a'}, Person{no=3, name='b'}, Person{no=2, name='c'}]
        Collections.sort(list);
        System.out.println(list);//[Person{no=1, name='a'}, Person{no=2, name='c'}, Person{no=3, name='b'}]

        //使用比較器自定義規則
        Collections.sort(list, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o2.no - o1.no;
            }
        });
        System.out.println(list);//[Person{no=3, name='b'}, Person{no=2, name='c'}, Person{no=1, name='a'}]
    }
}
  • 自定義比較器
public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(3);
        list.add(5);
        Collections.addAll(list, 2, 4, 6);

        //預設升序
        Collections.sort(list);
        System.out.println(list);//[1, 2, 3, 4, 5, 6]

        //自定義比較器變為降序
        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(list);//[6, 5, 4, 3, 2, 1]
    }
}