ArrayList详解

文章目录

ArrayList

List接口实现类中,最常用的就是ArrayListArrayList类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以添加或删除元素。ArrayList继承了AbstractList,并实现了ListRandomAccessCloneable接口:

java 复制代码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess,Cloneable,Serializable{}

RandomAccess

Random是随机的意思,Access是访问的意思,合起来RandomAccess就是随机访问的意思。RandomAccess接口是一个标记接口,用来标记实现的List集合具备快速随机访问的能力。所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了。

当一个List拥有快速访问功能时,其遍历方法采用随机访问速度最快,而没有快速随机访问的List采用顺序访问的速度最快。如果集合中的数据量过大需要遍历时,此时需要格外注意,因为不同的遍历方式会影响很大,可以使用instanceof关键字来判断该类有没有RandomAccess标记:

text 复制代码
// 假设 list 数据量非常大,推荐写法
List<Object> list = ...;

if(list instanceof RandomAccess){
    // 随机访问
    for (int i = 0;i< list.size();i++) {
        System.out.println(list.get(i));
    }
}else {
    // 顺序访问
    for(Object obj: list) {
        System.out.println(obj);
    }
}

ListArrayListRandomAccess接口标记,而LinkedList没有被RandomAccess接口标记,所以ArrayList适合随机访问,而LinkedList适合顺序访问。

Cloneable

Cloneable接口是Java开发中常用的一个接口,它是一个标记接口。如果一个想要拷贝一个对象,就需要重写Object中的clone方法并让其实现Cloneable接口。如果只重写clone方法,不实现Cloneable接口就会报CloneNotSupportedException异常。

clone方法源码:

java 复制代码
protected native Object clone() throws CloneNotSupportedException;

应当注意的是,clone()方法并不是Cloneable接口的方法,而是Object的一个protected方法。Cloneable接口只是规定,如果一个类没有实现Cloneable接口又调用了clone方法,就会抛出CloneNotSupportedException。换言之,clone方法规定了想要拷贝对象,就需要实现Cloneable方法,clone方法让Cloneable接口变得有意义。

拷贝分为浅拷贝与深拷贝:

  • 浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象;
  • 深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象;

如果一个被复制的属性都是基本类型,那么只需要实现当前类的Cloneable机制就可以了,此为浅拷贝。如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现Cloneable接口并覆盖clone方法。ArrayListclone方法可以创建一个浅拷贝。

java 复制代码
// ArrayList类
public class ArrayList implements Cloneable {

    transient Object[] elementData;     

    /**
     * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
     * elements themselves are not copied.)
     *
     * @return a clone of this <tt>ArrayList</tt> instance
     */
    public Object clone() {
        try {
            // 调用Object类的clone方法
            ArrayList<?> v = (ArrayList<?>) super.clone();
            
            // 将集合中的元素进行拷贝
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
}

// Arrays类
public class Arrays{
   public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
        return copy;
    }
}

ArrayList中核心方法最终调用Arrays.copyOf方法,其中调用Arrays.newInstance方法或者创建一个新数组,Arrays.newInstance方法作用,是创建具有指定组件类型和长度的新数组。所以不论怎样都会创建一个Object数组。最后使用System.arraycopy方法将之前的旧数组中的元素拷贝到新创建的数组中,然后赋值给ArrayList.elementData对象并返回。

如果需要深拷贝,即复制对象及其引用的所有对象,需要手动实现拷贝逻辑,通常涉及遍历列表并复制每个元素。

java 复制代码
class Item implements Cloneable {
    String name;

    Item(String name) {
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return name;
    }
}

public class ArrayListCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 创建并初始化一个ArrayList
        ArrayList<Item> originalList = new ArrayList<>();
        originalList.add(new Item("A"));
        originalList.add(new Item("B"));
        originalList.add(new Item("C"));

        // 深拷贝
        ArrayList<Item> deepCopy = new ArrayList<>();
        for (Item item : originalList) {
            deepCopy.add((Item) item.clone());
        }

        // 修改原始列表的元素
        originalList.get(0).name = "Z";

        // 输出结果
        System.out.println("Original List: " + originalList);
        System.out.println("Deep Copy: " + deepCopy);
    }
}

ArrayList扩容

因为ArrayList底层使用数组保存数据的,而数组一旦被创建就不能改变大小,但是ArrayList的长度是可以改变的,所以可以通过ArrayList类中的add方法找到数组扩容方法。add方法调用栈:

text 复制代码
add 
    -> ensureCapacityInternal()
    -> calculateCapacity()
    -> ensureExplicitCapacity()
    -> grow()

