sunny123456

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

深入理解系列之JAVA数据结构(1)——ArrayList
https://blog.csdn.net/u011552404/article/details/79833914

1、 ArrayList是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。提供了相关的添加、删除、修改、遍历等功能。
2、ArrayList实现了RandmoAccess接口,即提供了随机访问功能。在ArrayList中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。ArrayList实现了Cloneable接口,即覆盖了函数clone(),能被克隆。ArrayList实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
3、和Vector不同,ArrayList中的操作不是线程安全的!但是Vector的线程安全降低了数据操作的效率,所以一般情况下已经不建议使用!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList

说明

关于ArrayList的底层源码如构造函数、增删改查什么的,许多博客也都进行详细的概述!所以这里就不再一一赘述,详细参见:https://blog.csdn.net/fighterandknight/article/details/61240861,这里我只对关键的问题作出解释!为了便于下面的引入源码时的一些说明,首先贴出类参数:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {  
    // 序列化id  
    private static final long serialVersionUID = 8683452581122892189L;  
    // 默认初始容量  
    private static final int DEFAULT_CAPACITY = 10;  
    // 空对象  
    private static final Object[] EMPTY_ELEMENTDATA = new Object[0];  
    // 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值  
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];  
    // 当前数据对象存放地方,当前对象不参与序列化  
    transient Object[] elementData;  
    // 当前数组长度  
    private int size;  
    // 数组最大长度  
    private static final int MAX_ARRAY_SIZE = 2147483639;  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

问题一、 ArrayList传入集合的构造器是如何实现的?##

ArrayList一共有三种构造方法,分别是

	public ArrayList()
	public ArrayList(int initialCapacity)
	public ArrayList(Collection<? extends E> c)
  • 1
  • 2
  • 3

其中第一种方法默认构造数组大小是0,只有在添加第一个数据的时候才自动扩容至默认大小10;第二种构造器是自定义默认大小;第三种构造器即传入集合进行初始化,因为ArrayList的底层是数组实现,所以实际的初始化原理是首先把集合转变成Array类然后再通过深拷贝的方法初始化ArrayList!看源码:

public ArrayList(Collection<? extends E> c) {
			//转换为数组类
	        elementData = c.toArray();
	        if ((size = elementData.length) != 0) {
	            if (elementData.getClass() != Object[].class){
	            //进行深拷贝
	                elementData = Arrays.copyOf(elementData, size, Object[].class);
	        } else {
	                this.elementData = EMPTY_ELEMENTDATA;
	        }
	    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

其中,有人纠结Arrays.copy是深拷贝还是浅拷贝,我们首先看源码:

public static <T> T[] copyOf(T[] original, int newLength) {  
	    return (T[]) copyOf(original, newLength, original.getClass());  
	}  
public static &lt;T,U&gt; T[] copyOf(U[] original, int newLength, Class&lt;? extends T[]&gt; newType) {  
    T[] copy = ((Object)newType == (Object)Object[].class)  
        ? (T[]) new Object[newLength]  
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);  
    System.arraycopy(original, 0, copy, 0,  
                     Math.min(original.length, newLength));  
    return copy;  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到源码是在生成了一个新的对象后,通过系统调用进行拷贝的,即产生了一个新的拷贝数组对象

System.arraycopy(original, 0, copy, 0,   Math.min(original.length, newLength)); 
  • 1

original - 源数组。
0 - 源数组中的起始位置。
copy - 目标数组。
0 - 目标数据中的起始位置。 Math.min(original.length, newLength) - 要复制的数组元素的数量。

如果还是有疑问,我们可以测试:
图一
我们可以清晰的看到,当复制数组改变元素后,并不影响原先数组,所以复制的数组是一个新的对象,进行的是深拷贝!

问题二:扩容机制是什么?

扩容通常发生在添加元素的时候,发现超过当前数组大小(起初默认是10)的时候。其中默认扩容大小是1.5倍。由于ArrayList底层的数组本身并不是动态的,所以扩容之后并不是直接在数组后添加空位置,进而赋值,而是重新深拷贝一个数组长度为当前数组的1.5倍的新数组,然后再添加新的元素!其中扩容源码如下:

private void grow(int arg0) {  
	    int arg1 = this.elementData.length;  
	    int arg2 = arg1 + (arg1 >> 1);  
	    if (arg2 - arg0 < 0) {  
	        arg2 = arg0;  
	    }  
    if (arg2 - 2147483639 &gt; 0) {  
        arg2 = hugeCapacity(arg0);  
    }  
  
    this.elementData = Arrays.copyOf(this.elementData, arg2);  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

我们可以发现,新的数组大小的是arg2=arg1+(arg1>>1),即1.5倍,注意采用位移比乘除效率更高!然后还是调用Arrays.copy函数进行深拷贝!

问题三、为什么增删操作的效率低?

这个问题,需要分开来讲,举例只讲插入的话
1、如果只是插入尾节点且没有空间足够,其实直接赋值就可以,和链表的复杂度一样;
但是如果以下两种情况:
1、超过当前空间(不论是尾插还是中间插入),则需要扩容!因为上文已经说过,扩容会导致深拷贝一个新的数组,那么这个复杂度就是n了!
2、没有超过当前空间,但是在中间插入,假设插入位置为index,则插入操作要使得index后的数据全部往后移动一格——需要注意的是,这个移动一格还是通过深拷贝来实现的:

public void add(int index, E element) {  
	    rangeCheckForAdd(index);  
    ensureCapacityInternal(size + 1);  // Increments modCount!!  
    System.arraycopy(elementData, index, elementData, index + 1,  
                     size - index);  
    elementData[index] = element;  
    size++;  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这个时候,复杂度其实是size-index!删除操作也是这个原理,同样需要拷贝!

问题四、sublist方法中返回的视图是什么意思?

视图是个内部类,可以访问ArrayList的数据!视图代表是原数组对象的偏移索引,也就是说二者指向同一个对象,只不过表征的数组的起始点不同罢了!所以需要注意的是,如果改变了返回的视图的某个索引位的值,原数组也会改变;但是改变原数组的话,将会使得当前的的视图失效!并抛出异ConcurrentModificationException 看源码:

public List<E> subList(int fromIndex, int toIndex) {
	        subListRangeCheck(fromIndex, toIndex, size);
	        return new SubList(this, 0, fromIndex, toIndex);
	    }
  • 1
  • 2
  • 3
  • 4

可以看到,视图虽然是一个新的对象,但是实际数据是通过持有原数组并加上偏移参数表征的,那么改变视图数据自然改变的就会原数组!

问题五、为什么会抛出ConcurrentModificationException异常?

上文讲了获取视图后,如果更改原数组将会抛出ConcurrentModificationException异常,但是这个异常是什么意思呢?ArrayList继承至AbstractList,在AbstractList中有一个参数叫modCount,这个参数在子类包括ArrayList中随处可见:记录每次增删操作的次数,即每变更一次数组,就自加1操作。本身这个也不会出现什么异常,问题就是在sublist视图方法中每次变更操作之前,函数会做一个检查,拿sublist的set函数举例:

public E set(int index, E e) {
	    rangeCheck(index);
	    checkForComodification();
	    E oldValue = ArrayList.this.elementData(offset + index);
	    ArrayList.this.elementData[offset + index] = e;
	    return oldValue;
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里有一个checkForComodification()函数,我们可以进去看看:

private void checkForComodification() {
	            if (ArrayList.this.modCount != this.modCount)
	                throw new ConcurrentModificationException();
	        }
  • 1
  • 2
  • 3
  • 4

上文已经说过,sublist对象是个内部类,可以访问ArrayList数据,所以this.modCount代表的是内部类(即自己本身)的数据,而这个数据是在第一次生成内部类对象的时候(即生成sublist实例的时候)直接获取的外部类ArrayList的参数大小,这样外部类变更原数组的时候导致ArrayList.this.modCount加1,但是this.modCount还是生成实例的时候大小,于是将导致不一致,进而抛出异常!抛出这个异常的还有一个方法,即生成迭代器Iterator的时候,在外部类变更数据的后,继续执行迭代器的next函数,也会导致抛出异常!源码:

public E next() {
	            checkForComodification();
	            int i = cursor;
	            if (i >= size)
	                throw new NoSuchElementException();
	            Object[] elementData = ArrayList.this.elementData;
	            if (i >= elementData.length)
	                throw new ConcurrentModificationException();
	            cursor = i + 1;
	            return (E) elementData[lastRet = i];
	        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览40601 人正在系统学习中
posted on 2023-03-16 08:37  sunny123456  阅读(53)  评论(0编辑  收藏  举报