数据结构特性解析 (二) ArrayList

前言

ArrayList可能是Java中使用次数最多的数据结构了,因此了解其特性能比较重要

描述

ArrayList是一个数组队列,相当于动态数组.与Java中的数组相比,它的容量能动态增长.

并且ArrayList还有一些添加,遍历和移除的操作

特点

1.ArrayList内部实现是利用Java的数组

这是内部存储数据的Object数组

add方法的实现方式,根据源码可以看到,先判断了一下容量,然后在当前已存放数据的size的位置设置为传过来的数据,并且size+1

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 检查容量(扩充机制)下面再说
        elementData[size++] = e;
        return true;
    }

2.ArrayList的空参构造默认长度不是10,而是0

ArrayList一共有三个构造方法

//1设置初始化容量
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);
        }
    }

//2默认的无参构造
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//3传入一个Collection对象
    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;
        }
    }

首先第三个构造不多说了,是把别的集合转换为ArrayList内部的数组,然后如果有长度在设置一下size,跟本条关系不大

然后第一个构造,是指定默认的数组长度,小于0抛异常,等于零则设置内部数组为静态的length为0的空数组

重点是第二个构造,直接赋值成如下的数组,可以看到是一个空容量的数组

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

而在第一次add的时候会进行扩容,代码如下所示

    private static final int DEFAULT_CAPACITY = 10;//默认第一次扩容的容量
    private void ensureCapacityInternal(int minCapacity) {//这里会传入size+1,也就是1
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果是默认的空数组
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);//检查容量并扩容
    }

3.ArrayList的扩容方式,是先创建一个更大的数组,然后把旧数组内容拷贝过去,在把内部数组设置为新的大数组

我们看其add方法实现

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 检查自身的可用容量
        elementData[size++] = e;
        return true;
    }

如果ensureCapacityInternal检查到容量不够,最终会调用如下代码

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;//记录当前的长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);// >> 符号(右位移),相当于除2,所以扩容是1+0.5=1.5倍
        if (newCapacity - minCapacity < 0)//如果扩容后,容量还小于指定容量,就直接设置为指定容量,比如使用空参构造new,size为0,乘1.5还是0,就会在第一次扩容的时候设置为10
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//长度过长检查,如果长度溢出了int的值,就会变成负数导致抛异常(但说实话,它这个判断还是有可能会int溢出)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);//进行数组拷贝工作,容量为新扩容的大小
    }

所以,如果事先知道元素的大小或大概的大小,可以在构造中传入,就能减少扩容次数,能有效节省时间和减少内存消耗

4.ArrayList的indexOf实现是遍历

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

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

可以看到,indexOf是从前向后遍历,而lastIndexOf是从后向前遍历,所以如果你大概知道想查找的元素在前或者后,使用对应的方法,可以更节省时间

5.因为内部使用的是数组,所以其特性也和数组特性类似

ArrayList根据索引的方式去查找数据,会比较快,不用进行循环或多次寻址

而遍历查询则需要遍历索引来循环从数组中取出

6.ArrayList是线程不安全的

内部没有使用锁或同步机制,如果想要使用线程安全的ArrayList(类似特性),可以使用Collections.synchronizedList

7.ArrayList不支持forEach时修改数据

forEach其实是java的语法糖,内部使用的是迭代器Iterator,通过hasNext()和next()方法,来遍历数据

    private class Itr implements Iterator<E> {ArrayList的内部类迭代器
        protected int limit = ArrayList.this.size;

        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor < limit;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            int i = cursor;
            if (i >= limit)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

    }

而内部有一个modCount变量,标识的是自身修改的次数,会在扩容时,remove时和clear时进行+1操作,而在迭代器内next()方法取数据的时候,会判断modCount变量,看看到底修没修改过原数据,如果修改过就会抛异常(这是为了数据的准确性)

如果想要避免上述问题,可以使用CopyOnWriteArrayList(线程安全,支持forEach时修改数据)

8.ArrayList支持随机读取,并且随机添加和删除效率不高

因为ArrayList的实现基于数组,所以根据索引来获取是很快的

而添加到的时候,只有在添加在尾部时效率较好(且不触发扩容)

而随机插入,则会引起插入索引后面的位置都会被复制并向后位移,所以效率不高

删除也是,只有删除尾部时效率较好

随机删除会引起删除的索引后面的位置都会被复制并向前位移,所以效率也不高

下一篇: 数据结构特性解析 (三) 链表

猜你喜欢

转载自blog.csdn.net/qq_33505109/article/details/91507325
今日推荐