LInkedList底层源码

LInkedList底层源码

简述
  • 数据结构中存在两种数据结构,线性表和链式存储结构(链表)、
  • 顺序存储结构:Vector,Stack,ArrayList
  • 链式存储结构:linkedList,Queue

继承了AbstartSequentialList实现了List,Deque,Coneable,Serializable

List集合,Deque双端队列,Coneable克隆,Serializable序列化

建立持久化对象,LinkedList中的元素就是一个个节点,而真正的数据则存放在Node中

增和删除操作非常快(复杂度O(1)),查和改操作相对较慢

linkedList的操作单线程安全,多线程不安全

内部接口方法

list接口存在的方法

public interface List<E> extends Collection<E> {
    ...
    // 增
    boolean add(E e);
    void add(int index, E element);
    // 删
    boolean remove(Object o);
    E remove(int index);
    // 改
    E set(int index, E element);
    // 查
    E get(int index);
    ...
}

Linkedlist的成员变量

public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
    
    // 序列化唯一表示 UID
    private static final long serialVersionUID = 876323262645176354L;
    
    // LinkedList的大小,其实就是其内部维护的双向链表存储元素的数量
    transient int size = 0;

    // 头结点,指向第一个节点的指针或引用,默认为 null
    transient Node<E> first;

    // 尾节点,指向最后一个节点的指针或引用,默认为 null
    transient Node<E> last;
    ...
    
}

transient防止序列化,

分别存在链表长度,头节点,尾节点

构造函数
  public LinkedList() {
    }
    无参构造
    
  public LinkedList(Collection<? extends E> c) {
        // 指向无参的构造函数
        this();
        addAll(c);
    }  
    有参构造
    构造包含指定元素的列表集合
内部的Node节点

面试必会之LinkedList源码分析

    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是双向链表,故而从下面的Node节点的两个指针数据,一个指向向上的节点,一个指向向下的节点,相似于ArrayList源码中的next和province

这里的Node必须是静态的,Node在LInkedList类中是一个内部类,若不使用static修饰,那么Node类就是一个普通的内部类,在java中一个普通的内部类在实例化后,默认会引起外部类的引用,这就有可能造成内存的泄露

第一步:E item存储的元素 next指向下一个节点,prev指向上一个节点,内部类的构造方法

增加add方法()
 public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
      
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    
    

add(int index, E element)首先add方法两个参数,index为下标参数,element为元素参数

  • 第一步进行使用checkPositionIndex方法,查看当前节点位置是否存在,如果不存在,则抛出异常

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
对元素的位置下标进行判断
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}
  • 第二步对下标位置和集合长度进行比对,这里的index应该为指针,进行对链表尾部的扩容
   void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

在链表尾部进行尾插法扩容

  • 第三步如果不相等的话
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    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++;
}

在链表中进行断链添加数据再生成新的节点

  • 第四步:nextIndex此处为后移指针,expectedModCount为当前元素的指针

boolean add(E e)进行尾节点判断

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) {
            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)
                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;
    }

boolean addAll()

addAll对象为Collection的集合对象,返回值为下面的方法

首先检查这个下标是否符合,将获得到的集合复制为Object类型

获取转为数组的长度,并进行判断,是否等于0,如果等于0,则该集合在添加过程数据被进行了修改

定义前置节点和后置节点,然后判断是否是链表尾部,如果是,在链表尾部追加数据

尾部的后置节点一定是null,前置节点是队尾,如果不在链表的末端,在链表的中间,则取出index节点,并作为后继节点,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;
        }
    }

然后进行循环依次增加,靠for循环遍历数组,依次指向节点的插入操作,进行类型转换,如果前置节点为空,则newNode为头节点,否则为pred的next的节点

当循环结束,如果后置节点为null,说明此时是在队尾追加,否则实在堆中加入的,则需要更新前置节点和后置节点,然后对数量进行修改

node方法,首先是取出index节点,如果index小于size/2/,则从头部进行寻找,将头节点赋值给x,然后进行遍历,如果index大于等于size/2则从后开始遍历,然后检测index的位置是否合法

addFirst(E e)将元素添加到连头尾部
 public void addFirst(E e) {
        linkFirst(e);
    }
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++;
    }

首先建立一个新的节点,并以头节点为后继节点,然后进行判断,如果链表尾空,则last也指向该节点,否则,将头节点的前去指针指向新节点,也就是指向前一个元素

addLast(E e)将元素添加到链表的尾部
public void addLast(E e) {
        linkLast(e);
    }
获取数据的方法get
public E get(int index) {
        //检查index范围是否在size之内
        checkElementIndex(index);
        //调用Node(index)去找到index对应的node然后返回它的值
        return node(index).item;
    }

检查idnex是否在size中,调用Node(index)找到index对应的node并返回值

获取头节点的数据方法getFirst
public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }
public E element() {
        return getFirst();
    }
public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
 
public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     }

getFirst会在头节点为空时抛出异常,而element内部用的getFirst方法;peek和peekFirst方法在头结点为空时返回null

  • 第一步:定义头节点,如果为空抛出异常,并返回当前节点
  • 第二步:返回第一个节点
获取尾节点的数据getLast()

public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }
 public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;

第一步:getLast()定义尾节点,如果为空,抛出异常,并返回当前节点

这两者的区别也在尾节点为空时是抛出异常还是返回null

