集合之ArrayList

集合之ArrayList

这几天在看jdk源码,觉得集合是比较重要的基础框架,所以对它进行了一部分的学习和总结。也发现了不少问题。在此进行一个记录。

ArrayList集合的主要继承实现关系

1.所有集合的顶级接口类是Collection接口,该接口继承了Iterable接口。2.AbstractCollection实现了Collection接口,List接口继承Collection接口。
3.AbstaractList继承了AbstractCollection抽象类,实现了List接口。而我们的ArrayList接口则在继承AbstractList抽象类的同时,实现了List接口。
关系图如下:
根据ArrayList继承关系图

在这张图中我们会发现一件很有意思的事,我们的ArrayList集成了AbstractList的同时,还实现了List接口。如果小伙伴们观察仔细的话会发现,AbstractList已经实现了List接口,为什么ArrayList还要再实现呢?这个本人也没有琢磨清楚,有人说这其实是作者的一个错误,菜鸟也不敢问…如果有大佬知道的还请赐教哈。我们接着简单介绍一下相关类和接口,重点聊聊ArrayList的实现。

Iterable

iterable接口定义了 三个方法。
1.Iterator iterator
该方法规定所有集合必须要实现迭代。
2.foreach方法,该方法是一个默认方法,为jdk1.8新特性。

Collection

Collection接口继承了Iterable接口,定义了集合类的基础方法,如长度,是否为空,增删改查等,清空等方法。

AbstractCollection

AbstractCollection是一个抽象类,该类新增以及实现了部分方法。如:isEmpty,contains 以及集合转化为数组的等方法。

List接口

AbstractList继承了AbstractCollection

该类对AbstractCollection进行了扩展以及实现。

ArrayList

我们都知道,ArrayList底层实现是数组,接下来我们根据源码解读一下。

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

由上面代码我们可以看出ArrayList除了继承了AbstractList,还实现了List接口,RandomAccess接口,Cloneable接口以及Serializable接口。其中Serializable接口是用于需要远程传输,将对象持久化存储等。

 private static final long serialVersionUID = 8683452581122892189L;

结合serialVersionUID ,我们就可以使用,在这里我们需要知道的是,如果当前类实现了序列化接口,即使我们不提供默认的serialVersionUID 也是可以的,因为如果没有给出Serializable会有一个默认值,但是这个默认值在实际使用时可能会出现对象序列化失败的现象,所以这里提供了一个。

我们接着往下看:

private static final int DEFAULT_CAPACITY = 10;

这句话定义了ArrayList的默认长度,若初始化时未提供长度,则初始化为默认的长度。

	//实例化一个空数组,用于对比集合是否为空时用到
    private static final Object[] EMPTY_ELEMENTDATA = {};
	//实例化一个空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //真正存储数据的数组,为一个Object类型的数组。transient 关键字表示该属性不进行序列化。
    transient Object[] elementData; // non-private to simplify nested class access
    //集合的长度
    private int size;

我们接下来看看构造函数:

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

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


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

第一种构造函数需要用户初始化集合长度,如代码所见,用户在创建一个集合时需要传入一个int类型的名为initialCapacity的代表初始化长度的参数。如果用户传的初始化长度大于零,则elementData 指向一个长度为initialCapacity的数组,若为零,则指向上面定义的一个空数组。
第二个构造函数是一个无参构造函数,该构造函数直接创建一个空数组。
第三个构造函数则接收一个集合,系统将该集合转换成数组后直接赋值到elementData

由上面所知,当JVM执行了 List list = new ArrayList()时,调用了ArrayList的无参构造函数,指向了ArrayList中默认的空数组。当然也可以使用带参构造方法创建,总之,此时ArrayList底层已经维护了一个数组。

我们知道如何创建ArrayList集合了,接下来要做的就是往集合中添加数组,我们来看看添加方法:

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

由上面代码可知,elementData[size++] = e;即为往集合中添加内容的操作,但是众所周知,数组长度是不可边的,刚才我们创建了一个空数组,长度为0,现在要往size++角标赋值,这样会导致异常呀?相信大家都发现了在添加数组前还调用了ensureCapacityInternal方法,将数组此时长度加一后传到了该参数,所以我们知道了ensureCapacityInternal方法为ArrayList扩容的方法。我们进入方法一探究竟:

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

        ensureExplicitCapacity(minCapacity);
    }

