成员变量
- private static final int DEFAULT_CAPACITY
默认初始容量 10 - private static final Object[] EMPTY_ELEMENTDATA
所有实例共享的空数组 - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA
调用无参构造器时共享的赋值对象,虽然都是空数组,但与EMPTY_ELEMENTDATA
不同,详见无参构造器 - private int size
数组长度。与容量不同,表示数组中元素个数 - transient Object[] elementData
真正的存储结构
构造方法
-
int initialCapacity
// 传入数组的容量,与size不同,表示数组最大存储元素个数 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 这里指向公用空数组 this.elementData = EMPTY_ELEMENTDATA; } else { //传入容量小于0抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
-
无参
public ArrayList() { // 调用无参构造并不按默认容量DEFAULT_CAPACITY初始化 // 而是先指向公用对象DEFAULTCAPACITY_EMPTY_ELEMENTDATA // 在add中为elementData申请内存 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
-
Collection<? extends E>
// 传入其他集合类 public ArrayList(Collection<? extends E> c) { /* * 这里elementData初始化调用传入参数的toArray方法 * 此方法存在于Collection接口中,各类实现不同 * (但应该都是深拷贝,等我看完了Collection回来填坑) */ elementData = c.toArray(); if ((size = elementData.length) != 0) { // 验证c的toArray返回类型是否为Object // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 如果传入为其他类型的空数组 // 仍指向EMPTY_ELEMENTDATA this.elementData = EMPTY_ELEMENTDATA; } }
常用方法
-
扩容
private Object[] grow(int minCapacity) { // 将原有数组按新的容量复制 return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); } // 计算扩容容量 private int newCapacity(int minCapacity) { int oldCapacity = elementData.length; // 新的容量为旧容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity <= 0) { /* * 如果是无参构造,在此时确定数组的容量 * 取传入值和默认值10的最大值作为容量 * (好像一般都是10) */ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) return Math.max(DEFAULT_CAPACITY, minCapacity); if (minCapacity < 0) // 容量溢出,抛出异常(不是传负值) throw new OutOfMemoryError(); return minCapacity; } // 如果超过了最大容量Integer.MAX_VALUE - 8,调整为最大容量 return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity : hugeCapacity(minCapacity); }
此外还有个问题,为啥是扩容到原来的1.5倍呢?因为一次性扩容太大(例如2.5倍)可能会浪费更多的内存(1.5倍最多浪费33%,而2.5被最多会浪费60%,3.5倍则会浪费71%……)。但是一次性扩容太小,需要多次对数组重新分配内存,对性能消耗比较严重。所以1.5倍刚刚好,既能满足性能需求,也不会造成很大的内存消耗。
-
删除元素
ArrayList中对remove有两个重载,分别是
a.public E remove(int index)
删除指定位置的元素,并返回删除元素的值。实现如下public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; }
其中的删除操作依靠内部的私有函数
fastRemove
实现,具体如下private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; }
根据删除位置的不同,又分为两种情况:
当待删除元素为数组末尾时,直接将末尾位置置为null
当待删除元素在其他位置时,将该元素后续向前复制,再将末尾置nullb.
public boolean remove(Object o)
删除数组中第一次出现的o元素public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; /* * label语法,SE7之后加入的新特性 * 类似于goto指针(不全等,java里没有goto,goto有害!) * break label 跳出语句块,常用于跳出多重循环 * 参考链接:https://stackoverflow.com/questions/28381212/how-to-use-labels-in-java-code */ found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; }
-
添加
最终调用私有方法private void add(E e, Object[] elementData, int s) { // 判断是否扩容,扩容原理见上 if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }
注意添加的是浅拷贝(引用),而非深拷贝
public static void main(String[] args) { List<List<Integer>> arr = new ArrayList<>(5); ArrayList<Integer> tmp = new ArrayList(5); for (int i = 0; i < 5; i++) { tmp.add(i); arr.add(tmp); } show(arr); /* 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 */ System.out.println(); for (int i = 0; i < 5; i++) { tmp.set(i,i+5); } show(arr); /* 5 6 7 8 9 5 6 7 8 9 5 6 7 8 9 5 6 7 8 9 5 6 7 8 9 */ } // 打印函数 private static void show(List<? extends List> arr) { int len = arr.size(); for (int i = 0; i < len; i++) { for(var ele : arr.get(0)){ System.out.printf("%d ",ele); } System.out.println(); } }
此外ArrayList不检测添加元素是否为null,允许其加入
-
删除数组
public void clear() { /* 对数组的结构性改动(添加/删除)都会影响modCount * 上面的源码之所以没有显示,是因为我没把完整顺序截全 * modCount用于iterator的fast-fail机制 */ modCount++; final Object[] es = elementData; /* * 删除整个数组并不是简单的把elementData重新指向EMPTY_ELEMENTDATA * 而是将原有数组元素置为null,以便GC回收 */ for (int to = size, i = size = 0; i < to; i++) es[i] = null; }
-
hashCode and equals
public int hashCode() { int expectedModCount = modCount; // 计算[0,size)范围的数组hash值 int hash = hashCodeRange(0, size); // fast-fail检测线程安全 checkForComodification(expectedModCount); return hash; } int hashCodeRange(int from, int to) { final Object[] es = elementData; if (to > es.length) { throw new ConcurrentModificationException(); } /* * 以下是hashcode计算过程 * 将数组中的每一个数加权相乘的出hash值 */ int hashCode = 1; for (int i = from; i < to; i++) { Object e = es[i]; /* * 若为null,该位置不对hash产生贡献 * 注意:按位加权,所以 * null 1 null null * 和 * 1 null null null * 的hash值不同 */ hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode()); } return hashCode; } public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof List)) { return false; } final int expectedModCount = modCount; // ArrayList can be subclassed and given arbitrary behavior, but we can // still deal with the common case where o is ArrayList precisely // 上面说的挺好 boolean equal = (o.getClass() == ArrayList.class) ? equalsArrayList((ArrayList<?>) o) : equalsRange((List<?>) o, 0, size); checkForComodification(expectedModCount); return equal; }
线程安全
显然ArrayList并不是线程安全的,以add为例
public static void main(String[] args) {
ArrayList<Integer> store = new ArrayList<>();
int len = 10;
Thread[] t = new Thread[len];
for (int i = 0; i < len; i++) {
t[i] = new Thread(new Runnable() {
@Override
public void run() {
Random rand = new Random();
for (int j = 0; j < 500; j++) {
store.add(rand.nextInt()%100);
}
}
});
t[i].start();
}
// 要等所有线程都执行完再去查数组长度
// 否则得到的不是线程最终操作完的结果
for (int i = 0; i < len; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(store.size());
}
最后结果居然才3843,堪称在线吃数。在多线程下,ArrayList不能保证线程安全,一在于多线程同时扩容造成丢失,二在于多线程同时添加造成覆盖,所以每一个线程操控后的store可能是完全不同的。我们重写run看一下store的hashcode
public void run() {
Random rand = new Random();
for (int j = 0; j < 500; j++) {
store.add(rand.nextInt()%100);
}
System.out.println(Thread.currentThread().getName()+" "+store.hashCode());
}
执行结果如下
Exception in thread "Thread-6" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList.checkForComodification(ArrayList.java:604)
at java.base/java.util.ArrayList.hashCode(ArrayList.java:614)
at com.java.learning.language$1.run(language.java:20)
at java.base/java.lang.Thread.run(Thread.java:835)
Thread-2 709127055
Thread-0 -746687074
Thread-3 2143175265
Thread-9 1059229658
Thread-7 -1392478734
Thread-8 -612096406
Thread-5 -941813659
Thread-1 -334317182
Thread-4 450111088
可以看到每个线程操作后store五花八门,甚至还有个fast-fail抛了个ConcurrentModificationException。
补充:除了上次两个问题以外,还可能抛出ArrayIndexOutOfBoundsException,由于扩容不同步,当一个线程扩容,另一个线程写入时可能存在的情况
删除道理相似,这里不再演示了
那么如何保证ArrayList的线程安全呢?
请大声喊出那个线程安全容器的名字:Vector !
使用java.util.Collections.SynchronizedList
它能把所有 List 接口的实现类转换成线程安全的List,比 Vector 有更好的扩展性和兼容性实际上粗暴的一匹
/*
* SynchronizedList是Collections里一内部类
* 这货怎么保证线程安全呢?
* 很简单,增删改查全sync
* 那这样行么?行,怎么不行每次就一个线程能操作还能不行
* 但行归行,这不是最优的
* 如果好几个线程同时读取某个数,这需要加锁控制同步么?
* No,不用,仅读取不会造成任何线程安全问题,加锁反而降低吞吐量
*/
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
如果针对读多写少的场景下,可以采用CopyOnWriteArrayList
允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。
如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。
这里重点介绍写方法,因为读没啥好说的。。。
/*
* CopyOnWriteArrayList可以体现读写分离的思想:读和写分别在不同容器
* 写时需要同步控制,避免并发扩容
* 而读不需要同步控制,因为读取的容器永远不会添加
* (因为都是添加完了才给你看的,没添加完你也看不了)
*/
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
/*
* CopyOnWriteArrayList没有特殊的扩容机制
* 来一个扩一次,然后内部存储结构引用指向新的内存
*/
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
但是这里需要我们思考以下,不加同步的读是否真的安全,SynchronizedList
虽然慢了点,但是绝对的线程安全,CopyOnWriteArrayList
真的能实现线程安全么?废话肯定能,不能留着干嘛
虽然多线程切换具有随意性,但是仍可以分为如下三种情况
- 写发生在读之前
这感觉就像句废话,因为没啥用。。。 - 写读并发
由于读并没有同步控制,所以现在这两个操作是同时执行的
考虑最坏的情况,在读到一半换人写了,回来读的时候发现容器换了。如果不是读取新加入的元素,那么之前元素并没有改变,依然正确。
而且是先完成新数组构建,再修改引用,因此也不用担心数组长度大于实际值 从而导致ArrayIndexOutOfBoundsException的问题 - 写在读之后
同1
删除
/*
* COW发生结构性变化时,都会加整体lock
* 仅允许一个线程对其改动
* 同样也是改动到新数组,然后指向新数组
*/
public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
参考资料
1. 面试官问线程安全的List,看完再也不怕了! - Java技术栈的文章 - 知乎
2. 翻看 Java10 里面的 ArrayList 源码,remove 方法里面有个 found: {} 这是什么意思呢?
3. 第六章 Java数据结构和算法 之 容器类(一)- 李一恩的文章 - CSDN
4. Top 40 Java collection interview questions and answers