那一天,我看着电脑里的一堆笔记陷入沉思,有一个声音在我脑海浮现
“Co Co Da Yo !”,“你走开!”
“想明白生命的意义吗?想真正的活着吗?”,“大哥你走错片场了吧?”
“要不把笔记整理一下,写点文章?” ,“试试?” ,“干就完事了!奥利给!”
引言
对于一个从小学到高中都在为作文凑字数的人来说,写文章真是一件困难的事情,现在还在一个字一个字往外扣,以后再也不催火星更新逆天邪神了,哈哈。
上一篇博文我们了解了ArrayList,这篇博文我们来了解一下LinkedList,细细体会它们之间的区别。
正文
“咦 你这个小伙子有点面熟 昨天那个ArrayList是你答的吧?”
“是的”
“不错不错,那今天一定要好好考考你(虐死你)”
“…”
简单聊聊LinkedList
LinkedList当初都是和ArrayList一起看的 easy~ easy~
- LinkedList底层是双向链表,链表中的每一个node结点都存储了自己的前驱后驱。
- LinkedList允许存入null,且有序(插入取出有序)。
- LinkedList不是线程安全的。
源码分析
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;
}
}
说一下linkedList插入一个数据的底层实现
这不是手到擒来~
linkedList包含 first 和 last 两个Node引用,分别用来指向链表的头结点和尾结点。
第一次add操作时,会将 first 和 last 都指向第一个结点,
随后每一次add操作都会将新结点的prev指向last引用的Node,将last引用的Node的next指向新结点,然后将last引用指向新结点。
举个例子:在一个LinkedList中依次插入值666,777,888
画的是真的丑
源码分析
add操作
//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
public boolean add(E e) {
linkLast(e); //在链表尾部插入
return true;
}
void linkLast(E e) {
final Node<E> l = last; //last是链表的尾结点
final Node<E> newNode = new Node<>(l, e, null); //在尾结点后创建一个新结点
last = newNode; //新节点设置为尾节点
if (l == null)
first = newNode; //first为链表的头结点,链表为空时,将插入的节点设置为头结点。
else
l.next = newNode;
size++; //链表的大小加1
modCount++;
}
public void add(int index, E element) {
checkPositionIndex(index); //检测下标是否越界
if (index == size)
linkLast(element);
else
linkBefore(element, node(index)); //在index下标节点前插入element结点
}
Node<E> node(int index) { //遍历获取第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;
}
}
get操作时会判断当前索引的位置,如果在链表前半段则从前向后遍历,反之则从后向前遍历。
public E get(int index) {
//越界抛出异常
checkElementIndex(index);
return node(index).item;
}
// 返回指定索引下的元素的非空节点
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;
}
}
Tip :又出现了 modCount,之前在ArrayList简单提了一下,这里详细讲一下,这其实是集合的fail-fast 机制。
fail-fast是Collection中的一种错误机制。
在用迭代器遍历一个集合对象时,如果遍历过程中对集合进行了增删(非迭代器自带的增删),会改变modCount,集合迭代器的 next( )会判断modCount是否发生改变,发生改变则会抛出Concurrent Modification Exception。
这种情况在多线程时也会发生,当一个线程在遍历时,另一个线程增删了集合,也会导致modCount发生改变,抛出Concurrent Modification Exception。
fail-fast防止在遍历集合时,集合内的数组发生改变。
大部分list,set在迭代时都会对modcount值进行判断, 但是CopyOnWriteArrayList、CopyOnWriteArraySet不会,原因后续会讲。
面试官脸上露出了熟悉的微笑 ~
如果我要往List里面存放数据,你怎么判断用ArrayList还是LinkedList
哼哼哼,我清了清嗓子,准备好之后的连环炮~
如果不能确定List的大小,在频繁插入的情况下建议使用LinkedList,因为ArrayList随着数组增大是需要对数组扩容的,而扩容就涉及到内存的分配,频繁的扩容会增大虚拟机的负荷(消耗时间和空间)。
如果顺序插入删除频繁,ArrayList理论上会快,因为ArrayList底层是数组,内存是提前分配的。而LinkedList每次都需要创建Node对象,所以理论上是慢于ArrayList的。
如果插入删除的在数组前半段,可以考虑使用LinkedList,如果插入删除的在数组后半段,可以考虑使用ArrayList,因为ArrayList会调用arraycopy方法,这个方法会拷贝插入删除位置后的数据,所以插入的位置越靠前,插入的需要拷贝的数据越多,而LinkedList只需要改变node结点前后的引用地址即可。
说说怎么遍历ArrayList和LinkedList
ArrayList建议使用普通的for循环遍历,LinkedList要使用foreach循环也就是使用迭代器遍历。
List<Integer> list = new ArrayList<>();
//普通for循环
for (int i=0; i<list .size(); i++){
System.out.println(list .get(i));
}
//foreach循环,使用迭代器遍历
for (Integer i : list ){
System.out.println(i);
}
ArrayList它实现了RandomAccess 标记接口,用于标明实现该接口的List支持快速随机访问。
LinkedList使用for循环每次调用get(i)都会从链表头部遍历链表到i结点处,效率低下。使用foreach循环,迭代器遍历时每一次调用next()方法都会保存当前结点的引用,每一次都是从当前结点向下遍历。
Tip: 标记接口的作用是当某个类实现了这个接口,我们就认为这个类拥有了某种功能,我们不需要实现任何的方法。常见的标记接口还有Cloneable(可调用clone()方法)、Serializable(可以实现序列化)。
这怎么难得倒我,我笑嘻嘻的看着面试官。
面试官(内心:被小瞧了!!不行!!),突然他灵光一闪~
说一下LinkedList的迭代器和ArrayList有什么不同
裂开! 这可真难到我了!还好我昨天和陈小志夜谈,他提到了这一点
ArrayList使用的是Iterator(ArrayList也有ListIterator)而 LinkedList 使用的是ListIterator。
- ListIterator 在遍历的时候可以向 List 中添加和修改对象,而 Iterator 不能。
- ListIterator 和 Iterator 都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。
- ListIterator可以通过nextIndex()和previousIndex()定位当前索引的位置,Iterator没有此功能。
面试官(算了…只能下回找回场子了)
你之前说ArrayList和LinkedList不是线程安全的,那你说说怎么实现List的线程安全。
结束语
下一章会说说线程安全的List,希望大家能有所收获。
PS:上一篇博文一天的阅读量有三十,希望明天能上一百。
良师益友(推荐几个大佬)
@敖丶丙 @五月的仓颉 @占小狼