1. 程式人生 > 其它 >JavaSE:集合

JavaSE:集合

集合

涉及的知識點

  • Java 知識:泛型
  • 資料結構線性表、樹、散列表
  • 設計模式迭代器

集合:物件的容器,提供操作物件的方法

分為 Collection 和 Map 兩個體系。

  • Collection:儲存元素
  • Map:儲存記錄(K-V 鍵值對)

Collection 體系

  • List:有序、有下標、元素可重複

    JDK 儲存結構 執行緒安全
    ArrayList 1.2 陣列 執行緒不安全
    LinkedList 1.2 雙向連結串列
    Vector 1.0 陣列 執行緒安全
  • Set:無序、無下標、元素不可重複

    JDK 儲存結構 元素不重複 說明
    HashSet 1.2 HashMap 基於 hashCode 判斷相等:先 hashCode(),equals()
    LinkedHashSet 1.4 連結串列 基於hashCode 繼承自 HashSet,且可保留元素的插入順序
    TreeSet 1.2 NavigableMap 基於排列順序 實現 SortedSet 介面,對集合元素自動排序。
    (元素物件需實現 Comparable 介面,指定排序規則)

Collection 介面

Collection 體系的父介面。

強調幾個方法

  • iterator():返回集合的迭代器,由具體實現類負責迭代器的工作規則。
  • toArray():返回包含 collection 中所有元素的陣列。
  • removeAll():刪除此集合中包含在指定集合中的元素(集合相減)
  • retainAll():僅保留此集合中包含在指定集合中的元素(集合交)

--

List 介面

特點:有序、有下標、元素可以重複。

方法:除了 Collection 父介面中的方法,還定義了一些新的方法。

說明

1、強調幾個方法

增加了幾個與下標有關的方法,還引入了一個列表迭代器

  • add(int, E)、remove():指定位置插入

  • remove():指定位置刪除

  • get()、set():指定位置讀寫

  • indexOf():獲取元素下標(首次出現)

  • lastIndexOf():獲取元素下標(最後一次出現)

  • listIterator():列表迭代器,比 iterator 功能更強大

  • subList():子集,左閉右開

2、注意幾個問題

  • 自動裝箱:集合不能存放基本型別,但是 Java 會自動裝箱為對應的包裝型別。

  • remove():集合中存放的是整數時,呼叫此方法需要區分是 “按下標刪除” 還是 “按元素刪除”

    • 按下標:傳參為下標;
    • 按元素:傳參為物件,需要強轉為 Object 或使用包裝類。
  • equals()

    • remove()、indexOf()、lastIndexOf()、contains() 等,都是通過 equals() 方法來判斷元素是否相同;

    • Object 類預設實現:比較引用地址(return (this == obj);

    • 若要自定義比較規則,需要在自定義類中重寫 equals() 方法。

      @Override
      public boolean equals(Object obj) {
          if (obj == null) {
              return false;
          }
      
          if (this == obj) {
              return true;
          }
      
          if (obj instanceof Person) {
              Person p = (Person) obj;
      
              return this.name.equals(p.name) && p.age == this.age;
          }
      
          return false;
      }
      

3、遍歷 List

以 ArrayList 為例,演示一下遍歷的幾種方式。

  • for

  • 增強 for

  • 迭代器

  • list 迭代器:向後、向前

    ArrayList<Person> list = new ArrayList<>();
    
    @Test
    public void testTraverse() {
    	// for
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    
    	// foreach
        for (Person person : list) {
            System.out.println(person);
        }
    
    	// iterator
        Iterator<Person> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    
        // listIterator
        ListIterator<Person> listIterator = list.listIterator();
        // 向後
        while(listIterator.hasNext()){
            System.out.println(listIterator.next());
        }
    
        // 向前
        while(listIterator.hasPrevious()){
            System.out.println(listIterator.previous());
        }
    }
    

ArrayList 原始碼分析(!)

部分原始碼分析

屬性

  • elementData:存放元素的陣列
  • size:實際元素個數(ArrayList 的長度)
  • DEFAULT_CAPACITY:預設初始容量,10
  • EMPTY_ELEMENTDATA:空陣列
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:預設容量的空陣列

二者都是空陣列例項,區別在於使用的場景不同。後者是用於在新增首個元素時,瞭解膨脹的大小。

原始碼:無參建構函式

  • 將【預設容量的空陣列】賦值給 elementData
  • 說明:僅呼叫無參建構函式,沒有新增元素時,size 和 capacity 為 0
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

原始碼:add()

// 1
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
// 2
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
// 3
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// 4
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
    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);
}

