Java基础系列(四十四):集合之ArrayList

简介

ArrayListVector非常相似,他们都是基于数组实现的集合,都可以动态扩容,只不过Vector是同步的,所需的资源较多,而且比较老,有一些缺点,所以我们现在更多的是去使用ArrayList,而不是Vector。下面,我们在阅读源码的过程中遇到的一些问题对ArrayList进行分析。

源码解读

首先我们从构造函数说起:

    /**
     * 共享空数组对象
     */

    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 和上面的区别在于,
     * 第一次添加元素时知道该 elementData 从空的构造函数还是有参构造函数被初始化的。
     */

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 用于盛放集合元素的数组对象
     */

    transient Object[] elementData;

    /**
     * 有参构造,参数为初始长度,如果参数为0,调用EMPTY_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);
        }
    }

    /**
     * 无参构造,调用了DEFAULTCAPACITY_EMPTY_ELEMENTDATA来初始化
     */

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

这里新建了两个空的常量数组,分别用来构造有参初始长度为0的ArrayList实例和无参的ArrayList实例,这里的无参构造函数实际上的默认长度是10,而有参的初始长度和参数有关。这两个常量空数组起到的更多的是一种标记的作用,用于在后面的动态扩容中分不同的情况。

    /**
     * 提升该ArrayList对象容器的容量,确保可以提供一个该容器存放数据最低所需的容量
     *
     * @param   minCapacity   最低所需容量
     */

    public void ensureCapacity(int minCapacity) {
        //这里可以看出,如果是默认的无参构造,最低容量是10,如果不是,最小是0。这里体现了代码设计的严谨性!
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
        //如果最低所需的容量大于容器初始的最小容量,去调用扩容的方法,这里就体现出了两个不同常量的作用
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }


    /**
     * 计算最小所需容量
     */

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //判断该容器是否是默认无参构造,如果不是,直接返回传入的minCapacity
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //如果是,返回默认容量和期望的最小所需容量中的最大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    /**
     * 扩容到最小所需容量方法
     * @param minCapacity   最小容量
     */

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    /**
     * 扩容到最小所需容量的方法,这里的参数是经过计算后的最小容量
     * @param minCapacity   经过计算后的最小容量
     */

    private void ensureExplicitCapacity(int minCapacity) {
        //这里关于modCount的疑问可以去看前面AbstractList中的实现
        modCount++;
        //这里进行一个关于最小容量和数组的长度比较,如果最小容量大于数组的长度,才会进行扩容
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity);
        }
    }

    /**
     * 数组的最大容量
     * 因为数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8 byte
     */

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 动态扩容,确保数组的容量可以装下所有的元素
     *
     * @param 最低容量
     */

    private void grow(int minCapacity) {
        //首先获取当前的数组的容量
        int oldCapacity = elementData.length;
        //将数组扩容50%,比如原容量是4,扩容后为 4 + 4 / 2 = 6
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容后的容量小于最小所需的容量
        if (newCapacity - minCapacity < 0) {
            //直接将最小容量作为该容器的容量
            newCapacity = minCapacity;
        }
        //如果扩容后的容量大于数组最大的容量
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            //将经过处理后的最小所需容量作为新的容量,最大不超过Integer的最大值
            newCapacity = hugeCapacity(minCapacity);
        }
        //使用Arrays.copyOf将原数组复制到一个新容量的数组,并将拷贝的结果返回给原数组,完成动态扩容。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    

从上面的动态扩容部分的源码分析中,我们可以看到这两个空常量数组的作用所在,这里又会遇到一个问题,在ArrayList的扩容过程中,是按照50%的比例进行扩容的,这里就有一个问题,扩容后的数组的长度一定会大于数组的长度,就会造成空间和资源的浪费,这时候可以使用下列的方法。

     /**
     * 清除集合中的空元素所占的空间,一般在动态扩容后会产生空余的空间
     */

    public void trimToSize() {
        modCount++;
        //如果数组中数据的个数小于数组所占的空间,说明产生了多余的空间
        if (size < elementData.length) {
            //如果没有数据,返回一个EMPTY_ELEMENTDATA对象,否则将数据拷贝一个新的size长度的数组,并赋给原数组
            elementData = (size == 0)
                    ? EMPTY_ELEMENTDATA
                    : Arrays.copyOf(elementData, size);
        }
    }

