Java List总结

List

List集合的特点就是:有序(存储顺序和取出顺序一致),可重复。 几个重要的实现: image.png

ArrayList

ArrayList底层实现是一个数组,能实现自动扩容。 问题:

  1. 扩容机制
    1. 什么时候扩容?扩容多少?扩容机制?具体怎么实现的?初始容量?
  2. 常用方式以及实现?

扩容机制

初始容量?

创建时,如果没有传入容量,则默认容量:空数组,容量0。如果有传入容量或数组,则使用传入的容量。

// 默认空数组,0容量
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_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);
    }
}
// 传入数组 传入数组时,如果元素类型不为Object吗,会将元素类型转换为Object。
// 从这一点来看,ArrayList支持元素类型可变
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

复制代码

什么时候扩容?

添加元素时,会检查容量,如果容量不够,则进行扩容。

public boolean add(E e) {
    // 检查容量并进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
复制代码

扩容多少以及具体实现?

扩容过程:

  1. 计算需要的最少容量minCapacity
  2. 如果当前数组的实际容量小于minCapacity,则进行扩容
  3. 扩容规则:将数组长度增长1.5倍,如果大于minCapacity,则取该值,否则取minCapacity
  4. 扩容实现:复制数组,调用Arrays.copyOf()方法,其通过调用系统方法System.arraycopy()实现。

计算需要的最少容量:

  1. 如果是集合第一次添加元素,并且为空集合,则使用默认大小DEFAULT_CAPACITY = 10;否则,取添加后的元素数量。
  2. 如果当前集合不为空,则取添加后的集合容量,当前size+1
// 计算元素数组的需要的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 如果当前数组的实际容量小于最小需要容量,则进行扩容
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

复制代码

扩容过程:

private void grow(int minCapacity) {
    // overflow-conscious code
    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);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
复制代码

常用方法及实现?

  1. modCount:该字段记录列表在结构上的修改次数。

This field is used by the iterator and list iterator implementation returned by the iterator and listIterator methods.

该字段由 iterator 和 listIterator 方法返回的迭代器列表迭代器实现使用

If the value of this field changes unexpectedly, the iterator (or list iterator) will throw a ConcurrentModificationException in response to the next, remove, previous, set or add operations.

如果此字段的值意外更改,迭代器(或列表迭代器)将抛出 ConcurrentModificationException 以响应下一个、删除、上一个、设置或添加操作。

This provides fail-fast behavior, rather than non-deterministic behavior in the face of concurrent modification during iteration.
这提供了快速失败的行为,而不是面对迭代期间的并发修改时的不确定行为。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
复制代码
  1. 添加元素
  • add(E e)先检查容量,再添加元素
  • add(int index, E element)先检查下标,再检查容量,再添加(System.arraycopy()复制实现)
// 先扩容再添加
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

// 先检查执行下标是否存在,再检查容量,再添加(直接复制数组)
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++;
}
复制代码
  1. 替换元素set(int index, E element),先检查下标,然后直接替换
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
复制代码
  1. 获取元素get(int index)
  • 先检查下标是否存在,然后获取元素
  • 将元素强转为指定的类型
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

复制代码
  1. 删除元素
  • remove(int index),先检查下标,然后将数组的指定下标后的元素全部向前移动一位,再将最后一位的值这是为null。该操作只清除指定下标的值,不改变集合当前的容量。
  • remove(Object o),便利数组,找到值相等的第一个元素移除。同样,该操作不改变集合当前容量。
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;
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    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
}

复制代码
  1. 清空集合clear(),将数组中所有元素设置为null
public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}
复制代码
  1. 精简集合trimToSize(),将集合数组的容量,精简为当前实际容量

Trims the capacity of this ArrayList instance to be the list's current size. An application can use this operation to minimize the storage of an ArrayList instance.
将此 ArrayList 实例的容量修剪为列表的当前大小。应用程序可以使用此操作来最小化 ArrayList 实例的存储。

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}
复制代码

参考文章: List集合就这么简单【源码剖析】

Vector

问题:

  1. Vector与Arraylist的区别是什么?

