List集合之Vector

358 阅读4分钟

最近想把学到的知识汇总起来整理一遍,方便日后升职加薪,于是想了想还是从Java基础开始搞起吧~

我们先来梳理下Java集合中几个重要的接口:

java.lang.Iterable接口

List,Set,Queue集合的上层都需要继承java.lang.Iterable接口,实现该接口的类具备两种能力,一种是forEach(Consumer<? super T> action) 方法和spliterator方法

java.util.Collection接口

间接实现java.util.Collection接口的类都是可以按照某种逻辑结构存储一组数据的集合

java.util.AbstractList抽象类

根据list集合在各维度上的表现出来的工作特点,可以对其进行分类,分类标准可以说有三种:

  1. 根据是否支持随机访问的特点进行分类, 分为支持随机访问的list集合和不支持随机访问的list集合。
  2. 根据是否具有数据的可修改权限进行分类,可以分为可修改的list集合和不可修改的list集合。
  3. 根据集合的容量是否可变进行分类,可以分为大小可变的list集合和大小不可变的list集合。

java.util.AbstractList抽象类提供了一种能力,各种具体的list集合只需要根据自身情况重写java.util.AbstractList抽象类中的不同方法就可以定制化自己的工作特点。例如,set(int)方法的功能是替换指定索引位上的数据对象,如果当前List集合不支持修改,则一定会抛出UnsupportedOperationException异常;对于大小可变的类,只需要重写add(int, E)方法和remove(int)方法;如果开发人员不需要实现支持随机访问的List集合,则可以使其继承java.util.AbstractSequentialList抽象类。

java.util.RandomAccess接口

是一种标识接口。标识接口通常不需要实现任何方法。主要用于向调用者表示这些list集合支持集合中数据对象的随机访问。其他标识接口如java.lang.Cloneable接口和java.io.Serializable接口;

java.util.ArrayList,java.util.Vector,java.util.concurrent.CopyOnWriteArrayList都实现了java.util.RandomAccess接口,另外我们常用的三方类如JSONArray类也实现了RandomAccess接口;

如果类实现了java.util.RandomAccess接口,表示支持随机访问,例如ArrayList是支持随机访问的,LinkedList是不支持随机访问的,其set方法实现机制完全不同,代码如下:

ArrayList

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

LinkedList

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}


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

因为如此,所以我们再看Collections中的fill方法,其针对是否实现RandomAccess接口逻辑是不相同的

public static <T> void fill(List<? super T> list, T obj) {
    int size = list.size();
    // FILL_THRESHOLD 为 20
    if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
        for (int i=0; i<size; i++)
            list.set(i, obj);
    } else {
        ListIterator<? super T> itr = list.listIterator();
        for (int i=0; i<size; i++) {
            itr.next();
            itr.set(obj);
        }
    }
}

针对于不支持随机访问的集合使用迭代器的操作,从而避免不必要的索引位置查询操作,在每次调用next()方法时,迭代器都可以基于上次操作的索引位继续寻找下一个索引位,而不需要重新从第一个索引位进行查询;

Vector集合

Vector集合的工作特性除了支持数据对象的随机访问,还有集合的大小可变,保证线程安全的运行环境;在Vector中有三个关键的变量:

// 保存元素的数组,可以扩展,当数组容量大于当前已写入的数据时,存null,初始化大小由构造方法中的initialCapacity决定,initialCapacity参数默认值为10
protected Object[] elementData;

// 主要用于记录当前Vector集合中的数据对象数量
protected int elementCount;

// 支持扩容操作,capacityIncrement表示扩容大小,当小于或者等于0时,2倍扩容
protected int capacityIncrement;

Vector集合的扩容场景分析

vector扩容场景分为以下几种:

  1. 集合在初始化时会进行扩容操作;
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

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

public Vector() {
    // 默认初始化容量为10
    this(10);
}

在集合初始化的时候,如果没有指定initialCapacity,那么初始化容量为10, 如果没有指定capacityIncrement,那么在随后的每次扩容中都是扩容前容量的2倍

  1. 当数据对象数量elementCount大于最大容量capacity时,也会进行扩容操作;我们看Vector的add(E)方法;
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}



private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

当集合中的对象数据数量elementCount +1 的值大于当前elementData数组容量的值时,会进行扩容操作;

  1. 指定容量大小的时候,也会进行扩容
public synchronized void setSize(int newSize) {

    modCount++;
    if (newSize > elementCount) {
        ensureCapacityHelper(newSize);
    } else {
        for (int i = newSize ; i < elementCount ; i++) {
            elementData[i] = null;
        }
    }
    elementCount = newSize;
}

如果设置的容量值大于当前Vector集合的容量值,则会进行扩容,否则的话,多余的数据将会被丢弃掉;

Vector集合的扩容原理

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

我们重点讲一下Arrays.copyOf()

该方法主要用于将原始数组(original)复制为新的数组,后者的长度为指定的新的长度(newLength);那么将会出现以下四种情况

  1. 指定的长度小于当前数组的长度:原始数组无法被复制的部分将会被抛弃
  2. 指定的长度大于或者等于原始数组的长度:原始数组会一次复制到新的数组,多余的部分会被赋值为null(如果是基本类型,则会被赋值为默认值,如int为0)
  3. 指定的长度小于0:抛出异常NegativeArraySizeException
  4. 指定的长度等于0:等同于指定长度小于当前数组长度,返回一个空数组

Vector集合修改方法

public synchronized E set(int index, E element) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

从源码中我们可以看出index必须要小于elementCount,返回值为替换前该索引位置上的值,新的值可以是null;

Vector集合删除方法

public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    // j代表的是elementData要移动的长度
    int j = elementCount - index - 1;

    // j=0时表示的是要移除的是当前集合的最后一位
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }

    // elementData数据对象-1
    elementCount--;

    elementData[elementCount] = null; /* to let gc do its work */
}

这段代码表示的意思其实是:从指定的索引位置起,将后续数组依次向前挪动1位;