接下来,我们来看一下如何去获取ArrayList中的元素,

 /**
     * 返回用于存储元素的数组位于某个索引上的元素
     * @param index 需要返回元素的索引
     * @return  返回该索引位置上的元素
     */

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

    /**
     * 返回该集合指定位置的元素
     *
     * @param  index 需要返回元素的索引
     * @return 该集合指定位置的元素
     * @throws IndexOutOfBoundsException 当索引超过集合的长度时,抛出该异常
     */

    @Override
    public E get(int index) {
        //这一步调用的是检查索引越界的方法
        rangeCheck(index);
        //这一步调用的是上面的elementData()方法,本质上还是根据索引去用于存储数据的数组中取
        return elementData(index);
    }

可以看出,本质上底层还是通过数组来实现的,说到对于数组的操作,就必须说到这个在源码中频繁出现的方法

System.arraycopy(Object[] src, int srcPos, Object[] dest, int destPos, int length)

这几个参数的意思分别是:

src:源数组;
srcPos:源数组要复制的起始位置;
dest:目的数组;
destPos:目的数组放置的起始位置;
length:复制的长度。

看着看着,我们会发现一个问题,ArrayList中包括了两个remove方法

    /**
     * 删除位于某个索引位置的元素
     *
     * @param index 即将被删除的元素的索引
     * @return 返回的是被删除的元素
     * @throws IndexOutOfBoundsException 当索引超过集合的长度时,抛出该异常
     */

    @Override
    public E remove(int index) {
        //首先进行索引越界的检查
        rangeCheck(index);
        //由于这个操作会引起结构的变化,所以要将modCount+1
        modCount++;
        //获取原本位于该位置的元素,用于返回
        E oldValue = elementData(index);
        //获取位于被删除索引的前一位的索引
        int numMoved = size - index - 1;
        if (numMoved > 0) {
            //这里的原理是 elementData = {1 ,2 ,3 ,4} ===>
            // 删除索引为1 的元素,然后 0(numMoved)的元素{1}作为头,将{3, 4}(index+1)后的部分拼接到原本的index的位置 ==>
            // {1, 3, 4},
            System.arraycopy(elementData, index+1, elementData, index,
                    numMoved);
        }
        //将原本最后一位,置为null ==> {1,3,4,null},再将size-1,完成删除
        elementData[--size] = null;
        return oldValue;
    }

    /**
     * 删除集合中的指定元素,如果集合中包含该元素,返回true
     *
     * @param o 被删除的元素
     * @return 如果集合中包含该指定元素,返回true
     */

    @Override
    public boolean remove(Object o) {
        //分为两种情况 为null和不为null
        if (o == null) {
            for (int index = 0; index < size; index++) {
                //为null的时候使用 == 判断
                if (elementData[index] == null) {
                    //这里调用的这个方法其实和上面的方法类似,只不过没有返回值,而且没有进行索引的判断
                    fastRemove(index);
                    return true;
                }
            }
        } else {
            for (int index = 0; index < size; index++) {
                //不为null的时候使用 equals 判断
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 这里没有进行索引的越界判断,也没有返回被删除的值,其他的原理和remove(int index)类似
     * @param index 被删除元素的索引,这里之所以不用判断,是因为在调用这个方法的时候就已经进行了判断,不存在越界的可能
     */

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

可以看出两个删除方法的区别在于,一个是根据元素找到索引进行删除,返回的是否删除成功,而一个是根据直接索引进行删除,返回的是被删除的元素,说起删除,下面我们还会看到一个被private修饰的batchRemove(Collection<?> c, boolean complement)方法,而调用这个私有方法的分别是removeAll(Collection<?> c)retainAll(Collection<?> c),而这两个方法的区别在于一个是取交集,一个是取交集之外的元素,是两个刚好对立的方法。

    /**
     * 删除指定集合与collection的交集
     *
     * @param c 需要与集合进行判断的collection
     * @return 如果这次操作改变了集合的结构,返回true
     * @throws ClassCastException 如果集合的元素类型和该集合的元素类型不一致,抛出该异常
     * @throws NullPointerException 如果参数collection为空,抛出空指针异常
     */

    @Override
    public boolean removeAll(Collection<?> c) {
        //首先进行非空校验
        Objects.requireNonNull(c);
        //调用封装好的批量删除的方法,这里传入的参数为false的时候,删除的是交集
        return batchRemove(c, false);
    }

    /**
     * 删除collection元素和该集合交集之外的元素
     *
     * @param c 需要将元素保留在该集合中的collection对象
     * @return 如果这次操作改变了集合,返回true
     * @throws ClassCastException 如果该集合的元素类型和collection中的元素不一致,抛出该异常
     * @throws NullPointerException 如果collection中的元素为空,抛出该异常
     */

    @Override
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        //调用封装好的批量删除的方法,这里传入的参数为true的时候,保留的是交集
        return batchRemove(c, true);
    }

    /**
     * 批量删除的方法
     * @param c 需要和原集合进行对比的collection对象
     * @param complement    为false的时候,删除交集,为true的时候,取交集,删除其他
     * @return
     */

    private boolean batchRemove(Collection<?> c, boolean complement) {
        //下面我写了一个小例子帮助大家理解
        //假设原集合数组为{1,2,3,4},c为{2,3}
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            //size = 4
            for (; r < size; r++) {
                //a.当complement为false,r = 0 和 3 的时候会进入循环
                //b.当complement为true,r = 1 和 2 的时候会进入循环
                if (c.contains(elementData[r]) == complement) {
                    //r = 0 w = 0 elementData[0] = elementData[0]  {1,2,3,4}
                    //r = 3 w = 1 elementData[1] = elementData[3] {1,4,3,4}
                    // r = 1 w = 0 elementData[0] = elementData[1] {2,2,3,4}
                    //r = 2 w = 1 elementData[1] = elementData[2] {2,3,3,4}
                    elementData[w++] = elementData[r];
                }
            }
        } finally {
            //如果contains方法使用过程报异常,将剩余的元素赋给该集合,如果不出现异常的话,是不会进入这个代码块的
            if (r != size) {
                System.arraycopy(elementData, r,
                        elementData, w,
                        size - r);
                w += size - r;
            }
            // w = 2
            if (w != size) {
                for (int i = w; i < size; i++) {
                    //a. elementData[2] = null, elementData[3] = null {1,4,null,null},null元素会被垃圾回收器回收么?
                    //b. elmentData[2] = null, elementData[3] = null {2,3,null,null}
                    elementData[i] = null;
                }
                //修改次数+2
                modCount += size - w;
                //当前的数组数量就是符合条件的元素数量
                size = w;
                //返回操作成功的标志
                modified = true;
            }
        }
        return modified;
    }

接下来,我们会看到两个熟悉的迭代器的实现类ItrListItr,其实是换汤不换药,和AbstractList中的实现并没有太大的区别,下面我们来详细的说一下SubList

SubList

ArrayList类自己维护了一个java.util.ArrayList.SubList类,这里需要注意的是SubList类是作为一个视图存在的,也就是说,如果你对SubList类进行任何操作都会影响到原来的父集合,而不是说独立存在的,下面,我们来看看其中的一个实现。

        /**
         * 将该子集合的index索引位置设置为e
         * @param index 被设置的索引,这里的索引是子集合的相对索引
         * @param e 被设置的元素
         * @return
         */

        @Override
        public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            //这里获取的是由偏移量和相对索引相加得到的基于父集合的绝对索引,实际上操作的还是父集合。
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }

        /**
         * 在指定索引位置添加元素,原集合元素向后顺延一位
         * @param index
         * @param e
         */

        @Override
        public void add(int index, E e) {
            //对index的范围进行校验
            rangeCheckForAdd(index);
            //检查是否进行了多线程的操作
            checkForComodification();
            //由于多态,这里调用的是ArrayList的add方法
            parent.add(parentOffset + index, e);
            //实际上运行不到这里
            //这里由于包的原因,报错注释掉,将modCount与父集合保持一致
            this.modCount = parent.modCount;
            this.size++;
        }

