听说你还没搞懂ArrayList底层源码?
1、概述ArrayList
ArrayList底层数据结构是一个Oject类型的动态数组,可以自动扩容;线程不安全
ArrayList继承于 AbstractList ,实现了 List(规定了List的操作规范), RandomAccess(可随机访问), Cloneable(可拷贝), java.io.Serializable(可序列化) 这些接口。
- RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
- ArrayList 实现了 Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
- ArrayList 实现了 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
Arraylist 和 Vector 的区别?
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用Object[ ]
存储,线程安全的
Arraylist 与 LinkedList 区别?
- 线程安全性: ArrayList 和 LinkedList 都是不保证线程安全;
- 底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
- 插入和删除操作:
- ArrayList 采用数组存储,所以元素的索引位置直接影响插入和删除元素的时间复杂度。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
- LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
- 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)
2、ArrayList源码分析
2.1、ArrayList主要的成员变量
// ArrayList底层数组结构。用来存放数据,用transient来修饰,说明该字段不会被序列化
transient Object[] elementData;
// 默认容量。当构造方法中没有显式指出ArrayList的数组长度是,使用默认容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组。当构造方法中显式指出ArrayList的数组长度为0时,将该数组赋给elemetData[]
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组。当构造方法中没有显示指出ArrayList的数组长度时,将该数组赋给elemetData[]
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 实际ArrayList中存放元素的个数,默认为0
private int size;
// ArrayList中的对象数组的最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
- java 中的
length
属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. - java 中的
length()
方法是针对字符串说的,如果想看这个字符串的长度则用到length()
这个方法. - java 中的
size()
方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!
2.2、ArrayList的构造方法
- 空参构造器
// 将elementData的值设为空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
public ArrayList() {
// 这里并没有将其数组初始化为容量为10的Object数组!!!
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的 Object[]数组 elementData 。JDK7中的 ArrayList 的对象的创建类似于
单例的饿汉式
,而JDK 8中的 ArrayList 的对象的创建类似于单例的懒汉式
。
- int参数构造器
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 容量参数大于0,对数组进行初始化
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 容量参数为0,将数组设为空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 容量参数小于0,报异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
- Collection参数构造器
public ArrayList(Collection<? extends E> c) {
// 底层利用Arrays.copyOf(elementData, size)创建一个长度和内容相同的新数组
Object[] a = c.toArray();
if ((size = a.length) != 0) {
// 如果a是非空集合
if (c.getClass() == ArrayList.class) {
// 如果可以转换为Object类型数组,直接赋值给elementData
elementData = a;
} else {
// 如果不可以转为Object数组,则进行copy
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.空集合直接赋值为空
elementData = EMPTY_ELEMENTDATA;
}
}
2.3、ArrayList中的主要方法
2.3.1、add()方法
添加元素,添加成功则为true,否则为false。默认添加到末尾
三步走
- 判断ArrayList容量是否足以添加新元素,不够则进行扩容(详细见扩容机制);
- 添加元素到指定位置;
- 集合中的实际元素个数size+1。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2.3.2、扩容机制
// 首先在add()中传参size+1确保添加元素之后最小容量为minCapacity(即添加元素后的实际元素个数)
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断是否为空参所赋值的空数组
// 若用户构造器传入initialCapacity = 0时,不会扩容为10,只会扩容为1
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//若是空数组,则直接返回默认数组容量与minCapacity之间的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 不为空数组时,返回最小容量
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 当最小容量已经超过了elementData的长度时,需要进行扩容
grow(minCapacity);
}
grow(int minCapacity)扩容方法
ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)
private void grow(int minCapacity) {
// overflow-conscious code
// 原数组的容量
int oldCapacity = elementData.length;
// 位运算远远快于整除运算!!!
// 新数组的容量。相当于扩容1.5倍!!!
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 如果扩容后的数组容量仍小于最小容量,则直接将最小容量赋值给新数组容量
newCapacity = minCapacity;
// 比较新数组容量与MAX_ARRAY_SIZE大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果新数组容量大于MAX_ARRAY_SIZE,调用hugeCapacity(minCapacity)方法
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) {
// 容量小于0,抛出异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 比较最小容量与数组最大容量,若大于最大容量,则返回Integer.MAX_VALUE,否则返回最大数组容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
2.3.3、add(index,element)方法
将元素添加到某一位置。相当于插入操作
public void add(int index, E element) {
// 边界检查
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将原始数组进行拷贝扩容,将index开始的数据拷贝到index+1位置
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
2.3.4、indexOf(Object o)方法
查询传入参数的从左往右的第一个索引值并返回,不存在则返回-1
public int indexOf(Object o) {
// 逐个遍历,需要进行equals比较。所以要进行null值判断
if (o == null) {
// 逐个遍历是否为null。找到第一个为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;
}
// 不存在时返回-1
return -1;
}
2.3.5、remove(index)方法
移除某一索引上的元素,返回移除值
public E remove(int index) {
// 边界检查
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 需要移动的元素的个数(移除元素之后的元素需要向前移动)
int numMoved = size - index - 1;
if (numMoved > 0)
// 若大于0,则不是末尾元素,则进行copy到新的数组(相当于将后面元素向前平移一位)
// 此时,末尾元素重复
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将末尾元素赋值为null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
2.3.6、set(index, element)
将某一索引位置的数据进行修改,返回原来值
public E set(int index, E element) {
rangeCheck(index);
// 取出原来值
E oldValue = elementData(index);
// 将该索引下的元素赋予新值
elementData[index] = element;
return oldValue;
}
2.3.7、get(index)
查找某一索引下的元素,并返回
public E get(int index) {
// 检查边界
rangeCheck(index);
// 直接利用索引值对数据进行索引查找,并返回
return elementData(index);
}
2.3.8、ensureCapacity(minCapacity)方法
该方法在ArrayList类内没有主动进行调用,所以是提供给用户使用的。用户可以在add大量元素之前用
ensureCapacity
方法,以减少增量重新分配的次数
public void ensureCapacity(int minCapacity) {
// 如果是true,minExpand的值为0,如果是false,minExpand的值为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
// 如果最小容量大于已有的最大容量
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
final int N = 10000000;
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
list.add(i);
}
long endTime1 = System.currentTimeMillis();
System.out.println("没有使用ensureCapacity方法:"+(endTime1 - startTime1));
list = new ArrayList<>();
startTime1 = System.currentTimeMillis();
list.ensureCapacity(N);
for (int i = 0; i < N; i++) {
list.add(i);
}
endTime1 = System.currentTimeMillis();
System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
}
}
// 没有使用ensureCapacity方法:2617
// 使用ensureCapacity方法后:1020
// 性能提高
2.4、ArrayList中两种copy方法
2.4.1、System.arraycopy() 方法
native方法
/*
src: 源数组
srcPos: 复制源数组的起始位置
dest: 目标数组
destPos: 目标数组的起始下标位置
length: 需要复制的长度
*/
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
2.4.2、Arrays.copyOf()方法
Arrays工具类的方法。底层仍调用了System.arraycopy()方法
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@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;
}
2.4.3、比较两种copy方法
联系:
- Arrays.copyOf()方法底层仍调用了System.arraycopy()方法
区别:
- arraycopy()需要目标数组,可以选择要拷贝的部分
- copyOf()是内部定义了一个数组,并从索引为0的位置开始拷贝,最后返回该拷贝的数组
3、ArrayList的优缺点
3.1、优点
- ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
- ArrayList 在顺序添加一个元素的时候非常方便。
- 根据下标遍历元素,效率高。
- 根据下标访问元素,效率高。
3.2、缺点
- 插入和删除元素的效率不高。
- 根据元素查找元素索引需要遍历整个元素数组,效率不高。
- 线程不安全。
4、【补充知识】RandomAccess 接口
该接口源码中没有任何定义,则推断该接口仅为标识作用,代表该实现类具有随机访问功能
public interface RandomAccess {
}
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!