从上面的代码我们可以知道,该方法是主要用于计算需要扩容的长度。当elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,表示当前对象内容为空,还没有值,此时通过对比当前size+1与默认长度的大小,确定需要扩充的长度。当大于10时,扩充size+1个长度,当小于10时,扩充为默认长度10。

计算扩充长度后又调用了ensureExplicitCapacity方法。我们继续进入

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

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

1.modCount是从父类继承过来的参数,用于标识当前对象修改的次数,通过对比该值,可以防止在遍历集合时删除集合中的元素。
2.对集合长度与扩充的长度进行对比,如果需要扩充的长度小于集合原有的长度,就不再扩充。
3.调用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);
    }

minCapacity 为需要扩充的长度,该方法执行流程如下:
1.获取集合当前大小
2.计算出新的集合大小,我们通过代码可以看出,新的长度为当前集合长度+当前集合长度>>1,使用位运算,可以提高计算效率,当前集合长度>>1相当于 当前集合长度/2,所以我们可以得出,ArrayList实际扩充为原来的1.5倍。这个答案是这样得出来的。
3.如果计算得出的扩充长度小于需要扩充的长度,则取传递的扩充数。
4.当扩充长度大于MAX_ARRAY_SIZE ,也就是大于Int.MAX_VALUE-8,则调用hugeCapacity函数继续进行计算:

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
            //三元运算符判断,当需要扩充的容量大于最大的值时,只扩充到Integer.MAX_VALUE
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

5.将当前集合的数据拷贝一份。
现在我们知道了,每一次往集合中添加数据,都需要查看空间是否足够,如果不够,会进行一次扩容,扩容后的集合长度空间大小为原来的1.5倍。

在这里可以总结出一个小技巧:
当我们需要往集合中添加大量数据时,会十分耗费性能,我们是否可以一次性让它扩容完,以提高效率呢?
答案是可以的,ArrayList类为我们提供了ensureCapacity方法。

public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

ensureCapacity接收一个需要扩充的长度参数,方法在底层调用了ensureExplicitCapacity方法,直接扩充到指定大小。那么现在我们来验证一下,哪种方法效率会更高呢?

    @Test
    public void test(){
       //1.创建一个ArrayList
        List<String> list = new ArrayList();
        String line = "test======================";
        int number = 100;
        long start = System.currentTimeMillis();
        for (int i = 0;i < number;i++){
            list.add(line);
        }
        long stop = System.currentTimeMillis();
        System.out.println("直接往List中相加耗时:"+(stop-start));

        list.clear();
        ((ArrayList<String>) list).ensureCapacity(number);
        start = System.currentTimeMillis();
        for (int i = 0;i < number;i++){
            list.add(line);
        }
        list.remove(1);
        System.out.println(list.size());
        stop = System.currentTimeMillis();

        System.out.println("先ensureCapacity再往List中相加耗时:"+(stop-start));

    }

执行效果
我们下面再将number值设置为10000000结果如下:
将添加的数据条数设置为一千万,执行结果
我们可以看到速度快了许多。

除此之外,ArrayList还有一个方法值得了解一下:

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}


该方法用于去除多余的空间,因为ArrayList扩容,每次扩大1.5倍+1。这个方法是用于去除多余的空间。在内存空间紧张的时候使用.

其他问题:
1.为什么数组最大长度是Integer.MAX_VALUE - 8?
答:在数组的对象头里有一个_length字段,记录数组长度,所以为需要-8。
2.在Collection中我们发现toArray()方法代码如下:

public Object[] toArray() {
    // Estimate size of array; be prepared to see more or fewer elements
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        if (! it.hasNext()) // fewer elements than expected
            return Arrays.copyOf(r, i);
        r[i] = it.next();
    }
    return it.hasNext() ? finishToArray(r, it) : r;
}

为什么是使用for循环,而不是iterator(如下)进行迭代呢?

public Object[] toArray() {
    // Estimate size of array; be prepared to see more or fewer elements
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
  while ( it.hasNext()) {
        r[i] = it.next();
    }
    return it.hasNext() ? finishToArray(r, it) : r;
}

答:是因为如果使用了迭代器,那么当一个线程在进行转换操作时,如果另一个线程往集合里添加元素,此时数组r的长度等于添加元素以前的长度,这样会报角标越界异常。所以这是为了线程安全做的实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值