·ArrayList集合底层数据结构介绍
·ArrayList继承关系
·ArrayList源码
一、ArrayList继承关系
继承自AbstractList类,其中有一些add,remove方法,可以直接调用,也可以重写;
实现了RandomAccess, Cloneable, java.io.Serializable接口
三个接口:
|–都是标记接口:接口中没有需要实现的方法,是空接口,注解也可以实现标记
|–RandomAccess:说明该类支持随机访问,大部分是基于数组实现,仿佛打标签
|–遍历方式:for循环,如果不支持随机,用迭代器遍历
|–Cloneable:说明该类支持拷贝,数据量比较大的时候调用clone方法,速度很快,涉及深拷贝,浅拷贝
|–浅拷贝:直接使用Object中的clone方法,拷贝出来的对象不是独立的对象,都是拷贝的栈中的内容
|–引用类型对象拷贝之后,旧值随着新值改变,因为引用对象的值存在堆中,拷贝栈中的内容后,变 量名变了,但是地址没变,指向的对象也是一个
|–int类型对象拷贝之后,旧值不变,因为栈中不允许两个相同的变量名,改过变量名之后新值和旧值 分别有对应的数据
|–深拷贝:将对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响源对象,ArrayList是深拷贝
|–String虽然也是引用类型,但是由于存在常量池中,所以单拿出来说,string对象在栈中复制之后,不允许重名,但是地址没有变化,但是如果改变的话,会新开辟一段内存空间,然后将新复制的地址指向堆中对象,然后新地址也会更改
|–java.io.Serializable:序列化用的最多的地方,网络传输,写磁盘,arrayList都存在内存中,但是断电就 无了,所以要存盘; 反序列化就是从磁盘/网络读取对象,把流恢复成对象;
|–serialVersionUID:类文件的签名,理解为md5值,如果类发生变化,ID值就会变,md5值变化,反序列化就会失败,所以源代码中它用final修饰,写死就不会变了。
二、全局变量
private static final long serialVersionUID = 8683452581122892189L;
// 默认容量是10
private static final int DEFAULT_CAPACITY = 10;
// 给空对象用的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 看下面,说明调用的是无参构造器,默认容量是10,第一次添加元素时初始化容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 实际存储元素的数组,长度不可变
transient Object[] elementData; // non-private to simplify nested class access
private int size;
DEFAULTCAPACITY_EMPTY_ELEMENTDATA在内存中的位置,作用是节省空间
三、add方法(有两个,一个参数的,两个参数的)
①add(E e)方法
- 首先进入入口方法add,看是否指定了初始容量,未指定则默认为1,指定了则为指定的
- 修改modCount值,自增1,如果当前数组已使用长度+1后大于当前数组的容量,调用grow方法,增长数组,grow方法将当前数组扩容为1.5倍,如果扩容为1.5被还放不下,那只能扩容到你需要的最小,且不能超过Integer.Max_Value
- 确保新增数据有地方存储后,将新数组添加到位于size的位置上
- 返回bool值
public boolean add(E e) {
//检查当前数组长度 确保内部容量 如果不足size+1 则grow扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 确保内部容量的方法,第一次添加元素时size值为1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
值为1,将当前elementData数组的长度变为10,但是要是指定了容量的话就不走判断了
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 修改次数,modCount自增,判断是否需要扩充数组长度,若当前所需数组的最小长度大于数组长度,增长数组长度
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 如果当前数组使用空间+1后,大于数组长度,增大数组容量,
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容到1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容到1.5倍还不够,则扩容到当前期望的容量minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 需要的容量不能超出Integer.Max,超出了可能就oom了
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);
}
// 把原数组拷贝一份,重新赋值给new elementData,原数组没有引用了,就被回收啦
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 调用了arraycopy,这是个一native方法
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
插个modcount,迭代器
1、for:ArrayList线程不安全,数组中的元素占用的内存空间是相等的(对象是存的内存地址),当我们要找一个元素时,首先是寻址,只需要知道数组的头部地址,就能得到下标的内存地址,则第i个地址为xxx+i*y,y是初始每个元素占用的内存长度,时间复杂度O(1),外面裹上for循环就是O(n),如果是链表,那寻找起来有点麻烦,所以arrayList是通过for循环,没通过迭代器。
2、迭代器:有个游标,存在全局变量中,游标指向链表头部,next是通过游标找到的链表头部,再调用一次把游标往后移一位,然后next找到第二个,不需要一个一个遍历了,只需要取到游标就知道返回什么。
3、modCount:如果数组中有元素被删除了,那迭代器是感知不到的,会报错,数组下标越界;多线程时可能就发生这种情况了,modcount是存储操作数,然后给expectedModcount,如果后面modcount不等于expectedModcount了,说明list有改动,再去迭代,会导致脏读或者幻读,所以源码中直接抛出异常了,这也就是fail-fast机制,提前检查,节省性能。
4、并行迭代器:将list分割为多个,单线程累加,每一个都用单线程求和,最后汇总
int expectedModCount = modCount;
if (i >= elementData.length)
throw new ConcurrentModificationException();
②add(int index, E element)方法
根据元素的位置,指定位置去插入元素
- 确保插入的位置小于等于当前数组长度,并且不小于0,否则抛异常
- 确保size+1后能存下下一个数据
- 修改modcount值,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组
- grow方法会将当前数组的长度变为原来容量的1.5倍。
- 确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。
- 将新的数据内容存放到数组的指定位置(index)上
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//从index开始 所有元素后移一个,
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
四、subList方法,截取元素
指向的还是原list,并不是独立的list,没有生成新的数组,只是存了下标值而已。
public List<E> subList(int fromIndex, int toIndex) {
// 先检查是否越界
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
- // todo
get,set,remove方法整理
五、ArrayList特点
ArrayList自己实现了序列化和反序列化的方法,因为它自己实现了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法
ArrayList基于数组方式实现,无容量的限制(会扩容)
添加元素时可能要扩容(所以最好预判一下),删除元素时不会减少容量(若希望减少容量,trimToSize()),删除元素时,将删除掉的位置元素置为null,下次gc就会回收这些元素所占的内存空间。
线程不安全
add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位
get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))
remove(Object o)需要遍历数组
remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高
contains(E)需要遍历数组
使用iterator遍历可能会引发多线程异常
参考链接
https://blog.csdn.net/fighterandknight/article/details/61240861
https://www.bilibili.com/video/BV1gV411y772?p=6