阅读 143

Vector源码深度解析以及应用介绍

基于JDK1.8对Java中的Vector集合的源码进行了深度解析,包括各种方法、特有的Enumeration迭代器机制,并且给出了Vector和ArrayList的区别以及使用建议。

1 Vector的概述

public class Vector extends AbstractList implements List, RandomAccess, Cloneable, Serializable

Vector,来自于JDK1.0 的古老集合类,继承自 AbstractList,实现了 List 接口 ,底层是数组结构,元素可重复,有序(存放顺序),支持下标索引访问,允许null元素。

该类当中所有方法的实现都是同步的,方法采用了synchronized修饰,数据安全,效率低!可以看成ArrayList的同步版本,但是并不完全相同,比如迭代器。

实现了 RandomAccess标志性接口,这意味着这个集合支持 快速随机访问 策略,那么使用传统for循环的方式遍历数据会优于用迭代器遍历数据,即使用get(index)方法获取数据相比于迭代器遍历更加快速!

实现了Cloneable、Serializable两个标志性接口,所以Vector支持克隆、序列化。

2 Vector的源码解析

Vector的方法的原理和ArrayList非常相似,因此如果了解AarrayList那么理解Vector将会很简单!对于某些方法,本文在ArrayList集合中会有详细介绍,因此强烈建议先学习ArrayList:一万字的ArrayList源码深度解析

Vector的底层数据结构就是一个数组,数组元素的类型为Object类型,对Vector的所有操作底层都是基于数组的。

初始容量:调用空构造器时,立即初始化为10个容量的数组,也可以指定初始容量。

加载因子:1,即存放数据时,如果存放数据后的容量大于底层数组的容量,那么首先扩容。

扩容增量:新容量默认增加原容量的1倍,但是也可以在构造器指定扩容时的容量增量!如果新容量还是小于最小容量,则新容量还是等于最小容量!

2.1 主要类属性

相比于ArrayList,属性还是很简单的,多了一个capacityIncrement。

/**
 * 存放元素的底层容器,就是一个数组,当前数组的长度就是Vector的容量
 */
protected Object[] elementData;

/**
 * 元素的个数
 */
protected int elementCount;

/**
 * 当vector的大小大于其容量时,其容量扩充的大小,即容量增量。如果容量增量小于或等于零,则每次需要增长时vector的容量都会加倍。
 */
protected int capacityIncrement;
复制代码

2.2 构造器与初始化容量

2.2.1 Vector()

构造一个空集合,使其内部数据数组的大小初始化为10,容量增量为零。其源码为:

public Vector() {
    this(10);
}
复制代码

可以看到调用了另外一个构造方法。

2.2.2 Vector(int initialCapacity)

构造具有指定初始容量并且其容量增量等于零的空集合。

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
复制代码

可以看到调用了另外一个构造方法。

2.2.3 Vector(int initialCapacity, int capacityIncrement)

构造具有指定的初始容量和容量增量的空集合。

public Vector(int initialCapacity, int capacityIncrement) {
    //调用父类构造器
    super();
    //检查初始化容量,如果小于0就抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    //初始化指定容量的数组
    this.elementData = new Object[initialCapacity];
    //初始化容量增量
    this.capacityIncrement = capacityIncrement;
}
复制代码

2.2.4 Vector(Collection<? extends E> c)

构造一个包含指定 collection 中的元素的集合,这些元素按其 collection 的迭代器返回元素的顺序排列。

