深入理解ArrayList

本文详细分析了ArrayList的源码,包括常用方法如add、remove、set和get的实现原理,着重讲解了动态扩容机制。此外,讨论了ArrayList与LinkedList的区别,以及在何时选择使用ArrayList的情况。并探讨了ArrayList在并发环境下的线程安全问题和CopyOnWriteArrayList的使用。
摘要由CSDN通过智能技术生成

ArrayList可以说是Java开发中最常用的集合容器了,今天就来分析一下ArrayList的源码和注意点,可以更加深入的理解ArrayList实现原理。


概述

arrayList的继承类图

本文基于JDK1.8

在这里插入图片描述


常用方法

arrayList中的方法比较多,下面给出比较常用的一些方法

boolean add(E e)

将指定的元素添加到此列表的尾部。

void add(int index, E element)

将指定的元素插入此列表中的指定位置。

boolean addAll(Collection c)

按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

boolean addAll(int index, Collection c)

从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。

void clear()

移除此列表中的所有元素。

Object clone()

返回此 ArrayList 实例的浅表副本。

boolean contains(Object o)

如果此列表中包含指定的元素,则返回 true。

void ensureCapacity(int minCapacity)

如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。

E get(int index)

返回此列表中指定位置上的元素。

int indexOf(Object o)

返回此列表中首次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。

boolean isEmpty()

如果此列表中没有元素,则返回 true

int lastIndexOf(Object o)

返回此列表中最后一次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。

E remove(int index)

移除此列表中指定位置上的元素。

boolean remove(Object o)

移除此列表中首次出现的指定元素(如果存在)。

protected void removeRange(int fromIndex, int toIndex)

移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。

E set(int index, E element)

用指定的元素替代此列表中指定位置上的元素。

int size()

返回此列表中的元素数。

Object[] toArray()

按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组。

T[] toArray(T[] a)

按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。

void trimToSize()

将此 ArrayList 实例的容量调整为列表的当前大小。


源码分析

成员变量

// 初始容量:10
  private static final int DEFAULT_CAPACITY = 10;
// 空数组,没有元素数据
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组,默认容量为空,没有元素数据
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数组,用来存储ArrayList的元素
 transient Object[] elementData;
// size为ArrayList的大小,在elementData不为空数组的情况下,size是小于elementData.length的
 private int size;

根据elementData也能看出来,ArrayList的内部是通过数组来实现的,ArrayList对元素的增删改查实际上都是对数组的操作。

ArrayList的构造函数

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

构造ArrayList时,可以指定容器的初始容量initialCapacity,构造一个给定初始大小的数组作为数据集;使用无参构造时,则默认容量为空的数组作为初始数据集;也可以使用其他任意的集合Collection作为构造参数,可以看到,源码中就是直接将集合c转换数组来作为数据集(如果数据集是非Object数组,比如多维数组,则将元素拷贝到数据集数组中)。ArrayList的构造实际上就是对其内部数组的初始化。

add方法

public boolean add(E e) {
    // 确保当前数据集数组能够放得下新加入的元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将列表大小size自增1,并在数据集数组中放入元素e
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    // 检查新加入的位置index是否越界
    rangeCheckForAdd(index);
    // 确保当前数据集数组能够放得下新加入的元素,如果需要扩容的话就扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将index位置及后面的元素都向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 将index位置的元素设置为新建如的element
    elementData[index] = element;
    size++;
}

在添加元素时,如果不指定加入的位置,会添加到内部数组中已有元素的最后一位,也就是添加到了ArrayList的末尾。如果指定了添加位置index,判断index是否越界,是否需要扩容,最后移动index位置后的元素,并将index位置设置为新添加的元素。

需要注意的是,添加的元素并没有判空,所以ArrayList中的元素是可以为null的。

在add方法中,都调用了ensureCapacityInternal(int minCapacity)这个方法来确保数据集数组能够放得下新的元素:

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

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    // 如果添加新元素需要的最小容量大于数组的长度,就需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

看下扩容的方法grow(int minCapacity)

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 扩展至新的容量newCapacity为旧的容量的1.5倍
    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);
}

数组扩容的过程,实际上是新建了一个需要扩容的长度的数组,然后将原素组中的元素拷贝到这个新建的数组中,新的数组指定为ArrayList内部数据集数组。

