文章目录
- ArrayList源码解析
- 1、ArrayList简介
- 2、ArrayList继承关系
- 2.1 Seralizable接口
- 2.2 Cloneable接口
- 2.3 RandomAccess接口
- 3、源码解析
- 3.1 构造方法源码解析
- 3.1.1 无参构造源码解析
- 3.1.2 有参构造源码解析
- 3.2 `add`方法源码解析
- 3.2.1 `add(E e)`方法源码解析
- 3.2.2 `add(int index, E element)` 方法源码解析
- 3.2.3 `addAll(Collection<? extends E> c)`源码解析
- 3.2.4 `addAll(int index, Collection<? extends E> c)`源码解析
- 3.3 `remove`方法源码解析
- 3.3.1 `remove(int index)`方法源码解析
- 3.3.2 `remove(Object o)`
- 3.4 `get(int index)`和`set(int index, E element)`方法源码解析
- 3.5 `iterator()`方法源码解析
- 3.6 `clear()`和`isEmpty()`方法源码解析
- 3.7 `contains(Object o)`方法源码解析
- 总结
- ArrayList核心源码总结
- `System.arraycopy()`和`Arrays.copyOf()`方法
- ArrayList经典Demo
- ArrayList常见问题
- ArrayList频繁扩容导致添加性能急剧下降,如何处理?
- ArrayList插入或删除元素一定比LinkedList慢么?
- ArrayList是线程安全的么?
- 如何复制某个ArrayList到另一个ArrayList中去?
- 已知成员变量集合存储N多用户名称,在多线程的环境下,使用迭代器在读 取集合数据的同时如何保证还可以正常的写入数据到集合?
- ArrayList和LinkedList的区别有哪些?
ArrayList源码解析
1、ArrayList简介
-
ArrayList是什么?
ArrayList 是 Java 中的一个很常用的集合类,它具有有序、有索引、可重复‘、动态扩容、线程不安全等特点。
ArrayList 底层是基于数组存储元素的,数组是线性表,所以插入和删除元素的时间复杂度是 O ( n ) O(n) O(n),根据索引索取元素的时间复杂度 是 O ( 1 ) O(1) O(1),所以 ArrayList 查询快,增删比较慢
-
ArrayList常用操作:略……(本文只详细探究ArrayList源码,至于这个API讲解可以参考这篇文章:Java常用集合总结)
2、ArrayList继承关系
2.1 Seralizable接口
-
Seralizable接口是什么?
Serializable接口是Java中的一个标记接口(Marker Interface),它没有任何方法定义,只是作为一个标志,用于表示类的对象可以被序列化。序列化是将对象转换为字节序列的过程,使得对象可以被存储到文件、数据库中,或者通过网络进行传输。当一个类实现了Serializable接口时,就表明该类的对象是可序列化的,可以通过序列化和反序列化来保存和恢复对象的状态。
注意:在Java中,如果一个类如果想要实现序列化和反序列化,一定要实现
Seralizable
接口,否则会报NotSerializableException
异常,此外如果一个类的某一个属性我们不想进行序列化或者无法进行序列化可以使用transient
关键字来修饰某些字段,以排除它们不被序列化 -
什么是序列化和反序列化?
- 序列化(Serialization):是指将对象转换为字节序列的过程,使得对象可以被存储到文件、数据库中或者通过网络进行传输
- 反序列化(Deserialization):是指将字节序列转换回对象的过程,恢复对象的状态
序列化是将对象转换为字节序列的过程,而反序列化是将字节序列转换回对象的过程,通过序列化和反序列化可以实现对象的持久化、网络传输和分布式计算等功能
ArrayList实现序列化和反序列化Demo:
import java.io.*;
import java.util.ArrayList;
public class ArrayListSerializationDemo {
public static final String file = "./src/data.ser";
public static void main(String[] args) {
// 创建ArrayList对象并添加元素
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
// 序列化(将ArrayList对象转成二进制流存入文件)
serializeArrayList(list, file);
// 反序列化(读取文件中的二进制数据转成ArrayList对象)
ArrayList<String> deserializedList = deserializeArrayList(file);
// 输出反序列化后的ArrayList
System.out.println(deserializedList);
}
/**
* 序列化ArrayList
*/
private static void serializeArrayList(ArrayList<String> list, String fileName) {
try (FileOutputStream fileOut = new FileOutputStream(fileName);
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
objectOut.writeObject(list);
System.out.println("ArrayList序列化成功");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 反序列化ArrayList
*/
private static ArrayList<String> deserializeArrayList(String fileName) {
try (FileInputStream fileIn = new FileInputStream(fileName);
ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
ArrayList<String> list = (ArrayList<String>) objectIn.readObject();
System.out.println("ArrayList反序列化成功");
return list;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
2.2 Cloneable接口
-
Coneable接口是什么?
Cloneable接口是Java中的一个标记接口(Marker Interface),它没有任何方法定义,只是作为一个标志,表示实现该接口的类可以进行克隆(Clone)操作。
-
什么是深克隆和浅克隆?
- 浅克隆(Shallow Clone):是指创建一个新对象,并将原始对象的值直接复制到新对象中。新对象和原始对象共享相同的引用类型字段,即它们引用相同的对象
- 深克隆(Deep Clone):是指创建一个新对象,并将原始对象所有引用类型字段指向的对象也进行克隆,从而实现整个对象及其关联对象的完全复制
注意:浅克隆只复制对象本身的值,而不会复制对象内部的引用类型属性,所以当我们在使用浅克隆时,如果修改了克隆对象可能会影响原对象,而深克隆不仅复制了对象本身的值,还重新开辟空间将原本的引用也复制了一份,所以修改深克隆出来的对象是不会影响到原对象的
-
克隆的作用:
- 保护对象的原始状态:通过克隆对象,可以创建一个与原对象相同的副本,从而保护原始对象的状态。这在一些需要对对象进行修改、操作或者比较时非常有用,可以避免直接对原对象进行操作,导致其状态被改变。
- 提高性能:在某些情况下,如果需要频繁地创建相似的对象,使用克隆可以避免每次都进行初始化和赋值操作,从而提高代码的执行效率。
- 实现深拷贝:克隆操作可以实现对象的深拷贝,即复制对象及其引用对象的所有内容。这对于需要根据原对象创建完全独立的新对象时非常有用,确保新对象的属性值不受原对象的影响。
最常见的应用:原型模式就是利用了克隆,避免过多的new对象(感兴趣的可以参考这篇文章:每日一个设计模式之【原型模式】)
ArrayList实现深克隆和浅克隆的Demo:
import java.util.ArrayList;
import java.util.List;
class Person implements Cloneable {
private String name;
private List<String> hobbies;
public Person(String name, List<String> hobbies) {
this.name = name;
this.hobbies = hobbies;
}
public String getName() {
return name;
}
public List<String> getHobbies() {
return hobbies;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 浅克隆
// return super.clone();
// 深克隆
Person clonedPerson = (Person) super.clone();
clonedPerson.hobbies = new ArrayList<>(hobbies);
return clonedPerson;
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
List<String> originalHobbies = new ArrayList<>();
originalHobbies.add("Reading");
originalHobbies.add("Traveling");
Person originalPerson = new Person("Alice", originalHobbies);
// 浅克隆
Person shallowClone = (Person) originalPerson.clone();
System.out.println("Original Person: " + originalPerson.getName() + ", Hobbies: " + originalPerson.getHobbies());
System.out.println("Shallow Clone: " + shallowClone.getName() + ", Hobbies: " + shallowClone.getHobbies());
// 修改原始对象的爱好列表
originalPerson.getHobbies().add("Cooking");
// 验证浅克隆后,引用类型字段共享同一对象
System.out.println("Original Person: " + originalPerson.getName() + ", Hobbies: " + originalPerson.getHobbies());
System.out.println("Shallow Clone: " + shallowClone.getName() + ", Hobbies: " + shallowClone.getHobbies());
System.out.println("------------------------");
// 深克隆
Person deepClone = (Person) originalPerson.clone();
System.out.println("Original Person: " + originalPerson.getName() + ", Hobbies: " + originalPerson.getHobbies());
System.out.println("Deep Clone: " + deepClone.getName() + ", Hobbies: " + deepClone.getHobbies());
// 修改原始对象的爱好列表
originalPerson.getHobbies().add("Gardening");
// 验证深克隆后,引用类型字段具有独立的副本
System.out.println("Original Person: " + originalPerson.getName() + ", Hobbies: " + originalPerson.getHobbies());
System.out.println("Deep Clone: " + deepClone.getName() + ", Hobbies: " + deepClone.getHobbies());
}
}
2.3 RandomAccess接口
-
RandomAccess接口是什么?
RandomAccess接口是Java集合框架中的一个标记接口(Marker Interface),位于 java.util 包下。它并不包含任何方法,仅用于标识实现该接口的集合是否支持高效的随机访问。
-
RandomAccess接口的作用
RandomAccess 接口的作用是为那些实现了 List 接口的集合提供一个标志,表明该集合支持高效的随机访问。如果一个集合实现了 RandomAccess 接口,就意味着使用索引进行访问元素的操作(如
get()
、set()
、indexOf()
等)的性能较好。反之,如果一个集合没有实现 RandomAccess 接口,则表示使用索引进行访问的性能可能较差。注意:并非所有实现了 List 接口的集合都实现了 RandomAccess 接口。例如,ArrayList 实现了 RandomAccess 接口,因为它底层基于数组实现,支持通过索引快速访问元素。但是, LinkedList 并没有实现 RandomAccess 接口,因为它是基于链表实现的,对于随机访问需要遍历链表。
案例一:测试ArrayList使用了RandomAccess接口访问和不使用RandomAccess接口访问性能之间的差距
public class RandomAccessDemo01 {
public static void main(String[] args) {
//创建ArrayList
ArrayList<String> list = new ArrayList<String>();
//添加10W条数据
for (int i = 0; i < 100000; i++) {
list.add("黑马程序员"+i);
}
//随机访问
long startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
//取出集合的元素
list.get(i);
}
long endTime = System.currentTimeMillis();
System.out.println("ArrayList随机访问用时: "+(endTime - startTime)); // 输出 1
//顺序访问
startTime = System.currentTimeMillis();
//获取迭代器
Iterator<String> it = list.iterator();
//循环遍历
while (it.hasNext()){
it.next();
}
endTime = System.currentTimeMillis();
System.out.println("ArrayList顺序访问用时: "+(endTime - startTime)); // 输出 2
}
}
结论:两者差不多,但是总的来讲使用RandomAccess接口的get方法比顺序遍历的迭代器方法要更快,当 数据量为10w的时候,使用RandomAccess接口的随机访问耗时 1ms,而Iterator顺序访问的耗时 2ms
案例二:测试LinkedList使用get和迭代器访问耗时对比
public class RandomAccessDemo02 {
public static void main(String[] args) {
// 创建LinkedList集合
List<String> list = new LinkedList<>();
//添加10W条数据
for (int i = 0; i < 100000; i++) {
list.add(i + "");
}
long startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
// 仅仅为了演示取出数据的时间,因此不对取出的数据进行打印
list.get(i);
}
long endTime = System.currentTimeMillis();
System.out.println("随机访问: " + (endTime - startTime)); // 5104
startTime = System.currentTimeMillis();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
//仅仅为了演示取出数据的时间,因此不对取出的数据进行打印
it.next();
}
endTime = System.currentTimeMillis();
System.out.println("顺序访问: " + (endTime - startTime)); // 3
}
}
结论:LinkedList使用迭代器访问比get方法要快很多很多,10w条数据使用get访问耗时高达 5104ms,而迭代器只有 3ms,这是为什么呢?主要是由于LinkedList的底层结构是链表,所以LinkedList无法支持索引访问,也就没有实现RandomAccess 接口,而LinkedList使用get访问,每次访问都需要从头节点开始遍历,这样每次都需要遍历一次十分浪费时间,而迭代器访问下一层遍历可以直接接着上一次的位置往下访问,所以只需要遍历一遍即可
温馨提示:所以平常遍历的时候,如果是ArrayList尽量使用索引遍历,而LinkedList最好使用迭代器遍历。
但是一般开发中我们都是直接使用多态机制,我们无论是创建ArrayList或者LinkedList都是使用List类型,所以不确定的话同时我们想要遍历,我们最好使用迭代器遍历。当然不嫌麻烦,也可以直接使用 instaceof
关键字进行判断,如果当前 List 对象实现了RandomAccess接口就选择使用索引遍历,否则就使用迭代器遍历
3、源码解析
3.1 构造方法源码解析
3.1.1 无参构造源码解析
public class ArrayList<E> {
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 默认容量的空数组
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 集合真正存储数组元素的数组
*/
transient Object[] elementData;
/**
* 集合的大小
*/
private int size;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
}
可以看到通过空参构造方法创建集合对象并未构造一个初始容量为10的空数组,仅仅将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的地址赋值给elementData
3.1.2 有参构造源码解析
-
new ArrayList(int initialCapacity)
public ArrayList(int initialCapacity) { //判断初始容量initialCapacity是否大于0 if (initialCapacity > 0) { // 创建一个数组,且指定长度为initialCapacity this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 如果initialCapacity容量为0,把EMPTY_ELEMENTDATA的地址赋值给elementData // 相当于是无参构造 this.elementData = EMPTY_ELEMENTDATA; } else { // 以上两个条件都不满足报错 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } }
-
new ArrayList(Collection<? extends E> c)
public class ArrayList<E> { public ArrayList(Collection<? extends E> c) { // 将集合构造中的集合对象转成数组,且将数组的地址赋值给elementData elementData = c.toArray(); // 将elementData的长度赋值给 集合长度size,且判断是否不等于 0 if ((size = elementData.length) != 0) { // 判断 elementData 和 Object[] 是否为不一样的类型 if (elementData.getClass() != Object[].class) // 如果不一样,使用Arrays的copyOf方法进行元素的拷贝 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 用空数组代替 this.elementData = EMPTY_ELEMENTDATA; } } // 将集合转数组 public Object[] toArray() { // 调用数组工具类方法进行拷贝 return Arrays.copyOf(elementData, size); } }
// 数组工具类源码 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) { // 用三元运算符进行判断,不管结果如何都是创建一个新数组 T[] copy = ((Object) newType == (Object) Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 将数组的内容拷贝到 copy 该数组中 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); // 返回拷贝元素成功后的数组 return copy; } }
3.2 add
方法源码解析
3.2.1 add(E e)
方法源码解析
-
add(E e)
:/** * 往ArrayList添加元素 * * @param e 待添加的元素 * @return */ public boolean add(E e) { // 调用方法对内部容量进行校验,判断ArrayList现有容量能否添加一个元素 ensureCapacityInternal(size + 1); // 扩容完毕后直接在已有元素后面添加 elementData[size++] = e; return true; } /** * 确保ArrayList容量充足 * * @param minCapacity 当前所需最小容量 */ private void ensureCapacityInternal(int minCapacity) { // 判断当前数组是否已被初始化(即 elementData 是否为 null) if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 通过最小容量和默认容量求出较大值 (用于第一次扩容) minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } // 将if中计算出来的容量传递给下一个方法,继续校验 ensureExplicitCapacity(minCapacity); } /** * 确保ArrayList扩容后的容量充足 * * @param minCapacity 当前所需最小容量 */ private void ensureExplicitCapacity(int minCapacity) { // 集合修改次数+1(在扩容的过程中没用,主要是用于迭代器中,确保线程安全) // modCount这个字段主要用于实现 ArrayList 的快速失败机制 fail-fast modCount++; // 判断最小容量 - 数组长度是否大于 0(也就是判断当前数组容量是否满足最小容量,避免溢出) if (minCapacity - elementData.length > 0) // 当前数组容量不满足所需最小容量,则将第一次计算出来的容量传递给 核心扩容方法 grow(minCapacity); } /** * 扩容 elementData 数组 * * @param minCapacity 当前所需最小容量 */ private void grow(int minCapacity) { // 记录数组的实际长度,此时由于木有存储元素,长度为0 int oldCapacity = elementData.length; // 核心扩容算法 原容量的 1.5 倍(位运算效率更高) int newCapacity = oldCapacity + (oldCapacity >> 1); // 判断新容量 - 最小容量 是否小于 0(判断扩容后的容量是否足够) if (newCapacity - minCapacity < 0) // 当前容量不够,将最小容量赋值给新容量(确保新容量不会比最小容量小) newCapacity = minCapacity; // 判断新容量-最大数组大小(也就是 Integer.MAX_VALUE - 8) 是否>0(判断扩容后的容量是否足够) if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量已超过了当前数组的最大容量,需要重新计算出一个超大的容量(其实就是Integer.MAX_VALUE) newCapacity = hugeCapacity(minCapacity); // 调用数组工具类方法,浅拷贝出一个新数组(新数组的大小就是前面计算的newCapacity) elementData = Arrays.copyOf(elementData, newCapacity); } /** * 获取一个超大容量 * @param minCapacity * @return */ private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow(健壮性代码) throw new OutOfMemoryError(); // 如果当前超过了 MAX_ARRAY_SIZE(Integer.MAX_VALUE-8)就返回Integer.MAX_VALUE // 否则返回 MAX_ARRAY_SIZE(Integer.MAX_VALUE-8) return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
备注:
Arrays.copyOf
源码在前面已经解析过了,这个方法其实也很简单,平常刷算法题也有经常用到,其实就是将 源数组 按照从 0到 n进行拷贝,如果n比源数组大,也会开辟一个空间,这里add里面的 newCapacity 肯定是要比 源数组要打的,从而实现扩容机制
3.2.2 add(int index, E element)
方法源码解析
-
add(int index, E element)
:/** * 往指定索引位置添加元素 * * @param index 指定索引 * @param element 待添加的元素 */ public void add(int index, E element) { // 判断索引是否越界 rangeCheckForAdd(index); // 检验是否要扩容 ensureCapacityInternal(size + 1); // 浅拷贝 elementData 中 index 后面的所有元素 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 覆盖原来索引的位置 elementData[index] = element; size++; } /** * 判断索引是否越界 * * @param index */ private void rangeCheckForAdd(int index) { // 超出指定范围就报 IndexOutOfBoundsException 异常(相信对于之前刚学Javad的同学这个异常很常见吧(●ˇ∀ˇ●)) if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
插入元素示意图(这张图综合了前面的无参构造源码和add(E e)
源码以及add(int index, E e)
源码):
3.2.3 addAll(Collection<? extends E> c)
源码解析
addAll(Collection<? extends E> c)
源码其实大部分和add(E e)
很相似,只是将单个添加换成了批量添加
/**
* 往ArrayList中添加若干给元素
*
* @param c 待添加的元素集合
* @return
*/
public boolean addAll(Collection<? extends E> c) {
// 把集合的元素转存到Object类型的数组中
Object[] a = c.toArray();
// 新增元素的数量
int numNew = a.length;
// 调用方法检验是否要扩容
ensureCapacityInternal(size + numNew);
// 调用方法将a数组的元素拷贝到elementData数组中
System.arraycopy(a, 0, elementData, size, numNew);
// 集合的长度+=a数组的长度
size += numNew;
// 只要a数组的长度不等于0,即说明添加成功
return numNew != 0;
}
3.2.4 addAll(int index, Collection<? extends E> c)
源码解析
addAll(int index, Collection<? extends E> c)
:
public boolean addAll(int index, Collection<? extends E> c) {
// 判断是否索引越界
rangeCheckForAdd(index);
// 将数据源转成数组
Object[] a = c.toArray();
// 记录数据源的长度
int numNew = a.length;
// 目的就是为了给集合存储数据的数组进行扩容
ensureCapacityInternal(size + numNew);
// 计算元素要后移的长度
int numMoved = size - index;
// 判断需要移动的个数是否大于0(健壮性判断)
if (numMoved > 0)
// 将 [index, size] 这个闭区间的所有元素往后移动 numMoved 位
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
// 将要添加的元素浅拷贝到 elementData的[index, size]区间,覆盖原来的值
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
// 判断当前有没有新增成功,如果 numNew == 0 说明没有新增元素,也就是新增失败
// 这里使用 !=0 来判断,简化了判断逻辑,实际上 numNew 要么大于0,要么小于0,不可能是负数
return numNew != 0;
}
这里关于其它的方法源码,其实在前面都已经讲过了,比如rangeCheckForAdd
、ensureCapacityInternal
、arraycopy
,这里就不再赘述了
3.3 remove
方法源码解析
3.3.1 remove(int index)
方法源码解析
/**
* 移除指定索引位置的元素
*
* @param index 指定索引
* @return E 移除的元素
*/
public E remove(int index) {
// 判断索引是否越界
rangeCheck(index);
// 修改次数加一
modCount++;
// 将index对应的元素赋值给 oldValue,用于返回
E oldValue = elementData(index);
// 计算集合需要移动元素个数
int numMoved = size - index - 1;
// 判断numMoved是否大于0(健壮性判断)
if (numMoved > 0)
// 将elementData这个 [index+1, size] 闭区间中的所有元素往前移动一位
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
// 将源集合最后一个元素置为null
elementData[--size] = null;
// 返回被删除的元素
return oldValue;
}
3.3.2 remove(Object o)
/**
* 移除指定元素
* @param o 要移除的元素
* @return
*/
public boolean remove(Object o) {
// 判断要删除的元素是否为null
if (o == null) {
// 遍历集合,寻找为null的元素
for (int index = 0; index < size; index++)
// 判断集合的元素是否为null
if (elementData[index] == null) {
// 如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
}
} else {
// 遍历集合,寻找要删除的元素
for (int index = 0; index < size; index++)
// 用o对象的equals方法和集合每一个元素进行比较
// 注意这里是直接调用Object的equals方法进行比较,比较的是地址
if (o.equals(elementData[index])) {
// 如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
}
}
// 如果集合没有o该元素,那么就会返回false
return false;
}
/**
* 移除指定索引位置的元素
* @param index
*/
private void fastRemove(int index) {
// 修改次数+1
modCount++;
// 计算集合需要移动元素的个数
int numMoved = size - index - 1;
// 判断numMoved是否大于0(健壮性判断)
if (numMoved > 0)
// 将elementData这个 [index+1, size] 闭区间中的所有元素往前移动一位
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
// 将集合最后一个元素置为null
elementData[--size] = null;
}
可以看到remove(Object o)
中的fastRemove
方法移除元素 和 remove(int index)
是等价的
3.4 get(int index)
和set(int index, E element)
方法源码解析
/**
* 根据指定索引获取元素
* @param index 指定的索引
* @return
*/
public E {
// 判断是否发生索引越界
rangeCheck(index);
// 直接根据索引取出集合元素
return elementData(index);
}
/**
* 修改指定索引位置的元素值
* @param index 指定索引
* @param element 修改后的值
* @return
*/
public E set(int index, E element) {
// 判断索引是否越界
rangeCheck(index);
// 获取要修改的值,用于返回
E oldValue = elementData(index);
// 将element直接覆盖index对应的元素
elementData[index] = element;
// 返回被覆盖的元素
return oldValue;
}
3.5 iterator()
方法源码解析
/**
* 将ArrayList转成Iterator
*/
public Iterator<E> iterator() {
return new Itr();
}
/**
* ArrayList集合内部类
*/
private class Itr implements Iterator<E> {
// 游标,用于遍历集合。它表示当前迭代器所指向的元素的索引
int cursor;
// 记录上一个访问的元素的索引。默认值为-1,表示还没有进行过迭代
int lastRet = -1;
// 用于检测并发修改的计数器。它保存了创建迭代器时集合的modCount值,用于后续比较是否发生了并发修改
int expectedModCount = modCount;
/**
* 判断集合中是否还有元素
*/
public boolean hasNext() {
// cursor要么小于,要么等于,小于的时候不等式成立,说明集合中还有元素
// 等于的时候不等成不成立,说明游标已经遍历到集合最后一个元素了,已经没有最后一个元素了
return cursor != size;
}
/**
* 移动游标到下一个位置,获取游标上一个位置的值
*/
public E next() {
// 检测集合是否发生了并发修改
checkForComodification();
// 将当前游标的值赋值给i
int i = cursor;
// 健壮性判断
if (i >= size)
// 如果i大于size,此时已经发生了越界,没有元素,抛一个NoSuchElementException异常
throw new NoSuchElementException();
// 将ArrayList中的成员变量 elementData 赋值给局部变量 elementData
Object[] elementData = ArrayList.this.elementData;
// 健壮性判断
if (i >= elementData.length)
// 如果i大于数组的最大元素,此时已经发生了越界,没有元素,抛一个NoSuchElementException异常
throw new ConcurrentModificationException();
// 游标值+1
cursor = i + 1;
// 返回 lastRet 对应位置的元素(也就是游标上一个位置的值)
return (E) elementData[lastRet = i];
}
/**
* 检查 modCount 变量是否发生修改
*/
final void checkForComodification() {
// 创建迭代器的时候 就将初始的 modCount 赋值给了 expectedModCount
// 如果在迭代器遍历的过程中对ArrayList进行了修改就会抛ConcurrentModificationException异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
所以平常我们如果使用迭代器的方式遍历ArrayList集合时,不要一边遍历ArrayList一遍修改ArrayList集合,这个我有过深刻的教训,感兴趣的可以看看这篇文章:Exception in thread “Thread-2“ java.util.ConcurrentModificationException异常的解决方案
当然如果你实在想要在遍历的同时对ArrayList进行修改,可以使用迭代器提供的修改方法remove()
,但是这个方法好像只能删除符合指定条件的一个元素,不能重复调用
迭代器自带的remove
方法源码解析:
/**
* 删除 lastRet 位置的元素
*/
public void remove() {
// 判断游标上一个位置是否小于0(健壮性判断)
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 删除ArrayList中位于 lastRet 位置的元素
ArrayList.this.remove(lastRet);
// 将游标后移一位
cursor = lastRet;
// 将 lastRet 置为-1,恢复为迭代前的状态(也就是还未被迭代)
lastRet = -1;
// 重置expectedModCount,防止出现并发修改异常的发生(这个是核心操作)
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
示例一:演示迭代器的remove
方法
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
Integer value = iterator.next();
// 移动一个删除一个
iterator.remove();
System.out.println(value); // 打印 1 2 3
}
// 由于迭代器的remove方法一遍迭代一遍删除,所以最后list中的元素都被删除了
System.out.println(list); // []
}
反例一:使用迭代器遍历 ArrayList 的同时,调用 ArrayList 的方法进行修改(比如:add
、remove
、set
方法)
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
Integer value = iterator.next();
System.out.println(value);
list.add(5); // ConcurrentModificationException
}
}
3.6 clear()
和isEmpty()
方法源码解析
这两个方法的源码就相对简单很多了
/**
* 清空ArrayList
*/
public void clear() {
// 修改次数+1
modCount++;
// 遍历集合,将集合每一个索引对应位置上的元素都置为null
for (int i = 0; i < size; i++)
elementData[i] = null;
// 集合长度更改为0
size = 0;
}
/**
* 判断集合是否为null
*/
public boolean isEmpty() {
// 只需要判断size是否为0即可,size表示当前集合中的元素个数
return size == 0;
}
3.7 contains(Object o)
方法源码解析
/**
* 判断集合中是否含有元素 o
* @param o
* @return
*/
public boolean contains(Object o) {
// 调用indexOf方法进行查找
// 如果结果为-1,也就是小于0,就说明ArrayList中没有o,反之则说明有
return indexOf(o) >= 0;
}
/**
* 获取元素第一次出现的索引位置
*
* @param o
* @return -1表示集合中不存在该元素,>=0的时候表示该元素在集合中最先出现的索引
*/
public int indexOf(Object o) {
// 判断 o 是否为null,为null和不为null都需要进行遍历,判断方式不同
if (o == null) {
// 如果元素是null,也进行遍历操作,因为集合中有可能够会存储null
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {
// o非null,则直接调用equal方法进行判断,注意Object的equals方法判断是是地址
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
// 如果if、else两者都没有从ArrayList中找到o,则说明集合中压根不存在这个元素,直接返回-1
return -1;
}
总结
ArrayList核心源码总结
对于ArrayList我们需要知道的点:
-
扩容核心算法:
int newCapacity = oldCapacity + (oldCapacity >> 1);
这里通过
>>
(算术右移)计算出当前容量的一半 -
扩容流程
- 先由
ensureCapacityInternal
方法获取当前集合所需最小容量。也就是计算当前最小容量,也就是判断当前较大值是DEFAULT_CAPACITY
(默认容量10)还是minCapacity
,较大值为当前最小容量 - 然后再由
ensureExplicitCapacity
判断是否需要扩容,也就是判断minCapacity
和elementData.length
的大小,如果minCapacity大于elementData.length,就直接调用grow
方法进行扩容 - 最后由
grow
方法进行扩容,先按照核心扩容算法进行扩容(也也就扩容为原来的1.5倍,具体的扩容是利用Arrays.copyOf
方法进行浅拷贝),然后判断扩容后的容量是否充足,如果容量不足,再调用hugeCapacity
方法得到一个更大的容量(这个最大容量Integer.MAX_VALUE
)
- 先由
package java.util;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组(用于空实例)。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小空实例的共享空数组实例。
//我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存ArrayList数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的元素个数
*/
private int size;
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
*默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
//
elementData = c.toArray();
//如果指定集合元素个数不为0
if ((size = elementData.length) != 0) {
// c.toArray 可能返回的不是Object类型的数组所以加上下面的语句用于判断,
//这里用到了反射里面的getClass()方法
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
/**
* 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//下面是ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,如果每次只扩充一个,
//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(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方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
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);
}
//比较minCapacity和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/**
*返回此列表中的元素数。
*/
public int size() {
return size;
}
/**
* 如果此列表不包含元素,则返回 true 。
*/
public boolean isEmpty() {
//注意=和==的区别
return size == 0;
}
/**
* 如果此列表包含指定的元素,则返回true 。
*/
public boolean contains(Object o) {
//indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
return indexOf(o) >= 0;
}
/**
*返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
*/
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++)
//equals()方法比较
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。.
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。)
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
//Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// 这不应该发生,因为我们是可以克隆的
throw new InternalError(e);
}
}
/**
*以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
*返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
*因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
/**
* 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);
*返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。
*否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。
*如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。
*(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。)
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 新建一个运行时类型的数组,但是ArrayList数组的内容
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
//调用System提供的arraycopy()方法实现数组之间的复制
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// Positional Access Operations
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
/**
* 返回此列表中指定位置的元素。
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* 用指定的元素替换此列表中指定位置的元素。
*/
public E set(int index, E element) {
//对index进行界限检查
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
//返回原来在这个位置的元素
return oldValue;
}
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
/**
* 在此列表中的指定位置插入指定的元素。
*先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
*再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
/**
* 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
//从列表中删除的元素
return oldValue;
}
/**
* 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。
*返回true,如果此列表包含指定的元素
*/
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;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
/**
* 从列表中删除所有元素。
*/
public void clear() {
modCount++;
// 把数组中所有的元素的值设为null
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
/**
* 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
/**
* 将指定集合中的所有元素插入到此列表中,从指定的位置开始。
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
/**
* 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。
*将任何后续元素移动到左侧(减少其索引)。
*/
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
/**
* 检查给定的索引是否在范围内。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* add和addAll使用的rangeCheck的一个版本
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 返回IndexOutOfBoundsException细节信息
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
/**
* 从此列表中删除指定集合中包含的所有元素。
*/
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
//如果此列表被修改则返回true
return batchRemove(c, false);
}
/**
* 仅保留此列表中包含在指定集合中的元素。
*换句话说,从此列表中删除其中不包含在指定集合中的所有元素。
*/
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
/**
* 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。
*指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。
*返回的列表迭代器是fail-fast 。
*/
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
/**
*返回列表中的列表迭代器(按适当的顺序)。
*返回的列表迭代器是fail-fast 。
*/
public ListIterator<E> listIterator() {
return new ListItr(0);
}
/**
*以正确的顺序返回该列表中的元素的迭代器。
*返回的迭代器是fail-fast 。
*/
public Iterator<E> iterator() {
return new Itr();
}
System.arraycopy()
和Arrays.copyOf()
方法
可以看到在ArrayList的底层源码中,这两个拷贝方法出现了很多次,现在我们来研究一下两者的区别吧
-
联系:看两者源代码可以发现
copyOf()
内部调用了System.arraycopy()
方法,两者都是浅拷贝 -
区别:
-
参数传递方式不同:
-
System.arraycopy()
方法需要提供源数组、源数组的起始位置、目标数组、目标数组的起始位置以及要复制的元素个数作为参数。 -
Arrays.copyOf()
方法只需要提供源数组和要复制的长度作为参数。
-
-
返回结果不同:
-
System.arraycopy()
方法没有返回值,直接将源数组的元素复制到目标数组中。 -
Arrays.copyOf()
方法会返回一个新的数组,其中包含了源数组的指定长度部分。
-
-
目标数组长度不足时的处理不同:
-
System.arraycopy()
方法会将源数组尽可能多的元素复制到目标数组中,如果目标数组长度不足,则只复制部分元素。 -
Arrays.copyOf()
方法在目标数组长度不足时,会自动扩展目标数组的长度,并将剩余位置填充默认值(例如,对于整型数组,默认值为0)。
-
-
-
参数介绍
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
src
: 源数组对象,表示要复制元素的源数组。srcPos
: 源数组的起始位置,表示从源数组的哪个索引位置开始复制元素。dest
: 目标数组对象,表示将要复制元素到目标数组中。destPos
: 目标数组的起始位置,表示从目标数组的哪个索引位置开始存放复制的元素。length
: 要复制的元素个数,表示从源数组复制多少个元素到目标数组中。
Arrays.copyOf(T[] original, int newLength)
original
: 源数组对象,表示要进行复制的源数组。newLength
: 新数组的长度,表示复制后的数组应该具有的长度。
示例:
int[] sourceArray = {1, 2, 3, 4, 5};
// 使用 System.arraycopy() 方法复制数组
int[] targetArray1 = new int[5];
System.arraycopy(sourceArray, 0, targetArray1, 0, sourceArray.length);
System.out.println(Arrays.toString(targetArray1)); // 输出 [1, 2, 3, 4, 5]
// 使用 Arrays.copyOf() 方法复制数组
int[] targetArray2 = Arrays.copyOf(sourceArray, sourceArray.length);
System.out.println(Arrays.toString(targetArray2)); // 输出 [1, 2, 3, 4, 5]
ArrayList经典Demo
package list;
import java.util.ArrayList;
import java.util.Iterator;
public class ArrayListDemo {
public static void main(String[] srgs){
ArrayList<Integer> arrayList = new ArrayList<Integer>();
System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size());
arrayList.add(1);
arrayList.add(3);
arrayList.add(5);
arrayList.add(7);
arrayList.add(9);
System.out.printf("After add:arrayList.size() = %d\n",arrayList.size());
System.out.println("Printing elements of arrayList");
// 三种遍历方式打印元素
// 第一种:通过迭代器遍历
System.out.print("通过迭代器遍历:");
Iterator<Integer> it = arrayList.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
// 第二种:通过索引值遍历
System.out.print("通过索引值遍历:");
for(int i = 0; i < arrayList.size(); i++){
System.out.print(arrayList.get(i) + " ");
}
System.out.println();
// 第三种:for循环遍历
System.out.print("for循环遍历:");
for(Integer number : arrayList){
System.out.print(number + " ");
}
// toArray用法
// 第一种方式(最常用)
Integer[] integer = arrayList.toArray(new Integer[0]);
// 第二种方式(容易理解)
Integer[] integer1 = new Integer[arrayList.size()];
arrayList.toArray(integer1);
// 抛出异常,java不支持向下转型
//Integer[] integer2 = new Integer[arrayList.size()];
//integer2 = arrayList.toArray();
System.out.println();
// 在指定位置添加元素
arrayList.add(2,2);
// 删除指定位置上的元素
arrayList.remove(2);
// 删除指定元素
arrayList.remove((Object)3);
// 判断arrayList是否包含5
System.out.println("ArrayList contains 5 is: " + arrayList.contains(5));
// 清空ArrayList
arrayList.clear();
// 判断ArrayList是否为空
System.out.println("ArrayList is empty: " + arrayList.isEmpty());
}
}
ArrayList常见问题
ArrayList频繁扩容导致添加性能急剧下降,如何处理?
如果在容量大概确定的情况下,可以在创建ArrayList对象的时候就指定一个大致的初始容量,减少扩容的次数
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
// 不指定初始容量 新增10w条数据
List<String> list = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.add(i + "");
}
long endTime = System.currentTimeMillis();
System.out.println("未指定容量: " + (endTime - startTime)); // 22
// 指定初始容量 新增10w条数据
List<String> list1 = new ArrayList<>(100000);
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list1.add(i + "");
}
endTime = System.currentTimeMillis();
System.out.println("指定容量: " + (endTime - startTime)); // 10
}
}
注意:这种优化方式只针对特定的场景,如果添加的元素是少量的、未知的,不推荐使用
ArrayList插入或删除元素一定比LinkedList慢么?
不一定,如果ArrayList每次插入或删除的元素是集合最后一个元素,则ArrayList要比LinkedList要比LinkedList要快。
但是一般情况下ArrayList插入或删除元素比LinkedList要慢,这是因为数组插入或删除元素后,需要新建数组和数据拷贝;而LinkedList只需要修改指针的引用即可。
示例:分别使用ArrayList和LinkedList按顺序插入50w条数据
import java.util.ArrayList;
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
// 测试 ArrayList 插入和删除元素的耗时
ArrayList<String> arrayList = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 5000000; i++) {
arrayList.add(i+"XXX");
}
long endTime = System.currentTimeMillis();
System.out.println("ArrayList按顺序插入500w条数据: "+(endTime-startTime)); // 3740
startTime = System.currentTimeMillis();
arrayList.remove("5000XXX");
endTime = System.currentTimeMillis();
System.out.println("ArrayList集合删除元素的时间: "+(endTime-startTime)); // 3
// 测试 LinkedList 插入和删除元素的耗时
LinkedList<String> linkedList = new LinkedList<>();
startTime = System.currentTimeMillis();
for (int i = 0; i < 5000000; i++) {
linkedList.add(i+"XXX");
}
endTime = System.currentTimeMillis();
System.out.println("LinkedList按顺序插入500w条数据: "+(endTime-startTime)); // 2646
startTime = System.currentTimeMillis();
linkedList.remove("5000XXX");
endTime = System.currentTimeMillis();
System.out.println("LinkedList集合删除元素的时间: "+(endTime-startTime)); // 0
}
}
可以看到,新增LinkedList要比ArrayList要快,删除LinkedList也比ArrayList也要快,删除的元素是中间的,这个LinkedList要快没什么争议,但是为什么按照顺序添加为什么还是LinkedList要快,这是什么原因?这是由于ArrayList没有指定初始容量大小,导致在新增的过程中进行了多次扩容,从而降低了效率
解决措施:在创建ArrayList的时候指定初始容量
现在我们为ArrayList指定初始容量为 5000000 ,然后再来测试,可以看到此时ArrayList新增耗时 2998ms,而 LinkedList 耗时2509 ms,为什么还是 LinkedList 要快?其实这个已经相差不大了
ArrayList是线程安全的么?
ArrayList是线程不安全的。如果想要线程安全该怎么办?
- 通过
Collections
工具类把List变成一个线程安全的集合- 使用线程安全类
Vector
- 使用
CopyOnWriteArrayList
示例:
import java.util.ArrayList;
import java.util.List;
class Task implements Runnable {
private List<String> list;
public Task(List<String> list) {
this.list = list;
}
@Override
public void run() {
// 每次执行任务前都休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
Task task = new Task(list);
// 开启50个线程
for (int i = 0; i < 50; i++) {
new Thread(task).start();
}
Thread.sleep(1000);
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("集合长度: " + list.size());
}
}
通过Collections.synchronizedList()
方法将 ArrayList
转成SynchronizedRandomAccessList
List<String> list = new ArrayList<>();
list = Collections.synchronizedList(list);
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
如何复制某个ArrayList到另一个ArrayList中去?
- 使用
clone
方法- 使用
ArrayList
构造方法- 使用
addAll
方法- 使用
System.arraycopy()
已知成员变量集合存储N多用户名称,在多线程的环境下,使用迭代器在读 取集合数据的同时如何保证还可以正常的写入数据到集合?
使用 读写分离集合
CopyOnWriteArrayList
。CopyOnWriteArrayList开始迭代后,就会以最初始的集合进行遍历,如果中间发生了修改操作,会创建应该副本,遍历中间的所有修改操作都发生在这个副本上,等到遍历完毕后再将原来的数组引用指向这个副本。
import java.util.ArrayList;
class Task implements Runnable {
private static List<String> list = new ArrayList<>();
static {
list.add("Jack");
list.add("Lucy");
list.add("Jimmy");
}
@Override
public void run() {
for (String value : list) {
System.out.println(value);
// 在读取数据的同时又向集合写入数据
list.add("coco");
}
}
}
public class Test {
public static void main(String[] args) {
// 创建线程任务
Task task = new Task();
// 开启10条线程
for (int i = 0; i < 10; i++) {
new Thread(task).start();
}
}
}
private static List<String> list = new CopyOnWriteArrayList<>();
ArrayList和LinkedList的区别有哪些?
- ArrayList
- 基于动态数组的数据结构
- 对于随机访问的 get 和set,ArrayList要优于LinkedList
- 对于随机操作的add和remove,ArrayList不一定比LinkedList慢(ArrayList底层由于是动态数组,因此并不是每次add和removel的时候都需要创建新数组)
- LinkedList
- 基于链表的数据结构
- 对于l顺序操作,LinkedList不一定比ArrayList慢
- 对于随机操作,LinkedList效率明显较低