Java源码科普系列之数据结构篇(1):数组

Java源码科普系列之数据结构篇(1):数组

数组定义

首先来看数组的定义:

数组是具有相同类型的数据组成的有序集合。

这里的“有序”指的是按顺序排列,占用连续的内存空间。记住两个关键词“相同数据类型”和“连续内存空间”就可以啦。打个比方,学校做广播体操的时候,每列队伍占用操场中连续的一块空间,而且前后间隔相等,这列队伍的同学合起来就构成了一个“数组”。

这个队伍可以怎样调整,对应于计算机世界,就可以怎样操作数组。比如可以在头部、中间或尾部插入一位同学,可以移出一位同学,可以替换队伍中某位同学,还可以呼叫第N位同学……这些动作,分别对应了计算机中对数组的插入、删除、替换和随机访问。

Java的ArrayList类中封装了对数组的常用操作,其父接口List定义了如下方法(只截取部分方法):

public interface List<E> extends Collection<E> {
    
    
    boolean add(E e);
    void add(int index, E element);
    E get(int index);
    E set(int index, E element);
    E remove(int index);
}

ArrayList::add

add(E e)方法对应的就是从尾部加入一位同学。

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

从代码中可以看出,这位同学要先确认后面还有没有位置可以站,没有的话就需要把障碍物移开争取更大的连续空间,也就是“扩容”,有位置了之后,就可以直接站进去了。

那么Java中数组最大可以多大呢?因为创建ArrayList时,如果指定大小,就会在构造方法中创建对应大小的数组,所以我们直接用ArrayList写个单元测试。

@Test
public void maxArraySize() {
    
    
    for (int i = 2; i >= 0; i--) {
    
    
        System.out.println("new ArrayList<Integer>(Integer.MAX_VALUE-" + i + ")");
        try {
    
    
            List<Integer> list = new ArrayList<>(Integer.MAX_VALUE - i);
        } catch (Throwable t) {
    
    
            t.printStackTrace();
        }
    }
}

运行后输出如下:

new ArrayList(Integer.MAX_VALUE-2)

java.lang.OutOfMemoryError: Java heap space

new ArrayList(Integer.MAX_VALUE-1)

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

new ArrayList(Integer.MAX_VALUE-0)

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

可以看出,大小为Integer.MAX_VALUE-1时,开始超过了JVM数组大小的限制,而Integer.MAX_VALUE-2报错是因为堆内存溢出。可以加入启动参数-Xms16G -Xmx16G把堆内存调为16G,这样运行就不溢出了,再次运行如下:

new ArrayList(Integer.MAX_VALUE-2)

new ArrayList(Integer.MAX_VALUE-1)

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

new ArrayList(Integer.MAX_VALUE-0)

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

这个时候,已经不报“java.lang.OutOfMemoryError: Java heap space”异常了。

而add(int index, E element)方法则是从指定位置插入一位同学。

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++;
}

首先检查要插入的位置是不是存在的,比如队伍总共就30个人,却想从第31位同学的位置插入,肯定是不存在的。然后是检查地面位置够不够再插入。再接下来是一个数组拷贝操作,想象一下我们想从某位同学前面插进去,那么他会让后面的同学后退,一直传话到最后一位同学,这位同学后退一位之后,前面的同学跟上,这样就腾出了一个位置。也就是从指定位置开始的同学,全都往后移了一个位置。最后新同学就可以直接站进去了。

ArrayList::get

get(int index)方法相当于直接找第N位同学,只不过现实世界中我们N是从1开始,而计算机世界很多语言N从0开始。

public E get(int index) {
    
    
    rangeCheck(index);

    return elementData(index);
}

方法比较简单,先检查号数是在正常范围内的,然后根据号数定位出所站位置,从而找到这位同学。

ArrayList::set

set(int index, E element)方法相当于用一位新同学替换掉指定位置的同学。

public E set(int index, E element) {
    
    
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

和get类似,先检查号数是在正常范围内,然后被换掉的同学出列,新同学顶进去。

ArrayList::remove

remove(int 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; // clear to let GC do its work

    return oldValue;
}

可以继续想象一下,中间某位同学出去之后,后面的同学都要往前挪一位补上空缺,所以删除时代码上也有个数组拷贝操作。

小结

这节讲了数组的概念和ArrayList源码对数组的封装和操作。用学生队伍的排列和调整来做比喻,可以方便地在脑海中演练。重点关注下中间插入时后面同学的后退过程,以及中间移除时后面同学的前进过程。这节介绍完啦,剩下的就靠想象力和动手能力加以巩固了。


欢迎关注公众号,获取推送更方便,遇到问题来交流!

技术长跑

猜你喜欢

转载自blog.csdn.net/CanvaChen/article/details/104011202