前言
ArrayList可能是Java中使用次数最多的数据结构了,因此了解其特性能比较重要
描述
ArrayList是一个数组队列,相当于动态数组.与Java中的数组相比,它的容量能动态增长.
并且ArrayList还有一些添加,遍历和移除的操作
特点
1.ArrayList内部实现是利用Java的数组
这是内部存储数据的Object数组
add方法的实现方式,根据源码可以看到,先判断了一下容量,然后在当前已存放数据的size的位置设置为传过来的数据,并且size+1
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查容量(扩充机制)下面再说
elementData[size++] = e;
return true;
}
2.ArrayList的空参构造默认长度不是10,而是0
ArrayList一共有三个构造方法
//1设置初始化容量
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);
}
}
//2默认的无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//3传入一个Collection对象
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内部的数组,然后如果有长度在设置一下size,跟本条关系不大
然后第一个构造,是指定默认的数组长度,小于0抛异常,等于零则设置内部数组为静态的length为0的空数组
重点是第二个构造,直接赋值成如下的数组,可以看到是一个空容量的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
而在第一次add的时候会进行扩容,代码如下所示
private static final int DEFAULT_CAPACITY = 10;//默认第一次扩容的容量
private void ensureCapacityInternal(int minCapacity) {//这里会传入size+1,也就是1
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果是默认的空数组
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);//检查容量并扩容
}
3.ArrayList的扩容方式,是先创建一个更大的数组,然后把旧数组内容拷贝过去,在把内部数组设置为新的大数组
我们看其add方法实现
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查自身的可用容量
elementData[size++] = e;
return true;
}
如果ensureCapacityInternal检查到容量不够,最终会调用如下代码
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;//记录当前的长度
int newCapacity = oldCapacity + (oldCapacity >> 1);// >> 符号(右位移),相当于除2,所以扩容是1+0.5=1.5倍
if (newCapacity - minCapacity < 0)//如果扩容后,容量还小于指定容量,就直接设置为指定容量,比如使用空参构造new,size为0,乘1.5还是0,就会在第一次扩容的时候设置为10
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//长度过长检查,如果长度溢出了int的值,就会变成负数导致抛异常(但说实话,它这个判断还是有可能会int溢出)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);//进行数组拷贝工作,容量为新扩容的大小
}
所以,如果事先知道元素的大小或大概的大小,可以在构造中传入,就能减少扩容次数,能有效节省时间和减少内存消耗
4.ArrayList的indexOf实现是遍历
public int indexOf(Object o) {
if (o == 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;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
可以看到,indexOf是从前向后遍历,而lastIndexOf是从后向前遍历,所以如果你大概知道想查找的元素在前或者后,使用对应的方法,可以更节省时间
5.因为内部使用的是数组,所以其特性也和数组特性类似
ArrayList根据索引的方式去查找数据,会比较快,不用进行循环或多次寻址
而遍历查询则需要遍历索引来循环从数组中取出
6.ArrayList是线程不安全的
内部没有使用锁或同步机制,如果想要使用线程安全的ArrayList(类似特性),可以使用Collections.synchronizedList
7.ArrayList不支持forEach时修改数据
forEach其实是java的语法糖,内部使用的是迭代器Iterator,通过hasNext()和next()方法,来遍历数据
private class Itr implements Iterator<E> {ArrayList的内部类迭代器
protected int limit = ArrayList.this.size;
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor < limit;
}
@SuppressWarnings("unchecked")
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
if (i >= limit)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}
而内部有一个modCount变量,标识的是自身修改的次数,会在扩容时,remove时和clear时进行+1操作,而在迭代器内next()方法取数据的时候,会判断modCount变量,看看到底修没修改过原数据,如果修改过就会抛异常(这是为了数据的准确性)
如果想要避免上述问题,可以使用CopyOnWriteArrayList(线程安全,支持forEach时修改数据)
8.ArrayList支持随机读取,并且随机添加和删除效率不高
因为ArrayList的实现基于数组,所以根据索引来获取是很快的
而添加到的时候,只有在添加在尾部时效率较好(且不触发扩容)
而随机插入,则会引起插入索引后面的位置都会被复制并向后位移,所以效率不高
删除也是,只有删除尾部时效率较好
随机删除会引起删除的索引后面的位置都会被复制并向前位移,所以效率也不高
下一篇: 数据结构特性解析 (三) 链表