JavaSE:集合
集合
涉及的知識點
集合:物件的容器,提供操作物件的方法
分為 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()
-
add():呼叫方法 2,傳參為 1
-
ensureCapacityInternal()
-
if:二者相等,成立
-
Math.max():預設容量 == 10,minCapacity == 1,取最大值。minCapacity == 10
-
呼叫方法 3,傳參為 10
-
ensureExplicitCapacity()
- if:minCapacity == 10,elementData.length == 0,成立。
- 呼叫方法 4,傳參為 10
-
grow():陣列擴容
- 第一個 if:old == 0,new == 0,成立。new == 10
- 第二個 if:不成立
- Arrays.copyOf():將 elementData 賦值為一個容量為 10 的陣列
(此時 ArrayList 的陣列才具有預設容量)
- 回到 add():賦值並返回 true。
再次呼叫 add()
(陣列未滿的情況下)
假設集合中已有 7 個元素,size == 7
- add():呼叫方法 2,傳參為 8
- ensureCapacityInternal()
- if:二者不相等,不成立
- 呼叫方法 3,傳參為 8
- ensureExplicitCapacity()
- if:minCapacity == 8,elementData.length == 10,不成立。
- 說明不需要擴容,直接回到 add()
- 回到 add():賦值並返回 true。
陣列擴容
(陣列已滿)
目前已有 10 個元素,size ==10
- add():呼叫方法 2,傳參為 11
- ensureCapacityInternal()
- if:二者不相等,不成立
- 呼叫方法 3,傳參為 11
- ensureExplicitCapacity()
- if:minCapacity == 11,elementData.length == 10,成立。
- 呼叫方法 4,傳參為 11
- grow():陣列擴容
- old == 10,new == 15(old >> 1,表示右移一位,即除以 2)
- 第一個 if:不成立
- 第二個 if:不成立
- Arrays.copyOf():將 elementData 賦值為一個容量為 15 的陣列
- 回到 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)
先把原始碼放一遍,思考一下如何將元素插入雙向連結串列?
-
建立 nNode:prev 為 last,next 為空
Node nNode = new Node(last, data, null);
-
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; }
-
由於 if-else 有重複程式碼,可以抽取出來。
// 完整的新增語句 Node nNode = new Node(last, data, null); if (last == null){ first = nNode; } else { last.next = nNode; } last = nNode;
圖示過程
-
last == null(空表)
-
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