这里节选了两段源码解析,我们可以看到,SubList的操作基本上都是加上偏移量之后对父集合进行操作,这里还需要注意一点,如果父集合在过程中自行进行了结构性修改,子集合就会失效,再去调用,就会抛出一个异常。

public static void main(String[] args) {
        List<String> test1 = new ArrayList<>();
        test1.add("1");
        test1.add("1");
        test1.add("1");
        test1.add("1");
        List<String> strings = test1.subList(12);
        strings.add("1");
        System.out.println(strings);
        test1.remove(1);
        System.out.println(strings.get(2));

}

运行结果:

Exception in thread "main" java.util.ConcurrentModificationException
    at com.viyoung.collection.collection.list.arraylist.ArrayList$SubList.checkForComodification(ArrayList.java:1381)
    at com.viyoung.collection.collection.list.arraylist.ArrayList$SubList.listIterator(ArrayList.java:1197)
    at java.util.AbstractList.listIterator(AbstractList.java:299)
    at com.viyoung.collection.collection.list.arraylist.ArrayList$SubList.iterator(ArrayList.java:1187)
    at java.util.AbstractCollection.toString(AbstractCollection.java:454)
    at java.lang.String.valueOf(String.java:2994)
    at java.io.PrintStream.println(PrintStream.java:821)
    at com.viyoung.collection.collection.list.arraylist.Test.main(Test.java:35)

