谈谈Java容器-LinkedList

之前的文章中,我们谈到了ArrayList的源码分析,在今天的文章中,我们来看一种和ArrayList非常相似的Java容器——LinkedList。

链表

谈到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节点中存储一个值item,还有指向前后节点的prevnext,我们通过一张图来描述一下节点之间的关系。由下图我们可以知道LinkedList的底层实现是双向链表。

Node节点之间的关系

成员变量和构造方法

transient int size = 0;

transient Node<E> first;

transient Node<E> last;

public LinkedList() {
}

很尴尬,构造方法什么都没有。就谈谈三个成员变量吧,说实话,我觉得从变量名大家都能看出来size是LinkedList的长度,firstlast分别是LinkedList的头节点和尾节点。我们看一下调用了构造方法之后的运行时数据区的情况。

构造方法被调用后的运行时数据区

添加元素

我们首先尝试添加写一个添加元素的代码。

public static void main(String[] args) {
	LinkedList<String> list = new LinkedList<>();
	list.add("路人甲");
	list.add("路人乙");
}

add方法中调用了一个linkLast方法进行真实的链表节点插入操作,我们尝试插入第一个元素,传入一个String类型的值“路人甲”,看一下linkLast进行了哪些操作。

public boolean add(E e) {
    linkLast(e);
    return true;
}

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++;
}

由于构造方法中没有对成员变量做任何操作,所以last的值是null,也就是说l被赋值为null。接着创建了一个新的节点,这个节点的prev指向l(也就是为空),next也为空。接着LinkedList的last指向了我们新创建的节点,然后我们需要判断l是否为空,这里是空值,所以我们将first也指向新节点。最后将链表长度加1,操作次数加1。

添加一个元素后的运行时数据区

我们尝试再添加一个元素“路人乙”,由于插入了第一个元素,last指向了第一个元素,也就是l指向第一个元素。我们再创建一个新的node节点,prev指向l(也就是第一个节点),next仍然为空,再将last指向第二个节点。此时由于l已经不为空,所以判断语句会走else,也就是将第一个节点的next指向第二个节点,再次对链表长度和修改次数加1。看一下这时候内存区域的状态。

添加两个元素后的运行时数据区

再次添加元素的操作基本和第二次添加元素差不多,就是引用指向节点和链表长度的变化,这里不再赘述。

删除元素

我们来尝试一下删除元素“路人甲”。

public static void main(String[] args) {
	LinkedList<String> list = new LinkedList<>();
	list.add("路人甲");
	list.add("路人乙");
	list.remove("路人甲");
}

查看一下remove的源码部分,首先判断传入的元素是否为空,若为空则从链表头部开始查找节点值为空的元素,如果找到了就把进入unlink方法并返回true;若不为空就从链表头部开始查找节点值为传入的值的元素,如果找到了就把进入unlink方法并返回true;如果都没有找到传入的元素就返回false。由于传入的元素是“路人甲”,应该进入else循环。

public boolean remove(Object o) {
    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方法的实现,代码有点长,希望大家耐心看完。由于我们删除的是“路人甲”,是链表的第一个元素,所以prev为空,first指向路人甲的下一个节点,而next是不为空的,所以路人乙的节点的prev指向一个空值,并将待删除元素的next置为空,元素的值也被置为空,链表长度减1,操作次数加1,返回删除元素的值。

那么如果删除的是“路人乙”呢?路人乙位于链表尾部,因此next为空,prev不为空,路人乙的前一个节点的next也就指向null,当前节点的prev也置为空了。接着把last置为空,并将待删除元素的值置为空,链表长度减1,操作次数加1,返回删除元素的值。

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

以上讲了一些基础的添加与删除元素的操作,LinkedList还有一些其他的增/删元素的操作,比如在指定位置插入元素等,在指定位置操作元素就需要进行下标校验,原理上没有太大区别,大家可以自行阅读一下源码。

猜你喜欢

转载自blog.csdn.net/qq_32273417/article/details/106572387