总结:ArrayList在添加元素时,首先会判断添加的位置是否在内部数组中越界,如果越界,抛出异常;如果没有越界,则判断数组能否放得下新添加的元素,如果放得下,则直接存放到数组中;如果放不下,则将数组扩容,扩容后再存放到数组中

remove方法:

public E remove(int index) {
    // 检查越界
    rangeCheck(index);
    modCount++;
    // 需要移除的元素
    E oldValue = elementData(index);
    // 需要移动位置的元素的数量
    int numMoved = size - index - 1;
    // 将需要移除元素的位置后的所有元素复制到index位置开始后的numMoved个位置
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // size减1,并将之前的最后一个位置元素置空
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

remove还有一个重载的方法,是移除给定的元素,它的实现就是遍历数组,找到元素的索引值,然后调用remove(int index)方法,根据索引值去删除。

总结:ArrayList在删除元素时,根据删除的索引值判断是否越界,如果越界,抛出异常;如果没有越界,取出要删除的元素,然后将这个元素后面所有的元素向前移动一位。

set方法

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

set方法即修改列表中指定位置的元素值。它的实现非常简单:直接修改数组指定位置的值。

get方法

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

get方法实现非常简单只需要堆获取位置判断是否越界,然后直接从数组中取值即可。

从上面分析的源码中可以看出,ArrayList的实现就是对数组的操作,在添加和删除元素的时候,会涉及到数组的扩容和数组元素位置的移动,相对查询和修改元素要复杂一些,所以ArrayList适合用在查询和修改比较频繁,而添加和删除较少的情况下。


ArrayList相关

ArrayList和LinkedList的区别

通过阅读util包里面的源码可以很容易的看出两者的大致区别:
ArrayList是一种具有动态扩容特性且基于数组基础的数据结构,而LinkedList则是一种基于链表的数据结构。

在进行元素查找的时候适合用ArrayList进行操作,在进行元素的添加和删除的时候适合用LinkedList。由于ArrayList是采用数组作为数据结构的,因此在进行查找的时候只需要根据下标的索引进行判断即可。

LinkedList数据结构则是采用链表的结构进行设计的,因此在查找的时候需要进行逐一比较,所以效率会比较慢(并非是全链查询,而是采用了折半搜索的方式来进行优化)。在添加或者删除元素的时候,由于ArrayList需要进行元素的位置移动,而链表的移动和删除只需要将链表节点的头尾指针进行修改即可。

ArrayList的动态扩容有什么特点

当我们在进行ArrayList的插入元素时候,相应的元素会被插入到动态数组里面,但是由于数组本身所能存储的数据量是有限制的,因此在插入数据的时候,需要进行相应的动态扩容,在看源码的时候,可以看到相应的代码部分:

add操作源码

/**
 2     * Appends the specified element to the end of this list.
 3     *
 4     * @param e element to be appended to this list
 5     * @return <tt>true</tt> (as specified by {@link Collection#add})
 6     */
 7    public boolean add(E e) {
 8        ensureCapacityInternal(size + 1);  // Increments modCount!!
 9        elementData[size++] = e;
10        return true;
11    }

核心扩容部分的实现

private void ensureCapacityInternal(int minCapacity) {
 2        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 3            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
 4        }
 5
 6        ensureExplicitCapacity(minCapacity);
 7    }
 8
 9    private void ensureExplicitCapacity(int minCapacity) {
10        modCount++;
11
12        // overflow-conscious code
13        if (minCapacity - elementData.length > 0)
14            grow(minCapacity);
15    }

ArrayList默认数组的大小为10,扩容的时候采用的是采用移位运算

11int newCapacity = oldCapacity + (oldCapacity >> 1); 

这里也可以看出ArrayList的扩容因子为1.5。(4>>1 就相当于4除以2 即缩小了一半)。

什么时候会选择使用ArrayList

这又是一个大多数面试者都会困惑的问题。多数情况下,当遇到访问元素比插入或者是删除元素更加频繁的时候,应该使用ArrayList。另外一方面,当在某个特别的索引中,插入或者是删除元素更加频繁,或者压根就不需要访问元素的时候,不妨考虑选择LinkedList。

