前言
对于一个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();
}
总结
- Vector内部使用数组来存储数据,当数组内存不足以存储数据时,会进行扩容,默认在原来容器大小的基础上+10,以达到扩容的目的,这也就是我们常说的动态扩容,但我们使用的过程中进行去指定初始大小,避免扩容,因为扩容的效率和数据量成反比。
- Vector大部分的方法都使用synchronized进行修饰,保证了线程安全,但在并发场景下效率极低,这也是我们在工作中基本不使用的原因。
- modCount属性是父类AbstractList的一个属性,该属性是为了防止在使用迭代器Iterator遍历容器的过程中对容器进行修改,导致遍历数据不正确。
到这里我们对Vector分析也完成了,Vector的源码是真的很简单,适合初学者看,不要害怕,干就是了,如有写得不对的地方烦请指出,一起学习进步。
下篇文章分析ArrayList,其实看明白了Vector,ArrayList也就懂了。
我是一个爱看源码的老谢,知道越多,不知道的越多。