java集合二之LinkedList

一:LinkedList简介

LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,即LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。

LinkedList也和ArrayList一样实现了List接口,但是它执行插入和删除操作时比ArrayList更加高效,因为它是基于链表的。基于链表也决定了它在随机访问方面要比ArrayList逊色一点。

除此之外,LinkedList还提供了一些可以使其作为栈、队列、双端队列的方法。这些方法中有些彼此之间只是名称的区别,以使得这些名字在特定的上下文中显得更加的合适。

二:LinkedList源码解析

对于LinkedList而言,它实现List接口、底层使用Entry保存节点对象。其操作基本上是对双链节点的操作。下面我们来分析LinkedList的源代码:

  1. 私有属性
    LinkedList只定义了两个私有属性:

        private transient Entry<E> header = new Entry<E>(null, null, null);
        private transient int size = 0;

    size肯定就是LinkedList对象里面存储的元素个数了。LinkedList既然是基于链表实现的,那么这个header肯定就是链表的头结点了,Entry就是节点对象了。以下是Entry类的代码。

    private static class Entry<E> {
        E element;
        Entry<E> next;
        Entry<E> previous;
    
        Entry(E element, Entry<E> next, Entry<E> previous) {
            this.element = element;
            this.next = next;
            this.previous = previous;
        }
    }

    只定义了存储的元素、前一个元素、后一个元素,很明显就是双向链表的节点的定义,每个节点只知道自己的上一个节点和下一个节点。

  2. 构造方法
    LinkedList提供了两个构造方法。
    第一个构造方法不接受参数,只是将header节点的前一节点和后一节点都设置为自身,这样整个链表其实就只有header一个节点,用于表示一个空的链表。下面是其源码:

        /**
         * Constructs an empty list.
         */
        public LinkedList() {
            header.next = header.previous = header;
        }

    (注意,这个是一个双向循环链表,如果不是循环链表,空链表的情况应该是header节点的前一节点和后一节点均为null)

    第二个构造方法接收一个Collection参数c,调用第一个构造方法构造一个空的链表,之后通过addAll将c中的元素全部添加到链表中。

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

    接下来我们来看看addAll的内容:

    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
    // index参数指定collection中插入的第一个元素的位置
    public boolean addAll(int index, Collection<? extends E> c) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+size);
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew==0)
            return false;
        modCount++;
        Entry<E> successor = (index==size ? header : entry(index));
        Entry<E> predecessor = successor.previous;
        for (int i=0; i<numNew; i++) {
            Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
            predecessor.next = e;
            predecessor = e;
        }
        successor.previous = predecessor;
        size += numNew;
        return true;
    }

    整个添加过程,其实就是将Collection转换成toArray数组之后进行遍历添加;与C语言中的单双链别无二致。

