说明
在一次的笔试过程中,我遇到一个题目,ArrayList list = new ArrayList(20);这行代码ArrayList底层会扩容几次,这个时候我就懵了,因为我对ArrayList的底层不太理解。虽然我们不理解,但是没有关系,我们把它给学起来就可以了。
浅谈Array和List
说ArrayList之前,我们先把ArrayList拆开来,就说一下Array和List。
Array
我们可以先来说一说数组,Array数组,是一种基于索引的数据结构,使用索引来搜索和获取数据是很快的,Array检索和获取数据的时间复杂度是O(1),但是要删除数据的开销却很大,因为删除数据之后需要对数组的数据进行重新排列,删数据之后,后面的元素需要移动到前面。
而我们的数组有一个缺点,就说初始化时,必须指定长度,否则就会报错。
List
然后是我们的List,List是Collection的子接口,是一个有序、可重复的集合,List在Java中有两种实现方式,一种是LinkedList,而另一种就是我们的ArrayList,ArrayList是基于索引结构的实现。通常如果我们使用的话,如果我们在不明确插入多少条数据的情况下,那么使用数组就很尴尬了,因为你不知道初始化数组大小为多少。ArrayList可以使用默认大小,当元素达到一定个数后,会实现自动扩容,我们可以这样理解,数组是定死的,而ArrayList是动态的数组。
浅谈ArrayList
我们在学习Java的时候,我们都需要深入底层去学习,这有利于我们的提升,我们来看一下ArrayList的底层原理。
在ArrayList底层源码中有几个我们需要注意的属性:
- Default_capacity (int)
- Empty_elementdata (Obejct[ ]{ })
- DefaultCapacity_empty_elementdata(Object[ ]{ })
- elementData(Object[ ])
- size (int)
我们先来看一下几个构造器
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
从源码中我们可以看出,当我们直接创建ArrayList,elementData被赋予了默认空容量的Object[ ]数组,所以此时ArrayList数组是空的、固定长度的,也就是说其容量此时是0,元素个数size为默认值0。
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
从该构造器可以看出:
- 如果initialCapacity > 0 ,那么在堆上new 一个Object类型的数组,大小为initialCapacity,并将其引用到elementData上,此时ArrayList的容量为initialCapacity,元素个数大小size为0;
- 如果initialCapacity = 0 ,那么将属性中定义的空的数组赋值给elementData,此时ArrayList的容量为0,元素个数大小size为0;
- 如果initialCapacity > 0,那么直接抛出异常;
从这里我们可以看出:如果我们在创建ArraylList实例过程中我们使用初始化容量的构造器,那么,它底层只是直接在堆上new 一个Object类型的数组,大小为initialCapacity,并没有进行扩容。
笔试题:ArrayList list = new ArrayList(20);
底层扩容了几次?
答案:0次。
下面的构造器感兴趣的自己去看看源码,这里我们来说一个另一个重要的方法:add(Element e);
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;
}
}
ArrayList的扩容机制
我们来说一下ArrayList的一个方法:add()。
add():从底层源码可以看出,add()首次扩容会从0扩容成10,再次扩容到上次扩容的1.5倍,比如0,10,15,22,33…
add(),添加元素到集合的尾部,如果要增加的数据量很大的话,我们需要调用一下ensureCapicity(int minSize)。
ensureCapicity(int minSize)
ensureCapacity(int n)方法可以对ArrayList底层的数组进行扩容。
- 若参数值大于底层数组长度的1.5倍,则数组的长度就扩容为这个参数值大小。
- 若小于底层数组长度的1.5倍,则数组长度就扩容为底层数组长度的1.5倍。
使用它是因为它可以一次性将我们的ArraylIst扩容到指定的容量,而如果我们需要加入大量元素而不进行指定,那么可能ArrayList会进行多次扩容,这样子会消耗很多性能的。而使用ensureCapicity()方法的作用是预先设定ArrayList的大小,这样我们可以大大提高初始化速度和性能了。我们来看一个例子。
public class EnsureCapacityTest {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<Object>();
final int N =10000000;
long startTime = System.currentTimeMillis();
for(int i = 0;i<N;i++) {
list.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime);
list = new ArrayList<Object>();
long startTime1 = System.currentTimeMillis();
list.ensureCapacity(N);
for(int i = 0;i<N;i++) {
list.add(i);
}
long endTime1 = System.currentTimeMillis();
System.out.println(endTime1-startTime1);
}
}
结果:
4747
233
ArrayList的线程安全问题
我们来说一个著名的异常:ConcurrentModificationException并发修改异常。我们来写一个ArrayList并发修改异常的例子,并引出著名的fail-fast机制。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next.equals(1)) {
list.remove(1);
}
}
异常信息如下:
关于Fail-Fast机制和ConcurrentModificationException具体见我另一篇文章:Fail-Fast机制和ConcurrentModificationException并发修改异常。
在线程不安全的情况下,如果我们在遍历集合的同时,需要修改集合中的元素,那么在绝大多数情况下就很可能会出现ConcurrentModificationException并发修改异常 。那么我们需要如何解决快速失败机制中的并发修改异常呢?
这个其实很简单的,我们只需要将java.util包下的类改成java.util.concurrent包下的类就可以了,具体的话:
ArrayList ---- CopyOnWriteArrayList
HashMap — ConcurrentHashMap