参考文档: https://cloud.tencent.com/developer/article/1145014
https://segmentfault.com/a/1190000018578944
http://www.importnew.com/9928.html
https://blog.csdn.net/zero__007/article/details/52166306
1. ArrayList简介
ArrayList底层基于数组实现的一种线性数据结构,通过数组的索引原理实现了快速查找,是非线程安全的。
由于数组创建时必须制定容量而且不可更改,ArrayList通过自动扩容的方式弥补了数组容量不可更改的弊端,但同时也带了性能方面的隐患。
2.ArrayList继承关系
ArrayList继承自AbstractList,实现了List、RandomAccess、Cloneable、java.io.Serializable接口。
实现了所有List接口的操作,并ArrayList允许存储null值。除了没有进行同步,ArrayList基本等同于Vector。在Vector中几乎对所有的方法都进行了同步,但ArrayList仅对writeObject和readObject进行了同步,其它比如add(Object)、remove(int)等都没有同步。
- AbstractList提供了List接口的默认实现(个别方法为抽象方法)。
- List接口定义了列表必须实现的方法。
- 实现了RandomAccess接口:提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
- 实现了Cloneable接口:可以调用Object.clone方法返回该对象的浅拷贝。
- 实现了 java.io.Serializable 接口:可以启用其序列化功能,能通过序列化去传输。未实现此接口的类将无法使其任何状态序列化或反序列化。序列化接口没有方法或字段,仅用于标识可序列化的语义。
3. ArrayList实现
1. 核心属性
transient Object[] elementData; private int size;
elementData是ArrayList中用来存储数据的底层数组,size代表数组中存储的元素个数。
有个关键字需要解释:transient。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
2. 构造函数
private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 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) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
ArrayList有三个构造函数,一个无参构造,一个指定容量的有参构造,一个指定集合的有参构造。
无参构造会创建一个的列表,内部数组容量为0,当调用add方法添加元素时会扩容成默认容量10(为何网上都说是构造一个默认初始容量为10的空列表???)。
指定容量的有参构造会创建一个内部数组为指定容量大小的空列表。
指定集合的有参构造会创建一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。
3. 存储元素
当对ArrayList进行元素添加的时候,都会检查底层数组的容量是否足够,若是不够则进行自动扩容,每次对数组进行增删改的时候都会增加modCount(继承自AbstractList的属性,用来统计修改次数),添加单个元素时一般会扩容1.5倍[oldCapacity + (oldCapacity >> 1)],添加集合时,如果(原数组的长度 + 添加的集合长度 > 原数组的长度的1.5倍)则会扩容至(原数组的长度 + 添加的集合长度)
存储元素分为三种类型:追加,插入,替换,其中第二种和第三种类型都会检查下标是否越界(根据size属性而不是数组的长度)
(1)追加:这种方式最常用,直接添加到数组中最后一个元素的后面。
public boolean add(E paramE) { ensureCapacityInternal(this.size + 1); this.elementData[(this.size++)] = paramE; return true; } public boolean addAll(Collection extends E> paramCollection) { Object[] arrayOfObject = paramCollection.toArray(); int i = arrayOfObject.length; ensureCapacityInternal(this.size + i); System.arraycopy(arrayOfObject, 0, this.elementData, this.size, i); this.size += i; return i != 0; }
(2)插入:当调用下面这两个方法向数组中添加元素或集合时,会先查找索引位置,然后将元素添加到索引处,最后把添加前索引后面的元素追加到新元素的后面。
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } public boolean addAll(int index, Collection extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount 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; }
(3)替换:调用该方法会将index位置的元素用新元素替代。
public void set(E e) { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.set(lastRet, e); } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
4. 元素读取
public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); }
5. 元素删除
ArrayList提供了5种方式的删除功能。如下:
(1)romove(int index),首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素。
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; // clear to let GC do its work return oldValue; }
(2)remove(Object o),这里为了防止equals方法空指针异常,对remove对象为空的情况做了特殊处理,然后遍历底层数组找到与remove对象相同的元素调用fastRemove后返回true,fastRemove的逻辑和romove(int index)的逻辑一致,只是少了范围检查以及没有返回值。
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; // clear to let GC do its work }
(3)removeRange(int fromIndex, int toIndex),修改modCount,把toIndex后面的元素通过System.arraycopy复制到toIndex位置后面,计算移除后的数组大小newSize,数组中下标大于等于newSize的元素全部置为空,方便垃圾回收,重置size属性。
protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // clear to let GC do its work int newSize = size - (toIndex-fromIndex); for (int i = newSize; i < size; i++) { elementData[i] = null; } size = newSize; }
(4)removeAll(Collection> c),首先检查参数,为空则抛出异常,然后调用batchRemove(c, false),在batchRemove中,先把底层数组元素赋给一个final修饰的局部变量elementData,然后遍历elementData,把elementData中除了c包含的元素从下标为0开始依次存入elementData进行重排序,最后通过(r != size)判断try块中是否出现过异常,出现过异常则把elementData中未遍历过的元素全部复制到下标为w后面,通过(w != size)判断原数组是否已经改变,如果已改变则修改modCOunt,将下标为w后面的元素全部置为空,重置size属性,把modified属性设为true代表移除成功并返回。
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 { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }
(5)removeIf(Predicate super E> filter),在JDK1.8中,Collection
以及其子类新加入了removeIf
方法,作用是通过lambda表达式移除集合中符合条件的元素。如移除List
@Override public boolean removeIf(Predicate super E> filter) { Objects.requireNonNull(filter); // figure out which elements are to be removed // any exception thrown from the filter predicate at this stage // will leave the collection unmodified int removeCount = 0; final BitSet removeSet = new BitSet(size); final int expectedModCount = modCount; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { @SuppressWarnings("unchecked") final E element = (E) elementData[i]; if (filter.test(element)) { removeSet.set(i); removeCount++; } } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } // shift surviving elements left over the spaces left by removed elements final boolean anyToRemove = removeCount > 0; if (anyToRemove) { final int newSize = size - removeCount; for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) { i = removeSet.nextClearBit(i); elementData[j] = elementData[i]; } for (int k=newSize; k < size; k++) { elementData[k] = null; // Let gc do its work } this.size = newSize; if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } return anyToRemove; }
6. 调整数组容量
(1)ensureCapacity(int minCapacity),首先自定义一个变量,如果内部数组elementData为空则赋值为0,否则赋值为10,如果数组需要的容量大于自定义变量的值,则调用ensureExplicitCapacity(minCapacity)。ensureExplicitCapacity方法会先修改modCount,然后当需要的容量大于elementData的长度时调用grow(minCapacity),grow方法是最终进行扩容算法的方法。grow方法内先定义了两个变量oldCapacity(当前数组长度)和newCapacity(扩容后的数组长度),newCapacity的值是oldCapacity的1.5倍(oldCapacity >> 1 左移1位相当于除以2),如果需要扩容的容量大于newCapacity,则把newCapacity赋值为minCapacity(也就是说ArrayList扩容不一定每次扩容都是1.5倍,ArrayList内部默认扩容1.5倍,但由于ensureCapacity是一个public方法,我们可以外部手动调用,当需要向ArrayList中添加大量元素时,我们可以提前根据需要添加的元素数量调用ensureCapacity,根据需要添加的元素数量提前调用ensureCapacity来进行手动扩容,避免递增式自动扩容反复调用Arrays.copyOf带来的性能损耗,提高程序效率),然后就是判断需要扩容的容量是否大于int的最大值,如果超过最大值则抛出内存溢出异常,如果大于(int最大值 - 8),小于等于int最大值,则把newCapacity设为int最大值,最后也是最核心的一步,通过Arrays.copyOf进行数组的扩容。
public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
(2)trimToSize(),这个方法很简单,就是把底层数组的长度调整为元素实际个数大小,这个方法主要是为了防止ArrayList进行扩容时产生的空间浪费。由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。一般只有当确定了ArrayList的元素不再增加时进行调用。
public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); } }
7. 转为静态数组的两种方法
(1)直接将底层数组拷贝一份大小为size的新数组并返回
public Object[] toArray() { return Arrays.copyOf(elementData, size); }
(2)如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。这里需要注意的是参数中的T类型要与ArrayList中存储的数据类型一致,否则会出现ArrayStoreException。
// 返回ArrayList的模板数组。所谓模板数组,即可以将T设为任意的数据类型 publicT[] toArray(T[] a) { // 若数组a的大小 < ArrayList的元素个数; // 则新建一个T[]数组,数组大小是“ArrayList的元素个数”,并将“ArrayList”全部拷贝到新数组中 if (a.length < size) return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // 若数组a的大小 >= ArrayList的元素个数; // 则将ArrayList的全部元素都拷贝到数组a中。 System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
8.实现了Cloneable接口,进行数据浅拷贝
如果ArrayList中存的是引用类型数据,则把拷贝后的ArrayList中的对象的属性修改后,源ArrayList中对应的对象的属性也会改变,当然移除和添加元素没有影响,若是基本数据类型则没有这问题。
// 克隆函数 public Object clone() { try { ArrayListv = (ArrayList ) super.clone(); // 将当前ArrayList的全部元素拷贝到v中 v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(); } }
9.实现Serializable 接口,启用其序列化功能
// java.io.Serializable的写入函数 // 将ArrayList的“容量,所有的元素值”都写入到输出流中 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // 写入“数组的容量” s.writeInt(elementData.length); // 写入“数组的每一个元素” for (int i = 0; i < size; i++) s.writeObject(elementData[i]); if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } // java.io.Serializable的读取函数:根据写入方式读出 // 先将ArrayList的“容量”读出,然后将“所有的元素值”读出 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in size, and any hidden stuff s.defaultReadObject(); // 从输入流中读取ArrayList的“容量” int arrayLength = s.readInt(); Object[] a = elementData = new Object[arrayLength]; // 从输入流中将“所有的元素值”读出 for (int i = 0; i < size; i++) a[i] = s.readObject(); }
10. 实现了RandomAccess接口,启用随机访问
实际上RandomAccess是一个空的接口,它到的作用是做一个标记,用来区分List的实现类是否支持随机访问。事实上不同实现方式的不同遍历方式性能差异较大,如ArrayList使用for循环遍历比用iterator迭代器要快,而LinkedList使用iterator迭代器遍历比for循环要快的多,所以List不同子类需要采用不同遍历方式以提高性能。可是我们如何区分List的实现子类是ArrayList还是LinkedList呢?这时候RandomAccess接口就派上用场了,用instanceof方法来判断该子类是否实现了RandomAccess接口,进而更好的判断集合是否ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!
4. ArrayList常见问题
1. ArrayList
的默认初始长度是多少?最大长度是多少?
ArrayList默认长度为10,但新创建的ArrayList容量其实默认为0,首次添加元素时会自动扩容至默认容量10;最大长度为int的最大值(2147483647),而不是ArrayList内部定义的常量MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
2. ArrayList
是如何扩容的?
ArrayList底层是通过ensureCapacity方法来进行扩容的(具体扩容实现前面已经说过了),这个方法一般是在向ArrayList中添加元素时首先调用以确保容量足以容纳新增的元素,同时这个方法我们也可以手动调用来进行扩容,一般在知道数据量较大的情况下提前手动扩容以避免频繁扩容带来的性能损耗。大体的扩容思路就是每次添加元素时,检查底层数组容量是否足够,足够则直接添加,若是不够则扩容1.5倍,若是要添加的也是一个集合,则取这个集合的大小与当前ArrayList的大小之和与底层数组长度的1.5倍两者之间的最大值进行扩容。
3. ArrayList
扩容后是否会自动缩容?如果不能怎样进行缩容?
ArrayList
只能自动扩容,不能自动缩容。如果需要进行缩容,可以调用ArrayList
提供的trimToSize()
方法。
4. ArrayList底层数组扩容时是如何保证高效复制数组的?
表面上是调用Arrays.copyOf()
方法,实际上是Arrays.copyOf()
通过调用System.arraycopy()
方法复制数组的,但貌似并不高效~~。
5. 什么情况下你会使用ArrayList?什么时候你会选择LinkedList?
这个问题实际上考察的是ArrayList与LinkedList的区别,也就是数组与链表的区别。数组由于索引的存在,查找较快,而链表查找时需要进行遍历,所以当查找操作较多的情况下用ArrayList更合适;如果查找较少,增删较多的情况选择LinkedList则更为合适,因为在ArrayList中增加或者删除某个元素,通常会调用System.arraycopy方法,这是一种极为消耗资源的操作。
6. 当传递ArrayList到某个方法中,或者某个方法返回ArrayList,什么时候要考虑安全隐患?如何修复安全违规这个问题呢?
当array被当做参数传递到某个方法中,如果array在没有被复制的情况下直接被分配给了成员变量,那么就可能发生这种情况,即当原始的数组被调用的方法改变的时候,传递到这个方法中的数组也会改变。下面的这段代码展示的就是安全违规以及如何修复这个问题。
pulic void setArr(String[] arr){ this.arr = arr; }
修复这个安全隐患:
public void setArr(String[] newArr){ if(newArr == null) this.arr = new String[0]; else this.arr = Arrays.copyOf(newArr, newArr.length); }
7. 如何复制某个ArrayList到另一个ArrayList中去?写出你的代码?
下面就是把某个ArrayList复制到另一个ArrayList中去的几种技术:
- 使用clone()方法,比如ArrayList newArray = oldArray.clone();
- 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
- 使用Collection的copy方法。
注意1和2是浅拷贝(shallow copy)。
8. 在索引中ArrayList的增加或者删除某个对象的运行过程?效率很低吗?解释一下为什么?
在ArrayList中增加或者是删除元素,要调用System.arraycopy这种效率很低的操作