SubList到这里也算是告一段落,下面我们来说说怎么优雅的去用集合中的几个参数为函数式接口的方法。

在ArrayList中使用Lambda

ArrayList的源码中,我们可以发现四个参数为函数式接口的方法,下面我们来通过例子一一使用这些方法

forEach

//forEach 遍历
public static void main(String[] args) {
    List<String> test1 = new ArrayList<>();
    test1.add("1");
    test1.add("2");
    test1.add("3");
    test1.add("4");

    //第一种写法
    test1.forEach(s -> System.out.print(s));
    System.out.println();
    //第二种写法
    test1.forEach(System.out::print);
}

虽然第二种写法看起来更加简单,但是有一定的局限性,而且从代码的可读性上来说,我个人认为是不如lambda表达式来的直观的(PS:也可能是因为我看习惯了)

输出结果

1234
1234

removeIf

//removeIf    符合条件的会被删除
public static void main(String[] args) {
    List<String> test1 = new ArrayList<>();
    test1.add("1");
    test1.add("2");
    test1.add("3");
    test1.add("4");
    test1.removeIf(s -> "2".equals(s));
    test1.forEach(s -> System.out.println(s));
}

这个方法的意思就是,符合条件的元素会被删除,下面我们来看一下输出结果

1
3
4

replaceAll

//replaceAll 基于原有的元素进行操作后替换原有的元素
public static void main(String[] args) {
        List<String> test1 = new ArrayList<>();
        test1.add("1");
        test1.add("2");
        test1.add("3");
        test1.add("4");
        test1.replaceAll(s -> s + " + replaceAll");
        test1.forEach(s -> System.out.println(s));
}

输出结果

1 + replaceAll
2 + replaceAll
3 + replaceAll
4 + replaceAll

sort

//sort 按照指定的Comparator排序 
public static void main(String[] args) {
     List<String> test1 = new ArrayList<>();
     test1.add("1");
     test1.add("3");
     test1.add("2");
     test1.add("4");
     test1.sort((s, str) -> Integer.valueOf(s) - Integer.valueOf(str));
     test1.forEach(s -> System.out.println(s));
 }

输出结果为

1
2
3
4

写在后面

因为根据上篇的阅读量反应来说,大家对于源码整篇的兴趣并不是很高,所以,在这篇只是节选了一些我认为比较关键的点来讲解,有兴趣的可以查看原文去GitHub(https://github.com/viyog/source_code)看完整版~

原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知~

下篇预告

下篇我们来学习Set

公众号

Java基础系列(四十四):集合之ArrayList



发表评论