Java ArrayList源码剖析

当涉及到存储和操作数据时,动态数组容器类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 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。

你可能感兴趣的:(Java常用类的源码剖析,java,开发语言)