ArrayList详解

avatar
作者
筋斗云
阅读量:2

文章目录

ArrayList

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

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess,Cloneable,Serializable{} 

RandomAccess

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

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

// 假设 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方法源码:

protected native Object clone() throws CloneNotSupportedException; 

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

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

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

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

// 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对象并返回。

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

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方法调用栈:

add      -> ensureCapacityInternal()     -> calculateCapacity()     -> ensureExplicitCapacity()     -> grow() 

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

    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方法中的这段代码:

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

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

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

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异常。

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异常。

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

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

// 添加元素到指定的位置 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,性能略差不推荐使用;
    List list = new Vector(); list.add(UUID.randomUUID().toString()); 
  2. 使用Collections.synchronizedArrayList() 来创建 ArrayList。使用Collections工具类来创建ArrayList的思路是,在ArrayList的外边套了一个synchronized外壳,来保证ArrayList线程安全;
    List list = Collections.synchronizedArrayList(); list.add(UUID.randomUUID().toString()); 
  3. 使用CopyOnWriteArrayList()来保证 ArrayList 线程安全。CopyWriteArrayList字面意思就是在写的时候复制,主要思想就是读写分离的思想。CopyWriteArrayList之所以线程安全的原因是在源码里面使用ReentrantLock,所以保证了某个线程在写的时候不会被打断;
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add(UUID.randomUUID().toString()); 

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

    /** 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

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,便抛出异常。

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()方法能够确保在删除元素时,同步更新,从而避免异常。

ArrayList<String> list = new ArrayList<>(); // 添加元素到列表中 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) {     String element = iterator.next();     if (/* 满足删除条件 */) {         iterator.remove(); // 使用迭代器的 remove 方法安全删除元素     } } 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!