ArrayList 解析

来源:互联网 发布:知识库管理系统 java 编辑:程序博客网 时间:2024/06/07 17:40

ArrayList,在我们写 Android 的时候经常配合 ListView ,RecyclerView,ViewPager 使用。那么今天就来解析一下它。

创建

构造函数

    transient Object[] elementData;    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];    public ArrayList() {        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    }

我们可以发现,默认的构造函数中就只有一句话。但也表明了自己的真实身份,就是一个控制一个 Object 数组的类
这里写图片描述
但是,为什么还要给 elementData 赋值一个静态的Object数组呢?为什么不直接 new 一个Object 数组呢?我们知道,static final 修饰的常量存在于内存中的方法区,且有且仅有一个实例,所以当我们再次创建一个 ArrayList 的对象时,elementData 指向的还是同一个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
这里写图片描述
由图可以看出,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在这里充当的是一个缓冲区的作用,避免创建多个无用的数组。666

扩容

在上一节中我们可以发现 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的长度是 0 的,那还怎么存储数据呢?而且,难道程序中所有的数据都是存储在同一个静态数组中吗?带着疑问,我们来看一下 add()方法:

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

我们可以看到第二句很明显就是插入数据并使长度自增的操作,那么第一句的方法 ensureCapacityInternal() 应该就是扩容的操作了。

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

我们首先对 elementData 进行了判断,若相等则比较 10 与 var1 的大小,接着执行下一个方法。看到这里,我们可以猜测,ArrayList 在最初应该是直接扩增到了 10 。但是,为什么要加一个判断呢,是因为 elementData 的指向会改变吗?我们继续看下一个方法:

    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);    }    private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0) // overflow            throw new OutOfMemoryError();        return (minCapacity > MAX_ARRAY_SIZE) ?            Integer.MAX_VALUE :            MAX_ARRAY_SIZE;    }

grow()方法中,前面的都是对数组大小做一些限制,不多赘述。最后一行:

this.elementData = Arrays.copyOf(this.elementData, newCapacity);

这一行,应该就是做扩容操作了,Arrays.copyOf()的参数列表中,分别是,elementData 和 newCapacity。我们回过头:

int newCapacity = oldCapacity + (oldCapacity >> 1);

结合前面的猜想,每次扩容应该都是扩大了1.5倍的。继续深入:

    public static <T> T[] copyOf(T[] original, int newLength) {        return (T[]) copyOf(original, newLength, original.getClass());    }     public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {        T[] copy = ((Object)newType == (Object)Object[].class)            ? (T[]) new Object[newLength]            : (T[]) Array.newInstance(newType.getComponentType(), newLength);        System.arraycopy(original, 0, copy, 0,                         Math.min(original.length, newLength));        return copy;    }

在这里,利用传入的数据创建了一个新的数组,然后把原本数组的数据复制到了新的数组对象中并返回。现在,我们不仅证实了前面的猜想,疑问也都解决了。我们试着继续深入

    public static native void arraycopy(Object src,  int  srcPos,                                        Object dest, int destPos,                                        int length);

看到 native ,所以应该是用到了 c 或 c++ 的方法。但是源码看不到了 = =;

System.arraycopy()

通过查阅资料,我发现其实 System.arraycopy() 内部实现是用 c 语言中的 memmove()/memcpy() 函数来实现的。它们是一个内存操作函数,通过直接移动指针来实现数组的复制。更多内容,我也还不是很清楚,希望高手指点。

仰视源码,实现memmove
c语言模拟实现memmove

扩容总结

  • ArrayList 对象创建后,内部数组的初始长度未0
  • 首次添加数据时,会扩容到10
  • 每次扩容,都会扩大原来的1.5倍
  • 每次扩容,实际都是新创建一个数组然后再把数据复制到新数组中然后返回
  • size 表示的是逻辑容量而不是内部数组的实际容量

这里写图片描述

常用操作解析

get

    public E get(int index) {        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        return (E) elementData[index];    }

我们清楚,数组只要知道下标就能直接获取数据。ArrayList 也是如此,先检查是否越界,然后直接通过下标获取数据。

set

    public E set(int index, E element) {        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        E oldValue = (E) elementData[index];        elementData[index] = element;        return oldValue;    }

同样修改也一样,时间复杂度与获取一样 同为 O(1)

