小王,听说你对 ArrayList 很熟呀!今天我们就来聊一下它吧!
(小 case 了,这种问题早就滚瓜烂熟了呀!放马过来吧!)
好的,没问题,想了解什么都可以问!
你先说一下 ArrayList 是一个什么东西?可以用来干嘛?
ArrayList就是数组列表,主要用来装载数据,当我们装载的是基本类型的数据int,long,boolean,short,byte…的时候我们只能存储他们对应的包装类,它的主要底层实现是数组Object[] elementData。
ArrayList底层是用数组实现的存储,特点是查询效率高,增删效率低,线程不安全。使用频率很高,
我们知道 ArrayList 是线程不安全的,为什么还使用它呢?
因为我们正常使用的场景中,都是用来查询,不会涉及太频繁的增删,如果涉及频繁的增删,可以使用LinkedList,如果你需要线程安全就使用Vector,这就是三者的区别了,实际开发过程中还是ArrayList使用最多的。
不存在一个集合工具是查询效率又高,增删效率也高的,还线程安全的,至于为啥大家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。
说一下 ArrayList 的初始化过程
ArrayList 的无参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为一个默认空数组Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} 所以数组容量为0,只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量,源码如下:
// 指定初始化大小构造器
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() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 默认元素大小
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
ArrayList 的扩容机制是怎样的?
ArrayList 不同于数组,数组是固定长度的,而 ArrayList 是会动态调整数组元素的大小,也就是我们所说的扩容。我们知道,ArrayList 的初始化的是不会为元素分配空间的,只有当第一次 add() 的时候才会分配默认 10 个元素的空间大小:
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
后续不断添加元素的时候,首先会先判断元素空间是否已满,如果满了则会扩容 1.5 倍,然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组。
ArrayList 的默认数组大小为什么是10?
其实我也没找到具体原因,据说是因为sun的程序员对一系列广泛使用的程序代码进行了调研,结果就是10这个长度的数组是最常用的最有效率的,也有说就是随便起的一个数字,8个12个都没什么区别,只是因为10这个数组比较的圆满而已。
你说到 ArrayList 的增删很慢,那 ArrayList 在增删的时候是怎么做的么?
好的,我分别说一下他的新增的逻辑吧,它有指定index新增,也有直接新增的,在这之前他会有一步校验长度的判断ensureCapacityInternal,就是说如果长度不够,是需要扩容的。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
在扩容的时候,老版本的jdk和8以后的版本是有区别的,8之后的效率更高了,采用了位运算,右移一位,其实就是除以2这个操作:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 右移一位,扩容1.5倍
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);
}
指定位置新增的时候,在校验之后的操作很简单,就是数组的copy:
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++;
}
arraycopy 的原理是这样子的:假如有10个元素,需要在 index 5 的位置去新增一个元素A,它会把从index 5的位置开始的后面所有元素赋值到 index 5+1 的位置,然后在 index 5 的位置放入元素A就完成了新增的操作了。
至于为啥说他效率低,我想我不说你也应该知道了,我这只是在一个这么小的List里面操作,要是我去一个几百几千几万大小的List新增一个元素,那就需要后面所有的元素都复制,然后如果再涉及到扩容啥的就更慢了不是嘛。
我问你个真实的场景问题,ArrayList(int initialCapacity)会不会初始化数组大小?
答案是:不会初始化数组大小!
从源码可以看出,ArrayList(int initialCapacity) 只是给 elementData 分配了初始空间而已,数组元素大小 size 并没有初始化或者说赋值,所以说初始化之后调用 size() 返回的是0。只有调用 add() 方法才会改变 size 的值。
ArrayList插入删除一定慢么?
不一定,这取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。
那 ArrayList 的删除怎么实现的呢?
删除其实跟新增是一样的,不过叫是叫删除,但是在源码中我们发现,他还是在copy一个数组:
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;
}
比如我们现在要删除数组中的 index5 的这个位置上的元素 ,那代码他就复制一个index5+1开始到最后的数组,然后把它放到index开始的位置,index5的位置就成功被”删除“了其实就是被覆盖了,给了你被删除的感觉,同理他的效率也低,因为数组如果很大的话,一样需要复制和移动的位置就大了。
ArrayList 用来做队列合适么?
队列一般是FIFO(先入先出)的,如果用ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的,所以 ArrayList 不适合做队列。
那数组适合用来做队列么?
数组是非常合适的,比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的,另外著名的Disruptor开源Library也是用环形数组来实现的超高性能队列,具体原理不做解释,比较复杂,简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组。
ArrayList的遍历和LinkedList遍历性能比较如何?
论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。