一. 手寫高仿ArrayList集合
基本原理思想
Arraylist集合底層使用動態數組實現,隨機查詢效率非常快,插入和刪除需要移動整個數組、效率低。
1. 高仿ArrayList集合
public interface MyList<E> {
/**
* 集合的大小(長度)
* @return
*/
int size();
/**
* 往集合中添加我們的元素
* @param e
* @return
*/
boolean add(E e);
/**
* 使用下標查詢到我們的集合元素
* @param index
* @return
*/
E get(int index);
/**
* 使用下標位置刪除我們的元素
* @param index
* @return
*/
public E remove(int index);
}
public class MyArraylist<E> implements MyList<E> {
/**
* elementData數據存放我們Arraylist所有的數據 transient作用不能序列化
*/
transient Object[] elementData;
/**
* 給我們的數組容量賦值爲空
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/*
* 數組的容量默認大小爲0
*/
private int size;
/**
* 數組默認容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 2的31次方-1 -8
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public MayiktArraylist() {
// 給我們的數組容量賦值爲空
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
@Override
public int size() {
return size;
}
@Override
public boolean add(E e) {
// 對我們的數組實現【擴容】
ensureCapacityInternal(size + 1);
// 對我們的數據元素【賦值】
elementData[size++] = e;
return true;
}
@Override
public E get(int index) {
rangeCheck(index);
// 根據下標從數組中查詢到數據
return (E) elementData[index];
}
@Override
public E remove(int index) {
// 檢查我們的下標位置是否越界
rangeCheck(index);
// 獲取要刪除的對象
E oldValue = get(index);
//計算移動的位置
int numMoved = size - index - 1;
// 判斷如果刪除數據的時候 不是最後一個的情況下,將刪除後面的數據往前移動一位
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
}
// 如果numMoved 爲0的情況下,說明後面不需要往前移動,直接將最後一條數據賦值爲null
elementData[--size] = null; // clear to let GC do its work
return oldValue ;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("下標位置越界啦index:" + index);
}
private void ensureCapacityInternal(int minCapacity) {
// 添加元素的時候 如果我們數組是爲空的情況下
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//DEFAULT_CAPACITY =10; minCapacity=0+1 DEFAULT_CAPACITY=10 10>1
//minCapacity=10;
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
// modCount++; 增刪改的時候 modCount++
private void ensureExplicitCapacity(int minCapacity) {
// 10- 數組長度 (0) 10-0 >0 作用:判斷我們數組中是否需要繼續擴容
if (minCapacity - elementData.length > 0) {
// 對我們的數組實現擴容
grow(minCapacity);
}
}
// length 和size區別
private void grow(int minCapacity) {
// 獲取我們的數組的長度 old原來的 new新的 原來數組容量 0;
int oldCapacity = elementData.length;
// 新的容量 = 原來的容量 + 原來的容量/2 = 0
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 新的容量(0)-最小的容量(10) < 0 -10
if (newCapacity - minCapacity < 0) {
// 新的容量=10 作用:第一次對我們數組做初始化容量操作
newCapacity = minCapacity;
}
// 判斷我們擴容長度大於Integer 21 最大值的情況下
// 限制我們數組擴容最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 開始對我們的數組實現擴容 對我們的數據實現擴容(newCapacity) 將舊的數組數據複製到新的數組中,並可以指定長度
elementData = Arrays.copyOf(elementData, newCapacity);//這裏無非就是把相同的數據複製了一份,並重新指定了數組的length
}
/**
* 判斷我們最小的初始化容量
* @param minCapacity minCapacity==最小的容量>Integer 21 最大值的情況下 Integer.MAX_VALUE
* minCapacity 相當於當前添加元素的下標位置
* @return
*/
private static int hugeCapacity(int minCapacity) {
/*
Integer.MAX_VALUE : 2147483647
Integer.MAX_VALUE + 1 :-2147483648
只要大於IntegerMAX_VALUE,就會變爲負數,所以這裏判斷是否小於0
*/
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
}
public class Test {
public static void main(String[] args) {
MyArraylist<String> list= new MyArraylist<String>();
for (int i = 0; i < 10; i++) {
list.add("元素" + i);
}
System.out.println(list.size());
list.add("元素11");
System.out.println("刪除數據之前:");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// // 下一次數組擴容是在什麼時候呢?
list.remove(2);
System.out.println("刪除數據之後:");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// // 數組的下標爲0 開始 刪除第一條數據 10=11
// System.out.println(list.get(10));
}
}
小結:第一次add的時候,會擴容/初始化 數組length爲10,下一次擴容是在什麼時候?
答案:添加第11條數據的時候【原理見ensureExplicitCapacity()方法】
添加第11條數據的時候,length會擴容到多少呢?
答案:15 【原理見grow()方法】
// 新的容量 = 原來的容量 + 原來的容量/2 = 10 + 10/2 = 15
int newCapacity = oldCapacity + (oldCapacity >> 1);
以此類推,添加第16條的時候,會擴容到16+16/2=24
數組最大容量就是Integer.MAX_VALUE
2. 仔細看ArrayList的源碼,不難發現,每次add和remove的時候,都會調用全局變量modCount++
爲什麼這麼做呢,此時我們先引入一個理論:Fail-Fast機制原理
Fail-Fast是我們Java集合框架爲了解決集合中結構發生改變的時候,快速失敗的機制。
舉例說明:
package com.example;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Test004 {
// 定義一個全局的集合存放 存在在高併發的情況下線程安全的問題
private List<String> strings = new ArrayList<String>();
// private List<String> strings = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
new Test004().startRun();
}
public void startRun() {
new Thread(new ThreadOne()).start();
new Thread(new ThreadTWo()).start();
}
// 打印我們的數據
private void print() {
strings.forEach((t) -> System.out.println("t:" + t));
}
class ThreadOne implements Runnable {
@Override
public void run() {
// 存儲數據
for (int i = 0; i < 10; i++) {
strings.add("i:" + i);
print(); // 打印數據
}
}
}
class ThreadTWo implements Runnable {
@Override
public void run() {
for (int i = 10; i < 20; i++) {
strings.add("i:" + i);
print();
}
}
}
}
執行main方法,會報錯:ConcurrentModificationException即併發修改異常
那麼我們定位到ArrayList的第1252行:
即我們調用下圖的時候,會走到上圖ArrayList源碼(用for each的時候,底層還是用for循環去遍歷)
for each原理:會把modCount賦值給一個臨時變量/期望變量expectedModCount,for循環的時候,判斷modCount的值是否發生變化,如果沒發生變化,纔會進行打印值等操作,如果發生變化,則會拋出併發修改異常。
爲什麼會拋出異常:modCount是全局的,可能會被add,remove進行更改,而我們的臨時變量expectedModCount是局部的,不會更改。線程一和線程二可能會同時調用add方法,比如線程一調用完add,在打印之前,線程二調用add方法(modCount++),此時會導致modCount和expectedModCount不一致,則拋出併發修改異常。
解決上面Fail-Fast場景,可以使用CopyOnWriteArrayList,該集合是,添加和修改的時候都加了Lock鎖。
CopyOnWriteArrayList特點:
- 內部持有一個ReentrantLock lock = new ReentrantLock();
- 底層是用volatile transient聲明的數組 array
- 讀寫分離,寫時複製出一個新的數組,完成插入、修改或者移除操作後將新數組賦值給array
3. ArrayList刪除原理
這裏我們重點分析System.arraycopy方法,首先貼出ArrayList的remove方法
System提供了一個靜態方法arraycopy(),我們可以使用它來實現數組之間的複製,其函數原型是:
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) ;參數解析如下:
src | 源數組 |
srcPos | 源數組要複製的起始位置 |
dest | 目的數組 |
destPos | 目的數組放置的起始位置 |
length | 複製的長度 |
注意:src 和 dest都必須是同類型或者可以進行轉換類型的數組。下面舉例講解:
package com.example;
public class Test003 {
public static void main(String[] args) {
Object[] objects = new Object[]{"0", "1", "2", "3"};
// 要刪除元素的索引,也就是目的數組放置的起始位置
int index = 0;
// 源數組要複製的起始位置
int destPos = index + 1;
// 要複製的長度
int numMoved = objects.length - index - 1;
System.arraycopy(objects, destPos, objects, index, numMoved);
objects[objects.length - 1] = null;
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]);
}
}
}
運行結果爲:
那麼疑問來了,我都刪了,爲什麼還有四個元素?
答案:這是數組的長度爲4,在實際ArrayList刪除的時候,會發現,即集合的size會減1,但是數組的容量還是沒變的,這點不要混淆了~ 。如果要刪除1,即前面的元素不會影響,只移動後面的,並把最後一個置爲null。
下面貼出一段代碼:
public class AAA {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
System.out.println(list.size());
list.remove(3);
System.out.println(list.size());
System.out.println(list.get(4));
}
執行結果爲:
說好的刪了會置爲null呢?原因是ArrayList源碼中調用get()方法時有個下標監測判斷~
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("下標位置越界啦index:" + index);
}
}
ArrayList的remove方法核心就是使用了System的靜態方法arraycopy( );再次貼出我們手寫的ArrayList源碼:
可以發現,會先判斷numMoved,如果numMoved大於0,證明要刪除的不是最後一個元素,則執行System.arraycopy方法進行數組複製;複製完成後,會執行 elementData[--size] = null; 將最後一個元素置爲null。並且size減1。(--size代表先執行size=size-1,然後再使用size的值,所以將數組的第size-1個元素,也就是最後一個元素置爲null【注意索引從0開始】)
二. ArrayList,Vector,CopyOnWriteArrayList 對比
1. ArrayList與Vector的區別
說到這裏,我們又會想到一個集合Vector,該集合是ArrayList的前身,相信有一定基礎的小夥伴都瞭解過該集合。
這裏我結合源碼,列舉一下ArrayList與Vector的區別:
相同點:底層都是採用數組實現
不同點:
① 默認初始化時候
Arraylist 默認 不會對我們數組做初始化
(第一次調用add方法的時候 纔會初始化)
Vector 默認初始化的大小爲10
② 擴容區別
ArrayList擴容:在原來數組的基礎之上增加50%,即新容量是舊容量的1.5倍
Vector擴容:在原來數組基礎之上再增加100%,即新容量是舊容量的2倍(如果沒自定義容量capacityIncrement),所以新容量newCapacity = oldCapacity + oldCapacity;
那麼如何自定義capacityIncrement,直接調用如下Vector的方法:
③ 線程是否安全(主要看增刪,查的時候不存在線程安全問題)
ArrayList 默認的情況下 線程不安全的,因爲沒加線程同步,沒加鎖。
Vector 線程是安全的 效率是非常低的 查詢 增加 、刪除都加上了鎖。
2. CopyOnWriteArrayList和Vector區別:
Vector:讀,寫,查都加上了synchronized同步鎖(不建議使用)
CopyOnWriteArrayList:寫,刪的時候加了Lock鎖,但讀的時候沒加鎖;CopyOnWriteArrayList支持讀多寫少的併發情況