Java中ArrayList和Vector解析

最近在恶补数据结构和算法,顺带着把ArrayList和Vector看了看,因此写了这篇文章记录下。我们知道ArrayList和Vector底层数据结构都是数组,而数组这种数据结构一旦创建它的容量就不可改变了,而我们平常使用的这两个类却一直能插入元素,这是为什么呢?下面我们开始按照API一个一个解析,注意我们不可能把每个API都面面俱到,因此只介绍一些常用的,其它的自行查看

目录

  • ArrayList
    • 构造
    • add
    • remove
    • clear
    • set
    • get
    • indexOf
    • lastIndexOf
    • contains
    • size
    • isEmpty
  • Vector
    • 构造
    • add
    • remove
    • clear
    • set
    • get
    • indexOf
    • lastIndexOf
    • contains
    • size
    • isEmpty
  • 总结

一、ArrayList

构造

transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList() {
    //初始化一个没有容量数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //传入的容量如果大于0,初始化一个自定义容量的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //传入的容量等于0,初始化一个没有容量的数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

add

假设我们使用的是无参的构造,即初始容量为0

//成员变量,默认值是0
private int size;

public boolean add(E e) {
    //扩充容量,size+1是我们需要的容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //按照下标存入数组,存入后数组中元素个数size就会加1
    elementData[size++] = e;
    return true;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    //添加第一个元素时,会直接把我们需要的容量改为10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //决定是否需要扩充容量
    ensureExplicitCapacity(minCapacity);
}
//记录修改次数
protected transient int modCount = 0;

private void ensureExplicitCapacity(int minCapacity) {
    //modCount只是记录修改次数
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //当我们需要的容量大于数组中现有的容量时,需要扩充
        grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    //获取当前数组的容量
    int oldCapacity = elementData.length;
    //扩充为当前数组容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //扩充后的容量还是小于我们需要的容量,就直接扩充为我们需要的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //扩充后的容量大于Integer.MAX_VALUE - 8,就采用hugeCapacity方法中的策略扩充
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //该方法最终会复制旧的数组中的数据项到新的扩充后的数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

上面方法中,主要有hugeCapacity方法和Arrays.copyOf方法,我们先看hugeCapacity方法

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //判断我们需要的容量是否大于Integer.MAX_VALUE - 8
    //大于就扩充容量为Integer最大值,小于就扩充为Integer.MAX_VALUE-8
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

我们再看Arrays类的静态方法copyOf

public static <T> T[] copyOf(T[] original, int newLength) {
    //调另一个重载方法
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    //创建一个新的扩充容量后的数组
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    //将原始数组从0索引开始复制所有的数据项,到新的数组中,新的数组从0开始接收数据项
    //其实就是复制旧的数组的数据项到新的扩充后的数组中
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

从上面代码我们可以得到ArrayList的扩充容量机制,但是为什么要有这么一个扩容机制呢?其实这是由数组本身这种数据结构所决定的,数组一旦创建,它的容量就固定了,因此只能猜测它的大小,而我们却又不知道用户到底有多少数据项,猜大了浪费空间,猜小了又不能完全存储用户的数据。所以我们想到达到随着数据项的插入进行动态扩展的目的,就必须创建一个新的容量的数组,并且把旧的数组的数据项复制到新的数组中。ArrayList大体采用1.5倍扩容,并且并不是每次添加元素都要扩容,这是考虑到创建新数组,复制数据的效率问题,以空间换时间。具体的扩容策略见下表:

添加第N个元素 添加后的数组容量
只创建不添加 初始容量0
1 10
2 10
3 10
... 10
11 15
12 15
... 15
16 22
17 22
... ...
m Integer.MAX_VALUE - 8
... Integer.MAX_VALUE - 8
Integer.MAX_VALUE - 8 Integer.MAX_VALUE - 8
Integer.MAX_VALUE - 7 Integer.MAX_VALUE
... Integer.MAX_VALUE

remove

remove有两个重载的方法,一个是根据索引开始移除,一个是根据元素开始移除,其实归根到底都是根据索引移除,先看根据索引移除

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //修改次数+1
    modCount++;
    //获取数组中对应索引的元素,最后直接返回
    E oldValue = (E) elementData[index];
    //需要移动的元素数量
    int numMoved = size - index - 1;
    //判断是否需要移动,比如index为最后一个元素时,只需要最后一个元素置为null即可,不需要移动
    if (numMoved > 0)
        //从数组中删除项的后一位开始复制之后的所有元素,复制到数组中,数组从删除项的那一位开始接收
        //即删除项后面的所有元素都向前移动一位
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //最后一个数据项置为null,尽快让GC回收,并且ArrayList的size减一
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

再看另外一个根据元素移除

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                //注意这个数组是可以重复的,这里只是线性查找到元素的第一个索引,然后调fastRemove方法移除
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                //注意这个数组是可以重复的,这里只是线性查找到元素的第一个索引,然后调fastRemove方法移除
                fastRemove(index);
                return true;
            }
    }
    return false;
}
private void fastRemove(int index) {
    //修改次数+1
    modCount++;
    //需要移动的元素数量
    int numMoved = size - index - 1;
    //判断是否需要移动,比如index为最后一个元素时,只需要最后一个元素置为null即可,不需要移动
    if (numMoved > 0)
        //从数组中删除项的后一位开始复制之后的所有元素,复制到数组中,数组从删除项的那一位开始接收
        //即删除项后面的所有元素都向前移动一位
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //最后一个数据项置为null,尽快让GC回收,并且ArrayList的size减一
    elementData[--size] = null; // clear to let GC do its work
}

