java集合之ArrayList

      第一次接触集合这个概念时,我欣喜若狂,想不到世间竟存在此等便捷的类。相对于定长的数组,我更加钟爱可自动扩容的集合,而ArrayList也是我接触到的第一个集合,后来在不断地学习中得知,原来这东西的底层还是数组,记得当时我对它的内部构造产生了好奇,可惜被源码直接劝退,有限的英语水平也令我对那些陌生的命名望而却步。但终究还是抵挡不住对知识的渴望(其实是对自己菜鸡水平的极度恐惧),在一次机缘巧合之下,也就是今天,发现了一套讲解集合的文章,作者选取了其中的部分代码并给出了一些自己的讲解。文章很不错,但和我理想中的讲解还是有些差距,并没有逐行进行讲解,无奈之下,只好打开源码参照文章自己慢慢研究。


首先是我最常用的构造器,相信也是很多人常用的构造器:

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

无参构造器中只有一行代码,elementData这个变量的类型为Object[],也就是ArrayList中封装的真正的数组。DEFAULTCAPACITY_EMPTY_ELEMENTDATA是静态私有的Object空数组。所以说不论我们调用多少次ArrayList(),创建出的集合内部的elementData都指向同一个数组对象。这样做应该是为了节约内存空间吧?

然后是设定初始化容量的构造器:

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

这个构造器的判断也比较简单,如果设定的容量大于0,则直接创建Object数组并赋值给elementData,如果设定的容量为0,将private static final Object[] EMPTY_ELEMENTDATA = {};赋值给elementData,如果为负数,则抛出异常。

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

这个构造器稍微复杂了一丢丢,首先传入的参数必须是Collection接口的实现类,并且泛型必须是E或E的子类,这个E就是创建ArrayList时设置的泛型类型。将传入的参数转为数组赋给elementData,将数组的长度赋给size,这个size也就是存储集合元素个数的成员变量。如果它等于0,说明此时elementData数组中没有元素,所以将EMPTY_ELEMENTDATA赋给elementData,如果不为0,则判断数组类型是否为Object数组(之前了解到一个类只有一个class对象,所以此处使用 != 做判断),elementData = Arrays.copyOf(elementData, size, Object[].class);,这一步操作让我有点迷,经简单查阅资料发现这样做貌似时为了解决一个官方bug,暂时没有深入研究,所以在此处不做详细探讨。

然后文章中讲解了一些核心方法。

获取数据:

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

        return elementData(index);
    }

这个方法实际上做了两步操作,首先判断边界,然后返回具体数据。

判断边界的方法:

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

要获取的数据的下标是否在集合范围之内,如果集合中只有两个元素,而却要获取下标10的数据,那显然就超出了边界,这种情况下就会抛出异常。如果没有超出边界,则获取相应的数据。

@SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

获取数据的方法比较简单就是从底层数组中取出数据再类型转换。

将数据添加到末尾:

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

由代码可见,首先调用了ensureCapacityInternal方法,传入参数为元素个数+1

该方法内部代码如下:

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

判断当前数组是否为DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组,如果为true,则在默认容量(默认容量为10)和传入的参数之间选择一个较大的重新赋值给minCapacity。如果判断结果为false,则继续执行,调用方法ensureExplicitCapacity

该方法内部代码如下:

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
modCount记录的是修改的次数,如果传入参数大于当前数组长度,则调用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);
    }
oldCapacity存储旧的容量,newCapacity存储新的容量,可见新的容量实际上是旧的容量的1.5倍(这里涉及到了一点点位运算的知识)。接下来进行判断,如果新容量小于最小所需容量minCapacity(size+1),则新容
量设置为minCapacity,如果新容量超过最大上限,则调用hugeCapacity方法
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

此方法逻辑较为简单。

最终得到一个理想的newCapacity(集合新容量)后,调用Arrays.copyOf方法对数组进行扩容(实际上得到了一个新的数组)。

将元素插入指定位置:

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);首先判断选定的位置是否合理,比如数组长度为5,却要将新元素插入到下标10的位置,那显然不行。
ensureCapacityInternal(size + 1);这个方法与之前调用时的作用相同。
System.arraycopy(elementData, index, elementData, index + 1, size - 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;
    }

首先,判断是否超出边界,然后增加修改次数,取出要删除的值,最终需要将其返回。通过计算判断是否需要移动数组元素,如果需要,则依旧使用System.arraycopy移动数组元素将需要删除的元素覆盖掉。
注意此时最后一位元素会重复, elementData[--size] = null;将其置为null即可,目测注释是让GC将其回收掉的意思,忽然想起之前了解到将引用数据类型置为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可以直接使用==进行判断,而其他对象需要使用equals判断,由此可见如果集合中存在多个相同的元素,比如多个null,那么调用这个方法会删除位置最靠前的那个元素。

实际删除元素的方法是fastRemove,它的内部代码如下:

private void fastRemove(int index) {
        modCount++;
        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
    }

与按位置删除元素的方法相似度极高,但是,此处不需要做超出边界的判断,也不需要获取要删除的元素,这大概就是方法名中fast的由来,不得不说人家这代码是真的细节。

今天状态不是很好,一边参照着文章一边阅读源码,花费了不少时间才看完了几个核心的部分,倒也不是有多难,主要是精神真的不集中,但总的来说,还是有收获的,看完之后给我的感受就是ArrayList这东西实际上是封装了Object数组

并且提供了一些操作它的方法,之前学习这个集合的一些概念时只是去理解概念,而当阅读了一部分源码之后,感觉无形之中加深了对概念的理解,也矫正了一些之前的误解,看来以后要适当的阅读一些源码。

 
 
 
 
原文地址:https://www.cnblogs.com/wxdmw/p/13719059.html