1、知识点
1、ArrayList底层数组默认初始化大小为10,但是使用ArrayList的无参构造函数的时候,并没有马上进行扩容,我们查看源码就会发现,无参构造函数只是将ArrayList中的DEFAULTCAPACITY_EMPTY_ELEMENTDATA(一个空数组)赋值给了底层数组。 2、ArrayList是非线程安全的,是因为ArrayList底层数组,及维护数组的属性等信息,在修改的时候并没有进行同步控制(加锁),所以多线程环境下对这些数据进行修改的时候是互相不可见的,他们修改的值也存在互相覆盖的问题。
3、如果在增强for循环或者迭代器中使用ArrayList时,数组被改变会马上抛出异常。这里说的for循环只有增强的for循环,不包括普通for循环,因为增强for循环底层使用的是迭代器(iterator)ArrayList在实现Iterable接口时,加入了版本号进行控制(ArrayList每次对数组进行修改的操作都会同时增加版本号),迭代过程中会对版本号进行校验,如果与预期版本号不符就会抛出异常。
4、源码中的变量size并不是指的是ArrayList中数组的大小,而是数组中元素的个数,这个看源码的注释就能明白。这个也很好理解,当你初始化一个容量为100的数组的时候(new ArrayList(100); ),当没有添加任何元素时调用size方法,其返回值是0;这个问题其实是要说你要明白两个概念:大小和容量。
5、当采用添加初始化数据的方式初始化ArrayList时(ArrayList(Collection<? extends E> c) ),如果添加的集合中元素不是Object类型(如:数组类型),当调用toArray方法将这个集合转换为Object[]后再对该数组进行修改时,会报数组存储异常(ArrayStoreExecption)。
6、ArrayList在按照对象删除元素(remove(Object obj))的时候,其实也是通过循环并对比的方式来完成的,这里要注意的是这里用于比较的方法是equals方法,那么当这个类是我们自定义的类时就需要特别注意了,如果有这种使用场景那就需要重写equals方法了,比如你自定义的类Student,User。。。
7、说一下扩容: 无参构造函数,会创建一个空数组,当第一次add元素时会初始化为大小为10(默认初始化大小)的数组。
扩容时机:就是当ArrayList中数组满的时候,再加元素时会进行扩容。就是比如数组长度是10,加第11个元素的时候;或者长度是10,现在有9个元素,再一次性加2个元素的时候就会扩容。
扩容大小一般是当前数组大小的1.5倍;扩容的临界值是int的最大值减去8,也就是说ArrayList中允许的数组最大长度为int的最大值减去8。这里可以查看一下jdk1.8源码(网上有些教程中说是int最大值,略微有点不准确):
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
上面这个图其实就是扩容的源码,我们看到其实ArrayList在扩容的时候其实就是看最小容量(minCapacity这个值,也是你向数组中加入元素后元素的个数),有没有大于当前数组大小。如果大于,那就扩容到旧数组的1.5倍,如果旧数组1.5倍还是不够那就扩容到最小容量(比如初始化一个大小为10的数组,结果一次性加入18个元素),如果大于了ArrayList允许的最大长度,那大小就不再增长了。
ArrayList提供了两个空数组,一个是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,一个是EMPTY_ELEMENTDATA,空参构造函数将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给底层数组elementData,而new ArrayList(0)会将EMPTY_ELEMENTDATA赋值给elementData(看下面的图片),这里只是做个区分,好在扩容的时候为空参构造函数扩容为默认长度10的数组(这个可以看上面图片第一个方法)。
扩容其实就是创建一个新的大一些的数组,然后把旧数组中的数据拷贝到新的数组中去,用新的数组代替旧的数组。调用的是Arrays.copyOf()方法,这个方法又是调用了native方法:arraycopy,其实删除ArrayList中删除元素也是通过该方法进行删除操作的。
为什么不是每次扩容都是直接扩容到正好的大小呢?其实我们看到扩容其实就是不断的创建更大的数组来代替原来的数组,并且将旧值复制到新的数组,这样的操作其实是很耗资源的,为了节省频繁复制的开销就需要多预留一定的空间;那么当我们在开发时如果能预先知道我们要创建的数组有多大,就直接在初始化的时候指定数组大小,这样就可以达到节约系统资源的效果。还有就是如果能采用addAll()方法就不要使用循环add()的方式,这样也可以减少扩容次数。
这其中还隐藏着一个小细节,就是0.5倍的这个扩容的量,我们可以发现当数组较小的时候进行扩容其实每次增长的并不是特别多,所以不会占用太多空间;当数组较大时每次扩容增长的就比较多,这样在数据增长很快的情况下就不需要频繁扩容。
|