public Vector(Collection<? extends E> c) {
    //获取集合的元素数组,赋值给elementData
    elementData = c.toArray();
    //获取此时集合的容量,赋值给elementCount
    elementCount = elementData.length;
    if (elementData.getClass() != Object[].class)
        //如果新加入的数组不是object[]类型的数组,则转换为object[]类型的数组
        elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
复制代码

2.3 add方法与扩容机制

add方法的原理基本和ArrayList的add方法一致,在ArrayList的文章中有详细介绍,这里不再赘述一万字的ArrayList源码深度解析

/**
 * 加了synchronized的同步方法
 * @param e 需要添加的元素
 * @return 添加成功,返回true
 */
public synchronized boolean add(E e) {
    //集合结构修改次数自增1
    modCount++;
    //确保数组容量够用,最小容量为当前元素个数+1
    ensureCapacityHelper(elementCount + 1);
    //添加元素
    elementData[elementCount++] = e;
    return true;
}


private void ensureCapacityHelper(int minCapacity) {
    //如果最小容量减去数组的长度的值大于0
    if (minCapacity - elementData.length > 0)
        //那么有可能是需要扩容,或者数组长度移除
        grow(minCapacity);
}


private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;


private void grow(int minCapacity) {
    //获取老的容量
    int oldCapacity = elementData.length;
    //如果容量增量大于0,增新容量为老容量加上容量增量,否则新容量是老容量的两倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
            capacityIncrement : oldCapacity);
    //如果此时新容量减去老容量的值还是小于0,那么新容量等于最小容量,或者数组长度溢出
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //如果此时新容量减去建议最大容量的值还是小于0,那么新容量等于最小容量,或者数组长度溢出
    //此时需要抛出异常或者重新分配新容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //抛出异常或者重新分配新容量
        newCapacity = hugeCapacity(minCapacity);
    //数组拷贝
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 和arraylist是同样的逻辑
 * @param minCapacity
 * @return
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

复制代码

2.4 addAll方法

public synchronized boolean addAll(Collection<? extends E> c) {
    //结构改变+1
    modCount++;
    //获取加入集合的元素数组
    Object[] a = c.toArray();
    //获取加入的元素的数量
    int numNew = a.length;
    //确保容量能够容纳这些元素
    ensureCapacityHelper(elementCount + numNew);
    //元素的拷贝存放
    System.arraycopy(a, 0, elementData, elementCount, numNew);
    //元素数量增加
    elementCount += numNew;
    //如果此集合由于调用而更改了结构,即numNew>0,则返回 true 
    return numNew != 0;
}
复制代码

2.5 remove方法

public E remove(int index)

移除此集合中指定索引位置上的元素,向左移动所有后续元素(将其索引减1),并返回此集合中移除的元素。

从源码中可以看到,需要调用System.arraycopy() 将删除元素 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素和扩容一样,代价是非常高的

remove的源码,还是比较简单的:

public synchronized E remove(int index) {
    modCount++;
    //检查要移除的元素索引是否越界(上界)
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    //获取该索引处的元素
    E oldValue = elementData(index);
    //要移动的数据长度elementCount-(index + 1)  最小值0最大值size-1
    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        //将index+1后面的列表对象前移一位,该操作将会覆盖index以及之后的元素,相当于删除了一位元素
        System.arraycopy(elementData, index+1, elementData, index,
                numMoved);
    // 数组前移一位,size自减-,空出来的位置(原数组的有效数据的最后一位)置null,原来的具体的对象的销毁由Junk收集器负责
    elementData[--elementCount] = null;
    //返回被移除的元素
    return oldValue;
}
复制代码

2.6 get方法

public E get(int index)

返回此集合中指定索引位置上的元素。

public synchronized E get(int index) {
    //检查要获取的元素索引是否越界(上界)
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    //返回索引处的元素
    return elementData(index);
}
复制代码

2.7 set方法

public E set(int index,E element)

用指定的元素替代此列表中指定索引位置上的元素。

public synchronized E set(int index, E element) {
    //检查要设置的元素索引是否越界(上界)
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    //获取旧的值
    E oldValue = elementData(index);
    //替换值
    elementData[index] = element;
    //返回旧值
    return oldValue;
}
复制代码

2.8 clone方法

返回的是一个全新的Vector实例对象,但是其elementData,也就是存储数据的数组,存储的对象还是指向了旧的Vector存储的那些对象。也就是Vector这个类实现了深拷贝,但是对于存储的对象还是浅拷贝

public synchronized Object clone() {
    try {
        @SuppressWarnings("unchecked")
        //克隆集合对象
        Vector<E> v = (Vector<E>) super.clone();
        //克隆内部数组,导致虽然数组的引用不一样,但是但是数组内部的相同索引处的元素引用指向同一个堆内存地址,即还是同一个对象
        v.elementData = Arrays.copyOf(elementData, elementCount);
        //设置新集合的modCount为0
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}
复制代码

2.9 序列化

同ArrayList一样,Vector也有自己的writeObject方法,区别是Vector的内部数组被全部序列化存储了,包括没有使用道的部分,而ArrayList的内部数组没有全部进行序列化,只是序列化了储存了元素的部分。

并且Vector没有实现readObject方法,那么将会按照默认的方式进行反序列化。

writeObject:

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    final java.io.ObjectOutputStream.PutField fields = s.putFields();
    final Object[] data;
    synchronized (this) {
        fields.put("capacityIncrement", capacityIncrement);
        fields.put("elementCount", elementCount);
        //直接对整个数组进行了序列化,相比于ArrayList代码更加简单,但是更占用空间
        data = elementData.clone();
    }
    fields.put("elementData", data);
    s.writeFields();
}
复制代码

3 迭代器

3.1 Enumeration迭代器的概述

由于Vector属于List接口集合体系,因此具有通用的iterator 和 listIterator迭代器(关于这两个迭代器在ArrayList文章部分有详细讲解,这里不再赘述)。但是我们知道List接口是JDK1.2的时候加进来的,但是Vector在JDK1.0的时候就出现了,因此Vector还具有自己独有迭代器Enumeration,也被称为“枚举”!

Enumeration原本是一个接口,实现Enumeration接口的对象,又称为枚举对象。通过它的api方法可以遍历Vector集合的元素,但是该接口不属于集合体系,并且JDK的API给出了如下建议:此接口的功能与Iterator接口的功能是重复的。此外,Iterator 接口添加了一个可选的移除操作,并使用较短的方法名。新的实现应该优先考虑使用 Iterator 接口而不是 Enumeration 接口。

由于Enumeration迭代器比较古老,因此功能也很简略,并没有iterator 和 listIterator迭代器的快速失败机制,因此可能出现一些不会抛出异常的“异常情况”,比如下面的代码:

/**
 * Enumeration迭代器的死循环
 */
@Test
public void test2() {
    Vector<Integer> vector = new Vector<>(Collections.singletonList(1));
    //获取自己的迭代器Enumeration
    Enumeration elements = vector.elements();
    int j = 0;
    //是否存在更多元素
    while (elements.hasMoreElements()) {
        //内部采用集合的方法添加元素,如果是iterator 和 listIterator 迭代器,那么会马上抛出ConcurrentModificationException异常
        //但是由于Enumeration迭代器,没有这个功能,因此会导致死循环,直到OOM
        vector.add(j++);
        //获取下一个元素
        Object o = elements.nextElement();
        //打印元素
        System.out.println(o);
    }
}
复制代码

上面的代码将会造成死循环!

3.2 Enumeration迭代器的实现

public Enumeration elements()

返回此集合的枚举。返回的 Enumeration 对象将具有此集合中的所有元素。第一项为索引0处的元素,然后是索引1处的元素,依此类推。

下面是源码:

public Enumeration<E> elements() {
    //和iterator 与 listIterator迭代器不一样
    //Enumeration迭代器甚至简陋得"没有自己的内部类实现"
    //这里返回的就是一个匿名内部类,即返回一次实现一次。
    return new Enumeration<E>() {
        //用于遍历元素个数的计数
        int count = 0;

        //是否还存在元素  通过比较count和elementCount的值
        //如果遍历的元素个数小于外部集合的元素个数,那就说明有元素,可以获取,返回true
        public boolean hasMoreElements() {
            return count < elementCount;
        }

        //返回下一个元素
        public E nextElement() {
            synchronized (Vector.this) {
                //如果遍历的元素个数小于外部集合的元素个数,那就说明有元素,可以获取
                if (count < elementCount) {
                    //返回该遍历的元素个数索引处的元素,同时遍历的元素个数自增1
                    return elementData(count++);
                }
            }
            throw new NoSuchElementException("Vector Enumeration");
        }
    };
}
复制代码

我们看到,Enumeration的实现非常简单,返回的是一个匿名内部类,并没有“并发修改”的检测,并且内部只提供了两个方法hasMoreElements()和nextElement()方法。并没有add、remove等修改集合元素方法,功能更加简陋!

boolean hasMoreElements()

当且仅当此枚举对象至少还包含一个可提供的元素时,才返回 true;否则返回 false。

E nextElement()

如果此枚举对象至少还有一个可提供的元素,则返回此枚举的下一个元素。

3.3 分析Enumeration迭代器的死循环

我们来分析上面的案例是如何导致死循环的!

首先获取迭代器,该迭代器和外部集合共用一个内部数组。

进入循环,hasMoreElements()判断是否存在下一个元素,由于原集合存在一个元素,因此count=0 < elementCount=1,即返回true。

然后循环体内部,首先对外部集合添加了元素,这会导致elementCount++,变成2。继续nextElement()方法,同样判断存在元素,因此返回elementData(0++),即返回elementData[0],然后count自增一变成1,第一次循环结束。

第二次循环,这是hasMoreElements()发现count=1 < elementCount=2,即返回true。

然后循环体内部,又首先对外部集合添加了元素,这会导致elementCount++,变成3。继续nextElement()方法,同样判断存在元素,因此返回elementData(1++),即返回elementData[1],然后count自增一变成2,第二次循环结束。

我们发现,无论怎么循环,count始终小于elementCount,这就是导致死循环的原因,在开发过程如果要使用Vector集合,那么要避免这种情况,并且最好是不去使用老旧的Enumeration迭代器!

3.4 使用枚举遍历ArrayList?

/**
 1. 使用枚举遍历ArrayList
 */
@Test
public void test3() {
    ArrayList<String> al = new ArrayList<>(Arrays.asList("b", "a", "s", "c", "11", null));
    Vector<String> v = new Vector<>(al);
    Enumeration<String> elements = v.elements();
    while (elements.hasMoreElements()) {
        String s = elements.nextElement();
        System.out.println(s);
    }
}
复制代码

4 Vector的总结和使用

Vector和ArrayList的异同点:

  1. 相同点:
    1. 底层是数组结构,元素可重复,有序(存储顺序),支持下标索引访问,允许null元素。
  2. 不同点:
    1. Vector:在JDK1.0固有的集合类。该类当中所有方法的实现都是:线程同步,数据安全,效率低。在JDK1.2被加入到集合框架当中。
    2. ArrayList:在JDK1.2新添加的集合类。该类当中所有的方法的实现都是:线程异步,数据不安全,效率高。
    3. 扩容:Vector每次扩容变成原来的2倍(也可以通过构造函数设置容量增量),而 ArrayList 是 每次扩容变成原来的1.5 倍左右。当然对于它们两个都是:如果计算出的扩容后的容量还是小于最小容量,那么扩容后的容量就是最小容量。

Vector使用建议:

Vector集合的方法都加了snchronized,因此效率较低,如果想要使用线程安全的List集合,那么可以使用Collections.synchronizedList()得到一个线程安全的List,实际上它内部使用snchronized关键字修饰的代码块,效率也很一般,推荐使用JUC包下的CopyOnWriteArrayList类,效率比较高,但是它的“写时复制”机制可能会造成数据不同步。总之,面试中,Vector被问到的概率比较小,而在开发过程中,不推荐使用Vector集合

我们后续将会介绍的更多集合,比如LinkedList、TreeMap、HashMap,LinkedHashMap等基本集合以及JUC包中的高级并发集合。如果想学习集合源码的关注我的更新!

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

文章分类
后端
文章标签