简述
按照上篇笔记ArrayList集合继续进行介绍list的另一个常见子类LinkedList
?LinkedList介绍
1.数据结构
说明:linkedlist的底层数据结构是个双向链表结构,也意味着linkedlist在进行查询时效率会比ArrayList的慢,而插入和删除只是对指针进行移动,相对于ArrayList就会快很多
2.源码分析
2.1类的继承关系
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
2.2类的属性
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { // 实际元素个数 transient int size = 0; // 头结点 transient Node<E> first; // 尾结点 transient Node<E> last; } 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的结构中,一个头结点,一个尾节点,一个表示链表中实际元素个数的变量。注意,头结点、尾结点都有transient关键字修饰,这也意味着在序列化时该域是不会序列化的。
2.3构造函数
public LinkedList() { } public LinkedList(Collection<? extends E> c) { // 调用无参构造函数 this(); // 添加集合中所有的元素 addAll(c); }
说明:会调用无参构造函数,并且会把集合中所有的元素添加到LinkedList中。
2.4核心函数
1.add函数
public boolean add(E e) { // 添加到末尾 linkLast(e); return true; } void linkLast(E e) { // 保存尾结点,l为final类型,不可更改 final Node<E> l = last; // 新生成结点的前驱为l,后继为null final Node<E> newNode = new Node<>(l, e, null); // 重新赋值尾结点 last = newNode; if (l == null) // 尾结点为空 first = newNode; // 赋值头结点 else // 尾结点不为空 l.next = newNode; // 尾结点的后继为新生成的结点 // 大小加1 size++; // 结构性修改加1 modCount++; }
举个栗子
说明:初始化状态效果
说明:linkedlist允许传入值是重复的,并且也允许为null
2.add(int index, E element)函数
// 插入元素 public void add(int index, E element) { checkPositionIndex(index); // 检查是否越界 if (index == size) // 在链表末尾添加 linkLast(element); else // 在链表中间添加 linkBefore(element, node(index)); }
void linkBefore(E e, Node<E> succ) { final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
说明:通过先判断index的合法性,然后再与size进行比较,如果等于size的话,就相当于直接调用了addlast方法,若不是,则进行中间插入操作。
3.addAll函数
public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } // 添加一个集合 public boolean addAll(int index, Collection<? extends E> c) { // 检查插入的的位置是否合法 checkPositionIndex(index); // 将集合转化为数组 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 { // 插入位置为其他某个位置 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) // 表示在第一个元素之前插入(索引为0的结点) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { // 表示在最后一个元素之后插入 last = pred; } else { pred.next = succ; succ.prev = pred; } // 修改实际元素个数 size += numNew; // 结构性修改加1 modCount++; return true; }
说明:addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我们平时习惯调用的addAll(Collection<? extends E>)型会转化为addAll(int, Collection<? extends E>)型。
参数中的index表示在索引下标为index的结点(实际上是第index + 1个结点)的前面插入。在addAll函数中,addAll函数中还会调用到node函数,get函数也会调用到node函数,此函数是根据索引下标找到该结点并返回,具体代码如下
Node<E> node(int 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; // 返回该结点 } }
说明:在根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。
4.indexOf函数
public int indexOf(Object o) { int index = 0; if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; }
说明:indexOf操作非常简单,就是从头开始遍历整个链表,如果没有就反-1,有就返回当前下标
举个栗子
5.remove函数
public E remove() { return removeFirst(); } public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } E unlink(Node<E> x) { // 保存结点的元素 final E element = x.item; // 保存x的后继 final Node<E> next = x.next; // 保存x的前驱 final Node<E> prev = x.prev; if (prev == null) { // 前驱为空,表示删除的结点为头结点 first = next; // 重新赋值头结点 } else { // 删除的结点不为头结点 prev.next = next; // 赋值前驱结点的后继 x.prev = null; // 结点的前驱为空,切断结点的前驱指针 } if (next == null) { // 后继为空,表示删除的结点为尾结点 last = prev; // 重新赋值尾结点 } else { // 删除的结点不为尾结点 next.prev = prev; // 赋值后继结点的前驱 x.next = null; // 结点的后继为空,切断结点的后继指针 } x.item = null; // 结点元素赋值为空 // 减少元素实际个数 size--; // 结构性修改加1 modCount++; // 返回结点的旧元素 return element; } private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; //获取删除节点的元素值(f是头结点) final Node<E> next = f.next; //存储要删除节点指向的下一个节点地址 f.item = null; f.next = null; // help GC //将要删除节点的指针以及值全部设置为null,等待 垃圾回收 first = next; //将头结点向下移动 if (next == null) last = null; //如果要删除节点的下一个为null,则当前链表只有一个节点存在 else //如果不为null,则将前驱设置为null next.prev = null; size--; modCount++; return element; }
说明:如果直接调无参的remove(),就会默认删除头节点
,删除头节点非常简单,就是把头节点的值清空,next清空
,然后把nextNode只为头节点,然后清空next的prev
,最后size减1
,如果是删除中间节点,调用remove(int index)
,首先判断Index对应的节点是否为头节点,即index是否为0
,如果不是中间节点,就是x的prev指向x的next
。
举个栗子
说明:当LinkedList集合在一边遍历一边进行remove操作时,且当集合元素个数大于2个时,则会发生如下错误:
小结:ArrayList和LinkedList有什么区别?
- ArrayList查询快是因为底层是由数组实现,通过下标定位数据快。写数据慢是因为复制数组耗时。LinkedList底层是双向链表,查询数据依次遍历慢。写数据只需修改指针引用。
- ArrayList和LinkedList都不是线程安全的,小并发量的情况下可以使用Vector,若并发量很多,且读多写少可以考虑使用CopyOnWriteArrayList。因为CopyOnWriteArrayList底层使用ReentrantLock锁,比使用synchronized关键字的Vector能更好的处理锁竞争的问题。