ArrayList的实现细节(基于JDK1.8)
ArrayList是我们经常用到的一个类,下面总结一下它内部的实现细节和使用时要注意的地方。
基本概念
ArrayList在数据结构的层面上讲,是一个用数组实现的list,从应用层面上讲,就是一个容量会自己改变的数组,具有一系列方便的add、set、get、remove等方法,线程不安全。先上张类图吧。
ArrayList的容量
ArrayList有两个数据域与之相关。
1 transient Object[] elementData; // non-private to simplify nested class access 2 3 private int size;
很明显,size表示ArrayList中包含的元素数量,也就是size()方法的返回值,而elementData.length则是ArrayList的容量,表示在不扩容的情况下能存储多少个元素。By the way,JDK1.8的ArrayList的初始容量是0,之前的版本貌似10。
ArrayList还有一些关于扩大容量和缩小容量的方法
1 /** 2 * 当ArrayList中有空闲的空间时,缩减ArrayList的容量。应用程序可以使用这个方法最小化ArrayList实例 3 */ 4 public void trimToSize() 5 6 /** 7 * public修饰,供应用程序调用的扩容方法,内部调用ensureExplicitCapacity()方法 8 */ 9 public void ensureCapacity(int minCapacity) 10 11 /** 12 * private修饰,供ArrayList内部使用的扩容方法,内部同样是调用ensureExplicitCapacity()方法 13 */ 14 private void ensureCapacityInternal(int minCapacity) 15 16 /** 17 * 内部调用grow()方法 18 */ 19 private void ensureExplicitCapacity(int minCapacity) 20 21 /** 22 * grow()方法内部会做一个判断,如果ArrayList扩大1.5倍还不够的话,才会增加到minCapacity 23 * 这是为了防止扩容太小而导致多次扩容多次改变数组大小,从而影响性能。 24 * 比如说,我有一个装满了的ArrayList,现在我往其中加入10个元素,自然是要扩容的, 25 * 那么我是一次性扩容增加10个容量,还是每次add前扩容增加一个容量呢,答案可想而知。 26 */ 27 private void grow(int minCapacity) 28 29 /** 30 * 对ArrayList扩容的一个限制,扩得太大会抛出OutOfMemoryError 31 */ 32 private static int hugeCapacity(int minCapacity)
虽说的容量会随着数据量的增大而增大,使用时不用费心于容量的维护,不过在可以预估数据量的情况下,务必使用public ArrayList(int initialCapacity)来指定初始容量,这样的话,一来减少扩容方法的调用避免数组频繁更改,二来在一定程度上减少了内存的消耗(比如我就存5000个元素,当数组达到4000时扩容扩大1.5倍变成6000,白白耗费了1000个单位的内存)。经过测试,这是可以大大提高运行效率的。
Clone
ArrayList的clone()方法是浅复制,在这里直接上段demo。
1 public class Main { 2 public static void main(String[] args) { 3 User u1 = new User(); 4 u1.setUsername("qwe"); 5 u1.setPassword("qwePASSWORD"); 6 User u2 = new User(); 7 u2.setUsername("asd"); 8 u2.setPassword("asdPassword"); 9 ArrayList<User> list1 = new ArrayList<>(); 10 list1.add(u1); 11 list1.add(u2); 12 ArrayList<User> list2 = (ArrayList<User>) list1.clone(); 13 list2.get(0).setUsername("zxc"); //修改u1的username 14 list2.get(0).setPassword("zxcPassword"); ////修改u1的password 15 System.out.println(list1); //[User [username=zxc, password=zxcPassword], User //[username=asd, password=asdPassword]] 16 } 17 /** 18 * 实现深复制 19 */ 20 private static List<User> deepClone(List<User> from) throws CloneNotSupportedException { 21 List<User> list = new ArrayList<>(); 22 for(User item : from) { 23 list.add((User)item.clone()); 24 } 25 return list; 26 } 27 } 28 29 class User { 30 private String username; 31 private String password; 32 public User() { 33 } 34 public String getUsername() { 35 return username; 36 } 37 public void setUsername(String username) { 38 this.username = username; 39 } 40 public String getPassword() { 41 return password; 42 } 43 public void setPassword(String password) { 44 this.password = password; 45 } 46 @Override 47 public String toString() { 48 return "User [username=" + username + ", password=" + password + "]"; 49 } 50 }
有输出可知,list2中的u1就是list1中的u1,二者的引用指向了同一个User对象,具体见示意图。所以要想实现ArrayList的深复制得根据场景自己写。
public Object[] toArray()和public T[] toArray(T[] a)
1 /** 2 * 获得一个Object数组,这个方法会分配一个新数组(并不是单纯的return elementData;),所以调用者可以安全的修改数组而不影响ArrayList 3 */ 4 public Object[] toArray() 5 6 /** 7 * 获得一个泛型数组 8 */ 9 public <T> T[] toArray(T[] a) { 10 if (a.length < size) //数组a长度不足,则重新new一个数组 11 // Make a new array of a's runtime type, but my contents: 12 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); 13 System.arraycopy(elementData, 0, a, 0, size); //数组a长度足够,就将元素复制到a数组中,而后返回a 14 if (a.length > size) 15 a[size] = null; 16 return a; 17 }
This method acts as bridge between array-based and collection-based APIs.这是文档注释中的一句话,大意是这个方法是数组和集合之间的桥梁。通过函数签名,我们可以得知toArray()返回一个Object数组,toArray(T[] a)返回一个泛型数组。我们往往使用的是toArray(T[] a),常见的使用方式如下
1 List<Integer> list = new ArrayList<>(); 2 Collections.addAll(list,1,2,3,4,5,6); 3 // 方式1 // 4 list.toArray(new Integer[0]); //涉及到反射,效率较低 5 // 方式2 // 6 list.toArray(new Integer[list.size()])
构造函数:public ArrayList(Collection c)
1 public ArrayList(Collection<? extends E> c) { 2 elementData = c.toArray(); 3 if ((size = elementData.length) != 0) { 4 // c.toArray might (incorrectly) not return Object[] (see 6260652) 5 if (elementData.getClass() != Object[].class) 6 elementData = Arrays.copyOf(elementData, size, Object[].class); 7 } else { 8 // replace with empty array. 9 this.elementData = EMPTY_ELEMENTDATA; 10 } 11 }
利用这个构造方法,我们可以方便的使用其他容器来构造一个ArrayList。这里有一个要点,通过源码我们得知,当elementData不是Object数组时,它会使用Arrays.copyOf()方法构造一个Object数组替换elementData,为什么要这么做呢,Object[] objArr = new String[5];之类的代码完全不会报错啊。我们先看一段代码,理解Java数组的一个特性,Java数组要求其存储的元素必须是new数组时的实际类型的实例。
1 Object[] objArr = new String[5]; 2 objArr[0] = "qwe"; 3 objArr[1] = new Object(); //java.lang.ArrayStoreException 4 System.out.println(Arrays.toString(objArr));
数组objArr的实际类型是String数组,所以它只能存储Stirng类型的对象实例(String没有子类),不然就抛出异常。
理解了ArrayStoreException,我们再回到ArrayList。假设在使用上面那个构造函数时,不转换成Object数组类型,当我们使用toArray()方法时就会出问题了,正如注释所说:c.toArray
might (incorrectly) not return
Object[]。使用toArray()方法获得一个Object数组,直观意思就是可以往里面加任何类型的实例啊,但是如果不在上面那个构造函数中特殊处理,是会抛java.lang.ArrayStoreException。这就是为什么ArrayList要对非Object数组特殊处理:为了toArray()返回的Object数组能够正常使用。
List list = new ArrayList(new StringCollection()); //假设StringCollection集合内部是一个String数组 Object[] arr = list.toArray(); // 由于构造函数转换了数组类型,所以这个arr数组可以正常使用,真是nice啊 System.out.println(arr.getClass());// class [Ljava.lang.Object;返回的是Object数组 arr[0] = ""; arr[0] = 123; arr[0] = new Object();
fail-fast:快速失败
fail-fast是指在多线程环境下,比如一个线程在读(这里仅考虑迭代器迭代),一个线程在写的情况下容易出现匪夷所思的bug,为了更好的调试,采用了快速失败机制,一旦发现异步修改,马上抛异常而不是继续迭代下去。当然,ArrayListd的实现更加严格,在单线程环境下作死的话也会抛出异常。
1 List<Integer> list = new ArrayList<Integer>(); 2 Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7); 3 Iterator<Integer> iterator = list.iterator(); 4 list.add(8); //修改了ArrayList 5 while(iterator.hasNext()) { 6 System.out.println(iterator.next()); //java.util.ConcurrentModificationException 7 }
下面再简单讲几句ArrayList实现快速失败的机制。ArrayList的快速失败是围绕着迭代器的,所以定位到迭代器的源码。获得一个迭代器后, expectedModCount值就确定了,可是modCount可能会改变(trimToSize()、ensureExplicitCapacity()、remove()、clear()等等都会修改modCount)。往后使用迭代器的过程中,一旦expectedModCount不等于modCount,就认为迭代的结果有问题,不管三七二十一就抛出ConcurrentModificationException。
1 private class Itr implements Iterator<E> { 2 /** 3 * 每构造一个迭代器都会记录当前的modCount,modCount之后有可能会改变 4 */ 5 int expectedModCount = modCount; 6 /** 7 * 当modCount不等于expectedModCount就抛出ConcurrentModificationException 8 */ 9 final void checkForComodification() { 10 if (modCount != expectedModCount) 11 throw new ConcurrentModificationException(); 12 } 13 }
务必理解文档注释中的一段话。
1 he iterators returned by this class's iterator and listIterator methods are fail-fast: 2 if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. 3 Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. 4 5 快速失败是指:迭代器被创建后,list发生了结构型的变化(除了使用迭代器自己的add或者remove操作),迭代器使用时会抛出ConcurrentModificationException。 6 该类的iterator和listIterator都是快速失败的。 7 因此,面对并发修改,迭代器将快速的抛出异常终止迭代,而不是冒着风险在非确定的未来进行非确定性行为。
ArrayList的序列化机制
通过UML图,我们知道ArrayList实现了Serializable接口,通过源码,我们又知道ArrayList的序列化机制、反序列化机制是自定义的。
/** * 自定义序列化机制 */ private void writeObject(java.io.ObjectOutputStream s) /** * 自定义反序列化机制 */ private void readObject(java.io.ObjectInputStream s)
那么为什么要自定义序列化、反序列化机制呢?是由于ArrayList实质上是一个动态数组,往往数组中会有空余的空间,如果采用默认的序列化机制,那些空余的空间会作为null写入本地文件或者在网络中传输,耗费了不必要的资源。所以,ArrayList使用自定义序列化机制,仅写入索引为【0,size)的有效元素以节省资源。
ArrayList的遍历
ArrayList的遍历方式有三种:foreach语法糖、普通for循环,迭代器。其中foreach相当于使用迭代器遍历,而是用迭代器时会有个迭代器对象的开销,所以一般情况下普通的for循环遍历效率更高。
1 ArrayList<Integer> list = new ArrayList<>(); 2 Collections.addAll(list,1,2,3,4,5,6,7); 3 int len = list.size(); //避免重复调用list.size()方法 4 for(int i=0;i<len;i++) { 5 System.out.print(list.get(i)); //随机访问 6 }
RandomAccess接口
RandomAccess是一个标记接口,用于标记当前类是可以随机访问的,有什么用?我们先看看JDK中一个典型的应用场景。
1 /** 2 * Collections.fill() 3 */ 4 public static <T> void fill(List<? super T> list, T obj) { 5 int size = list.size(); 6 if (size < FILL_THRESHOLD || list instanceof RandomAccess) { 7 for (int i=0; i<size; i++) 8 list.set(i, obj); 9 } else { 10 ListIterator<? super T> itr = list.listIterator(); 11 for (int i=0; i<size; i++) { 12 itr.next(); 13 itr.set(obj); 14 } 15 } 16 }
上面这段代码,大概的业务逻辑是指当list是RandomAccess的实例时,便用普通的for循环遍历,如果不是RandomAccess实例时,则用迭代器遍历。
前面一点已经讲了,对于ArrayList,普通的for循环遍历效率比用迭代器遍历效率高。现在拓展这一点:当一个类标记了RandomAccess接口,那么表明该类使用for循环遍历效率更高,如果没用RandomAccess标记,则使用迭代器遍历效率更高。平时我们可以模仿Collections.fill(),使用这个特性写出更美好的代码。
另外,如果使用普通的for循环遍历非RandomAccess的实例,效率是很低的,比如LinkedList(实质是一个双向链表),每次get一个元素都要遍历半个链表,所以要格外注意。
System.arraycopy()方法
记得刚学数据结构时,删除一个元素,添加一个元素是这么写的。
/** * 在第索引{@param i}处插入元素{@param item} */ @Override public void add(int i, T item) { // 参数校验 // if (i < 0 || i > size) { throw new IllegalArgumentException(String.format("i=%d,无效索引值", i)); } // 插入元素 // for (int p = size; p > i; p--) { // 移动数组 arr[p] = arr[p - 1]; } arr[i] = item; size++; } /** * 删除索引{@param i}处的元素 */ @Override public T remove(int i) { // 参数校验 // if (i < 0 || i >= size) { throw new IllegalArgumentException(String.format("i=%d,无效索引值", i)); } // 移除节点 // T item = arr[i]; for (int p = i; p < size - 1; p++) { // 移动数组 arr[p] = arr[p + 1]; } arr[--size] = null; return item; }
不论添加、删除,因为移动数组,所以得用for循环来移动,而且循环的边界条件很难掌握很容易写错,而ArrayList使用了System.arraycopy()来简化的这一切。掌握了这个,平时我们也可以使用System.arraycopy()来编写代码了!
1 public void add(int index, E element) { 2 rangeCheckForAdd(index); //检查index有没有越界 3 ensureCapacityInternal(size + 1); // Increments modCount!! 4 System.arraycopy(elementData, index, elementData, index + 1, 5 size - index); //将elementData位于index之后的元素全部向后移一位 6 elementData[index] = element; 7 size++; 8 } 9 10 public E remove(int index) { 11 rangeCheck(index);//检查index有没有越界 12 modCount++; 13 E oldValue = elementData(index); 14 int numMoved = size - index - 1; 15 if (numMoved > 0) 16 System.arraycopy(elementData, index+1, elementData, index, 17 numMoved);//将elementData位于index+1之后的元素全部向前移一位 18 elementData[--size] = null; // clear to let GC do its work 19 return oldValue; 20 }
总结
ArrayList是一个线程不安全的动态数组,使用ensureCapacity()扩容,trimToSize缩减容量。
toArray()的使用
System.arraycopy()的使用
引用
1.http://www.cnblogs.com/skywang12345/p/3308556.html
2.http://blog.csdn.net/jzhf2012/article/details/8540410
3.http://blog.csdn.net/ljcITworld/article/details/52041836
4.http://www.cnblogs.com/dolphin0520/p/3933551.html
5.http://www.cnblogs.com/ITtangtang/p/3948555.html
6.http://www.cnblogs.com/java-zhao/p/5102342.html
7.http://www.tuicool.com/articles/uIBB3q
8.http://blog.csdn.net/gulu_gulu_jp/article/details/51457492
9.http://blog.csdn.net/chenssy/article/details/38373833
10.https://www.zhihu.com/question/19882918
11.http://www.cnblogs.com/vinozly/p/5171227.html