前言
对于Java开发者来说ArrayList类的使用是非常高频的,ArrayList是基于数组实现的线性表并在数组的基础上提供了丰富的操作方法。比较常见的有
- ArrayList 自动动态扩容
- ArrayList 支持数据向前,向后移动
- ArrayList 支持按照对象查找
- ArrayList 保存有效元素个数
其实ArrayList还有一个回收空间的方法
- 如果ArrayList实际元素个数小于其底层分配数组长度则可以进行空间回收,当然这种空间回收是指将额外的空间回收而不是把有效数据占用空间回收
本文将通过对比数组与ArrayList进行分析ArrayList源码中部分方法,其中包括空间回收方法。说实话在我工作中空间回收方法使用较少,但是掌握该方法在某些场景下确实可以提高内存利用率。
数组介绍
- 数组的内存空间是连续的,通过下标可以随机访问元素,但查找元素需要遍历整个数组
- 数组在内存中通过 数组首地址+元素空间大小*元素下标获取元素地址
- 获取数组中有效元素个数需要遍历整个数组
- 数组元素删除后数据无法连续存放
数组做为基础数据结构只提供了物理存储特性缺少方便的方法操纵数据,如果通过数组操纵数据需要实现很多方法,因此jdk作者提供了ArrayList类并封装了大量的方法供开发者使用。如下图所示
ArrayList 查询类方法分析
ArrayList 中两个非常重要的变量 elementData,size
- elementData 实际存储元素的buffer,buffer代表缓冲区所以其长度会比实际长度大,ArrayList每次扩容都会预分配长度
- size 代表了 elementData中实际元素的容量而非elementData长度,这二者区别很重要
transient Object[] elementData;
private int size;
ArrayList很多方法围绕这两个变量实现,例如查找,扩容,增加,删除元素等。
ArrayList get(int index)方法支持随机访问的原理是查询elementData数组下标并将元素返回,在访问下标前会检查index是否大于size来控制访问有效数据,index超过size直接抛出数据越界异常。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
sort 方法通过比较器来
indexOf(Object o)方法
因为 elementData 为对象数组,判断对象是否在数组中出现需要通过equals方法,但null作为特殊值需要使用 == 进行判断。
此方法只返回第一个与目标值相等的下标,使用时需要注意
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
contains(Object o)
通过indexOf方法实现,判断列表中是否存在指定元素
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
ArrayList 内存分配与回收方法分析
ArrayList提供了三个构造方法,分别为无参构造,指定容量构造,通过已有集合对象构造
无参构造方法
高频构造方法,相信这个是使用最多的构造方法。该方法只将elementData指向一个空数组没分配任何内存空间。真正分配内存是第一次插入数据时
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
指定容量构造
根据初始容量分配指定长度内存,如果传入大小为0,elementData指向 EMPTY_ELEMENTDATA
此时未分配内存空间。
通过默认构造方法与指定容量为0的构造方法进行进行初始化后内存分布是不同的
ArrayList<String> a =new ArrayList<String>();
ArrayList<String> b = new ArrayList<String>(0);
a.add("abc");
b.add("abc");
System.out.println("a list size:"+a.size());
System.out.println("b list size:"+b.size());
// 打印结果
// a list size:1
// b list size:1
虽然以上代码打印size 都为1但其底层elementData长度不同。
a 列表elementData为10,b 列表elementData为1,通过debug可以观察到两个变量值不同,究其原理将在后面分析。
集合对象构造
通过将Collection对象转化为对象数组后将数组元素复制到elementData完初始化,同事设置size大小为集合长度
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
elementData = EMPTY_ELEMENTDATA;
}
}
add方法向数据中插入元素,在插入元素前需要检查是否有足够空间进行存放,其原理是通过新增后size大小(新增后size=size+1)是否超过elementData数组长度,如果超过则扩容后再将元素存放。
- calculateCapacity 计算后确定扩容容量,此处会检查是否用过默认无参构造函数初始化,如果是第一次插入后扩容大小为10,如果否扩容大小为(size+1)此处解释了上文示例代码中根本原因。
- grow 方法进行移位运算首先右移计算原容量大小为1/2,然后加上原容量,整体扩容容量为原容量1.5倍
此方法调用代码链虽长但逻辑很简单
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
而真正内存分配通过调用底层cpp代码执行arraycopy方法,感兴趣的话可以下载openjdk源码进行查看
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,int length);
remove方法同样使用arraycopy移动数据元素,此处逻辑比较简单
- 检查index是否越界
- 计算需要移动数量
- 移动数据
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
elementData[--size] = null;
return oldValue;
}
冗余空间回收
冗余空间指扩容时额外分配的空间,通过size与elementData的长度比较很容易判断是否存在冗余空间,回收机制同样通过Arrays.copyOf 方法将数组长度重新分配为 size大小。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
总结
ArrayList作为数组的升级版提供了丰富的功能,避免直接使用数组时需要实现的基础功能,掌握了ArrayList的内部原理后在使用上更加得心应手