java中的LinkedList和ArrayList的选择和区别

一、LinkedList

LinkedList同时实现了List接口和Deque对口,也就是收它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack),这样看来,linkedList简直就是无敌的,当你需要使用栈或者队列时,可以考虑用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(只是一个接口的名字)。关于栈或队列,现在首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)更好的性能。
虽然LinkedList是一个List集合,但是它的实现方式是通过一个动态的Object[]数组来实现的,而LinkedList的底层原理是通过链表来实现的,因此它的随机访问速度比较差,但是它的删除,插入操作会很快。

  • LinkedList是基于双向循环链表来实现的,除了可以当做链表来操作外,它还可以当做栈、队列和双端队列来使用。
  • LinkedList是非线程安全的,只在单线程下适合使用。
  • LinkedList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现Cloneable接口,能被克隆。
    在这里插入图片描述

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

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    
    
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
}

LinkedList内部定义了一个Node节点,它包含3个部分:元素内容item,前引用prev和后引用next,代码如下:

private static class Node<E> {
    
    
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;

    Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
    
    
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList 还实现了 Cloneable 接口,这表明 LinkedList 是支持拷贝的。
LinkedList 还实现了 Serializable 接口,这表明 LinkedList 是支持序列化的。LinkedList 中的关键字段 size、first、last 都使用了 transient 关键字修饰,这不又矛盾了吗?到底是想序列化还是不想序列化?
先来看看writeObject()方法

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    
    
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out size
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (Node<E> x = first; x != null; x = x.next)
            s.writeObject(x.item);
    }

会发现LinkedList在序列化的时候只保留了元素的内容item,并没有保留元素的前后引用。这样就节省了不少内存空间。当我们在反序列化的时会有一个linkLast()方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。

   /**
     * Links e as last element.
     */
    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++;
    }

和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问

二、ArrayList

ArrayList是如何实现的?
在这里插入图片描述
ArrayList实现了List接口,继承了AbstractList抽象类,底层是基于数组实现的,并且实现了动态扩容

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    
    
    private static final long serialVersionUID = 8683452581122892189L;

  
    private static final int DEFAULT_CAPACITY = 10;//默认分配长度


    private static final Object[] EMPTY_ELEMENTDATA = {
    
    };

 
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
    
    };

    transient Object[] elementData; // non-private to simplify nested class access

    private int size;
}

ArrayList还实现了RandomAccess接口,这是一个标记接口

public interface RandomAccess {
    
    
}

内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。
ArrayList还实现了Cloneable接口,这表明ArrayList是支持拷贝的。ArrayList内部的确也重写了Object类的Clone()方法。

 /**
     * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
     * elements themselves are not copied.)
     *
     * @return a clone of this <tt>ArrayList</tt> instance
     */
    public Object clone() {
    
    
        try {
    
    
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
    
    
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

ArrayList还实现了Serializable接口,同样是一个标记接口:

public interface Serializable {
    
    
}

内部也是空的,标记“实现了这个接口的类支持序列化”。Java的序列化是指将对象转换成以字节序列的形式来表示,这些序列中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。
ArrayList中的关键字段elementData使用了transient 关键字修饰,这个关键字的作用是,让它修饰的字段不被序列化。

  /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

这个时候会有疑问,一个类既然实现了Serializable 接口,肯定是想要被序列化,那么为什么保存关键数据的elementData又不想被序列化呢?
这要从ArrayList是基于数组实现开始,数组是固定长度的,一旦声明了,长度就是固定的。数组一旦装满,就不能添加新的元素进来了。
因此ArrayList选择实现动态扩容,通过oldCapacity >> 1从而实现按照原来数组的长度的1.5倍进行扩容。假设oldCapacity=10,oldCapacity >> 1=5,那么 int newCapacity = oldCapacity + (oldCapacity >> 1);newCapacity 的长度就扩容到15。

 /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
    
    
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

动态扩容表示实际数组大小可能永远无法被填满的,总有多余出来空置的内存空间。
比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧?
序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。
于是ArrayList内部提供了两个私有方法writeObject和readObject来完成序列化和反序列化。

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
    
    
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
    
    
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
    
    
            throw new ConcurrentModificationException();
        }
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    
    
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
    
    
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
    
    
                a[i] = s.readObject();
            }
        }
    }

从writeObject和readObject方法的源码中可以看出,它使用了ArrayList的实际大小size而不是数组长度(elementData.length)来作为元素的上限进行序列化。

LinkedList和ArrayList的选择

  • 如果列表很大很大,ArrayList 和 LinkedList 在内存的使用上也有所不同。LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址。ArrayList 没有这样的开销。
  • 但是,ArrayList 占用的内存在声明的时候就已经确定了(默认大小为 10),不管实际上是否添加了元素,因为复杂对象的数组会通过 null 来填充。LinkedList 在声明的时候不需要指定大小,元素增加或者删除时大小随之改变。
  • 另外,ArrayList 只能用作列表;LinkedList 可以用作列表或者队列,因为它还实现了 Deque 接口。

ArrayList和LinkedList之间的区别

插入之间的区别

  • ArrayList可以插入到指定下标位置,或者数组末尾,这种插入普通情况下是很快的,但是如果某次插入操作触发了扩容,那么本次插入就增加了额外的扩容成本。

  • 对于LinkedList,如果是插在链表的头部或者是尾部都是很快的,因为LinkedList中有单独的属性记录的链表的头结点和尾结点,不过,如果是插在指定下标位置,那么就需要遍历链表找到指定位置,从而降低了效率。但是,使用LinkedList是不用担心扩容问题的,链表是不需要扩容的。

查询之间的区别
首先,从名字就可以看出,ArrayList和LinkedList的区别,ArrayList是基于数组的,LinkedList是基于链表的。
从这一点,我们可以推理出来,ArrayList适合查询,LinkedList适合插入,但是这只是一个广泛的结论,我们应该了解的更细致一点。

  1. 比如,对于ArrayList,它真正的优点是按下标查询元素(快速随机访问),相比于LinkedList,LinkedList也可以按下标查询元素,但是LinkedList需要对底层链表进行遍历,才能找到指定下标的元素,而ArrayList不用,所以这是ArrayList的优点。

  2. 但是,如果我们讨论的是获取第一个元素,或最后一个元素,ArrayList和LinkedList在性能上是没有区别的,因为LinkedList中有两个属性分别记录了当前链表中的头结点和尾结点,并不需要遍历链表。

总结

  1. 默认情况下,比如调用ArrayList和LinkedList的add(e)方法,都是插入在最后,如果这种操作比较多,那么就用LinkedList,因为不涉及到扩容。
  2. 如果调用ArrayList和LinkedList的add(index, e)方法比较多,就要具体考虑了,因为ArrayList可能会扩容,LinkedList需要遍历链表,这两种到底哪种更快,没有一个完全的结论,得具体情况具体分析。
  3. 如果是插入场景比较少,但经常需要查询的话,查询分两种,第一种就是普通遍历,也就是经常需要对List中的元素进行遍历,那么这两种是区别不大的,遍历链表和遍历数组的区别,第二种就是经常需要按指定下标获取List中的元素(ArrayList有快速随机访问的特性),如果这种情况如果比较多,那么就用ArrayList

猜你喜欢

转载自blog.csdn.net/ChenYiRan123456/article/details/129091255
今日推荐