• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

Java8 ArrayList源码分析

java 来源:却把清梅嗅 2次浏览

学习动机

ArrayList是我日常Android开发中使用频率最高的数据容器类,非常简单的接口,底层为数组结构,有序性可以保证我们按照索引获取我们想要的数据。

不论是想进阶学习Java,或者想加深对数据结构的理解,甚至想要在面试中达到游刃有余逼格满分,学习分析Java容器类的源码都是不错的选择。

Java Collection库中有三类:List,Queue,Set;而List接口,有三个子实现类:ArrayList,Vector,LinkedList。

本文进行ArrayList源码的分析。

文章内容偏长,建议认真阅读。

Java8在线源码地址参考(请自备梯子):点我查看

构造&成员属性

   //默认容量:默认容纳10个元素
   private static final int DEFAULT_CAPACITY = 10;

   //默认值:通过空的构造参数生成的ArrayList实例,默认为空集合
   private static final Object[] EMPTY_ELEMENTDATA = {};

   //ArrayList对象实际上就是一个容器数组
   transient Object[] elementData;

   //数组的size
   private int size;

ArrayList默认有三个构造方法:

//空构造,默认实例化一个空的容器数组
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

//实例化一个对应容量的容器数组
public ArrayList(int initialCapacity) {
    super();
    //如果传入的值小于0,报错:非法的容量
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    //实例化数组 
    this.elementData = new Object[initialCapacity];
}

//通过传入一个Collection实例化ArrayList对象
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

我们日常开发中更多的是通过new Arraylist<>()空构造的方法实例化ArrayList容器,实际上我们可以看到,空构造只是初始化了一个空的数组。

那么ArrayList是如何动态改变数组容量大小的呢?

简单的Add()方法

我们实例化了一个空的对象数组,接下来我们来看一下最简单的add()方法:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 容量检测&容量动态增加
    elementData[size++] = e;  //将新的元素添加到数组的末尾
    return true;
}

我们点击进入看一下 ensureCapacityInternal()方法:

//ensureCapacityInternal -> 保证内部容量
private void ensureCapacityInternal(int minCapacity) {
    //如果数组容器为空,初始化容器的容量
    if (elementData == EMPTY_ELEMENTDATA) {
        //DEFAULT_CAPACITY成员属性,默认容量为10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

我们继续看ensureExplicitCapacity方法:

//modCount为操作次数,每次数据结构改变,都会调用该属性modCount++
protected transient int modCount = 0;

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

    // 当容器数组的长度已经满足不了需求的最小容量时,增加容量
    if (minCapacity - elementData.length > 0)
        //增加数组容量
        grow(minCapacity);
}

终于可以看到如何实现动态容量增长了:

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

private void grow(int minCapacity) {
     int oldCapacity = elementData.length;
     //位运算符,容量在当前的基础上+50%
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
     //当新的容量已经超出MAX_ARRAY_SIZE时,继续扩容
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
     //扩容完毕,将扩容后的数组赋值给elementData
     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;
 }

可以看到,仅仅一个简单的add(E e)方法,背后竟然隐藏着这么多逻辑代码的实现,其实更细心的小伙伴已经对这行代码产生了疑惑:

//扩容完毕,将扩容后的数组赋值给elementData elementData = Arrays.copyOf(elementData, newCapacity);

我们本着追究到底的精神,点进去追查到底:

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

显然这还不够,因为最重要的数组赋值的逻辑还在这行代码中:

System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));

我们继续深入,结果并不尽人意:

/** * @param src the source array. * @param srcPos starting position in the source array. * @param dest the destination array. * @param destPos starting position in the destination data. * @param length the number of array elements to be copied. */
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

显然该方法是用了native关键字,调用的为C++编写的底层函数,可见其为JDK中的底层函数。

我们从注释中可以得到其解释:

src – 源数组。
srcPos – 源数组中的起始位置。
dest – 目标数组。
destPos – 目标数据中的起始位置。
length – 要复制的数组元素的数量。

举个例子

比如 :我们有一个数组数据

  byte[]  srcBytes = new byte[]{2,4,0,0,0,0,0,10,15,50};  // 源数组
  byte[] destBytes = new byte[5]; // 目标数组

我们使用System.arraycopy进行转换(copy)

  System.arrayCopy(srcBytes,0,destBytes ,0,5)

上面这段代码就是 : 创建一个一维空数组,数组的总长度为 12位,然后将srcBytes源数组中 从0位 到 第5位之间的数值 copy 到 destBytes目标数组中,在目标数组的第0位开始放置.

