');}

ArrayList常用方法和实现原理

Author Avatar
Rico 4月 08, 2015

ArrayList常用方法

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

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

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

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

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

结果为:

  1. [AA, BB, DD]
  2. [AA, BB, CC, DD]

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

  1. public static void main(String[] args) {
  2. List<String> list = new ArrayList<String>();
  3. list.add("AA");
  4. list.add("BB");
  5. list.add("DD");
  6. List<String> list2 = new ArrayList<String>();
  7. list2.add("CC");
  8. list2.add("EE");
  9. list.addAll(list2);
  10. System.out.println(list);
  11. }

结果为:

  1. [AA, BB, DD, CC, EE]

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

  1. public static void main(String[] args) {
  2. List<String> list = new ArrayList<String>();
  3. list.add("AA");
  4. list.add("BB");
  5. list.add("DD");
  6. List<String> list2 = new ArrayList<String>();
  7. list2.add("CC");
  8. list2.add("EE");
  9. list.addAll(2, list2);
  10. System.out.println(list);
  11. }

结果为:

  1. [AA, BB, CC, EE, DD]

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

  1. list.clear();

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

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

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

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

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

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

  1. public static void main(String[] args) {
  2. List<String> list = new ArrayList<String>();
  3. list.add("AA");
  4. list.add("BB");
  5. list.add("BB");
  6. int indexOf = list.indexOf("BB");
  7. System.out.println(indexOf);
  8. }

简单的方法就查询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.底层使用数组实现

  1. private transient Object[] elementData;

关于关键字transient:

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

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

2.构造方法:

  1. /**
  2. * Constructs an empty list with an initial capacity of ten.
  3. */
  4. public ArrayList() {
  5. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  6. }

其中

  1. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

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

  1. public ArrayList() {
  2. this(10);
  3. }
  4. public ArrayList(int initialCapacity) {//可以指定初始大小
  5. super();
  6. if (initialCapacity < 0)
  7. throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
  8. this.elementData = new Object[initialCapacity];
  9. }
  10. public ArrayList(Collection<? extends E> c) {
  11. elementData = c.toArray();
  12. size = elementData.length;
  13. // c.toArray might (incorrectly) not return Object[] (see 6260652)
  14. if (elementData.getClass() != Object[].class)
  15. elementData = Arrays.copyOf(elementData, size, Object[].class);
  16. }

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

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

  1. public E set(int index, E element) {
  2. rangeCheck(index);
  3. E oldValue = elementData(index);
  4. elementData[index] = element;
  5. return oldValue;
  6. }

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

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

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

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

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

往下看就知道了

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

  1. /**
  2. * Returns the number of elements in this list.
  3. *
  4. * @return the number of elements in this list
  5. */
  6. public int size() {
  7. return size;
  8. }

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

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

  1. //如何判断和扩容的
  2. private void ensureCapacityInternal(int minCapacity) {
  3. //如果目前实际存储数组 是空数组,则最小需要容量为 默认容量(10)和0+1=1之间最大值,也就是10,其中DEFAULT_CAPACITY=10
  4. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  5. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  6. }
  7. ensureExplicitCapacity(minCapacity);
  8. }
  9. private void ensureExplicitCapacity(int minCapacity) {
  10. //当第一次插入值时,由于minCapacity 一定大于等于10 而elementData.length是0 所以需要扩容
  11. //如果不是第一次插入则是size+1
  12. modCount++;
  13. //如果数组(elementData)的长度小于最小需要的容量(minCapacity)就扩容,如果还够用则不扩容
  14. if (minCapacity - elementData.length > 0)
  15. grow(minCapacity);
  16. }
  17. /**
  18. * Default initial capacity.
  19. */
  20. private static final int DEFAULT_CAPACITY = 10;
  1. /*
  2. *增加容量,以确保它至少能容纳
  3. *由最小容量参数指定的元素数。
  4. * @param mincapacity所需的最小容量
  5. */
  6. private void grow(int minCapacity) {
  7. // overflow-conscious code
  8. int oldCapacity = elementData.length;
  9. //>>位运算,右移动一位。 整体相当于newCapacity =oldCapacity + 0.5 * oldCapacity 即旧elementData数组长度的1.5倍 jdk1.7采用位运算比以前的计算方式更快
  10. int newCapacity = oldCapacity + (oldCapacity >> 1);
  11. if (newCapacity - minCapacity < 0)
  12. newCapacity = minCapacity;
  13. //jdk1.7这里增加了对元素个数的最大个数判断,jdk1.7以前是没有最大值判断的,MAX_ARRAY_SIZE 为int最大值减去8 最大的容量之所以设为Integer.MAX_VALUE - 8,在定义上方的注释中已经说了,大概是一些JVM实现时,会在数组的前面放一些额外的数据,再加上数组中的数据大小,有可能超过一次能申请的整块内存的大小上限,出现OutOfMemoryError。
  14. if (newCapacity - MAX_ARRAY_SIZE > 0)
  15. newCapacity = hugeCapacity(minCapacity);
  16. // 最重要的复制元素方法 把原来数组的数据复制到新的数组中。调用了Arrays的copyOf方法。内部是System的arraycopy方法,由于是native方法,所以效率较高。
  17. elementData = Arrays.copyOf(elementData, newCapacity);
  18. }

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

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

  1. private static int hugeCapacity(int minCapacity) {
  2. if (minCapacity < 0) // overflow
  3. throw new OutOfMemoryError();
  4. return (minCapacity > MAX_ARRAY_SIZE) ?
  5. Integer.MAX_VALUE :
  6. MAX_ARRAY_SIZE;
  7. }

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

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

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

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

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

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

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

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

