上篇文章说了下,数据和ArrayList,这篇文章咱们说下在面试中有很大几率二者做为兄弟同时出现的LinkedList,但愿你们看完这篇文章后可以有恍然大悟的感受。java
LinkedList底层是链表实现的,那么咱们首先说下什么是链表。node
和上篇文章的数组相比,链表要相对于更复杂一点,二者也是很是基础、经常使用,并且在面试中同时出现的几率也是很大的。面试
上篇文章咱们说到,数据是须要连续的内存空间来存储的,而链表恰好与它相反,链表是不须要连续的内存空间的,它是经过将好多的零散的内存使用“指针”串联起来使用,若是数组和链表都想在计算机中申请大小为10M的内存,而计算机中只有10M的零散内存,那么数组就会申请内存失败,链表就会成功。数组
想对数组和ArrayList有多些了解的小伙伴能够看个人上篇文章 :缓存
[juejin.im/post/5c99c7…]bash
既然链表在内存中都是零散的块,那么咱们是怎么称呼这些小块块呢?数据结构
咱们把这些块叫作“结点”,为了把每一个结点都串联起来,结点中不只会存储数据,还有存储下一个结点的地址。app
那咱们怎么称呼记录下个结点地址的指针呢?函数
咱们把他们叫作“后继指针next”。post
链表中最特殊的是头结点和尾结点,顾名思义,也就是链表的第一个结点和最后一个结点,第一个结点保存着链表的基地址,经过这个结点,咱们就能定位到它,最后一个结点的指针指向的是NULL,说明这个是链表的最后一个结点。
LinkedList中元素的全部操做都是在这样的数据结构上进行的,你们能够脑补下。
链表也能够进行插入、删除和查询操做。数组在进行插入和删除操做的时候,会进行数据的搬移操做,由于数据要保证他的内存空间是连续的,而链表则不须要,由于他的内存空间本就不是连续的 ,它只须要改变相邻结点的指针改变就够了,因此速度是很是快的。
可是查询就不会那么快了,链表查询数据要一个一个的遍历结点,直到找到相应的结点。
链表还有双向链表和循环链表,咱们要说的LinkedList就是一个双向链表,上面说到的单向链表是只存了后一个结点的地址,而双向链表呢,是同时保存了前一个和后一个共两个元素的地址,因此就能够很方便的获取到一个结点的先后两个元素,并且咱们也能够从先后两个方向来遍历。
接下来咱们看下LinkedList的源码:
首先是继承关系:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
复制代码
也是同时继承了Cloneable 和 Serializable ,Deque是一个双端队列,说明在LinkedList中同时也支持对队列的操做。
LinkedList的属性只有三个:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
复制代码
头结点,尾结点,链表中的元素数量,三者都是被 transient关键字修饰的,有小伙伴不理解这个关键字的,欢迎看我上上篇文章:面试问你java中的序列化怎么答?
Node是它的一个内部类,用来保存数据:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
复制代码
接下来,咱们看下核心的 add方法,此次我仍是在每行代码的上面添加上个人注释,帮助你们可以更好的理解。由于个人读者水平不一,因此必需要照顾到全部人:
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
//保存 last 尾结点
final Node<E> l = last;
//将要保存的元素放到 新建一个结点中,
final Node<E> newNode = new Node<>(l, e, null);
// 这样这个新的节点就变成 了尾结点
last = newNode;
// 判断下若是这个尾结点为空,就说明这个链表是空的
//那么这个新的结点就是 首结点。
if (l == null)
first = newNode;
//若是不是空的,那么以前旧的尾结点的 next 保存的就是这个新结点
else
l.next = newNode;
size++;
modCount++;
}
复制代码
接下来,addAll 方法有两个重载函数,前一个是调用的后一个,因此咱们只说一个:
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//首先进行下标合理性检查,下面有这个方法
checkPositionIndex(index);
//将集合转换为 Object 数组
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//定义下标位置的前置结点和后继结点
Node<E> pred, succ;
if (index == size) {
//从尾部添加,前置结点是 以前的尾结点,后继结点为null
succ = null;
pred = last;
} else {
//从指定位置添加,后继结点是下标是index的结点;
//前置结点是下标位置的前一个结点
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
//若是插入位置在头部
first = newNode;
else
//非空链表插入
pred.next = newNode;
pred = newNode; //更新前置结点为最新的结点
}
if (succ == null) {
//若是是从尾部插入的,插入完成后重置尾结点
last = pred;
} else {
//若是不是尾部,那么把以前的数据和尾部链接起来
pred.next = succ;
succ.prev = pred;
}
//集合的原来数量+新集合的数量
size += numNew;
modCount++;
return true;
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
复制代码
咱们再说下根据下标来获取元素的方法 get
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
//元素的下标检查
checkElementIndex(index);
return node(index).item;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
复制代码
从代码中能够看得出来,链表获取指定元素效率仍是很低的,须要将元素遍历才能找到目标元素。须要的时间复杂度是O(n)的,可是咱们在工做中不能仅仅利用复杂度分析就决定使用哪一个数据结构。
数组简单易用,在实现上使用的是连续的内存空间,能够经过CPU的缓存机制,来实现预读,访问速度会比较快。而链表,因为内存不是连续的,因此不能经过这种方法来实现预读。链表自己没有大小限制,自然支持动态扩容,这也是和数组最大的区别。
若是你的程序对内存使用要求很高,那么就能够选择数组,由于链表中的每个结点都须要消耗额外的内存去存储指向下一个结点的指针,因此内存消耗会翻倍,并且对于链表的频繁删除和插入,会致使频繁的内存申请和释放,形成内存碎片,就会引发频繁的GC(Garbage Collection 垃圾回收)。
此次只分析了这几个比较核心的方法源码,你们也能够本身尝试着去看看源码,学习下JDK中代码的风格,多思考下为何要这么写,相信会有很多的进步。
简单分析完以后,咱们总结下问题吧。
ArrayList和LinkedList的区别是什么?(老经典的面试题)
欢迎你们能在留言区中留言,说出你的答案。
若是对本文有任何异议或者说有什么好的建议,能够加我好友(有没有问题都欢迎你们加我好友,公众号后台联系做者),也能够在下面留言区留言,我会及时修改。但愿这篇文章能帮助你们在面试路上乘风破浪。
这样的分享我会一直持续,你的关注、转发和好看是对我最大的支持,感谢。关注我,咱们一块儿成长。
关注公众号,最新的文章会出如今那里哦。