那么这行代码的运行效果应该是 2,4,0,0,0

参考文章:java System.arrayCopy使用说明 @Albin

回顾

好的,如果您伴随着思考一并走到了这里,相信您应该对add()方法有所领会,现在我们再回到这里:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 容量检测&容量动态增加
    elementData[size++] = e;  //将新的元素添加到数组的末尾
    return true;
}

当我们第一次添加元素后,数组容器容量变为10,同时,数组容器的第一个元素添加到elementData[0],size += 1。

之后,如果我们继续添加第二个元素,在容量检测到这一步:

//modCount为操作次数,每次数据结构改变,都会调用该属性modCount++
protected transient int modCount = 0;

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; //修改次数+1

    // 此时最小容量需求应该为2,而数组长度为10,因此不需要动态添加容量
    if (minCapacity - elementData.length > 0)
        //直到添加第11个元素时,才会执行该方法,然后容量10->15(+50%)
        grow(minCapacity);
}

add/remove/get/set方法

重载的add()

我们继续看第二个add方法

public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    ensureCapacityInternal(size + 1);  // 容量检测&动态增加
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

我们已经在上面学习理解了System.arraycopy()方法的作用,容量动态处理后,将elementData[]数组在index位置一分为2,空出一个位置,然后将新的元素赋值,最后size++。

举例

array = [0,1,2,3,4] -> 在index=2的位置添加-1

System.arraycopy(elementData, index, elementData, index + 1,size – index);

-> array = [0,1,2,2,3,4] ->elementData[index] = -1; ->array = [0,1,-1,2,3,4]

remove()

public E remove(int index) {
     if (index >= size)//越界检测
         throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    //操作数量+1
     modCount++;
     E oldValue = (E) elementData[index];//获取删除的元素

    //删除对应index的元素后,有多少个元素要移动
     int numMoved = size - index - 1;
     if (numMoved > 0)
         //移动对应的元素
         System.arraycopy(elementData, index+1, elementData, index,
                          numMoved);
     //删除最后一个(--size)元素 
     elementData[--size] = null; // clear to let GC do its work

     return oldValue;//将删除的元素返回
 }

举例理解System.arraycopy

array = [0,1,2,3,4] -> 删除index =2

System.arraycopy(elementData, index+1, elementData, index,numMoved);

-> array = [0,1,3,4,4] -> elementData[–5] = null; ->array = [0,1,3,4,null]

我们再看remove的另外一个重载方法:

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

这里我们可以看到,我们只删除index从小到大的第一个o对象,即如果一个集合中有两个该对象,只删除index值低的那个对象。
我们看一下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
}

和remove(int index)差不多,换汤不换药。

get(int index)

set(int index, E element)

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];
}

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

这两个方法放在一起是因为真的没什么讲的……一看就懂,只是把数组对应index的元素取出并返回,请注意,因为这不设计数据结构的变化,因此modCount并没有改变。

其他方法

contains(Object o)

//数组中是否有某个对象
 public boolean contains(Object o) {
        return indexOf(o) >= 0;
 }

//正序查找是否包含某个对象,若有,将距离index=0最近元素的index值返回
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

//倒序查找是否包含某个对象,若有,将距离index=size-1最近元素的index值返回
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

toArray()

//将ArrayList转换为对象数组返回
public Object[] toArray() {
     return Arrays.copyOf(elementData, size);
}
//将ArrayList转换进指定的数组中返回
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

clear()

//请注意,该方法仅仅将元素中所有元素全部置为null,并且size =0 ,但该数组的length不变。
 public void clear() {
    modCount++;

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

    size = 0;
}

trimToSize()

//当数组的length>size时,将数组的大小变为size,从而达到节省内存的作用
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = Arrays.copyOf(elementData, size);
    }
}

ArrayList分析总结

1.对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配。

2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动。

3.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间。

ArrayList 是线性表(数组)
get() 直接读取第几个下标,时间复杂度 O(1)
add(E) 添加元素,直接在后面添加,时间复杂度O(1)
add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,时间复杂度O(n)
remove()删除元素,后面的元素需要逐个移动,时间复杂度O(n)

可以这样说:当操作是大量查询和获取,或需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能。

在Android开发中,我们经常使用ArrayList作为容器存储数据,并进行数据的读和展示,ArrayList可堪当重任。


版权声明:本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。
喜欢 (0)