ArrayList简介
1、ArrayList是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口。
2、ArrayList与Collection的关系如下图,实现代表继承,虚线代表实现接口:
3、ArrayList实现了RandmoAccess接口,即提供了随机访问的功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
4、ArrayList实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
5、ArrayList实现了java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
6、ArrayList中的操作不是线程安全的,可以选择CopyOnWriteArrayList或者使用Collections中的synchronizedList方法将其包装成一个线程安全的List。
ArrayList的API
1 | // Collection中定义的API |
ArrayList源码分析
属性
ArrayList的主要属性如下代码所示:
1 | //序列化id |
关于Java中transient关键字的解释:
我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
构造函数
1、无参构造函数
如果不传入参数,则使用默认无参构造方法创建ArrayLisy对象,如下:
1 | public ArrayList() { |
注意:此时我们创建的ArrayList对象中的elementData中的长度是0,size是0,当进行第一次add的时候,elementDate将会变成默认的长度:10。
2、带int类型的构造函数
如果传入参数,则代表指定ArrayList的初始数组长度;传入参数如果是大于0,则使用用户的参数初始化;如果参数等于0,则用内部的空对象EMPTY_ELEMENTDATA的地址直接赋值给elementData;否则抛出异常,如下:
1 | public ArrayList(int initialCapacity) { |
3、带Collection对象的构造函数
1.将Collection对象转换成数组,然后将数组的地址赋值给elementData。
2.更新size的值,如果size的值等于0直接将内部空对象EMPTY_ELEMENTDATA的地址赋值给elementData。
3.如果size的值大于0,则执行Arrays.copy方法,把Collection对象的内容copy(可以理解为深拷贝)到elementData中,并且这些元素是按照该collection的迭代器返回它们的顺序排列的。
1 | public ArrayList(Collection<? extends E> c) { |
介绍下System.arraycopy和Arrays.copy方法,分析源码时会经常用到。
System.arraycopy方法:它就是从指定的源数组将元素中复制到目标数组,复制从指定的位置开始,到设定的复制长度结束,然后从目标数组的指定起始位置依次插入。
1 | // src 源数组 |
Arrays.copy方法:它新建了一个数组并且将原数组的内容拷贝到长度为newLength的新数组中,并且返回该新数组。
1 | // original 要复制的数组 |
两者的区别:
1、System.arraycopy需要目标数组,将原数组拷贝到你自己定义的数值里,而且可以选择拷贝的起点和长度以及放入新数组中的位置。
2、Arrays.copyof是系统自动在内部新建一个数组,调用System.arraycopy将原数组的内容拷贝到长度为newLength的新数组中,并返回新建的数组。
添加元素
ArrayList提供了add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)、set(int index, E element)这个五个方法来实现ArrayList增加。
1 | //官方解释:将指定的元素追加到列表(elementData)的末尾 |
看下ensureCapacityInternal方法,以及它内部调用的方法。
1 | 看下ensureCapacityInternal方法,以及它内部调用的方法。 |
1 | //这个方式是判断当前数组是否是个空数组,如果是就返回默认长度10,否则就返回size+1;也就是说如果你是用无参构造函数初始化ArrayList,那么在第一次调用add方法时,默认长度会变成10 |
1 | //这个方法首先将集合修改次数加1,然后判断数组的长度是否可以存入下一个元素,如果长度不够会调用grow方法进行扩容 |
1 | //这个方法首先定义数组新的长度为原来数组长度的1.5倍,如果新长度减去所需数组的最小长度小于0,那么新长度就等于所需数组最小长度;再下面的判断是如果新长度大于MAX_ARRAY_SIZE(ArrayList内部定义MAX_ARRAY_SIZE的值是:2147483639)就调用hugeCapacity方法,最后调用Arrays.copyOf将扩容后的新数组地址赋值给elementData |
1 | //如果扩容长度超过MAX_ARRAY_SIZE,则设置长度为Integer.MAX_VALUE,但不是能百分百成功的,这取决于虚拟机。(如果我们可以在某些虚拟机上可以避免OutOfMemory,我们将另外分配Integer.MAX_VALUE,如果你很幸运(取决于虚拟机),我们将成功) |
最后总结一下add方法的逻辑:
- 确保数组已使用长度(size)加1后可以存入下一个元素。
- 修改次数modCount标识自增1,如果当前数组已使用长度+1后大于当前数组长度,则调用grow方法,扩容数组,grow方法会将当前数组的长度变为原来容量的1.5倍。
- 确保新加的元素有地方存储后,则将新元素添加到位于size++的位置上。
- 返回添加成功的布尔值。
1 | add(int index, E element) |
这个方法和上面的add类型,该方法可以按照元素的位置,指定新元素位置插入。
1 | public void add(int index, E element) { |
该方法首先调用rangeCheckForAdd方法判断指定的位置小于当前数组的长度并且大于0,否则抛出异常。
1 | private void rangeCheckForAdd(int index) { |
第二步调用的ensureCapacityInternal方法和上面的add方法逻辑一样。
第三步调用System.arraycopy方法把指定下标以及后面的元素全部往后移一位。
最后将新的元素放到指定位置(index)上,并将size+1。
1 | addAll(Collection<? extends E> c) |
1 | //按照指定的Collection迭代器所返回的顺序,依次插入到列表尾部。 |
该方法首先传过来的Collection集合转换为数组,然后做扩容处理,接着使用System.arraycopy把转换后的数组复制到列表尾部。
1 | addAll(int index, Collection<? extends E> c) |
1 | //将指定集合中的所有元素插入到此列表中,从指定的位置开始 |
1 | //set(int index, E element)用指定的元素替换此列表中指定位置的元素 |
删除元素
ArrayList提供了外界remove(int index)、remove(Object o)、removeAll(Collection<?> c)、clear()四个方法进行元素的删除。
1 | //remove(int index)移除指定位置上的元素 |
1 | // remove(Object o)移除指定元素 |
1 | // removeAll(Collection<?> c)从此列表中删除指定集合中包含的所有元素 |
1 | //clear() 清空ArrayList内的所有元素,不减小数组容量 |
查找元素
ArrayList提供了get(int index)用读取ArrayList中的元素。由于ArrayList是动态数组,所以我们完全可以根据下标来获取ArrayList中的元素,而且速度还比较快。
1 | public E get(int index) { |
判断元素是否存在列表中
ArrayList提供了contains(Object o)用于判断元素是否存在于列表中。
注意:contains方法会遍历ArrayList。
1 | public boolean contains(Object o) { |
最小化ArrayList的实际存储量
ArrayList提供了trimToSize()方法用于将底层数组的容量调整为当前列表保存的实际元素的大小
1 | public void trimToSize() { |
截取ArrayList部分内容
ArrayList提供了subList(int fromIndex, int toIndex)方法来实现部分数据的截取。
可以从源码中看到其实是创建了一个SubList的内部对象,可以理解为是返回当前ArrayList的部分视图,其实指向的存放数据的还是一个地方。如果修改了subList返回的内容的话,原来的内容也会被修改。
1 | public List<E> subList(int fromIndex, int toIndex) { |
其它方法
1 | //判断ArrayList是否为空 |
1 | //反向查找元素位置,与上述的indexOf相反 |
1 | //将元素全部拷贝到v中 |
1 | //返回ArrayList拷贝后的Object数组 |
1 | //返回ArrayList的模板数组。所谓模板数组,即可将T设置为任意数据类型 |
1 | //将ArrayList中的元素写入到输入流中,先写容量,在写元素 |
1 | //从输入流中读取数据到elementData中,一样是先读容量,再读数据 |
小结
- ArrayList自己实现了序列化和反序列化,因为它实现了writeObject和readObject方法。
- ArrayList基于数组实现,会自动扩容。
- 添加元素时会自己判断是否需要扩容,最好指定一个大概的大小,防止后面多次扩容带来的内存消耗;删除元素时不会减少容量,删除元素时,将删除掉的位置元素置为null,下次gc就会自动回收这些元素所占的空间。
- ArrayList是线程不安全的。
- 使用iterator遍历可能会引发多线程异常。
- 本文作者: 生活,生活?
- 本文链接: ayjcsgm.github.io/2019/12/09/学习ArrayList看这篇就够了/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!