如:

  1. public class Test {
  2. private static void test(int n) {
  3. long start = System.currentTimeMillis();
  4. List<Integer> list = new ArrayList<Integer>(n);
  5. for (int i = 0; i < n; i++) {
  6. list.add(i);
  7. }
  8. long end = System.currentTimeMillis();
  9. System.out.println(end-start);
  10. }
  11. private static void test0(int n) {
  12. long start = System.currentTimeMillis();
  13. List<Integer> list2 = new ArrayList<Integer>();
  14. for (int i = 0; i < n; i++) {
  15. list2.add(i);
  16. }
  17. long end = System.currentTimeMillis();
  18. System.out.println(end-start);
  19. }
  20. public static void main(String[] args) {
  21. test(10000000);
  22. test0(10000000);
  23. }
  24. }

输出 :

2412
3898

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

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

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

System.arraycopy

  1. 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.如何读取元素?

  1. public E get(int index) {
  2. rangeCheck(index);
  3. return elementData(index);
  4. }

5.删除元素

  1. public boolean remove(Object o) {
  2. if (o == null) {
  3. for (int index = 0; index < size; index++)
  4. if (elementData[index] == null) {
  5. fastRemove(index);
  6. return true;
  7. }
  8. } else {
  9. for (int index = 0; index < size; index++)
  10. if (o.equals(elementData[index])) {
  11. fastRemove(index);
  12. return true;
  13. }
  14. }
  15. return false;
  16. }

注意:从集合中移除一个元素,如果这个元素不是最后一个元素,那么这个元素的后面元素会向向左移动一位。

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

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

  1. public void trimToSize() {
  2. modCount++;
  3. int oldCapacity = elementData.length;
  4. if (size < oldCapacity) {
  5. elementData = Arrays.copyOf(elementData, size);
  6. }
  7. }

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

JDK注释

  1. /**
  2. * The number of times this list has been <i>structurally modified</i>.
  3. * Structural modifications are those that change the size of the
  4. * list, or otherwise perturb it in such a fashion that iterations in
  5. * progress may yield incorrect results.
  6. *
  7. * <p>This field is used by the iterator and list iterator implementation
  8. * returned by the {@code iterator} and {@code listIterator} methods.
  9. * If the value of this field changes unexpectedly, the iterator (or list
  10. * iterator) will throw a {@code ConcurrentModificationException} in
  11. * response to the {@code next}, {@code remove}, {@code previous},
  12. * {@code set} or {@code add} operations. This provides
  13. * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
  14. * the face of concurrent modification during iteration.
  15. *
  16. * <p><b>Use of this field by subclasses is optional.</b> If a subclass
  17. * wishes to provide fail-fast iterators (and list iterators), then it
  18. * merely has to increment this field in its {@code add(int, E)} and
  19. * {@code remove(int)} methods (and any other methods that it overrides
  20. * that result in structural modifications to the list). A single call to
  21. * {@code add(int, E)} or {@code remove(int)} must add no more than
  22. * one to this field, or the iterators (and list iterators) will throw
  23. * bogus {@code ConcurrentModificationExceptions}. If an implementation
  24. * does not wish to provide fail-fast iterators, this field may be
  25. * ignored.
  26. */
  27. 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/