首次呼叫 add()

  1. add():呼叫方法 2,傳參為 1

  2. ensureCapacityInternal()

  3. if:二者相等,成立

  4. Math.max():預設容量 == 10,minCapacity == 1,取最大值。minCapacity == 10

  5. 呼叫方法 3,傳參為 10

  6. ensureExplicitCapacity()

    1. if:minCapacity == 10,elementData.length == 0,成立。
    2. 呼叫方法 4,傳參為 10
  7. grow():陣列擴容

  • 第一個 if:old == 0,new == 0,成立。new == 10
  • 第二個 if:不成立
  • Arrays.copyOf():將 elementData 賦值為一個容量為 10 的陣列
    (此時 ArrayList 的陣列才具有預設容量)
  1. 回到 add():賦值並返回 true。

再次呼叫 add()

(陣列未滿的情況下)

假設集合中已有 7 個元素,size == 7

  1. add():呼叫方法 2,傳參為 8
  2. ensureCapacityInternal()
    1. if:二者不相等,不成立
    2. 呼叫方法 3,傳參為 8
  3. ensureExplicitCapacity()
    1. if:minCapacity == 8,elementData.length == 10,不成立。
    2. 說明不需要擴容,直接回到 add()
  4. 回到 add():賦值並返回 true。

陣列擴容

(陣列已滿)

目前已有 10 個元素,size ==10

  1. add():呼叫方法 2,傳參為 11
  2. ensureCapacityInternal()
    1. if:二者不相等,不成立
    2. 呼叫方法 3,傳參為 11
  3. ensureExplicitCapacity()
    1. if:minCapacity == 11,elementData.length == 10,成立。
    2. 呼叫方法 4,傳參為 11
  4. grow():陣列擴容
    • old == 10,new == 15(old >> 1,表示右移一位,即除以 2)
    • 第一個 if:不成立
    • 第二個 if:不成立
    • Arrays.copyOf():將 elementData 賦值為一個容量為 15 的陣列
  5. 回到 add():賦值並返回 true。

結論陣列擴容增量,每次是原來的 1.5 倍new = old + (old >> 1)

LinkedList 原始碼分析

需要了解:資料結構:線性表——連結串列

Node<E>

LinkedList 將 Node<E> 宣告為內部類。

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

屬性

  • size:實際元素個數(LinkedList 的長度)
  • first:頭結點
  • last:尾結點

如何新增元素

為了方便描述

  • 當前頭結點(first)、當前尾結點(last)
  • 新結點(nNode):前驅(prev)、後繼(next)

先把原始碼放一遍,思考一下如何將元素插入雙向連結串列?

  1. 建立 nNode:prev 為 last,next 為空

    Node nNode = new Node(last, data, null);
    
  2. last.next 改為 nNode:需要考慮一個問題,last == null 嗎?

    • 如果 last == null,說明連結串列為空。新增 nNode 後作為當前連結串列的 first 和 last。
    • 如果 last != null,則將 last.next 改為 nNode,並且 nNode 作為當前連結串列的 last。
    if (last == null){
        first = nNode;
        last = nNode;
    } else {
        last.next = nNode;
        last = nNode;
    }
    
  3. 由於 if-else 有重複程式碼,可以抽取出來。

    // 完整的新增語句
    Node nNode = new Node(last, data, null);
    
    if (last == null){
        first = nNode;
    } else {
        last.next = nNode;
    }
    
    last = nNode;
    

圖示過程

  1. last == null(空表)

  2. last != null

原始碼:add()

知道如何新增元素後,再來看看原始碼。

