Java ArrayList
从名字就可以看得出来,ArrayList 实现了 List 接口,并且是基于数组实现的。
有人就会问了 那ArrayList和数组有什么区别呢
数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。
初始化ArrayList
java
ArrayList<String> alist = new ArrayList<String>();
这就是一个标准的初始化 当然 也有简化版本 比如
java
List<String> alist = new ArrayList<>();
由于 ArrayList 实现了 List 接口,所以 alist 变量的类型可以是 List 类型;new 关键字声明后的尖括号中可以不再指定元素的类型,因为编译器可以通过前面尖括号中的类型进行智能推断。
如果非常确定 ArrayList 中元素的个数,在创建的时候还可以指定初始大小。
java
List<String> alist = new ArrayList<>(20);
这样做的好处是,可以有效地避免在添加新的元素时进行不必要的扩容。
2. add()添加元素
java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e; // 将元素 e 添加到数组中,并更新 size
return true;
}
2.1 ensureCapacityInternal(size + 1)
这个方法用于确保 ArrayList
在添加新元素时有足够的容量。size + 1
表示在现有的元素基础上,还要为新元素腾出空间。
java
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 首先,
ensureCapacityInternal
方法检查elementData
是否是默认的空数组。如果是空数组(即DEFAULTCAPACITY_EMPTY_ELEMENTDATA
),它将把最小容量设置为DEFAULT_CAPACITY
(通常为 10),以确保最小的容量。 - 然后,判断
minCapacity
(所需的最小容量)是否大于当前elementData
的长度。如果是,就调用grow(minCapacity)
来扩展数组。
2.2 grow(minCapacity)
当容量不足时,ArrayList
会调用 grow
方法来增加数组的容量。grow
方法的源码如下:
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 增加 50% 的容量
// 如果扩展后的容量小于所需的最小容量,直接设置为所需容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity > MAX_ARRAY_SIZE)
newCapacity = hugeCapacity(minCapacity);
// 扩展数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow
方法通过增加现有数组容量的 50% 来扩展数组。如果扩展后的容量不够minCapacity
,则直接将容量设置为minCapacity
。- 如果扩展后的容量大于
MAX_ARRAY_SIZE
,grow
会使用hugeCapacity
方法来返回一个合适的容量。 - 最后,使用
Arrays.copyOf
方法将旧数组的内容复制到新的扩展数组中。
2.3 elementData[size++] = e
这行代码将新元素 e
添加到 ArrayList
中,并将 size
增加 1。size
是 ArrayList
中元素的当前数量,elementData
是存储元素的数组。
- 通过
size
作为索引,将新元素e
存储在elementData
数组中。 size++
是自增操作,表示在将元素添加到数组之后,size
变量增加 1。
java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e; // 将元素 e 添加到数组中,并更新 size
return true;
}
2.1 ensureCapacityInternal(size + 1)
这个方法用于确保 ArrayList
在添加新元素时有足够的容量。size + 1
表示在现有的元素基础上,还要为新元素腾出空间。
java
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 首先,
ensureCapacityInternal
方法检查elementData
是否是默认的空数组。如果是空数组(即DEFAULTCAPACITY_EMPTY_ELEMENTDATA
),它将把最小容量设置为DEFAULT_CAPACITY
(通常为 10),以确保最小的容量。 - 然后,判断
minCapacity
(所需的最小容量)是否大于当前elementData
的长度。如果是,就调用grow(minCapacity)
来扩展数组。
2.2 grow(minCapacity)
当容量不足时,ArrayList
会调用 grow
方法来增加数组的容量。grow
方法的源码如下:
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 增加 50% 的容量
// 如果扩展后的容量小于所需的最小容量,直接设置为所需容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity > MAX_ARRAY_SIZE)
newCapacity = hugeCapacity(minCapacity);
// 扩展数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow
方法通过增加现有数组容量的 50% 来扩展数组。如果扩展后的容量不够minCapacity
,则直接将容量设置为minCapacity
。- 如果扩展后的容量大于
MAX_ARRAY_SIZE
,grow
会使用hugeCapacity
方法来返回一个合适的容量。 - 最后,使用
Arrays.copyOf
方法将旧数组的内容复制到新的扩展数组中。
2.3 elementData[size++] = e
这行代码将新元素 e
添加到 ArrayList
中,并将 size
增加 1。size
是 ArrayList
中元素的当前数量,elementData
是存储元素的数组。
- 通过
size
作为索引,将新元素e
存储在elementData
数组中。 size++
是自增操作,表示在将元素添加到数组之后,size
变量增加 1。
3. add(int index, E element)
向指定位置插入元素 源码如下
java
/**
* 在指定位置插入一个元素。
*
* @param index 要插入元素的位置
* @param element 要插入的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public void add(int index, E element) {
rangeCheckForAdd(index); // 检查索引是否越界
ensureCapacityInternal(size + 1); // 确保容量足够,如果需要扩容就扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 将 index 及其后面的元素向后移动一位
elementData[index] = element; // 将元素插入到指定位置
size++; // 元素个数加一
}
其中有一个方法 arraycopy 我们具体讲讲这个函数的应用
在 ArrayList.add(int index, E element)
方法中,具体用法如下:
java
System.arraycopy(elementData, index, elementData, index + 1, size - index);
- elementData:表示要复制的源数组,即 ArrayList 中的元素数组。
- index:表示源数组中要复制的起始位置,即需要将 index 及其后面的元素向后移动一位。
- elementData:表示要复制到的目标数组,即 ArrayList 中的元素数组。
- index + 1:表示目标数组中复制的起始位置,即将 index 及其后面的元素向后移动一位后,应该插入到的位置。
- size - index:表示要复制的元素个数,即需要将 index 及其后面的元素向后移动一位,需要移动的元素个数为 size - index。
4. 更新元素 set(索引,更新值)
与数组无差别 源码也非常简单 我就不细讲了
判断索引越界+查找索引元素+替换索引+返回arraylist
5. 删除元素
remove()两种实现方式 第一种是根据索引删除 第二种是根据所需要的值来删除
先来看 remove(int index)
方法的源码:
java
/**
* 删除指定位置的元素。
*
* @param index 要删除的元素的索引
* @return 先前在指定位置的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public E remove(int index) {
rangeCheck(index); // 检查索引是否越界
E oldValue = elementData(index); // 获取要删除的元素
int numMoved = size - index - 1; // 计算需要移动的元素个数
if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间
return oldValue; // 返回被删除的元素
}
需要注意的是,在 ArrayList 中,删除元素时,需要将删除位置后面的元素向前移动一位,以填补删除位置留下的空缺。如果需要移动元素,则需要使用 System.arraycopy 方法将删除位置后面的元素向前移动一位。最后,将数组末尾的元素置为 null,以便让垃圾回收机制回收该元素占用的空间
再来看 remove(Object o)
方法的源码:
java
/**
* 删除列表中第一次出现的指定元素(如果存在)。
*
* @param o 要删除的元素
* @return 如果列表包含指定元素,则返回 true;否则返回 false
*/
public boolean remove(Object o) {
if (o == null) { // 如果要删除的元素是 null
for (int index = 0; index < size; index++) // 遍历列表
if (elementData[index] == null) { // 如果找到了 null 元素
fastRemove(index); // 调用 fastRemove 方法快速删除元素
return true; // 返回 true,表示成功删除元素
}
} else { // 如果要删除的元素不是 null
for (int index = 0; index < size; index++) // 遍历列表
if (o.equals(elementData[index])) { // 如果找到了要删除的元素
fastRemove(index); // 调用 fastRemove 方法快速删除元素
return true; // 返回 true,表示成功删除元素
}
}
return false; // 如果找不到要删除的元素,则返回 false
}
注意到 在删除字符串时 我们区分了null与其他 原因是什么呢
这里我们讲一下equals and ==的差别
== 和 equals 是 Java 中两种不同的比较方式:
== 运算符
- 对于基本数据类型(int, char, boolean等),== 比较的是值是否相等
- 对于引用类型(如String, Integer等对象),== 比较的是引用地址是否相同(即是否指向同一个对象)
equals() 方法
- equals() 是Object类的方法,默认实现与 == 相同
- 很多类(如String, Integer等)重写了equals()方法,用于比较对象的内容是否相同
- 使用equals()方法时要注意防止空指针异常,这就是为什么ArrayList的remove方法要单独处理null的情况
java
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2); // 输出false,因为是不同对象
System.out.println(str1.equals(str2)); // 输出true,因为内容相同
这就解释了为什么在ArrayList的remove方法中,对null值要特殊处理:因为不能对null调用equals()方法(会抛出NullPointerException),所以必须使用==来判断。
来看一下 fastRemove()
方法:
java
/**
* 快速删除指定位置的元素。
*
* @param index 要删除的元素的索引
*/
private void fastRemove(int index) {
int numMoved = size - index - 1; // 计算需要移动的元素个数
if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间
}
同样是调用 System.arraycopy()
方法对数组进行复制和移动。