大话ArrayList

153 阅读12分钟

1.面试经

ArrayList 可以说是我们经常用到的数据结构类了。那么面试会有哪些考点呢?你能回答这些问题吗?

  1. 基本概念:ArrayList 是什么,它是如何实现的,以及它与其他集合(例如 LinkedList 和 Vector)的区别。
  2. 底层数据结构:ArrayList 内部是如何实现存储元素的,它使用了什么样的数据结构。
  3. 初始容量:ArrayList 的初始容量是多少,如何设置,以及影响它的因素。
  4. 扩容机制:ArrayList 在添加元素时如何扩容,以及扩容后如何影响效率。
  5. 效率:ArrayList 的插入、删除和查询操作的效率,以及如何通过调整容量或者使用其他集合类来优化效率。
  6. 其他方法:ArrayList 提供了哪些常用方法,它们的作用和用法。
  7. 线程安全:ArrayList 是否是线程安全的,如果不是,如何保证线程安全。
  8. 使用场景:ArrayList 适用于什么样的场景,它有什么优势和劣势。
  9. Java 8 特性:ArrayList 在 Java 8 中有哪些新特性,例如并行流、Lambda 表达式等。
  10. 泛型:ArrayList 如何使用泛型以避免类型转换错误。
  11. 序列化:ArrayList 如何实现序列化,以及如何保存和读取 ArrayList 中的数据。
  12. 其他应用:ArrayList 可以用来实现栈、队列等数据结构吗?
  13. 性能:ArrayList 和其他集合类的性能比较,以及在什么情况下选择 ArrayList。
  14. 迭代器:ArrayList 可以使用迭代器进行遍历数据吗,以及迭代器的特点。

如果你能全部回答,那么恭喜你你对ArrayList已经掌握的非常扎实了。如果还有回答不上来的,那么跟着啊Q一起复习一下吧。

ArrayList

ArrayList的位置

首先我们今天的主角是:java.util.ArrayList ,并不是 java.util.Arrays.ArrayList

通过上面的类图我们用一句话描述一下ArrayList:ArrayList是一个可迭代,可拷贝,可以被序列化的一个动态数组。

ArrayList的组成成分

ArrayList 组成成分很好理解,一个元素数组elementData和用于存放元素和一个数组大小size

transient Object[] elementData;

private int size;

  • transientelementData使用了transient标记,这是为什么呢?
    • 是因为elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
  • 为什么没有使用private修饰elementData呢?
    • 是为了简化内部类的访问,提高程序的可读性和编写效率。 在 Java 中,内部类可以访问外部类的所有成员变量,但是默认情况下内部类无法访问外部类的私有成员变量。因此,如果外部类的某个成员变量需要在内部类中使用,那么外部类必须把这个成员变量声明为非私有的,这样内部类就可以直接访问。
  • 为什么 Object[] elementData 而不是 E[] elementData
    • ArrayList在 Java 1.2 时首次发布,当时并没有泛型。所以为了向后兼容性,在 Java 1.5 中引入泛型时,仍然使用了原来的数组类型 Object[]。因为 Object[] 数组可以存储任何类型的对象,所以泛型类型 E 也可以在该数组中使用。所以 ArrayList 声明的数组类型为 Object[],而不是泛型类型 E<>

ArrayList的创建

ArrayList提供了三种构造方法:

  • public ArrayList(int initialCapacity)
