源码阅读(2):Java中主要的List结构——Vector集合

(接上文《源码阅读(1):Java中主要的List结构——概述》)

3.java.util.Vector结构解析

java.util.Vector类是从Java较早版本就开始提供的List形式的集合结构(从JDK 1.0开始),其主要的继承体系如下图所示:
在这里插入图片描述
从上图我们可知,Vector是支持“随机访问”特性的,该特性在上一篇文章中已经进行了讲解,这里就不再赘述了。如果严格描述Vector的特性的话,那么Vector是一个支持集合元素读写、且大小可变、且线程安全、最后还支持“随机访问”特性的List性质的集合。下面我们就对Vector类中的典型操作进行详细介绍,首先我们需要详细描述的是存在于Vector类中和其上级AbstractList类中的重要变量信息:

// 向量
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  // ......
  /**
   * 这个数组就是用来存储Vector中每个元素的。其数组大小可以扩展,并且数组的最小值都足以存储下已写入Vector集合的每一个元素
   * 数组的大小也可以大于当前已写入Vector集合的所有元素的大小。如果是那样的话,那么多出来的数组位置上的值都为null
   * 该数组的初始化大小由构造函数中的initialCapacity参数决定,initialCapacity参数的默认大小为10
   */
  protected Object[] elementData;
  /**
   * 这个变量用于记录当前Vector集合中真实的元素数量,在后续的源代码阅读中会发现这个值在整个操作过程中更多起到的是验证作用。
   * 例如判断元素的位置是否超过了最大位置。
   */
  protected int elementCount;
  /**
   * Vector集合支持大小扩容,实际上也就是对其中的elementData进行“变长”操作。
   * capacityIncrement变量表示每次扩容的大小,如果capacityIncrement的值小于等于0,那么扩容大小为其当前大小的一倍
   */
  protected int capacityIncrement;
  // ......
}

通过对以上三个重要变量的描述,我们可以大致知晓Vector集合的基本结构,其包括了一个数组,一个指向当前数组的可验证“边界”,以及一个数组扩容的参考值,如下图所示:

在这里插入图片描述
elementCount变量的在Vector集合中非常重要,它在Vector集合进行addXXXX()、removeXXXX()性质的操作时都会发生变化。elementCount变量的值可能小于elementData数组的容量大小(capacity()方法可获取当前数组的容量大小),也可能和elementData数组的容量大小相等——当整个elementData数组的每一个索引位置都已经设定了元素(元素也可以设置为null)。

另外一个已知的知识点是,Java中的数组是内存中的一个连续地址,一旦完成了初始化其大小不可变化。那么如果理解上文中提到的“变长”了,我们将在下文中专门讲解Vector集合的扩容操作。搞清楚了Vector集合中的重要变量信息后,我们就可以介绍Vector类中的典型操作方法了。

3.1、Vector集合的扩容操作

上文已经描述过Vector集合支持扩容操作,说得具体一点就是支持Vector类中用于真实存储数据的“elementData”数组的大小允许变化,再说得根本一点,就是允许重新为“elementData”数组变量指定新的地址引用。

3.1.1、什么时候扩容?

Vector集合在两种情况下需要进行扩容。首先Vector集合在初始化时会进行扩容,其“elementData”数组变量将从Null第一次指向一个新的数组地址;其次当Vector集合中“elementCount”代表的元素数量将要超出“elementData”数组的最大容量capacity时,也会进行扩容操作,这时“elementData”数组中的元素将会被“拷贝”到另一个更大的数组中,并且“elementData”数组变量将从新指向后者。

  • 初始化Vector集合时的详细扩容过程:

首先关注以下Vector集合的构造函数:

/**
 * Constructs an empty vector with the specified initial capacity and capacity increment.
 * @param   initialCapacity     the initial capacity of the vector
 * @param   capacityIncrement   the amount by which the capacity is increased when the vector overflows
 * @throws IllegalArgumentException if the specified initial capacity is negative
 */
public Vector(int initialCapacity, int capacityIncrement) {
	super();
	if (initialCapacity < 0)
	  throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
	// elementData 数组从null被重新指定了一个数组的地址,数组大小默认为10;
	this.elementData = new Object[initialCapacity];
	this.capacityIncrement = capacityIncrement;
}
/**
 * Constructs an empty vector with the specified initial capacity and
 * with its capacity increment equal to zero.
 * @param   initialCapacity   the initial capacity of the vector
 * @throws IllegalArgumentException if the specified initial capacity is negative
 */
public Vector(int initialCapacity) {
	this(initialCapacity, 0);
}
/**
 * Constructs an empty vector so that its internal data array
 * has size {@code 10} and its standard capacity increment is zero.
 */
public Vector() {
	this(10);
}

以上三个构造函数的执行内容和关联关系一目了然,不需要在做过多介绍。通过以上的代码片段我们知道,如果没有在Vector集合初始化时指定集合的初始化容量(initialCapacity),那么初始化容量将设定为10;如果没有在Vector集合初始化时指定扩容增量(capacityIncrement),那么扩容增量的值将被设定为0;从上文中的介绍我们还可以知道,一旦扩容增量(capacityIncrement)被设置成了0,那么随后进行的每次扩容中,“elementData”数组的大小都会变为当前大小的一倍,也就是说说 10->20->40->80…………以此类推。

  • 当前Vector集合的数据总量将超出数组容量限制时也会进行扩容:

我们来看一下的判定方法:

/**
 * 此私有方法用于在进行各种会引起容器内数据量发生变化的操作前,进行容器容量检查。
 * Vector集合是一个线程安全的集合(虽然锁性能不高),也就是说以上提到的各种“可能引起容器内数据量发生变化”的操作都是通过
 * synchronized关键字进行了线程安全控制的,所以此私有方法就无需再加synchronized关键字了,以便减少性能开销
 */
private void ensureCapacityHelper(int minCapacity) {
  // overflow-conscious code
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

ensureCapacityHelper方法中的内容很简单:如果当前调用该私有方法时,入参所传入的最小容量(minCapacity)如果大于当前Vector集合elementData数组的大小,则进行扩容——调用grow(int)方法。而ensureCapacityHelper方法的调用广泛存在于那些“可能引起Vector集合内数据量发生变化”的方法中,例如:setSize()方法、insertElementAt()方法、addElement()方法、add()方法、addAll()方法等等,以及那些主动寻求容量验证的方法:ensureCapacity()方法。

3.1.2、怎样进行扩容?

当Vector集合进行初始化时的扩容操作很简单,实际上就是elementData数组的初始化过程;这里我们主要讲解grow(int)方法,也就是上文提到的ensureCapacityHelper()方法中调用的grow(int)方法,首先上源代码:

private void grow(int minCapacity) {
  // 将“扩容”操作前当前elementData数组的大小保存下来(保存成oldCapacity),后面可能用到。
  int oldCapacity = elementData.length;
  // 这句代码非常关键,确定新的容量有两种情况:
  // 1、如果当前设定了有效的扩容大小(在Vector集合初始化时可以设定),那么新的容量 = 老的容量 + 设定的扩容值
  // 2、如果当前没有设定有效的扩容大小(既是capacityIncrement的值 <= 0 ),那么新的容量 = 老的容量 * 2
  int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
  // 如果当前新的容量 小于 grow方法调用时传入的最小容量,则新的容量以传入的最小容量为准
  if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
  // 如果当前新的容量 大于 MAX_ARRAY_SIZE常量,这个常量为2147483639[0x7ffffff7]
  // 那么调用hugeCapacity()方法确认新的容量,
  if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
  
  // 最后使用Arrays工具类提供的copyOf方法完成真实的扩容过程
  elementData = Arrays.copyOf(elementData, newCapacity);
}

// 该私有方法的名字叫做:巨大的容量.......
// 好吧,如果真的在Vector集合中管理这么大一个数组,那真的是非常危险的。
private static int hugeCapacity(int minCapacity) {
  if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
  // 如果当前方法调用传入的minCapacity的值,大于常量2147483639,那么取Java中整数类型的最大值2147483647
  // 否则就取MAX_ARRAY_SIZE常量的值2147483639
  return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
} 

以上的方法注释已经把每一句源代码描述得很清楚了,这里重点说明一下Arrays.copyOf()方法和hugeCapacity()方法。

  • Arrays.copyOf(T[] original, int newLength)方法:该方法是一个工具性质的方法,其方法意义为复制指定数组(original)为一个新的数组对象,后者的长度为给定的新长度(newLength)。按照这样的描述,根据给定的新长度(newLength)就会出现两种不同的情况:第一种情况是新长度(newLength)小于指定的数组(original)的原始长度,那么无法复制的数组部分将会被阶段;第二种情况是新长度(newLength)大于等于指定的数组(original)的原始长度,这时原始数组中的所有元素对象(的引用地址)将依据原来的索引位置被依次复制到新的数组中,多出来空余的部分将被填充null值。如下图所示:

在这里插入图片描述
注意上图所代表的过程描述中,并不包括新长度(newLength)参数无效的情况,例如当newLength为负数时,该方法会抛出java.lang.NegativeArraySizeException异常。那么有的读者会问当newLength为0时会出现的情况,这种情况满足以上描述的第一种情况的输出——没有任何元素可以进行填充,当然就是输出一个空数组

上图所代表的过程描述中,也不包括类似int[]、long[]、float[]等java基础类型数组进行复制的场景,在这些基础类型数组的复制过程中,新数组中多余的位置将填充这个基础类型的默认值,例如int[]数组的复制过程中,新数组的多余的位置将被填充“0”。

  • hugeCapacity()方法:
    该方法中出现了两个关键的常量MAX_ARRAY_SIZE和MAX_VALUE,MAX_ARRAY_SIZE的大小为2147483639(7FFF FFF7),表示支持的最大数组大小;MAX_VALUE的大小为2147483647(7FFF FFFF),标识32位int类型所代表的最大整数值。那么按照上文源代码的意义,Vector集合最大支持的数组容量就是2147483647。

3.2、add(E) 方法

add(E)方法的意义是在Vector集合的尾部增加一个新的元素,这个尾部并不是以当前Vector集合中elementData数组的长度决定,而是以Vector集合中当前元素数量elementCount来决定的——elementData的长度永远大于或者等于elementCount。

/**
 * Appends the specified element to the end of this Vector.
 * @param e element to be appended to this Vector
 * @return {@code true} (as specified by {@link Collection#add})
 * @since 1.2
 */
public synchronized boolean add(E e) {
  // 该变量来自于Vector集合的父类AbstractList,后文将进行详细介绍
  // 目前可以单纯的理解为当前Vector集合被操作的次数。
  modCount++;
  // 该方法来确认是否进行elementData数组的“扩容”操作,并在满足条件时进行扩容。
  ensureCapacityHelper(elementCount + 1);
  // 在当前数组的elementCount位置之后,添加新的对象e
  elementData[elementCount++] = e;
  return true;
}

那么整个add方法的操作过程实际上就分为两种情况,第一种情况当elementCount代表的集合中元素数量小于当前elementData数组大小时,这时不必进行elementData数组的“扩容”,直接在elementData数组的第elementCount的位置增加新的数据即可;当elementCount代表的集合中元素数量大于或者等于当前elementData数组大小时,就需要先进行elementData数组的“扩容”操作,再进行新数据在elementCount位置的添加操作。

3.3、set(int , E) 方法

该方法在指定的索引位置设定指定的元素,指定的元素可以为null。该方法有两个参数第一个参数为int类型的索引位置,第二参数为需要在这个索引位置设定的新的元数值。该方法有几个关键点:

  • 可指定的索引位置的有效范围并不是当前Vector集合中elementData数组的大小,而是当前Vector集合中存在的数据数量elementCount——这个elementCount数据值在Vector集合中的另一个表达就是Vector集合的大小(Vector集合的size值)

  • 该方法有一个返回值,这个返回值将向调用者返回指定索引位置上变更之前的值。

/**
 * Replaces the element at the specified position in this Vector with the
 * specified element.
 * @param index index of the element to replace
 * @param element element to be stored at the specified position
 * @return the element previously at the specified position
 * @since 1.2
 */
public synchronized E set(int index, E element) {
  // 如果当指定的索引位置大于等于当前集合的数据量,则抛出超界异常
  if (index >= elementCount)
    throw new ArrayIndexOutOfBoundsException(index);
  
  // 原始值将在替换操作之前被保存下来,以便进行返回
  E oldValue = elementData(index);
  // 将elementData数组的指定位置的数据值替换成新的值
  elementData[index] = element;
  return oldValue;
}

3.4、removeElementAt(int) 方法

该方法用于移除Vector集合中elementData数组指定位置的数据值,并且改变其索引位置的指向。在操作者看来这个操作可以成功移除索引位置X上的元素(X < elementCount),并且操作成功后,操作者虽然依旧可以使用索引位置X取得数据(X < elementCount),但是之后取得的值将是“紧邻”的数据。如下图所示:

在这里插入图片描述
上图展示了removeElementAt(int) 方法的运行实质:既是以当前指定索引位置为几点,将后续元素位置向前一次移动一个索引位置,请看该方法的源代码:

public synchronized void removeElementAt(int index) {
  modCount++;
  if (index >= elementCount) {
    throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
  }
  else if (index < 0) {
    throw new ArrayIndexOutOfBoundsException(index);
  }
  // 以上代码中两种会报错的情况就不做详细分析了,其意义就是当index的位置 < 0 或者大于等于当前elementData数组的大小时抛出异常
  // =========
  // j 代表elementData数组将要移动部分的长度
  int j = elementCount - index - 1;
  // 什么时候 j == 0 呢?就是index指向当前elementData数组的最后一个索引位置时
  if (j > 0) {
    System.arraycopy(elementData, index + 1, elementData, index, j);
  }
  // 元数数量 - 1
  elementCount--;
  // 这句代码将着重进行说明
  elementData[elementCount] = null; /* to let gc do its work */
}

首先来描述一下以上代码中的System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法,该方法是一种JNI native方法,是JDK提供的进行两个数组中元素复制的性能最快的方法。其参数意义描述如下:

  • src:该参数只能传入数组,表示当前进行数组元素复制的来源
  • srcPos:该参数描述源数组中进行复制操作时的起始元素位置
  • dest:该参数同样只能传入数组,标识当前进行数组元素复制的目标数组
  • destPos:该参数描述目标数组中进行复制操作时的起始元素位置
  • length:该参数指定进行复制的长度。

那么这样,以上代码段落中使用System.arraycopy方法的意图就很好理解了,如下图所示:

在这里插入图片描述
上图所示,虽然完成了数组自身的元素移动,但这时数组最后一个元素的值并没有改变,所以需要人工进行数组中元素值的减少,并手动设置最后一个位置上的元素为null。所以会出现上文中源代码的内容:

public synchronized void removeElementAt(int index) {
  // ......
  elementCount--;
  elementData[elementCount] = null; /* to let gc do its work */
  // ......
}

====================
(接下文《源码阅读(3):Java中主要的List结构——ArrayList集合》)

posted @ 2019-06-10 23:48  点点爱梦  阅读(193)  评论(0编辑  收藏  举报