数组的简单理解

数组

简单的说,数组就是一组定长连续的用来存储数据的结构,定长是指,在创建一个数组的时候,会设置该数组的长度,以便在内存里申请连续的内存地址,连续是指,这些申请的内存块是一个接一个的,只要知道了起始的地址,只要加上存储内容的子节长度x要访问的是第几个,就可以方便的找到需要访问的数据。

Java里的数组操作

这里看的是jdk1.8里的 javautilArrayList 主要看一下其中的增加和删除方法。

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_VALUEInteger.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函数的简化版,去掉了一些验证,因为已经可以保证参数的准确性了。

总结

可以从增删改查这四个操作去理解数组,改查可以算是同一个方向,都是在原数组上操作,不需要对数组进行大规模的修改,都比较简单。删除操作,需要前移后面的数据。增加操作,可能涉及到扩容操作,这时数组已经换成一个更大容量的数组了。

原文地址:https://www.cnblogs.com/ljsh/p/12661244.html