ArrayList & Vector

    1. ArrayList和Vector都是List的实现类,他两处于同一个地位上的。他们所实现的功能大同小异,源码相似度90%以上。
    2. 他俩的区别,ArrayList是非线程安全,而Vector是线程安全的,那么表现在源码上是怎么样的区别呢?就是在每个ArrayList的方法前,加上synchronized。哈哈,不相信?直接上代码

    3.他俩都实现了Iterator和LinkIterator接口,具有相同的遍历方式。
    4.ArrayList和Vector都具有动态扩容的特性,唯一的区别是,ArrayList扩容后是原来的1.5倍。Vector中有一个capacityIncrement变量,每次扩容都在原来大小基础上增加capacityIncrement。如果capacityIncrement==0,那么就在原大小基础上再扩充一倍。
    5.Vector中有一个方法setSize(int newSize),而ArrayList并没有,我觉得这个方法有点鸡肋。setSize允许用户主动设置容器大小,如果newSize小于当前size,那么elementData数组中只会保留newSize个元素,多出来的会设为null。如果newSize大于当前size,那么就扩容到newSize大小,数组中多出来的部分设为null,以后添加元素的时候,之前多出来的部分就会以null的形式存在,直接试验一下吧

    1. v2.add(1);
    2. v2.setSize(3);
    3. v2.add(3);
    4. System.out.println(v2.size());
    5. setSize之前:
    6. [1]
    7. setSize之后:
    8. [1, null, null]
    9. 当我再次添加一个元素后:
    10. [1, null, null, 3]
    11. 所以我觉得这个方法并没有太大实用意义。而且会是用户困惑,出现一些不必要的错误。

    6.因为Vector是同步的,所以性能上肯定不如ArrayList,所以在不需要考虑多线程的环境下,建议使用ArrayList。

    既然上面我讲了这么多Vector和ArrayList的异同点,而且两个类的实现基本一致,那么下面我就已ArrayList为例子来进行讲解,Vector部分就不再赘述。 ArrayList是List的实现类,可以说是最重用的一个容器之一。他之所以被频繁的使用,必然有其优势之处。下面就来讲讲ArrayList的几个优点:

    一、 动态扩容

    首先来谈谈ArrayList的数据是如何存储的,他的底层其实就是封装了一个Array数组,数组的类型为Object。

    1. private static final int DEFAULT_CAPACITY = 10;
    2. private static final Object[] EMPTY_ELEMENTDATA = {};
    3. transient Object[] elementData;
    4. private int size;

    从这段定义中可以看出,ArrayList维护了两个数组DEFAULT_CAPACITY和elementData。DEFAULT_CAPACITY是一个空数组,当创建一个空的ArrayList的时候就会使用DEFAULT_CAPACITY,这个时候elementData==DEFAULT_CAPACITY,当在容器中添加一个元素以后,则会使用elementData来存储数据。

    这里值得讨论的是DEFAULT_CAPACITY常量,他代表的含义是一个默认数组大小,当我们创建的容器没用指定容量大小时,就会默认使用这个常量作为数组大小。因此当我们创建一个ArrayList实例的时候,最好考虑一下业务场景,如果我们将频繁的存储大量的元素,那么最好在创建的时候指定一个合理的size。所谓动态扩容,就是当数组中存储的元素达到容量上限以后,ArrayList会创建一个新的数组,新数组的大小为当前数组大小的1.5倍。随后将数组元素拷贝到新数组,如果这个动作频繁执行的话,会增大性能开销。

    1. public ArrayList(int initialCapacity) {
    2. super();
    3. if (initialCapacity < 0)
    4. throw new IllegalArgumentException("Illegal Capacity: "+
    5. initialCapacity);
    6. this.elementData = new Object[initialCapacity];
    7. }
    8. public ArrayList() {
    9. super();
    10. this.elementData = EMPTY_ELEMENTDATA;
    11. }
    12. public ArrayList(Collection<? extends E> c) {
    13. elementData = c.toArray();
    14. size = elementData.length;
    15. // c.toArray might (incorrectly) not return Object[] (see 6260652)
    16. if (elementData.getClass() != Object[].class)
    17. elementData = Arrays.copyOf(elementData, size, Object[].class);
    18. }

    这三个方法都是ArrayList的构造方法,从前两个方法中可以看出初始化ArrayList的时候是如何指定容器初始大小的,这里也无需多言了。那么我们再看看,当容量达到上限的时候,是如何动态扩充数组大小的呢。

    1. public boolean add(E e) {
    2. // 每次添加元素之前先动态调整数组大小,避免溢出
    3. ensureCapacityInternal(size + 1);
    4. // 为什么ArrayList的元素都是顺序存放的?这就是原因,每次都会把最新添加的元素放到数组末尾。
    5. elementData[size++] = e;
    6. return true;
    7. }
    8. private void ensureCapacityInternal(int minCapacity) {
    9. // 如果当前容器为空,那么就先初始化数组,数组大小不能小于DEFAULT_CAPACITY
    10. if (elementData == EMPTY_ELEMENTDATA) {
    11. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    12. }
    13. ensureExplicitCapacity(minCapacity);
    14. }
    15. private void ensureExplicitCapacity(int minCapacity) {
    16. modCount++;
    17. // 容器会在什么时候扩容? 就是他了! 如果当前元素数量达到了容器的上限,那么就扩充数组
    18. if (minCapacity - elementData.length > 0)
    19. grow(minCapacity);
    20. }
    21. private void grow(int minCapacity) {
    22. // oldCapacity为当前容器大小
    23. int oldCapacity = elementData.length;
    24. // oldCapacity >> 1和oldCapacity / 2是等效的,因此newCapacity为原来的1.5倍
    25. int newCapacity = oldCapacity + (oldCapacity >> 1);
    26. // 因为第一次容器有可能为空,elementData.length==0,newCapacity会小于minCapacity
    27. if (newCapacity - minCapacity < 0)
    28. newCapacity = minCapacity;
    29. // 当然newCapacity也不能大于MAX_ARRAY_SIZE,因为数组能分配的最大空间就是Integer.MAX_VALUE
    30. if (newCapacity - MAX_ARRAY_SIZE > 0)
    31. newCapacity = hugeCapacity(minCapacity);
    32. // 当确定好数组大小后,就可以进行数组拷贝,Arrays.copyOf的底层是一个native方法,后续有机会会讲到他的实现。
    33. elementData = Arrays.copyOf(elementData, newCapacity);
    34. }
    35. private static int hugeCapacity(int minCapacity) {
    36. if (minCapacity < 0) // overflow
    37. throw new OutOfMemoryError();
    38. return (minCapacity > MAX_ARRAY_SIZE) ?
    39. Integer.MAX_VALUE :
    40. MAX_ARRAY_SIZE;
    41. }

    以上就是ArrayList实现动态扩容的原理。那么我有一个问题,当容器满了以后需要扩容,那当容器元素不足1/2或者更少的时候是否需要动态减容呢?

    下面写了若干测试代码,分别给出了3中情况,创建的时候设定容器大小和使用默认大小,然后通过逐个增加元素,观察数组大小变化。

    2.第二种情况,创建一个默认大小的容器,则数组大小为10。

    3.第三种情况,创建一个大小为11的容器,则数组大小为11。
    4. ArrayList & Vector - 图1

    4.第三种情况下,向容器中添加元素,当添加到第12个的时候,数组动态扩容到16,正好符合之前描述的,扩容1.5倍的说法。

    5.第三种情况下,删除容器中的元素,数组大小并不会因此而减小。
    4. ArrayList & Vector - 图2![]
    (/projects/jdk_source_learning/src/img/8.png)

    二、添加元素

    其实ArrayList的add,set方法都非常简单。一句话概括,就是对数组元素的操作。

    1.add方法:

    1. public boolean add(E e) {
    2. // 检查扩容
    3. ensureCapacityInternal(size + 1);
    4. elementData[size++] = e;
    5. return true;
    6. }
    7. public void add(int index, E element) {
    8. ensureCapacityInternal(size + 1);
    9. System.arraycopy(elementData, index, elementData, index + 1,
    10. size - index);
    11. elementData[index] = element;
    12. size++;
    13. public boolean addAll(Collection<? extends E> c) {
    14. Object[] a = c.toArray();
    15. int numNew = a.length;
    16. ensureCapacityInternal(size + numNew);
    17. System.arraycopy(a, 0, elementData, size, numNew);
    18. size += numNew;
    19. return numNew != 0;
    20. }
    21. public boolean addAll(int index, Collection<? extends E> c) {
    22. rangeCheckForAdd(index);
    23. Object[] a = c.toArray();
    24. int numNew = a.length;
    25. ensureCapacityInternal(size + numNew);
    26. int numMoved = size - index;
    27. if (numMoved > 0)
    28. System.arraycopy(elementData, index, elementData, index + numNew,
    29. numMoved);
    30. System.arraycopy(a, 0, elementData, index, numNew);
    31. size += numNew;
    32. return numNew != 0;
    33. }

    这里给出了四种add方法,add(E e)添加到数组末尾,add(int index, E element)添加到指定位置。添加元素的时候,首先都要检查扩容,而add(int index, E element)方法中多一步操作,就是将指定位置以后的所有元素向后移动一位,留出当前位置用来存放添加的元素。后面两种addAll方法原理和前两种一样,无非他是添加一个集合元素的区别。

    2.set方法:

    1. public E set(int index, E element) {
    2. rangeCheck(index);
    3. E oldValue = elementData(index);
    4. elementData[index] = element;
    5. return oldValue;
    6. }

    set和add的区别就是,add是添加一个元素,而set是替换元素,size不变。

    1.remove单个元素:

    1. public E remove(int index) {
    2. rangeCheck(index);
    3. modCount++;
    4. E oldValue = elementData(index);
    5. int numMoved = size - index - 1;
    6. if (numMoved > 0)
    7. // 直接进行数组拷贝操作,把index后的所有元素向前移动一位。
    8. System.arraycopy(elementData, index+1, elementData, index,
    9. numMoved);
    10. elementData[--size] = null; // 把元素设空,等待垃圾回收
    11. return oldValue;
    12. }
    13. public boolean remove(Object o) {
    14. if (o == null) {
    15. for (int index = 0; index < size; index++)
    16. if (elementData[index] == null) {
    17. fastRemove(index);
    18. return true;
    19. }
    20. } else {
    21. for (int index = 0; index < size; index++)
    22. if (o.equals(elementData[index])) {
    23. fastRemove(index);
    24. return true;
    25. }
    26. }
    27. return false;
    28. }
    29. // 之所以叫做快速删除,是因为他被设置为一个私有方法,只能在内部调用,删除元素的时候,省去了数组越界的判断。也不返回被删除的元素,直接进行数组拷贝操作。
    30. private void fastRemove(int index) {
    31. modCount++;
    32. int numMoved = size - index - 1;
    33. if (numMoved > 0)
    34. System.arraycopy(elementData, index+1, elementData, index,
    35. numMoved);
    36. elementData[--size] = null;
    37. }

    2.删除集合元素
    removeAll和remove方法思想也是类似的,但是这里有个细节我认为作者处理的非常妙,有必要拿出来品味一下。那么妙在哪里呢?原来这里有两个方法removeAll和retainAll他们正好是互斥的两个操作,但是底层都调用了同一个方法来实现,请看!

    1. // 删除包含集合C的元素
    2. public boolean removeAll(Collection<?> c) {
    3. Objects.requireNonNull(c);
    4. return batchRemove(c, false);
    5. }
    6. // 除了包含集合C的元素外,一律被删除。也就是说,最后只剩下c中的元素。
    7. public boolean retainAll(Collection<?> c) {
    8. Objects.requireNonNull(c);
    9. return batchRemove(c, true);
    10. }
    11. private boolean batchRemove(Collection<?> c, boolean complement) {
    12. final Object[] elementData = this.elementData;
    13. int r = 0, w = 0;
    14. boolean modified = false;
    15. try {
    16. for (; r < size; r++)
    17. // 我认为这里有两点值得我们学习
    18. // 第一,作者巧妙的提取了逻辑上的最大公约数,仅通过一行逻辑判断就实现了两个互斥的效果。
    19. // 第二,作者的所用操作都集中于elementData一个数组上,避免了资源的浪费。
    20. if (c.contains(elementData[r]) == complement)
    21. elementData[w++] = elementData[r];
    22. } finally {
    23. // 理论上r==size 只有当出现异常情况的时候,才会出现r!=size,一旦出现了异常,
    24. // 那么务必要将之前被修改过的数组再还原回来。
    25. System.arraycopy(elementData, r,
    26. elementData, w,
    27. w += size - r;
    28. }
    29. if (w != size) {
    30. // 被删除部分数组后,剩余的所有元素被移到了0-w之间的位置,w位置以后的元素都被置空回收。
    31. for (int i = w; i < size; i++)
    32. elementData[i] = null;
    33. modCount += size - w;
    34. size = w;
    35. modified = true;
    36. }
    37. }
    38. return modified;
    39. }

    当我读到这段代码的时候,我忍不住赞叹,代码之美,美在逻辑的严谨,美在逻辑的简约,也终于明白了,何为对称美。

    四、迭代器

    在java集合类中,所有的集合都实现了Iterator接口,而List接口同时实现了ListIterator接口,这就决定了ArrayList他同时拥有两种迭代遍历的基因—Itr和ListItr。

    1.Itr
    Itr实现的是Iterator接口,拥有对元素向后遍历的能力

    2.ListItr
    ListItr不但继承了Itr类,也实现了ListIterator接口,因此他拥有双向遍历的能力。这里着重介绍一下向前遍历的原理。

    1. public boolean hasPrevious() {
    2. return cursor != 0;
    3. }
    4. public int previousIndex() {
    5. // 通过cursor-1,将指针向前移位。
    6. return cursor - 1;
    7. }
    8. public E previous() {
    9. checkForComodification();
    10. int i = cursor - 1;
    11. if (i < 0)
    12. throw new NoSuchElementException();
    13. Object[] elementData = ArrayList.this.elementData;
    14. if (i >= elementData.length)
    15. throw new ConcurrentModificationException();
    16. cursor = i;
    17. return (E) elementData[lastRet = i];
    18. }

    ListItr同时增加了set和add两个方法

    1. // 替换当前遍历到的元素
    2. public void set(E e) {
    3. if (lastRet < 0)
    4. throw new IllegalStateException();
    5. checkForComodification();
    6. try {
    7. ArrayList.this.set(lastRet, e);
    8. } catch (IndexOutOfBoundsException ex) {
    9. throw new ConcurrentModificationException();
    10. }
    11. }
    12. // 添加一个元素到当前遍历到的位置
    13. public void add(E e) {
    14. checkForComodification();
    15. try {
    16. int i = cursor;
    17. ArrayList.this.add(i, e);
    18. cursor = i + 1;
    19. lastRet = -1;
    20. expectedModCount = modCount;
    21. } catch (IndexOutOfBoundsException ex) {
    22. throw new ConcurrentModificationException();
    23. }
    24. }
    1. public List<E> subList(int fromIndex, int toIndex) {
    2. subListRangeCheck(fromIndex, toIndex, size);
    3. return new SubList(this, 0, fromIndex, toIndex);
    4. }

    这里指的子集,就是指定list的起始位置和结束位置,获取这段范围内的集合元素。那么这有什么作用呢?当单独获取了这段子集以后,就可以独立的对待他,他的起始元素将从0开始。那么这是怎么实现的呢?原来他是通过维护一个SubList内部类,每次读取元素的时候,配合一个offset偏移量,精确的找到elementData数组中对应位置的元素了。由于代码量过多,我这里就象征性的展示其中的一个get方法。

    1. public E get(int index) {
    2. rangeCheck(index);
    3. checkForComodification();
    4. // 子集中的位置+偏移量==实际数组中的位置
    5. return ArrayList.this.elementData(offset + index);
    6. }

    六、ArrayList和Vector在多线程环境下对比。

    1.测试代码

    1. list size = 2
    2. list size = 2
    3. list size = 2
    4. list size = 5
    5. list size = 4
    6. list size = 7
    7. list size = 8
    8. list size = 9
    9. list size = 3
    10. list size = 10
    11. list size = 11
    12. list size = 12
    13. list size = 6
    14. list size = 13
    15. list size = 14
    16. ArrayList 的测试结果: 理论上最终的size应该等于15,但是他却是14,说明ArrayList不是线程安全的
    17. vectorrun size = 3
    18. vectorrun size = 3
    19. vectorrun size = 3
    20. vectorrun size = 5
    21. vectorrun size = 7
    22. vectorrun size = 8
    23. vectorrun size = 9
    24. vectorrun size = 4
    25. vectorrun size = 6
    26. vectorrun size = 10
    27. vectorrun size = 11
    28. vectorrun size = 12
    29. vectorrun size = 13
    30. vectorrun size = 14