ArrayList底层详解
- 一、ArrayList集合底层数据结构
- 二、ArrayList继承关系
- 1、Serializable标记性接口
- 2、Cloneable 标记性接口
- 浅拷贝
- 深拷贝
- 3、RandomAccess标记接口
- 4、AbstractList抽象类
- 三、ArrayList源码
- 四、迭代器 / 并发修改异常
- 并发修改异常的特殊情况
- 迭代器默认的remove()方法
一、ArrayList集合底层数据结构
- ArrayList集合介绍
List 接口的可调整大小的数组实现。
数组:一旦初始化长度就不可以发生改变 - 数组结构介绍
增删慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。
查询快:由于数组在内存中是一块连续空间,因此可以根据地址+索引的方式快速获取对应位置上的元素。
二、ArrayList继承关系
1、Serializable标记性接口
- 介绍 类的序列化由实现java.io.Serializable接口的类启用。 不实现此接口的类将不会使任何状态序列化或反
序列化。 可序列化类的所有子类型都是可序列化的。 序列化接口没有方法或字段,仅用于标识可串行化的语
义。
序列化:将对象的数据写入到文件(写对象)
反序列化:将文件中对象的数据读取出来(读对象) - Serializable源码介绍
public interface Serializable{
}
2、Cloneable 标记性接口
- 介绍 一个类实现Cloneable 接口来指示Object.clone() 方法,该方法对于该类的实例进行字段的复制是合
法的。在不实现Cloneable 接口的实例上调用对象的克隆方法会导致异常CloneNotSupportedException 被抛
出。简言之:克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝 - Cloneable源码介绍
- 克隆的前提条件
被克隆对象所在的类必须实现Cloneable 接口
必须重写clone 方法 - clone的基本使用
/*
* 克隆的基本使用:将ArrayList集合的数据clone到另外一个集合
*/
public class ArrayList_Clone {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("人生就是旅途");
list.add("也许终点和起点会重合");
list.add("但是一开始就站在起点等待终点");
list.add("那么其中就没有美丽的沿途风景和令人难忘的过往");
//调用方法进行克隆
Object o = list.clone();
System.out.println(o == list); // false;
System.out.println(o);
System.out.println(list);
}
- clone源码分析
public class ArrayList<E> {
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
}
浅拷贝
- 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
- 使用默认的clone()方法来实现
//浅克隆
注意:首先方法的权限修饰符需要更改为public
方法的返回值可以更改为当前类的类名
@Override
public Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
存在的问题: 基本数据类型可以达到完全复制,引用数据类型则不可以
深拷贝
- 将两个对象完全分离,包括属性也不相同。相当于会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
- 深拷贝实现方式1:重写clone()方法来实现深拷贝
@Override
public Object clone() throws CloneNotSupportedException {
//先克隆出来一个学生对象
Student stu = (Student) super.clone();
//调用Skill类中的克隆方法,克隆出来一个Skill对象
Skill skill = (Skill) this.skill.clone();
//将克隆出来的skill赋值给stu该对象的成员变量
stu.setSkill(skill);
//需要把stu返回
return stu;
}
- 深拷贝实现方式2:通过对象序列化实现深拷贝(推荐)
public Object deepClone(){
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try{
//序列化
bos = new ByteArrayOutputStream();
//把字节流转为对象流
oos = new ObjectOutputStream(bos);
oos.writeObject(this); //当前这个对象以对象流的方式输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
Student copy = (Student) ois.readObject();
return copy;
}catch (Exception e) {
e.printStackTrace();
return null;
}finally {
try{
bos.close();
oos.close();
bis.close();
ois.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
3、RandomAccess标记接口
此接口的主要目的是允许通用算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。
查看Collections类中的binarySearch()方法,源码如下:
由此可以看出,判断list是否实现RandomAccess接口来实行indexedBinarySerach(list,key)或iteratorBinarySerach(list,key)方法。
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快
总结:RandomAccess接口这个空架子的存在,是为了能够更好地判断集合是否ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!
4、AbstractList抽象类
三、ArrayList源码
//默认初始容量为10
private static final int DEFAULT_CAPACITY = 10;
//指定ArrayList容量为0时,返回该空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//没有指定容量时,默认返回该空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//该数组保存添加到ArrayList里的元素
transient Object[] elementData;
/**
* ArrayList构造器
*/
public ArrayList(int initialCapacity) {
//指定容量则创建对应容量数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//指定容量为0返回 EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
//无参构造
public ArrayList() {
//未指定容量返回 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 添加元素
*/
public boolean add(E e) {
//尝试容量加1,看有无必要
ensureCapacityInternal(size + 1); // Increments modCount!!
//添加元素
elementData[size++] = e;
return true;
}
/**
* 添加元素到指定位置
*/
public void add(int index, E element) {
//检查index合法性
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
/**
* ensureCapacityInternal执行细节
* 确认list容量,尝试容量+1,看看有无必要
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//创建的是空数组,那么第一次添加元素指定容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//否则返回size+1
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果需要的最小容量大于数组长度:比如添加第11个元素大于容量10
if (minCapacity - elementData.length > 0)
//扩容
grow(minCapacity);
}
/**
* 扩容操作
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容为1.5倍(10->15)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//扩容后还是小于所需容量,则将容量扩充为minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//扩容到最大容量
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);
}
/**
* set remove get 方法一目了然,不做记录。
*/
ArrayList总结:
ArrayList是基于动态数组实现的,在增删时候,需要数组的拷⻉复制。
ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的⼀半,也就是变为原来的1.5倍
删除元素时不会减少容量,若希望减少容量则调⽤trimToSize()
它不是线程安全的。它能存放null值。
四、迭代器 / 并发修改异常
对集合进行添加元素操作后,获取迭代器
ArrayList 中有一个private 修饰的类 Itr ,它实现了 Iterator 接口
真正删除的方法:
结论:
一、集合每次调用add方法的时候,实际修改次数变量的值都会自增一次
二、在获取迭代器的时候,集合只会执行一次将实际修改集合的次数赋值给预期修改集合的次数
三、集合在删除元素的时候也会针对实际修改次数的变量进行自增的操作
并发修改异常的特殊情况
当要删除的元素在集合的倒数第二个位置的时候,不会产生并发修改异常
原因:
是因为在调用hasNext方法的时候,光标的值和集合的长度一样,那么就会返回false
因此就不会再去调用next方法获取集合的元素,既然不会调用next方法那么底层就不会产生并发修改异常
迭代器默认的remove()方法
如果使用迭代器的remove方法的话,不会产生并发修改异常
结论:
1.迭代器调用 remove 方法删除元素,其实底层真正调用的是集合自己的删除方法来删除元素
2.在调用 remove 方法中会每次都给预期修改次数的变量赋值