remove

    public E remove(int index) {        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        modCount++;        E oldValue = (E) 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 把后面的参数移动到前面,最后在制空最后一位。

add && addAll

    public void add(int index, E element) {        if (index > size || index < 0)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        ensureCapacityInternal(size + 1);  // Increments modCount!!        System.arraycopy(elementData, index, elementData, index + 1,                         size - index);        elementData[index] = element;        size++;    }    public boolean addAll(Collection<? extends E> var1) {        Object[] var2 = var1.toArray();        int var3 = var2.length;        this.ensureCapacityInternal(this.size + var3);        System.arraycopy(var2, 0, this.elementData, this.size, var3);        this.size += var3;        return var3 != 0;    }

两个方法都很相似,扩容后,利用 System.arraycopy 进行复制。

clear

  public void clear() {        modCount++;        // clear to let GC do its work        for (int i = 0; i < size; i++)            elementData[i] = null;        size = 0;    }

简单粗暴,直接全部置空。

SubList

在查看源码的时候还发现了 ArrayList 中,还存在一个内部类 SubList,他与 ArrayList 一样都是继承 Abstractlist 。同时也包含get , set , add ,remove 等方法。

    public List<E> subList(int fromIndex, int toIndex) {        subListRangeCheck(fromIndex, toIndex, size);        return new SubList(this, 0, fromIndex, toIndex);    }    private class SubList extends AbstractList<E> implements RandomAccess {       private final AbstractList<E> parent;        private final int parentOffset;        private final int offset;        int size;       SubList(AbstractList<E> parent,                int offset, int fromIndex, int toIndex) {            this.parent = parent;            this.parentOffset = fromIndex;            this.offset = offset + fromIndex;            this.size = toIndex - fromIndex;            this.modCount = ArrayList.this.modCount;        }        public void add(int index, E e) {            if (index < 0 || index > this.size)                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));            if (ArrayList.this.modCount != this.modCount)                throw new ConcurrentModificationException();            parent.add(parentOffset + index, e);            this.modCount = parent.modCount;            this.size++;        }        .        .        .        .        .        .    }

我们发现,parent 即是原始的 list 的引用,在其他对数组操作的方法中,最后也都是调用了 parent 中的方法。即,对 subList 的操作其实就是对 ArrayList 的操作,会改变 ArrayList 的内容

SubList 的妙用

list.subList(100,200).clear();

直接删除一段数据(100-199),而不用 for 循环。

SubList 的缺陷

创建 subList 后,如果修改了 ArrayList ,再次调用 SubList 的时候,会发生异常

        List<String> stringList = new ArrayList<>();        stringList.add("1");        stringList.add("2");        stringList.add("3");        stringList.add("4");        stringList.add("5");        List<String> subList = stringList.subList(0,stringList.size());        stringList.add("6");        System.out.print("stringList = "+stringList.size());        System.out.print("subList = "+subList.size());

异常

Exception in thread "main" java.util.ConcurrentModificationExceptionstringList = 6  at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)    at java.util.ArrayList$SubList.size(ArrayList.java:1040)    at com.jinjunhuang.arraylistdemo.Demo.main(Demo.java:32)    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)    at java.lang.reflect.Method.invoke(Method.java:498)    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

至于原因,我们来看一下 size()方法

        public int size() {            if (ArrayList.this.modCount != this.modCount)                throw new ConcurrentModificationException();            return this.size;        }

当修改了 ArrayList 后,ArrayList.modeCount 会改变,而 sublist 的 modCount 只在构造函数中获取 ArrayList.modeCount 的值,而不会动态改变,所以,在执行 sublist 的方法时,两者不相等,就会抛出错误。这种机制也叫 fail-fast。

fail-fast 机制

是Java集合的一种检测错误机制。当多个线程对集合进行机构上的改变的操作时,有可能会产生 fail-fast 机制。这种机制,不会保证一定会出现错误,但会进最大努力去抛出 ConcurrentModificationException 异常。迭代器中也存在这种机制。获取更多信息推荐阅读

序列化

在谈到构造函数的时候,我们会发现,ArrayList 中的 Object 数组中用了 transient 来修饰,表示不用序列化,但是我们的数据都是放在数组中的,数据不序列化,我们持久化了有什么用呢?这是因为,elementDa 不一定是满的,没有用上的空间我们并不想再耗时间去序列化。但有存储数据的空间我们还是要序列化的,因此,Java 中重写了 writeObject 方法,以实现局部序列化。

 private void writeObject(java.io.ObjectOutputStream s)        throws java.io.IOException{        // Write out element count, and any hidden stuff        int expectedModCount = modCount;        s.defaultWriteObject();        // Write out size as capacity for behavioural compatibility with clone()        s.writeInt(size);        // Write out all elements in the proper order.        for (int i=0; i<size; i++) {            s.writeObject(elementData[i]);        }        if (modCount != expectedModCount) {            throw new ConcurrentModificationException();        }    }

总结

  1. ArrayList 其实
  2. 就是维护一个一维数组的对象
  3. 每次创建 ArrayList 对象,内部的数组引用都会指向一个长度为0的数组常量
  4. 扩容就是新建一个更大的数组对象,然后利用 System.arraycopy() 复制旧数组中的数据
  5. 首次扩容会扩张到 10
  6. 每次扩容都会扩张到原来的 1.5倍
  7. ArrayList 和数组一样,获取数据很快速,但是插入删除会很慢
  8. 插入删除操作也是利用 System.arraycopy()来实现数据的移动的
  9. System.arraycopy()是采用 c 语言的 memcpy/memmove 实现的
  10. ArrayList 中 SubList 是 ArrayList 中的一个视图,修改 SubList ,ArrayList 中的数据也会改变
  11. 若创建了 SubList 后修改了 ArrayList ,再次调用前面创建的 SubList 的实例,会触发 fail-fast机制的发生
  12. ArrayList 中重写了 writeObject 方法以优化数组的序列化过程
原创粉丝点击