一、ArrayList继承关系
- Serializable接口:支持序列化
- Cloneable接口:支持克隆
- RandomAccess接口:支持快速访问,即可以通过索引下标快速的移动到对应的元素
二、ArrayList源码分析
1、成员变量
// 序列化id
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 集合真正存储数组元素的数组
transient Object[] elementData;
// 集合的大小
private int size;
2、构造方法
ArrayList():构造一个初始容量为十的空列表。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList(int initialCapacity): 构造具有指定初始容量的空列表。
public ArrayList(int initialCapacity) {
//如果initialCapacity大于0
if (initialCapacity > 0) {
//创建一个数组,且指定长度为initialCapacity
this.elementData = new Object[initialCapacity];
} else if (initialCapacityr == 0) {
//如果initialCapacity容量为0,把EMPTY_ELEMENTDATA的地址赋值给elementData
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
ArrayList(Collection<? extends E> c) :构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
public ArrayList(Collection<? extends E> c) {
// 将集合构造中的集合对象转成数组,且将数组的地址赋值给elementData
elementData = c.toArray();
// 将elementData的长度赋值给集合长度size,且判断是否不等于0
if ((size = elementData.length) != 0) {
// c.toArray 可能不会返回 Object[]
if (elementData.getClass() != Object[].class)
//如果不是Object[].class类型,那么就需要改造一下。
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
/**
* Arrays类中的copyOf方法进行数组的拷贝
* @param original 原始的数组
* @param newLength 新的容量
*/
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
/*
重点:(Object)newType == (Object)Object[].class
1.如果newType是Object[]类型的,那么直接使要返回的copy数组为new出的
Object[newLength]
2.如果不等,而Java中我们是无法直接new出T[](泛型数组)的,因此通过反射去获取。
数组特有的getComponentType()方法,可得到数组元素的类型。
*/
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//拷贝数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
/**
* @param src 源对象
* @param srcPos 源数组中的起始位置
* @param dest 目标数组对象
* @param destPos 目标数据中的起始位置
* @param length 要拷贝的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
3、 添加方法
add(E e): 将指定的元素追加到此列表的末尾。
public boolean add(E e) {
//调用方法对内部容量进行校验
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//判断集合存数据的数组是否等于空容量的数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//通过最小容量和默认容量求出较大值(用于第一次扩容)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//将if中计算出来的容量传递给下一个方法,继续校验
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//实际修改集合次数++ (在扩容的过程中没用,主要是用于迭代器中)
modCount++;
//判断最小容量 - 数组长度是否大于0
if (minCapacity - elementData.length > 0)
//将第一次计算出来的容量传递给核心扩容方法
grow(minCapacity);
}
private void grow(int minCapacity) {
//记录数组的实际长度
int oldCapacity = elementData.length;
//核心扩容算法:原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断新容量 - 最小容量是否小于0
if (newCapacity - minCapacity < 0)
//还是将最小容量赋值给新容量
newCapacity = minCapacity;
//判断新容量 - 最大数组大小是否大于0,如果条件满足就计算出一个超大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用数组工具类方法,创建一个新数组,将新数组的地址赋值给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
add(int index, E element):在此列表中的指定位置插入指定的元素。
public void add(int index, E element) {
//添加范围检查
rangeCheckForAdd(index);
//调用方法检验是否要扩容
ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
//超出指定范围就报错
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
addAll(Collection<? extends E> c):按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
public boolean addAll(Collection<? extends E> c) {
//把集合的元素转存到Object类型的数组中
Object[] a = c.toArray();
int numNew = a.length;
//调用方法检验是否要扩容
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
addAll(int index, Collection<? extends E> c):将指定集合中的所有元素插入到此列表中,从指定的位置开始。
public boolean addAll(int index, Collection<? extends E> c) {
//校验索引
rangeCheckForAdd(index);
Object[] a = c.toArray();
//数组c长度
int numNew = a.length;
//调用方法检验是否要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//移动元素的个数
int numMoved = size - index;
if (numMoved > 0)
//移动elementData的索引index后的数据到 index + numNew 位置
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//移动数组c中的数据到elementData的index位置
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
4、删除方法
remove(int index):根据索引删除元素
public E remove(int index) {
//范围校验
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
//计算集合需要移动元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//尽早让垃圾回收机制对其进行回收
elementData[--size] = null;
return oldValue;
}
remove(Object o):根据元素删除元素
public boolean remove(Object o) {
//判断要删除的元素是否为null
if (o == null) {
for (int index = 0; index < size; index++)
//判断集合的元素是否为null
if (elementData[index] == null) {
//如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
//用o对象的equals方法和集合每一个元素进行比较
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;
}
5、修改方法
set(int index, E element):根据索引修改集合元素
public E set(int index, E element) {
//范围校验
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
6、获取方法
get(int index):根据索引获取元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
7、转换方法
toString():把集合所有数据转换成字符串
//AbstractCollection类的方法
public String toString() {
//获取迭代器
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
//创建StringBuilder,对集合的内容进行拼接,避免字符串频繁拼接产生很多无效对象
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
//调用ArrayList中next方法取出元素
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
8、迭代器
//ArrayList集合内部类
private class Itr implements Iterator<E> {
//表示下一个元素的索引位置
int cursor;
//表示上一个元素的索引位置
int lastRet = -1;
//将实际修改集合次数赋值给预期修改次数,注意只会赋值一次
//以后在迭代器获取元素的时候,每次都会判断集合实际修改次数是否和预期修改次数一致
//如果不一致就会产生并发修改异常
int expectedModCount = modCount;
//判断下一个元素的索引位置和集合的大小是否不相等
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
//获取元素的方法
public E next() {
//每次获取元素,会先调用该方法校验预期修改次数是否 == 实际修改次数
checkForComodification();
int i = cursor;
//判断是否有元素
if (i >= size)
throw new NoSuchElementException();
//将集合底层存储数据的数组赋值给迭代器的局部变量 elementData
Object[] elementData = ArrayList.this.elementData;
/*
再次判断,如果下一个元素的索引大于集合底层存储元素的长度,并发修改异常
是否多余?上面已经判断了 i >= size,不多余
可能调用trimToSize()缩容方法
*/
if (i >= elementData.length)
throw new ConcurrentModificationException();
//每次成功获取到元素,下一个元素的索引都是当前索引+1
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
//判断最后返回元素的索引是否小于0,满足条件就产生非法状态异常
//比如连着调用两次remove方法
if (lastRet < 0)
throw new IllegalStateException();
//校验是否会产生并发修改异常,第一次调用不会,因为与其修改次数和实际修改次数一致
checkForComodification();
try {
//真正删除集合元素的方法,调用方法为ArrayList的remove方法
ArrayList.this.remove(lastRet);
//将lastRet赋值给cursor
cursor = lastRet;
//再次等于-1
lastRet = -1;
//再次将集合实际修改次数赋值给预期修改次数,不管集合自身是否删除成功
//实际修改次数和预期修改次数又一致了,所以并不会产生并发修改异常
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
//如果预期修改次数 和 实际修改次数不相等 就产生并发修改异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
案例:已知集合:List list = new ArrayList();里面有三个元素:"hello"、"Java"、"PHP",使用迭代器遍历集合看有没有"PHP"这个元素,如果有,就使用集合对象删除该元素
//创建集合对象
List<String> list = new ArrayList();
//添加元素
list.add("hello");
list.add("Java");
list.add("PHP");
//获取迭代器
Iterator<String> it = list.iterator();
//遍历集合
while (it.hasNext()) {
String s = it.next();
if(s.equals("PHP")) {
list.remove("PHP");
}
}
结果:checkForComodification()方法抛出异常
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at iterator.Test02.main(Test02.java:23)
建议使用迭代器的remove方法而不是集合的remove方法
结论:
- 迭代器remove方法底层调用的还是集合自身的remove方法删除元素
- 之所以不会产生并发修改异常,其原因是因为在迭代器的remove方法中会再次将 集合时机修改次数赋值给预期修改次数
9、清空方法
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
10、包含方法
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
//如果元素是null,也进行遍历操作,因为集合中有可能够会存储null
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
//如果没有走if,也没有走else,那么就说明o该元素在集合中不存在
return -1;
}
11、判断集合是否为空
public boolean isEmpty() {
return size == 0;
}
三、面试题
1、ArrayList是如何扩容的?
第一次扩容10,以后每次都是原容量的1.5倍。
2、 ArrayList频繁扩容导致添加性能急剧下降,如何处理?
创建集合的时候指定足够大的容量,这种优化方式只针对特定的场景,如果添加的元素是少量的、未知的,不推荐使用。
3、ArrayList插入或删除元素一定比LinkedList慢么?
1)LinkedList根据索引删除元素源码
public E remove(int index) {
//调用方法校验元素的索引
checkElementIndex(index);
//先调用node(index)方法,找到需要删除的索引
//再调用unlink方法解开链条
return unlink(node(index));
}
//校验索引是否在合法范围之内,不再就报错
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
Node<E> node(int index) {
//不管索引是多少,在源码底层都会对整个链表上的元素进行折半的动作
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//解开链表,让前后节点相互记录地址
E unlink(Node<E> x) {
//获取要删除的元素
final E element = x.item;
//获取被删除节点下一个节点的地址
final Node<E> next = x.next;
//获取被删除节点上一个节点的地址
final Node<E> prev = x.prev;
//双向链表删除节点的操作
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
//被删除元素的内容置为null
x.item = null;
//集合长度--
size--;
//实际修改次数++
modCount++;
return element;
}
2)LinkedList根据元素删除源码
public boolean remove(Object o) {
//判断要删除的元素是否为null
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
//用o对象的equals方法和集合每一个元素进行比较
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
//如果集合没有o该元素,那么就会返回false
return false;
}
插入源码类似,这里不再进行说明
测试:
public class CollectionTest {
public static long addTime(List<Integer> list) {
long start = System.currentTimeMillis(); // 起始时间
for (int i = 0; i < 50000; ++i) {
list.add(1); // ArrayList和LinkedList的add(element e)方法都是向末尾追加元素
}
long end = System.currentTimeMillis(); // 终止时间
return end - start;
}
public static void main(String[] args) {
System.out.println("ArrayList(add):" + addTime(new ArrayList<>()) + "ms");
System.out.println("LinkedList(add):" + addTime(new LinkedList<>()) + "ms");
}
}
结果:时间消耗差不多,因为时间复杂度都为O(1)
ArrayList(get):3ms
LinkedList(get):2ms
将上述例子中的add方法改为向指定位置添加元素,并事先在list里面添加50000
个元素:
public class CollectionTest {
public static long addTime(List<Integer> list) {
// 先让list里面有点东西,不然下面的add(i,1)就会变成在末尾添加元素,看不到效果
for (int i = 0; i < 50000; ++i) {
list.add(1);
}
long start = System.currentTimeMillis(); // 起始时间
for (int i = 0; i < 50000; ++i) {
list.add(i, 1);
}
long end = System.currentTimeMillis(); // 终止时间
return end - start;
}
public static void main(String[] args) {
System.out.println("ArrayList(get):" + addTime(new ArrayList<>()) + "ms");
System.out.println("LinkedList(get):" + addTime(new LinkedList<>()) + "ms");
}
}
结果:
ArrayList(get):531ms
LinkedList(get):1987ms
观察源码发现,LinkedList寻找指定节点的方式为,靠近末尾结点就从末尾开始找,靠近开头就从开头开始找,这样就可以只遍历一半结点,也就是位置为list的中间时候将会是最费时的。
总结:
首部插入,LinkedList更快;中间和尾部插入,ArrayList更快;在集合里面删除元素类似,首部删除,LinkedList更快;中间删除和尾部删除,ArrayList更快;
由此建议,数据量不大的集合,主要进行插入、删除操作,建议使用LinkedList;数据量大的集合,使用ArrayList就可以了,不仅查询速度快,并且插入和删除效率也相对较高
4、ArrayList是线程安全的么?
ArrayList不是线程安全的
需要线程安全怎么办?
- 方式一:使用Collections.synchronizedList(list)
- 方式二:创建线程安全的集合类Vector
5、如何复制某个ArrayList到另一个ArrayList中去?
- 使用clone()方法
- 使用ArrayList构造方法
- 使用addAll方法
6、已知成员变量集合存储N多用户名称,在多线程的环境下,使用迭代器在读取集合数据的同时如何保证还可以正常的写入数据到集合?
//线程任务类
class CollectionThread implements Runnable{
private static ArrayList<String> list = new ArrayList<String>();
static{
list.add("Jack");
list.add("Lucy");
list.add("Jimmy");
}
@Override
public void run() {
for (String value : list) {
System.out.println(value);
//在读取数据的同时又向集合写入数据
list.add("coco");
}
}
}
//测试类
public class ReadAndWriteTest {
public static void main(String[] args) {
//创建线程任务
CollectionThread ct = new CollectionThread();
//开启10条线程
for (int i = 0; i < 10; i++) {
new Thread(ct).start();
}
}
}
结果:抛出异常
Exception in thread "Thread-8" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at CollectionThread.run(ReadAndWriteTest.java:18)
at java.lang.Thread.run(Thread.java:748)
读写分离集合
//线程任务类
class CollectionThread implements Runnable{
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
static{
list.add("Jack");
list.add("Lucy");
list.add("Jimmy");
}
@Override
public void run() {
for (String value : list) {
System.out.println(value);
//在读取数据的同时又向集合写入数据
list.add("coco");
}
}
}
//测试类
public class ReadAndWriteTest {
public static void main(String[] args) {
//创建线程任务
CollectionThread ct = new CollectionThread();
//开启10条线程
for (int i = 0; i < 10; i++) {
new Thread(ct).start();
}
}
}
结果:正常运行
7、ArrayList 和 LinkList区别?
ArrayList
- 基于动态数组的数据结构
- 对于随机访问的get和set,ArrayList要优于LinkedList
- 对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此并不是每次add和remove的时候都需要创建新数组)
LinkedList
- 基于链表的数据结构
- 对于顺序操作,LinkedList不一定比ArrayList慢
- 对于随机操作,LinkedList效率明显较低
8、如何高效的遍历List集合
使用普通for遍历好还是使用迭代器(增强for), 特别是数据量特别大的时候一定要考虑!
对返回的集合进行判断,如果返回的集合实现了 RandomAccess 就使用普通for,否则使用迭代器(增强for)。
if(list instanceof RandomAccess){
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}else {
for (Stutb stutb : list) {
System.out.println(stutb);
}
}