根据对象获取得到index方法

int index(Object o)

从头遍历找

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;
    }
  • 第一步:定义指针位置(初始为0)
  • 第二步:对传入对象进行为空判断,传入节点不为空时,进行遍历,定义新节点等于第一个节点,判断条件节点为不为空,遍历节点不断向后,再进行判断,如果遍历当前节点为空时,则返回指针位置,传入节点为空时,同上

int lastIndexOf(Object o): 从尾遍历找

public int lastIndexOf(Object o) {
        int index = size;
        if (o == null) {
            //从尾遍历
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (x.item == null)
                    return index;
            }
        } else {
            //从尾遍历
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    return index;
            }
        }
        return -1;
    }

contains(Object o): 检查对象o是否存在于链表中

public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

从indexOf()中可以看出如果没有此Object会返回-1

删除方法

remove() ,removeFirst(),pop(): 删除头节点

public E pop() {
        return removeFirst();
    }
public E remove() {
        return removeFirst();
    }
public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

removeLast(),pollLast(): 删除尾节点

public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }

remove(Object o): 删除指定元素

public boolean remove(Object o) {
        //如果删除对象为null
        if (o == null) {
            //从头开始遍历
            for (Node<E> x = first; x != null; x = x.next) {
                //找到元素
                if (x.item == null) {
                   //从链表中移除找到的元素
                    unlink(x);
                    return true;
                }
            }
        } else {
            //从头开始遍历
            for (Node<E> x = first; x != null; x = x.next) {
                //找到元素
                if (o.equals(x.item)) {
                    //从链表中移除找到的元素
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

unlink(Node x) 方法:

E unlink(Node x) {
// assert x != null;
final E element = x.item;
final Node next = x.next;//得到后继节点
final Node 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--;
    modCount++;
    return element;
}

本质就是将一个节点删除,然后将原来的前驱节点与原来的后继节点连接起来

remove(int index):删除指定位置的元素

public E remove(int index) {
        //检查index范围
        checkElementIndex(index);
        //将节点删除
        return unlink(node(index));
    }

经过一番学习和测试以后,得出以下结论:这些方法从设计之初,分别来自于集合Collections,队列Queue,栈Stack,双端队列Deque,因此它们是有语义的,不建议笼统归为添加/删除。

  • addremove是一对,源自Collection
  • offerpoll是一对,源自Queue
  • pushpop是一对,源自Deque,其本质是栈(Stack类由于某些历史原因,官方已不建议使用,使用Deque代替);
  • offerFirst/offerLastpollFirst/pollLast是一对,源自Deque,其本质是双端队列。

那为什么这些方法,全都出现在LinkedList/Deque中呢,那是由它们的继承关系导致的,请看下图。

img

image.png

关注圈住的部分,接口Deque继承了以上所有的方法,而类LinkedList实现了以上所有的方法。
注:由于历史原因,在Java中,官方不建议使用Stack类,而是使用Deque代替,也就是说,接口Deque是栈和双端队列这两种数据结构的集合体。

说了这么多,这一堆方法到底有什么区别?其实从他们的出处便可以快速区分并且牢记他们的不同之处。

  • add/remove源自集合,所以添加到队尾,从队头删除;
  • offer/poll源自队列(先进先出 => 尾进头出),所以添加到队尾,从队头删除;
  • push/pop源自栈(先进后出 => 头进头出),所以添加到队头,从队头删除;
  • offerFirst/offerLast/pollFirst/pollLast源自双端队列(两端都可以进也都可以出),根据字面意思,offerFirst添加到队头,offerLast添加到队尾,pollFirst从队头删除,pollLast从队尾删除。
    总结:
  • add/offer/offerLast添加队尾,三个方法等价;
  • push/offerFirst添加队头,两个方法等价。
  • remove/pop/poll/pollFirst删除队头,四个方法等价;
  • pollLast删除队尾。

虽说某几个方法等价,但是我们在使用的时候,建议根据用途来使用不同的方法,比如你想把LinkedList当做集合list,那么应该用add/remove,如果想用作队列,则使用offer/poll,如果用作栈,则使用push/pop,如果用作双端队列,则使用offerFirst/offerLast/pollFirst/pollLast。根据语义使用,就不会发生:我想删队尾,结果删了队头这种事了。

出 => 尾进头出),所以添加到队尾,从队头删除;

  • push/pop源自栈(先进后出 => 头进头出),所以添加到队头,从队头删除;
  • offerFirst/offerLast/pollFirst/pollLast源自双端队列(两端都可以进也都可以出),根据字面意思,offerFirst添加到队头,offerLast添加到队尾,pollFirst从队头删除,pollLast从队尾删除。
    总结:
  • add/offer/offerLast添加队尾,三个方法等价;
  • push/offerFirst添加队头,两个方法等价。
  • remove/pop/poll/pollFirst删除队头,四个方法等价;
  • pollLast删除队尾。

虽说某几个方法等价,但是我们在使用的时候,建议根据用途来使用不同的方法,比如你想把LinkedList当做集合list,那么应该用add/remove,如果想用作队列,则使用offer/poll,如果用作栈,则使用push/pop,如果用作双端队列,则使用offerFirst/offerLast/pollFirst/pollLast。根据语义使用,就不会发生:我想删队尾,结果删了队头这种事了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值