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

前言

对于一个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也就懂了。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容