《Java源码解析》集合框架List之ArrayList

Java的集合框架的底层实现在面试中用的比较多,接下来我就用一些时间来分析一下Java的集合框架中的List(ArrayList,LinkedList),Set(Set的各种实现类),Map(Hashtable、HashMap、ConcurrentHashMap)

对于集合框架我们的关注点一般在一下几点:
1. 集合底层实现的数据结构是什么?
2. 集合中元素是否允许为空?
3. 是否允许重复的数据?
4. 是否有序(这里的有序是指读取数据和存放数据的顺序是否一致)
5. 是否线程安全。

下面先从List的实现类开始解读源码

ArrayList

1.首先看看 ArrayList 的继承体系的结构

《Java源码解析》集合框架List之ArrayList_第1张图片

ArrayList主要是继承自AbstractList抽象类并实现了List接口、实现了Cloneable和Serializable接口使得ArrayList具有克隆和序列化的功能、实现了RandomAccess接口以实现随机访问的功能。

2.重要属性

ArrayList中两个最重要的属性如下:

/**存放List中元素的数组*/
private transient Object[] elementData;
/**ArrayList中元素的数量*/
private int size;

elementData数组用来存放List中的元素,可知ArrayList的底层是数组实现的,此外这里elementData是用transient修饰的,表示该变量不可序列化;size表示List中已存放的元素的容量。

此外还有三个重要的属性,其各自功能见源码注释:

//ArrayList默认的初始容量
private static final int DEFAULT_CAPACITY = 10;
//这是一个共享的空的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//元素数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

3. 五个关注点在ArrayList上面的答案

ArrayList

关注点 结论
集合底层实现的数据结构 数组
集合中元素是否允许为空 允许
是否允许重复的数据 允许
是否有序 有序
是否线程安全。 非线程安全

4. ArrayList的构造函数

ArrayList有三个构造函数:

1. 无参构造函数

public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

这个无参构造函数直接把EMPTY_ELEMENTDATA传递给elementData属性。根据注释说明:构造一个初始容量为10的空列表。至于EMPTY_ELEMENTDATA是什么时候初始化的,且看后面的分析。

2. 指定初始容量的构造函数

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    //new一个指定容量的数组传递给elementData
    this.elementData = new Object[initialCapacity];
}

3. 使用传递的集合初始化List的构造函数

public ArrayList(Collection c) {
    //将集合c转换成数组元素,然后传递给elementData
    elementData = c.toArray();
    //获取数组的大小,然后赋值给size
    size = elementData.length;
    //这里会进行判断elementData是否正确的被赋值
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

这里有一点值得注意的是最后一句:通过调用Arrays.copyOf()函数复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。我们进入这个函数发现这个函数主要是调用了System.arraycopy(),继续跟踪下去,发现这个函数是一个native方法。

5.ArrayList关键函数之add(E e)

源代码如下:

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

JDK对这个函数的解释是:在List的后面添加一个元素。
在源码里面,首先调用如下函数
ensureCapacityInternal(size + 1);
这个函数主要是确保size+1不会导致数组越界,下面我们先看看这个函数里面的实现:

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

这是一个内部调用的private函数,这个函数里面又主要调用如下函数
ensureExplicitCapacity(minCapacity);
这个函数传入的参数是list的最小容量。而且这个函数也是private的,看看这个函数的源码:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    //这里判断当前List所需的容量大于elementData本身的容量,那么就调用grow()函数对数组扩容。
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这里有个变量modCount是定义父类AbstractList中的
protected transient int modCount = 0;
这个变量的主要作用是修饰结构化的修改此List的次数。这里有个重要的函数private void grow(int minCapacity)需要分析,源代码如下:

private void grow(int minCapacity) {
    //获取elementData也就是List中本来的容量
    int oldCapacity = elementData.length;
    //oldCapacity的大约1.5倍赋值给新的newCapacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //根据新的数组容量,调用接口扩容elementData
    elementData = Arrays.copyOf(elementData, newCapacity);
}

根据上述的源码注释,我们可以知道这个函数主要是将原来的list的大小扩大到现在的大约1.5倍。
至此,add()函数中的第一行ensureCapacityInternal(size + 1);已经分析完毕,主要也就是在add元素的时候确保当前的list的容量可用。在add()函数中剩下的也就是在数组末尾增加一个元素了,注意这里size自加了一次,也就是size表示的是list中元素的个数。

elementData[size++] = e;
return true;

自此,add()函数的源码也就分析完毕。

6.ArrayList关键函数之add(int index, E element)

这个函数与add(E e)的区别就是add(int index, E element)是在index位置插入一个元素。这个地方由于加了index,所以很可能涉及到数组内大量的元素移动。

public void add(int index, E element) {
    rangeCheckForAdd(index);
    //和add(E e)函数一样,确保add操作对于list容量可用
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //通过调用System的系统调用数组copy操作
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //数组的添加
    elementData[index] = element;
    size++;
}

上面首先通过rangeCheckForAdd(index); 检查index是否对于数组合法。下面是rangeCheckForAdd()函数的源码:

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

7.ArrayList关键函数之addAll(Collection

public boolean addAll(Collection c) {
    //将集合中元素转换成数组
    Object[] a = c.toArray();
    int numNew = a.length;
    //确保添加集合到list中后,list容量仍可用,如果不可用就扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;//集合中元素添加到list中之后,size相应的添加。
    return numNew != 0;
}

8. get(int index)和set(int index, E element) 操作

首先贴出这两个函数的源码:

public E get(int index) {
    //确保index在List中可用
    rangeCheck(index);
    //返回数组中index下标的元素
    return elementData(index);
}

public E set(int index, E element) {
    //确保index在List中可用
    rangeCheck(index);
    //将新的element设置在index位置,并返回旧的element
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

9. clear()函数

public void clear() {
    modCount++;

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

看上面的源码可知,就是直接把elementData数组全部赋值为null了,然后将size清零。

10. remove(Object o) 删除list中的某个对象

public boolean remove(Object o) {
    //如果o为null,就删除list中的所有的null
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {//删除list中的所有o
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

11. subList(int fromIndex, int toIndex)获取子List

这个函数是根据索引fromIndex和toIndex来获取父List的一个视图,注意,这里并不是创建了一个新的List,而只是原来List的部分数据的一个视图,subList的任何操作都会影响到原来的父List。
源码如下:

public List subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

static void subListRangeCheck(int fromIndex, int toIndex, int size) {
    if (fromIndex < 0)
        throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
    if (toIndex > size)
        throw new IndexOutOfBoundsException("toIndex = " + toIndex);
    if (fromIndex > toIndex)
        throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                           ") > toIndex(" + toIndex + ")");
}

SubList是一个ArrayList内部的private类,构造函数如下:

SubList(AbstractList 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;
}

这个SubList的任何操作都是基于ArrayList这个类的elementData这个数组来实现的,所以只是父类的一个子视图。所以需要额外注意这一点

12. elementData为啥使用transient修饰?

我们知道elementData使用transient修饰,那么这个List不是不能被序列化了吗?但是我们的ArrayList又实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了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; iif (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:
1、加快了序列化的速度
2、减小了序列化之后的文件大小
不失为一种聪明的做法,如果以后开发过程中有遇到这种情况,也是值得学习、借鉴的一种思路。

自此,ArrayList的源码分析完毕。

你可能感兴趣的:(Java集合框架)