可以看出幾點區別

  • 將 last 賦給一個常值變數 l
  • 先將 last 指向 nNode,再判斷空值
  • 其它操作基本相同,對著原始碼理解即可。
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++;
}

為什麼要將 last 賦值給一個區域性常量

而不是像剛才我們分析的那樣,直接用 last 操作呢?

我的個人理解(從 JVM 角度)

  • last 是一個成員變數,位於堆中。
  • 將 last 儲存到一個區域性常量,位於常量池(相當於一個本地快取)
  • 從常量池中讀取資料,比從堆中讀取資料的效率高。
  • 結論先將 last 賦值給一個區域性常量,在一定程度上能提高效率。

Vector

Vector 現已很少使用,值得注意的是。類中使用到了 Enumeration 列舉器(JDK 1.0)。

  • Enumeration(JDK 1.0)
  • Iterator(JDK 1.2)

在 Iterator 誕生之前,就是使用 Enumeration 遍歷集合。

而 Iterator 就是用來取代 Enumeration 的,具體可檢視 設計模式:迭代器模式

Set 介面

特點:無序、無下標、元素不可重複。

方法:只有 Collection 父介面方法,沒有定義其它方法。

  • List 介面:有下標的概念,因此定義了一系列有關下標的方法。
  • Set 介面:沒有下標的概念,所有方法都是繼承自 Collection 父介面。

遍歷 Set

遍歷 List 的方式:for、增強 for、迭代器、list 迭代器。

  • Set 無下標,無法用 for 迴圈。
  • list 迭代器是 List 集合獨有的。

以 HashSet 為例,演示遍歷的兩種方式。

  • 增強 for

  • 迭代器

    HashSet<Person> set = new HashSet<>();
    
    @Test
    public void testTraverse() {
        // for
        for (Person person : set) {
            System.out.println(person);
        }
    
    	// 迭代器
        Iterator<Person> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
    

淺聊 HashTable

在學習 HashSet 之前,先了解一下 HashTable 的結構,有助於理解。

  • HashTable 是一種資料結構,它是陣列與連結串列的結合。
  • 屬於 Map 體系,在之後會詳細講解

從整體上看,HashTable 是一個數組,而陣列中的每個元素是一張連結串列。

  • 如何理解每個元素是一張連結串列

    • HashTable 宣告一個結點型別的陣列,每個陣列元素就是一個連結串列的頭結點。
    • 通過頭結點,就得到一張連結串列。
  • HashTable 中的結點是什麼樣的

    • HashTable 屬於 Map 體系,存放的是記錄(鍵值對),記為 Entry<K,V>
    • 從原始碼可以看出,Entry 類就是結點類(Node)。
    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
    
    	// 方法
    }
    
  • 結論HashTable 的儲存結構,是一個 Entry<K,V> 型別的陣列

    private transient Entry<?,?>[] table;
    

HashSet(!)

儲存機制:通過 hashCode() 找位置,通過 equals() 判斷相等

以新增元素 e 為例,根據 e 的雜湊值找到要存放的陣列位置。

  • 對應位置上沒有元素,將 e 儲存到當前位置。
  • 對應位置上已有元素,對當前位置的連結串列進行遍歷,逐個判斷 equals()。
    • 如果 equals() 返回 true,說明已有相同記錄,無法新增。
    • 否則,新記錄插入到連結串列中。

Object 類中的預設實現

public native int hashCode();

public boolean equals(Object obj) {
    return (this == obj);
}
  • equals():比較物件引用地址,同一個物件引用才相等。

  • hashCode():本地方法,通常與執行環境有關。

    • 同一個物件引用的雜湊值相等。
    • 不同物件引用,即使屬性完全相等,雜湊值也不相等。

    關於 hashCode() 的小實驗

    • 使用 new 關鍵字建立 10000 個物件,將物件的雜湊值新增到集合中。

    • 進入雙重 for 迴圈,判斷是否存在相同的雜湊值。

    • 結果是 fasle

    • 為了證明是沒有重複 hashCode,而不是我的程式碼敲錯了。向集合中加入一個重複的 hashCode(通過列印 ArrayList 得出),再次執行結果為 true