【集合】2.List

1. List基本介绍

  1. List接口是Collection接口的子接口。
  2. List集合中元素有序(添加顺序和取出顺序一致)并且可重复。
  3. List集合中每个元素都有其对应的顺序索引,即支持索引。

2. List接口常用方法

  • void add(int index, Object ele)
    • 在 index 位置插入 ele 元素
  • boolean addAll(int index, Collection eles)
    • 从 index 位置开始将 eles 中的所有元素添加进来
  • Object get(int index)
    • 获取指定 index 位置的元素
  • int indexOf(Object obj)
    • 返回 obj 在集合中首次出现的位置
  • int lastIndexOf(Object obj)
    • 返回 obj 在当前集合中末次出现的位置
  • Object remove(int index)
    • 移除指定 index 位置的元素,并返回此元素
  • Object set(int index, Object ele)
    • 设置指定 index 位置的元素为 ele , 相当于是替换
  • List subList(int fromIndex, int toIndex)
    • 返回从 fromIndex 到 toIndex 位置的子集合

3. ArrayList源码分析

3.1 ArrayLIst介绍

  1. ArrayList可以加入null,并且可以加入多个。
  2. ArrayList是由数组来实现数据存储。
  3. ArrayList基本等同于Vector,但Vector是线程安全的,而ArrayList是线程不安全的。

3.2 源码分析

3.2.1 构造方法

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

    transient Object[] elementData;
    
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始的elementData容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容element的1.5倍。

    // 默认空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 无参构造,将elementData指向空数组
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    // 默认容量
    private static final int DEFAULT_CAPACITY = 10;
    
  3. 如果使用的是指定大小的构造器,则初始element容量为指定大小,如果需要扩容,则扩容element的1.5倍。

    // 空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 有参构造
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    
  4. 如果使用的是指定集合的构造器,则判断传入的集合大小是否为0,为0则初始化为空数组,不为0就将集合元素复制到elementData中,初始大小为传入集合元素的大小。

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }
    

3.2.2 扩容机制

  1. 在添加元素时,首先调用ensureCapacityInternal方法确保容量足够。

    public boolean add(E e) {
        // 确定容量
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }
    
  2. 之后调用calculateCapacity方法获得需要的容量大小。

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
  3. 如果是一个空数组就返回默认大小10所需最小容量的最大值,否则返回所需最小容量

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
  4. 然后将容量大小传入ensureExplicitCapacity方法,如果所需最小容量超过了数组大小,就进行扩容。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
  5. 调用grow方法可进行扩容,扩容大小为原先1.5倍。

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 原大小的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 防止溢出
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 复制数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

4. Vector源码分析

4.1 Vector介绍

  1. Vector底层也是一个对象数组。

    protected Object[] elementData;
    
  2. Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized。

    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
    
        return elementData(index);
    }
    
  3. 开发中需要线程同步安全是考虑使用Vector,但效率较低。

4.2 源码分析

4.2.1 构造方法

  1. Vector一共3个构造方法,无参构造方法调用一个参数的构造方法,一个参数的构造方法调用两个参数的构造方法。
  2. 调用无参构造方法时,默认初始容量为10,增量为0,扩容时会以2倍方式扩容。
public Vector() {
    this(10);
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

4.2.2 扩容机制

  1. 在添加元素时,首先调用ensureCapacityHelper方法确保容量足够。

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    
  2. 如果所需容量大于数组大小就会调用grow方法扩容。

    private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
  3. 在创建Vector时,如果指定增量大小大于0,则采用递增式扩容方法,每次扩容大小为增量大小;如果指定增量大小为0,则采用加倍式扩容方法,每次扩容原数组大小的两倍

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        // 防止溢出
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 复制数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

5. ArrayList和Vector对比

底层结构 版本 线程安全(同步)及效率 扩容
ArrayList 可变数组 JDK1.2 不安全,效率高 如果是有参构造按1.5倍扩容。如果是无参第一次扩容为10,之后按1.5倍扩容
Vector 可变数组 JDK1.0 安全,效率不高 如果是无参或一个参数,按两倍加倍式扩容。如果指定增量大小,按增量大小递增式扩容。

6. LinkedList源码分析

6.1 LinkedList介绍

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

6.2 源码分析

  1. LinkedList底层维护了一个双向链表。

  2. LinkedList中维护了两个属性first和last分别指向头节点和尾节点。

    transient Node<E> first;transient Node<E> last;
    
  3. 节点是内部类Node对象,里面维护了prev、next、item三个属性,prev指向前一个节点,next指向后一个节点。

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

6.2.1 添加元素

使用add方法添加元素时默认添加到链表尾部,会调用linkLast方法。

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

6.2.2 删除元素

使用无参remove方法删除元素时,会删除头部元素,会调用removeFirst方法。

public E remove() {
    return removeFirst();
}

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

6.2.3 查找元素

使用get方法查找元素时首先会判断索引是否在链表范围内,如果不在抛出IndexOutOfBoundsException异常。

之后会调用node方法查找元素,先判断索引在前半部分还是后半部分,在前半部分就从前往后遍历;在后半部分就从后往前遍历。

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

Node<E> node(int 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;
    }
}

7. ArrayList和LinkedList对比

底层结构 增删效率 改查效率
ArrayList 可变数组 较低,增加需扩容,删除需移动 较高
LinkedList 双向链表 较高,通过链表追加 较低

如何选择ArrayList和LinkedList:

  1. 如果改查操作多,选择ArrayList。
  2. 如果增删操作多,选择LinkedList。
  3. 在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList。
posted @ 2021-08-08 16:07  haojinglei  阅读(66)  评论(0)    收藏  举报