Java容器框架——List接口及其实现类

176 阅读6分钟

List接口

经典口诀: 有序、可重复、有索引

List接口有多个实现类,但是最常用的实现类一般为 ArrayListLinkedListVector

ArrayList

ArrayList的注意事项:

  1. ArrayList可以加入null,甚至可以加入多个null

    • List<String> list =new ArrayList<>();
      list.add(null);
      
  2. ArrayList是由数组来实现数组存储的

  3. ArrayList基本等同于Vector,除了ArrayList是线程不安全的(正因为它是线程不安全的,所以它的执行效率高)。在多线程的情况下,不建议使用ArrayList

    • 查看源码,ArrayList的源码并没有加上synchronized关键字
    •     public boolean add(E e) {
              ensureCapacityInternal(size + 1);  // Increments modCount!!
              elementData[size++] = e;
              return true;
          }
      

ArrayList底层结构和源码分析

序列化
  1. ArrayList中维护了一个Object类型的数组elementData

    • transient Object[] elementData; // transient修饰,表示该属性并不会被序列化

    • 但是事实上ArrayList实现了序列化接口,ArrayList实际上是可以被序列化的。

      由于ArrayList的数组是基于动态扩增的,所以并不是所有被分配的内存空间都存储了数据。如果采用外部序列化方法实现数组的序列化,会序列化整个数组。ArrayList为了避免这些没有存储数据的内存空间被序列化,内部提供了两个私有方法writeObject以及readObject 来自我完成序列化和反序列化。从而在序列化和反序列化数组时节省了空间和时间。

      因此使用transient修饰数组,是防止对象数组被其他外部方法序列化

      私有方法writeObject以及readObject源码如下:

          /**
           * Save the state of the <tt>ArrayList</tt> instance to a stream (that
           * is, serialize it).
           *
           * @serialData The length of the array backing the <tt>ArrayList</tt>
           *             instance is emitted (int), followed by all of its elements
           *             (each an <tt>Object</tt>) in the proper order.
           */
          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();
              }
          }
      ​
          /**
           * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
           * deserialize it).
           */
          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
                  int capacity = calculateCapacity(elementData, size);
                  SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
                  }
              }
          }
      ​
      
添加元素、扩容

当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加元素时,则扩容elementData为10,如果需要再次扩容,则扩容elementData为1.5倍

源码为:

ArrayList源码.png

  1. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

Snipaste_2024-11-06_16-43-28.png

如果形参大于0 ,那么之后扩容就会直接扩容为1.5倍,其他源码就是2中的源码。

删除元素

ArrayList删除元素public E remove(int index)

ArrayList每次删除元素,实际上都要进行一次元素的挪动重组,删除元素所在的下标越靠前,那么数组重组的开销就越大

源码如下:

    public E remove(int index) {
        //判断被删除下标是否合规
        rangeCheck(index);
​
        modCount++;
        E oldValue = elementData(index);
            
        int numMoved = size - index - 1;
        //数组重组
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
​
        return oldValue;
    }

Vector

Vector也是List接口的实现类。

  1. Vector底层也是一个对象数组,protected Object[] elementData;
  2. Vector是线程同步的,即线程安全,Vector类的操作方法都带有synchronized
  3. 在开发中,如果需要线程同步安全时,考虑使用Vector

Vector和ArrayList的比较

底层结构版本线程安全(同步)效率扩容倍数
ArrayList可变数组jdk1.2线程不安全,效率较高如果是有参构造器,那么每次扩容都是原容量的1.5倍。 如果是无参构造器进行创建,那么容量一开始为0,第一次扩容,容量为10,后面每次扩容都扩充为1.5倍
Vector可变数组Object[]jdk1.0线程安全,效率不高如果是无参构造器,容量默认为10,满后,就按照2倍进行扩容。 如果有参构造器,指定大小,则每次按照2倍进行扩容。

LinkedList

  1. LinkList底层实现了双向量表和双端队列的特点
  2. 可以添加任意元素(元素可以重复),包括null
  3. 线程不安全,没有实现同步

LinkedLis

  1. LinkedList底层维护了一个双向链表
  2. LinkedList中维护了两个属性first和last分别指向首节点和尾节点
  3. 每个节点(Node对象)里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表

LinkedList底层结构和源码分析

LinkedList的成员变量:

first和last均为Node结构,分别指向链表头和链表尾。

Node结构中,每个节点都有一个prev指针指向前一个节点,next指针指向下一个节点。item表示当前节点中存储的值

    transient int size = 0;
​
    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;
​
    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;
//--------------------
//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;
        }
LinkedList添加元素

通过源码可以看到,相较于ArrayList的add方法中还要判断容量是否足够,如果不够还要扩容,此外如果ArrayList插入元素是插入到中间位置的话,还需要数组整合开销,LinkedList插入或者移除操作就简洁明了很多。

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

ArrayList、Vector、LinkedList三种比较

首先三者都是List接口的实现类,共性是 有序、可重复、有索引。但是三者之间也有所差异

  1. Vector是Java操起提供的线程安全的动态数组,如果不需要线程安全的话,那么不建议使用Vector,因为同步是有额外开销。Vector内部是适用对象数组来保存数据,会根据需要自动增加容量。(初始创建时,如果没有指定容量,初始容量就是10,后续扩容的话均会扩容到原来的2倍)。
  2. ArrayList是线程不安全的,正是由于是线程不安全的,所以性能会好很多。同Vector,ArrayList也是对象数组来保存数据,只不过ArrayList如果创建时使用的是无参构造器,那么初始容量是0,一旦加入数据,容量就会扩充为10,如果后续还需要扩容,那么就会扩充到原来的1.5倍。
  3. LinkedList是Java提供的双向链表,它并不涉及到所谓的扩容,它不是线程安全的。