ArrayList
1. ArrayList 的基本特点
- 动态数组: ArrayList 使用动态数组来存储元素。它的大小可以根据元素的增加或减少动态调整,初始大小通常是10,当存储的元素超过当前容量时,ArrayList 会自动扩展其容量。
- 随机访问: 由于底层是数组结构,ArrayList 提供快速的随机访问,可以通过索引快速访问元素。
- 非同步: ArrayList 不是线程安全的,如果在多线程环境中使用,需要手动同步(可以使用 Collections.synchronizedList 方法来包装 ArrayList)。
- 允许null元素: ArrayList 允许在列表中存储 null 值。
2. ArrayList 的构造方法
ArrayList 提供了几个构造方法,用于创建不同类型的 ArrayList 实例:
- ArrayList(): 创建一个默认容量为10的空列表。
- ArrayList(int initialCapacity): 创建一个具有指定初始容量的空列表。
- ArrayList(Collection<? extends E> c): 创建一个包含指定集合的元素的列表,这些元素按照集合的迭代器返回的顺序排列。
3. ArrayList 的常用方法
ArrayList 继承了 List 接口的大多数方法,并提供了一些特有的实现。以下是一些常用方法:
- add(E e): 将指定的元素添加到列表的末尾。
- add(int index, E element): 在指定位置插入元素,后面的元素依次后移。
- get(int index): 返回指定位置的元素。
- set(int index, E element): 用指定元素替换指定位置的元素。
- remove(int index): 移除指定位置的元素,后面的元素依次前移。
- remove(Object o): 移除列表中第一次出现的指定元素。
- size(): 返回列表中的元素数量。
- clear(): 移除列表中的所有元素。
- isEmpty(): 检查列表是否为空。
- contains(Object o): 判断列表是否包含指定的元素。
- indexOf(Object o): 返回列表中第一次出现的指定元素的索引。
- lastIndexOf(Object o): 返回列表中最后一次出现的指定元素的索引。
- toArray(): 将列表中的元素转换为数组。
4. 内部工作原理
ArrayList 的核心是一个动态数组,内部通过 Object[] 数组来存储元素。当向 ArrayList 中添加新元素时,如果数组已满,ArrayList 会创建一个更大的新数组(通常是原数组的1.5倍),然后将旧数组中的元素复制到新数组中。这个过程被称为 动态扩展,它确保了 ArrayList 可以容纳更多的元素。
内部扩展机制
- 默认情况下,ArrayList 的初始容量为10。
- 当 ArrayList 容量不足时,会将当前容量增加为原来的1.5倍(具体为 newCapacity = oldCapacity + (oldCapacity >> 1))。
5. ArrayList 的性能
- 随机访问性能: 由于底层是数组结构,通过索引访问元素的时间复杂度为 O(1),这是 ArrayList 的一个显著优点。
- 插入和删除性能: 在列表的末尾插入或删除元素的时间复杂度为 O(1),但在中间插入或删除元素时,可能需要移动大量元素,因此在最坏情况下,时间复杂度为 O(n)。
- 扩展成本: 扩展操作会涉及数组的重新分配和元素的复制,因此在扩展时会有性能开销。
6. ArrayList 的使用场景
ArrayList 非常适合以下场景:
- 频繁随机访问: 需要频繁通过索引访问元素的场景,如搜索、排序等。
- 插入或删除操作较少: 在列表的中间插入或删除元素较少的场景。
- 元素数量不确定: 当列表中的元素数量在运行时动态变化,并且不确定最终大小时,ArrayList 是一个理想的选择。
7. 示例代码
以下是一个 ArrayList 的简单示例,展示了如何使用 ArrayList 存储、访问和操作元素:
import java.util.ArrayList;
public class ArrayListExample {
public static void main(String[] args) {
// 创建一个ArrayList
ArrayList<String> list = new ArrayList<>();
// 添加元素
list.add("Apple");
list.add("Banana");
list.add("Orange");
// 插入元素
list.add(1, "Mango");
// 获取元素
String fruit = list.get(2);
System.out.println("The fruit at index 2 is: " + fruit);
// 修改元素
list.set(2, "Grapes");
// 移除元素
list.remove("Banana");
// 遍历列表
for (String item : list) {
System.out.println(item);
}
// 列表大小
System.out.println("The size of the list is: " + list.size());
// 清空列表
list.clear();
System.out.println("The list is empty: " + list.isEmpty());
}
}
8. ArrayList 与其他 List 实现的比较
- ArrayList vs LinkedList:
ArrayList 更适合随机访问(O(1)),而 LinkedList 更适合频繁的插入/删除操作(O(1))。
ArrayList 在内存中是连续存储的,LinkedList 是链式存储的。 - ArrayList vs Vector:
Vector 是线程安全的,而 ArrayList 不是。Vector 的所有方法都被同步过,但这也导致了性能开销。
总结
ArrayList 是一个非常灵活和高效的 List 实现,适用于大多数场景,尤其是那些需要频繁随机访问和动态调整大小的场景。然而,如果你的应用程序需要在多线程环境下使用,或者需要频繁在列表中间插入或删除元素,你可能需要考虑使用其他集合类型。
面试题
ArrayList 默认长度是多少?
默认长度为10
// 默认容量大小
private static final int DEFAULT_CAPACITY = 10;
new ArrayList()这时他的长度是多少?
// 无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
使用无参构造函数创建ArrayList,创建成功后长度为0,但是存放一个元素后长度为10.
介绍下ArrayList的扩容机制
ArrayList 的扩容机制是其重要的性能优化点,确保在动态增加元素时有足够的容量来存储新元素。ArrayList 通过一个内部的动态数组来管理元素,并在需要时自动扩展这个数组。
1. 扩容触发条件
ArrayList 会在添加元素时检查当前容量是否足够:
当你调用 add(E e) 方法时,ArrayList 会先检查当前内部数组的容量是否足够存储新元素。
如果当前容量不足以容纳新元素,ArrayList 会触发扩容。
2. 扩容大小计算
扩容的具体机制可以从 ArrayList 的源码中找到。具体来说,扩容逻辑在 ArrayList 类中的 grow(int minCapacity) 方法中实现。
扩容大小的计算逻辑如下:
private void grow(int minCapacity) {
// 获取旧的容量
int oldCapacity = elementData.length;
// 计算新容量为旧容量的1.5倍 (右移1位相当于除以2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量仍然小于需要的最小容量,则使用minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超出最大值,使用最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 扩容实际操作 - 创建新数组并将旧数组内容复制到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
解释:
- 旧容量 (oldCapacity): 这是当前内部数组的容量。
- 新容量 (newCapacity): 扩展后的容量为旧容量的 1.5 倍(具体为 oldCapacity + (oldCapacity >> 1)),这确保了 ArrayList 能够容纳更多的元素。
例如,如果当前容量是 10,则扩展后的容量为 15。 - 最小容量 (minCapacity): 如果计算的新容量仍然不足以满足所需的最小容量,ArrayList 会直接使用 minCapacity。
- 最大容量 (MAX_ARRAY_SIZE): 如果扩展后的容量超过了 ArrayList 的最大允许容量(Integer.MAX_VALUE - 8),会调整到更大的容量,但不会超过允许的最大值。
3. 扩容后的操作
扩容后的操作主要涉及数组的复制和更新:
- 创建新数组: 扩容时,ArrayList 会创建一个新的更大容量的数组。
- 复制元素: 然后,ArrayList 会使用 Arrays.copyOf 方法将旧数组中的元素复制到新数组中。
- 更新引用: 最后,ArrayList 会将 elementData 引用指向新的数组,旧数组会被垃圾回收器回收。
4. 扩容的影响
扩容是一个相对昂贵的操作,因为它涉及到数组的重新分配和元素的复制。因此,在实际开发中,如果可以预知 ArrayList 的大致大小,建议在创建 ArrayList 时指定容量,以减少扩容操作的次数,提高性能。
5. 示例代码
import java.util.ArrayList;
public class ArrayListExpansionExample {
public static void main(String[] args) {
// 创建一个容量为1的ArrayList
ArrayList<Integer> list = new ArrayList<>(1);
// 添加元素,触发扩容
list.add(1); // 容量1
list.add(2); // 扩容到1.5倍,容量2
list.add(3); // 再次扩容到1.5倍,容量3
list.add(4); // 再次扩容到1.5倍,容量4
}
}
在这个示例中,ArrayList 会在添加 2 和 4 时自动扩容。随着元素的增加,扩容操作会根据当前容量的 1.5 倍进行调整。
总结
ArrayList 的扩容机制确保了其动态数组的灵活性。扩容操作在插入新元素且容量不足时触发,通常会扩展为原容量的 1.5 倍。扩容后的数组会进行复制,并更新内部引用。为了优化性能,建议在可能的情况下预估 ArrayList 的大小以减少扩容频率。
ArrayList 中查找一个元素,时间复杂度是多少?
在 ArrayList 中查找一个元素的时间复杂度取决于你如何查找元素。下面是两种常见的查找方式及其对应的时间复杂度:
1. 通过索引查找(随机访问)
如果你要通过索引直接访问某个元素,例如使用 get(int index) 方法,时间复杂度是 O(1)。
- 解释: ArrayList 底层是一个数组,可以通过索引直接访问数组中的元素,因此查找的时间复杂度是常数时间 O(1)。
2. 通过元素值查找
如果你要通过元素的值查找,例如使用 contains(Object o)、indexOf(Object o) 或 lastIndexOf(Object o) 方法,时间复杂度是 O(n),其中 n 是 ArrayList 中的元素数量。
- 解释: 这些方法会从头到尾遍历整个列表,检查每个元素是否等于要查找的值(通过 equals 方法判断)。在最坏的情况下,需要遍历完整个列表才能找到元素或确认元素不存在,因此时间复杂度是线性时间 O(n)。
总结
随机访问(通过索引查找): 时间复杂度为 O(1)。
按值查找: 时间复杂度为 O(n)。
ArrayList 删除元素后,数组长度会减小吗?
在 ArrayList 中删除元素后,数组的长度不会立即减小,但 ArrayList 的实际大小 (size) 会减少。
详细解释
1. 内部数组的长度
- ArrayList 底层是一个数组 (elementData) 来存储元素。这个数组的容量(即数组的物理长度)是在 ArrayList 扩容时确定的,并不会因为元素的删除而自动减少。
- 删除元素后,数组中的对应位置会被设置为 null,但数组本身的长度(容量)不会变化。
2. ArrayList 的大小 (size)
- ArrayList 的 size 属性表示实际存储的元素个数。当删除元素时,size 会减少,但底层数组的容量保持不变。
- 例如,如果你有一个 ArrayList,它的 size 是 10,容量是 16(数组长度为 16),删除一个元素后,size 会变为 9,但数组的容量仍然是 16。
3. 是否会自动缩容
- ArrayList 不会自动缩小底层数组的容量。如果需要,可以手动调用 trimToSize() 方法,使 ArrayList 的容量与当前的 size 匹配,这会创建一个新的数组,大小正好等于 size,并将元素复制到这个新的数组中。
示例
ArrayList<String> list = new ArrayList<>(10);
list.add("a"); // size = 1, capacity = 10
list.add("b"); // size = 2, capacity = 10
list.remove("a"); // size = 1, capacity = 10
list.trimToSize(); // size = 1, capacity = 1
在 trimToSize() 之前,容量仍然保持不变。
调用 trimToSize() 后,容量会缩小以匹配当前的 size。
总结
删除元素后: ArrayList 的 size 会减少,但底层数组的容量不会立即减少。
手动缩容: 可以通过调用 trimToSize() 来手动缩小数组的容量,以匹配实际元素个数。