Vector与ArrayList的内部实现基本相同。
1、Vector是线程安全的,ArrayList不是线程安全的。方法上增加了同步关键字synchronized
2、ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍。

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public boolean remove(Object o) {
    return removeElement(o);
}

public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}

// 扩容实现
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);
}

复制代码

参考: Arraylist与Vector的区别

LinkedList

因为LinkedList是双向链表实现的,是链式存储结构,所以不存在容量的限制。 问题:

  1. 内部实现是怎样的?
  2. 与ArrayList相比,有啥优缺点?

常用方法及实现

  1. 构造函数
public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
复制代码
  1. 增加元素

增加元素时,如果没指定下标,则直接在最后添加;如果指定了下标,则在指定下标添加。通过改变元素的前后指针来实现元素添加。

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

// 该属性记录最后一个元素
transient Node<E> last;

// 指定位置增加元素
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++;
}
复制代码
  1. 获取元素

通过遍历,获取指定下标的元素。如果下标值小于长度一半,则从前往后遍历,否则从后往前遍历。

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

// 通过便利,获取指定下标的元素
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;
    }
}
复制代码
  1. 删除元素

基本思路,更改元素前后节点的指针指向,将该元素从链表中移除,再将元素内容置空,等待回收。

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

// 将指定元素从链表中移除
unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}
复制代码

与ArrayList相比,有啥优缺点?

  • ArrayList的底层是数组,LinkedList的底层是双向链表。
  • ArrayList支持以角标位置进行索引出对应的元素(随机访问),而LinkedList则需要通过遍历回来获取元素。因此一般来说ArrayList的访问速度是要比LinkedList要快的
  • 对于增加和删除元素,ArrayList需要通过数组复制来实现,LinkedList只需要修改指针即可。因此一般来说LinkedList的增删速度是要比ArrayList要快的。

ArrayList的增删未必就是比LinkedList要慢。

  • 如果增删都是在末尾来操作【每次调用的都是remove()和add()】,此时ArrayList就不需要移动和复制数组来进行操作了。如果数据量有百万级的时,速度是会比LinkedList要快的。(我测试过)
  • 如果删除操作的位置是在中间。由于LinkedList的消耗主要是在遍历上,ArrayList的消耗主要是在移动和复制上(底层调用的是arraycopy()方法,是native方法)。
    • LinkedList的遍历速度是要慢于ArrayList的复制移动速度的
    • 如果数据量有百万级的时,还是ArrayList要快。(我测试过)

参考: 集合总结
ArrayList扩容时,也是通过复制数组实现的,所以即使始终在末尾添加元素时,也存在数组复制的情况,只是次数少很多。

队列操作

因为LinkedList实现了Deque接口,所以LinkedList也能当成队列使用。真是个全能的小能手啊!!

Stack

不推荐使用

A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.
Deque 接口及其实现提供了一组更完整和一致的 LIFO 堆栈操作,应优先使用该类

继承Vector,新增了几个接口

public class Stack<E> extends Vector<E>{……}

// 在栈顶添加元素,与addElement(item)相同
public E push(E item);
// 返回并移除实栈顶元素
public synchronized E pop();
// 返回栈顶元素,但不移除
public synchronized E peek();
// 检查堆栈是否为空
public boolean empty();
// 返回对象在堆栈中的位置
public synchronized int search(Object o);

复制代码

总结

  • ArrayList
    • 底层数据结构是数组。线程不安全
    • 初始容量10,增加元素时进行扩容检查,扩容0.5倍。
    • 增删元素时,需要拷贝数组,调用本地方法
  • LinkedList
    • 底层数据结构是链表。线程不安全。
  • Vector
    • 底层数据结构是数组。线程安全。但很少用
      • 所有方法都是同步时,有性能损失。
      • 每次扩容1倍,有内存损耗
  • Stack:Vector的子类,实现了一个标准的后进先出的栈
    • Java官方已经声明不建议使用_Stack_类

查询多用ArrayList,增删多用LinkedList。如果需要线程同步,使用Collections.synchronizedList()方法代替。

おすすめ

転載: juejin.im/post/7076279731489865758