通过add方法,最终找到了grow方法,也就是扩容的核心方法:

java 复制代码
    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);
    }

ArrayList如果没有指定容量创建数组,默认会创建一个长度为10的数组用来保存元素,之后通过grow方法中的这段代码:

java 复制代码
 int newCapacity = oldCapacity + (oldCapacity >> 1);

>>右移几位就是相当于除以2的几次幂,所以每次扩容都是原容量的1.5倍。最后通过Arrays.copyOf方法将之前的数组中元素,全部移到新创建的数组上。

由于频繁的扩容数组会对性能产生影响,如果在ArrayList中要存储很大的数据,需要在ArrayList的有参构造中指定数组的长度,来减少扩容的次数。需要注意的是创建指定长度的ArrayList,在没有add之前ArrayList中的数组已经初始化了,但是List的大小没变,因为List的大小是由size决定的。

java 复制代码
List<String> list = new ArrayList(1000000);

ArrayList与LinkedList

ArrayListLinkedList性能比较是一道经典的面试题,先说答案ArrayList查找快,增删慢,而LinkedList增删快,查找慢。造成这种原因是因为底层的数据结构不一样。ArrayList底层是数组,而数组的中的元素内存分配都是连续的,并且数组中的元素只能存放一种,这就造成了数组中的元素地址是有规律的,数组中查找元素快速的原因正是利用了这一特点。查询方式为: 首地址+(元素类型长度*下标)。例如:new int arr[5]; arr数组的地址假设为0x1000,那么arr[3]地址可看作为 0x1000 + (3 * 4),3为数组下标,4为int元素类型长度。

LinkedList在Java中的底层结构是对象,每一个对象结点中都保存了下一个结点的位置形成的链表结构,由于LinkedList元素的地址是不连续的,所以没办法按照数组那样去查找,所以就比较慢。

由于数组一旦分配了大小就不能改变,所以ArrayList在进行添加操作时会创建新的数组,如果要添加到ArrayList中的指定的位置,是通过System.arraycopy方法将数组进行复制,新的数组会将待插入的指定位置空余出来,最后在将元素添加到集合中。在进行删除操作时是通过System.arraycopy方法,将待删除元素后面剩余元素复制到待删除元素的位置。当ArrayList里有大量数据时,这时候去频繁插入或删除元素会触发底层数组频繁拷贝,所以效率不高,还会造成内存空间的浪费。

LinkedList在进行添加,删除操作时,会用二分查找法找到将要添加或删除的元素,之后再设置对象的下一个结点来进行添加或删除操作,所以增加删除的效率高。

二分查找法:也称为折半查找法,是一种适用于大量数据查找的方法,但是要求数据必须的排好序的,每次以中间的值进行比较,根据比较的结果可以直接舍去一半的值,直至全部找完(可能会找不到)或者找到数据为止。此处LinkedList会比较查找的元素是距离头结点比较近,还是尾结点比较近,距离哪边较近则从哪边开始查找。

ArrayList获取元素效率非常的高,时间复杂度是O(1),而查找、插入和删除元素效率似乎不太高,时间复杂度为O(n)LinkedListArrayList相反,获取第几个元素依次遍历复杂度O(n),添加到末尾复杂度O(1),添加到指定位置复杂度O(n),删除元素直接指针指向操作复杂度O(1)

注意,ArrayList的增删不一定比LinkedList效率低,但是ArrayList查找效率一定比LinkedList高。如果在靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。

所以在实际开发中,ArrayList适用于需要快速随机访问和较少插入删除操作的场景,而LinkedList适用于频繁插入删除操作和需要实现队列或双端队列的场景。

线程安全

众所周知,ArrayList是线程不安全的,因为它不保证在多线程环境下的同步操作,当多个线程同时访问和修改同一个ArrayList对象时可能会导致数据不一致或抛出异常。多线程下抛出ConcurrentModificationException异常。

java 复制代码
public class MainTest {
    // 如果没有报错,需要多试几次
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        for(int i=0; i< 10; i++) {
            new Thread(() -> {
                arrayList.add(UUID.randomUUID().toString());
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }
    }
}

出现该异常的原因是集合中的fail-fast机制。在查看源码的时候,发现调用remove方法时,会执行checkForComodification方法。若modCount不等于expectedModCount,则抛出ConcurrentModificationException异常。

java 复制代码
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

那为什么会抛出ConcurrentModificationException异常呢?在调用add方法时,会修改modCount++。一个线程调用add方法,一个线程调用next遍历方法,都共同读取modCount变量的值。因为是多线程操作,expectedModCountmodCount变量为公共的,所以很容易出现modCount != expectedModCount,所以便抛出异常。

java 复制代码
// 添加元素到指定的位置
public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(
                "Index: " + index + ", Size: " + size);

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

因此,在多线程环境中使用ArrayList时,需要手动同步操作,或者使用线程安全的集合类。保证ArrayList线程安全有以下几种方法:

  1. 可以使用 Vector 来代替 ArrayListVector是线程安全的ArrayList,但是由于底层是加了synchronized,性能略差不推荐使用;

    text 复制代码
    List list = new Vector();
    list.add(UUID.randomUUID().toString());
  2. 使用Collections.synchronizedArrayList() 来创建 ArrayList。使用Collections工具类来创建ArrayList的思路是,在ArrayList的外边套了一个synchronized外壳,来保证ArrayList线程安全;

    text 复制代码
    List list = Collections.synchronizedArrayList();
    list.add(UUID.randomUUID().toString());
  3. 使用CopyOnWriteArrayList()来保证 ArrayList 线程安全。CopyWriteArrayList字面意思就是在写的时候复制,主要思想就是读写分离的思想。CopyWriteArrayList之所以线程安全的原因是在源码里面使用ReentrantLock,所以保证了某个线程在写的时候不会被打断;

    text 复制代码
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add(UUID.randomUUID().toString());

其中比较推荐的解决方案是使用CopyOnWriteArrayListCopyWriteArrayList字面意思就是在写的时候复制,思想就是读写分离的思想。它的基本原理是每次修改操作都会创建该列表的一个新副本,因此读操作不需要加锁,可以并发执行。以下是CopyOnWriteArrayListadd()方法源码:

java 复制代码
    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

     /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true}
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

CopyWriteArrayList之所以线程安全的原因是在源码里面使用ReentrantLock保证了某个线程在写的时候不会被打断。可以看到源码开始先是复制了一份数组,同一时刻只有一个线程写,其余的线程会读。在复制的数组上边进行写操作,写好以后在返回true。这样就把读写进行了分离,写好以后因为array加了volatile修饰,所以该数组是对于其他的线程是可见的,就会读取到最新的值。由于每次写操作都会创建一个数组的新副本,所以写操作的开销较大。但是读取操作非常高效且不需要加锁,因此适用于读操作远多于写操作的场景,例如缓存、白名单等。

ArrayList安全删除

ArrayList中删除元素时,"安全删除" 指的是在删除元素过程中避免出现异常或错误,并确保集合的结构和元素的状态保持一致。在使用增强型for-each循环遍历ArrayList时,如果尝试删除元素,会抛出ConcurrentModificationException

java 复制代码
public class ArrayListError {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");

        for (String element : list) {
            if ("B".equals(element)) {
                list.remove(element); // 这会抛出 ConcurrentModificationException
            }
        }
    }
}

在前面讲过add方法,会操作modCount变量的值,在查看源码的时候,发现调用remove方法时,也会操作modCount变量的值。当调用remove方法时执行了modCount++,此时modCount变成了N+1。然后接着再循环中遍历调用next方法,调用checkForComodification比较expectedModCountmodCount的大小,此时modCount != expectedModCount,便抛出异常。

java 复制代码
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

 // 删除指定位置的元素 
public E remove(int index) {
    RangeCheck(index);

    // 修改modCount
    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // Let gc do its work

    return oldValue;
}

安全删除的关键在于,确保在遍历和删除操作中不会同时对集合的结构造成不一致性,从而导致程序运行时出现异常或者结果不符合预期。

最经典的解决方案是使用Iterator遍历集合,在遍历过程中删除元素。在使用迭代器遍历ArrayList时,迭代器会维护一个expectedModCount,它记录了迭代开始时的modCount。每次调用迭代器的next方法时,都会检查当前的modCount是否与expectedModCount相等,如果不相等就抛出ConcurrentModificationException异常。使用迭代器的remove()方法能够确保在删除元素时,同步更新,从而避免异常。

text 复制代码
ArrayList<String> list = new ArrayList<>();
// 添加元素到列表中
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    if (/* 满足删除条件 */) {
        iterator.remove(); // 使用迭代器的 remove 方法安全删除元素
    }
}
相关推荐
morris1316 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
七星静香31 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员32 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU32 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie636 分钟前
在IDEA中使用Git
java·git
Elaine2023911 小时前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
落落鱼20131 小时前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存
镰刀出海1 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试