整体删除过程如下图

ArrayList.png

clear

public void clear() {
    //修改次数+1
    modCount++;
    //所有元素置为null,尽快让GC回收
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    //元素个数置为0
    size = 0;
}

set

public E set(int index, E element) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //获取数组中对应索引的旧元素,最后直接返回
    E oldValue = (E) elementData[index];
    //直接插入新的元素到数组中指定的索引
    elementData[index] = element;
    return oldValue;
}

get

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //返回数组中对应索引的元素
    return (E) elementData[index];
}

indexOf

public int indexOf(Object o) {
    if (o == null) {
        //ArrayList中的元素是可以重复的,这里从0索引开始线性查找元素的第一个索引,直接返回
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        //ArrayList中的元素是可以重复的,这里从0索引开始线性查找元素的第一个索引,直接返回
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    //找不到就返回-1
    return -1;
}

lastIndexOf

public int lastIndexOf(Object o) {
    if (o == null) {
        //ArrayList中的元素是可以重复的,这里从最后一个索引开始线性查找元素,
        //找到第一个索引就直接返回,因此这里返回的是元素的最后一个索引
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        //ArrayList中的元素是可以重复的,这里从最后一个索引开始线性查找元素,
        //找到第一个索引就直接返回,因此这里返回的是元素的最后一个索引
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    //找不到就返回-1
    return -1;
}

contains

public boolean contains(Object o) {
    //ArrayList是否包含某个元素,其实就是看indexOf方法能否找到某个元素的索引
    return indexOf(o) >= 0;
}

size

private int size;

public int size() {
    //add、remove、clear等都会对元素个数修改
    return size;
}

isEmpty

public boolean isEmpty() {
    //ArrayList是否为空,即判断元素个数size是否等于0
    return size == 0;
}

二、Vector

由于Vector和ArrayList很多方法的实现都是一样的,部分只是在方法上加了同步,因此只介绍和ArrayList不同的API

构造

假设我们调的是无参构造

protected Object[] elementData;
//扩充容量时的增长量
protected int capacityIncrement;

public Vector() {
    //调一个参数的构造
    this(10);
}

public Vector(int initialCapacity) {
    //initialCapacity为10,调两个参数的构造
    this(initialCapacity, 0);
}

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    //第一个参数为10表示初始化数组的容量,第二个参数为0表示扩充容量时的增长量
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    //初始化容量为10的数组
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

可以看到Vector对象一创建就会初始化一个容量为10的数组,而ArrayList是初始化一个容量为0的数组,添加第一个元素才会扩充容量为10

add

protected int elementCount;

//加了同步修饰符
public synchronized boolean add(E e) {
    //修改次数+1
    modCount++;
    //扩充容量,elementCount+1是我们需要的容量,elementCount就是ArrayList中的size
    ensureCapacityHelper(elementCount + 1);
    //按照下标存入数组,存入后数组中元素个数elementCount就会加1
    elementData[elementCount++] = e;
    return true;
}
private void ensureCapacityHelper(int minCapacity) {
    //需要的容量比现有的容量大的话,需要扩充容量
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    //由于我们调的是无参构造,这里扩充容量的增长量capacityIncrement为0,
    //所以这里是扩充为原来的2倍,而ArrayList是扩充1.5倍
    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和ArrayList的扩容机制稍有不同,ArrayList不添加元素时容量为0,Vector不添加元素时容量为10,并且ArrayList是1.5倍扩容,Vector是2倍扩容,具体见下表:

添加第N个元素 添加后的数组容量
只创建不添加 10
1 10
2 10
3 10
... 10
11 20
12 20
... 20
21 40
22 40
... ...
m Integer.MAX_VALUE - 8
... Integer.MAX_VALUE - 8
Integer.MAX_VALUE - 8 Integer.MAX_VALUE - 8
Integer.MAX_VALUE - 7 Integer.MAX_VALUE
... Integer.MAX_VALUE

remove

一个根据索引移除,一个根据元素移除,这两个方法和ArrayList一样,只是加了synchronized修饰符

clear

和ArrayList一样,只是加了synchronized修饰符

set

和ArrayList一样,只是加了synchronized修饰符

get

和ArrayList一样,只是加了synchronized修饰符

indexOf

比ArrayList多了一个方法,并且加了同步,而且可以从指定索引向后开始查找,这种设计要比ArrayList要好

public int indexOf(Object o) {
    return indexOf(o, 0);
}
//加了同步,并且可以从指定索引向后开始查找
public synchronized int indexOf(Object o, int index) {
    if (o == null) {
        for (int i = index ; i < elementCount ; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = index ; i < elementCount ; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

lastIndexOf

比ArrayList多了一个方法,并且加了同步,而且可以从指定索引向前开始查找,这种设计要比ArrayList要好

public synchronized int lastIndexOf(Object o) {
    return lastIndexOf(o, elementCount-1);
}
//加了同步,可以从指定索引向前开始查找
public synchronized int lastIndexOf(Object o, int index) {
    if (index >= elementCount)
        throw new IndexOutOfBoundsException(index + " >= "+ elementCount);

    if (o == null) {
        for (int i = index; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = index; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

contains

和ArrayList一样,只是加了synchronized修饰符

size

和ArrayList一样,只是加了synchronized修饰符

isEmpty

和ArrayList一样,只是加了synchronized修饰符

三、总结

相同之处

  • 都实现了List接口
  • 底层数据结构都是数组
  • API的实现几乎都是一样的
  • 扩容机制是一样的,由于数组这种数据结构,在创建时它的容量就已经确定,因此想要随着数据项的插入数组容量发生动态变化,就需要创建一个新容量的数组,把旧数组中的所有元素拷贝到新容量的数组中,这就是扩容机制,但是它们的扩容策略稍有不同
  • 查询和修改都比较快,增删都比较慢。有索引的话查询和修改都会很快,但是扩容的话,需要创建新的数组并复制旧数组的元素到新数组中,因此添加会比较慢。移除元素的话,需要后面的元素前移,因此也比较慢

不同之处

  • 扩容策略稍有不同,ArrayList对象创建后会初始化一个容量为0的数组,添加第一个元素才会扩充容量为10,并且是1.5倍扩容,而Vector对象一创建后就会初始化一个容量为10的数组,并且是2倍扩容
  • ArrayList是线程不安全的,Vector是线程安全的

推荐阅读更多精彩内容

  • Collection & Map Collection 子类有 List 和 Set List --> Array...
    任教主来也阅读 3,003评论 1 9
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,233评论 0 3
  • Vector集合是 jdk1.0 版本就提供的集合类,它的所有操作集合的方法,都进行了加锁(就是对方法使用sync...
    wo883721阅读 226评论 0 1
  • 转自Java中ArrayList类的使用方法ArrayList源码分析 一、什么是ArrayList ArrayL...
    合肥黑阅读 436评论 0 6
  • 最近自己有时会处于纠结状态,不过经过一段时间的调整,好了很多。有些事情,慢慢的想清楚了。人有了些存量,彻底放弃,从...
    行者方圆阅读 209评论 0 0
  • 一直不知道用何样的词语来形容对父母辈的爱情感觉,总觉得淡淡的,但又绝不平凡,就如同梅婷与郭涛所演的《父母爱情》,平...
    山涧静流阅读 412评论 2 2
  • 相聚 离开 都有时候 没有什么永垂不朽 那天,学妹说:学姐,我们明天就实习结束啦~ 我说:那好,你们以后就不用来了...
    小時光sunshine阅读 255评论 12 2