ArrayList是最多见的集合类,顾名思义,ArrayList就是一个以数组形式实现的集合。它拥有List集合的特色:java
- 存取有序
- 带索引
- 容许重复元素
它自己的特色是:数组
- 查找元素快
- 顺序插入快
那ArrayList为何会有这些特性的?其实从源码中咱们就可以了解到它是如何实现的。安全
Resizable-array implementation of the {@code List} interface. Implements all optional list operations, and permits all elements, including {@code null}. In addition to implementing the {@code List} interface,this class provides methods to manipulate the size of the array that is used internally to store the list.数据结构
实现了List接口的,一个可调整大小的数组。能够存储全部的元素,包括null,除了实现了List接口的方法外,还提供了一些方法用于操做内部用于存储元素的数组的大小。多线程
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
dom
- RandomAccess标记接口 表示ArrayList支持随机访问。什么是随机访问呢?随机访问是说你能够随意访问该数据结构中的任意一个节点,假设该数据结构有10000个节点,你能够随意访问第1个到第10000个节点。由于ArrayList用数组来存数据,Java中给数组划份内存来存储元素时,划分的是一块连续的内存地址,这些相邻元素是按顺序连续存储的。只要知道起始元素的地址First,直接first+N,即可以获得第N个元素的地址。这就是ArrayList查找快的缘由。
- Cloneable标记接口 表示ArrayList是能够被克隆的。
- java.io.Serializable标记接口 表示ArrayList是能够被序列化的。
主要成员变量:ide
@java.io.Serial private static final long serialVersionUID = 8683452581122892189L;
性能
用于标明序列化时的版本号。this
private static final int DEFAULT_CAPACITY = 10;
.net
默认初始化大小。
private static final Object[] EMPTY_ELEMENTDATA = {};
默认空数组大小。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
默认初始化空数组大小。
看到这可能有人会有疑问,一样是空数组,为何要有两个引用变量来表示呢?且看后面的扩容机制说明。
transient Object[] elementData;
这就是ArrayList所维护的用于存储数据的数组了,transient标明这个存储数据的数组不会被序列化,而ArrayList却又打上了标记接口java.io.Serializable说明是可序列化的,这不是自相矛盾了吗?暂且按下不表,看看ArrayList的内部机制再回头说明。
private int size;
ArrayList的大小(其实也就是其中包含的元素个数)。
构造方法:
//传入了初始值的 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; } //传入一个Collection集合的 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 { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
能够看到,若是传入了初始值initialCapacity,就会按照这个值来初始化数组的大小。若是传入0,则数组等于EMPTY_ELEMENTDATA,若是用了空参构造,则数组等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
扩容机制:
咱们知道,Java中数组一旦定义则长度是不容许改变的,那么ArrayList如何实现_Resizable-array implementation(可调整大小的数组实现)?_
答案就在扩容机制上:
//传入最小所须要的容量 private Object[] grow(int minCapacity) { //当前容量大小 int oldCapacity = elementData.length; //若当前容量大小大于0且数组建立是否是经过空参构造建立的 if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //定义新的最小所需容量 int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ //右移一位,相似于除以2 oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } //传入当前容量,最小须要增长的容量,首选增加的容量 public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // assert oldLength >= 0 // assert minGrowth > 0 //将最小所须要增长的容量与首选增加的容量比较,取较大的与当前容量相加 int newLength = Math.max(minGrowth, prefGrowth) + oldLength; if (newLength - MAX_ARRAY_LENGTH <= 0) { return newLength; } return hugeLength(oldLength, minGrowth); }
咱们每次添加元素的时候,都会去确认一下当前ArrayList所维护的数组存储的元素是否已经达到上限(元素个数等于数组长度)?若是是的话,就会触发扩容机制grow()去建立一个新的更大的数组来转移数据。
能够看到,如果咱们经过传入参数0来构造ArrayList,grow()就会判断出来这个是一个长度为0的自定义空数组,那么就会按照最小的所须要的容量扩容。
若是没有传入参数,就用默认初始容量10,来建立初始的数组。
并且咱们能够看到,扩容的时候会判断所须要的最小容量是否是比当前数组的1.5倍还大?不是就按照当前数组长度的1.5倍来扩容,不然就按传进来的最小所需容量来扩容。
为何是1.5倍呢?
1.若是一次性扩容扩得太大,必然形成内存空间的浪费。
2.若是一次性扩容扩得不够,那么下一次扩容的操做必然比较快地会到来,这会下降程序运行效率,要知道扩容仍是比较耗费性能的一个操做,由于会新建数组移动数据。
因此扩容扩多少,是JDK开发人员在时间、空间上作的一个权衡,提供出来的一个比较合理的数值。
添加元素:
//添加单个元素,默认添加到集合尾 public boolean add(E e) { modCount++; add(e, elementData, size); return true; } //实际调用的方法 private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } //添加另外一个集合,默认添加到集合尾 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); modCount++; int numNew = a.length; if (numNew == 0) return false; Object[] elementData; final int s; if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); //5个参数的意思分别是:源数组,源数组开始复制的位置,目标数组,目标数组开始接收的位置,接收的长度 System.arraycopy(a, 0, elementData, s, numNew); size = s + numNew; return true; }
基本上很浅显易懂,只要容量够就能直接添加,不然得扩容。
其中modCount是用于快速失败的一种机制,由于基本集合在多线程环境下是不安全的,这个之后再讨论。
删除元素:
//按元素删除 public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; //标签旧方法,用于跳出多个循环 found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; } //按索引删除 public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; } //实际执行方法 private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; }
分两种状况,一种是按照传入的元素去删除,这样得从数组开头遍历过去而且逐一调用equals方法比较,只要找到第一个符合的就将其删除。
第二种是按照传入的索引去删除,这个能够直接定位。另外须要注意的一点是当ArrayList里存储的是Integer包装类的时候,依然是选择索引删除。
删除的意思是将数组此位置的元素的引用设置为null,若是没有另外的引用指向此元素,那么此元素就会被标记为垃圾被回收。
若是删除的元素在最后就不用移动元素了,若是不在就须要移动元素。
插入元素:
public void add(int index, E element) { rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); System.arraycopy(elementData, index, elementData, index + 1, s - index); elementData[index] = element; size = s + 1; }
先判断容量是否足够,再去将此位置以及后面的元素所有向后移动一位,最后将传入的元素插入此位置。
最后说明:
了解了ArrayList的各类机制,咱们就能知道为何存储元素的elementData用transient修饰了
//序列化 @java.io.Serial private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioral compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } //反序列化 @java.io.Serial private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // like clone(), allocate array based upon size not capacity SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size); Object[] elements = new Object[size]; // Read in all elements in the proper order. for (int i = 0; i < size; i++) { elements[i] = s.readObject(); } elementData = elements; } else if (size == 0) { elementData = EMPTY_ELEMENTDATA; } else { throw new java.io.InvalidObjectException("Invalid size: " + size); } }
其实主要是看里边的两个循环的方法,涉及到的是size。咱们已经了解了存放元素的数组会动态的改变,所以里边未必就存满了元素。真正有多少个元素是size负责的。因此咱们序列化的时候仅仅去遍历size个对象就能完成序列化了。这样就能避免序列化太多不须要的东西,加快序列化速度以及减少文件大小。
总结: 在须要顺序添加数据以及快速访问数组的场景,ArrayList最适合被使用。