Java集合——LinkedList

1. LinkedList概述

  LinkedList是我们除了ArrayList外,我们常用的列表结构;根据名字我们大致也能拆除其主要依赖链表实现,和ArrayList不同,因为基于链表实现,因此,插入和删除会比较快速,因为不会涉及到数组中元素的移动;但是,依据索引访问元素会相对较慢。因此,使用哪种List需要根据实际情况确定,另外,LinkedList也是线程不安全的,多线程的情况下使用需要注意。

2. LinkedList源码

2.1 LinkedList定义

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

  根据LinkedList的声明代码,可以看到它除了实现List接口外,还实现了Deque接口,而Deque主要是一个双端队列,因此LinkedList除了可以作为List使用外,还可以当做队列使用。

2.2 成员变量

// 元素个数
transient int size = 0;
// 头结点
transient Node<E> first;
// 尾节点
transient Node<E> last;

  LinkedList只有元素个数,和头结点、尾节点三个成员变量,而其成员变量均被transient修饰,其序列化也是通过writeObject(java.io.ObjectOutputStream s)readObject(java.io.ObjectInputStream s)两个方法实现的。
  另外,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;
    }
}

2.3 构造方法

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

  LinkedList主要有两个构造方法,一个无参构造方法,一个初始化时可以传入一个集合。因为,其基于链表结构,所以不需要指定初始化容量,可以任意添加。

2.4 List相关方法

2.4.1 添加方法

  上边已经看到LinkedList实现了ListDeque两个接口,其中就应该实现两个接口的方法;关于添加元素,首先我们看一下和ArrayList中方法名相同的方法:

// 添加元素
public boolean add(E e) {
    linkLast(e);
    return true;
}
// 在指定位置添加元素
public void add(int index, E element) {
	// 验证 index 是否在 size范围内
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

  上边两个方法,在ArrayList中,我们也已经见过了,因为LinkedList使用的是链表结构,添加元素默认在链表尾部添加:

void linkLast(E e) {
	// 获取最后一个节点
    final Node<E> l = last;
    // 新建一个节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 将成员变量last指向新建的节点
    last = newNode;
    // 如果之前最后一个节点为null,说明一个元素也没有,first也指向新建的节点
    if (l == null)
        first = newNode;
    else
    	// 将原来最后一个节点的next设置为新建节点。
        l.next = newNode;
    size++;
    modCount++;
}

  关于在指定索引添加元素,虽然,之前说过LinkedList使用链表结构,添加元素会相对较快,但是由于使用链表,对于索引的操作又会相对较慢,因为不能根据索引,立马确定应该插入的位置,因此,我们需要先确定指定索引处的节点。

Node<E> node(int index) {
	// 首先确定index,在size的前一半还是后一半
    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;
    }
}

  上边的方法,通过遍历链表,找到了指定索引处的元素;其中,首先判断给定的索引,是在链表的前一半还是后一半,来减少了循环的次数。
  获取到该索引的节点之后,我们就可以执行插入操作了,在该节点之前插入:

// 在给定节点的之前插入节点
void linkBefore(E e, Node<E> succ) {
    // 获取前一个节点pred
    final Node<E> pred = succ.prev;
    // 新建节点,并指定该节点的前一个节点为pred,后一个节点为succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改succ的前一个节点为新建的节点newNode
    succ.prev = newNode;
    
    if (pred == null)
        // 如果 pred 为null,说明索引位置可能是first节点,设置fisrt为新建节点
        first = newNode;
    else
    	// 修改pred的后一个节点为newNode
        pred.next = newNode;
    size++;
    modCount++;
}

  根据以上两个方法,在添加单个元素的情况下,默认是从队列末尾位置开始添加,此时需要注意,last == null也就是可能是初始化之后第一次添加,需要设置first值;如果指定在某个索引位置添加元素,需要注意在队首添加的情况,需要重新设置first的值。

  上边都是添加单个元素,在ArrayList中,还有两个添加集合的方法,在LinkedList也有相同的方法:

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
	// 验证索引是否在size范围内
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

	// 创建两个变量
    Node<E> pred, succ;
    if (index == size) {
    	// 如果是从队尾添加,pred指向last节点
        succ = null;
        pred = last;
    } else {
    	// 如果不是,succ指向index处的节点;pred为前一个节点
        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;
    }

	// 连接 succ 节点
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}

