1. Java List集合刷题指南:从理论到实战的20道精华题
作为一名Java开发者,我深知集合框架是日常开发中最基础也最重要的部分。List作为最常用的集合类型之一,掌握它的各种特性和使用技巧对提升编码效率至关重要。今天,我将分享一套精心设计的20道List集合练习题,涵盖从基础概念到实际应用的各个方面。
2. List基础概念判断题解析
2.1 Vector的线程安全性
Vector是Java早期提供的集合类,它的所有方法都使用了synchronized关键字进行同步,因此是线程安全的。但在实际开发中,由于同步带来的性能开销,我们更推荐使用Collections.synchronizedList()包装ArrayList,或者使用CopyOnWriteArrayList这类并发集合。
注意:虽然Vector是线程安全的,但在高并发场景下性能较差,现代Java开发中已很少直接使用。
2.2 ArrayList的底层实现
ArrayList确实是基于数组实现的动态数组结构。它的扩容机制值得深入理解:当元素数量超过当前容量时,会创建一个新数组(默认是原大小的1.5倍),然后将旧数组元素复制到新数组中。这种机制使得ArrayList在随机访问时效率很高(O(1)时间复杂度),但在中间位置插入或删除元素时需要移动后续所有元素,效率较低(O(n)时间复杂度)。
2.3 List集合的特性
List接口的两个核心特性确实是有序和可重复。这里的"有序"指的是元素的插入顺序会被保留,而不是元素本身的排序。与Set集合不同,List允许存储相同的元素多次,这在某些业务场景下非常有用。
3. List选择题深度解析
3.1 List集合的实现类
List接口的主要实现类包括:
- ArrayList:基于动态数组,随机访问快
- LinkedList:基于双向链表,插入删除快
- Vector:线程安全的动态数组(已过时)
Set是与List并列的集合接口,不属于List体系。理解这一点对构建正确的集合类层次认知很重要。
3.2 ArrayList与LinkedList的核心区别
这两种List实现类的选择取决于具体的使用场景:
- 查询密集型操作:选择ArrayList,因为它的get(index)操作是O(1)时间复杂度
- 增删密集型操作:选择LinkedList,因为它的add/remove操作在已知位置时是O(1)时间复杂度
在实际项目中,我通常会根据业务场景的数据操作特点来选择合适的实现。例如,对于需要频繁随机访问的配置项列表,使用ArrayList;对于需要频繁在头部/中间插入删除的消息队列,使用LinkedList。
3.3 LinkedList的多接口实现
LinkedList不仅实现了List接口,还实现了Deque接口,这意味着它可以作为:
- 普通列表使用
- 队列(Queue)使用
- 双端队列(Deque)使用
这种灵活性使得LinkedList在某些特定场景下非常有用,比如实现LRU缓存时。
4. 代码阅读题实战分析
4.1 ArrayList的add与set方法区别
java复制List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add(1, "C"); // 在索引1处插入"C",原"B"后移
list.set(0, "D"); // 替换索引0处的元素
关键区别:
- add(int index, E element):插入操作,会导致后续元素后移,列表长度增加
- set(int index, E element):替换操作,不改变列表长度
在实际编码中,混淆这两个方法是常见错误,特别是在循环中修改列表时。
5. 编程题详解与最佳实践
5.1 ArrayList综合操作题
java复制// 生成随机数列表
List<Integer> numbers = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 20; i++) {
numbers.add(random.nextInt(100));
}
// 计算偶数和
int evenSum = 0;
for (int num : numbers) {
if (num % 2 == 0) {
evenSum += num;
}
}
// 替换大于等于80的数字
for (int i = 0; i < numbers.size(); i++) {
if (numbers.get(i) >= 80) {
numbers.set(i, 79);
}
}
// 安全删除小于10的元素(倒序遍历)
for (int i = numbers.size() - 1; i >= 0; i--) {
if (numbers.get(i) < 10) {
numbers.remove(i);
}
}
重要技巧:删除元素时使用倒序遍历可以避免下标错位问题,这是实际开发中常见的陷阱。
5.2 LinkedList综合操作与异常处理
java复制Scanner scanner = new Scanner(System.in);
List<Integer> list = new LinkedList<>();
for (int i = 0; i < 10; ) {
try {
System.out.print("请输入第" + (i + 1) + "个整数: ");
int num = scanner.nextInt();
list.add(num);
i++;
} catch (InputMismatchException e) {
System.out.println("输入错误,请重新输入整数!");
scanner.next(); // 清除错误的输入
}
}
// 计算平均值
double average = list.stream().mapToInt(Integer::intValue).average().orElse(0);
// 查找最大最小值
int max = Collections.max(list);
int min = Collections.min(list);
// 排序
Collections.sort(list);
异常处理要点:
- 使用try-catch捕获输入不匹配异常
- 调用scanner.next()清除错误输入,避免死循环
- 使用Java 8 Stream API简化统计计算
5.3 ArrayList的四种遍历方式
java复制List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
// 1. 普通for循环
for (int i = 0; i < nums.size(); i++) {
System.out.println(nums.get(i));
}
// 2. 增强for循环
for (int num : nums) {
System.out.println(num);
}
// 3. 迭代器
Iterator<Integer> it = nums.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 4. 列表迭代器(支持双向遍历)
ListIterator<Integer> lit = nums.listIterator();
while (lit.hasNext()) {
System.out.println(lit.next());
}
遍历方式选择建议:
- 需要索引时:使用普通for循环
- 只读遍历:增强for循环最简洁
- 需要删除元素:使用迭代器
- 需要双向遍历或修改:使用ListIterator
6. 实战经验与性能优化
6.1 初始化容量优化
对于已知大小的ArrayList,初始化时指定容量可以避免多次扩容:
java复制// 不好的做法:默认初始容量10,可能多次扩容
List<String> list1 = new ArrayList<>();
// 好的做法:预先设置足够容量
List<String> list2 = new ArrayList<>(expectedSize);
6.2 批量操作优化
使用addAll()方法比循环add()更高效:
java复制// 低效做法
for (String item : anotherCollection) {
list.add(item);
}
// 高效做法
list.addAll(anotherCollection);
6.3 并行流处理
对于大型列表,考虑使用并行流提高处理速度:
java复制List<Integer> processed = numbers.parallelStream()
.map(this::heavyProcessing)
.collect(Collectors.toList());
7. 常见问题排查指南
7.1 ConcurrentModificationException
这个异常通常发生在使用迭代器遍历集合时,同时修改了集合结构:
java复制// 错误代码
for (String item : list) {
if (item.equals("remove")) {
list.remove(item); // 抛出异常
}
}
// 正确做法1:使用迭代器的remove方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("remove")) {
it.remove(); // 安全删除
}
}
// 正确做法2:使用Java 8 removeIf
list.removeIf(item -> item.equals("remove"));
7.2 性能问题诊断
当发现List操作性能不佳时,可以检查:
- 是否频繁扩容(ArrayList)
- 是否在中间位置频繁插入(ArrayList)
- 是否大量随机访问(LinkedList)
- 是否使用了不合适的同步策略
8. 扩展知识与高级技巧
8.1 不可变列表
创建不可变列表可以保证线程安全:
java复制List<String> immutable = Collections.unmodifiableList(list);
// 或者Java 9+
List<String> immutable = List.of("a", "b", "c");
8.2 子列表视图
subList()方法创建的是视图而非新列表,对子列表的修改会影响原列表:
java复制List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> sub = numbers.subList(1, 4);
sub.set(0, 99); // 会修改原numbers列表
8.3 自定义List实现
了解List接口的核心方法,可以创建自定义实现:
java复制class CustomList<E> implements List<E> {
// 必须实现的方法包括:
// size(), get(index), add(e), remove(index)等
}
通过这20道精心设计的练习题,我们从基础概念到高级应用全面覆盖了Java List集合的核心知识点。建议读者不仅要做题,更要动手实践每个编程示例,特别是那些涉及性能优化和异常处理的场景。List集合看似简单,但深入理解其实现原理和使用技巧,将显著提升你的Java编程能力和代码质量。