这里的主要原因是,在ArrayList中访问元素的最糟糕的时间复杂度是O(1),而在LinkedList中可能就是O(n)了。在ArrayList中增加或者删除某个元素时候,如果触发到了扩容机制,那么底层就会调用到System.arraycopy方法,如果有兴趣深入挖掘jdk源码的话,会发现这是一个本地调用方法,被native修饰,该方法会直接通过内存复制,省去了大量的数组寻址访问等时间,但是相比于LinkedList而言,在频繁的修改元素的情况下,选用LinkedList的性能会更加好一点。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
 2        @SuppressWarnings("unchecked")
 3        T[] copy = ((Object)newType == (Object)Object[].class)
 4            ? (T[]) new Object[newLength]
 5            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
 6        //复制集合
 7        System.arraycopy(original, 0, copy, 0,
 8                         Math.min(original.length, newLength));
 9        return copy;
10    }

如果读者有去学习过jvm的话,应该会对“内存碎片“这个名词比较熟悉。基于数组结构的数据在存储信息的时候都需要有连续的内存空间,所以如果当内存碎片化情况较为严重的时候,可能在使用ArrayList的时候会有OOM的异常抛出。

如何复制某个ArrayList到另一个ArrayList中去

1.使用clone()方法,比如ArrayList newArray = oldArray.clone();
2.使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
3.使用Collection的copy方法。

关于list集合的拷贝问题在面试中可能还会引申出深拷贝和浅拷贝的对比。

请说说ArrayList、Vector和LinkedList的区别

相同点

这三者都是单列集合Collection下List集合的实现类,所以他们的共同点,元素有序,允许重复元素 。

不同点

1.ArrayList和Vector底层都是数组实现,这样的实现注定查找快、增删慢 。
2.Vector支持线程同步,是线程访问安全的,ArrayList线程不安全 。
3.LinkedList底层是链表结构,查找元素慢、增删元素速度快,线程不安全。

modCount参数的意义

在ArrayList设计的时候,其实还包含有了一个modCount参数,这个参数需要和expectedModCount 参数一起使用,expectedModCount参数在进行修改的时候会被modCount进行赋值操作,当多个线程同时对该集合中的某个元素进行修改之前都会进行expectedModCount 和modCount的比较操作,只有当二者相同的时候才会进行修改,两者不同的时候则会抛出异常。这个机制也被称之为fail-fast机制

COW容器

jdk1.5之前,由于常用的ArrayList并不具有线程安全的特性,因此在1.5之后的并发包里面出现了CopyOnWrite容器,简称为COW。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

并发包juc里面的CopyOnWriteArrayList中,核心原理主要是通过加入了ReentrantLock来保证线程安全性,从而解决了ArrayList的线程安全隐患问题。

相应的add操作源码如下

/**
 2     * Appends the specified element to the end of this list.
 3     *
 4     * @param e element to be appended to this list
 5     * @return {@code true} (as specified by {@link Collection#add})
 6     */
 7    public boolean add(E e) {
 8        final ReentrantLock lock = this.lock;
 9        lock.lock();
10        try {
11            Object[] elements = getArray();
12            int len = elements.length;
13            Object[] newElements = Arrays.copyOf(elements, len + 1);
14            newElements[len] = e;
15            setArray(newElements);
16            return true;
17        } finally {
18            lock.unlock();
19        }
20    }

删除问题

使用过ArrayList的人一般都知道,在执行for循环的时候一般情况是不会去执行remove的操作的,因为remove的操作会改变这个集合的大小, 所以会有可能出现数组角标越界异常,我们可以试一下

看代码

package cn.wideth.util.other;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        list.add("E");
        list.add("F");

        for(String s : list){

            if(s.equals("D")){
                list.remove(s);
            }
        }

        System.out.println(list);
    }
}

程序结果

在这里插入图片描述


使用Iterator的remove()方法

看代码

package cn.wideth.util.other;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        list.add("E");
        list.add("F");

        Iterator<String> iterator = list.iterator();

        while (iterator.hasNext()) {
            
            String platform = iterator.next();
            if (platform.equals("D")) {
                iterator.remove();
            }
        }

        System.out.println(list);
    }
}

程序结果

在这里插入图片描述


使用for循环正序遍历

看代码

package cn.wideth.util.other;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        list.add("E");
        list.add("F");

        for (int i = 0; i < list.size(); i++) {
            String item = list.get(i);

            if (item.equals("D")) {
                list.remove(i);
                i = i - 1;
            }
        }

        System.out.println(list);
    }
}

程序结果

在这里插入图片描述


本文小结

本文详细介绍了arraylist相关的知识点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值