ArrayList常用方法和实现原理

Author Avatar
Rico 4月 08, 2015

ArrayList常用方法

ArrayList常用的方法和Collection基本一样

1.add(“AA”);向集合中添加一个元素

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
}

2.add(2, “CC”);将指定的元素插入此列表中的指定位置。索引从0开始

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
    list.add("BB");
    list.add("DD");
    System.out.println(list);
    list.add(2, "CC");
    System.out.println(list);
}

结果为:

[AA, BB, DD]
[AA, BB, CC, DD]

3.addAll(list2);将形参中的集合元素全部添加到当前集合中的尾部。

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
    list.add("BB");
    list.add("DD");
    List<String> list2 = new ArrayList<String>();
    list2.add("CC");
    list2.add("EE");
    list.addAll(list2);
    System.out.println(list);
}

结果为:

[AA, BB, DD, CC, EE]

4.addAll(2, list2);将形参中集合元素添加到当前集合指定的位置。

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
    list.add("BB");
    list.add("DD");
    List<String> list2 = new ArrayList<String>();
    list2.add("CC");
    list2.add("EE");
    list.addAll(2, list2);
    System.out.println(list);
}

结果为:

[AA, BB, CC, EE, DD]

5.clear();清空当前集合中所有元素

list.clear();

注意:以下方法和Collection一样,都依赖元素对象的equals方法。更多的时候都需要重写equals方法。

6.contains(“aa”);返回当前 元素是否包含某一个对象。当前放回false。

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
    list.add("BB");
    list.add("DD");
    boolean contains = list.contains("aa");
    System.out.println(contains);
}

7.get(1);获取当前集合中指定位置的元素,这里返回BB

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
    list.add("BB");
    list.add("DD");
    String string = list.get(1);
    System.out.println(string);
}

8.indexOf(“BB”);返回当前集合中首次出现形参对象的位置,如果集合中不存在就返回-1.

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("AA");
    list.add("BB");
    list.add("BB");
    int indexOf = list.indexOf("BB");
    System.out.println(indexOf);
}

简单的方法就查询JDK API文档吧,下面作简要的说明:

1.size();返回当前集合元素个数.

2.isEmpty();判断集合是否为空,返回布尔类型的结果。

3.lastIndexOf(Object o);返回集合中最后一次出现形参元素的索引,不存在就返回-1。

4.toArray();将集合转换为数组

5.set(int index,E element);用指定元素替代集合中指定位置的元素。

6.remove(Object o); 移除集合中首次出现的元素

7.remove(int index);移除集合中指定位置的元素。

ArrayList实现原理

ArrayList是List接口的可变数组的实现,允许包括null在内的所有元素,既然是数组,那么该类肯定会存在改变存储列表的数组大小的方法。每一个ArrayList实例都有一个容量,该容量是用来存储列表元素的数组的大小,他总是等于列表的大小,随着往ArrayList中添加元素,这个容量也会相应的总动增长,自动增长就会带来数据向新数组的重新拷贝

1.底层使用数组实现

 private transient Object[] elementData;

关于关键字transient:

一个对象只要实现了Serilizable接口,这个对象就可以被序列化。

不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化

2.构造方法:

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

其中

 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

可以看出它的默认数组为长度为0。而在之前JDK1,6中,无参数构造器代码是初始长度为10。
JDK6代码这样的:

 public ArrayList() {
    this(10);
}
 public ArrayList(int initialCapacity) {//可以指定初始大小
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
}
 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);
}

3.如何实现存储和扩容的?

3.1 使用set(int index, E element) ;方法,用指定的元素替代指定位置的元素

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

3.2 add(E e);如何扩容的?

将指定的元素添加到集合尾部

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

代码中关于size含义的源码注释

The size of the ArrayList (the number of elements it contains). 意思就是目前ArrayList中实际包含的元素数量 不一定等于elementData的长度 一般小于等于elementData的长度 ,elementData长度会大于等于size

往下看就知道了

