【Java源码分析】ArrayList源码分析

类的定义

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
  1. List接口的实现类,AbstractList 的子类,支持随机访问,因此底层实现的数据结构是数组
  2. 实现了所有list的操作,允许所有类型的元素,包括NULL
  3. 提供计算Array大小的接口,可以看做是一个Vector,区别在于Vector是线程安全的,ArrayList不是
  4. 由于底层实现数据结构是数组,所以get set iterator遍历, 以及size判断等都是常数时间。其他操作是线性时间复杂度,而且常数因子比LinkedList要小,因此效率略高
  5. 含有一个容量capacity实例,表示容量的大小,随着数据的加入,该实例会自增,自增
  6. 非线程安全,因此如果在多线程环境下需要自行处理线程安全问题,也可以使用Collections.synchronizedList方法List list = Collections.synchronizedList(new ArrayList(...));进行包装,而且最好是在声明变量的时候就考虑是否需要做多线程的处理,避免出错
  7. ConcurrentModificationException异常,如果ArrayList的实例在使用迭代器Iterator遍历的时候,如果出现了不是使用这个迭代器所做的remove或者add操作,那么就会直接抛出并发修改的异常。这个异常即使是在单线程下也是会出现的,常见的场景是用迭代器一边遍历一边修改ArrayList对象的时候
  8. 对于7中的现象也叫做是fail-fast,该行为只能尽量检测出并发异常,但是不能做到绝对的线程安全,所以编程的时候不要因为没有抛出该异常就觉得是线程安全的

构造函数

三种类型,默认构造(默认构造的Listd的size 为10),传递size的构造,传递一个Collection对象的构造。

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

public ArrayList() {
    this(10);
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

扩容方式

private transient Object[] elementData; // 对象数组,实际存放数据的一个数组容器,默认初始10个Object
private int size; // 实际存放对象数目
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 最大容量;由于一些虚拟机会在Array首部存储一些信息,所以此处减去8字节 

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容发生在增加元素的时候发现现有数组放不下了,就调用该扩容函数。由于扩容的过程中存在着拷贝过程,而拷贝是整个集合的拷贝,所以还是很影响性能的,因此最好在开始的时候能够预估将要使用多大的容量,避免使用过程中频繁的扩容。扩容的思路是默认扩大为原来的1.5倍,如果指定值minCapacity比原来容量的1.5倍大,那么按指定值扩容;如果指定值minCapacity比阈值MAX_ARRAY_SIZE大,那么按如下策略确定扩容最终大小(minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;,这样会存在一定的OOM风险

减少容量,用于减少一个ArrayList实例所占存储空间

public void trimToSize() {
    modCount++;
    int oldCapacity = elementData.length;
    if (size < oldCapacity) {
        elementData = Arrays.copyOf(elementData, size);
    }
}

从如下的一个侧面说明ArrayList是允许空的对象存入的。

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

连接集合类Collection和线性表Array类的桥梁,该方法将集合中所存储的对象拷贝到一个对象数组中返回

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

该方法还有另外一个多态实现,根据运行时类型返回指定类型的数组

public <T> T[] toArray(T[] a) { ... }

添加一个元素到集合

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 boolean add(E e) { ... }只需要做一次赋值即可

删除指定位置的元素

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; // Let gc do its work

    return oldValue;
}

从代码来看也是比较好懂的,删除指定位置元素,然后指定位置之后的元素前移,这是一个数组拷贝过程。但是需要注意最后一点,就是将最后一个空出的位置置空,这个问题在Effective Java中有提到过,就是及时的消除过期引用。对于一些自定义的数组类如果对象被移走了,但是指针没有置为空,那么强引用还是会一直保持,就会导致内存泄露。

另一个删除操作是删除指定对象

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

其中用到了一个私有的删除操作fastRemove(index),该操作的特点是不检查边界也不返回被删除值,毕竟是自己内部调用,已经可以保证边界是正确的。

清空操作

public void clear() {
    modCount++;

    // Let gc do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

比较直接,循环置空,然后将对象的清除工作交给GC,注意clear只是将内容清空,集合容器还是在的、

删除集合中某些元素

public boolean removeAll(Collection<?> c) {
    return batchRemove(c, false);
}

功能就是将所有位于c中的元素全部删除

保留集合中某些元素

public boolean retainAll(Collection<?> c) {
    return batchRemove(c, true);
}

功能就是将所有位于c中的元素全部保留,不在c中的元素全部删除

对比这两个方法就可以看出,实际的工作都是由private boolean batchRemove(Collection<?> c, boolean complement) {}完成的,当第二个参数是true的时候代表保留元素,否则代表删除。

集合数据的状态保存和恢复

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 array length
    s.writeInt(elementData.length);

    // 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();
    }

}

恢复,将数据从流中取出

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in array length and allocate array
    int arrayLength = s.readInt();
    Object[] a = elementData = new Object[arrayLength];

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++)
        a[i] = s.readObject();
}

类似于序列化和反序列化,这里状态保存和恢复也是成对出现的,保存和恢复的顺序都是一样的,否则不可能得到正确的对象或者集合。注意在保存状态的时候同样是不能够修改集合的,否则也是抛出并发修改异常

再来看看ArrayList的迭代器

