Joshua Bloch:我写了 LinkedList,但我自己都不用!
对,Joshua Bloch 就是 LinkedList 的作者!
如果你真信了作者的话,那就真的大错特错了,LinkedList 虽然用的没有 ArrayList 多,但使用它的地方可不少。
我在技术派这个开源项目的引用的第三方依赖中,随便搜了一下引用,好多好多地方在用呢,比如说大名鼎鼎的 Jackson。
所以,遇到一个问题,比如说:ArrayList 和 LinkedList 之间应该怎么选择?你应该先去思考,两者到底有什么区别,搞清楚事情的本质,再去做出选择。
下面我们就通过对话+源码的形式彻底来搞清楚两者之间的区别!
“终于,哥,我们要聊 LinkedList 和 ArrayList 之间的差别了,我期待了很久。”三妹嘟囔着说。
“其实经过前面两节的分析,差别已经很清晰了。”我喃喃道。
“哥,你再说点吧,深挖一下,OK?”
“好吧,那就让我们出发吧!”
PS:为了和前面两节的源码做适当的区分,这里采用的是 Java 11 的源码,请务必注意。但整体上差别很小。
01、ArrayList 是如何实现的?
ArrayList 实现了 List 接口,继承了 AbstractList 抽象类。
底层是基于数组实现的,并且实现了动态扩容(当需要添加新元素时,如果 elementData 数组已满,则会自动扩容,新的容量将是原来的 1.5 倍),来看一下 ArrayList 的部分源码。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10; // 默认容量为 10
transient Object[] elementData; // 存储元素的数组,数组类型为 Object
private int size; // 列表的大小,即列表中元素的个数
}
ArrayList 还实现了 RandomAccess 接口,这是一个标记接口:
public interface RandomAccess {
}
内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。而 LinkedList 没有实现该接口,表示它不支持高效的随机访问,需要通过遍历来访问元素。
/**
* 返回列表中指定位置的元素。
*
* @param index 要返回的元素的索引
* @return 列表中指定位置的元素
* @throws IndexOutOfBoundsException 如果索引越界(index < 0 || index >= size())
*/
public E get(int index) {
Objects.checkIndex(index, size); // 检查索引是否越界
return elementData(index); // 调用 elementData 方法获取元素
}
/**
* 返回列表中指定位置的元素。
* 注意:该方法并没有检查索引是否越界,调用该方法前需要先检查索引是否越界。
*
* @param index 要返回的元素的索引
* @return 列表中指定位置的元素
*/
E elementData(int index) {
return (E) elementData[index]; // 强制类型转换,将 Object 类型转换为 E 类型
}
ArrayList 还实现了 Cloneable 接口,这表明 ArrayList 是支持拷贝的。ArrayList 内部的确也重写了 Object 类的 clone() 方法。
/**
* 返回该列表的浅表副本。
* (元素本身不会被复制。)
*
* @return 该列表的副本
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone(); // 调用 Object 类的 clone 方法,得到一个浅表副本
v.elementData = Arrays.copyOf(elementData, size); // 复制 elementData 数组,创建一个新数组作为副本
v.modCount = 0; // 将 modCount 置为 0
return v; // 返回副本
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
ArrayList 还实现了 Serializable 接口,同样是一个标记接口:
public interface Serializable {
}
内部也是空的,标记“实现了这个接口的类支持序列化”。序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。
眼睛雪亮的小伙伴可能会注意到,ArrayList 中的关键字段 elementData 使用了 transient 关键字修饰,这个关键字的作用是,让它修饰的字段不被序列化。
这不前后矛盾吗?一个类既然实现了 Serilizable 接口,肯定是想要被序列化的,对吧?那为什么保存关键数据的 elementData 又不想被序列化呢?
这还得从 “ArrayList 是基于数组实现的”开始说起。大家都知道,数组是定长的,就是说,数组一旦声明了,长度(容量)就是固定的,不能像某些东西一样伸缩自如。这就很麻烦,数组一旦装满了,就不能添加新的元素进来了。
ArrayList 不想像数组这样活着,它想能屈能伸,所以它实现了动态扩容。一旦在添加元素的时候,发现容量用满了 s == elementData.length,就按照原来数组的 1.5 倍(oldCapacity >> 1)进行扩容。扩容之后,再将原有的数组复制到新分配的内存地址上 Arrays.copyOf(elementData, newCapacity)。
这部分源码我们在之前讲 ArrayList 的时候就已经讲的很清楚了,这里就一笔带过。
动态扩容意味着什么?
意味着数组的实际大小可能永远无法被填满的,总有多余出来空置的内存空间。
比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧?
序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。
于是,ArrayList 做了一个愉快而又聪明的决定,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化。
/**
* 将此列表实例的状态序列写入指定的 ObjectOutputStream。
* (即,保存这个列表实例到一个流中。)
*
* @param s 要写入的流
* @throws java.io.IOException 如果写入流时发生异常
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject(); // 写出对象的默认字段
// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size); // 写出 size
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]); // 依次写出 elementData 数组中的元素
}
}
从 writeObject 方法的源码中可以看得出,它使用了 ArrayList 的实际大小 size 而不是数组的长度(elementData.length)来作为元素的上限进行序列化。
此处应该有掌声啊!不是为我,为 Java 源码的作者们,他们真的是太厉害了,可以用两个词来形容他们——殚精竭虑、精益求精。
这是readO