看同学人手一本《STL源码剖析》,实在亚历山大。借过一看,实在晦涩难懂。于是想着从熟悉的java入手,看看java里面自带的类库是怎么实现这些容器的。于是乎,便有了这个java stl系列。其实这个名字是我自己起,java里面提供了许多常用的类库,使用十分方便。废话不多说,从Vector类开始分析吧。
Vector类与ArrayList的实现基本相同,只是Vector类是线程安全的,而ArrayList不是线程安全的。
首先看Vector类的实例变量,如下所示:
protected Object[]elementData; protectedintelementCount; protectedintcapacityIncrement; |
其中对象数组elementData就是用来存储对象的引用,这一点需要注意,存储的是对象的引用而不是对象本身,即我们指的对象数组中的元素都是指对象的引用。elementCount变量代表当前Vector中存储元素的数目。而capacityIncrement表示当Vector容量不够时扩容的增长因子,当增长因子capacityIncrement<=0时,则Vector的新容量增长为原来的两倍;如果增长因子capacityIncrement>0,则设置Vector新容量为旧容量的大小加上增长因子的值。如果增长后的容量大小还是小于需要的容量大小,则设置新的容量值为需要的容量值。后面代码中有进一步分析。
同时Vector类提供了一系列方法,常用的方法包括add(E), add(int, E),remove(int ),remove(Object)等,我们一个个的来分析。
先从构造方法入手,Vecto
Vector()
/*Vector的构造函数*/ public Vector() { this(10); //默认容量为10 } public Vector(int initialCapacity) { this(initialCapacity, 0); } public Vector(int initialCapacity,int capacityIncrement) { super(); this.elementData =new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } |
整个构造函数主要是调用父类的构造函数,其父类为AbstractList,它提供了一些List接口某些方法的简单实现。创建一个大小为initialCapacity(默认为10)的元素数组,然后设置容量增长的值,默认为0.
既然容量默认是10,那么当我们向使用无参数构造函数构造Vector对象后,向其中增加元素时,超过10就需要对数组扩容了,这也是使用无参数的Vector构造函数效率会比较低的原因,如果能够事先知道元素数目,大可以设定初始大小或者直接用数组。我们接着看vector加入元素的方法add(E)。
Vector(Collectionextends E> c):使用容器初始化Vector
public Vector(Collectionextends E> c) { elementData = c.toArray(); elementCount =elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData,elementCount, Object[].class); } |
注意到其中加的判断语句,这是java以前的一个bug。如果不加上这个判断,考虑如下代码:
List list = Arrays.asList("hehe",“haha”);
Vectornew Vector
vec.add(new Integer(33));
这样会抛出ArrayStoreException。而加入该判断语句后,转换数组类型,就不会出现这个问题了。
add(E e): 向Vector中加入元素,加入到尾部。
publicsynchronizedbooleanadd(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; returntrue; } |
首先要做的事情是modCount加1,该值表示对Vector的修改次数。elementCount为Vector中当前元素数目,方法ensureCapacityHelper(elementCount + 1)的目的在于判断元素数目是否超出了Vector初始大小,如果超过了,则需要扩容。方法ensureCapacityHelper(intminCapacity)方法代码如下。然后将对象的引用加入到对象数组elementData的尾部,并将当前元素个数变量elementCount的值加1.
privatevoid ensureCapacityHelper(int minCapacity) { int oldCapacity =elementData.length; if (minCapacity > oldCapacity) { //这个时候需要扩容了 Object[] oldData =elementData; int newCapacity = (capacityIncrement > 0) ? (oldCapacity +capacityIncrement) : (oldCapacity * 2); if (newCapacity < minCapacity) { newCapacity = minCapacity; } elementData = Arrays.copyOf(elementData, newCapacity); } } |
ensureCapacityHelper方法说明:
如果当前所需要的容量大于初始容量时,则需要扩容。例如我们调用无参数构造函数,对象数组elementData的初始大小为10,如果我们加入的元素超过了10个,就会扩容了。这里扩容的策略是:如果设定的增长参数capacityIncrement>0,则新容量为原来的容量oldCapacity加上capacityIncrement;如果capacityIncrement<=0,比如我们默认capacityIncrement=0,则直接设置新的大小为原来的两倍。如果新的容量还是小于所需要的大小,则设置新容量为所需要的容量大小minCapacity。最后使用Arrays.copyOf()方法创建一个大小为newCapacity的新数组,并将原来对象数组中的值拷贝到新的对象数组中,并将引用elementData指向新的数组。(copyOf方法最终调用本地方法newArray()创建数组,并通过本地方法System.arraycopy()将原来数组中的值拷贝到新创建的数组中)。
add(intindex, E element):向Vector指定位置加入元素。
publicvoid add(int index, E element) { insertElementAt(element, index); } publicsynchronizedvoid insertElementAt(E obj,int index) { modCount++; //判断index不能超过当前元素的个数,否则抛出异常 if (index >elementCount) { thrownew ArrayIndexOutOfBoundsException(index + " > " + elementCount); } //判断是否需要扩容 ensureCapacityHelper(elementCount + 1); //将对象数组中的元素从index开始的位置的元素全部后移1位, //然后在index位置处插入元素,最后将Vector中的当前元素数目加1. System.arraycopy(elementData, index,elementData, index + 1,elementCount - index); elementData[index] = obj;//加入元素值 elementCount++;//当前元素数目加1 } |
可以看到该方法最终调用insertElementAt()方法来完成指定位置的插入操作。前面的验证语句用来指定index不能大于当前元素的数目。比如当前我们加入了12个元素到Vector中,这时elementCount的值为12,若此时调用add(13, “hehe”), 则由于13>12,此时会抛出异常。(注意一点,ArrayIndexOutOfBoundsException和除0异常等都是运行时异常,是可以不用catch语句进行捕获的。对于可检查的异常,则是必须捕获的)。
下面看看方法的核心代码的实现,首先同add(E e)方法一样,保证Vector的容量足够。然后将对象数组中的元素从index开始的位置的元素全部后移1位,在index位置处插入元素,最后将Vector中的当前元素数目加1.例如当前的Vector中的内容为[“one”, “three”, “four”, “five”],此时调用add(1, “two”),则先将”three”, “four”, “five”全部后移一位,然后在索引为1处插入”two”,最终Vector内容为[“one”, “two”, “three”, “four”,“five”]。(注意我这里只是为了形象地说明该函数的作用,实际Vector存储的是只是String对象”one”, “two”…的引用值,并不是String对象本身)arraycopy 的函数原型为System.arraycopy(Objectsrc, int srcPos, Object dest, int destPos, int length),即从指定的位置srcPos拷贝length个元素到目的地址destPos处。这里需要注意的一点是,函数拷贝的源数组和目的数组是同一个,即内存区有重叠,不是简单的将元素后移就行,需要使用辅助数组来实现拷贝。当然,这都是本地方法arraycopy帮我们实现了,我们不需要关注过多的细节。关于内存区重叠的拷贝可以参看C标准库中memmove方法的实现,有一定的参考价值。
get(int index):返回指定位置处的元素
publicsynchronized E get(int index) { if (index >=elementCount) thrownew ArrayIndexOutOfBoundsException(index); return(E)elementData[index]; }
|
如果指定位置大于当前元素数目,则抛出异常。否则返回对象数组中的第index个元素。
remove(Object o):移除Vector第一次出现的元素o,若不存在该元素,则返回false,否则返回true.
publicboolean remove(Object o) { return removeElement(o); } publicsynchronizedbooleanremoveElement(Object obj) { modCount++; //获得元素在Vector中的位置 int i = indexOf(obj); if (i >= 0) { removeElementAt(i); returntrue; } returnfalse; } publicint indexOf(Object o) { return indexOf(o, 0); } publicsynchronizedintindexOf(Object o,int index) { /** *如果要移除元素为null,则判断vector中是否有元素值为null,若有返回位置i. **/ if (o ==null) { for (int i = index ; i <elementCount ; i++) if (elementData[i]==null) return i; } else { for (int i = index ; i <elementCount ; i++) //若存在待移除的元素,则返回第一次出现的位置i。 if (o.equals(elementData[i])) return i; } //不存在该元素,返回-1 return -1; } publicsynchronizedvoidremoveElementAt(int index) { modCount++; /*下面两个判断语句判断位置是否合法*/ if (index >=elementCount) { thrownew ArrayIndexOutOfBoundsException(index +" >= " + elementCount); } elseif (index < 0) { thrownew ArrayIndexOutOfBoundsException(index); } int j =elementCount - index - 1; if (j > 0) { //对象数组中从index+1开始到结尾所有元素均前移一位 System.arraycopy(elementData, index + 1,elementData, index, j); } elementCount--; //元素数目减去1 elementData[elementCount] =null;/*设置对象数组最后一个元素值为null,便于gc垃圾回收 */ } |
remove(Object o)代码分析:
可以发现remove(Object o)调用removeElement(Object obj)方法实现移除元素的功能。过程很清晰,首先调用indexOf(Object o)方法获取元素在Vector中的位置,如果位置i>=0,则调用removeElementAt(int i)方法移除指定位置处的元素并返回true,否则什么都不做并返回false.
需要注意的是,在indexOf(Object o, int index)方法中,元素是否存在是根据equals方法来判断的,即判断的是待删除元素指向的内容是否与Vector中某个元素指向的内容相等,若相等,则返回true.好比如下代码,我们知道s1和s2指向的对象内容相同,但是引用s1和引用s2本身并不相等,所以s1==s2会返回false。但是当存入到Vector中后,由于remove方法是判断元素所指向的对象内容是否相等,所以最后的remove方法会返回true。这是因为虽然s1和s2本身不相等,但是它们指向的对象内容相等。(因为String覆写了Object类的equals方法,所以会产生这个结果,如果是你自己定义一个类而没有覆写equals方法的话,那么默认的equals方法是比较元素本身是否相等,那样的话就会返回false了)
String s1 = new String("haha"); String s2 = new String("haha"); System.out.println(s1 == s2); //false Vector vec =newVector(); vec.add(s1); boolean exists = vec.remove(s2); System.out.println(exists);//true |
方法removeElementAt(int index)移除Vector中指定位置的元素。这跟add(int index, E e)相反,它是将Vector中index+1开始到尾端的元素全部前移1位,将元素数目变量elementCount减1,并设置对象数组最后一个元素为null,便于垃圾回收。
//判断Vector是否为空:Vector当前元素数目为0,则为空 publicsynchronizedboolean isEmpty() { returnelementCount == 0; }
//返回元素数目 publicsynchronizedint size() { returnelementCount; }
//将Vector设置为当前元素数目大小。如果元素数目小于容量大小,需要将对象数组长度截断为元素数目大小。 publicsynchronizedvoid trimToSize() { modCount++; int oldCapacity =elementData.length; if (elementCount < oldCapacity) { elementData = Arrays.copyOf(elementData,elementCount); } }
//与get方法对应,设置某个位置处的元素为指定的元素 publicsynchronized E set(int index, E element) { if (index >=elementCount) thrownew ArrayIndexOutOfBoundsException(index); Object oldValue = elementData[index]; elementData[index] = element; return(E)oldValue; }
//移除Vector中所有元素,将对象数组值都置为null,并将元素数目置为0.调用该方法后若在调用set(index, element)或者get(index)方法,会抛出异常。 publicsynchronizedvoid removeAllElements() { modCount++; for (int i = 0; i <elementCount; i++) elementData[i] =null; elementCount = 0; } |
由上面代码分析知道,Vector是线程安全的,因为它在方法中加了synchronized关键字。java集合框架提供了多种synchronized集合, 比如Vector, HashTable, Collections的synchronizedXxx方法的返回值等.需要说明的是,synchronized集合如Vector等是线程安全的,但并不是严格线程安全的。根据《Java Concurrency in Practice》第二章关于线程安全的定义--线程安全的类无需调用方进行额外的同步—synchronized集合是不满足该定义的。
public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); }
public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } |
考虑上面代码,假设Vector对象中含有10个元素, 多线程环境下可能出现这样的场景:
线程1调用getLast方法, 计算得知lastIndex为9, 然后线程失去CPU使用权. 接着线程2调用deleteLast方法, 其lastIndex也为9, 线程2删除了第9个元素. 然后线程1重新获得CPU时间, 线程1会试图获取第9个元素, 但是该元素已经被线程2删除了, 此时将抛出ArrayIndexOutOfBoundsException异常.
从上面的例子可知, 尽管Vector对象是线程安全的,但是如果对其进行复合操作的话(getLast方法既需要取得最后一个元素的索引,还需要取得最后一个元素的值--类似这样的操作成为复合操作),仍然需要调用方同步。正确的做法是:
public static Object getLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } }
public static void deleteLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } } |
这里使用Vector对象list作为锁,就能保证不同线程执行时某个特定时刻只能进行一项操作。当第一个线程还没执行完getLast方法的时候,第二个线程不能执行deleteLast方法同步块中的移除操作。
迭代是最常见的复合操作, 迭代时调用方也需要进行额外的同步, 以保证整个迭代期间集合没有发生变化。如下面代码使用了额外的同步:
List list = Collections.synchronizedList(new ArrayList()); //得到synchronized集合 // … //调用方同步 synchronized (list) { Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); } |
迭代集合时, 可能会发生ConcurrentModificationException异常. 每个集合内部都拥有一个名为modCount的成员变量, 如果集合发生了变化, 就会更改modCount的值. 使用Iterator开始迭代时,会将modCount的赋值给expectedModCount, 在迭代过程中, 通过每次比较两者是否相等来判断集合是否在内部或被其它线程修改. 如果expectedModCount和modCount不相等, 将抛出ConcurrentModificationException异常。如下面代码,由于remove操作会修改modCount的值,所以会抛出ConcurrentModificationException异常。
Iterator while(it.hasNext()) { System.out.println(it.next()); // remove操作会导致modCount的值被修改, 从而引发ConcurrentModificationException异常 list.remove(0); } |
java集合类采用的这种机制被称为Fail-Fast机制--检查状态。如果没有问题则忽略,如果有问题就抛出异常,java集合采用这种方式避免同步所带来的开销。如果确实需要同步,可以使用synchronized集合如Vector,HashTable等,或者在调用方进行同步。需要注意的是,不管是使用普通集合还是synchronized集合,进行复合操作时,都需要调用方额外进行同步。
可以通过Collections.synchronizedXxx方法返回synchronized集合, 查看源码可知, 返回的Synchronized集合对象只是一个包装者, 其对每一个方法都进行同步。下面给出了一个代码例子,java源码中大致如此。
publicboolean add(E e) { synchronized(mutex) {returnc.add(e);} } |