2.4.2 删除方法

// 移除指定的元素
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;
}
// 	移除指定索引的元素
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

  无论是删除指定的元素,还是删除指定的索引,均需要先找到对应的节点Node,然后,执行删除节点操作:

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

  删除节点时,也比较简单,修改前一个节点的next和后一个节点的prev就可以了,但是需要注意头结点和尾节点的情况。
  除了删除单个元素外,还有一个删除所有元素的方法:

public void clear() {
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}

  主要方法:遍历链表,将每个节点的元素和前后连接设置为null,最后情况first和last节点;将元素个数size设置为0.

2.4.3 修改元素

public E set(int index, E element) {
	// 验证索引是否在 size 范围内
    checkElementIndex(index);
    // 获取指定索引处的节点
    Node<E> x = node(index);
    // 获取该节点原来的旧值,用于返回
    E oldVal = x.item;
    // 设置新值
    x.item = element;
    return oldVal;
}

2.4.4 查询方法

回去指定索引的元素值:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

判断指定元素是否存在:

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

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

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

   LinkedList中,实现自List的方法和ArrayList中的基本相同,但是,因为他们实现方式的不同,LinkedList基于索引的操作会相同较慢,根据索引找节点需要通过遍历的方式,而不能像ArrayList中一样,直接获取;但是,插入和删除相对较快,不会涉及到元素的移动,不过需要注意头结点和尾节点的边界处理。

2.5 Deque相关方法

  上边已经说过,LinkedList不仅实现了List接口,还是先Deque接口,因此,其中还有部分队列相关的方法:

2.5.1 添加方法

// 从队列头添加元素
public void addFirst(E e) {
    linkFirst(e);
}
// 和上边的add(E e)方法相同,从末尾添加
public void addLast(E e) {
    linkLast(e);
}

  在add(E e)方法中,默认是从队列尾部开始添加,但是,作为双端队列,LinkedList又提供了队列头部添加的方法,可以根据需要使用。除此之外,还有其他的添加方法:

// 添加元素默认从队列尾部添加
public boolean offer(E e) {
    return add(e);
}
// 从队列头添加元素,返回boolean类型
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

public void push(E e) {
    addFirst(e);
}

2.5.2 移除元素

public E remove() {
    return removeFirst();
}
// 移除队列头元素
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
// 移除末尾元素
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

// 出队列,移除队首元素
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// 移除队尾元素
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}

// 移除队首元素
public E pop() {
    return removeFirst();
}

// 移除指定元素第一次出现的节点
public boolean removeFirstOccurrence(Object o) {
    return remove(o);
}
// 移除指定元素最后一次出现的节点
public boolean removeLastOccurrence(Object o) {
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

2.5.3 获取元素

// 获取队首元素值
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
public E element() {
    return getFirst();
}
public E peekFirst() {
   final Node<E> f = first;
   return (f == null) ? null : f.item;
}
// 获取队尾元素
public E peekLast() {
   final Node<E> l = last;
   return (l == null) ? null : l.item;
}

  根据上边的方法,由于LinkedList实现双端队列,使用push(),pop(),peek()几个方法可以将LinkedList当做栈使用;也可以使用add()/offer(),poll(),peek()将LinkedList当做先进先出(队尾进队,队首出队)的队列Queue使用。

总结

  LinkedList作为List集合,其中的元素有序,可重复,可以为null;此外,LinkedList还是先Deque接口,而Deque继承Queue接口,因此LinkedList还可以作为FIFO队列使用,而且还可以作为双端队列使用,提供了各种队首队尾元素的操作。另外,LinkedList也是线程不安全的,多线程条件下使用需要注意。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值