文章目录
- ArrayList实现原理
- 初始化
- 扩容方式
- add()方法
- 实现接口
- 线程安全与否
- LinkedList实现原理
- 初始化
- add()方法
- 实现接口
- 线程安全与否
- 总结:面试如何介绍ArrayList和LinkedList
ArrayList实现原理
初始化
ArrayList的底层是一个动态数组,初始化时,ArrayList首先会对传进来的初始化参数initalCapacity进行判断:
- 如果参数等于0,则将数组初始化为一个空数组,
- 如果不等于0,将数组初始化为一个容量为10的数组。
扩容方式
当数组的大小大于初始容量的时候(比如初始为10,当添加第11个元素的时候),就会进行扩容,新的容量为旧的容量的1.5倍。
扩容的时候,会以新的容量建一个原数组的拷贝,修改原数组,指向这个新数组,原数组被抛弃,会被GC回收。
add()方法
对于顺序插入,ArrayList特别快,没什么好说的。
这里想说的的是随机插入,随机插入的实现方式为:原来数组插入位置后面的元素按顺序复制到原数组插入位置+1的位置。
public void add(int index, E element) {
/*判断插入的索引是否符合ArrayList范围,在0 和 size之间,size是ArrayList实际元素个数,不包括底层数组的null元素*/
rangeCheckForAdd(index);
/*扩容机制:判断添加是否需要进行扩容*/
ensureCapacityInternal(size + 1); // Increments modCount!!
/*将旧数组拷贝到一个新数组中,参数:被复制的原数组, 被复制数组的第几个元素开始复制, 复制的目标数组, 从目标数组index + 1位置开始粘贴, 复制的元素个数,*/
System.arraycopy(elementData, index, elementData, index + 1, size - index);
/*将新元素赋予该下标*/
elementData[index] = element;
/*元素个数+1*/
size++;
}
实现接口
ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。
为什么ArrayList里面的elementData为什么要用transient来修饰?
看源码可以知道,ArrayList包含两个私有属性:
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
private transient Object[] elementData;
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
elementData里面不是所有的元素都有数据,因为容量的问题,elementData里面有一些元素是空的,这种是没有必要序列化的。ArrayList的序列化和反序列化依赖writeObject和readObject方法来实现。可以避免序列化空的元素。
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
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 behavioural 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();
}
}
线程安全与否
ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
LinkedList实现原理
初始化
Linkedlist底层是一个双向链表,头尾都包含一个不含任何元素的dummy节点。
//指向头节点的变量first
transient Node<E> first;
//指向尾节点的变量last
transient Node<E> last;
其初始化方法很简单(也就是什么都不用做):
/**
* Constructs an empty list.
*/
public LinkedList() {
}
add()方法
LinkedList包括了头插和尾插(很容易理解):
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
直接使用add()方法,其实就是调用的尾插:
public boolean add(E e) {
linkLast(e);
return true;
}
而带index的插入:
public void add(int index, E element) {
//检查插入位置的合法性
checkPositionIndex(index);
if (index == size)
//如果插入位置在尾部直接使用尾插
linkLast(element);
else
//如果index在前半段就使用头插,否则使用尾插
linkBefore(element, node(index));
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index在前半段就使用头插,否则使用尾插
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
实现接口
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
线程安全与否
同样,也不是线程安全的。可以考虑:
- Collections.synchronizedList(new LinkedList<>());
- ConcurrentLinkedQueue
总结:面试如何介绍ArrayList和LinkedList
- 数据结构:
ArrayList底层是数组,采用懒加载的方式初始化,默认长度是10。添加元素如果超过数组长度,采用1.5倍大小进行扩容。
LinkedList底层是双向链表,使头尾使用不包含有效元素的dummy节点。 - 查询效率:
由于底层结构的关系,ArrayList实现了随机访问,查询更快。而LinkedList查找某个元素的时间复杂度是O(n)。 - 增删效率:
ArrayList对于顺序增加非常快,但是指定索引的增删,需要移动索引后面的元素,效率不高。
LinkedList增删一般来说很快,只需要删除指定节点并且连同前后两个节点即可。但是对于指定了索引的删除,就需要根据索引位置,选择从头节点或者尾节点出发找到索引,然后删除。 - 内存空间占用:
ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间