Zer0e's Blog

ArrayList扩容之后内存地址会发生改变吗?

字数统计: 1.3k阅读时长: 5 min
2020/09/13 Share

前言

那标题的这个内容,是我在面试时候的一道问题。那这篇文章我们来讲讲ArrayList的扩充机制以及扩容之后会发生什么样的改变。

正文

基础

首先使用过Java的基本上都用过ArrayList,那它是一个数组队列,其实现了List接口,那ArrayList的底层是一个动态数组。我们来看看默认的构造器

1
2
3
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这里的源码基于JDK1.8,那其他的源码这里就不贴了,我直接进行总结:

  • ArrayList内部有一个数组,使用默认的构造函数时,默认容量为0。而在1.6以前,默认容量为10.
  • 当第一次插入元素时,容量分配10,之后会以1.5倍的增长。

扩容机制

还是简单写写ArrayList的扩容机制。首先默认的构造器

1
2
3
4
5
6
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

我们可以知道默认的容量是0。那扩容机制一定会发生在add操作中,我们查看add方法

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

ensureCapacityInternal是确保内部容量,也就是确保数组容量应该是现有数据数量加上1。接着跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}

那这里就很明朗,calculateCapacity通过传入的需要容量与当前数组容量,计算出需要的最小容量,其中如果数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那么就返回DEFAULT_CAPACITY与需要容量的最大值,DEFAULT_CAPACITY为10。
因此当第一次扩容时,容量被设置为10,其余情况最小容量都为传入的所需容量。
接着是ensureExplicitCapacity方法,当最小容量大于当前数组长度时,进行扩容操作,即grow方法,我们接着跟进:

1
2
3
4
5
6
7
8
9
10
11
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);
}

首先新容量的计算方式为oldCapacity + (oldCapacity >> 1),那么相当于oldCapacity + 0.5 * oldCapacity,即1.5倍的原容量,而采用位运算则更快。其次这里判断了元素的最大个数,源码里的注释解释道,原因是因为jvm允许数组的最大大小为Integer.MAX_VALUE - 8,超过这个值会OutOfMemory,因此MAX_ARRAY_SIZE设置为Integer.MAX_VALUE - 8避免溢出。
最后是调用Arrays.copyOf方法来获得一个新的数组,而copyOf方法是调用System.arraycopy方法,这个方法是个native方法,就不是我们所关心的内容了。
至此,扩容的机制就是如果是默认构造器初始化,则设置当前容量为10,当所需容量大于10时,会进行1.5倍的扩容,以此类推。

ArrayList对象分配在哪?

首先ArrayList是new出来的,那么毫无疑问,它存放在堆中,而变量存放在栈中,指向堆中的对象。就比如:

1
ArrayList<Integer> list = new ArrayList<>();

其中list变量在栈中,指向堆中的某个ArrayList实例对象。
那数组也不例外,因为数组也是个对象,所以内部的elementData也存放在堆当中。

那扩容之后的地址改变吗?

这就是标题所提到的问题,先说结论,肯定是不变的。我们先来做个试验:

1
2
3
4
5
6
public static void main(String[] args) throws Exception{
ArrayList<Integer> list = new ArrayList<>(1);
for (int i = 0;i<20;i++){
list.add(i);
}
}

通过对add下断点,我们可以发现list的id是不变的,因此内存当中的地址是不改变的。通过步入调试,可以发现elementData的id发生了改变,因为我们可以得出结论,ArrayList扩容之后地址不会发生改变,发生改变的是内部数组的内存地址。
那为什么不变呢?根本原因在于list变量所指向的对象是同一个,很好理解,尽管实例内部的elementData一直在改变,但实例中只是某个变量的指向不断在改变而已,并没有重新分配内存,只有当内部方法重新new本类时,list所指向的内存地址才会发生改变。
因此,大部分通过内部变量引用机制进行扩容的容器并不会改变自身的内存地址
但一些基础容器,例如数组,通过Arrays.copyOf方法进行扩容时,因为数组内存是有序的,所以需要重新分配内存,导致地址发生改变。

总结

本篇谈了谈ArrayList的扩容机制以及扩容之后地址的改变情况。那相似的问题,比如hashmap扩容等等我们都知道了,因为其内部有引用,所以只是内部变量的地址发生改变,其本身的内存地址不发生改变,对象还是同一个。
那以上就是这篇文章的内容,如有问题,望指出。

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 基础
    2. 2.2. 扩容机制
    3. 2.3. ArrayList对象分配在哪?
    4. 2.4. 那扩容之后的地址改变吗?
  3. 3. 总结