public ArrayList(int initialCapacity) {
    // 如果 初始化容量大于0,则初始化当前大小的数组对象。
    if (initialCapacity > 0) {
    this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { 
    // 如果 初始化容量==0,则初始化一个默认的空数组对象
    this.elementData = EMPTY_ELEMENTDATA;
} else {
    throw new IllegalArgumentException("Illegal Capacity: "+
                                       initialCapacity);
}
}
  • public ArrayList(){this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}
    • 这里初始化的是默认容量为10的数组大小,可以看到,ArrayList提供了 EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 两个空数组对象。EMPTY_ELEMENTDATA 可以理解为数组大小为0的数组,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 将在后续 add()方法中被扩容到10。这里又有两个知识点:1.当我们初始化initialCapacity=0时,ArrayList认为我们使用0是有用意的,所以特意使用了EMPTY_ELEMENTDATA。 2.使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 将延迟扩容。
  • public ArrayList(Collection<? extends E> c)

ArrayList的方法

public boolean add(E e)队尾添加元素。涉及到扩容。
public void add(int index, E element)将一个元素插入指定的位置中,其位置本身元素和右边元素皆要往右移动一位。涉及到index的边界检测,数组扩容和数组拷贝。
public boolean addAll(Collection<? extends E> c)将一个集合的数组拷贝到该ArrayList中
public boolean addAll(int index, Collection<? extends E> c)从指定位置开始插入集合数据。原有索引位置右边所有数据右移。
public E get(int index)获取索引位置的元素
public E set(int index, E element)指定位置的元素替换
public void clear()清除数据
public Object clone()数组拷贝 。注意:该拷贝对于数组元素来说是浅拷贝(不会复制元素本身),对于元素属性的操作将会作用于两个列表上。
public boolean contains(Object o)当且仅当此列表包含至少一个元素 e 时返回 true
public Object[] toArray()返回一个对象数组。
public E remove(int index)删除指定位置元数,并左移其后续元素。

面试宝典

  1. 基本概念:ArrayList 是什么,它是如何实现的,以及它与其他集合(例如 LinkedList 和 Vector)的区别。

ArrayList 是 Java 中的一种动态数组,允许用户在列表的末尾添加,插入,删除元素,并可以随时调整其大小。它是通过使用数组实现的,并具有动态扩展数组的能力。当列表中的元素数量超过数组的大小时,ArrayList 会自动扩容,以便容纳更多元素。

与 ArrayList 相比,LinkedList 是一种双向链表,允许在列表中任意位置插入和删除元素。它在动态添加和删除元素方面比 ArrayList 更快,但在随机访问元素方面比 ArrayList 慢。

Vector 是一种线程安全的动态数组,允许用户在列表的末尾添加,插入,删除元素,并可以随时调整其大小。它与 ArrayList 类似,但是 Vector 具有线程安全性,因此可以在多线程环境中使用。但是,由于同步机制的开销,Vector 的性能略低于 ArrayList。

因此,ArrayList 是一种适合在单线程环境中快速添加和删除元素,随机访问元素的集合,而 LinkedList 更适合快速在列表中插入和删除元素,而 Vector 适合在多线程环境中使用的集合。

  1. 底层数据结构:ArrayList 内部是如何实现存储元素的,它使用了什么样的数据结构。

ArrayList 内部是使用数组存储元素的。当创建 ArrayList 时,会创建一个数组,并在需要时通过扩容来更新数组的大小。

当向 ArrayList 中添加一个元素时,会将元素放入数组中相应的位置。当 ArrayList 中的元素数量增加时,如果数组的大小不够,则需要扩容,并创建一个新的更大的数组,并将原始数组中的元素复制到新数组中。ArrayList 的优点是插入和删除操作的时间复杂度为 O(n),但因为使用数组实现,随机访问操作的时间复杂度为 O(1),而 LinkedList 则相反。

与 ArrayList 相比,Vector 使用的是同步数组,因此在多线程环境中是线程安全的,而 ArrayList 则是非线程安全的。LinkedList 使用链表存储元素,因此它比 ArrayList 更加适合用于删除和插入操作,但随机访问的时间复杂度为 O(n)。

  1. 初始容量:ArrayList 的初始容量是多少,如何设置,以及影响它的因素。

ArrayList 的默认初始容量为 10。您可以通过在构造函数中指定容量来设置初始容量。例如:

ArrayList<Integer> list = new ArrayList<>(20);

这将创建一个初始容量为 20 的 ArrayList。

实际初始容量会受到许多因素的影响,包括预期数据大小,程序运行内存限制等。如果您知道 ArrayList 中大致需要存储的数据数量,那么指定初始容量是一种有效的内存优化方法,因为这可以避免在程序运行过程中的多次扩容,从而减少内存开销。

  1. 扩容机制:ArrayList 在添加元素时如何扩容,以及扩容后如何影响效率。

ArrayList 在添加元素时如果发现当前数组长度不够用,就会触发扩容操作。扩容是通过创建一个新的数组并将原来的数组元素复制到新数组中来实现的。默认情况下,每次扩容数组的大小将增加原来的一半(即:原来的数组长度 * 1.5)。

当 ArrayList 触发扩容操作时,效率会受到影响。因为它需要复制元素并重新分配内存。扩容操作会影响 ArrayList 的插入性能,但是在元素数量较少时对查询效率的影响较小。如果您知道需要存储的元素数量,那么通过指定足够的初始容量可以减少扩容的次数,从而最大程度地保持效率。

  1. 效率:ArrayList 的插入、删除和查询操作的效率,以及如何通过调整容量或者使用其他集合类来优化效率。

ArrayList 的插入操作是在数组末尾添加元素,复杂度为 O(1),但是如果需要扩容数组,则复杂度为 O(n)。删除操作需要移动数组中剩余元素,复杂度为 O(n)。查询操作根据索引复杂度为 O(1)。

为了优化效率,可以通过预先设置更大的容量来减少扩容的频率。如果有大量的插入和删除操作,可以考虑使用 LinkedList,因为它的插入和删除操作的复杂度为 O(1)。如果需要高效的随机访问和插入/删除操作,可以使用 ArrayDeque。

  1. 其他方法:ArrayList 提供了哪些常用方法,它们的作用和用法。
  1. add(E element):向 ArrayList 末尾添加元素。
  2. add(int index, E element):在指定的索引处插入元素。
  3. remove(int index):删除指定索引处的元素。
  4. get(int index):返回指定索引处的元素。
  5. size():返回 ArrayList 中元素的数量。
  6. clear():清空 ArrayList 中的所有元素。
  7. contains(Object o):如果 ArrayList 包含指定元素,则返回 true。
  8. indexOf(Object o):返回指定元素的第一次出现的索引,如果不存在,则返回 -1。
  9. isEmpty():如果 ArrayList 为空,则返回 true。
  10. set(int index, E element):用指定的元素替换指定索引处的元素。
  1. 线程安全:ArrayList 是否是线程安全的,如果不是,如何保证线程安全。

ArrayList 本身是不是线程安全的,如果多个线程对同一个 ArrayList 对象进行操作时可能会出现线程安全问题。要保证线程安全,可以使用如下几种方法:

  1. 使用 Collections.synchronizedList() 方法,它可以返回一个线程安全的 ArrayList。
  2. 使用 Java5 新增的 java.util.concurrent 包下的 java.util.concurrent.CopyOnWriteArrayList。
  3. 使用 synchronized 关键字,对 ArrayList 进行加锁,保证线程安全。
  1. 使用场景:ArrayList 适用于什么样的场景,它有什么优势和劣势。

ArrayList 适用于需要快速随机访问的数据的存储和操作的场景。它具有快速查询、插入和删除操作的优势,时间复杂度为 O(1)。在动态数组的存储和操作中,ArrayList 是一种非常方便和常用的选择。

但是,ArrayList 的劣势在于在从中间插入或删除元素时,其复杂度为 O(n),在这种情况下,可以考虑使用 LinkedList。另外,ArrayList 不是线程安全的,如果需要线程安全,可以使用 Collections.synchronizedList() 方法或使用 CopyOnWriteArrayList。

  1. Java 8 特性:ArrayList 在 Java 8 中有哪些新特性,例如并行流、Lambda 表达式等。

Java 8 中的 ArrayList 新特性,例如并行流和 Lambda 表达式,使得 ArrayList 的操作更加灵活和高效。

并行流可以利用多核 CPU 的优势,在大规模数据处理中大幅提升性能。例如,可以通过使用 ArrayList 的 parallelStream() 方法获得一个并行流,从而在多线程环境下对 ArrayList 中的元素进行处理。

Lambda 表达式是一种函数式编程的重要工具,可以使代码更加简洁和可读。在 Java 8 中,ArrayList 支持通过 Lambda 表达式对元素进行过滤、排序、聚合等操作,大大提高了代码的可用性。

总体而言,Java 8 中的 ArrayList 新特性可以帮助我们编写更高效、更灵活、更简洁的代码,增强了 ArrayList 的使用价值。

  1. 泛型:ArrayList 如何使用泛型以避免类型转换错误。

ArrayList 是 Java 中的一种常用集合类,它可以使用泛型来声明存储的数据类型,从而避免类型转换错误。

使用泛型时,需要在创建 ArrayList 对象时指定数据类型,例如:

ArrayList<String> list = new ArrayList<String>();

这样可以保证存储在 ArrayList 中的元素都是指定类型的,如果试图插入不符合类型的元素,编译器会报错,避免了在运行时发生错误。

在 Java 7 以前,ArrayList 不支持泛型,必须使用 Object 类型作为元素类型,然后再进行类型转换。

因此,使用泛型可以提高代码的可读性和可维护性,增加程序的安全性。

  1. 序列化:ArrayList 如何实现序列化,以及如何保存和读取 ArrayList 中的数据。

ArrayList 通过实现 java.io.Serializable 接口来实现序列化。实现序列化后,可以使用 ObjectOutputStream 和 ObjectInputStream 将 ArrayList 中的数据保存到磁盘文件中,并在以后读取该数据。

下面是一个示例,该示例显示了如何实现 ArrayList 的序列化:

import java.io.*;
import java.util.*;

public class SerializeArrayList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Hello");
        list.add("World");
        list.add("!!!");

        try (FileOutputStream fos = new FileOutputStream("/Users/list");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(list);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (FileInputStream fis = new FileInputStream("/Users/list");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            List<String> readList = (List<String>) ois.readObject();
            System.out.println(readList);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在此示例中,我们首先创建了一个 ArrayList,并将一些字符串元素添加到该列表中。然后,使用 ObjectOutputStream 将 ArrayList 序列化为磁盘文件。最后,使用 ObjectInputStream 从磁盘文件读取该列表。

  1. 其他应用:ArrayList 可以用来实现栈、队列等数据结构吗?。

ArrayList 可以用来实现一些基本的数据结构,例如栈和队列。

如果要实现栈,可以使用 ArrayList 的 add() 方法在列表的末尾添加元素,并使用 remove() 方法从列表的末尾删除元素,从而模拟栈的后进先出(LIFO)特性。

如果要实现队列,可以使用 add() 方法在列表的末尾添加元素,并使用 remove() 方法从列表的开头删除元素,从而模拟队列的先进先出(FIFO)特性。

  1. 性能:ArrayList 和其他集合类的性能比较,以及在什么情况下选择 ArrayList。
  • ArrayList 的随机访问速度比较快,因为它使用连续的数组来存储元素。因此,在快速读取单个元素或对整个列表进行遍历的情况下,ArrayList 更加高效。
  • 在从列表的中间位置插入或删除元素时,LinkedList 的效率更高,因为它不需要移动整个数组。
  • Vector 是线程安全的,因此在多线程环境下使用时很安全。然而,它的性能略低于 ArrayList,因为需要加锁来保证线程安全。

因此,选择 ArrayList 还是其他集合类,取决于你的需求。如果你需要快速随机访问和遍历元素,或者不在乎线程安全性,那么 ArrayList 是一个不错的选择。如果需要在列表的中间位置插入或删除元素,或者需要在多线程环境下使用,那么 LinkedList 和 Vector 可能是更好的选择。

  1. 迭代器:ArrayList 可以使用迭代器进行遍历数据吗,以及迭代器的特点。

ArrayList 可以使用迭代器进行遍历。

迭代器有以下特点:

  • 迭代器是对集合数据结构的一种封装,它隐藏了集合的内部实现,提供了统一的遍历方法。
  • 迭代器支持一次性遍历,遍历结束后不能再次遍历。
  • 迭代器在遍历过程中只能向后移动,不能向前移动。
  • 迭代器遍历 ArrayList 时不能对 ArrayList 的内容进行修改,可以使用迭代器进行元素的删除。
ArrayList<Integer> array = new ArrayList<>();
for (int i = 6; i > 0; i--) {
    array.add(i);
}
Iterator<Integer> iterator = array.iterator();
while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next == 5) iterator.remove();
    //if (next == 5) array.remove(5); //ConcurrentModificationException异常
}