ArrayList中的size()方法返回的值就是size

    /**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }

ensureCapacityInternal方法名的英文大致是“确保内部容量”,size表示的是执行添加之前的实际元素个数,并非ArrayList的容量,容量是数组elementData的长度。ensureCapacityInternal该方法通过将现有的元素个数数组的容量比较。看如果需要扩容,则扩容。

下面具体看 ensureCapacityInternal(size + 1);

//如何判断和扩容的
private void ensureCapacityInternal(int minCapacity) {
      //如果目前实际存储数组 是空数组,则最小需要容量为  默认容量(10)和0+1=1之间最大值,也就是10,其中DEFAULT_CAPACITY=10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
      //当第一次插入值时,由于minCapacity 一定大于等于10 而elementData.length是0  所以需要扩容
      //如果不是第一次插入则是size+1  
        modCount++;
        //如果数组(elementData)的长度小于最小需要的容量(minCapacity)就扩容,如果还够用则不扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
    /*
    *增加容量,以确保它至少能容纳
    *由最小容量参数指定的元素数。
    * @param mincapacity所需的最小容量
    */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
//>>位运算,右移动一位。 整体相当于newCapacity =oldCapacity + 0.5 * oldCapacity  即旧elementData数组长度的1.5倍 jdk1.7采用位运算比以前的计算方式更快
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       //jdk1.7这里增加了对元素个数的最大个数判断,jdk1.7以前是没有最大值判断的,MAX_ARRAY_SIZE 为int最大值减去8  最大的容量之所以设为Integer.MAX_VALUE - 8,在定义上方的注释中已经说了,大概是一些JVM实现时,会在数组的前面放一些额外的数据,再加上数组中的数据大小,有可能超过一次能申请的整块内存的大小上限,出现OutOfMemoryError。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 最重要的复制元素方法  把原来数组的数据复制到新的数组中。调用了Arrays的copyOf方法。内部是System的arraycopy方法,由于是native方法,所以效率较高。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

如果新的容量大于数组的最大值MAX_ARRAY_SIZE

这时,又会进入另一个方法hugeCapacity

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

会对minCapacity 和 MAX_ARRAY_SIZE进行比较,minCapacity 大的话,就将Integer.MAX_VALUE 作为新数组的大小,否则将MAX_ARRAY_SIZE作为数组的大小。

通过分析可以发现,ArrayList的扩容会产生一个新的数组,将原来数组的值复制到新的数组中。会消耗一定的资源。所以我们初始化ArrayList时,最好可以估算一个初始的大小。 而且就算你之后往ArrayList中添加了超过数量的元素也不会报错,会自动扩容

经过打印输出,我们可以看到

向数组中添加第一个元素时,数组容量为10.

向数组中添加到第10个元素时,数组容量仍为10.

向数组中添加到第11个元素时,数组容量扩为15.

向数组中添加到第16个元素时,数组容量扩为22.

对比执行时间要使用控制变量法,先后执行所使用的时间会有不同。

如:

public class Test {
    private static void test(int n) {
        long start = System.currentTimeMillis();
        List<Integer> list = new ArrayList<Integer>(n);
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }
    private static void test0(int n) {
        long start = System.currentTimeMillis();
        List<Integer> list2 = new ArrayList<Integer>();
        for (int i = 0; i < n; i++) {
            list2.add(i);
        }
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }
     public static void main(String[] args) {
             test(10000000);
             test0(10000000);
        }
}

输出 :

2412
3898

明显可以看出初始化长度比没有初始化长度节省时间。

3.3 add(int index, E element)将指定的元素插入此列表中的指定位置

public void add(int index, E element) {  
    if (index > size || index < 0)  
        throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
    // 如果数组长度不足,将进行扩容。  
    ensureCapacity(size+1);  // Increments modCount!!  
    // 将 elementData中从Index位置开始、长度为size-index的元素,  
    // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
    // 即 将当前位于该位置的元素以及所有后续元素右移一个位置。  
    System.arraycopy(elementData, index, elementData, index + 1, size - index);  
    elementData[index] = element;  
    size++;  
}

System.arraycopy

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

参数注释

  • @param src the source array. 源数组
  • @param srcPos starting position in the source array. 源数组的位置
  • @param dest the destination array.目标数组
  • @param destPos starting position in the destination data.目标数组的位置
  • @param length the number of array elements to be copied. 数量

4.如何读取元素?

  public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

5.删除元素

 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 remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
          //将当前位于该位置的元素以及所有后续元素左移一个位置。
          //将index后边的对象往前复制一位,并将数组中的最后一位元素设置为null 不置为null的话最后2个元素值是一模一样的
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // clear to let GC do its work 让GC来回收 
    }

6.手动调整底层数组的容量为列表实际元素大小的方法

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

另外有个高频率出现的属性但是我们没有提过它就是modCount ,它是当前这个ArrayList被修改的次数

JDK注释

    /**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://hogwartsrico.github.io/2015/04/08/About-ArrayList/