[关闭]
@gzm1997 2018-12-24T07:32:41.000000Z 字数 2993 阅读 885

ArrayList

java基础


郭柱明


前言

集合框架有很多可以直接使用的集合 比如常用的就有ArrayList跟HashMap 这篇文章主要研究一下ArrayList


基本用法

声明并且初始化一个arraylist

  1. ArrayList<Integer> arrayList = new ArrayList<Integer>();
  2. arrayList.add(1);
  3. arrayList.add(2);
  4. arrayList.add(3);
  5. /*获取list的大小*/
  6. System.out.println("size of ArrayList is " + arrayList.size());

注意

ArrayList<Integer> arrayList = new ArrayList<Integer>();

尖括号里面只可以是类比如Integer 而不可以是基本数据类型比如int那些

我们先来看看arraylist中有哪些字段

  1. //序列化id
  2. private static final long serialVersionUID = 8683452581122892189L;
  3. /**
  4. * 默认的初始化容量是10
  5. */
  6. private static final int DEFAULT_CAPACITY = 10;
  7. /**
  8. * 被空实例用的空数组实例
  9. */
  10. private static final Object[] EMPTY_ELEMENTDATA = {};
  11. /**
  12. * 被那些默认容量 但是目前还是空的实例用的数组实例
  13. * 跟上面那个EMPTY_ELEMENTDATA不一样的地方是 这个当第一个元素被添加的时候他知道什么时候开始扩张容量
  14. */
  15. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  16. /**
  17. * 这就是真正存储着arraylist的元素的数组
  18. */
  19. transient Object[] elementData; // non-private to simplify nested class access
  20. /**
  21. * 表示数组的长度
  22. */
  23. private int size;

接下来我们看一下

arrayList.add(1);

中发生了什么

  1. public boolean add(E e) {
  2. ensureCapacityInternal(size + 1); // Increments modCount!!
  3. elementData[size++] = e;
  4. return true;
  5. }

上面很明显是先确定容量是否足够 然后使用下标定位到数组中元素到最后 把新的元素添加上去 那么我们看一下ensureCapacityInternal函数是怎样确定容量是否足够 以及怎么扩张容量的 这个函数最终是使用grow函数进行扩容的

  1. private void grow(int minCapacity) {
  2. // overflow-conscious code
  3. int oldCapacity = elementData.length;
  4. int newCapacity = oldCapacity + (oldCapacity >> 1);
  5. if (newCapacity - minCapacity < 0)
  6. newCapacity = minCapacity;
  7. if (newCapacity - MAX_ARRAY_SIZE > 0)
  8. newCapacity = hugeCapacity(minCapacity);
  9. // minCapacity is usually close to size, so this is a win:
  10. elementData = Arrays.copyOf(elementData, newCapacity);
  11. }

从上面我们很显而易见地知道 每一次扩张容量都是在原来容量的基础上增加一半

int newCapacity = oldCapacity + (oldCapacity >> 1);

从上面我们可以知道ArrayList的空间都是不够就动态增加的 主要使用Arrays.copyOf这个函数重新分配空间 那么就会有一个问题 如果一开始我们无参构造ArrayList的时候 没有指定容量 那么默认容量就是10 如果这时候我们要add大量的元素 比如1000个 那么就需要多次动态扩容 这样会造成被动扩容和数组复制的额外开销

所以 使用ArrayList之前指定恰当的容量 那么是可以避免上面的额外开销 但是如果容量没有指定恰当 有可能会造成内存溢出


遍历问题

我看别人的博客 说ArryList一般有三种遍历方式


下标遍历

  1. /*下标遍历 这是最快的一种方式*/
  2. for (int i = 0; i < arrayList.size(); i++) {
  3. /*使用下标定位数组元素的时候 只可以使用get函数定位*/
  4. System.out.println(arrayList.get(i));
  5. }

显而易见 这是最快的一种遍历方式 没什么好说的

for循环遍历

  1. /*for循环遍历*/
  2. for (Integer integer : arrayList) {
  3. System.out.println(integer);
  4. }

这种方法效率介于中间 但是因为无法看到这个方法的源代码 所以无从分析

迭代器遍历

  1. /*使用迭代器进行遍历*/
  2. Iterator<Integer> iterator = arrayList.iterator();
  3. while (iterator.hasNext()) {
  4. System.out.println(iterator.next());
  5. }

这是效率最低的一种方法 我们可以看一下他的源代码分析一下

arrayList.iterator();

这里返回一个内置类的实例 这个内之类实现了Iterator接口 我们可以看一下这个内置类的一些字段比较有意思

  1. int cursor; // index of next element to return
  2. int lastRet = -1; // index of last element returned; -1 if no such
  3. int expectedModCount = modCount;

前面两个变量顾名思义 看注释也可以知道他们是干什么的 后面两个
expectedModCount表示预期的数组被修改的次数 modCount是实际上被修改的次数 这两个字段有什么用呢?

首先我们来看一个问题

ArrayList是非线性安全的 就是有可能同时被多个线程操作的话 出现数据不一致性 所以ArrayList必须一个时间内只可以给一个单独的线程操作

但是如果在遍历的时候 如何防止我在遍历一个ArrayList的时候 避免这个arraylist同时被其他线程修改呢

迭代器遍历这种方法就可以做到这种效果 因为迭代器的hasNext函数跟Next函数都可以使用checkForComodification函数来检查是否在遍历的时候被其他线程修改了 怎么检查? 靠的就是上面两个字段expectedModCount modCount 具体如下

  1. final void checkForComodification() {
  2. if (modCount != expectedModCount)
  3. throw new ConcurrentModificationException();
  4. }

所以了 我们可以看得出 上面三种遍历ArrayList的方式 从效率来看肯定是
下标定位 > for循环 > 迭代器

但是如果涉及线程安全的时候 又恰巧这时候使用的ArrayList(虽然在线程安全的时候不推荐使用ArrayList) 我们就可以使用迭代器遍历的方法遍历ArrayList

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注