private class Itr implements Iterator<E> {
    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 != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

前面说迭代器的时候提到过在使用同一个迭代器迭代访问元素的时候,是不能修改集合的结构的(也就是大小,但是内容变化大小不变还是可以的比如add(index, value)或者set(index, value)是可以的,但是add(value)不可以,该操作会modCount++),愿意就在于每当初始化一个迭代器实例的时候,该迭代器都保存了当前集合的修改次数。迭代器的每一个操作结束之后都会检查保存的值是否和集合的modCount值相同,如果不同直接抛出异常并且结束,这就是所谓的fail-fast

获取子集合操作

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

返回从fromIndex到toIndex之间的元素构成的子集合,不包括toIndex。当toIndex和fromIndex相等的时候返回的是空的子集合而不是空指针

注意SubListprivate class SubList extends AbstractList<E> implements RandomAccess {}是ArrayList的一个内部子类,该子类实现了和ArrayList一样的方法.

最后补充一下经常调用的连个拷贝函数Arrays.copyOf(elementData, newCapacity)System.arraycopy(elementData, index, elementData, index + 1, size - index);

前者最终调用了Arrays的copyOf()方法

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

该方法实现如下

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

实现的过程是先确定泛型类型,然后调用系统的copy函数,将指定集合中的元素拷贝到目标集合中,目标集合是一个新建的newLength大小的集合。在实际的拷贝过程中还是调用了System.arraycopy()函数

是一个Native方法

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

该方法将srcPos+length-1个元素拷贝到dest中从destPos到destPos+length-1。由于是Native方法,所以在JDK中是看不到的,但是可以在openJDK中查看源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。

时间: 2024-11-10 01:22:48

【Java源码分析】ArrayList源码分析的相关文章

【源码】ArrayList源码剖析

//-------------------------------------------------------------------- 转载请注明出处:http://blog.csdn.net/chdjj by Rowandjj 2014/8/7 //-------------------------------------------------------------------- 从这篇文章开始,我将对java集合框架中的一些比较重要且常用的类进行分析.这篇文章主要介绍的是Array

Java集合系列之ArrayList源码分析

一.ArrayList简介 ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类. 该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加.如果想ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能. ArrayList的用法和Vector向类似,但是Vect

Java集合源码剖析——ArrayList源码剖析

ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类. ArrayList实现了Serializable接口,因此它支持序列化,能够通过

[Java源码分析]ArrayList源码分析

ArrayList是java集合中最常用的,基于一个数组实现的,容量可以动态增长. ArrayList不是现成安全的,只能在单线程环境下使用. 本文以jdk1.8的源码为例,分析其实现机制. 1.基本属性与构造函数 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private sta

jdk1.8.0_45源码解读——ArrayList的实现

jdk1.8.0_45源码解读——ArrayList的实现 一.ArrayList概述 ArrayList是List接口的可变数组的实现.实现了所有可选列表操作,并允许包括 null 在内的所有元素.除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小. 每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小.它总是至少等于列表的大小.随着向ArrayList中不断添加元素,其容量也自动增长.自动增长会带来数据向新数组的重新拷贝,因此,如果可预

Java - ArrayList源码分析

java提高篇(二一)-----ArrayList 一.ArrayList概述 ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 null 在内的所有元素.除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小. 每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小.默认初始容量为10.随着ArrayList中元素的增加,它的容量也会不断的自动增长.在每次添加新的元素时,Array

Java笔记---ArrayList源码分析

一.前言 一直就想看看java的源码,学习一下大牛的编程.这次下狠心花了几个晚上的时间,终于仔细分析了下 ArrayList 的源码(PS:谁说的一个晚上可以看完的?太瞎扯了).现在记录一下所得. 二.ArrayList 源码分析 2.1 如何分析? 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 Eclipse 找到所需要分析的类(此处就是 ArrayList) 新建类:新建一个类,命名为 ArrayList,将源码拷贝到该类.因为我

java 集合类源码分析--arrayList

ArrayList就是传说中的动态数组,就是Array的复杂版本,它提供了如下一些好处:动态的增加和减少元素.灵活的设置数组的大小...... 认真阅读本文,我相信一定会对你有帮助.比如为什么ArrayList里面提供了一个受保护的removeRange方法?提供了其他没有被调用过的私有方法? 首先看到对ArrayList的定义: public class ArrayList<E> extends AbstractList<E> implements List<E>,

Java源码之ArrayList分析

一.ArrayList简介 ArrayList底层的数据结构是数组,数组元素类型为Object类型,即可以存放所有类型数据. 与Java中的数组相比,它的容量能动态增长.当创建一个数组的时候,就必须确定它的大小,系统会在内存中开辟一块连续的空间,用来保存数组,因此数组容量固定且无法动态改变.ArrayList在保留数组可以快速查找的优势的基础上,弥补了数组在创建后,要往数组添加元素的弊端.实现的基本方法如下: 快速查找:在物理内存上采用顺序存储结构,因此可根据索引快速的查找元素. 容量动态增长:

Java ArrayList源码分析(有助于理解数据结构)

arraylist源码分析 1.数组介绍 数组是数据结构中很基本的结构,很多编程语言都内置数组,类似于数据结构中的线性表 在java中当创建数组时会在内存中划分出一块连续的内存,然后当有数据进入的时候会将数据按顺序的存储在这块连续的内存中.当需要读取数组中的数据时,需要提供数组中的索引,然后数组根据索引将内 存中的数据取出来,返回给读取程序.在Java中并不是所有的数据都能存储到数组中,只有相同类型的数据才可以一起存储到数组中.    因为数组在存储数据时是按顺序存储的,存储数据的内存也是连续的