简述ArrayList
- ArrayList 是一个动态数组,它是线程不安全的,允许元素为null。
- 其底层数据结构依然是数组,它实现了List<E>, RandomAccess, Cloneable, java.io.Serializable接口,其中RandomAccess代表了其拥有随机快速访问的能力,ArrayList可以以O(1)的时间复杂度去根据下标访问元素。
- 因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length),所以它也有数组的缺点,空间效率不高。
- 由于数组的内存连续,可以根据下标以O(1)的时间读写(改查)元素,因此时间效率很高。
- 当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList 的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}构造方法,指定集合的大小,去构建ArrayList实例,以减少扩容次数,提高效率。
ArrayList结构
看一下继承、实现了什么
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
我们首先需要明白并且牢记在内心的是,ArrayList本质上是一个数组,但是与Java中基础的数组所不同的是,它能够动态增长自己的容量。
通过ArrayList的定义,可以知道ArrayList继承了AbstractList,同时实现了List,RandomAccess,Cloneable和java.io.Serializable接口。
那么这些提供了什么功能呢?
- 继承了AbstractList类,实现了List,意味着ArrayList是一个数组队列,提供了诸如增删改查、遍历等功能。
- 实现了RandomAccess接口,意味着ArrayList提供了随机访问的功能。RandomAccess接口在Java中是用来被List实现,用来提供快速访问功能的。在ArrayList中,即我们可以通过元素的序号快速获取元素对象。
- 实现了Cloneable接口,意味着ArrayList实现了clone()函数,能被克隆。
- 实现了java.io.Serializable接口,意味着ArrayList能够通过序列化进行传输或者持久保存。
属性分析
/** * 默认初始容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 共享的空数组 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 使用默认大小的共享的空数组 * 与EMPTY_ELEMENTDATA的区别:当向数组添加第一个元素时,知道数组该扩容多少. */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * ArrayList基于该数组实现,用该数组保存数据 * ArrayList的容量是该数组的长度 * 数组的默认大小为DEFAULT_CAPACITY * transient:在实现Serializable接口后,将不需要序列化的属性前面加上transient */ transient Object[] elementData; // 没有被私有化是为了简化内部类访问 /** * ArrayList的大小(实际所含元素的个数) */ private int size; /** * 被修改的次数 */ protected transient int modCount = 0; /** * 数组的最大值 * -8是因为要保留数组的一些头信息 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造器
/** * 给定容量的构造器 */ public ArrayList(int initialCapacity) { //如果给的容量为正数,则数组初始化就是这个值 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; //如果为0就采用默认 } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; //否则抛出异常 } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** * 无参构造器,默认容量为10 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 带泛型参数的构造器 */ public ArrayList(Collection<? extends E> c) { //先把参数转为数组类型 //toArray()方法重载自 elementData = c.toArray(); if ((size = elementData.length) != 0) { // 官方的bug,说不一定能返回Object[]类型 if (elementData.getClass() != Object[].class) //如果真的不是Object[]类型,就强制转回Object[]类型 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 如果传入的容器参数为0,就把他替换为空数组 this.elementData = EMPTY_ELEMENTDATA; } }
常用方法
增加
add(E e)
/** * 在数组末尾增加元素 * 先不管ensureCapacityInternal的话, * 这个方法就是将一个元素增加到数组的size位置上,然后size=size+1。 * 再说回ensureCapacityInternal,它是用来扩容的,准确说是用来进行扩容检查的。下面我们来看一下整个扩容的过程 */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! // 下面的操作可分为下面两步 // elementData[size] = e // size=size+1 elementData[size++] = e; return true; }
这个add(E e)函数涉及很多函数,下面逐一分析
// 检查容量大小 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 计算容量大小 private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果是空数组 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 从默认容量和穿进来的容量选择一个最大值 return Math.max(DEFAULT_CAPACITY, minCapacity); } //直接返回minCapacity return minCapacity; } // 扩容判断 private void ensureExplicitCapacity(int minCapacity) { //记录修改的次数 modCount++; // 判断是否需要扩容 // elementData.length数组的大小,并不是数组的元素个数,size才是 if (minCapacity - elementData.length > 0) // 进行真正的扩容操作 grow(minCapacity); }
扩容
重点:grow是真正的扩容操作,所以单独拿出来讲
/** * 进行真正的扩容操作 */ private void grow(int minCapacity) { // 获取旧的列表大小 int oldCapacity = elementData.length; // 新的容量是在原有的容量的基础上+50% 右移一位就是二分之一 // 上面1处>>表示右移,也就是相当于除以2,减为一半 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容一半之后还不足,则新的容器大小等于minCapacity if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果新的容量大于MAX_ARRAY_SIZE,则进行hugeCapacity()操作 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 复制原先的数组,并给他一个新容量 elementData = Arrays.copyOf(elementData, newCapacity); } // 最大不能超过Integer.MAX_VALUE private static int hugeCapacity(int minCapacity) { // 因为一旦大小超过了Integer.MAX_VALUE,数值就会为负数 if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
扩容机制如下:
- 先默认将列表大小newCapacity增加原来一半,即如果原来是10,则新的大小为15;
- 如果新的大小newCapacity依旧不能满足add进来的元素总个数minCapacity,则将列表大小改为和minCapacity一样大;即如果扩大一半后newCapacity为15,但add进来的总元素个数minCapacity为20,则15明显不能存储20个元素,那么此时就将newCapacity大小扩大到20,刚刚好存储20个元素;
- 如果扩容后的列表大小大于2147483639,也就是说大于Integer.MAX_VALUE - 8,此时就要做额外处理了,因为实际总元素大小有可能比Integer.MAX_VALUE还要大,当实际总元素大小minCapacity的值大于Integer.MAX_VALUE,即大于2147483647时,此时minCapacity的值将变为负数,因为int是有符号的,当超过最大值时就变为负数
删除
/** * 删除指定位置的元素 * 把这个元素后面的元素全部往左移一位(下标减一) */ public E remove(int index) { // 数组下标越界检查 rangeCheck(index); // 记录修改次数 modCount++; // 得到要删除的元素 E oldValue = elementData(index); // 需要移动的元素的数量=实际元素个数-当前要删除元素下标-1 int numMoved = size - index - 1; // 如果这个值大于0,说明后续还有元素需要左移 if (numMoved > 0) // 被删除元素的下标为index // 删除原理:index之后的所有元素都往前移一位,覆盖前面的元素,总共需要移动numMoved个元素 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 最后一个元素的值赋值为null,这样就可以被GC回收了 elementData[--size] = null; // 返回删除的值 return oldValue; }
常见问题:
ArrayList为什么线程不安全?
主要分析ArrayList中的add()函数为什么线程不安全
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
通过上面的分析可以看到add()函数中有两个步骤,这两个步骤都在多线程的情况下都有可能会出现问题
首先分析第一个步骤:ensureCapacityInternal(size+1);
我这里画了一个EXCEL方便分析
然后分析第二个步骤elementData[size++] = e;
其实这里就是size++;不是线程安全
ArrayList与LinkedList区别
- List是接口类,ArrayList和LinkedList是List的实现类。
- ArrayList是动态数组(顺序表)的数据结构。顺序表的存储地址是连续的,所以在查找比较快,但是在插入和删除时,由于需要把其它的元素顺序向后移动(或向前移动),所以比较耗时。
- LinkedList是链表的数据结构。链表的存储地址是不连续的,每个存储地址通过指针指向,在查找时需要进行通过指针遍历元素,所以在查找时比较慢。由于链表插入时不需移动其它元素,所以在插入和删除时比较快。
ArrayList和LinkedList的时间复杂度
ArrayList 是线性表(数组)
- get() 直接读取第几个下标,复杂度 O(1)
- add(E) 添加元素,直接在后面添加,复杂度O(1)
- add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)
- remove() 删除元素,后面的元素需要逐个移动,复杂度O(n)
- get() 获取第几个元素,依次遍历,复杂度O(n)
- add(E) 添加到末尾,复杂度O(1)
- add(index, E) 添加第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n)
- remove() 删除元素,直接指针指向操作,复杂度O(1)
ArrayList和Vector的区别
- ArrayList是线程不安全的,Vector是线程安全的
- 扩容时候ArrayList扩0.5倍,Vector扩1倍
ArrayList有没有办法线程安全?
Collections工具类有一个synchronizedList方法
可以把list变为线程安全的集合,但是意义不大,因为可以使用Vector
Vector为什么是线程安全的?
通过对比ArrayList和Vector的源码可以清楚的看到,Vector之所以线程安全是因为加了大量的synchronized
如何复制某个ArrayList到另一个Arraylist中去?
- 使用clone()方法,比如ArrayList newArray = oldArray.clone();
- 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
- 使用Collection的copy方法。
注意1和2是浅拷贝(shallow copy)。
浅拷贝和深拷贝的定义
- 浅拷贝:只复制一个对象,对象内部存在的指向其他对象数组或者引用则不复制
- 深拷贝:对象,对象内部的引用均复制
为了更好的理解它们的区别我们假设有一个对象A,它包含有2对象对象A1和对象A2
对象A进行浅拷贝后,得到对象B但是对象A1和A2并没有被拷贝
对象A进行深拷贝,得到对象B的同时A1和A2连同它们的引用也被拷贝
参考:
https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/2-ArrayList.md
https://juejin.im/post/5b2c5eefe51d4558c0442e95?utm_source=gold_browser_extension
http://developer.51cto.com/art/200905/124592.htm