ArrayList源码解析(jdk1.6)
2017-07-03 21:02
591 查看
一、定义
ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList类,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
要点:
1、继承于AbstractList类,实现了List接口
2、实现RandomAccess接口,提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能。
3、ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList(系列源码后续讲到)。
二、属性
1、elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。可进行动态扩容
2、有个关键字需要解释:transient。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。简单理解为就是:序列化该对象时,某个属性不想被序列化,可加transient关键字避免。
3、注意size为动态数组的实际大小。
三、构造函数
第一个构造方法使用提供的initialCapacity来初始化elementData数组的大小。
第二个构造方法调用第一个构造方法并传入参数10,即默认elementData数组的大小为10。
第三个构造方法则将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[]),也很常用。
四、常见API
1、添加
在讲添加元素之前,我们先来看一个函数:
当进行添加元素时,需要进行是否扩容判断。当增加modCount之后,判断minCapacity(即size+1)是否大于oldCapacity(即elementData.length),若大于,则调整容量为max((oldCapacity*3)/2+1,minCapacity)(将一个集合插入到list会出现后者,比如addAll),调整elementData容量为新的容量,即将返回一个内容为原数组元素,大小为新容量的数组赋给elementData;否则不做操作。
注意:当需要扩容时,jdk1.6容量为max((oldCapacity*3)/2+1,minCapacity);
jdk1.7容量为max(oldCapacity + (oldCapacity >> 1),minCapacity)
。
关于添加的四个函数:
删除操作常见API如下:
3、查询/更新太简单,就不贴上了。
4、是否包含:
此类的
注意,迭代器的快速失败行为无法得到保证,快速失败迭代器会尽最大努力抛出
前面说到
举个例子:移除ArrayList某一个指定元素:
如果要想删除list的b字符,有下面两种常见的错误例子:
错误写法实例一:
错误的原因:这种最普通的循环写法执行后会发现第二个“b”的字符串没有删掉。
错误写法实例二:
错误的原因:这种for-each写法会报出著名的并发修改异常:java.util.ConcurrentModificationException。
先解释一下实例一的错误原因。翻开JDK的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的:
因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
接着解释一下实例二的错误原因。错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):
总结:
在需要扩容的时候,调用Arrays.copyOf(),这个是创建新数组,底层也是System.arraycopy()
其他操作时用的是System.arraycopy(),直接在原数组上操作。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList类,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
要点:
1、继承于AbstractList类,实现了List接口
2、实现RandomAccess接口,提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能。
3、ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList(系列源码后续讲到)。
二、属性
// 保存ArrayList中数据的数组 private transient Object[] elementData; // ArrayList中实际数据的数量 private int size;要点:
1、elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。可进行动态扩容
2、有个关键字需要解释:transient。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。简单理解为就是:序列化该对象时,某个属性不想被序列化,可加transient关键字避免。
3、注意size为动态数组的实际大小。
三、构造函数
// ArrayList带容量大小的构造函数。 public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); // 新建一个数组 this.elementData = new Object[initialCapacity]; } // ArrayList构造函数。默认容量是10。 public ArrayList() { this(10); } // 构造一个包含指定元素的list,这些元素的是按照Collection的迭代器返回的顺序排列的 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }
第一个构造方法使用提供的initialCapacity来初始化elementData数组的大小。
第二个构造方法调用第一个构造方法并传入参数10,即默认elementData数组的大小为10。
第三个构造方法则将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[]),也很常用。
四、常见API
1、添加
在讲添加元素之前,我们先来看一个函数:
/** * 数组容量检查,不够时则进行扩容 */ public void ensureCapacity( int minCapacity) { //对于modCount变量,我们后面会给出解释 modCount++; // 当前数组的长度 int oldCapacity = elementData .length; // 最小需要的容量大于当前数组的长度则进行扩容 if (minCapacity > oldCapacity) { Object oldData[] = elementData; // 新扩容的数组长度为旧容量的1.5倍+1 int newCapacity = (oldCapacity * 3)/2 + 1; // 如果新扩容的数组长度还是比最小需要的容量小,则以最小需要的容量为长度进行扩容 if (newCapacity < minCapacity) newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win: // 进行数据拷贝,Arrays.copyOf底层实现是System.arrayCopy() elementData = Arrays.copyOf( elementData, newCapacity); } }
当进行添加元素时,需要进行是否扩容判断。当增加modCount之后,判断minCapacity(即size+1)是否大于oldCapacity(即elementData.length),若大于,则调整容量为max((oldCapacity*3)/2+1,minCapacity)(将一个集合插入到list会出现后者,比如addAll),调整elementData容量为新的容量,即将返回一个内容为原数组元素,大小为新容量的数组赋给elementData;否则不做操作。
注意:当需要扩容时,jdk1.6容量为max((oldCapacity*3)/2+1,minCapacity);
jdk1.7容量为max(oldCapacity + (oldCapacity >> 1),minCapacity)
。
关于添加的四个函数:
/** * 添加一个元素 */ public boolean add(E e) { // 进行扩容检查 ensureCapacity( size + 1); // Increments modCount // 将e增加至list的数据尾部,容量+1 elementData[size ++] = e; return true; } /** * 在指定位置添加一个元素 */ public void add(int index, E element) { // 判断索引是否越界,这里会抛出多么熟悉的异常。。。 if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: "+index+", Size: " +size); // 进行扩容检查 ensureCapacity( size+1); // Increments modCount // 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置,共有 //size-index个元素需要进行移动 System. arraycopy(elementData, index, elementData, index + 1, size - index); // 将指定的index位置赋值为element elementData[index] = element; // list容量+1 size++; } /** * 增加一个集合元素 */ public boolean addAll(Collection<? extends E> c) { //将c转换为数组 Object[] a = c.toArray(); int numNew = a.length ; //扩容检查 ensureCapacity( size + numNew); // Increments modCount //将c添加至list的数据尾部 System. arraycopy(a, 0, elementData, size, numNew); //更新当前容器大小 size += numNew; return numNew != 0; } /** * 在指定位置,增加一个集合元素 */ public boolean addAll(int index, Collection<? extends E> c) { if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size); Object[] a = c.toArray(); int numNew = a.length ; ensureCapacity( size + numNew); // Increments modCount // 计算需要移动的长度(index之后的元素个数) int numMoved = size - index; // 数组复制,空出第index到index+numNum的位置,即将数组index后的元素向右移动numNum个位置 if (numMoved > 0) System. arraycopy(elementData e82f , index, elementData, index + numNew, numMoved); // 将要插入的集合元素复制到数组空出的位置中 System. arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }2、删除
删除操作常见API如下:
/** * 根据索引位置删除元素 */ public E remove( int index) { // 数组越界检查 RangeCheck(index); modCount++; // 取出要删除位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 计算数组要复制的数量 int numMoved = size - index - 1; // 数组复制,就是将index之后的元素往前移动一个位置 if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); // 将数组最后一个元素置空(因为删除了一个元素,然后index后面的元素都向前移动了,所以最后一个就没用了),好让gc尽快回收 // 不要忘了size减一 elementData[--size ] = null; // Let gc do its work return oldValue; } /** * 根据元素内容删除,只删除匹配的第一个 */ public boolean remove(Object o) { // 对要删除的元素进行null判断 // 对数据元素进行遍历查找,知道找到第一个要删除的元素,删除后进行返回,如果要删除的元素正好是最后一个那就惨了,时间复杂度可达O(n) 。。。 if (o == null) { for (int index = 0; index < size; index++) // null值要用==比较 if (elementData [index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) // 非null当然是用equals比较了 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++; // 原理和之前的add一样,还是进行数组复制,将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 } /** * 数组越界检查 */ private void RangeCheck(int index) { if (index >= size ) throw new IndexOutOfBoundsException( "Index: "+index+", Size: " +size); }上面讲增加元素可能会进行扩容,而删除元素却不会进行缩容,如果进行一次大扩容后,我们后续只使用了几个空间或者继续进行删除操作?
/** * 将底层数组的容量调整为当前实际元素的大小,来释放空间。 */ public void trimToSize() { modCount++; // 当前数组的容量 int oldCapacity = elementData .length; // 如果当前实际元素大小 小于 当前数组的容量,则进行缩容 if (size < oldCapacity) { elementData = Arrays.copyOf( elementData, size ); }
3、查询/更新太简单,就不贴上了。
4、是否包含:
public boolean contains(Object o) { return indexOf(o) >= 0; } //找到指定对象的第一个索引位置,从前往后,判断对象是否为null 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; } //与前面相反,从后往前 public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData [i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData [i])) return i; } return -1; }五、Fast-Fail快速失败机制
此类的
iterator和
listIterator方法返回的迭代器是快速失败的:在创建迭代器之后,除了通过迭代器自身的
remove或
add方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出
ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为无法得到保证,快速失败迭代器会尽最大努力抛出
ConcurrentModificationException。迭代器的快速失败行为应该仅用于检测 bug。
前面说到
ArrayList中定义了一个
modCount来记录对容器进行结构修改的次数,在
add、
addAll、
remove、
clear、
clone方法中都会引起
modCount变化,而在创建迭代器时,会使用局部变量保存当前的
modCount值:
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; ...在进行迭代的过程中,会先检查
modCount有没有发生变化,以此来判定是否有外部操作改变了容器:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }最后,因为
ArrayList是非同步的,因此,在多线程环境下,如果有对容器进行结构修改的操作,则必须使用外部同步。
举个例子:移除ArrayList某一个指定元素:
import java.util.ArrayList; public class ArrayListRemove { public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("a"); list.add("b"); list.add("b"); list.add("c"); list.add("c"); list.add("c"); remove(list); for (String s : list) { System.out.println("element : " + s); } } public static void remove(ArrayList<String> list) { // TODO: } }
如果要想删除list的b字符,有下面两种常见的错误例子:
错误写法实例一:
public static void remove(ArrayList<String> list) { for (int i = 0; i < list.size(); i++) { String s = list.get(i); if (s.equals("b")) { list.remove(s); } } }
错误的原因:这种最普通的循环写法执行后会发现第二个“b”的字符串没有删掉。
错误写法实例二:
public static void remove(ArrayList<String> list) { for (String s : list) { if (s.equals("b")) { list.remove(s); } } }
错误的原因:这种for-each写法会报出著名的并发修改异常:java.util.ConcurrentModificationException。
先解释一下实例一的错误原因。翻开JDK的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的:
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; }一般情况下程序的执行路径会走到else路径下最终调用faseRemove方法:
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; // Let gc do its work }可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第一个字符串b时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时后一个字符串b并没有遍历到,所以无法删除。针对这种情况可以倒序删除的方式来避免:
public static void remove(ArrayList<String> list) { for (int i = list.size() - 1; i >= 0; i--) { String s = list.get(i); if (s.equals("b")) { list.remove(s); } } }
因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
接着解释一下实例二的错误原因。错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):
public Iterator<E> iterator() { return new Itr(); }这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法:
public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }第一行checkForComodification方法:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。
public static void remove(ArrayList<String> list) { Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("b")) { it.remove(); } } }
总结:
在需要扩容的时候,调用Arrays.copyOf(),这个是创建新数组,底层也是System.arraycopy()
其他操作时用的是System.arraycopy(),直接在原数组上操作。
相关文章推荐
- ArrayList源码解析 给jdk写注释系列之jdk1.6容器(1)
- 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析
- java ArrayList 源码解析(jdk1.6)
- 给jdk写注释系列之jdk1.6容器(1)-ArrayList源码解析
- 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析
- ArrayList源码解析(基于JDK1.6)
- 给jdk写注释系列之jdk1.6容器(10)-Stack&Vector源码解析
- ArrayList源码分析(基于JDK1.6)
- Jdk1.6 JUC源码解析(7)-locks-ReentrantLock
- LinkedHashMap源码解析 给jdk写注释系列之jdk1.6容器(5)
- 给jdk写注释系列之jdk1.6容器(11)-Queue之ArrayDeque源码解析
- LinkedList源码解析 给jdk写注释系列之jdk1.6容器(2)
- PriorityQueue源码解析 给jdk写注释系列之jdk1.6容器(12)
- (7) java源码分析------之ArrayList (对应数据结构中线性表中的顺序表,JDK1.6)
- HashMap源码解析 给jdk写注释系列之jdk1.6容器(4)
- ArrayList源码分析(基于JDK1.6)
- Java中ArrayList源码深入分析(JDK1.6)
- java动态代理Proxy源码解析(Jdk 1.6)
- 给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器
- TreeMap源码解析 给jdk写注释系列之jdk1.6容器(7)