ArrayList空间回收机制分析

371 阅读4分钟

前言

本文通过数组与arraylist对比重点分析arraylist原理,针对图中所列内容逐一讲解。

  数组

数组是非常常见的数据结构,数组是一块连续的内存空间,通过下标可以随机访问数组元素。 如下图所示,通过左侧index访问数组内容data。由于使用连续空间所以每次分配要保证内存充足。由于java中int 长度32位是四个字节

                         

  1. 图中长度为4此时如果插入第5个元素将会报错,因为数组创建后长度固定但可以覆盖之前元素即第6个元素可以覆盖前五个元素中任意一个,数组不支持插入新的元素但可以修改原有元素
  2. 因为数组在创建时已经分配固定空间并且连续,所以可以使用下标访问,在C语言中分配了数组的变量地址指向数组中第一个元素内存地址,因此通过下标访问就是基于第一个元素地址+下表*元素空间大小=内存地址
  3. 如果创建10个元素的数组但存放的数据少于10个,此时想获取有效数据的个数是无法直接得到的,需要额外遍历后进行判断
  4. 如果在数组中间位置删除一个元素后,数组内容无法对齐需要额外的移动操作才可以

在java中为了丰富数组的操作引入了ArrayList

ArrayList

ArrayList 底层基于数组实现,但提供了更加强大的功能 如下图所示

在java中数组长度和ArrayList中的size的区别

数组中长度是指初始化时设置的长度

  • 例如new String[10] 表示数组长度为10即使数组中未存放任何元素其长度也为10

ArrayList中没有长度的概念而是用容量代表,容量代表实际存放元素的个数

  • 例如 new ArrayList(10) 表示分配了长度为10的list,如果没有存放任何元素其容量为0

动态分配空间

由于ArrayList底层是用对象数组作为存储数据结构,因此当空间不够时需要重新分配数组空间。

  1. ArrayList提供两种构造方法,不带参数和指定容量的构造
  2. EMPTY_ELEMENTDATA&DEFAULTCAPACITY_EMPTY_ELEMENTDATA分别用于两种首次扩容方式,这两个值代表了两种不同的扩容方式

而真正实现扩容逻辑在add方法中

第一种add(E)插入方式如下

假设使用new ArrayList()进行初始化,在插入三个数据后内存空间使用情况如下

  1. new ArrayList()默认分配长度为10的数组

  2. add(E)是在size++位置插入而不是elementData.length-1后插入

第二种add(i,E)将元素插入中间位置,因此需要将该位置以后的所有元素进行移动然后在插入

两者在插入前均需要做扩容判断防止空间不足

扩容具体逻辑存在此方法中

ensureCapacityInternal(size + 1);
  1. 判断数组长度是小于size+1 如果小于需要扩容,如果不小表示空间充足
  2. 扩容后是在原有空间的基础上增加0.5倍大小
  3. 通过位运算右移实现除2操作
  4. 关于扩容为何不是扩容1个容量而是扩容为原来的1.5倍防止插入操作过多引起频繁扩容影响效率

思考:在扩容时由于分配了多余的空间如果这部分空间长期不使用是否可以回收?

下面介绍trimToSize方法来实现该功能

  • 此方法首先判断当前容量是否超过了数组总长度如果没有超过,重新分配实际元素个数作为数据长度此时数组长度=size大小

在ArrayList中 get set sort逻辑比较简单 通过指定下标获取元素,更新元素内容,对整个list进行排序

重点说下remove&contain方法

  • remove方法重点说下remove(E)方法,删除与E相同的元素
  • contain(E)方法判断list中是否包含该元素

方法都需要进行元素间的比较来确定目标元素而java对象的比较使用的是equals方法,在一些自定义对象中一定要重写此方法不然在使用ArrayList的时候remove,contains 方法不生效或出现预期之外的效果

总结

ArrayList作为数组的升级版提供了丰富的功能,避免直接使用数组时需要实现的基础功能,掌握了ArrayList的内部原理后在使用上更加得心应手