面试官问:ArrayList的随机访问和顺序访问的区别?
我:嗯...,什么是随机访问和顺序访问?
OOS:(只要自己不尴尬,尴尬的就是别人)😁
我们知道Java中的ArrayList是一个可调整大小的数组实现,是一种可以动态改变数组大小的数据结构。ArrayList增加删除比较低效,查找很高效。随机访问效率高,随机插入、随机删除效率低。
1.随机访问&顺序访问
对于ArrayList来说,因为它是基于动态数组实现的,所以随机访问和顺序访问的效率都很高。
-
随机访问
若你需要访问ArrayList中特定位置的元素,例如,list.get(5)会立即返回数组中第六个元素(索引从0开始),这种直接根据下标获取元素的方式为随机访问,ArrayList的随机访问是非常效率的,它允许你直接通过数组索引以常数时间复杂度O(1)访问任何位置的元素。
-
顺序访问
当你需要遍历整个ArrayList时,创建一个迭代器Iterator对象,并通过调用hasNext()和next()方法进行遍历,这种顺序遍历访问的方式为顺序访问。ArrayList顺序访问也效率很高的方法。 ArrayList使用随机访问和顺序访问原理上都是通过数组索引获取元素,但是由于迭代器的使用,每次迭代都会调用Iterator的next()方法,而这些方法调用会引入一定的开销。实际使用会总体上较慢的,我们来看一个例子。
java
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList();
for(int i =0;i < 100000000;i++){
list.add(new Integer(i));
}
long time = System.currentTimeMillis();
for(int i =0;i < 100000000;i++){
list.get(i);
}
System.out.println("cost 随机访问 time:"+(System.currentTimeMillis()-time));
//顺序访问
long time1 = System.currentTimeMillis();
for(Integer i:list){
//list.get(i);
}
System.out.println("cost 顺序访问2 time:"+(System.currentTimeMillis()-time1));
Iterator iterator = list.iterator();
long time2 = System.currentTimeMillis();
//顺序访问
while (iterator.hasNext()){
iterator.next();
}
System.out.println("cost 顺序访问2 time:"+(System.currentTimeMillis()-time2));
}
运行结果:
我们发现第一种for循环的随机访问更高效,使用iterator比增强型for(Object i:list)高效。 增强型for循环的方式为啥是最慢的,原因是增强型for循环遍历ArrayList时,底层会创建一个Iterator对象,并通过调用hasNext()和next()方法进行遍历。而普通for循环是直接通过索引来访问元素,没有额外创建Iterator对象和调用方法next()的开销。
当然这是ArrayList集合数据量很大的时候对比。当数据集合在比较小的时候,比如小于1000,基本上ArrayList的上面三种遍历方式就没有区别了。
此外ArrayList中的数组都是Object对象。如果是存放基本类型数据,会有自动装箱和拆箱的开销,获取元素的效率就很较低。
java
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList();
for(int i =0;i < 100000000;i++){
list.add(i); //直接存放int 基本类型,自动装箱
}
long time = System.currentTimeMillis();
for(int i =0;i < 100000000 ;i++){
int o = list.get(i);// 自动拆箱
}
System.out.println("cost 随机访问 time:"+(System.currentTimeMillis()-time));
//顺序访问
long time1 = System.currentTimeMillis();
for(int i:list){// 自动拆箱
list.get(i);
}
System.out.println("cost 顺序访问2 time:"+(System.currentTimeMillis()-time1));
Iterator iterator = list.iterator();
long time2 = System.currentTimeMillis();
//顺序访问
while (iterator.hasNext()){
int o = (int) iterator.next();//拆箱
}
System.out.println("cost 顺序访问2 time:"+(System.currentTimeMillis()-time2));
}
运行结果如下:
ArrayList的随机访问和顺序访问搞明白了,随机访问和顺序访问效率都很高,由于ArrayList顺序访问额外创建Iterator对象和调用方法next()的开销,在数据量很大的时候,ArrayList的随机访问效率高于顺序访问。
我们再来仔细看看这个简单却又不简单的ArrayList类的其他细节,学习java封装的优雅。下回如果遇到面试官说"就是做了一个封装,这也没什么技术突出点"的时候,拿这个Josh Bloch大佬的Collection的封装举例怼他。
2.标记性接口
我们先看看ArrayList的声明,如图所示,ArrayList除了实现List接口,还实现了RandomAccess,Cloneable,Serializable等标记性接口。
List接口提供了集合Collection和Iterable操作方法和定义,比如LinkedList,Vector都是List的具体实现。RandomAccess,Cloneable,Serializable等标记性接口。
-
RandomAccess
RandomAccess就是随机访问的意思,用以标记实现的List集合具备快速随机访问的能力。源码中注释说明也说了这个标记接口,表示List的随机访问快于顺序访问的能力
所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了,这里的快速随机访问,那么就不是所有List集合都支持了。 比如,ArrayList和Vector基于数组实现,天然带下标,可以实现常量级的随机访问,而LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n) ,所以ArrayList和Vector具备快速随机访问功能,而LinkedList没有。
-
Cloneable
Cloneable接口没有包含clone()方法,只是标记类是否支持克隆,如果不实现此接口,调用对象的clone()就会抛出异常。即使重写Object的clone(),但是没有加上Cloneable接口也会报错。
ArrayList中对于Object的clone()。我们看到是基于Arrays.copyOf(elementData, size)
给元素复制。注释说这是一个a shallow copy of this ArrayList instance
,就是一个浅拷贝。原因是Arrays.copyOf()是基于System.arraycopy()实现的,所以ArrayList中的其他元素也都是copy原型对象的地址。都是指向一个对象。
-
Serializable
Serializable同样属于标记接口,他表示类的对象可以实现序列化的能力。关于序列化先关知识原理可能大家都明白,我就不赘述了。我们只要看看ArrayList他是怎么实现的。看源码有实现
writeObject
和readObject
方法,for循环将每个元素写入和读出。而其中每个元素对象也需要实现序列化。否则就会报NotSerializableException
异常
我们看到截图代码中有一个if (modCount != expectedModCount)
的判断,不相等就会出现ConcurrentModificationException
。modCount是啥?这个就是非线程安全的原因。
3.非线程安全
ArrayList非线程安全的,modCount为列表在结构上被修改的次数。
注释的意思是,此字段由迭代器和list Iterator方法返回的迭代器与列表迭代器实现使用。如果此字段的值意外更改,迭代器(或列表迭代器)将抛出ConcurrentModificationException,也就是我们每次增删操作或者改变列表大小的操作,modCount就会加1。如下源码:
java
public boolean add(E e) {
//最终在ensureExplicitCapacity方法修改modCount
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureExplicitCapacity(int minCapacity) {
//修改
modCount++;
//省略
.......
}
private void fastRemove(int index) {
//修改
modCount++;
//省略
.......
}
public void clear() {
//修改
modCount++;
//省略
.......
}
//每次new Itr(),初始化iterator的expectedModCount为当前modCount
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
//省略
.......
int expectedModCount = modCount;
@SuppressWarnings("unchecked")
public E next() {
//判断修改版本
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//省略
.......
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
//省略
.......
//断修改版本
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;//修改expectedModCount
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//省略
.......
}
所以在iterator进行遍历的时候,对list进行增加或者删除都会报ConcurrentModificationException
异常。如果想要在遍历过程中删除元素,可以使用迭代器本身iterator
的remove()
方法,因为他有对本身的expectedModCount
重新赋值。
可以思考怎么优化ArrayList的新增删除的优化。新增上优化方向就是减少扩容的次数。对于删除的优化方向是减少每次
remove
的System.arraycopy
操作.
4.扩容方案
ArrayList是一个动态数组,它是怎么实现扩容的呢。上面我们发现 ensureExplicitCapacity 在每次添加元素的时候就会调用。我们先看看ArrayList的初始大小
java
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
发现有一个DEFAULT_CAPACITY
常量。但是初始化时候,并没有使用,默认在不设置initialCapacity
的情况下,都是空数组{}
。当我们开始添加元素的时候,才开始初始化数组的初始容量。
java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果为空数组,初始化数组的初始容量DEFAULT_CAPACITY,当minCapacity大于DEFAULT_CAPACITY的时候,就开始新的扩容
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 判断是需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//在原来的oldCapacity基础上 扩大1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//判断是否超过最大值 MAX_ARRAY_SIZE,如果超过,扩容到最大的Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
//如果minCapacity已经开始小于0了,已经超出int的最大字节数,比Integer.MAX_VALUE还大,那么抛出OutOfMemoryError异常。
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//否则直接给到Integer.MAX_VALUE最大的大小
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从源码中,我们总结ArrayList的扩容方案,ArrayList没有指定初始容量的情况下,默认elementData为空数组,再添加元素的时候,判断是否需要扩容,每次扩容为当前容量的1.5倍,当数组长度大于Integer.MAX_VALUE - 8
的时候,开始设置超大容量Integer.MAX_VALUE
,当容量超过Integer.MAX_VALUE
的时候,就抛出OutOfMemoryError
异常。
和ArrayList不同的是Vector是线程安全的,是因为Vector对于操作使用synchronized加锁了。
5.总结
- ArrayList的随机访问和顺序访问效率都很高,由于ArrayList顺序访问额外创建Iterator对象和调用方法next()的开销,在数据量很大的时候,ArrayList的随机访问效率高于顺序访问。
- ArrayList实现RandomAccess,Cloneable,Serializable等标记接口,具有快速访问的能力,又具有克隆和序列化的能力。其中克隆是浅拷贝,序列化过程中元素也需要对应实现序列化能力。
- ArrayList是非线程安全的,主要在迭代器对于modCount在列表在结构上被修改的次数的预期判断。
- ArrayList没有指定初始容量的情况下,默认elementData为空数组,再添加元素的时候,判断是否需要扩容,每次扩容为当前容量的1.5倍,当数组长度大于
Integer.MAX_VALUE - 8
的时候,开始设置超大容量Integer.MAX_VALUE
,当容量超过Integer.MAX_VALUE
的时候,就抛出OutOfMemoryError
异常。