ArrayList看这一篇就够了,点开即食~!

ArrayList作为平常使用最多的集合之一,今天就彻彻底底地搞懂它,话不多说,开干!博主拒绝做标题党,相信我,看完你一定有所收获~

博主的另一篇文章:深入LinkedList底层(一文搞定):

请戳

别急,整体把控继承实现关系

在这里插入图片描述

  • 实现了RandomAccess接口,可以随机访问

  • 实现了Cloneable接口,可以克隆

  • 实现了Serializable接口,可以序列化、反序列化

  • 实现了List接口,是List的实现类之一

  • 实现了Iterable接口,可以使用for-each迭代

三大接口分析

首先三大接口都是标志接口,点开源码可以发现接口中什么代码也没有,只是起到一个标志作用,所以叫标志接口

在这里三大接口详细展开篇幅过长,如果想深入了解相关的特性,博主精挑细选,看完相信会对你有所帮助:

Serializable接口:Serializable详解
Cloneable接口:Cloneable实现深拷贝和浅拷贝
RandomAccess接口:RandomAccess接口实现随机访问

看看属性

 //序列化ID
 private static final long serialVersionUID = 8683452581122892189L;
 //默认的容器初始容量
 private static final int DEFAULT_CAPACITY = 10;
 //用于空实例的共享空数组实例,容量为0的时候给数组变量赋值
 //new ArrayList(0);
 private static final Object[] EMPTY_ELEMENTDATA = {
    
    };
 //用于提供默认大小的实例的共享空数组实例
 //new ArrayList()
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
    
    };
 //底层的数组存储结构
 transient Object[] elementData;
 //当前集合中元素的个数
 private int size;

这里你肯定会有许多疑问

为什么同样是空数组,非要用两个来区分呢?

先说结论,再结合后文中构造方法,扩容机制深入理解

数组为EMPTY_ELEMENTDATA就走基于用户设置大小值进行1.5倍扩容,数组为默认空DEFAULTCAPACITY_EMPTY_ELEMENTDATA就会走基于默认值的大小10扩容进行1.5倍扩容

为什么elementData要用transient修饰?

elementData之所以用transient修饰,是因为JDK不想将整个elementData都序列化或者反序列化,而只是将size和实际存储的元素序列化或反序列化,从而节省空间和时间。

构造方法

空参构造器

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

指定容量构造器

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);
        }
    }
  • 如果指定初始容量大于0,就创建一个指定容量大小的数组
  • 如果指定初始容量等于0,就使用 EMPTY_ELEMENTDATA
  • 如果指定初始容量小于0,就抛出异常

指定集合构造器

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

这里可以发现elementData.getClass() != Object[].class这个判断条件到底有什么用呢?

ArrayList底层中elementData[]Object类型,为了防止没有返回指定类型,那么问题来了,什么情况下c.toArray()会不返回Object[]呢?

详情可以点击:什么情况下c.toArray()会不返回Object[]

插入方法

在列表尾部插入元素

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

这里调用ensureCapacityInternal()方法确保容器的容量足够容纳新的元素,然后再size++的位置插入新元素,接下来看看ensureCapacityInternal()方法
在这里插入图片描述
1.来看calculateCapacity()方法,从这里也可以看出,当使用默认构造器创建容器时即new ArrayList()时,并不是一开始就创建了容量为10的数组,而是在第一次添加元素时为容量赋值

若是DEFAULTCAPACITY_EMPTY_ELEMENTDATA则以默认为10的容量和赋值的容量的最大值作为数组的最小容量,否则以赋值的容量为最小容量

2.接下来调用ensureExplicitCapacity()方法

首先modCount是父类AbstractList的属性

用于记录对象的修改次数,可以类比为并发中乐观锁的实现机制中version字段,在特定的操作下需要对version进行检查,适用于Fail-Fast机制,后文讲到

接下来如果最小容量大于了数组的长度说明需要扩容,这里调用了grow()方法

grow()方法会在稍后详细说明扩容机制,这里就是将数组扩容,执行反之后,已经确保了容器有足够的容量能够装下新加入的元素,就返回到add()方法中,接着执行add方法

在指定位置插入元素

public void add(int index, E element) {
    
    
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }


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

这里的逻辑十分明了,首先确保index是合法范围,接着确保容量,然后调用System.arraycopy()方法,就是将index后的元素全部后移一位,然后再index位置插入元素,最后size++

这里缺点很明显

这里的插入算法最坏的情况向头元素插入节点时间复杂度达到o(n),所以对于频繁插入删除的情况,并不适合使用ArrayList,以链表尾底层结构的LinkedList更能胜任

