1.ArraysList继承体系
1.1Serializable标记性接口
介绍:
介绍类的序列化由实现java.io.Serializable接口的类启用。不实现此接口的类将不会使任何状态序列化或反序列化。可序列化类的所有子类型都是可序列化的。序列化接口没有方法或字段,仅用于标识可串行化的语义。
序列化:将对象的数据写入到文件(写对象)
反序列化:将文件中对象的数据读取出来(读对象)
源码
public interface Serializable {
}
1.2 Cloneable标记性接口
介绍:
介绍一个类实现cloneable接口来指示object.clone()方法,该方法对于该类的实例进行字段的复制是合法的。在不实现Cloneable接口的实例上调用对象的克隆方法会导致异常cloneNotSupportedException被抛出。
简言之:克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝
源码:
public interface Cloneable {
}
克隆的前提条件:
- 被克隆对象所在的类必须实现cloneable接口
- 必须重写clone方法
clone的基本使用:
/**
* @author 陈栋
* @create 2021/9/8 21:48
*/
public class MyArrayListTest {
public static void main(String[] args) {
ArrayList<String > arrayList = new ArrayList<>();
arrayList.add("陈1");
arrayList.add("陈2");
arrayList.add("陈3");
arrayList.add("陈4");
ArrayList<String > clone = (ArrayList<String>) arrayList.clone();
System.out.println(clone);
System.out.println(arrayList);
}
}
源码分析:
1.3 RandomAccess标记接口
介绍标记接口由List 实现使用,以表明它们支持快速(通常为恒定时间)随机访问。
此接口的主要目的是允许通用算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。
用于操纵随机访问列表的最佳算法(例如ArrayList)可以在应用于顺序访问列表时产生二次行为(如LinkedList)。鼓励通用列表算法在应用如果将其应用于顺序访问列表之前提供较差性能的算法时,检查给定列表是否为instanceof,并在必要时更改其行为以保证可接受的性能。(简单的来说就是如果继承了RandomAccess接口那么遍历就使用随机访问,否则就使用顺序访问)
人们认识到,随机访问和顺序访问之间的区别通常是模糊的。例如,一些List实现提供渐近的线性访问时间,如果它们在实践中获得巨大但是恒定的访问时间。这样的一个List实现应该通常实现这个接口。根据经验,List 实现应实现此接口,如果对于类的典型实例,此循环:
随机访问
for (int i = 0; i < arrayList.size(); i++) {
arrayList.get(i);
}
比下面这个循环要快:
顺序访问:
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
iterator.next();
}
1.4 AbstractList抽象类
AbstractList不仅仅是ArrayList类的抽象类也是LinkList的抽象类
AbstractList是List的骨架,继承该类的必须重写get方法
其外,点进AbstractList的源码可以看到其set和add方法也是不能直接调用的,否则会报UnsupportedOperationException的异常
2、ArrayList源码分析
要研究源码我们首先需要了解其字段,因为字段在整个ArrayList类中可以说是无处不在
ArrayList底层的数据结构其实就是Object数组,这也是他可以支持随机访问的原因
ArrayList 底层是基于数组来实现容量大小动态变化的。
2.1字段分析
字段 | 含义 |
---|---|
DEFAULT_CAPACITY = 10 | 默认数组的大小 |
Object[] EMPTY_ELEMENTDATA = {} | 用于有参函数的初始化 |
Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} | 用于无参函数的初始化 |
Object[] elementData | ArrayList的数据结构 |
int size | 表示数组中的元素个数,注意和elementData.length区分 |
modCount | 父类的字段 每对集合有效操作一次,该数+1 其实就是当插入和删除的时候该数+1 |
2.2构造方法
Constructor | Constructor描述 |
---|---|
ArrayList() | 构造一个初始化容量为10的空列表 |
ArrayList(int initialCapacity) | 构造一个初始化容量为initialCapacity的列表 |
ArrayList(Collection<? extends E> c) | 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 |
一、无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这里的代码非常简单,只是把一个空的数组给了elementData,但是有的小伙伴可能会想不应该进行初始化吗?
这里可以留一个伏笔(后面再说),无参构造的初始化,是放在第一次调用add方法时,初始化一个大小为10的数组
二、有参构造,指定初始容量
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);
}
}
这里的代码就是简单除暴
首先判断初始容量是否大于0,然后创建一个指定初始化容量的数组
如果初始化容量等于0,那么就会给一个空的数组
注意:EMPTY_ELEMENTDATA 与DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的区别,前者是用于指定容量大小的有参构造空数组,后者是无参构造的空数组
三、指定一个集合创建ArrayList
public ArrayList(Collection<? extends E> c) {
// 调用集合的toArray得到Object数组然后赋值给elementData
elementData = c.toArray();
// 先把数组的大小赋值给size,然后判断size是否为0
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
// 如果数组大小不为0,那么创建一个elementData副本重新赋给elementData
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
// 如果集合大小为0,那么赋值给一个空的数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
-
调用集合的toArray得到Object数组然后赋值给elementData
-
先把数组的大小赋值给size,然后判断size是否为0
1、如果数组大小不为0,那么创建一个elementData副本重新赋给elementData
(有兴趣的小伙伴可以看看Arrays.copyOf源码)
2、如果集合大小为0,那么赋值给一个空的数组(这里的数组就是上一个有参构造使用的空数组)
2.3 add方法
这里面的add方法分为3个
方法 | 参数解析 |
---|---|
add(E e) | e要插入的元素 |
add(int index, E element) | 私有方法 index – 要插入指定元素的索引 element – 要插入的元素 |
add(E e, Object[] elementData, int s) | e要插入的元素 elementData要操作的数组 s数组中的元素个数 |
一、最常用的add方法
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
该方法先对操作计数+1,然后套娃三个参数的add方法
下面我们具体讲三个参数的add方法
二、私有的add方法
private void add(E e, Object[] elementData, int s) {
// 先判断该数组中的元素个数,是否等于数组的长度了,
// 其实就是进行扩容的判断
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
我们接着看grow()方法
private Object[] grow() {
return grow(size + 1);
}
只是对元素的数量+1之后又是套娃,我们继续看有参的grow()方法
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
真正的重点来了,该方法对elementData数组进行了复制,传入要复制的数组,以及数组长度,返回了副本,然后再赋给elementData
下面我们看真正的扩容函数newCapacity(minCapacity)方法
我们先总的说一下newCapacity方法
返回至少与给定最小容量一样大的容量。 如果足够,返回当前容量增加 50%。 也就是说会返回一个原Capacity * 1.5倍的新的Capacity容量
private int newCapacity(int minCapacity) {
// overflow-conscious code
// 计算原先的数组的长度
int oldCapacity = elementData.length;
// 把原先的长度与长度有符号右移位一位,即可以得到原先长的1.5倍大小
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后的容量比最小需要的容量都小
if (newCapacity - minCapacity <= 0) {
// 如果数组是通过无参构造的数组,那么返回默认容量(10)与最少需要的容量,这里的判断其实也就是在进行无参构造的初始化数组大小
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
// 如果最小容量的取值超出范围,就会报错OOM
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
// 如果扩容之后大小比最小需要的大小大的话,就判断一下扩容之后的大小是否超出了默认的最大数组长度,其实这里一般都不会超出,所有都是返回扩容之后的大小
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
上面的代码整体来说比较简单,但是这里有两个知识点
一:有符号的左移与右移,话不多说,大家直接看图
二、newCapacity - minCapacity <= 0这个条件的判断
我们简单说明一下,举个栗子
- 比如说入参minCapacity = 1(第一次添加add方法的时候)并且elementData.length= 0,(其实就是对应无参构造之后,第一次调用add方法)此时newCapacity扩容之后任是0,所以会满足newCapacity - minCapacity <= 0判断
- 又或者当elementData.length为1是,newCapacity任然是1,minCapacity = 2,此时也会满足newCapacity - minCapacity <= 0
三、指定位置插入指定元素的add方法
/**
*
* @param index 指定位置的索引
*/
private void rangeCheckForAdd(int index) {
// 判断索引是否合法,否则抛出IndexOutOfBoundsException异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public void add(int index, E element) {
// 检查指定索引是否合法
rangeCheckForAdd(index);
// 操作计数器+1
modCount++;
final int s;
Object[] elementData;
// 先把ArrayList底层数组赋给局部变量elementData请计算得到长度,然后把ArrayList底层数组的元素个数赋给s,在判断这两个变量是否相等
// 其实这一部判断就是判断当前数组是否已经满了
if ((s = size) == (elementData = this.elementData).length)
// 如果数组满了,进行扩容调用我们上面讲的grow方法
elementData = grow();
// index及其后面的元素向后移位
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
// 把index索引上,插入指定元素
elementData[index] = element;
// 元素个数+1
size = s + 1;
}
这里稍微讲一下题中数组的插入也就是System.arraycopy
从这个图可以看到index及其以后的元素整体向后移动一位,要插入的元素再放进行
2.4set方法
public E set(int index, E element) {
// 对索引进行检查
Objects.checkIndex(index, size);
// 先得到以前位置上的元素
E oldValue = elementData(index);
// 覆盖以前的元素,放入新的元素
elementData[index] = element;
// 返回以前的元素
return oldValue;
我们点进Objects.checkIndex源码进行查看,其实又是套娃Preconditions类的静态方法checkIndex,进行index的合法性检查
2.5 get方法
public E get(int index) {
// 进行index的合法性检查
Objects.checkIndex(index, size);
// 返回该索引下的元素
return elementData(index);
}
2.6 toString方法
首先我们先来看一下ArrayList的继承树,toString不是写在ArrayList中的也不是写在其父类AbstractList中的,而是在其父类的父类AbstractCollection类中
public String toString() {
// 生成一个迭代器,迭代器后面具体细讲,小伙伴不用急
Iterator<E> it = iterator();
// 判断集合是否为空
if (! it.hasNext())
return "[]";
// 创建一个StringBuilder缓冲区
StringBuilder sb = new StringBuilder();
sb.append('[');
// 无限循环
for (;;) {
// 调用迭代器的next方法取出元素,并且移动索引指针
E e = it.next();
// 判断是否是AbstractCollection类
sb.append(e == this ? "(this Collection)" : e);
// 判断是否还有下一个元素
if (! it.hasNext())
// 已经迭代完了,在缓冲区最后加上']',然后把整个缓冲区的数据转成String返回
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
2.7 remove方法
remove分为两个方法,一个是根据指定位置删除元素,另一个是根据值来删除元素
一、根据指定位置删除元素
public E remove(int index) {
// 检查index是否合法
Objects.checkIndex(index, size);
// 把集合的数组地址赋给局部变量es
final Object[] es = elementData;
// 从集合中取出要删除的元素
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
// 调用真正执行删除操作的方法
fastRemove(es, index);
return oldValue;
}
上面的代码非常简单,也是在套娃,调用一个私有删除元素的方法
下面我们看看fastRemove的源代码
private void fastRemove(Object[] es, int i) {
// 修改计数器+1
modCount++;
// 用于记录新的数组元素的个数
final int newSize;
// 如果要删除的元素不是最后一位
if ((newSize = size - 1) > i)
// 拷贝数组,删除数组中的元素可以借助上面插入数组元素的图进行理解,简单来说就是把要删除的指定位置及其以后的元素向前移动一位
System.arraycopy(es, i + 1, es, i, newSize - i);
// 把最后一位元素赋为null,等待GC
es[size = newSize] = null;
}
二、根据值来删除元素
public boolean remove(Object o) {
// 把集合的数组地址赋给局部变量es
final Object[] es = elementData;
final int size = this.size;
// 定义初始变量i = 0
int i = 0;
// goto语句
found: {
// 如果要查找的元素为null,那么找到第一个为null的元素,然后返回到found
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else { // 找到与传入的值相等的元素,这里的相等是对象的地址必须相等,因为调用的是Object的equals方法
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
// 套外,传入数组与相应的要删除元素的位置
fastRemove(es, i);
return true;
}
2.8 迭代器
点进源码可以看到调用iterator()方法的时候其实是返回了一个私有内部类
public Iterator<E> iterator() {
return new Itr();
}
那么我们重点分析这个私有内部类
首先我们认识他的字段
字段分析
字段 | 含义 |
---|---|
cursor | 记录当前位置,我们记做光标 |
lastRet = -1 | 记录当前的前一个元素位置 |
expectedModCount = modCount | 将集合实际修改次数赋值给预期修改次数 |
private class Itr implements Iterator<E> {
// 记录当前位置
int cursor; // index of next element to return
// 记录当前的前一个元素位置
int lastRet = -1; // index of last element returned; -1 if no such
// 将集合实际修改次数赋值给预期修改次数
int expectedModCount = modCount;
// prevent creating a synthetic constructor
Itr() {}
// 判断集合中是否还有元素
public boolean hasNext() {
// 这里的size是ArrayList中的size,内部类是可以直接调用外部类的字段的
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
// 检查集合的修改次数和局部变量的修改次数是否相同
checkForComodification();
// 将当前位置给i
int i = cursor;
// 判断i是否超过了集合中的元素个数
if (i >= size)
throw new NoSuchElementException();
// 把集合存储数据数组的地址赋值给该方法的局部变量
Object[] elementData = ArrayList.this.elementData;
// 在多线程环境下,防止i的值超过了数组的最大长度
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 将光标向后移动一位
cursor = i + 1;
// 把i赋值给lastRet,这时lastRet表示前一个元素(因为此时cursor已经+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
public void forEachRemaining(Consumer<? super E> action) {
// ......
}
// 这个方法就是为了防止并发操作导致集合异常,比如说线程A在使用
// 迭代器进行遍历,线程B删除了集合中的一个元素,那么此时线程A的expectedModCount值和集合的modCount就会不一样,这时就会抛出并发异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
异常分析
可能有很多小伙伴还是不清楚,什么时候会发生ConcurrentModificationException(),这里我们举个简单的例子
public static void main(String[] args) {
ArrayList<String > arrayList = new ArrayList<>(0);
arrayList.add("陈1");
arrayList.add("陈2");
arrayList.add("陈3");
arrayList.add("陈4");
arrayList.add("陈5");
arrayList.add("陈6");
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("陈3")) {
arrayList.remove("陈3");
}
}
System.out.println(arrayList);
}
可以看到这里发生了ConcurrentModificationException错误,为什么呢?
我们打上断点进去查看
当代码运行到这一行
我们进入源码查看值
然后调用next方法
这时就会抛出ConcurrentModificationException(),多线程并发亦是如此,多个线程操作同一个ArrayList会导致modCount增大,但是迭代器中的expectedModCount却没有更新
那么我们在迭代的时候就不能删除元素了吗?
其实不是,我们需要调用Itr中的remove方法
Itr中的remove方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 检查expectedModCount 是否等于 modCount
checkForComodification();
try {
// 调用ArrayList的remove方法删除光标的前一个元素
ArrayList.this.remove(lastRet);
// 移动光标向前移动一位
cursor = lastRet;
// 重新赋值为-1
lastRet = -1;
// 修改期望修改次数使其等于修改计数器
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
测试:
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("陈3")) {
iterator.remove();
}
}
这样就可正常运行了
2.9 clear方法
调用clear方法可以清空集合中的所有元素
public void clear() {
// 修改计数器+1
modCount++;
// 把数组地址赋给局部变量es
final Object[] es = elementData;
// 让每一个元素都为null,这样可以方便垃圾回收器尽早地回收
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;
}
测试:
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("陈3")) {
iterator.remove();
}
}
看运行结果图,没有抛出异常了
这样就可正常运行了
2.9 clear方法
调用clear方法可以清空集合中的所有元素
public void clear() {
// 修改计数器+1
modCount++;
// 把数组地址赋给局部变量es
final Object[] es = elementData;
// 让每一个元素都为null,这样可以方便垃圾回收器尽早地回收
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;
}
这里代码虽然简单但是需说明一下
为什么不直接es = null呢,而是遍历每一个元素让其等于null呢?
其实就是为了方便垃圾回收器尽早地回收