简介 特点:
按照下标快速随机访问
允许存放多个null元素
存储的元素有序且可以重复
底层是Object数组
增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素)
继承关系:
可以看到继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作;对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类。
实现了List接口,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合。
实现了RandomAccess接口标识着其支持随机快速访问,实际上,我们查看RandomAccess源码可以看到,其实里面什么都没有定义.因为ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1)。
实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅拷贝 。
实现了Serializable 标识着集合可被序列化。
构造方法 全局变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static final int DEFAULT_CAPACITY = 10 ; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size;
底层容器数组的前面有一个transient关键字,transient标识之后是不被序列化的,但是ArrayList又实现了Serializable,那反序列化之后数据岂不是都不见了?
答:其实是ArrayList在序列化的时候会调用writeObject(),直接将size和element写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取size和element,再恢复到elementData。
原因:elementData其实是一个缓存数组,它通常会预留一些容量,等容量不足时再扩容,那么那些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
构造方法:
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 public ArrayList () { this .elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } 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 (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; } }
Arrays.copyOf(elementData, size, Object[].class)
System.arraycopy(Object src, int srcPos,Object dest, int destPos,int length)
这两个是Java集合框架中很常用了两个深拷贝方法。
增加元素 add(E e) 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 public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; } 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); } private static int hugeCapacity (int minCapacity) { if (minCapacity < 0 ) throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
大体思路:
首先判断如果新添加一个元素是否会导致数组溢出
判断是否溢出:如果原数组是空的,那么第一次添加元素时会给数组一个默认大小10.接着是判断是否溢出,如果溢出则去扩容,扩容规则: 新数组大小是原来数组大小的1.5 倍,最后通过Arrays.copyOf()去浅复制.
添加元素到末尾
add(int index, E element) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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++; } private void rangeCheckForAdd (int index) { if (index > size || index < 0 ) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
思路:index之后的元素都往后移一位,腾出一个坑,最后将该元素放到index处。
addAll(Collection<? extends E> c) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ; }
思路:将需要插入的集合转成数组a,再将a数组插入到当前elementData的末尾(其中还判断了一下是否需要扩容)。
addAll(int index, Collection<? extends E> c) 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 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 rangeCheckForAdd (int index) { if (index > size || index < 0 ) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
思路:
其实就是一个先挖坑,再填坑的故事。首先判断一下添加了集合之后是否需要扩容,因为需要将集合插入到index处,所以需要将index后面的元素往后挪动,需要挪动的元素个数为:size - index,挪动的间隔是index + numNew(因为需要留出一个坑,用来存放需要插入的集合)。
总结 add、addAll。 先判断是否越界,是否需要扩容。 如果扩容, 就复制数组。 然后设置对应下标元素值。
值得注意的是:
如果需要扩容的话,默认扩容一半。如果扩容一半不够,就用目标的size作为扩容后的容量。
在扩容成功后,会修改modCount
删除 remove(int index) 可能会抛出IndexOutOfBoundsException或ArrayIndexOutOfBoundsException
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 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; } private void rangeCheck (int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
大体思路:
首先将旧值取出来,保存起来
然后将数组的index后面的元素往前挪动一位
将数组的末尾元素赋值为null,方便GC回收。因为已经将index后面的元素往前挪动了一位,所以最后一位是多余的,及时清理掉
remove(Object o) 作用:移除指定元素,只移除第一个集合中与指定元素相同(通过equals()判断)的元素.移除成功了则返回true,未移除任何元素则返回false。
如果传入的是null,则移除第一个null元素
如果传入的是非null元素,则移除第一个相同的元素,通过equals()进行比较。所以,如果是自己写的类,则需要重写equals()方法。一般需要用到元素比较的,都需要实现equals()方法,有时候还需要重写hashCode()方法。
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 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 void fastRemove (int index) { modCount++; int numMoved = size - index - 1 ; if (numMoved > 0 ) System.arraycopy(elementData, index+1 , elementData, index, numMoved); elementData[--size] = null ; }
大体思路:
首先判断需要移除的元素是否为null
如果为null,则循环遍历数组,移除第一个为null的元素
如果非null,则循环遍历数组,移除第一个与指定元素相同(equals() 返回true)的元素
可以看到最后都是移除指定位置的元素,源码中为了追求最佳的性能,加了一个fastRemove(int index)方法,次方法的实现与remove(int index)是几乎是一样的,就是少了返回index索引处元素的值。
removeAll(Collection<?> c) 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 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; }
大体思路:
首先我们进行c集合检查,判断是否为null
然后我们调用batchRemove()方法去移除 c集合与当前列表的交集
循环遍历当前数组,记录c集合中没有的元素,放在前面(记录下标为w),w前面的是留下来的元素,w后面的是需要删除的数据
第3步可能会出错,出错的情况下,则将出错位置的后面的全部保留下来,不删除。
然后就是将w之后的元素全部置空(方便GC回收),然后将size(标记当前数组有效元素)的值赋值为w,即完成了删除工作
再笼统一点说吧,其实就是将当前数组(elementData)中未包含在c中的元素,全部放在elementData数组的最前面,假设为w个,最后再统一置空后面的元素,并且记录当前数组有效元素个数为w。即完成了删除工作。
修改 set(int index, E element) 替换index索引处的元素为element,可能会抛出IndexOutOfBoundsException。这里比较简单,就是将index处的元素替换成element。
1 2 3 4 5 6 7 8 9 10 public E set (int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
查询 简单查询 1 2 3 4 5 6 7 8 9 10 11 12 E elementData (int index) { return (E) elementData[index]; } public E get (int index) { rangeCheck(index); return elementData(index); }
iterator()遍历 首先我们了解一下fail-fast,fail-fast 机制是java集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。
我们先来看看iterator()方法,它new了一个Itr(ArrayList的内部类)进行返回:
1 2 3 4 5 6 7 8 9 10 public Iterator<E> iterator () { return new Itr(); }
接下来我们来看看这个内部类
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 private class Itr implements Iterator <E > { int cursor; int lastRet = -1 ; int expectedModCount = modCount; public boolean hasNext () { return cursor != size; } @SuppressWarnings ("unchecked" ) public E next () { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this .elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1 ; return (E) elementData[lastRet = i]; } public void remove () { if (lastRet < 0 ) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this .remove(lastRet); cursor = lastRet; lastRet = -1 ; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings ("unchecked" ) public void forEachRemaining (Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this .size; int i = cursor; if (i >= size) { return ; } final Object[] elementData = ArrayList.this .elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } cursor = i; lastRet = i - 1 ; checkForComodification(); } final void checkForComodification () { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
这里可以看出之前插入和删除时经常要修改的 modCount 的值发挥了作用。
总结 下面我们来总结一下ArrayList的关键点
底层是Object数组存储数据
扩容机制:默认大小是10,扩容是扩容到之前的1.5倍的大小,每次扩容都是将原数组的数据复制进新数组中。我的领悟:如果是已经知道了需要创建多少个元素,那么尽量用new ArrayList<>(13)这种明确容量的方式创建ArrayList,避免不必要的浪费.
添加:如果是添加到数组的指定位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制;如果是添加到末尾的话,那么只可能触发扩容机制。
删除:如果是删除数组指定位置的元素,那么可能会挪动大量的数组元素;如果是删除末尾元素的话,那么代价是最小的。ArrayList里面的删除元素,其实是将该元素置为null。
查询和改某个位置的元素是非常快的( O(1) )。