Java学习笔记-ArrayList(2)和LinkedList

上一篇中我们大致介绍了ArrayList的优点和隐藏的,不容易被发现的弊端。但是这一篇,我们还要再对ArrayList批判一番。


又因为它是数组,当我们需要往列表最后丢一个数据的时候很简单,但是如果要往中间丢呢?方法大家肯定都想到了。挪呗!后面的各位同学让让,挤个人进来:


    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

先看一下这个人是不是真的进来的地方对,别跑到很后面去了;再看看进来以后地方还够不够了,不够还得扩容(grow),然后不好意思,这人位置后面的全部copy往后移一位。这个System.arraycopy和Arrays.copyof可不一样,它不会新建一个新的数组对象,但是会挨个去赋值交换,就跟我们自己写for循环,arr[i+1]=arr[i]一样。极端一点的情况,假如我们要往List的头部插一个数据(虽然ArrayList并没有addFirst方法),那就得把后面所有的数据都挨个移位!


而addall是怎么操作的呢?


    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

一样是移,只不过这次不是一个人了,我得算算要进来几个人。怎么算呢,先用toArray,再获取length。这个toArray也是一比开销,如果原来传进来的也是个ArrayList还好,偷懒copyof就行了;如果不是,那就还得先转换成数组,再移位,再复制,是不是头都大了?


同理,如果我想从列表里删掉一个或者几个节点,那么后面的也得统统移位,这个操作量就很大了。所以这里我们隆重推荐ArrayList的一个兄弟:LinkedList。


不难想到,LinkedList的构造函数中不用去指定默认大小了。它里面的数据结构也不是数组了,而是节点(Node)。别误会,这个Node可不是xml的,也跟org.w3c.dom半毛钱关系都没有。这个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的add和remove操作,实际上是通过一系列link和unlink进行操作的,这些方法有:linkFirst、linkLast、linkBefore(Node)、unlinkFirst、unLinkLast、unLink(Node)。他们实际做的,就是修改节点中指向前一个和后一个的指针。


我们用add(E, index)举例子:


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

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

首先判断了一下下标是否合法,然后看是不是在队尾(是则视作linkLast),然后调用了一个node方法去取得某个下标的节点。这个方法中实际上也进行了一遍遍历,所以开销其实也是不小的。然后就调用linkBefore的方法,修改了这个插入的节点之前节点的”向后的指针“让它指向自己,修改这个插入的节点之后节点的”向前的“节点让它也指向自己,自己的两个指针则指向前后两个节点,这样这个节点就算插入进去了。


有点绕是不是?鉴于我的绘图水平有限,建议看不懂的去搜一下链表的图,很简单就懂了。


那么有人问了最后那个节点呢?它的往下一个节点的指针指向啥?null呗。


从上面这个例子我们可以看出,对一个LinkedList的头和尾进行数据操作是很高效的,因为只需要改改指针就行了。但是如果要往中间增删节点,由于有一个遍历过程,效率就没那么高了,但是仍然优于ArrayList(因为不需要进行大规模的数据迁徙),而addAll方法需要先把传入的集合变化成数组,再往里插,效率会更加低一些,和ArrayList孰优孰劣我也没验证过,大家有兴趣可以去试一下。


由于LinkedList往头尾增删数据很方便这种特性,我们可以用它模拟栈(stack)这种数据结构,实际上LinkedList也提供了一系列的方法,其中就有栈操作的push和pop:


    public void push(E e) {
        addFirst(e);
    }  //往头(栈顶)上插个数据(压栈)

    public E pop() {
        return removeFirst();
    }  //删除并返回头上的数据(出栈)

    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;
     } //等于peek

    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }  //返回但不删除尾巴的数据

    public boolean offer(E e) {
        return add(e);
    } //等同于add,再尾部添加数据

    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }  //addFirst 不多解释了,返回值不同而已

    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }//类比上面

    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    } //其实就是pop 写法不同而已

    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 element() {
        return getFirst();
    }//getFirst换个名字而已=_= 用来instanceof名字更直观?

其他的不再赘述了,大家可以自己去翻源代码。另外,它的toArray就比较痛苦了:


    public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }


一样是要遍历整个链表,效率比ArrayList低了不止一点半点,从这里我们可以看出只要是addAll都得经历一个痛苦地转数组的过程,而LinkedList要更加痛苦一些。对于将一大批对象丢到集合里这个过程,set比List效率更优,即使不看具体实现也比较好理解:set不需要维护里面对象的有序性,自然更有优势。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值