【JAVA】 ArrayList 详细介绍(附带面试题)

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 = 1trimToSize() 之前,容量仍然保持不变。
调用 trimToSize() 后,容量会缩小以匹配当前的 size。

总结

删除元素后: ArrayList 的 size 会减少,但底层数组的容量不会立即减少。
手动缩容: 可以通过调用 trimToSize() 来手动缩小数组的容量,以匹配实际元素个数。

  • 41
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值