扩容方法(核心)

 private void grow(int minCapacity) {
    
    
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //扩容1.5倍,位运算快于除法
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //假设构造方式new ArrayList(0),oldC为0,newC一直为0,
        //那么永远无法扩容,所以这里判断一下,扩容到minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
         //大型数组
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 这里的copyOf是浅复制
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  • 通常情况新容量是原来容量的1.5倍

  • 如果原容量的1.5倍比minCapacity小,那么就扩容到minCapacity

  • 特殊情况扩容到Integer.MAX_VALUE

到这里相信你也明白为什么要用两个不同的空数组了

  • 当你使用new ArrayList()默认方式构造时,使用EFAULTCAPACITY_EMPTY_ELEMENTDATA在第一次添加元素时,会扩容到10,然后基于10进行1.5倍扩容
  • 当你使用new ArrayList(0)构造容量为0的容器时,使用EMPTY_ELEMENTDATA在第一次添加元素时,基于自定义容量1.5倍扩容

其他的一些常用方法

移除指定下标元素

/**
 * 移除列表中指定下标位置的元素
 * 将所有的后续元素,向左移动
 *
 * @param 要移除的指定下标
 * @return 返回被移除的元素
 * @throws 下标越界会抛出IndexOutOfBoundsException
 */
public E remove(int index) {
    
    
	//范围检测
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
    //将index后的所有元素向前复制
            System.arraycopy(elementData, 
                    index+1, elementData, index,  numMoved);
    // 将引用置空,让GC回收
    elementData[--size] = null;

    return oldValue;
}

删除指定元素

/**
 * 移除第一个在列表中出现的指定元素
 * 如果存在,移除返回true
 * 否则,返回false
 *
 * @param o 指定元素
 */
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()方法,不进行边界检测,也不返回删除的值,效率较高
private void fastRemove(int index) {
    
    
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将引用置空,让GC回收
    elementData[--size] = null;
}

查找指定元素的位置

/**
 * 返回指定元素第一次出现的下标
 * 如果不存在该元素,返回 -1
 * 如果 o ==null 会特殊处理
 */
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 E get(int index) {
    
    
    rangeCheck(index);

    return elementData(index);
}

序列化方法

记得开头提到的,ArrayList()方法实现了Serial able接口吗?这里就是具体的序列化操作,将ArrayLisy实例的状态保存到一个流里面

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; i<size; i++) {
    
    
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
    
    
            throw new ConcurrentModificationException();
        }
    }

反序列方法,根据一个流(参数)重新生成一个ArrayList

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    
    
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt();

    if (size > 0) {
    
    
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
    
    
            a[i] = s.readObject();
        }
    }
}

序列化反序列化,开头中的elementData被transient修饰,意义就在此,并没有直接把整个数组序列化,而是序列化size和具体的存储元素,加快了效率

fail-fast(快速失败机制)

在前文讲述modCount变量时埋下了一个伏笔,这里就来解释

无论在单线程还是多线程的工作前提下,都是有可能发生fail-fast机制

以单线程为栗子,十分简单,具体做法就是在iterator遍历时,对集合内容发生了改变,多线程同理,当一个线程正在使用itr遍历,另一个线程修改了集合内容,就会抛出以下异常

  public static void main(String[] args) throws CloneNotSupportedException {
    
    
        ArrayList<Object> objects = new ArrayList<>();
        for(int i = 0;i<3;i++){
    
    
            objects.add(i);
        }
        
        Iterator<Object> iterator = objects.iterator();
        while(iterator.hasNext()){
    
    
            objects.add(3);
            System.out.println(iterator.next());
        }
    }
}

运行一下就会发现,程序抛出了ConcurrentModificationException异常

具体的实现原理?

原理很简单,就是在ArrayList源码中你可以发现,crud操作都会更新modCount的值,当迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。

每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

如何解决?

1.在单线程中,我们调用remove方法的时候可以换成迭代器的remove方法而不是集合类的remove方法。

2.使用并发包中的容器,涉及到安全失败(fail-safe)机制

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历,待修改完毕后再指向旧数组指向新数组

由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常

CopyOnWriterArrayList 代替 ArrayList, CopyOnWriterArrayList在使用上跟 ArrayList几乎一样,底层原理就用到了上述的安全失败的机制

ArrayList是线程不安全的

举一个简单的栗子你就明白了,再add()方法中我们可以看到,添加操作并不是原子操作,而是分为了两步,首先检测容量并扩容,其次设置元素

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

现在有多条线程来操作这个集合:

1.假设此时容器元素数目size=9,两个线程同时来操作集合

2.线程A调用add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。

3.线程B同时也来调用了add方法,同样它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。

4.此时线程A发现集合的容量为10,刚刚好够自己放下,所以设置元素

5.线程B也来了,也发现集合容量为10,以为够自己放下,其实位置已经被占用了,这时,线程B再添加元素时,就会抛出ArrayIndexOutOfBoundsException.异常

如何解决?

1.使用线程安全的集合:Vector(底层就是将全部的方法都加上sync,所以效率低下也是难免的)

2.Collections.synchronizedList(new ArrayList<>());将集合构造成线程安全的,其实原理一样,将ArrayList中各个方法都加上sync锁

不存在一个集合工具是查询效率又高,增删效率也高的,还线程安全的,至于为啥大家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。

最后来一个小bug看看你是否明白了ArrayList机制

public static void main(String[] args) throws CloneNotSupportedException {
    
    

        ArrayList<Object> list = new ArrayList<>(10);
        System.out.println(list.size());
        list.set(5,1);

    }

运行以下你会发现什么?如果没有看过源码你一定会疑惑?为什么我明明声明了一个容量为10的容器,为什么设置下标为5的位置会抛出IndexOutOfBoundsException的异常

原因很简单,无论是有参还是无参构造,初始化的过程并不会赋予初始容量,而是赋值了两个空数组,再第一次添加元素之后才会设置容量,所以这就是问题所在

这篇文章全面且深入的带你了解了ArrayList的方方面面,如果你觉得不错,点个小小的赞,就是对作者最大的鼓励

猜你喜欢

转载自blog.csdn.net/CPrimer0/article/details/115305497