容器源码分析之ArrayList(二)

ArrayList的类声明

首先看一下ArrayList的类声明

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, 
        java.io.Serializable

ArrayList类主要是继承AbstractList类并实现了List接口,实现Cloneable,Serializable接口,使得ArrayList具有克隆,序列化功能。
而实现RandomAccess接口使得ArrayList具有快速访问,即通过下标访问的功能。

ArrayList的属性

 private static final long serialVersionUID = 8683452581122892189L;

  transient Object[] elementData; 
  // non-private to simplify nested class access

首先我们看到serialVersionUID的声明,为了保持序列化的兼容性,
参考:Effective Java之谨慎地实现Serializable(七十四)

elementData数组用来存储ArrayList中的元素,从这个可以看出,ArrayList是底层是借组于数组来实现的。为什么声明为transient呢?
是因为避免默认的序列化带来的内存浪费,参考:Effective Java之考虑自定义的序列化模式(七十五)

有关容器类的序列化问题在后面的容器源码中就不赘述了,无非就是把默认的序列化不满足需求,自定义一个序列化。

private int size;
private static final int DEFAULT_CAPACITY = 10;

size用来记录ArrayList中存储的元素的个数。

DEFAULT_CAPACITY是ArrayList中存储的元素的默认个数,默认是10。

private static final Object[] EMPTY_ELEMENTDATA = {};
    //下面这个是共享空常量数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这两个是共享空常量数组,用于空的实例对象,区别是DEFAULTCAPACITY_EMPTY_ELEMENTDATA知道如何扩张;
(这里先知道他们的区别,至于为什么看到后面就知道了)

   protected transient int modCount = 0;

使用迭代器遍历的时候,用来检查列表中的元素是否发生结构性变化(列表元素数量发生改变)了,主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构,如果这种情况发生,抛出
ConcurrentModificationException,实现了快速失败(fail-fast)。

ArrayList的构造函数

1.无参构造方法

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

源码上介绍的功能为:构造一个初始容量为 10 的空列表。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度为什么是10呢?在下面的add(E e)函数源码的介绍中会给出答案。

2.指定容量构造方法

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

根据参数的大小作为容量来实例化底层的数组对象,其中对参数的3中情况进行了处理。当参数小于0时,抛异常。当参数等于0时,用空的常量数组对象EMPTY_ELEMENTDATA来初始化底层数组elementData。

3.容器作为参数的构造函数

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转化为数组赋给数组elementData,还对Collection转化是否转化为了Object[]进行了检查判断。如果Collection为空,则就将空的常量数组对象EMPTY_ELEMENTDATA赋给了elementData;

Add的所有方法

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

既然此函数中涉及到ensureCapacityInternal函数,那我们就看一下这个函数,源码如下:

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

        ensureExplicitCapacity(minCapacity);
    }

继续看

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
 }

从源码可以看出,如果elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则就用默认容量10来进行开辟空间。这里的源码就解释了DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组和EMPTY_ELEMENTDATA数组的区别之所在。

当我们用无参构造函数来实例化一个对象时,只需要调用一次add(E e) 就能构造的一个长度为10的数组对象。
小小程序看出端倪:

public static void main(String[] args) {
    ArrayList<Integer> list1 = new ArrayList<>(0);
    ArrayList<Integer> list2 = new ArrayList<>();
    list1.add(3);
    list2.add(3);
    System.out.println(list1.get(3));
}

debug看到的两个对象区别

![](/Users/yj/Desktop/屏幕快照 2018-01-08 下午9.53.24.png)
![](/Users/yj/Desktop/屏幕快照 2018-01-08 下午9.53.42.png)

if (minCapacity - elementData.length > 0)
            grow(minCapacity);

当添加的元素数目大于了数组elementData,就grow:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        /*
        下面两个if的作用为处理两种情况:
        1)第一种情况为:如果newCapacity扩展的过小。则应该至少扩张到所需的空间大小minCapacity
        2)第二种情况为:newCapacity扩张的过大,如果过大,则用Integer.MAX_VALUE来代替。

         */
        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 final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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

这段代码其实就是把elementData的大小扩展成原来的1.5倍,细心的人会发现,如果是elementData通过无参构造方法赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组那么它将从10开始扩张。
EMPTY_ELEMENTDATA的话它将从2开始扩张。

有存在一个小疑点:
elementData = Arrays.copyOf(elementData, newCapacity);

实际上最终调用到一个系统方法

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

这是因为C在操作内存的效率远高于Java。所以在许多优化文档中都能看见推荐使用arraycopy方法来批量处理数组。

终于把add方法有了初步的了解,其他带参数的add方法其实都一样,无非就是

  • 检查参数的有效性
  • 添加addmode,并检查是否需要扩张
  • 添加元素

    其他方法

    为什么将其他方法放在同一栏中呢?因为so ez

    首先get和set方法就不必多言了,不用看都知道怎么做吧?
    remove方法需要注意一下,

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

其实也就是将数组从index+1位置开始到末尾的元素拷贝到从index开始处)
也并不是什么高明的方法。

另一个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;
    }

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

其实区别不大,区别是fastRemove函数没有对index进行有效性检查,以及没有返回移除的旧值,为什么?想想就知道了~

需要注意的地方:

第一点:有3种遍历数组的方法,iterator迭代器,用array.get随机访问,用for-each循环,但是使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低!

第二点:ArrayList提供了2个toArray()函数:

Object[] toArray()
<T> T[] toArray(T[] contents)

调用 toArray() 函数会抛出“java.lang.ClassCastException”异常,但是调用 toArray(T[] contents) 能正常返回 T[]。

toArray() 会抛出异常是因为 toArray() 返回的是 Object[] 数组,将 Object[] 转换为其它类型(如如,将Object[]转换为的Integer[])则会抛出“java.lang.ClassCastException”异常,因为Java不支持向下转型。

解决该问题的办法是调用 T[] toArray(T[] contents) , 而不是 Object[] toArray()。

public static Integer[] vectorToArray2(ArrayList<Integer> v) {
    Integer[] newText = (Integer[])v.toArray(new Integer[0]);
    return newText;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值