来聊聊动态数组ArrayList

1.引言

  • ArrayList在Java集合中的使用非常广泛,不论是用来存放数据库表查询的结果还是用于Excel导入解析都需要使用到。想要很好的使用ArrayList,去了解其原理和底层实现不言而喻

2.ArrayList与数组的关系

  • ArrayList其实就是数组列表,主要用来装载数据。它的主要底层实现是数组Object[] elementData。
  • 与数组不同的是,我们数组的长度一旦初始化完后就不能再进行变动,而ArrayList虽然底层用的也是数组,但是它是却可以进行动态的改变数组长度,当我们向ArrayList中add元素时,如果长度不够时就会进行扩容。

3.ArrayList的扩容

  • 在我们向ArrayList中新增元素时,会进行一个长度校验(ensureCapacityInternal),如果长度不够时就需要进行扩容。请看下面示例代码
/**
     * 将指定的元素追加到此列表的末尾。
     *
     * @param e element to be appended to this list
     * @return true (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
  • 在jdk1.8里进行扩容的时候,扩容的大小为原来的1.5倍,再把原先在数组的元素copy到扩容后的数组中,最后将引用指向扩容后的数组即完成了ArrayList的扩容。请看下面示例代码
/**
     * 增加容量以确保它至少可以容纳
     * 最小容量参数指定的元素数。
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //这里采用了位运算,右移移位,相当于除以2的操作,位运算的操作效率高于直接除2的运算
        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);
    }

4.我们在创建一个ArrayList时不指定它的长度会有问题吗?

  • 要回答这个问题,我们先来看一段代码

来聊聊动态数组ArrayList_第1张图片
可以看到其实我们并没有在创建时指定其长度大小,那它是怎么做到的呢?哈哈,别急,我们现在就去探索其中的原由

  • 我们先去看下它的一个无参构造方法
/**
     * 构造一个初始容量为10的空列表
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

我们再点DEFAULTCAPACITY_EMPTY_ELEMENTDATA进去看看

/**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

也就是说当我们没有去指定ArrayList的初始长度时,数组的长度默认是0,当我们向ArrayList中add元素时,才分配一个默认长度为10的初试容量

  • 我们再来看看它的一个有参构造方法
/**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }

看到这里想必小伙伴们应该可以解除自己心中的一些迷惑了吧。

5.ArrayList线程安全吗?

  • 显然ArrayList是线程不安全的,为什么呢?下面举个简单的例子。请看下面示例代码
public static void main(String[] args) throws InterruptedException {
        List<Object> arrayList = new ArrayList<>();
        for (int i = 0; i < 20000; i++) {
            new Thread(()->{arrayList.add(Thread.currentThread().getName());}).start();
        }
        Thread.sleep(3000);
        System.out.println(arrayList.size());
    }

经过多次运行,其输出结果分别为18842、19997、19996等,可见输出的结果不仅不一样,而且并不是我们想要得到的结果(20000)

  • 那我们怎么去保证在使用ArrayList时保证它的一个线程安全呢?

1.加同步锁synchronized

public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 20000; i++) {
                new Thread(()->{
                    synchronized (list) {
                        list.add(Thread.currentThread().getName());
                    }
                }).start();
            }
        Thread.sleep(3000);//由于计算机运行速度过快,这里不进行阻塞的话在list还没有add完元素之前就已经执行主线程的输出语句了(这样的结果是一点会小于20000的)
        System.out.println(list.size());
    }

结论:这样可以达到我们ArrayList线程安全的效果,但是加synchronized同步锁的话会降低系统的并发性,效率低,不推荐使用。

2.使用JUC安全类型的集合CopyOnWriteArrayList

public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 20000; i++) {
            new Thread(()->{
                copyOnWriteArrayList.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(copyOnWriteArrayList.size());
    }

结论:使用JUC中并发包的CopyOnWriteArrayList类可以达到我们所期望的结果,推荐使用。

3.ArrayList是继承自AbstractList类,而AbstractList实现了List接口,List接口又继承了Collection类,我们知道Collections是我们Collection集合类的一个帮助类,提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。因此,我们可以使用它为我们提供的静态方法来实现线程安全操作

public static void main(String[] args) throws InterruptedException {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 20000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }

结论:使用Collections工具类所提供的静态方法可以保证对一些集合操作线程安全性,推荐使用。

6.ArrayList适合做队列吗?

  • 我们知道,队列一般是先进先出(FIFO)的,如果用ArrayList做队列的话,就需要在数组尾部来添加数据,在数组头部删除数据,但是无论怎么样总会有一些操作会涉及到数据的搬迁,这个是比较消耗性能的,所以并不推荐用ArrayList做队列。

总结

ArrayList就是一个动态数组,它提供了动态的增加和减少元素,这样的灵活性高于数组。再则ArrayList底层实现是数组,因此其查询的效率很高,但其增加和删除都有可能会导致其他元素的移动,这样的效率很低。因此,在进行频繁的查询时强烈建议使用ArrayList进行存储。

你可能感兴趣的:(来聊聊动态数组ArrayList)