数组的简单理解
数组
简单的说,数组就是一组定长连续的用来存储数据的结构,定长是指,在创建一个数组的时候,会设置该数组的长度,以便在内存里申请连续的内存地址,连续是指,这些申请的内存块是一个接一个的,只要知道了起始的地址,只要加上存储内容的子节长度x要访问的是第几个,就可以方便的找到需要访问的数据。
Java里的数组操作
这里看的是jdk1.8里的 java\util\ArrayList 主要看一下其中的增加和删除方法。
add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里的add函数是向数组末尾添加一个对象,代码并不多,首先是一个意思是确定内部容量的函数ensureCapacityInternal ,然后就是把这个元素添加到数组后,size是类的私有变量,记录了数组的长度,接下来我们就来看看确定内部容量函数
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
首先看一下这个calculateCapacity计算容量函数,elementData存储的是数组里的数据,DEFAULTCAPACITY_EMPTY_ELEMENTDATA定义的是一个空的对象数组,这个计算容量函数的功能是,确保数组的大小最小是默认大小10,在来看这个ensureExplicitCapacity函数
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
modCount是一个私有变量,表示数组修改的次数,这个if判断是确保要确认的容量大于存储数据的对象数组的长度,因为只有这样确认容量才有意义,接下来继续看grow函数
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);
}
newCapacity 等于原本的容量加上该容量右移一位,相当于原容量的1.5倍,然后用newCapacity -MAX_ARRAY_SIZE,这个MAX_ARRAY_SIZE也是在之前定义了的,为Integer.MAX_VALUE-8,这里就有两种情况了,首先是newCapacity 处于Integer.MAX_VALUE到Integer.MAX_VALUE-8之间,其次是newCapacity 已经为负数了,因为之前的1.5倍操作,导致该数值超过了Integer.MAX_VALUE,所以变为了负数(最高位变为1),然后这个hugeCapacity函数就是用来判断如果是负数了就是溢出了抛出异常,没溢出就把最大值放开到Integer.MAX_VALUE,至于为什么是Integer.MAX_VALUE-8代码注释也做了解释,有的vm在数组中保存了数组的长度,所以需要留下8位去存储。最后就调用了Arrays.copyOf来创建新的数组,把之前的数据复制过去,最底层调用了System.arraycopy这个native函数,这块我们就不看了。接下来来看add的两个参数的重载方法
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
首先这个rangeCheckForAdd函数检查了index的范围,不能大于size或者小于0,这很好理解,然后就是确认容量,然后是复制的过程,把index之后的元素都往后移一个单位,然后再index插入需要插入的元素,应该很好理解。
remove
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; // clear to let GC do its work
return oldValue;
}
先检查index大小是否大于等于size了,数组的实际下标是size -1 大于等于size都会抛出异常,然后获取该index的值,如果index是负数这步会报错,接下来就是把删除index之后的数据全部前移,然后把最后的一个数据设置为null,如果数组只有一个对象那就直接赋值为null,接下来是同名重载函数
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
首先判断该对象是不是null,因为null的判断方式不能用equals,不是null的话就用for循环查找到与传入的对象相同的对象的index,然后用fastRemove函数删除,这个函数就是第一个remove函数的简化版,去掉了一些验证,因为已经可以保证参数的准确性了。
总结
可以从增删改查这四个操作去理解数组,改查可以算是同一个方向,都是在原数组上操作,不需要对数组进行大规模的修改,都比较简单。删除操作,需要前移后面的数据。增加操作,可能涉及到扩容操作,这时数组已经换成一个更大容量的数组了。