吐槽
周末啊,冷啊啊啊啊,然后怕自己周六中午睡起来都晚上了,就不睡午觉了去实验室看下ArrayList的源码。
之前自己学集合只是简单的看了下用法,写项目的时候虽然用这块但是也没仔细看下这块到底咋实现的。
ArrayList的基本功能
首先这个货是个数组
数组就是存放东西的一个仓库
但是这个和普通的普通的数组还是有区别的
它的特殊的地方就是可以动态的添加或者减少这个数组里面的元素emmmmm//当然也是有限制的不可能无限放东西
我们今天要看的就是ArrayList的几个问题
- Arraylist动态添加或者减少怎么样实现的?
- 它的线程的安全性?
- 它的最大的容量是多少?
- java中Array和ArrayList区别?
ArrayList类的介绍
首先去文档上看下这块的继承关系
发现这个货继承了AbstractList然后实现了四个个接口List, RandomAccess, Cloneable, Serializable
根据我们看他继承的类和试下的接口看到他有一下的能力
- ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能
- ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。
- ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
- ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
我们还是去看下这块的源码吧233也不是很长,我们一部分一部分看
类的成员变量介绍
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
//可序列化的版本号
private static final long serialVersionUID = 8683452581122892189L;
//默认的数组大小为10 重点
private static final int DEFAULT_CAPACITY = 10;
//实例化一个空的数组 当用户指定的ArrayList为0的时候 返回这个
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
//一个空数组实例 当用户没有指定 ArrayList 的容量时(即调用无参构造函数),返回的是该数组==>刚创建一个 ArrayList 时,其内数据量为 0。
//当用户第一次添加元素时,该数组将会扩容,变成默认容量为 10(DEFAULT_CAPACITY) 的一个数组===>通过 ensureCapacityInternal() 实现
//它与 EMPTY_ELEMENTDATA 的区别就是:该数组是默认返回的,而后者是在用户指定容量为 0 时返回
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
//存放List元素的数组 保存了添加到ArrayList中的元素。实际上,elementData是个动态数组 ArrayList基于数组实现,用该数组保存数据, ArrayList 的容量就是该数组的长度
//该值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入 ArrayList 中时,数组将扩容值 DEFAULT_CAPACITY(10)
transient Object[] elementData;
//List中元素的数量 存放List元素的数组长度可能相等,也可能不相等
private int size;
//这个数字就是最大存放的大小emmmmmm
private static final int MAX_ARRAY_SIZE = 2147483639;
里面也蛮清楚的有两个总要的对象:
elementData 数组 "Object[]类型的数组,后面的初始化,其他方面有很重要的用处
size 这个是动态数组的实际大小
构造方法
有三个
public ArrayList(int var1) {
if (var1 > 0) {
//创建一样大的elementData数组
this.elementData = new Object[var1];
} else {
if (var1 != 0) {
//传入的参数为负数时候 报错
throw new IllegalArgumentException("Illegal Capacity: " + var1);
}
//初始化这个为空的数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
//构造方法 无参 数组缓冲区 elementData = {}, 长度为 0
//当元素第一次被加入时,扩容至默认容量 10
public ArrayList() {
//初始化这个为空的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造方法,参数为集合元素
public ArrayList(Collection<? extends E> var1) {
//将集合元素转换为数组,然后给elementData数组
this.elementData = var1.toArray();
if ((this.size = this.elementData.length) != 0) {
//如果不是object类型的数组,转换成object类型的数组
if (this.elementData.getClass() != Object[].class) {
this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
}
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
构造方法里面这块就发现elementData数组真的是作用很大啊
我们发现这块它被修饰的时候是用transient修饰的
transient是干嘛的啊
当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。例如,当反序列化对象——数据流(例如,文件)可能不存在时,原因是你的对象中存在类型为java.io.InputStream的变量,序列化时这些变量引用的输入流无法被打开。
那么为什么ArrayList里面的elementData为什么要用transient来修饰?
因为ArrayList不能序列化和反序列化吗?肯定不是,是因为elementData里面不是所有的元素都有数据,因为容量的问题,elementData里面有一些元素是空的,这种是没有必要序列化的。ArrayList的序列化和反序列化依赖writeObject和readObject方法来实现。可以避免序列化空的元素。
序列化的
存放元素和改变容量的方法
//改变数组的长度,使长度和List的size相等。
//集合中元素个数的size和表示集合容量的elementData.length可能不同,在不太需要增加//集合元素的情况下容量有浪费,可以使用trimToSize方法减小elementData的大小
public void trimToSize() {
++this.modCount;//继承自AbstractList中的字段,表示数组修改的次数,数组每修改一次,就要增加modCount
if (this.size < this.elementData.length) {
this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size);
}
}
//确定ArrayList的容量
//判断当前elementData是否是EMPTY_ELEMENTDATA,若是设置长度为10
public void ensureCapacity(int var1) {
int var2 = this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA ? 0 : 10;
if (var1 > var2) {
//需要扩充
this.ensureExplicitCapacity(var1);
}
}
//最小扩充容量,默认是 10
//如果elementData为空的时候 看下长度和10比较的结果,找最大的
//判断是不是空的ArrayList,如果是的最小扩充容量10,否则最小扩充量为0
//上面无参构造函数创建后,当元素第一次被加入时,扩容至默认容量 10,就是靠这句代码
private void ensureCapacityInternal(int var1) {
// 若用户指定的最小容量 > 最小扩充容量,则以用户指定的为准,否则还是 10
if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
var1 = Math.max(10, var1);
}
//扩容
this.ensureExplicitCapacity(var1);
}
//minCapacity和默认大小(10)比较,如果需要扩大容量,继续调用
private void ensureExplicitCapacity(int var1) {
++this.modCount;
if (var1 - this.elementData.length > 0) {
this.grow(var1);
}
}
//扩容的操作
private void grow(int var1) {
// 防止溢出代码
int var2 = this.elementData.length;
//容量扩容为之前的1.5倍
int var3 = var2 + (var2 >> 1);
//对新的容量进行判断
// 若 newCapacity 依旧小于 minCapacity
if (var3 - var1 < 0) {
var3 = var1;
}
// 若 newCapacity 大于最大存储容量,则进行大容量分配
if (var3 - 2147483639 > 0) {
var3 = hugeCapacity(var1);
}
//复制旧元素到新的数组上面
this.elementData = Arrays.copyOf(this.elementData, var3);
}
//这个就是判断大小的
private static int hugeCapacity(int var0) {
if (var0 < 0) {
//如果传入的小于0的话 报错
throw new OutOfMemoryError();
} else {
//大于0的话 如果大于 ((2^31)-1) = 2147483647-8 = 2147483639
return var0 > 2147483639 ? 2147483647 : 2147483639;
}
}
我们看到这块的代码
扩容的时候利用位运算把容量扩充到之前的1.5倍
比如说用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,会重新分配内存空间,使数组的大小增长到16。
可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->…
将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。
可以通过调试看到动态增长的数量变化:4->7->11->17->26->…
公式就是 新的容量 = (旧的容量*3)/2 +1;
然后还要检测下大小,不能超出最大的范围
那那那这个Java中ArrayList最大容量是多少啊?
大约是8G
看下源码发现是2147483639 这个数字emmmmmmmmm
看下别人的源码是private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;好像是我看的源码版本问题
Integer.MAX_VALUE 是 2的31次方减一
为什么要减8,,,原因好像是因为只是为了避免一些机器内存溢出, -8 是为了减少出错的几率,虚拟机在数组中保留了一些头信息。避免内存溢出。
增加
//这个是 不指定位置的话 添加到数组末尾
public boolean add(E var1) {
this.ensureCapacityInternal(this.size + 1);
this.elementData[this.size++] = var1;
return true;
}
//指定位置的情况
public void add(int var1, E var2) {
//检验下是否插入的位置在数组容量范围内
this.rangeCheckForAdd(var1);
//检查是否需要扩容
this.ensureCapacityInternal(this.size + 1);
System.arraycopy(this.elementData, var1, this.elementData, var1 + 1, this.size - var1);
//腾出新空间添加元素
this.elementData[var1] = var2;
//修改数组内元素的数量
++this.size;
}
private void rangeCheckForAdd(int var1) {
if (var1 > this.size || var1 < 0) {
throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
}
}
//检验下是否插入的位置在数组容量范围内
private void rangeCheckForAdd(int var1) {
if (var1 > this.size || var1 < 0) {
throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
}
}
//检测是否要扩容
private void ensureCapacityInternal(int var1) {
if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
var1 = Math.max(10, var1);
}
this.ensureExplicitCapacity(var1);
}
private void ensureExplicitCapacity(int var1) {
++this.modCount;
if (var1 - this.elementData.length > 0) {
this.grow(var1);
}
}
查找
它这个有两个查找
普通查找
逆序查找 思路也蛮清楚的
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
删除
这个和数组的删除类似的emmmmmmmmm
都是把指定位置的元素删除后,它后面的元素统一向前移动一位
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
//要移动的长度
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一个元素置空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
//移除指定的第一个元素
// 如果list中不包含这个元素,这个list不会改变
// 如果包含这个元素,index 之后的所有元素依次左移一位
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//快速删除下标第index的元素
private void fastRemove(int index) {
modCount++;//这个地方改变了modCount的值了
int numMoved = size - index - 1;//移动的个数
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //将最后一个元素清除
}
ArrayList线程不安全的原因
虽然之前一直背的是ArrayList的线程不安全,但是还是不知道是为什么,操作系统课上对这块的理解就是当两个线程请求数据的时候,可能一个线程不小心把另一个线程的里面的东西改了,这块又和计算机的存储有关了
一般来说,,,遇到 变量++这种的,,,很容易遇到线程不安全的问题
我们来看下这块的线程不安全的原因
在之前的成员变量里面我们说过里面有两个货特别重要
transient Object[] elementData;
private int size;
elementData是个动态数组 ArrayList基于数组实现,用该数组保存数据, ArrayList 的容量就是该数组的长度
个size变量用来保存当前数组中已经添加了多少元素
外面再进行add()操作时候的一系列源代码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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);
}
之前上面分析了这几个函数了的 这块就不讲了
主体思路就是我要添加一个元素,先进行判断一下,看下里面空间够不够,不够的话我就扩容什么的,然后再把这个元素加进去。里面其实就是两部操作
1判断elementData数组容量是否满足需求
2在elementData对应位置上设置值
所以,这块就是出现线程不安全的第一个地方,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:
- 列表大小为9,即size=9
- 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断
- 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法
- 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回
- 线程B也发现需求大小为10,也可以容纳,返回。 线程A开始进行设置值操作, elementData[size++] = e线程B也发现需求大小为10,也可以容纳,返回
- 线程A开始进行设置值操作, elementData[size++] = e线程A开始进行设置值操作, elementData[size++] = e操作。此时size变为10。
- 线程B也开始进行设置值操作,它尝试设置elementData[10] =线程B也开始进行设置值操作,它尝试设置elementData[10] =e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.
然后第二个线程不安全的地方就是 elementData[size++] = e 设置值的操作同样会导致线程不安全。这块不是原子操作,所以会脏数据,他由两步操作构成
1elementData[size] = e;
2size = size + 1;
所以这块也就是在多线程的时候执行的话出现问题了
- 列表大小为0,即size=0
- 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
- 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
- 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
- 线程A开始将size的值增加为1线程A开始将size的值增加为1
- 线程B开始将size的值增加为2线程B开始将size的值增加为2
这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。
如何使ArrayList线程安全
- 继承Arraylist,然后重写或按需求编写自己的方法,这些方法要写成synchronized,在这些synchronized的方法中调用ArrayList的方法。
- List list = Collections.synchronizedList(new ArrayList());
当多线程访问这些容器类时,可能会出现数据同步导致的问题,java的工具类java.util.Collections提供了将非同步对象转换为同步对象的方法
当多线程访问这些容器类时,可能会出现数据同步导致的问题,java的工具类java.util.Collections提供了将非同步对象转换为同步对象的方法
自己对ArrayList的语言总结
因为昨天学长给我们模拟面试,然后我发现昨天早上才把源码看了,但是自己还是讲东西给面试官还是很卡顿。所以我觉的把每次学的东西自己组织语言过一遍,然后再给别人讲就会好一点。
问:简单介绍下ArrayList
答:ArrayList是以数组实现,可以自动扩容的动态数组,当超出限制的时候会增加50%的容量,用ystem.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。arrayList的性能很高效,不论是查询和取值很迅速,但是插入和删除性能较差,该集合线程不安全。
问:ArrayList的自动扩容怎么样实现的
关键字:elementData size ensureCapacityInternal
答:每次在add()一个元素时,arraylist都需要对这个list的容量进行一个判断。如果容量够,直接添加,否则需要进行扩容。在1.8 arraylist这个类中,扩容调用的是grow()方法。
在核心grow方法里面,首先获取数组原来的长度,然后新增加容量为之前的1.5倍。随后,如果新容量还是不满足需求量,直接把新容量改为需求量,然后再进行最大化判断。
通过grow()方法中调用的Arrays.copyof()方法进行对原数组的复制,在通过调用System.arraycopy()方法进行复制,达到扩容的目的。
问:ArrayList的构造方法过程答:ArrayList里面有三种构造方法,第一种:无参的构造方法 先将数组为空,第一次加入的时候 然后扩充默认为10, 第二种是有参的构造方法 ,直接创建这个数组 第三种是传入集合元素,先将集合元素转换为数组,把不是object的数组转化为object数组。
问:ArrayList可以无限扩大吗?答:不能,大于是8G,因为在ArrayList扩容的时候,有个界限判断。 private static final int MAX_ARRAY_SIZE = 2147483639,2的31次方减一然后减8,-8 是为了减少出错的几率,虚拟机在数组中保留了一些头信息。避免内存溢出。