前言
List是一种有序、可以重复的数据结构,底层实现是动态数组或者链表;常用的List结构有:ArrayList、LinkedList、CopyOnWriteList还有古老的Vector和Stack。
先带着如下几个问题去学习ArrayList的源码:
1、ArrayList是怎么扩容的?
2、ArrayList的插入、删除、查询的时间复杂度?
3、怎么求两个集合的交集、并集、差集?
4、ArrayList怎么实现序列化和反序列化?
5、集合的toArray方法有什么问题?
ArrayList属性
// 默认容量 private static final int DEFAULT_CAPACITY = 10; // 空数组, 创建list时容量为0或者初始化集合为空时使用 private static final Object[] EMPTY_ELEMENTDATA = {}; // 空数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 数组元素 transient Object[] elementData; // non-private to simplify nested class access // 当前数组中有几个元素, 区别容量 private int size;
构造方法
ArrayList提供了如下三个构造方法:
1、指定初始容量
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
注意一点:可以指定初始容量为0,此时数组元素为EMPTY_ELEMENTDATA
2、无参构造器
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
初始容量为0,第一次添加元素时,指定容量,默认是10
3、入参为集合类型构造器
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
流程为:先将输入集合转换成数组,然后赋值;这里为什么要判断elementData是不是Object类呢、或者说什么时候elementData不是Object类?
答案是在重写了ArrayList的toArray方法的类可能出现上述场景
添加单个元素方法
ArrayList提供了如下两个添加元素的方法:
1、添加单个元素到数组的最后(时间复杂度为O(1))【返回boolean类型】
public boolean add(E e) { // 先做容量检查, 容量不够, 扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // 序列化时使用 // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return 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); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
流程为:计算所需的最小容量,容量不够,做扩容处理,最后添加元素到末尾;这里重点说下扩容;
扩容机制为:先扩容1.5倍,如果还是不够分配需要的容量;如果大于Integer最大数值-8,则按照超大数组流程处理,防止整数溢出
2、添加单个元素到指定索引(涉及元素拷贝,时间复杂度为O(n))【无返回值】
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! 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)); }
流程:1、验证索引,超上界or低于下界,抛IndexOutOfBoundsException异常(注意和ArrayIndexOutOfBoundsException的区别,ArrayIndexOutOfBoundsException为数组越界异常,IndexOutOfBoundsException为索引越界异常)
2、扩容处理
3、做元素拷贝:将index开始到结尾的元素拷贝到index+1到结尾
4、元素复制&&元素个数加一
移除元素方法
1、移除指定索引的元素(时间复杂度为O(n))【返回值为被删除的元素】
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; // clear to let GC do its work return oldValue; }
2、移除指定元素(时间复杂度为O(n))【返回值为删除结果boolean类型】
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 remove method that skips bounds checking and does not * return the value removed. */ 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 }
这里注意的是:按需重写list对象的equals方法,否则默认的equals方法比较的是两个对象的地址
获取&修改元素方法
1、获取指定索引的元素(时间复杂度为O(1))
public E get(int index) { rangeCheck(index); return elementData(index); }
2、修改指定索引的元素为指定值(时间复杂度为O(1))【返回值为被修改的元素】
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
求两个集合的并集
实际上就是在指定list中批量添加元素,提供了如下两个方法:
1、在末尾批量添加元素(时间复杂度为O(n))【返回值为新添加元素个数是否为0(boolean)】
public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); // 转换为数组 int numNew = a.length; ensureCapacityInternal(size + numNew); // 扩容处理 System.arraycopy(a, 0, elementData, size, numNew); // 元素拷贝到原数组末尾 size += numNew; return numNew != 0; }
2、在指定索引批量添加元素(时间复杂度为O(n))【返回值为新添加元素个数是否为0(boolean)】
public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); // 上界&下界校验 Object[] a = c.toArray(); // 转换为数组 int numNew = a.length; ensureCapacityInternal(size + numNew); // 扩容处理 int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); // 先将原数组从index开始, 预留位置 System.arraycopy(a, 0, elementData, index, numNew); // 将元素批量插入到index位置到index+numNew size += numNew; return numNew != 0; }
求两个集合的交集(返回值为boolean类型, 表示原数组是否有改变)
public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, true); } private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; // 读写指针, 写指针之前的元素是要保留的; 这样在原数组上操作, 节省空间 int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // 理论上是r一定等于size的, 除非contains抛异常 if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }
求两个集合的差集(返回值为boolean类型, 表示原数组是否有改变)
public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false); }
ArrayList序列化
从前面的属性中可以看到ArrayList的elementData由transient修饰的,是不参与序列化的,那么ArrayList怎么序列化呢?答案是ArrayList的writeObject方法和readObject方法
ArrayList通过这两个方法自己控制序列化
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // 记录list修改次数, 防止序列化期间list变化 int expectedModCount = modCount; s.defaultWriteObject(); // 写入非静态 & 非transient属性 s.writeInt(size); // 写入数据个数 // 写入数组所有元素 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } // 序列化期间list变化, 抛出异常 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // 数组初始化 // 去取非静态 & 非transient属性 & size s.defaultReadObject(); // 读取数据个数, 没有卵用, 只是为了按照序列化步骤反序列化 s.readInt(); // ignored if (size > 0) { // 计算容量 int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); // 扩容 Object[] a = elementData; // 依次读取数据 for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
为什么要这样序列化呢?我的理解,ArrayList存在容量远远大于size的情况(比如创建list时指定超大初始容量),按需序列化会大大减小内存占用
前面问题的答案
1、ArrayList是怎么扩容的?
先扩容1.5倍,如果容量还是不够,按照需要的容量扩容 --> 如果是超大容量(Integer最大值-8),检查是否可能整数溢出
2、ArrayList的插入、删除、查询的时间复杂度?
插入:插入尾部O(1),插入到指定索引O(n)
删除:指定index和指定值都为O(n),涉及数组拷贝
查询:O(1)
3、怎么求两个集合的交集、并集、差集?
交集:addAll(collection)、addAll(index, collection)
并集: remainAll()
差集: removeAll()
4、ArrayList怎么实现序列化和反序列化?
自己实现私有的writeObject、readObject方法,按照实际元素个数序列化
5、集合的toArray方法有什么问题?