前言 ArrayList继承了AbstractList,实现了List。ArrayList在工作中经常用到,所以要弄懂这个类是极其重要的。
构造图如下: 蓝色线条:继承 绿色线条:接口实现
ArrayList简介 定义 1 2 public class ArrayList <E > extends AbstractList <E > implements List <E >, RandomAccess , Cloneable , java .io .Serializable
ArrayList 是一个数组队列 ,相当于动态数组 。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandomAccess接口,即提供了随机访问功能。RandomAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。
ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
和Vector不同,ArrayList中的操作不是线程安全的 !所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
ArrayList属性 顾名思义,ArrayList就是用数组实现的List容器,既然是用数组实现,当然底层用数组来保存数据。
1 2 3 4 transient Object[] elementData;private int size;
ArrayList包含了两个重要的对象:elementData 和 size。
elementData 是”Object[]类型的数组”,它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数ArrayList(int initialCapacity)
来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()
来创建ArrayList,则elementData的容量默认是10 。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()
函数;
size 则是动态数组的实际大小。
ArrayList构造函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public ArrayList (int initialCapacity) { if (initialCapacity > 0 ) { this .elementData = new Object[initialCapacity]; } else if (initialCapacity == 0 ) { this .elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } } public ArrayList () { this .elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public ArrayList (Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0 ) { if (elementData.getClass() != Object[].class ) elementData = Arrays.copyOf(elementData, size, Object[].class ) ; } else { this .elementData = EMPTY_ELEMENTDATA; } }
第一个构造方法使用提供的initialCapacity来初始化elementData数组的大小。
第二个构造方法调用第一个构造方法并传入参数10,即默认elementData数组的大小为10。
第三个构造方法则将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[])。
源码解析 增加 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; } public void add (int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1 ); System.arraycopy(elementData, index, elementData, index + 1 , size - index); elementData[index] = element; size++; } public boolean addAll (Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); System.arraycopy(a, 0 , elementData, size, numNew); size += numNew; return numNew != 0 ; } public boolean addAll (int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); int numMoved = size - index; if (numMoved > 0 ) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0 , elementData, index, numNew); size += numNew; return numNew != 0 ; } private void ensureCapacityInternal (int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity (int minCapacity) { modCount++; 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); }
删除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 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 ; } public boolean removeAll (Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false ); } private boolean batchRemove (Collection<?> c, boolean complement) { final Object[] elementData = this .elementData; int r = 0 , w = 0 ; boolean modified = false ; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { for (int i = w; i < size; i++) elementData[i] = null ; modCount += size - w; size = w; modified = true ; } } return modified; } 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 ; }
代码是很简单,主要需要特别关心的就两个地方:
数组扩容
数组复制
这两个操作都是极费效率的,最惨的情况下(添加到list第一个位置,删除list最后一个元素或删除list第一个索引位置的元素)时间复杂度可达O(n)。
上面讲增加元素可能会进行扩容,而删除元素却不会进行缩容,如果在已删除为主的场景下使用list,一直不停的删除而很少进行增加,那么会出现什么情况?再或者数组进行一次大扩容后,我们后续只使用了几个空间,如何处理这种情况呢?
1 2 3 4 5 6 7 8 9 10 11 12 public void trimToSize () { modCount++; if (size < elementData.length) { elementData = (size == 0 ) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
size和length是不同的:size是私有变量,只能通过size()来访问,返回当前数组的实际元素大小;length是数组的属性,返回当前数组的容量。
关于ArrayList删除元素的坑 关于ArrayList的remove方法,在使用中经常会遇到坑,这也是面试中经常会问的点 —— 输出是什么?为什么会这样?我觉得有必要把它单独拿出来说明一下。
我们经常遇到的需求是:当集合中的某些元素符合一定的条件时,需要删除这个元素,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ListTest { public static void main (String[] args) { List<Integer> intList = new ArrayList<Integer>(); Collections.addAll(intList, 1 , 2 , 3 , 5 , 6 ); for (int i = 0 , size = intList.size(); i < size; i++) { Integer value = intList.get(i); if (value == 3 || value == 5 ) { intList.remove(i); } } System.out.println(intList); } }
在执行之后,会抛出IndexOutOfBoundsException,因为ArrayList删除掉符合条件的元素后,长度动态发生了改变,由于长度只获取了一次,导致越界。
去掉for循环优化的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ListTest { public static void main (String[] args) { List<Integer> intList = new ArrayList<Integer>(); Collections.addAll(intList, 1 , 2 , 3 , 5 , 6 ); for (int i = 0 ; i < intList.size(); i++) { Integer value = intList.get(i); if (value == 3 || value == 5 ) { intList.remove(i); } } System.out.println(intList); } }
输出:[1, 2, 5, 6]。
因为在i == 2时,值为3,删除后,后面的元素往前补一位,所以跳过了5。
使用foreach:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ListTest { public static void main (String[] args) { List<Integer> intList = new ArrayList<Integer>(); Collections.addAll(intList, 1 , 2 , 3 , 5 , 6 ); for (Integer value : intList) { if (value == 3 || value == 5 ) { intList.remove(value); } } System.out.println(intList); } }
执行后,会抛出ConcurrentModificationException,意思是并发修改异常,异常追踪信息如下:
1 2 3 4 Exception inthread "main" java.util.ConcurrentModificationException atjava.util.AbstractList$Itr.checkForComodification(AbstractList.java:449) at java.util.AbstractList$Itr.next(AbstractList.java:420) at ListTest.main(ListTest.java:13)
大概看出是执行到AbstractList中内部类的checkForComodification方法抛出的异常。集合遍历是使用Iterator,Iterator是工作在一个独立的线程中,并且拥有一个互斥锁。Iterator被创建之后会建立一个指向原来对象的单链索引表 ,当原来的对象数量发生变化时,这个索引表的内容不会同步改变 ,所以当索引指针往后移动的时候就找不到要迭代的对象。
AbstractList$Itr.checkForComodification:
1 2 3 4 final void checkForComodification () { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
所以记住一点:Iterator在工作的时候是不允许被迭代的对象被改变的 。
那么正确删除ArrayList元素的姿势是什么呢?这里有三种方法:
使用Iterator的remove方法 该方法会删除当前迭代对象的同时,维护索引的一致性。
Iterator的remove方法源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void remove () { if (lastRet < 0 ) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this .remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1 ; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } }
使用Iterator删除元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ListTest { public static void main (String[] args) { List<Integer> intList = new ArrayList<Integer>(); Collections.addAll(intList, 1 , 2 , 3 , 5 , 6 ); Iterator<Integer> it = intList.iterator(); while (it.hasNext()) { Integer value = it.next(); if (value == 3 || value == 5 ) { it.remove(); } } System.out.println(intList); } }
输出:[1, 2, 6]。
自己维护索引 即,删除元素后,索引-1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ListTest { public static void main (String[] args) { List<Integer> intList = new ArrayList<Integer>(); Collections.addAll(intList, 1 , 2 , 3 , 5 , 6 ); for (int i = 0 ; i < intList.size(); i++) { Integer value = intList.get(i); if (value == 3 || value == 5 ) { intList.remove(i); i--; } } System.out.println(intList); } }
输出:[1, 2, 6]。
从后向前遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 public class ListTest { public static void main (String[] args) { List<Integer> intList = new ArrayList<Integer>(); Collections.addAll(intList, 1 , 2 , 3 , 5 , 6 ); for (int i = intList.size() - 1 ; i >= 0 ; i--) { Integer value = intList.get(i); if (value == 3 || value == 5 ) { intList.remove(i); } } System.out.println(intList); } }
输出:[1, 2, 6]。
更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public E set (int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
查找 1 2 3 4 5 public E get (int index) { rangeCheck(index); return (E) elementData[index]; }
总结
ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。
当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“原始容量 * 1.5”。
ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
ArrayList遍历方式 ArrayList支持3种遍历方式:
迭代器遍历 1 2 3 4 5 Integer value = null ; Iterator iter = list.iterator(); while (iter.hasNext()) { value = (Integer)iter.next(); }
随机访问 即通过索引值去遍历,由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
1 2 3 4 5 Integer value = null ; int size = list.size();for (int i = 0 ; i < size; i++) { value = (Integer)list.get(i); }
for-each遍历 1 2 3 4 Integer value = null ; for (Integer i : list) { value = i; }
遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低。
总结 ArrayList和LinkedList的区别
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
ArrayList和Vector的区别
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。
Vector还有一个子类Stack.
附:集合框架思维导图