Loading...

文章背景图

Day 009-知识点汇总

2026-02-11
21
-
- 分钟

1. Java异常体系

1.1 异常的分类

Q: Java异常体系的顶层父类是什么?分为哪两大类?Exception又分为哪两种?

A:

  • 顶层父类java.lang.Throwable
  • 两大类
    1. Error:JVM级别错误(如内存溢出),不需要程序员处理
    2. 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:

  1. try-catch捕获:自己处理异常,适合最外层方法(如Controller)
  2. 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:

  1. catch顺序:子类异常在前,父类异常在后。因为优先匹配明确的子类异常,父类用于兜底
  2. 简化抛出:多个子类异常可以直接 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无法提供所有业务异常(如"性别异常"、"年龄非法异常"),自定义可提高代码可读性和规范性
  • 步骤
    1. 定义类继承 RuntimeException
    2. 重写构造器(无参+有参)
    3. 使用 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:

  • 泛型:在编译阶段约束集合或类可以操作的数据类型
  • 本质:把具体的数据类型作为参数传给类型变量
  • 好处
    1. 类型安全(编译期检查)
    2. 消除强制类型转换
    3. 代码复用
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:无法解决,必定报错
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;
    }
}
评论交流

文章目录