从底层实现和性能优劣势两方面再理解ArrayList

来源:互联网 发布:java计算代码执行时间 编辑:程序博客网 时间:2024/06/15 18:27

ArrayList是开发人员经常使用的一个集合类,从名称上看,ArrayList是一个以数组的形式实现的集合,
注:以下的代码中的中文注释并不是源码中的注释,而是本人根据自己的理解添加上去的,jdk版本1.7

 private static final Object[] EMPTY_ELEMENTDATA = {}; public ArrayList() {        super();        this.elementData = EMPTY_ELEMENTDATA;    }

再使用ArrayList的时候如果没有初始化容量,ArrayList会自动给予一个默认的初始化容量

 private static final int DEFAULT_CAPACITY = 10;

当然在创建对象的时候可以手动设置一个自定义的初始化的容量,具体是通过带参的构造函数得以实现:

`public ArrayList(int initialCapacity) {    super();     if (initialCapacity < 0)         throw new IllegalArgumentException("Illegal      Capacity: "+ initialCapacity);        this.elementData = new Object[initialCapacity];    }`  

如果传入的参数是负数,则会抛出不合法的参数异常。在了解了ArrayList的底层实现之后可以先初步总结一下:
1.可以为空;通过数组区存储数据,决定了在ArrayList的任意位置可以为空。
2.是有序的;存入的数据是以一定的顺序进行存放的,每一个存进来的数 据,都有一个唯一的索引标识相应的数据,所以,读取数据的顺序与存入数据的顺序是一致的。
3.可以重复;因为每一个数据都有相应的索引标识,所以可以存储重复的数据。

扩容
通过

    private static final int DEFAULT_CAPACITY = 10;

我们知道ArrayList的默认的大小是10;当存储的数据量大于10的时候,会动态扩容:

  private void ensureCapacityInternal(int minCapacity) {        //如果数组是初始化的那个数组,则将初始化容量和现在的容量中较大的数值传入后续的方法中        if (elementData == EMPTY_ELEMENTDATA) {            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) {        modCount++;           //如果现在的容量大于数组原本的长度调用grow方法             if (minCapacity - elementData.length > 0)            grow(minCapacity); }  private void grow(int minCapacity) {                int oldCapacity = elementData.length;        int newCapacity = oldCapacity + (oldCapacity >> 1);        if (newCapacity - minCapacity < 0)            newCapacity = minCapacity;        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);                        elementData = Arrays.copyOf(elementData, newCapacity);    }

其中 int newCapacity = oldCapacity + (oldCapacity >> 1)中的”>>”标识右移,转为二进制的形式可能更好理解,0000 1111(15)右移2位的结果是0000 0011(3),0001 1010(18)右移3位的结果是0000 0011(3)。
可以看到在源码中通过严格的大小比较判断,决定是否要进行扩容,扩容后的容量大小是多少;将最终的容量大小,和原数组elementData作为参数,调用Arrays.copyOf方法,将原数组里的内容复制到新的数组里面去。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {        T[] copy = ((Object)newType == (Object)Object[].class)            ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);        System.arraycopy(original, 0, copy, 0,                         Math.min(original.length, newLength));        return copy; }

添加元素
在没有泛型约束的情况下ArrayList中是可以添加任意的元素:

 public boolean add(E e) {        //给予一个新的容量大小进行扩容        ensureCapacityInternal(size + 1);        //在数组的最后一个位置放入对象e        elementData[size++] = e;        return true; }

制定任意的位置添加元素

 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++;    }

在理解了扩容之后就很容易理解添加操作了

删除

首先是指定具体位置的删除

 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;        return oldValue;    }

删除指定的元素

public boolean remove(Object o) {        if (o == null) {            for (int index = 0; index < size; index++)                if (elementData[index] == null) {                    fastRemove(index);                    return true;                }        } else {            for (int index = 0; index < size; index++)                //通过循环遍历查找与指定元素相同的元素的索引进行删除                if (o.equals(elementData[index])) {                    fastRemove(index);                    return true;                }        }        return false;    }private void fastRemove(int index) {        modCount++;        int numMoved = size - index - 1;        if (numMoved > 0)            System.arraycopy(elementData, index+1, elementData, index,numMoved);        elementData[--size] = null;    }

删除指定的元素原理跟删除指定位置的元素类似

在了解了ArrayList的增加和删除的原理之后,总结一下ArrayList的优缺点:
1.因为ArrayList底层是以数组实现的所以决定了它在查找的时候会非常的快。
2.ArrayList在顺序添加元素的时候其实是相当于向数组中添加一个元素,所以速度也很快。
3.ArrayList在向指定位置添加元素或者是删除元素的时候涉及到一次元素的复制和移动,所以如果元素比较多的话会比较耗费性能。
所以,综上所述,ArrayList适合顺序添加(add())和随机访问(get())的场景。
ArrayList的并发问题
ArrayList是线程非安全的,因为其中的方法都不是同步的,并发情况下回出现一些问题。为了解决这个问题,有两种办法可以尝试:
1.通过Collections.synchronizedList()方法将ArrayList变成线程安全的list;代码实现如下:

 public static <T> List<T> synchronizedList(List<T> list) {        return (list instanceof RandomAccess ?                new SynchronizedRandomAccessList<>(list) :                new SynchronizedList<>(list));    }

另外的一个方法就是使用Vector,Vector中的方法都是线程安全的。例如:

 public synchronized void addElement(E obj) {        modCount++;        ensureCapacityHelper(elementCount + 1);        elementData[elementCount++] = obj;    }

在看ArrayList的底层实现的时候,看到一个比较少见的关键字:transient

 private transient Object[] elementData;

当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。
那为什么底层的数组要用这个变量修饰呢?因为序列化ArrayList的时候elementData未必是满的。比如说容量是10,但是只有3个元素放在了里面,没有必要序列化整个elementData。ArrayList中重写了writeObject方法:

  private void writeObject(java.io.ObjectOutputStream s)        throws java.io.IOException{        // Write out element count, and any hidden stuff        int expectedModCount = modCount;        s.defaultWriteObject();        // Write out size as capacity for behavioural compatibility with clone()        s.writeInt(size);        // Write out all elements in the proper order.        for (int i=0; i<size; i++) {            s.writeObject(elementData[i]);        }        if (modCount != expectedModCount) {            throw new ConcurrentModificationException();        }    }

每次序列化的时候调用这个方法只序列化elementData中的非transient元素。即加快了序列化的速度,又减小了序列化之后的文件大小。