今天Java上机的时候有一道题是这样的:
本题目要求定义一个长度可变的整型数组IntArray,数组初始长度为5,当输入的数组元素个数超过数组长度时,数组就自动增加5个元素的容量,即数组长度增加5。
也就是说实现一个类似于ArrayList的自动扩容int型的数组. 既然类似于ArrayList, 那不妨来看看ArrayList是如何动态扩容的.
ArrayList是集合类List基于数组的一个实现, 也就是说, ArrayList底层实质上就是一个Object[]数组. 但是数组是定长的, 而我们在使用ArrayList的时候之所以不会有这样的感受就是因为它封装了内部的数组扩容操作, 所以ArrayList如何安全的实现扩容就成了我们的关注点.
1. ArrayList底层数组容量的初始化
在ArrayList初始化的时候, 是可以通过参数initialCapacity来指定底层数组的初始大小. 其构造方法源码节选如下:
// ArrayList的默认容量为10
private static final int DEFAULT_CAPACITY = 10;
// 其底层的Object[]数组默认为空
private static final Object[] EMPTY_ELEMENTDATA = {};
// 带参的构造方法, 参数initialCapacity为创建Object[]数组的时候默认分配的大小.
// 如果initialCapacity为正常的正整数, 将直接创建该大小的Object数组.
// 如果initialCapacity等于0, 则默认创建空的Object[]数组.
// 如果initialCapacity小于0, 那么将抛出IllegalArgumentException异常.
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 不带参的构造方法默认创建一个空的Object[]数组.
// 在之前的JDK1.6中ArrayList的无参构造方法默认创建一个大小为10的Object[]数组.(emmm, 所以李刚的Java疯狂讲义中是讲错了的..)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2. ArrayList底层数组的扩容
关于动态扩容, ArrayList暴露了一个方法void ensureCapacity(int minCapacity), 它的方法名的中文意思就是保证容量. 我们可以通过调用这个方法来设置Object[]数组的大小, 而在这个方法里面就调用了一系列关于ArrayList底层数组扩容的方法, 下来来看一下源码:
// ArrayList暴露的可以更改底层数组容量的方法
// 参数minCapacity为要扩展到的最小容量
public void ensureCapacity(int minCapacity) {
//先获取当前数组的大小
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
//如果希望扩展到的最小容量大于当前实际大小的时候, 调用ensureExplicitCapacity方法
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
// ensureCapacityInternal会在add方法中调用, 用来确保内部容量(add 方法源码附在本代码段最后)
// ensureCapacityInternal中调用ensureExplicitCapacity方法
private void ensureCapacityInternal(int minCapacity) {
// 如果当前数组为空, 那么所需的最小容量就是默认容量(10)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果当前数组的实际长度小于最小需要的容量(minCapacity)就调用扩容函数grow()
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 扩容函数
// >>位运算为右移动一位, 整体相当于newCapacity = oldCapacity + 0.5 * oldCapacity(实现1.5倍扩容)
// JDK1.7以后采用位运算, 提高了计算速度
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);
}
说了前面那么多, 其实就是为了这一句!!! 这句代码就是ArrayList数组扩容的真正操作!!!
elementData = Arrays.copyOf(elementData, newCapacity);
这句代码的意思就是, 重新分配一段大小为newCapacity大小的空间, 再将原数组中的元素复制过去.
为了看得更加清晰, 我们再来看一下Arrays.copyOf()这个方法的源码:
// 将当前数组的实质类型和希望扩展的容量传给下面重载的copyOf()方法
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 执行复制元素操作, 并返回新的数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
2019/03/15补充:
最近学弟被小仁师兄面试,问到了ArrayList的动态扩容,想起来我之前写的这篇博客,回过头看,真的是知其然,但不知其所以然。
之前只知道ArrayList的扩容是调用Arrays.copyOf()方法去创建一个新的数组,但是并没有仔细去研究Arrays.copyOf()里到底是什么样子的。今天去看了源码才知道,ArrayList扩容时创建创建新的数组时分两种情况,一种是直接用new的方式去创建,另一种是通过反射的方式去创建,具体看Arrays.copyOf()方法里的这行代码:
// 如果当前ArrayList的元素的类型等于Object的类型,直接调用new关键字去创建,
// 如果不等于,则使用反射的方式,用newInstance()去创建。
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
这样看下来是不是对ArrayList的底层数组的动态扩容有了一个清晰的认识呢? 其实这种操作就类似于C语言的realloc()函数. 既然提到了realloc()函数, 那就顺便提一下它的原理:
*realloc(void __ptr, size_t __size): 更改已经分配的内存空间, 即更改由malloc()函数分配的内存空间的大小.
如果将分配的内存减少, realloc仅仅是改变索引的信息.
如果是将分配的内存扩大, 则有以下情况:
1)如果当前内存段后面有足够的所需要的内存空间, 则直接扩展这段内存空间, realloc()将返回原指针.
2)如果当前内存段后面的空闲字节不够, 那么就使用堆中的第一个能够满足这一要求的内存块, 将目前的数据复制到新的内存块, 并将原来的内存块释放掉, 返回新的内存块首地址.
3)如果申请失败, 将返回NULL, 此时, 原来的指针仍然有效.