美文网首页
Vector源码分析,一个古老的List实现类

Vector源码分析,一个古老的List实现类

作者: 知道越多不知道越多 | 来源:发表于2021-04-20 23:16 被阅读0次

    前言

    对于一个Java开发来说,集合是无时无刻不存在的一个工具类,我们经常使用集合存储同一类型的数据,当然也可以存储不同类型的数据,具体得看集合的范型是什么。集合的总类有多种,今天我们来聊一聊List集合。
    List是一个集合的接口,实现List接口的实现类都有一个特性:有序,可重复。常见的实现类有Vector、ArrayList和LinkedList,接下来我们一起讨论一下他们的区别,为什么一个List接口会有这么多实现,他们分别适用于什么场景。

    Vector

    这是一个元老级的集合实现 Since JDK1.0 也就是JDK诞生的时候就存在了。它相比于其他两个实现类最大的区别就是Vector可以保证原子性,能保证线程安全,成也萧何拜也萧何,正是因为它的原子性导致它的性能低下,这也是我们工作中不考虑使用的原因。

    源码分析

    它有三个重要的属性

    /*
     * 数据存储的地方,没错,就是用一个数组存储
     */
     protected Object[] elementData;
     /*
      * 当前容器中数据的数量,也就是elementData数组存储了多少数据
      */
     protected int elementCount;
    /*
     * 当数组容量不足时,扩容的大小,可通过构造方法指定,不指定默认扩大两倍
     */
     protected int capacityIncrement;
    

    整个容器方法的实现基本都是围绕这三个参数来实现。
    当我们使用一个类时,需要通过构造方法进行实例化,Vector提供的构造方法如下

    /*
     * 无参构造方法
     */
    public Vector() {
        this(10);
    }
    /*
     * 指定初始容量大小
     */
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    /*
     * 以上两个构造方法最终都是调用这个构造方法实例化,
     * @param initialCapacity 初始容量大小,也就是指定数组长度,比需大于0
     * @param capacityIncrement 扩容增长数量
     */
    public Vector(int initialCapacity, int capacityIncrement) {
          super();
          if (initialCapacity < 0)
              throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
          this.elementData = new Object[initialCapacity];
          this.capacityIncrement = capacityIncrement;
    }
    

    以上构造方法我们可以根据实际情况而定,如果我们能知道存储数据的量,我们可以通过第二个构造方法指定容量大小,减少扩容带来的性能问题。
    执行构造方法后,我们的容器就创建好了,接下来我们就可以往容器里放数据。

    add(E e)
    add方法是容器为我们提供添加数据的方法,老规矩,先看源码

    public synchronized boolean add(E e) {
          modCount++;
          // 扩容相关
          ensureCapacityHelper(elementCount + 1);
          // 插入数据
          elementData[elementCount++] = e;
          return true;
    }
    /*
     *  为什么这个方法没有使用synchronized修饰呢?
     *    因为调用该方法的所有方法都使用了synchronized修饰,外部已经保证原子* 性,无需多此一举
     */
    private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
          // 所需容量 - 数组长度
          if (minCapacity - elementData.length > 0)
              grow(minCapacity); // 真正扩容的地方
    }
    /**
     *  扩容
     */
    private void grow(int minCapacity) {
            // overflow-conscious code
          // 原数组长度
          int oldCapacity = elementData.length;
          // 计算数组长度=原数组长度(默认10) +  (capacityIncrement 不指定默认0),所以最终新的数组长度=10+10=20
          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);
    }
    

    首先看方法使用synchronized进行修饰,这也就是它能保证原子性的原因,基本所有的方法都加了synchronized。
    属性modCount我们先不说,后边再统一说明。
    add主要的逻辑都在代码里写了注释,相信聪明的你一定能看懂。

    add还有另一个方法add(int index, E element),我将它和set(int index,E element)一起说,他们很相似,但又有不同之处。
    add(int index, E element)

    /*
     *  什么也不干,之间丢给insertElementAt,这不就是套娃嘛
     *  相比add方法,多了一个index参数,我们可以通过这个方法在指定位置插入一条数据,也就是插队 
     */
    public void add(int index, E element) {
         insertElementAt(element, index);
    }
    /*
     * 真正干苦力的人
     */
    public synchronized void insertElementAt(E obj, int index) {
          modCount++;
          // 如果index大于容器大小,之间抛异常
          if (index > elementCount) {
              throw new ArrayIndexOutOfBoundsException(index
                                                         + " > " + elementCount);
          }
          // 和add方法一样,扩容的地方
          ensureCapacityHelper(elementCount + 1);
          // 将指定位置的数据往后移动,数据量大的时候,此方法的性能是很差的
          System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
          // 在指定位置插入数据
          elementData[index] = obj;
          // 容器数据量+1
          elementCount++;
    }
    

    set(int index,E element)

    /*
     *  此方法和add方法不一样,它是对指定位置的数据进行修改,然后返回修改之前的数据
     */
    public synchronized E set(int index, E element) {
          // 一样,都是先判断index
          if (index >= elementCount)
              throw new ArrayIndexOutOfBoundsException(index);
          // 先获取到老的数据
          E oldValue = elementData(index);
          // 修改成新的数据
          elementData[index] = element;
          return oldValue;
    }
    

    源码分析做了注释,他们的区别如下
    add 是插入数据,在指定位置插入数据,数据让后移动
    set 是修改数据,将指定位置的数据进行修改,然后返回修改前的数据

    我们往容器中放数据就是为了需要使用的数据读取出来,容器为我们提供了如下几个方法
    get(int index)

    /*
     *  获取指定位置的数据
     */
    public synchronized E get(int index) {
        // 判断获取数据的位置是否存在
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
         // 获取位置并返回
         return elementData(index);
    }
    

    firstElement()
    我们可以通过此方法获取容器中第一条数据,很简单,不分析了

    public synchronized E firstElement() {
        if (elementCount == 0) {
           throw new NoSuchElementException();
       }
        return elementData(0);
    }
    

    lastElement()
    获取容器最后一条数据

    public synchronized E lastElement() {
        if (elementCount == 0) {
            throw new NoSuchElementException();
        }
        return elementData(elementCount - 1);
    }
    

    容器能插入数据,肯定也能删除数据,容器为我们提供了remove方法进行删除,源码太简单了,一看就明白,这里不分析了。

    modCount
    在源码中多处存在这个参数,那么它有什么作用呢?
    modCount是Vector的父类AbstractList里的一个属性
    用于实现fail-fast机制(快速失败),在并发集合中,对集合进行迭代操作,若期间有其他线程对集合的结构进行操作,此时迭代器能快速感知到,并抛出ConcurrentModificationException异常。

    protected transient int modCount = 0;
    

    该参数主要用来标记容器中数据变更的次数,仔细观察发现在所有的数据变更的方法中都有它的影子。
    它的作用是防止我们在使用迭代器遍历的过程中,对数据进行修改,迭代器在对容器进行遍历的时候,首先会判断此属性的值是否有变更,有变更则抛异常。

    我们实现一个迭代器遍历容器的案例

    // 创建一个容器
    Vector vector = new Vector();
    vector.add("a");
    vector.add("b");
    // 创建迭代器
    Iterator iterator = vector.iterator();
    System.out.println(iterator.next());
    // 新增一条数据,则modCount++
    vector.add("c");
     System.out.println(iterator.next());
    
    输出结果
    a
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.Vector$Itr.checkForComodification(Vector.java:1210)
        at java.util.Vector$Itr.next(Vector.java:1163)
        at com.xsx.study.collection.VectorDemo.main(VectorDemo.java:17)
    
    由于两次next()方法中间修改了容器的数据
    

    vector.iterator(); 其实是new Itr();创建一个Itr实例,Itr是Vector的一个内部类,该内部类实现Iterator接口

    private class Itr implements Iterator<E> {
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
            // 关键在这,我们创建一个Itr的时候,将modCount赋值给expectedModCount,next()方法会判断这两个值是否相等,不相等说明容器被修改过了,抛异常
            int expectedModCount = modCount;
    }
    

    什么?你不信,让你死心塌地

    public E next() {
         synchronized (Vector.this) {
             // 关键!!
              checkForComodification();
              int i = cursor;
              if (i >= elementCount)
                  throw new NoSuchElementException();
             cursor = i + 1;
              return elementData(lastRet = i);
           }
    }
    final void checkForComodification() {
            // 真像在此,无话可说了吧
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
    }
    

    总结

    1. Vector内部使用数组来存储数据,当数组内存不足以存储数据时,会进行扩容,默认在原来容器大小的基础上+10,以达到扩容的目的,这也就是我们常说的动态扩容,但我们使用的过程中进行去指定初始大小,避免扩容,因为扩容的效率和数据量成反比。
    2. Vector大部分的方法都使用synchronized进行修饰,保证了线程安全,但在并发场景下效率极低,这也是我们在工作中基本不使用的原因。
    3. modCount属性是父类AbstractList的一个属性,该属性是为了防止在使用迭代器Iterator遍历容器的过程中对容器进行修改,导致遍历数据不正确。

    到这里我们对Vector分析也完成了,Vector的源码是真的很简单,适合初学者看,不要害怕,干就是了,如有写得不对的地方烦请指出,一起学习进步。

    下篇文章分析ArrayList,其实看明白了Vector,ArrayList也就懂了。

    我是一个爱看源码的老谢,知道越多,不知道的越多。

    相关文章

      网友评论

          本文标题:Vector源码分析,一个古老的List实现类

          本文链接:https://www.haomeiwen.com/subject/ykdolltx.html