一、ArrayList的概述
ArrayList 是我们开发中常用的一种数据结构,它的底层是基于数组实现的,是一个动态数组,容量你可以动态 增加,ArrayList实现Serializable 接口,他能支持序列化传输;实现了RandomAccess接口,支持快速随机访问,也就是通过下标可以实现快速访问;实现了Cloneable 接口,意味着可以被克隆。
二、ArrayList 的源码解析
首先看下 构造函数
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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);
}
}
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;
②、而第二个有参数构造器是先判断initialCapacity与0之间的关系,如果大于0,则创建一个大小为initialCapacity的对象数组赋给elementData;如果等于0,则将EMPTY_ELEMENTDATA赋给 elementData, 如果小于0;则抛出异常;
③、第三个构造器将参数集合转换成数组判断集合是否为空,为空将 EMPTY_ELEMENTDATA 赋值给elementData,否则将转换后的数组拷贝到elementData中,由此可知elementData就是ArrayList底层维护的数据
通过上面发现一个神奇的问题:在无参数构造函数中将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个数组赋值给elementData但是在有参构造函数中initialCapacity大小为0时 将 EMPTY_ELEMENTDATA 赋值给elementData, 带着疑问继续看源码发现:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(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);
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是在calculateCapacity 这个方法里面使用的,这个方法是判断默认容量大小的。
如果是无参的构造,他的默认容量会被设置为10;
而如果是有参的话就会取minCapacity的值,这也体现出jdk源码设计的微妙,如果我们在有参构造中将elementData赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA 那么就会导致小于10的容量变成10,假如我们只存储3个元素,那么无形中就有7个空间被白白的浪费掉了。
接着往下走我们会发现,添加的时候会先去判断容量是否够用,如果够用就不在去扩容,如果不够用,就进行扩容,扩大到上次的1.5 倍,因为数组的大小一旦声明是无法修改的,源码中是使用了copyOf 函数, copyOf 底层的原理是在内存中重新开辟一块空间将已有的数据拷贝过去,这样就在不影响原有数据的基础上进行了扩容,可以编写一个demo验证一下:
public static void main(String[] args) {
// List list = new ArrayList<>();
// System.out.println(list);
Object[] object = {1,2,3};
System.out.println(object);
object = Arrays.copyOf(object, 10);
System.out.println(object);
}
运行后发现使用 copyof 函数后并没有指向当前数组,而是指向了其他,这也就证明了他不会在当前数组进行扩容,而是在其他的地方重新创建了一块空间。
假设数据量很大的时候,会导致效率大大的降低,所以以后在使用ArrayList 的时候即使不知道具体大小,也可以指定一个大概的容量,这也就可以避免重复的扩容,降低效率.
通过刚才的分析,还发现了一个问题是add 方法居然没有加锁,这样如果在多线程的情况下,ArrayList 的add方法是线程不安全的,容易产生数组越界问题.
1.当线程A调用ensureCapacityInternal 方法时 这时size=9 判断容量后发现不用扩容
2.当线程B 进入时 他得到size 也是9,而elementData的大小是10,判断容量后发现不用扩容
3.当线程A去执行elementData[size++] = e 操作时,size++, size 的大小变成了10,
4.当线程B再去执行elementData[size++] = e 操作时,就会产生数组越界的问题,
解决方法:为了解决线程安全问题可以使用 Collections.synchronizedList(),但是会降低效率 如:
List list = Collections.synchroizedList(new ArrayList<>());
删除 remove():
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;
}
删除的方法比较简单,每次进行数组的复制,然后将旧的 elementData=null 让垃圾回收机制去回收。
Arrylist 的 get ,set,clear 也没有其他复杂的操作, 都是一些对数组的简单操作。
ArrayList的优点
1.ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快。
2.可以做到自动扩容,无需开发者关心数组大小
ArrayList的缺点
1.因为每次插入和删除都需要对数组进行拷贝操作,所以插入和删除的效率并不高
2.是线程不安全