前言
集合框架有很多可以直接使用的集合 比如常用的就有ArrayList跟HashMap 这篇文章主要研究一下ArrayList
基本用法
声明并且初始化一个arraylist1
2
3
4
5
6ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
/*获取list的大小*/
System.out.println("size of ArrayList is " + arrayList.size());
注意
ArrayList<Integer> arrayList = new ArrayList<Integer>();
尖括号里面只可以是类比如Integer 而不可以是基本数据类型比如int那些
我们先来看看arraylist中有哪些字段1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//序列化id
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认的初始化容量是10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 被空实例用的空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 被那些默认容量 但是目前还是空的实例用的数组实例
* 跟上面那个EMPTY_ELEMENTDATA不一样的地方是 这个当第一个元素被添加的时候他知道什么时候开始扩张容量
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 这就是真正存储着arraylist的元素的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 表示数组的长度
*/
private int size;
接下来我们看一下
arrayList.add(1);
中发生了什么
1 | public boolean add(E e) { |
上面很明显是先确定容量是否足够 然后使用下标定位到数组中元素到最后 把新的元素添加上去 那么我们看一下ensureCapacityInternal函数是怎样确定容量是否足够 以及怎么扩张容量的 这个函数最终是使用grow函数进行扩容的
1 | private void grow(int minCapacity) { |
从上面我们很显而易见地知道 每一次扩张容量都是在原来容量的基础上增加一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
从上面我们可以知道ArrayList的空间都是不够就动态增加的 主要使用Arrays.copyOf这个函数重新分配空间 那么就会有一个问题 如果一开始我们无参构造ArrayList的时候 没有指定容量 那么默认容量就是10 如果这时候我们要add大量的元素 比如1000个 那么就需要多次动态扩容 这样会造成被动扩容和数组复制的额外开销
所以 使用ArrayList之前指定恰当的容量 那么是可以避免上面的额外开销 但是如果容量没有指定恰当 有可能会造成内存溢出
遍历问题
我看别人的博客 说ArryList一般有三种遍历方式
- 下标遍历
- for循环遍历
- 迭代器遍历
下标遍历1
2
3
4
5/*下标遍历 这是最快的一种方式*/
for (int i = 0; i < arrayList.size(); i++) {
/*使用下标定位数组元素的时候 只可以使用get函数定位*/
System.out.println(arrayList.get(i));
}
显而易见 这是最快的一种遍历方式 没什么好说的
for循环遍历1
2
3
4/*for循环遍历*/
for (Integer integer : arrayList) {
System.out.println(integer);
}
这种方法效率介于中间 但是因为无法看到这个方法的源代码 所以无从分析
迭代器遍历1
2
3
4
5/*使用迭代器进行遍历*/
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
这是效率最低的一种方法 我们可以看一下他的源代码分析一下
arrayList.iterator();
这里返回一个内置类的实例 这个内之类实现了Iterator
1 | int cursor; // index of next element to return |
前面两个变量顾名思义 看注释也可以知道他们是干什么的 后面两个
expectedModCount表示预期的数组被修改的次数 modCount是实际上被修改的次数 这两个字段有什么用呢?
首先我们来看一个问题
ArrayList是非线性安全的 就是有可能同时被多个线程操作的话 出现数据不一致性 所以ArrayList必须一个时间内只可以给一个单独的线程操作
但是如果在遍历的时候 如何防止我在遍历一个ArrayList的时候 避免这个arraylist同时被其他线程修改呢
迭代器遍历这种方法就可以做到这种效果 因为迭代器的hasNext函数跟Next函数都可以使用checkForComodification函数来检查是否在遍历的时候被其他线程修改了 怎么检查? 靠的就是上面两个字段expectedModCount modCount 具体如下1
2
3
4final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
所以了 我们可以看得出 上面三种遍历ArrayList的方式 从效率来看肯定是
下标定位 > for循环 > 迭代器
但是如果涉及线程安全的时候 又恰巧这时候使用的ArrayList(虽然在线程安全的时候不推荐使用ArrayList) 我们就可以使用迭代器遍历的方法遍历ArrayList