当涉及到存储和操作数据时,动态数组容器类ArrayList
是Java中最常用的集合类之一。本文会介绍它的基本用法、迭代操作、实现的一些接口,最后分析它的特点。
ArrayList是一个泛型容器,新建ArrayList需要实例化泛型参数,比如:
ArrayList intList = new ArrayList();
ArrayList的主要方法有:
public boolean add(E e) //添加元素到末尾
public boolean isEmpty() //判断是否为空
public int size() //获取长度
public E get(int index) //访问指定位置的元素
public int indexOf(Object o)//查找元素,如果找到,返回索引位置,否则返回-1
public int lastIndexOf(Object o) //从后往前找
public boolean contains(Object o)//是否包含指定元素,依据是equals方法的返回值
public E remove(int index) /删除指定位置的元素,返回值为被删对象
//删除指定对象,只删除第一个相同的对象,返回值表示是否删除了元素
//如果o为nul1,则删除值为nu11的元素
public boolean remove(Object o)
public void clear()//删除所有元素
//在指定位置插入元素,index为0表示插入最前面,index为ArrayList的长度表示插到最后面
public void add(int index, E element)
public E set(int index, E element) //修改指定位置的元素内容
ArrayList内部有一个数组elementData,一般会有一些预留的空间,有一个整数size记录实际的元素个数,如下所示:
private transient Object[] elementData;
private int size;
各种public方法内部操作的基本都是这个数组和这个整数,elementData会随着实际元素个数的增多而重新分配,而size则始终记录实际的元素个数。
下面,我们具体来看下add和remove方法的实现。add方法的主要代码为:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
它首先调用ensureCapacityInternal确保数组容量是够的,ensureCapacityInternal的代码是:
private void ensureCapacityInternal(int minCapacity) {
if(elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
它先判断数组是不是空的,如果是空的,则首次至少要分配的大小为DEFAULT_CAPACITY,
DEFAULT_CAPACITY的值为10,接下来调用ensureExplicitCapacity,主要代码为:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if(minCapacity - elementData.length > 0)
grow(minCapacity);
}
modCount表示内部的修改次数,modCount++当然就是增加修改次数。如果需要的长度大于当前数组的长度,则调用grow方法,其主要代码为:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到每次扩容是变为1.5倍。
我们再来看remove方法的代码:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null;
}
代码中也增加了modCount,然后计算要移动的元素个数,从index往后的元素都往前移动一位,实际调用System.arraycopy方法移动元素。elementData[-size]=null;这行代码将size减1,同时将最后一个位置设为null,设为null后不再引用原来对象,如果原来对象也不再被其他对象引用,就可以被垃圾回收。其他方法大多是比较简单的,我们就不赘述了。上面的代码中,我们删减了一些边界情况处理的代码,完整代码要晦涩复杂一些,但接口一般都是简单直接的,这就是使用容器类的好处,这也是计算机程序中的基本思维方式,封装复杂操作,提供简化接口。
循环打印ArrayList中的每个元素时,ArrayList支持foreach语法:
for(Integer a : intList){
System.out.println(a);
}
foreach看上去更为简洁,而且它适用于各种容器,更为通用。这种foreach语法背后是怎么实现的呢?其实,编译器会将它转换为类似如下代码:
Iterator it = intList.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
ArrayList实现了Iterable接口,Iterable表示可迭代,Java中的定义为:
public interface Iterable {
Iterator iterator();
}
Iterator返回一个实现了Iterator接口的对象,Java中Iterator接口的定义为:
public interface Iterator {
boolean hasNext();
E next();
void remove();
}
hasNext()判断是否还有元素未访问,next()返回下一个元素,remove()删除最后返回的元素。
为什么要通过选代器这种方式访问元素呢?直接使用size()/get(index)语法不也可以吗?在一些
场景下,确实没有什么差别,两者都可以。不过,foreach语法更为简洁一些,更重要的是,迭代器语法更为通用,它适用于各种容器类。此外,迭代器表示的是一种关注点分离的思想,将数据的实际组织方式与数据的选代遍历相分离,是一种常见的设计模式。需要访问容器元素的代码只需要一个Iterator接口的引用,不需要关注数据的实际组织方式,可以使用一致和统一的方式进行访问。而提供Iterator接口的代码了解数据的组织方式,可以提供高效的实现。在ArrayList中,
size/get(index)语法与迭代器性能是差不多的,但在后续介绍的其他容器中,则不一定,比如
LinkedList,迭代器性能就要高很多。从封装的思路上讲,迭代器封装了各种数据组织方式的迭代操作,提供了简单和一致的接口。
Java的各种容器类有一些共性的操作,这些共性以接口的方式体现。ArrayList实现了三个主要的接口:Collection、List和Random-Access。
Collection表示一个数据集合,数据间没有位置或顺序的概念:
public interface Collection extends Iterable {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator iterator();
Object[] toArray();
T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection> c);
boolean addAll(Collection extends E> c);
boolean removeAll(Collection> c);
boolean retainAll(Collection> c);
void clear();
boolean equals(Object o);
int hashCode();
}
List表示有顺序或位置的数据集合,它扩展了Collection。而RandomAccess是没有任何代码的接口,在Java中被称为标记接口,用于声明类的一种属性。这里,实现了RandomAccess接口的类表示可以随机访问,可随机访问就是具备类似数组那样的特性,数据在内存是连续存放的,根据索引值就可以直接定位到具体的元素,访问效率很高。
对于ArrayList,它的特点是内部采用动态数组实现,这决定了以下几点:
1)可以随机访问,按照索引位置进行访问效率很高,用算法描述中的术语,效率是O(1),简单说
就是可以一步到位。
2)除非数组已排序,否则按照内容查找元素效率比较低,具体是O(N),N为数组内容长度,也就
是说,性能与数组长度成正比。
3)添加元素的效率还可以,重新分配和复制数组的开销被平摊了,具体来说,添加N个元素的效率为O(N)。
4)插入和删除元素的效率比较低,因为需要移动元素,具体为O(N)。
ArrayList不是线程安全的,实现线程安全的一种方式是使用Collections提供的方法装饰ArrayList,此外,还有一个类Vector,它是Java最早实现的容器类之一,也实现了List接口,基本原理与ArrayList类似,内部使用synchronized实现了线程安全,不需要线程安全的情况下,推荐使用ArrayList。