文章目录
ArrayList
在List
接口实现类中,最常用的就是ArrayList
,ArrayList
类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以添加或删除元素。ArrayList
继承了AbstractList
,并实现了List
、RandomAccess
,Cloneable
接口:
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); } }
在List
中ArrayList
被RandomAccess
接口标记,而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
方法。ArrayList
中clone
方法可以创建一个浅拷贝。
// 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
ArrayList
与LinkedList
性能比较是一道经典的面试题,先说答案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)
。LinkedList
与ArrayList
相反,获取第几个元素依次遍历复杂度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
变量的值。因为是多线程操作,expectedModCount
、modCount
变量为公共的,所以很容易出现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
线程安全有以下几种方法:
- 可以使用
Vector
来代替ArrayList
。Vector
是线程安全的ArrayList
,但是由于底层是加了synchronized
,性能略差不推荐使用;List list = new Vector(); list.add(UUID.randomUUID().toString());
- 使用
Collections.synchronizedArrayList()
来创建ArrayList
。使用Collections
工具类来创建ArrayList
的思路是,在ArrayList
的外边套了一个synchronized
外壳,来保证ArrayList
线程安全;List list = Collections.synchronizedArrayList(); list.add(UUID.randomUUID().toString());
- 使用
CopyOnWriteArrayList()
来保证ArrayList
线程安全。CopyWriteArrayList
字面意思就是在写的时候复制,主要思想就是读写分离的思想。CopyWriteArrayList
之所以线程安全的原因是在源码里面使用ReentrantLock
,所以保证了某个线程在写的时候不会被打断;CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add(UUID.randomUUID().toString());
其中比较推荐的解决方案是使用CopyOnWriteArrayList
。CopyWriteArrayList
字面意思就是在写的时候复制,思想就是读写分离的思想。它的基本原理是每次修改操作都会创建该列表的一个新副本,因此读操作不需要加锁,可以并发执行。以下是CopyOnWriteArrayList
的add()
方法源码:
/** 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
比较expectedModCount
和modCount
的大小,此时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 方法安全删除元素 } }