Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说java框架集合List子接口之ArrayList源码剖析「建议收藏」,希望能够帮助你!!!。
感兴趣的话大家可以关注一下公众号 : 猿人刘先生 , 欢迎大家一起学习 , 一起进步 , 一起来交流吧!
ArrayList实现了List接口 , 它是有序且可以重复的 , 允许存放所有所有元素 , 包括null , 除了实现List接口之外这个类还提供了一些方法来操作内部存储列表数组的大小 , 这个类大致相当于Vector , 只是它不是同步的 , 同时ArrayList还实现了RandomAccess, Cloneable, java.io.Serializable
Random是随机的意思,Access是访问的意思,合起来就是随机访问的意思。RandomAccess是一个标记接口 , 用以标记实现的List集合具备快速随机访问的能力 , 那么什么是随机访问的能力呢?其实很简单,随机访问就是随机访问List中的任何一个元素。
所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了,这里的快速随机访问,那么就不是所有List集合都支持了
ArrayList基于数组实现,天然带下标 ,可以实现常量级的随机访问,复杂度为O(1)
LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n)
通过代码的实现也能看出来 , ArrayList实现了RandomAccess接口 , 而LinkedList没有实现
支持拷贝:实现Cloneable接口,重写clone方法、方法内容默认调用父类的clone方法。
浅拷贝(shallowCopy)
如果是基础类型和String类型的变量拷贝之后是独立的,不会随着源变量变动而变 , 但是如果修改了任意一个变量的值 , 对另一个都是有影响的
引用类型拷贝的是引用地址,拷贝前后的变量引用同一个堆中的对象
基本类型是复制的变量名和值,值变了不影响源变
用类型复制的是变量名和值(引用地址),对象变了,会影响源变量(引用地址是一样的)
String:是不可变对象,重新赋值时,会在常量表新生成字符串(如果已有,直接取他的引用地址),将新字符串的引用地址赋值给栈中的新变量,因此源变量不会受影响
也可以理解为浅拷贝是修改堆内存中的同一个值 , 而深拷贝是修改堆内存中的不同的值
序列化 : 把Java对象转换为字节序列(二进制)的过程
序列化作用 : 在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中
反序列化 : 把字节序列(二进制)恢复为Java对象的过程
反序列化作用 : 根据字节流中保存的对象状态及描述信息,通过反序列化重建对象
在数据传输(也可称为网络传输)前,先通过序列化工具类将Java对象序列化为json/xml文件。
在数据传输(也可称为网络传输)后,再将json/xml文件反序列化为对应语言的对象
序列化优点
将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。
序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。
通过序列化可以在进程间传递对象。
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = {
};
// 用于默认大小的空实例的共享空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
// 当前数据对象存放地方,当前对象不参与序列化
transient Object[] elementData
// 当前ArrayList的长度
private int size;
// 数组最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/** * Constructs an empty list with an initial capacity of ten. */
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
如果使用无参构造创建ArrayList对象 , 此时我们创建的ArrayList对象中的elementData中的长度是1,size是0 , 当第一次添加元素的时候 , elementData才会变成默认长度10
/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */
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);
}
}
如果传入参数,则代表指定ArrayList的初始数组长度,传入参数如果是大于等于0,则使用用户的参数初始化,如果用户传入的参数小于0,则抛出异常
/** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */
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;
}
}
将collection对象转换成数组,然后将数组的地址赋给elementData。
更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。
注意:this.elementData = arg0.toArray(); 这里执行的简单赋值是浅拷贝,所以要执行Arrays.copy 做深拷贝
这种情况下默认使用尾插法 , 时间复杂度O(1) , 如果需要扩容 , 则使用Arrays.copy()进行深拷贝 , 以此来生成新的数组 , 然后把旧数组的值赋值给新数组 , 并且如果此时数组的长度+1小于默认容量 , 那么就会初始化一个容量为10的数组 , 并不是在new ArrayList<>()的时候就会生成默认容量的数组
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */
public boolean add(E e) {
// 判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 从默认容量和当前ArrayList的size + 1选一个最大的最为扩容阈值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 将修改次数(modCount)自增1
modCount++;
// overflow-conscious code
// 判断是否需要扩容 , 如果当前容量减去数组容量大于0 , 表示需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */
private void grow(int minCapacity) {
// overflow-conscious code
// 旧的容量
int oldCapacity = elementData.length;
// 新容量等于旧容量 + 旧容量左移1位
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 如果新容量 - 需要扩容阈值 < 0 那么新容量就使用扩容阈值
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果新容量 - 数组最大长度 > 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;
}
旧数组值赋值给新数组之后可能会出现三种情况 :
如果旧数组之前经过多次年轻代的GC(yongGC)没有被清理掉的话 ,旧数组就会进入老年代 , 那么生成新数组之后 ,旧数组会被老年代GC掉
如果旧数组经过多次yongGC , 但是没有符合进入老年代的条件及每次yongGC都会在Survivor 0 ,Survivor 1进行移动 , 每移动一次同时记录次数, 达到15次 , 才会被放入老年代 ,这个时候就会被yongGC
如果新数组分配的和旧数组是同一块地址 ,这个时候不需要进行回收
如果使用的这个add方法 , 那么将会在数组指定位置插入一个新元素 , 其它元素向后移动 ,此时时间复杂度为O(N) , 扩容机制和add(E e)方法是相同的
public void add(int index, E element) {
// 如果index大于当前ArrayList的size , 或者小于0 , 则抛异常
rangeCheckForAdd(index);
// 判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将新的数据内容存放到数组的指定位置(index)上
elementData[index] = element;
size++;
}
数组的查询是通过一个公式来计算的 :a[n] = 起始地址 + (n * 字节数) , n表示的就是下标 , 比如我们此时要寻找下边为2的元素 , 那么通过公式计算 , 100 + (2*4) , 那么找到的就是地址为108的元素 , 也就是说5 , 因为它有一个公式 , 经过计算 , 可以很快的找到这个元素的地址 , 所以它的查询的时间复杂度是o(1) , 因此使用get方法随机访问元素的时候 , 时间复杂度为O(1)
public E get(int index) {
// 检查下标是否越界
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
// 返回下标对应的元素
return (E) elementData[index];
}
调用indexOf方法,遍历数组中的每一个元素作对比,如果找到对于的元素,则返回true,没有找到则返回false
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
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;
}
根据下标删除的情况下时间复杂度分为几种情况考虑 : 如果使用尾删法及删除最后一个元素 , 那个时间复杂度为O(1) , 不涉及到其他元素的移动 , 否则时间复杂度为O(N) , 将会涉及到其他元素的移动
public E remove(int index) {
// 检查下标是否越界
rangeCheck(index);
// 修改次数 + 1
modCount++;
// 根据下标获取需要修改的值
E oldValue = elementData(index);
// 判断是否需要移动数组元素
int numMoved = size - index - 1;
if (numMoved > 0)
// 使用System.arraycopy 将需要插入的位置(index)后面的元素统统往前移动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 不需要移动元素的话 , 把最后一位置为空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
如果使用这种方法进行删除元素 , 那么时间复杂度为O(N) , 涉及到元素的比阿尼寻找以及移动
public boolean remove(Object o) {
// 循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作
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
}
添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
我们看到代码中是创建了一个ArrayList 类里面的一个内部类SubList对象,传入的值中第一个参数时this参数,其实可以理解为返回当前list的部分视图,真实指向的存放数据内容的地方还是同一个地方,如果修改了sublist返回的内容的话,那么原来的list也会变动。
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
interator方法返回的是一个内部类,由于内部类的创建默认含有外部的this指针,所以这个内部类可以调用到外部类的属性
public Iterator<E> iterator() {
return new Itr();
}
一般我们调用interator之后会进行集合的遍历 , 这里使用next做遍历的时候有个需要注意的地方,就是调用next的时候,可能会引发ConcurrentModificationException,当修改次数,与期望的修改次数(调用iterator方法时候的修改次数)不一致的时候,会发生该异常,详细我们看一下代码实现
@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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount这个值是在用户调用ArrayList的iterator方法时候确定的,但是在这之后用户add,或者remove了ArrayList的元素,那么modCount就会改变,那么这个值就会不相等,将会引发ConcurrentModificationException异常,这个是在多线程使用情况下,比较常见的一个异常。
参数 | 说明 |
src | 原数组 |
srcPos | 原数组 |
dest | 目标数组 |
destPos | 目标数组的起始位置 |
length | 要复制的数组元素的数目 |
参数 | 说明 |
original | 要复制的数组 |
newLength | 要返回副本的长度 |
newType | 要返回副本的类型 |
其实Arrays.copyOf底层也是调用System.arraycopy实现的源码如下:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
ArrayList底层是基于动态数组实现的 , 可以进行动态扩容
ArrayList自己实现了序列化和反序列化方法 , 因为它实现了writeObject和readObject方法
ArrayList在编码时如果知道数据比较多 , 那么可以提前传入容量值 , 避免进行频繁扩容
ArrayList使用尾插法的时间复杂度为O(1) , 指定位置插入时间复杂度为O(N)
ArrayList支持随机访问 , 其时间复杂度为O(1)
ArrayList使用尾删法时 , 时间复杂度为O(1) , 并且会把最后一个元素置位null , 其它删除时间复杂度为O(N) , 因为会涉及到元素的移动以及元素的遍历
ArrsyList是线程不安全的