ArrayList
参考:https://blog.csdn.net/weixin_36378917/article/details/81812210
一、ArrayList的数据结构
ArrayList的底层数据结构就是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。
二、ArrayList的线程安全性
对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。
具体举例说明:在单线程运行的情况下,如果Size = 0,添加一个元素后,此元素在位置 0,而且Size=1;而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增 加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而Size却等于 2。这就是“线程不安全”了。
如果非要在多线程的环境下使用ArrayList,就需要保证它的线程安全性,通常有两种解决办法:第一,使用synchronized关键字;第二,可以用Collections类中的静态方法synchronizedList();对ArrayList进行调用即可。
三、ArrayList的主要成员变量
- private static final int DEFAULT_CAPACITY = 10;
当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组的容量大小,为10。
- private static final Object[] EMPTY_ELEMENTDATA = {};
当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
- private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
- transient Object[] elemetData;
ArrayList的底层数据结构,只是一个对象数组,用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候此字段是不会被序列化的。
- private int size;
实际ArrayList中存放的元素的个数,默认时为0个元素。
- private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;
ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE – 8。
四、ArrayList的构造方法
-
无参构造方法
对于无参构造方法,将成员变量elementData的值设为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
1 public ArrayList() {
2 // 无参构造函数,设置元素数组为空
3 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
4 }
-
int类型参数构造方法
参数为希望的ArrayList的数组的长度,initialCapacity。首先要判断参数initialCapacity与0的大小关系:
如果initialCapacity大于0,则创建一个大小为initialCapacity的对象数组赋给elementData。
如果initialCapacity等于0,则将EMPTY_ELEMENTDATA赋给elementData。
如果initialCapacity小于0,抛出异常(非法的容量)。
1 public ArrayList(int initialCapacity) {
2 if (initialCapacity > 0) { // 初始容量大于0
3 this.elementData = new Object[initialCapacity]; // 初始化元素数组
4 } else if (initialCapacity == 0) { // 初始容量为0
5 this.elementData = EMPTY_ELEMENTDATA; // 为空对象数组
6 } else { // 初始容量小于0,抛出异常
7 throw new IllegalArgumentException("Illegal Capacity: "+
8 initialCapacity);
9 }
10 }
-
Collection<? extends E>类型构造方法
第一步,将参数中的集合转化为数组赋给elementData;
第二步,参数集合是否是空。通过比较size与第一步中的数组长度的大小。
第三步,如果参数集合为空,则设置元素数组为空,即将EMPTY_ELEMENTDATA赋给elementData;
第四步,如果参数集合不为空,接下来判断是否成功将参数集合转化为Object类型的数组,如果转化成Object类型的数组成功,则将数组进行复制,转化为Object类型的数组。
1 public ArrayList(Collection<? extends E> c) { // 集合参数构造函数
2 elementData = c.toArray(); // 转化为数组
3 if ((size = elementData.length) != 0) { // 参数为非空集合
4 if (elementData.getClass() != Object[].class) // 是否成功转化为Object类型数组
5 elementData = Arrays.copyOf(elementData, size, Object[].class); // 不为Object数组的话就进行复制
6 } else { // 集合大小为空,则设置元素数组为空
7 this.elementData = EMPTY_ELEMENTDATA;
8 }
9 }
五、ArrayList的add()方法
在add()方法中主要完成了三件事:首先确保能够将希望添加到集合中的元素能够添加到集合中,即确保ArrayList的容量(判断是否需要扩容);然后将元素添加到elementData数组的指定位置;最后将集合中实际的元素个数加1。
1 public boolean add(E e) { // 添加元素 2 ensureCapacityInternal(size + 1); // Increments modCount!! 3 elementData[size++] = e; 4 return true; 5 }
六、ArrayList的扩容机制
ArrayList的扩容主要发生在向ArrayList集合中添加元素的时候。由add()方法的分析可知添加前必须确保集合的容量能够放下添加的元素。主要经历了以下几个阶段:
第一,在add()方法中调用ensureCapacityInternal(size + 1)方法来确定集合确保添加元素成功的最小集合容量minCapacity的值。参数为size+1,代表的含义是如果集合添加元素成功后,集合中的实际元素个数。换句话说,集合为了确保添加元素成功,那么集合的最小容量minCapacity应该是size+1。在ensureCapacityInternal方法中,首先判断elementData是否为默认的空数组,如果是,minCapacity为minCapacity与集合默认容量大小中的较大值。
第二,调用ensureExplicitCapacity(minCapacity)方法来确定集合为了确保添加元素成功是否需要对现有的元素数组进行扩容。首先将结构性修改计数器加一;然后判断minCapacity与当前元素数组的长度的大小,如果minCapacity比当前元素数组的长度的大小大的时候需要扩容,进入第三阶段。
第三,如果需要对现有的元素数组进行扩容,则调用grow(minCapacity)方法,参数minCapacity表示集合为了确保添加元素成功的最小容量。在扩容的时候,首先将原元素数组的长度增大1.5倍(oldCapacity + (oldCapacity >> 1)),然后对扩容后的容量与minCapacity进行比较:① 新容量小于minCapacity,则将新容量设为minCapacity;②新容量大于minCapacity,则指定新容量。最后将旧数组拷贝到扩容后的新数组中。
1 private void ensureCapacityInternal(int minCapacity) {
2 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判断元素数组是否为空数组
3 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取较大值
4 }
5
6 ensureExplicitCapacity(minCapacity);
7 }
1 private void ensureExplicitCapacity(int minCapacity) {
2 // 结构性修改加1
3 modCount++;
4 if (minCapacity - elementData.length > 0)
5 grow(minCapacity);
6 }
1 private void grow(int minCapacity) {
2 int oldCapacity = elementData.length; // 旧容量
3 int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍
4 if (newCapacity - minCapacity < 0) // 新容量小于参数指定容量,修改新容量
5 newCapacity = minCapacity;
6 if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大于最大容量
7 newCapacity = hugeCapacity(minCapacity); // 指定新容量
8 // 拷贝扩容
9 elementData = Arrays.copyOf(elementData, newCapacity);
10 }
七、向指定位置添加元素
1 public void add(int index, E element) {
2 //指定位置检查
3 rangeCheckForAdd(index);
4 // 扩容检查
5 ensureCapacityInternal(size + 1); // Increments modCount!!
6 //通过拷贝使数组内位置为 index 到 (size-1)的元素往后移动一位
7 System.arraycopy(elementData, index, elementData, index + 1,
8 size - index);
9 // 找到位置添加元素
10 elementData[index] = element;
11 // 元素个数加一
12 size++;
13 }
ArrayList在指定位置添加元素时,是先检查指定位置是否在数组范围内,即数组中元素个数是1,则index得小于或者低于1。然后通过拷贝使数组内位置为 index 到 (size-1)的元素往后移动一位,腾出位置之后放入元素,数组个数加一。
八、删除元素
1 public E remove(int index) {
2 //指定位置检查
3 rangeCheck(index);
4 modCount++;
5 //保留要删除的值
6 E oldValue = elementData(index);
7 //要移动元素个数
8 int numMoved = size - index - 1;
9 if (numMoved > 0)
10 //通过拷贝使数组内位置为 index+1到 (size-1)的元素往前移动一位
11 System.arraycopy(elementData, index+1, elementData, index,
12 numMoved);
13 //清除末尾元素让GC回收
14 elementData[--size] = null; // clear to let GC do its work
15 //返回删除的值
16 return oldValue;
17 }
删除指定位置的元素,先对index进行检查,在将要删除的值保留,计算出需要移动元素个数,再通过拷贝使数组内位置为 index+1到 (size-1)的元素往前移动一位,最后将末尾元素清理让GC回收返回删除值。由于List接口继承了Collection,因此ArrayList还有一个来自Collection接口定义的删除对象boolean remove( Object o ) ,而ArrayList自定义的remove方法是T remove(int index)删除的是下标位置的对象并返回值。
九、总结
-
ArrayList的优点
1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快。
2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已。
3、根据下标遍历元素,效率高。
4、根据下标访问元素,效率高。
5、可以自动扩容,默认为每次扩容为原来的1.5倍。
-
ArrayList的缺点
1、插入和删除元素的效率不高。
2、根据元素下标查找元素需要遍历整个元素数组,效率不高。
3、线程不安全。