ArrayList和Vector的使用及底层剖析
集合类与接口的关系

接口是不能实例化对象的,只有具体的类才能实例化对象,Java的聚合框架库中的集合基本都继承了一个或两个接口。(比如ArrayList就继承了List接口,就具有List接口所具有的特点)
- 集合的顶级接口:Map、Collection
- 所有的集合类都是继承了Collection接口或者是Map接口。
ArrayList
我们点开ArrayList的源码,发现ArrayList实现了以下接口:
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
特点:
-
List:存放单值,里面的内容是允许重复、允许为NULL并且有序的,插入的第一个元素实际上就存放到了数组的0号下标下。
-
RandomAccess:可以被随机访问。由于ArrayList就是对数组这种数据结构的封装,所以ArrayList也支持随机访问。
-
Cloneable:可以使用clone()方法被克隆的。
-
Serializable:可序列化的。
-
可以使用迭代器遍历
使用方法
实例化ArrayList对象
要实例化一个ArrayList对象,就要先了解它有哪些构造函数,所以我们点开源码查看一下它的构造函数。
一共提供了四个构造函数,我们先使用最简单的构造函数。
ArrayList<Integer> list = new ArrayList();
这样就创造出了一个默认容量的空的ArrayList。
数组在指定类型时既可以使用普通类型,也可以使用引用类型,而集合只能使用引用类型(如不能指定int类型,而要使用Integer),而数组在创建的时候就需要指定类型,而创建集合时不需要(可有可无),如果在创建集合对象的时候没有在泛型中指定类型的话,那么这个集合对象中可以存放任意类型的元素。
最好在创建对象时就用泛型规定好想要使用的数据类型。
增删改查方法
增
往ArrayList中添加元素,我们会用到add方法,add方法也有多种用法
public boolean add(E e)
有序添加,依次往后添加元素。
public void add(int index, E element)
可以在指定位置添加元素,如果index上面有元素,则以前的元素会被覆盖。
删
public E remove(int index)
删除指定index下标的元素。
public boolean remove(Object o)
删除指定值的元素。如果元素也是integer类型的,为了避免编译器以为是上一种删除方法,可以做这样的操作:
Integer i = 100;
list.remove(i);
批量操作:
添加和删除除了单个操作之外,其实还有批量操作的方法,方法如下:
public boolean addAll(Collection<? extends E> c)
传一个单值类型的集合,将这个集合里面所有的元素全部添加到调用者调用的集合中的最后面去。
public boolean addAll(int index, Collection<? extends E> c)
传一个单值类型的集合,将这个集合里面所有的元素全部添加到调用者调用的集合中的指定位置去。(求并集,但会有重复元素)
public boolean removeAll(Collection<?> c)
传一个单值类型的集合,从调用者调用的集合将这个集合里面包含的所有的对应元素全部删除。(求差集)
public boolean retainAll(Collection<?> c)
传一个单值类型的集合,得到两个集合的所有重复元素(求交集)
public List<E> subList(int fromIndex, int toIndex)
求调用集合从元素fromIndex到toIndex的子集(求子集)
改
public E set(int index, E element)
index是要修改的元素下标,element是要修改的元素的新值。返回元素的旧值。
查
public E get(int index)
用get方法加要查看元素的下表即可得到该元素的值
遍历方式
for
for(int i = 0; i<list.size(); i++){
System.out.print(list.get(i)+" ");
}
foreach
for (Character a: list) {
System.out.print(a+" ");
}
迭代器
Iterator<Character> iterator= list.iterator();
while(iterator.hasNext()){
System.out.print(iterator.next()+" ");
}
迭代器是一种设计模式,会在《设计模式|迭代器的设计模式》这篇博客里做详细分析,在这里我们只要知道这种遍历方式就好。
ListIterator listIterator = list.listIterator(4); // 从4号下标从后往前遍历
while(listIterator.hasPrevious()){
System.out.print(listIterator.previous()+" ");
}
ListIterator是Iterator的一个子接口,可以从前往后遍历,也可以从后往前遍历。
源码分析
为了方便源码的分析,我们自己创建一个MyArrayList.java的类来实现一个自己的ArrayList。
首先我们知道,ArrayList底层是封装了一个数组的,所以我们在写的时候先定义一个私有类型的泛型或Object类型数组,再定义一个私有成员size来代表元素个数。
private Object[] elemadata;
private int size;//元素个数
在实现具体的源码之前,我们先定义好一个基本的ArrayList所需要的简单方法:
// 构造函数
public MyArrayList(){
}
// 增加方法
public void add(T elem){
}
// 删除方法
public void remove(int index){
}
//查询方法
public T get(int index){
}
// 修改方法
public void set(int index, T newValue){
}
// 扩容方法
public void grow(){
}
在进行源码的模仿之前,我们先分析一下各个函数的作用都是什么
构造函数:
对集合进行初始化,对于ArrayList来说,我们要对数组进行初始化,给数组开辟空间,在ArrayList源码当中,其实是给定了一个数组大小的初始值的。
private int DEFULT_SIZE = 10; // 默认大小
所以在构造函数中,我们应该先给数组开辟十个空间。但是还有一种情况是,饿哦们的ArrayList实例化出来后,过了很久才被使用,那这十个空间的数组就被白白浪费了,所以我们先将ArrayList在构造函数中初始化成一个空的数组,当往ArrayList中添加第一个元素的时候,我们再将其开辟十个空间大小。
因此,我们再最开始的成员变量中先定义一个空的数组:
public class MyArrayList<T> {
private Object[] elemadata;
private int size;//元素个数
private int DEFULT_SIZE = 10;//默认大小
private static final Object[] EMPYT_ELEMENTDATA = {};
然后在构造函数中,把这个空的数组赋值给elemdata
// 构造函数
public MyArrayList() {
elemadata = EMPYT_ELEMENTDATA;
}
所以关于ArrayList的默认容量,如果调用的是默认构造函数,那么容量先是0,当添加第一个元素时容量变为10。
添加方法
我们先考虑最简单的那种添加方法,即public void add(T elem),这个方法我们不用考虑下表是否合法,只需考虑数组当中是否还有足够的空间存放新元素。
判断数组中是否还能存放新元素,首先就要判断size的大小,最开始size的大小默认是0的,每当添加成功一个新元素,就会进行一次size+1操作,如果size的大小大于elemdata的数组长度了,那么就要进行扩容,将所有元素存放到新数组中。
public boolean add(T elem) {
//(1)数组中是否还有足够的空间存储新元素
ensureCapacityInternal(size + 1);
//(2)将数据存储到数组中
elemadata[size] = elem;
size++;
return true;
}
public void ensureCapacityInternal(int size) {
if (size > elemadata.length) { // 判断是否有足够的空间存储下一个元素
grow(size);
}
}
然后我们来研究一下随机插入的add方法,牵扯到随即插入,那么就要牵扯index是否合法的问题,为了方便和提升代码的可读性,我们将判断下标是否合法的操作抽象成一个方法 。
private void rangrCheck(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException();
}
}
随机添加的第一步即为判断index是否合法,判断完成之后,如果index是合法的,我们还得判断一下当前size+1之后是否需要扩容,将index之后的所有元素全部后移一位,然后再将参数中的元素值直接赋值到对应位置即可。
public void add(int index, T elem) {
rangrCheck(index);
ensureCapacityInternal(size + 1);
System.arraycopy(elemadata, index, elemadata, index + 1, size - index);
/*for(int i = size; i >= index; i--){
elemadata[i+1] = elemadata[i];
}*/
elemadata[index] = elem;
size++;
}
ArrayList不建议做随机插入操作,因为需要移动的元素过多,效率是很低的。
扩容方法
如果这个数组之前是一个空数组,那么这个第一次扩容的大小就是从0扩容到10,如果不是第一次扩容的话,那么扩容的大小就是以前的1.5倍。
public void grow(int size) {
// 第一次扩容
if (size == 1) {
elemadata = new Object[DEFULT_SIZE];
return;
}
int old_size = elemadata.length;
int new_size = old_size + (old_size >> 1);//右移一位 相当于除以二
//扩容
//Object []arr = new Object[new_size];
//System.arraycopy();先把elemadata中数据拷贝到arr中
//elemadata = arr;
elemadata = Arrays.copyOf(elemadata, new_size);
}
我们只写出了扩容方法中的核心成分,在JDK源码中的扩容方法还有一些细节,在获取到oldCapacity和newCapacity之后,先进行第一次扩容的判断,如果不是第一次扩容的话,会判断newCapacity是否达到规定上限,因为如果ArrayList一致扩容下去的话,其没有被用到的空白容量也会变得很大,这步操作会让扩容不要超过最大上限MAX_ARRAY_SIZE。
删除方法
和随机插入操作一样,先要判断index的值是否合法,而且删除操作不能直接将对应下标上的元素直接置为null,因为ArrayList是允许储存元素为空的,我么将index位置之后的所有元素向前移动一位,然后再将最后一位置为空,就完了ArrayList的删除操作。
public void remone(int index) {
rangrCheck(index);
System.arraycopy(elemadata, index + 1, elemadata, index, size - index - 1);
elemadata[--size] = null;
}
同样,由于删除操作所要移动的元素过多,ArrayList的删除操作效率也是很低的。
获取方法
判断一下下标是否合法,然后直接return出来该元素就可以。
//查
public T get(int index) {
rangrCheck(index);
return (T) elemadata[index];
}
修改方法
先判断index是否合法,然后直接进行修改即可。
//改
public void set(int index, T newValue) {
rangrCheck(index);
elemadata[index] = newValue;
}
Vector
vector和arraylist是十分相似的,所以我们把两个集合放到一篇博客来讲。
首先我们还是观察一下vector都有什么特点,从源码中可以看到,它和ArrayList所实现的接口是一模一样的,所以Vector和ArrayList的特点,几乎是一样的,可以直接参考上面ArrayList的特点。
在使用方面,两个集合是没有区别的,他们的增删改查方法提供的是一模一样的,底层数据结构其实也是相同的,都是数组。
当我们观察到具体方法的时候,我们发现与ArrayList的构造方法有所不同的是,Vector初始的数组大小就是10,没有最开始是0的那一步操作。
Vector的grow方法和ArrayList也有所不同,Vector的扩容方法中,有一个capacityIncrement的参数,而capacityIncrement是可以在构造函数中被传入的,即capacityIncrement就是增长因子。如果capacityIncrement不大于0的话,是二倍扩容的,如果capacityIncrement大于0的话,那么容量扩大capacityIncrement个容量。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
相比下来其实Vector的扩容方法其实更加合理一点,因为可以人为控制扩容所浪费的容量。
还有一个很关键的点是,Vector中所有的方法都加上了synchronized锁,所以Vector是线程安全的,而ArrayList不是线程安全的。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
相比下来其实Vector的扩容方法其实更加合理一点,因为可以人为控制扩容所浪费的容量。
还有一个很关键的点是,Vector中所有的方法都加上了synchronized锁,所以Vector是线程安全的,而ArrayList不是线程安全的。

本文深入解析了ArrayList与Vector这两种集合类的特点与使用方法,包括它们的底层数据结构、增删改查操作、扩容机制等,并对比了两者的区别。
859





