1. Java异常体系
1.1 异常的分类
Q: Java异常体系的顶层父类是什么?分为哪两大类?Exception又分为哪两种?
A:
- 顶层父类:
java.lang.Throwable - 两大类:
- Error:JVM级别错误(如内存溢出),不需要程序员处理
- Exception:程序可能出现的问题,程序员需要处理
- Exception分为两种:
- 运行时异常:
RuntimeException及其子类,编译不报错,运行时报错 - 编译时异常:非
RuntimeException,编译阶段就必须处理
- 运行时异常:
public class Demo011 {
public static void main(String[] args) {
// Error示例:栈溢出(JVM错误,无法处理)
// test(); // StackOverflowError
// 运行时异常:编译正常,运行报错
int a = 1/0; // ArithmeticException
int[] arr = {10, 20, 30};
System.out.println(arr[3]); // ArrayIndexOutOfBoundsException
// 编译时异常:写代码就报错,必须处理
// new FileInputStream("d:/demo.txt"); // 编译报错!必须try-catch或throws
// Class.forName("com.itheima.Demo"); // 编译报错!
}
public static void test() { test(); }
}
1.2 运行时异常 vs 编译时异常
Q: 运行时异常和编译时异常的核心区别是什么?分别举例说明。
A:
| 特性 | 运行时异常 | 编译时异常 |
|---|---|---|
| 继承关系 | 继承 RuntimeException |
不继承 RuntimeException |
| 编译阶段 | 不报错 | 必须处理,否则编译失败 |
| 提醒时机 | 运行时才暴露 | 写代码时就提醒 |
| 是否强制处理 | 不强制 | 强制处理 |
public class ExceptionTypeDemo {
public static void main(String[] args) {
// ========== 运行时异常(编译不报错)==========
String s = null;
// s.length(); // 编译正常,运行报NullPointerException
Integer.parseInt("abc"); // 编译正常,运行报NumberFormatException
// ========== 编译时异常(编译就报错)==========
// 以下代码如果不处理,编译器直接标红报错
// FileInputStream fis = new FileInputStream("test.txt");
// 错误:Unhandled exception: FileNotFoundException
// Class.forName("com.test.Demo");
// 错误:Unhandled exception: ClassNotFoundException
// 必须这样处理:
try {
new FileInputStream("test.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
1.3 异常处理的两种方式
Q: Java处理异常的两种方式是什么?企业开发推荐如何使用?
A:
- try-catch捕获:自己处理异常,适合最外层方法(如Controller)
- throws抛出:抛给上层调用者处理,适合非最外层方法
企业推荐模式:底层异常层层上抛,最外层集中捕获,记录日志并返回友好提示给用户
public class Demo012 {
public static void main(String[] args) {
// main方法是最外层,推荐try-catch捕获
try {
demo(); // 调用可能异常的方法
} catch (FileNotFoundException e) {
// 给程序员看:打印堆栈,定位问题
e.printStackTrace();
// 给用户看:友好提示(实际返回给前端)
System.out.println("您访问的资源不存在,请检查路径");
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.out.println("资源不完整,请稍后重试");
} catch (Exception e) { // 父类兜底,捕获未知异常
e.printStackTrace();
System.out.println("服务器繁忙,请稍后重试");
}
}
// 非最外层方法,推荐throws抛出,让调用者决定如何处理
// public static void demo() throws FileNotFoundException, ClassNotFoundException {
public static void demo() throws Exception { // 简化:直接抛父类
// 编译时异常1:文件不存在
InputStream in = new FileInputStream("d:/demo.txt");
// 编译时异常2:类不存在
Class.forName("com.itheima.Demo");
// 运行时异常:空指针(不需要throws声明)
Demo012 d = null;
d.toString();
}
}
1.4 异常处理的注意事项
Q: catch多个异常时顺序有什么要求?抛出多个子类异常如何简化代码?
A:
- catch顺序:子类异常在前,父类异常在后。因为优先匹配明确的子类异常,父类用于兜底
- 简化抛出:多个子类异常可以直接
throws Exception(父类)
public class ExceptionOrderDemo {
public static void main(String[] args) {
try {
test();
}
// ✅ 正确:子类在前(明确异常先处理)
catch (FileNotFoundException e) {
System.out.println("文件不存在");
} catch (ClassNotFoundException e) {
System.out.println("类不存在");
}
// ✅ 父类在后:兜底处理未知异常
catch (Exception e) {
System.out.println("其他异常");
}
// ❌ 错误示例:父类在前会导致子类catch永远无法到达(编译报错)
/*
try {
test();
} catch (Exception e) { // 父类已经捕获所有异常
// ...
} catch (FileNotFoundException e) { // 编译报错:不可到达的代码
// ...
}
*/
}
// 简化前:冗余,需要列出所有子类
// public static void test() throws FileNotFoundException, ClassNotFoundException, SQLException {
// 简化后:直接throws父类Exception
public static void test() throws Exception {
new FileInputStream("test.txt");
Class.forName("com.test.Demo");
}
}
1.5 异常的作用(一):定位Bug
Q: 异常如何帮助定位程序Bug?
A: 异常信息包含:异常类型、出错类名、具体行号、调用堆栈,可以快速定位问题代码位置
public class BugLocationDemo {
public static void main(String[] args) {
try {
methodA(); // 第10行
} catch (Exception e) {
// 打印异常堆栈:从发生点逐层向上显示调用链
e.printStackTrace();
/*
输出示例:
java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at com.itheima.BugLocationDemo.methodC(BugLocationDemo.java:25) ← 真正出错位置
at com.itheima.BugLocationDemo.methodB(BugLocationDemo.java:20)
at com.itheima.BugLocationDemo.methodA(BugLocationDemo.java:15)
at com.itheima.BugLocationDemo.main(BugLocationDemo.java:10)
*/
}
}
public static void methodA() {
methodB(); // 第15行
}
public static void methodB() {
methodC(); // 第20行
}
public static void methodC() {
String s = null;
s.length(); // 第25行:真正发生异常的位置
}
}
1.6 异常的作用(二):作为特殊返回值
Q: 异常如何作为方法的特殊返回值?什么场景下使用?
A: 当方法需要提前终止并通知调用者"执行出现问题"时,可以抛异常作为特殊返回值。适用于业务规则校验失败场景。
public class ExceptionAsReturnDemo {
public static void main(String[] args) {
try {
checkGender(); // 如果是男生,方法会抛异常终止
System.out.println("欢迎进入女生宿舍"); // 只有女生能执行到这里
} catch (RuntimeException e) {
// 捕获异常,获取"返回值"(错误信息)
System.err.println(e.getMessage()); // 输出:"男生禁止进入"
}
}
public static void checkGender() {
Scanner sc = new Scanner(System.in);
System.out.println("请输入性别:");
String gender = sc.next();
if ("男".equals(gender)) {
// 抛异常作为特殊返回值:终止方法,通知调用者
throw new RuntimeException("男生禁止进入");
// 抛出后,下面代码不会执行
}
// 只有女生能执行到这里
System.out.println("校验通过");
}
}
1.7 异常的作用(三):恢复程序运行
Q: 如何利用异常恢复程序继续运行?
A: 在循环中捕获异常,不抛出,提示错误后继续下一轮循环,直到输入合法数据。
public class ExceptionRecoveryDemo {
public static void main(String[] args) {
getValidAge(); // 即使输入错误,程序也不会崩溃,会提示重新输入
System.out.println("程序正常结束");
}
public static void getValidAge() {
while (true) { // 无限循环,直到输入合法
try {
Scanner sc = new Scanner(System.in);
System.out.println("请输入年龄(0-150):");
int age = sc.nextInt(); // 如果输入"abc",会抛InputMismatchException
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄范围错误");
}
System.out.println("您输入的合法年龄是:" + age);
break; // 输入合法,退出循环
} catch (InputMismatchException e) {
// 捕获但不抛出:恢复程序,提示重新输入
System.out.println("输入格式错误,请输入数字!");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage() + ",请重新输入!");
}
}
}
}
1.8 自定义运行时异常(推荐)
Q: 为什么要自定义异常?如何自定义运行时异常?步骤是什么?
A:
- 原因:Java无法提供所有业务异常(如"性别异常"、"年龄非法异常"),自定义可提高代码可读性和规范性
- 步骤:
- 定义类继承
RuntimeException - 重写构造器(无参+有参)
- 使用
throw new抛出
- 定义类继承
// ========== 步骤1:定义异常类 ==========
public class GenderRuntimeException extends RuntimeException {
// 无参构造器
public GenderRuntimeException() {}
// 有参构造器:传入错误消息
public GenderRuntimeException(String message) {
super(message); // 调用父类构造器保存消息,后续可用getMessage()获取
}
}
// ========== 步骤2:使用自定义异常 ==========
public class Demo015 {
public static void main(String[] args) {
try {
checkGender();
} catch (GenderRuntimeException e) { // 捕获明确的异常类型,可读性好
System.err.println(e.getMessage()); // 获取构造时传入的消息
}
}
public static void checkGender() {
Scanner sc = new Scanner(System.in);
System.out.println("请输入性别:");
String gender = sc.next();
if ("男".equals(gender)) {
// 抛出自定义异常,比RuntimeException语义更明确
throw new GenderRuntimeException("男生禁止进入");
}
System.out.println("欢迎进入");
}
}
1.9 自定义编译时异常
Q: 如何自定义编译时异常?与运行时异常有什么区别?
A:
- 定义:继承
Exception(而非RuntimeException) - 核心区别:编译时异常必须在方法上声明
throws,强制调用者处理;运行时异常不需要
// ========== 步骤1:定义编译时异常 ==========
public class GenderException extends Exception {
public GenderException() {}
public GenderException(String message) {
super(message);
}
}
// ========== 步骤2:使用编译时异常 ==========
public class Demo016 {
public static void main(String[] args) {
// 必须try-catch或继续throws,否则编译报错
try {
checkGender();
} catch (GenderException e) {
System.err.println(e.getMessage());
}
}
// 必须声明throws,强制调用者处理
public static void checkGender() throws GenderException {
Scanner sc = new Scanner(System.in);
System.out.println("请输入性别:");
String gender = sc.next();
if ("男".equals(gender)) {
throw new GenderException("男生禁止进入");
}
System.out.println("欢迎进入");
}
}
// ========== 对比:运行时异常不需要throws ==========
public void checkAge(int age) { // 不需要throws
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数"); // 运行时异常
}
}
2. Java泛型
2.1 泛型的基本概念
Q: 什么是泛型?泛型的本质是什么?使用泛型有什么好处?
A:
- 泛型:在编译阶段约束集合或类可以操作的数据类型
- 本质:把具体的数据类型作为参数传给类型变量
- 好处:
- 类型安全(编译期检查)
- 消除强制类型转换
- 代码复用
public class GenericDemo {
public static void main(String[] args) {
// ========== 不使用泛型(JDK5之前)==========
ArrayList list1 = new ArrayList();
list1.add("hello");
list1.add(100); // ❌ 什么都能存,类型不安全
list1.add(new Date());
Object obj = list1.get(0);
String str = (String) obj; // ❌ 必须强转,可能ClassCastException
// String str2 = (String) list1.get(1); // 运行时报错!Integer不能转String
// ========== 使用泛型 ==========
ArrayList<String> list2 = new ArrayList<>();
list2.add("hello");
// list2.add(100); // ✅ 编译报错!类型安全
String s = list2.get(0); // ✅ 直接得到String,无需强转
}
}
2.2 自定义泛型类
Q: 如何定义泛型类?类型变量命名有什么约定?
A:
- 语法:
修饰符 class 类名<类型变量, ...> { } - 常用命名:
E:Element(元素)T:Type(类型)K:Key(键)V:Value(值)
// ========== 定义泛型类 ==========
public class MyArrayList<E> { // E代表元素类型,使用时指定具体类型
private ArrayList<E> list = new ArrayList<>();
public void add(E e) { // E作为参数类型
list.add(e);
}
public E get(int index) { // E作为返回值类型
return list.get(index);
}
public void update(int index, E e) {
list.set(index, e);
}
public void del(E e) {
list.remove(e);
}
@Override
public String toString() {
return list.toString();
}
}
// ========== 使用泛型类 ==========
public class Demo021 {
public static void main(String[] args) {
// 指定String类型,类中所有E都变成String
MyArrayList<String> strList = new MyArrayList<>();
strList.add("黑马");
strList.add("程序员");
// strList.add(100); // 编译报错!
String s = strList.get(0); // 直接返回String
System.out.println(s); // "黑马"
// 指定Integer类型
MyArrayList<Integer> intList = new MyArrayList<>();
intList.add(100);
intList.add(200);
Integer num = intList.get(0);
}
}
2.3 自定义泛型接口
Q: 如何定义和使用泛型接口?实现类如何处理类型参数?
A: 实现类可以选择指定具体类型,或继续保持泛型。
// ========== 定义泛型接口 ==========
public interface DataOperator<E> {
void add(E e);
void update(E e);
void delete(E e);
E get(int index);
void printAllData();
}
// ========== 方式1:实现类指定具体类型 ==========
public class StudentDataOperator implements DataOperator<Student> {
private ArrayList<Student> list = new ArrayList<>();
@Override
public void add(Student student) { // 参数类型确定为Student
list.add(student);
}
@Override
public Student get(int index) { // 返回类型确定为Student
return list.get(index);
}
@Override
public void printAllData() {
System.out.println(list);
}
// ... 其他方法
}
public class TeacherDataOperator implements DataOperator<Teacher> {
private ArrayList<Teacher> list = new ArrayList<>();
// 同理,操作Teacher类型
@Override
public void add(Teacher teacher) { list.add(teacher); }
@Override
public void printAllData() { System.out.println(list); }
// ... 其他方法
}
// ========== 使用 ==========
public class Demo022 {
public static void main(String[] args) {
// 操作学生数据
DataOperator<Student> op1 = new StudentDataOperator();
op1.add(new Student("播仔", 20));
op1.add(new Student("播妞", 18));
op1.printAllData(); // [Student(name=播仔, age=20), Student(name=播妞, age=18)]
// 操作老师数据
DataOperator<Teacher> op2 = new TeacherDataOperator();
op2.add(new Teacher("波哥", "篮球"));
op2.printAllData(); // [Teacher(name=波哥, hobby=篮球)]
}
}
2.4 泛型方法
Q: 如何在普通类(非泛型类)中定义泛型方法?语法是什么?
A: 在非泛型类中,泛型方法需要在返回值前声明 <类型变量>。
public class GenericMethodDemo {
public static void main(String[] args) {
Student[] students = {
new Student("播仔", 20),
new Student("播妞", 18)
};
Teacher[] teachers = {
new Teacher("波哥", "篮球"),
new Teacher("宇哥", "敲代码")
};
// 同一个方法处理不同类型数组
printArray(students); // T被推断为Student
printArray(teachers); // T被推断为Teacher
// 显式指定类型(很少用)
GenericMethodDemo.<Student>printArray(students);
}
// 泛型方法:<T>必须写在返回值void前面
public static <T> void printArray(T[] array) {
System.out.println("数组长度:" + array.length);
for (T item : array) {
System.out.println(item);
}
}
// 泛型方法可以有返回值
public static <T> T getFirst(T[] array) {
return array.length > 0 ? array[0] : null;
}
// 泛型方法可以有多个类型参数
public static <K, V> void printPair(K key, V value) {
System.out.println("Key: " + key + ", Value: " + value);
}
}
2.5 泛型通配符与上下限
Q: 什么是泛型通配符?上限 ? extends和下限 ? super有什么区别?
A:
- 通配符
?:表示任意类型 - 上限
? extends Car:只能是Car或其子类(取数据安全) - 下限
? super Car:只能是Car或其父类(存数据安全)
public class WildcardDemo {
public static void main(String[] args) {
List<Che> ches = new ArrayList<>(); // 祖父类
List<Car> cars = new ArrayList<>(); // 父类
List<BMW> bmws = new ArrayList<>(); // 子类
List<Cat> cats = new ArrayList<>(); // 无关类
// ========== <?> 任意类型 ==========
printAny(ches); printAny(cars);
printAny(bmws); printAny(cats); // 都可以传
// ========== <Car> 固定类型 ==========
printCar(cars); // 只能传List<Car>
// printCar(bmws); // 报错!List<BMW>不是List<Car>
// ========== <? extends Car> 上限 ==========
// printUpper(ches); // 报错!Che是Car的父类,不是子类
printUpper(cars); // ✅ Car本身可以
printUpper(bmws); // ✅ BMW是Car的子类,可以
// printUpper(cats); // 报错!无关类
// 上限集合:可以安全地"取"数据(得到的一定是Car或其子类)
// 但不能"存"数据(不知道具体是哪个子类)
// ========== <? super Car> 下限 ==========
printLower(ches); // ✅ Che是Car的父类,可以
printLower(cars); // ✅ Car本身可以
// printLower(bmws); // 报错!BMW是子类,不是父类
// printLower(cats); // 报错!
// 下限集合:可以安全地"存"Car及其子类数据
// 但"取"数据只能得到Object(不知道是哪个父类)
}
public static void printAny(List<?> list) {
System.out.println("任意类型:" + list);
}
public static void printCar(List<Car> list) {
System.out.println("固定类型Car:" + list);
}
// 上限:? extends Car
public static void printUpper(List<? extends Car> list) {
// list.add(new Car()); // ❌ 编译报错!不能存数据
// list.add(new BMW()); // ❌ 编译报错!
Car car = list.get(0); // ✅ 可以取,得到的是Car类型
System.out.println("上限:" + list);
}
// 下限:? super Car
public static void printLower(List<? super Car> list) {
list.add(new Car()); // ✅ 可以存Car
list.add(new BMW()); // ✅ 可以存Car的子类
// Car car = list.get(0); // ❌ 编译报错!得到的是Object
Object obj = list.get(0); // ✅ 只能得到Object
System.out.println("下限:" + list);
}
}
class Che {}
class Car extends Che {}
class BMW extends Car {}
class Cat {}
2.6 包装类与自动装箱拆箱
Q: 为什么需要包装类?什么是自动装箱和拆箱?Integer缓存机制是什么?
A:
- 原因:泛型和集合不支持基本数据类型,只能支持对象类型
- 自动装箱:基本类型 → 包装类(编译器自动调用
valueOf()) - 自动拆箱:包装类 → 基本类型(编译器自动调用
xxxValue()) - Integer缓存:-128~127之间的整数会被缓存到常量池,相同值共享同一对象
public class WrapperDemo {
public static void main(String[] args) {
// ========== 为什么需要包装类 ==========
// List<int> list = new ArrayList<>(); // ❌ 编译报错!泛型不支持基本类型
List<Integer> list = new ArrayList<>(); // ✅ 使用包装类
// ========== 手动装箱(JDK5之前)==========
int a = 100;
Integer b = Integer.valueOf(a); // 推荐
// Integer b2 = new Integer(a); // 已过时,不建议
// ========== 自动装箱(JDK5+)==========
Integer i1 = 100; // 编译器自动转为:Integer.valueOf(100)
// ========== 自动拆箱 ==========
int i2 = i1; // 编译器自动转为:i1.intValue()
// ========== Integer缓存面试题(重要!)==========
Integer i3 = 100;
Integer i4 = 100;
System.out.println(i3 == i4); // true!缓存范围内,同一对象
Integer i5 = 200; // 超出缓存范围-128~127
Integer i6 = 200;
System.out.println(i5 == i6); // false!新建不同对象
// 原理:Integer.valueOf()的源码
// public static Integer valueOf(int i) {
// if (i >= -128 && i <= 127)
// return IntegerCache.cache[i + 128]; // 从缓存取
// return new Integer(i); // 新建对象
// }
// ========== 包装类的其他功能 ==========
// 1. 基本类型转字符串
String str1 = Integer.toString(100); // "100"
String str2 = Double.toString(3.14); // "3.14"
// 2. 字符串转基本类型(常用!处理前端参数)
int num = Integer.parseInt("100"); // 100
double d = Double.parseDouble("3.14"); // 3.14
boolean flag = Boolean.parseBoolean("true"); // true
// 3. 包装类转字符串
Integer obj = 100;
String str3 = obj.toString(); // "100"
}
}
3. Java集合
3.1 Collection集合体系与特点
Q: Collection集合分为哪两大系列?各自的特点是什么?
A:
- List系列:有序、可重复、有索引(ArrayList、LinkedList)
- Set系列:无序、不重复、无索引(HashSet、LinkedHashSet、TreeSet)
public class CollectionFeaturesDemo {
public static void main(String[] args) {
// ========== List:有序、可重复、有索引 ==========
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("88");
list.add("100");
list.add("99");
System.out.println(list); // [a, b, c, 88, 100, 99] 有序(添加顺序)
list.add("99"); // 可以重复
System.out.println(list); // [a, b, c, 88, 100, 99, 99]
System.out.println(list.get(2)); // c 有索引,可以get(index)
// ========== Set:无序、不重复、无索引 ==========
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set.add("88");
set.add("100");
set.add("99");
System.out.println(set); // 无序,如 [a, b, c, 99, 88, 100] 顺序不固定
boolean added = set.add("99"); // 添加失败,返回false
System.out.println(added); // false,因为已存在,不重复
// set.get(0); // ❌ 编译报错!Set没有get方法,无索引
}
}
3.2 Collection常用方法
Q: Collection接口提供了哪些通用方法?
A:
| 方法 | 功能 | 返回值 |
|---|---|---|
add(E e) |
添加元素 | boolean |
size() |
元素个数 | int |
remove(Object o) |
删除指定元素 | boolean |
isEmpty() |
是否为空 | boolean |
clear() |
清空集合 | void |
contains(Object o) |
是否包含 | boolean |
toArray(T[] a) |
转数组 | T[] |
public class CollectionMethodsDemo {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>();
// add:添加
c.add("黑马");
c.add("java");
c.add("程序");
c.add("AI");
System.out.println(c); // [黑马, java, 程序, AI]
// size:大小
System.out.println("元素个数:" + c.size()); // 4
// remove:删除(根据equals判断)
c.remove("AI"); // 删除成功返回true,不存在返回false
System.out.println(c); // [黑马, java, 程序]
// isEmpty:判空
System.out.println(c.isEmpty()); // false
// clear:清空
c.clear();
System.out.println(c.isEmpty()); // true
// 重新添加
c.add("黑马");
c.add("java");
c.add("程序");
c.add("AI");
// contains:包含判断(根据equals)
System.out.println(c.contains("AI")); // true
System.out.println(c.contains("PHP")); // false
// toArray:转数组(指定类型,避免Object[]强转)
String[] array = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(array)); // [黑马, java, 程序, AI]
// 如果不指定类型,得到Object[]
Object[] objArray = c.toArray();
}
}
3.3 集合遍历的六种方式
Q: List集合有哪六种遍历方式?分别适用于什么场景?
A:
| 方式 | 语法 | 适用场景 |
|---|---|---|
| 1. 迭代器 | Iterator it = list.iterator(); while(it.hasNext()) |
集合通用,遍历中可安全删除 |
| 2. fori | for(int i=0; i<list.size(); i++) |
需要索引,或反向遍历 |
| 3. 增强for | for(String s : list) |
简洁,最常用 |
| 4. forEach+匿名类 | list.forEach(new Consumer(){...}) |
了解原理 |
| 5. forEach+Lambda | list.forEach(s -> ...) |
简洁,推荐 |
| 6. 方法引用 | list.forEach(System.out::println) |
最简洁 |
public class CollectionTraversalDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("播仔");
list.add("hello");
list.add("播妞");
list.add("java");
// ========== 方式1:迭代器(Iterator)==========
// 特点:集合专有,可以安全删除元素
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String data = iterator.next();
System.out.println(data);
}
// 注意:迭代器只能使用一次,用完需重新获取
// ========== 方式2:fori(索引遍历)==========
// 特点:需要索引时使用,或反向遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// ========== 方式3:增强for循环(最常用)==========
// 特点:简洁,但不能获取索引
for (String data : list) {
System.out.println(data);
}
// ========== 方式4:forEach + 匿名内部类 ==========
// 原理:Consumer函数式接口
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// ========== 方式5:forEach + Lambda(推荐)==========
list.forEach(s -> System.out.println(s));
// ========== 方式6:方法引用(最简洁)==========
// 格式:对象::实例方法
list.forEach(System.out::println);
}
}
3.4 遍历同时修改的问题与解决方案
Q: 为什么在遍历集合时删除元素会出现问题?各种遍历方式的解决方案是什么?
A:
- 根本原因:ArrayList底层是数组,删除元素后后续元素前移,导致索引错位;或触发并发修改检测
- 解决方案:
- fori正向:删除后
i--,或从后往前遍历 - 迭代器:使用
iterator.remove()而非list.remove() - 增强for:无法解决,必定报错
- fori正向:删除后
public class ConcurrentModificationDemo {
public static void main(String[] args) {
// 需求:删除所有包含"枸杞"的元素
}
// ========== ❌ 问题:fori正向删除会漏删 ==========
public static void testForiProblem() {
ArrayList<String> list = createList();
// [Java入门, 宁夏枸杞, 黑枸杞, 人字拖, 特级枸杞, 枸杞子]
for (int i = 0; i < list.size(); i++) {
if (list.get(i).contains("枸杞")) {
list.remove(list.get(i));
}
}
// 结果:[Java入门, 黑枸杞, 人字拖, 枸杞子]
// 漏删了"黑枸杞"和"枸杞子"!
// 原因分析:
// i=0: Java入门(保留)
// i=1: 宁夏枸杞(删除)→ 后续元素前移,黑枸杞移到i=1
// i=2: 人字拖(跳过了黑枸杞!)
}
// ========== ✅ 方案1:fori删除后i-- ==========
public static void testForiSolution1() {
ArrayList<String> list = createList();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).contains("枸杞")) {
list.remove(list.get(i));
i--; // 关键:索引回退,下次仍检查当前位置
}
}
// 结果:[Java入门, 人字拖] 正确!
}
// ========== ✅ 方案2:从后往前遍历(推荐)==========
public static void testForiSolution2() {
ArrayList<String> list = createList();
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).contains("枸杞")) {
list.remove(list.get(i));
// 不需要i--,因为是从后往前,前面的元素索引不受影响
}
}
// 结果:[Java入门, 人字拖] 正确!
}
// ========== ❌ 问题:迭代器使用list.remove() ==========
public static void testIteratorProblem() {
ArrayList<String> list = createList();
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String data = iterator.next();
if (data.contains("枸杞")) {
list.remove(data); // ❌ 报错!ConcurrentModificationException
}
}
// 原因:list.remove()会修改modCount,与迭代器记录的expectedModCount不一致
}
// ========== ✅ 方案3:迭代器使用iterator.remove() ==========
public static void testIteratorSolution() {
ArrayList<String> list = createList();
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String data = iterator.next();
if (data.contains("枸杞")) {
iterator.remove(); // ✅ 正确!会同步modCount
}
}
// 结果:[Java入门, 人字拖] 正确!
}
// ========== ❌ 无法解决:增强for循环 ==========
public static void testEnhancedFor() {
ArrayList<String> list = createList();
for (String s : list) {
if (s.contains("枸杞")) {
list.remove(s); // ❌ 必定报错!ConcurrentModificationException
}
}
// 原因:增强for底层是迭代器,但无法获取iterator对象调用remove()
}
private static ArrayList<String> createList() {
ArrayList<String> list = new ArrayList<>();
list.add("Java入门");
list.add("宁夏枸杞");
list.add("黑枸杞");
list.add("人字拖");
list.add("特级枸杞");
list.add("枸杞子");
return list;
}
}