一、实现原理
ArrayList底层是通过数组实现的,元素是存在其成员变量的transient Object[] elementData;里面的。在增加元素的时候会自动为我们扩容。也可以称之为动态数组。
常用构造方法:
public ArrayList() 构造方法为我们创建了一个长度为0的空数组。最长用的构造方法。
public ArrayList(int initialCapacity) 为我们创建一个长度为initialCapacity的数组。其实我们在使用过程中如果知道列表的规模的话,最好使用此构造方法。尽量降低列表扩容的次数。
二、新增元素
最常用的方法 add(E e)
//新增元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//检查容量大小
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//计算需要当前所需容量是否大于默认容量 10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return 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);
}
- 首先检查当前数组是否为默认长度为0的空数组,计算本次操作所需容量
- 比较本次操作所需容量是否大于数组长度。如果大于的话 则需要对数组扩容。
- 计算扩容后的数组长度 等于原来数组长度的1.5倍。
- 将原始的数组 拷贝到新建的数组里面。
- 列表当前的长度加1,并且将本次添加的元素 放到数组的最后面。
三、删除元素 remove(Object o)
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;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
- 判断当前删除的元素是否为null。因为集合类判断元素是否存在,或者删除元素 都是通过equals来判断的。如果为null的话 则使用==判断。
- 找到删除元素所在的下标。
- 将数组元素 往前移位。
- 将列表的长度建减1
四、列表遍历时删除元素:
package com.winston.javase.testcollections;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestIterator {
public static void main (String[] args) {
testRemove1();
System.out.println();
testRemove2();
}
private static void testRemove1 () {
List<String> lists = new ArrayList<String>();
lists.add("data1");
lists.add("data2");
lists.add("data3");
lists.add("data4");
System.out.println(lists);
Iterator<String> it = lists.iterator();
while (it.hasNext()) {
String tmp = it.next();
if ("data3".equals(tmp)) {
lists.remove(tmp);
}
}
System.out.println(lists);
}
private static void testRemove2 () {
List<String> lists = new ArrayList<String>();
lists.add("data1");
lists.add("data2");
lists.add("data3");
lists.add("data4");
System.out.println(lists);
Iterator<String> it = lists.iterator();
while (it.hasNext()) {
String tmp = it.next();
if ("data2".equals(tmp)) {
lists.remove(tmp);
}
}
System.out.println(lists);
}
}
输出结果:
[data1, data2, data3, data4]
[data1, data2, data4]
[data1, data2, data3, data4]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.winston.javase.testcollections.TestIterator.testRemove2(TestIterator.java:48)
at com.winston.javase.testcollections.TestIterator.main(TestIterator.java:13)
我们都知道使用Iterator 遍历ArrayList的时候不能删除、修改元素。
testRemove2也按照我们预期的抛出了异常java.util.ConcurrentModificationException。但是为什么,testRemove1()就正常删除了呢。
查看源码:
//以下仅仅是Itr 的部分代码
private class Itr implements Iterator<E> {
//记录下一个元素的下标 从0开始
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
//记录ArrayList被修改的次数。
int expectedModCount = modCount;
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public boolean hasNext() {
return cursor != size;
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
根据源码我们可以看到Itr的成员变量expectedModCount被赋值为ArrayList的modCount(该值为记录ArrayList的变更次数。可以从add()remove()等方法中看到ArrayList的每次变更modCount都会自增1),所以当我们遍历过程中 删除元素的话 会导致modCount 加1。即在调用next()方法的时候会调用checkForComodification判断modCount != expectedModCount,所以会抛出异常。
在来看testRemove1()方法不抛出异常的原因。首先看到hasNext()方法。cursor表示即将访问元素的下标。size表示ArrayList的当前长度。因为我们删除的元素是倒数第二个元素。遍历data3后 cursor的值变为 3。删除data3后 ArrayList的长度也变为了 3 。导致调用hasNext()的方法的时候 认为该列表已经没有下一个元素了。所以不会再调用后续的next方法。即不抛出异常。(这样会导致最后一个元素data4未进行判断)
那么我们就没有办法在遍历的时候。判处不需要的元素了吗?肯定是有办法的。
方式1、新建一个列表 在遍历时将符合条件的元素添加到新的列表里。
方式2、使用Iterator提供的remove()方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
我们可以看到该方法也是直接调用的ArrayList的remove方法。但是操作完成后 重新赋值了 expectedModCount = modCount;。所以不会抛出异常。
方式3、在jdk8版本开始支持lambda。使用lists.removeIf(str ->"data2".equals(str));删除。
五、ArrayList和LinkedList比较
LinkedList是基于双向链表结构实现的。其特点就是维护了前后元素的引用。使得添加元素和删除元素的时候比ArrayList快。但是查询指定元素的时候 就相对较慢。
package com.winston.javase.testcollections;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* java.util.List 是有序集合也成为 列表 可以重复。
* 它是Collection的子接口 主要实现类
* java.util.ArrayList
* java.util.LinkedList
* java.util.Vector
* @Author Winston
* @Version 1.0 2018年9月17日 下午4:11:54
*/
public class TestList {
public static void main (String[] args) {
for (int i = 0; i < 20; i++) {
compareArrayListLinkedList();
}
}
private static void compareArrayListLinkedList () {
List<String> list = new ArrayList<String>();
List<String> linkedList = new LinkedList<String>();
linkedList.add("西");
list.add("西");
long s1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
//list.add("西");
linkedList.add(1, "西");
}
long s2 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
//linkedList.add("西");
list.add(1, "西");
}
long s3 = System.currentTimeMillis();
System.out.println("linkedList:" + (s2 - s1) + "||" + "ArrayList:" + (s3 - s2));
}
}
输出结果:
linkedList:4||ArrayList:825
linkedList:3||ArrayList:825
linkedList:1||ArrayList:822
linkedList:2||ArrayList:822
linkedList:1||ArrayList:828
linkedList:1||ArrayList:820
linkedList:1||ArrayList:823
linkedList:1||ArrayList:835
linkedList:1||ArrayList:832
linkedList:1||ArrayList:847
linkedList:1||ArrayList:880
linkedList:1||ArrayList:825
linkedList:1||ArrayList:823
linkedList:1||ArrayList:835
linkedList:1||ArrayList:828
linkedList:1||ArrayList:842
linkedList:2||ArrayList:838
linkedList:1||ArrayList:828
linkedList:1||ArrayList:828
linkedList:1||ArrayList:830
从结果中可以看出 LinkedList 在列表第一个元素的位置添加的时候 比ArrayList明显要快很多。因为ArrayList每次会移动列表里面的所有元素。但是在列表最后追加元素的话 这个差距不会这么明显。
六、线程不安全测试:
package com.winston.javase.testcollections;
import java.util.ArrayList;
import java.util.List;
/**
* @Author Winston
* @Version 1.0 2018年9月17日 下午4:11:54
*/
public class TestList {
public static void main (String[] args) {
testThreadSafe();
}
static void testThreadSafe () {
List<String> list1 = new ArrayList<String>();
Thread t1 = new Thread(new Runnable() {
@Override
public void run () {
for (int i = 0; i < 10; i++) {
list1.add("A" + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run () {
for (int i = 0; i < 10; i++) {
list1.add("B" + i);
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list1);
System.out.println(list1.size());
}
结果一
[null, B0, B1, B2, B3, A1, B4, A2, A3, A4, A5, A6, null, B5, A8,null, A9, B7, B8, B9]
19
其中出现为空的原因 在于新增的时候 elementData[size++] = e; 分为两步
elementData[size] = e; ①
size++; ②
当线程A执行 了步骤①还未来得及执行步骤②的时候 此时size为n elementData[n] = A(n) 则
cpu切换到线程B执行。由于此时size还没加1 导致线程B执行的时候 elementData[n] = B(n).
然后线程B 执行size++ 此时 size变为 n+1并且 elementData[n+1]=null。最后CPU再切换回A线程
执行第②步。导致 size变为 n+2 。后面在追加进来的元素 执行步骤①的时候
就将新元素放到了n+2的位置上。所以n+1的位置就永远为null了
结果二
Exception in thread "Thread-0"
java.lang.ArrayIndexOutOfBoundsException: 10 at
java.util.ArrayList.add(ArrayList.java:463) at
com.winston.javase.testcollections.TestList$1.run(TestList.java:83)
at java.lang.Thread.run(Thread.java:748)
[A0, A1, B0, A2, A3, A4, A5,B2, A6, B3, null, B4, B5, B6, B7, B8, B9] 17
if (minCapacity - elementData.length > 0)
如果当前 列表容量为10个 数组目前里面包含 9个元素 下标是 0至8 当A线程和B线程同时加入新元素
当线程A 执行完以上代码 此时10 - 10 刚好等于0 当数组容量刚好达到临界值的时候 不会进行扩容。此时还未将新元素放到数组CPU切换 到线程B,线程B 在以上代码 也刚好达到临界值 不会扩容 然后线程B继续执行 elementData[size++] = e;
此时B线程元素刚好放到数组里面下标为9的位置 并且size变为10
再切换回线程A。此时线程A直接执行elementData[size++] = e 因为此时size 为10 elementData[10]
引发数组下标越界异常