三:LinkedList的方法解析

  1. 数据存储

    LinkedList提供了add(E e)、add(int index, E element)、addAll(Collection<?extends E> c)、addAll(int index, Collection< ? extends E> c)、addBefore(E e, Entry entry)、addFirst(E e)、addLast(E e)这些添加元素的方法。下面我们一一讲解:

        //添加一个元素,在头之前
        public boolean add(E e) {
            addBefore(e, header);
            return true;
        }
        //在特点位置添加一元素
        public void add(int index, E element) {
            addBefore(element, (index==size ? header : entry(index)));
        }
        //该方法在构造方法中已使用,即批量添加
        public boolean addAll(int index, Collection<? extends E> c) {
            if (index < 0 || index > size)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+size);
            Object[] a = c.toArray();
            int numNew = a.length;
            if (numNew==0)
                return false;
            modCount++;
    
            Entry<E> successor = (index==size ? header : entry(index));
            Entry<E> predecessor = successor.previous;
            for (int i=0; i<numNew; i++) {
                Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
                predecessor.next = e;
                predecessor = e;
            }
            successor.previous = predecessor;
    
            size += numNew;
            return true;
        }
        //调用上一个方法
        public boolean addAll(Collection<? extends E> c) {
            return addAll(size, c);
        }
        //在某节点对象之后添加一对象e,私有的
        private Entry<E> addBefore(E e, Entry<E> entry) {
            Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
            newEntry.previous.next = newEntry;
            newEntry.next.previous = newEntry;
            size++;
            modCount++;
            return newEntry;
        }
        //在首位添加
        public void addFirst(E e) {
            addBefore(e, header.next);
        }
        //在末尾添加
        public void addLast(E e) {
            addBefore(e, header);
        }

    整个数据添加过程,重点在于 addAll和addBefore两个方法的实现上,也就是对节点之间的操作上;

  2. 数据读取
    读取的话主要有 get(int index)、getFirst()、getLast();即获取index索引处数据和首末尾数据,下面看看具体实现:

        public E get(int index) {
            return entry(index).element;
        }
    
        public E getFirst() {
            if (size==0)
                throw new NoSuchElementException();
    
            return header.next.element;
        }
    
        public E getLast()  {
            if (size==0)
                throw new NoSuchElementException();
    
            return header.previous.element;
        }
  3. 数据修改
    数据修改就一个方式,即set(int index, E element),对特定index索引处数据进行覆盖操作

    public E set(int index, E element) {
            Entry<E> e = entry(index);
            E oldVal = e.element;
            e.element = element;
            return oldVal;
        }
  4. 数据删除

        //删除第一个
        public E removeFirst() {
            return remove(header.next);
        }
        //删除最后一个
        public E removeLast() {
            return remove(header.previous);
        }
        //删除某特定对象
        public boolean remove(Object o) {
            if (o==null) {
                for (Entry<E> e = header.next; e != header; e = e.next) {
                    if (e.element==null) {
                        remove(e);
                        return true;
                    }
                }
            } else {
                for (Entry<E> e = header.next; e != header; e = e.next) {
                    if (o.equals(e.element)) {
                        remove(e);
                        return true;
                    }
                }
            }
            return false;
        }
        //清除所有
        public void clear() {
            Entry<E> e = header.next;
            while (e != header) {
                Entry<E> next = e.next;
                e.next = e.previous = null;
                e.element = null;
                e = next;
            }
            header.next = header.previous = header;
            size = 0;
        modCount++;
        }
        //删除特定index处
        public E remove(int index) {
            return remove(entry(index));
        }
  5. 转出数组

        //将本身转换为数组
        public Object[] toArray() {
            Object[] result = new Object[size];
            int i = 0;
            for (Entry<E> e = header.next; e != header; e = e.next)
                result[i++] = e.element;
            return result;
        }
        //将特定数进行转换
        public <T> T[] toArray(T[] a) {
            if (a.length < size)
                a = (T[])java.lang.reflect.Array.newInstance(
                                    a.getClass().getComponentType(), size);
            int i = 0;
            Object[] result = a;
            for (Entry<E> e = header.next; e != header; e = e.next)
                result[i++] = e.element;
    
            if (a.length > size)
                a[size] = null;
    
            return a;
        }

    先判断出入的数组a的大小是否足够,若大小不够则拓展。这里用到了发射的方法,重新实例化了一个大小为size的数组。之后将数组a赋值给数组result,遍历链表向result中添加的元素。最后判断数组a的长度是否大于size,若大于则将size位置的内容设置为null。返回a。

    从代码中可以看出,数组a的length小于等于size时,a中所有元素被覆盖,被拓展来的空间存储的内容都是null;若数组a的length的length大于size,则0至size-1位置的内容被覆盖,size位置的元素被设置为null,size之后的元素不变。

  6. 其他方法

    clone():调用父类的clone()方法初始化对象链表clone,将clone构造成一个空的双向循环链表,之后将header的下一个节点开始将逐个节点添加到clone中。最后返回克隆的clone对象

    public Object clone() {
            LinkedList<E> clone = null;
        try {
            clone = (LinkedList<E>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    
            // Put clone into "virgin" state
            clone.header = new Entry<E>(null, null, null);
            clone.header.next = clone.header.previous = clone.header;
            clone.size = 0;
            clone.modCount = 0;
    
            // Initialize clone with our elements
            for (Entry<E> e = header.next; e != header; e = e.next)
                clone.add(e.element);
    
            return clone;
        }

    clear():将整个集合容器进行清空操作;

    public void clear() {
            Entry<E> e = header.next;
            while (e != header) {
                Entry<E> next = e.next;
                e.next = e.previous = null;
                e.element = null;
                e = next;
            }
            header.next = header.previous = header;
            size = 0;
        modCount++;
        }

四:总结

  1. LinkedList 实际上是通过双向链表去实现的。
    它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  2. 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
  3. LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  4. LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  5. 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。
  6. LinkedList可以作为FIFO(先进先出)的队列,作为FIFO的队列时,该对象提供了一些等价,如下表;

    队列方法       等效方法
    add(e)        addLast(e)
    offer(e)      offerLast(e)
    remove()      removeFirst()
    poll()        pollFirst()
    element()     getFirst()
    peek()        peekFirst()
    
  7. LinkedList可以作为LIFO(后进先出)的栈,作为LIFO的栈时,该对象提供了一些等价方法,如下表:

    栈方法        等效方法
    push(e)      addFirst(e)
    pop()        removeFirst()
    peek()       peekFirst()
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值