JAVA基础【异常处理】

Java 异常处理详解

一、异常处理的重要性
  • 错误分类:程序运行中可能出现用户输入错误(如NumberFormatException)、资源缺失(如FileNotFoundException)、系统级错误(如内存耗尽、网络中断)等。

  • 处理方式对比

    • 错误码(如 C 语言) :通过返回整数表示错误,需手动枚举和判断,代码繁琐。
    • Java 异常机制:通过class封装错误类型,可在任意位置抛出,上层统一捕获,分离错误处理逻辑。
二、Java 异常继承体系
Object
├─ Throwable(所有异常/错误的根)
│  ├─ Error(严重错误,程序无法处理,无需捕获)
│  │  例:`OutOfMemoryError`(内存耗尽)、`StackOverflowError`(栈溢出)、`NoClassDefFoundError`(类缺失)
│  └─ Exception(可处理的运行时错误)
│     ├─ RuntimeException(非受检异常,编译器不强制捕获)
│     │  例:`NullPointerException`(空指针)、`IndexOutOfBoundsException`(索引越界)、`IllegalArgumentException`(非法参数)
│     └─ 非RuntimeException(受检异常,编译器强制要求捕获或声明抛出)
│        例:`IOException`(IO错误)、`UnsupportedEncodingException`(编码不支持)、`SQLException`(数据库错误)
三、受检异常(Checked Exception)与非受检异常(Unchecked Exception)
特性受检异常(Exception 非 Runtime 子类)非受检异常(RuntimeException 及其子类 / Error)
编译器检查强制要求try...catch捕获或throws声明不强制,可自由选择是否处理
处理责任调用方必须处理通常由程序员修复代码逻辑错误,而非运行时处理
常见场景外部资源操作(文件、网络、数据库)程序逻辑错误(空指针、越界、非法参数等)
四、异常处理核心语法:try...catchthrows
  1. try...catch捕获异常

    • 将可能抛出异常的代码放入try块,通过多个catch块捕获不同类型的异常(子类异常需放在父类之前)。

    • 示例

      try {
          String data = readFile("data.txt"); // 可能抛出FileNotFoundException
      } catch (FileNotFoundException e) {
          System.out.println("文件未找到:" + e.getMessage());
          e.printStackTrace(); // 打印异常栈轨迹
      } catch (IOException e) { // 处理更宽泛的异常
          System.out.println("IO错误:" + e);
      }
      
  2. throws声明异常

    • 方法定义时用throws声明可能抛出的受检异常,将处理责任交给调用方。

    • 示例

      static byte[] toGBK(String s) throws UnsupportedEncodingException {
          return s.getBytes("GBK"); // 声明抛出受检异常,调用方必须处理
      }
      
  3. 注意事项

    • 受检异常必须处理:未捕获或声明受检异常会导致编译错误。
    • 避免空捕获:捕获异常后至少记录日志(如printStackTrace()),避免静默忽略错误。
    • main方法作为最后屏障:可声明throws Exception直接终止程序,或在main中统一捕获所有异常。

捕获异常

一、核心概念

在 Java 中,try...catch机制用于捕获可能抛出的异常。将可能引发异常的代码置于try块中,通过catch块捕获对应的Exception及其子类。

二、多 catch 语句
  1. 执行逻辑:JVM 按顺序匹配catch块,匹配成功后执行对应代码块,不再继续匹配。

  2. 顺序要求:子类异常必须写在父类异常前面,否则子类异常永远无法被捕获。

    • 错误示例:catch(IOException e)后接catch(UnsupportedEncodingException e)(子类在后,无法捕获)。
    • 正确示例:先子类UnsupportedEncodingException,再父类IOException
三、finally 语句
  1. 作用:无论是否发生异常,finally块中的代码都会执行,用于资源清理等必须执行的逻辑。

  2. 特点

    • 非必需,可省略。
    • 始终最后执行:正常执行完try后执行;发生异常时,执行完匹配的catch后执行。
    • 可单独使用try...finally(无需catch,但需方法声明抛出异常)。
四、捕获多种异常
  • 场景:处理逻辑相同但无继承关系的异常(如IOExceptionNumberFormatException)。

  • 语法:使用|合并多个异常类型,简化代码。

    catch(IOException | NumberFormatException e) {
        // 统一处理逻辑
    }
    

抛出异常

一、异常的传播
  1. 传播机制:当方法抛出异常且未被捕获时,异常会向上层调用链传播,直至被try...catch捕获。
  2. 调试工具:通过printStackTrace()打印异常调用栈,显示异常发生的方法调用层次及源码行号,便于定位问题。示例中NumberFormatException的栈信息清晰展示了从main()Integer.parseInt()的调用路径。
二、抛出异常的核心要点
  1. 基本步骤

    • 创建Exception实例(如new NullPointerException())。
    • 使用throw语句抛出,可合并为一行代码(throw new NullPointerException();)。
  2. 异常转换与原始信息保留

    • 捕获异常后若需抛出新异常,应在新异常构造器中传入原始异常(如throw new IllegalArgumentException(e)),通过Caused by保留完整栈信息,避免丢失 “第一案发现场”。
    • 最佳实践:始终保留原始异常,确保调用链信息完整。
  3. finally语句的执行规则

    • 即使在trycatch中抛出异常,finally仍会执行,JVM 会先执行finally再抛出异常。
三、异常屏蔽与处理
  1. 屏蔽现象:若finally中抛出新异常,catch中准备抛出的异常会被屏蔽(称为Suppressed Exception)。
  2. 保留被屏蔽异常:通过Throwable.addSuppressed(origin)将原始异常添加到新异常中,可通过getSuppressed()获取。
  3. 最佳实践:绝大多数情况下,避免在finally中抛出异常,减少调试复杂度。
四、异常信息的重要性
  • 提问或调试时,必须提供完整的异常栈信息(包括Caused bySuppressed部分),否则难以定位问题根源。
五、核心总结
关键点说明
异常传播异常沿调用链向上抛出,printStackTrace()打印栈信息辅助定位。
抛出异常throw语句创建并抛出异常,转换异常时传入原始异常以保留完整栈(Caused by)。
finally规则无论是否抛出异常,finally都会执行;避免在finally中抛异常,防止屏蔽原始异常。
异常屏蔽通过addSuppressed()保存被屏蔽异常,通常无需处理,但需知晓机制。
调试建议贴出完整异常栈(含Caused by),是定位问题的关键线索。

自定义异常

一、Java 标准库异常体系
  1. 核心结构

    • 顶层异常为Exception,分为RuntimeException(运行时异常)和受检异常(如IOException)。
    • 常见运行时异常:NullPointerExceptionIllegalArgumentExceptionIndexOutOfBoundsException等。
    • 常见受检异常:IOExceptionSQLExceptionParseException等。
二、自定义异常的设计原则
  1. 优先复用标准异常

    • 当参数检查不合法时,应抛出IllegalArgumentException及其子类(如NumberFormatException)。
    • 避免重复造轮子,标准异常已覆盖大多数通用场景。
  2. 定义根异常(BaseException)

    • 继承选择:推荐从RuntimeException派生,作为业务异常的根类,避免强制try-catch
    • 作用:统一业务异常体系,便于集中处理和区分系统级异常(如 JDK 原生异常)。
  3. 派生业务异常

    • BaseException派生出具体业务异常类,如UserNotFoundException(用户未找到)、LoginFailedException(登录失败)。
    • 示例代码:
    public class BaseException extends RuntimeException {
        // 构造方法需覆盖父类所有重载形式
        public BaseException() { super(); }
        public BaseException(String message) { super(message); }
        public BaseException(String message, Throwable cause) { super(message, cause); }
        public BaseException(Throwable cause) { super(cause); }
    }
    
    public class UserNotFoundException extends BaseException { /* 同上构造方法 */ }
    public class LoginFailedException extends BaseException { /* 同上构造方法 */ }
    
三、构造方法的实现要求
  • 完整覆盖父类构造:需包含无参、单字符串参数、字符串 + Throwable、单 Throwable 参数四种构造方法,确保异常创建的灵活性。
  • IDE 辅助:通过 IDE 自动生成构造方法,避免手动编写错误。

四、最佳实践

  1. 异常分类清晰

    • 根异常BaseException作为业务异常顶层,便于全局捕获(如catch (BaseException e)统一处理业务逻辑异常)。
  2. 减少受检异常

    • 继承RuntimeException的非受检异常,无需在方法签名中声明throws,简化代码结构。
  3. 提供有意义的错误信息

    • 构造异常时传入具体消息(如new UserNotFoundException("用户ID 123未找到")),便于调试定位。

Java断言

一、核心概念
  • 断言定义:一种调试工具,用于在开发和测试阶段验证程序状态,通过 assert 关键字实现。
  • 作用:确保代码逻辑符合预期,捕获潜在错误,辅助调试。
二、语法与用法
  1. 基本语法

    java

    assert 条件表达式; // 断言条件为 true,否则抛出 AssertionError
    assert 条件表达式 : 断言消息; // 失败时附带自定义消息
    
    • 例:assert x >= 0 : "x 必须非负";
  2. 执行逻辑

    • 条件为 false 时,抛出 AssertionError(Error 的子类),终止程序。
    • 断言消息可选,用于增强错误信息可读性。
三、核心特点
  1. 适用场景

    • 仅限开发 / 测试阶段:用于检查 “不应该发生” 的错误(如参数合法性的极端假设)。
    • 不处理可恢复错误:对可预期的程序错误(如 null 参数),应显式抛出异常(如 IllegalArgumentException)并由上层处理,而非断言。
  2. 默认行为

    • JVM 默认关闭断言assert 语句会被忽略,需手动启用。
四、启用断言的方式
  1. 全局启用

    java -enableassertions 或 -ea 类名
    
    • 例:java -ea Main
  2. 局部启用

    • 指定类-ea:包名.类名(如 -ea:com.example.Main)。
    • 指定包(含子包)-ea:包名...(如 -ea:com.example...)。

反射

反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

Class 类

一、Class 类本质
  1. 定义:JVM 为每个加载的class(含interface)创建的唯一实例,存储类的完整元信息(类名、包名、父类、接口、字段、方法等)。

  2. 特性

    • 由 JVM 动态加载,首次使用时加载到内存。
    • Class类构造方法为private,仅 JVM 可创建实例,用户无法直接实例化。
    • 每个class/interface对应唯一Class实例,可通过==比较是否为同一类型。
二、获取 Class 实例的三种方式
方法示例说明
类字面量(.classClass<String> cls1 = String.class;直接通过类的静态变量class获取,编译期确定类型
实例的getClass()String str = "hello"; <br> Class<?> cls2 = str.getClass();通过对象实例调用,返回其运行时类型的Class实例
Class.forName("全类名")Class<?> cls3 = Class.forName("java.lang.String");运行期根据全类名动态加载,可能抛出ClassNotFoundException
  • 关键点:三种方式获取的Class实例相同,可通过==验证(如cls1 == cls2 == cls3true)。
三、Class 实例核心功能
1. 类型信息查询
方法作用示例
getName()返回完整类名(含包名)String[].class.getName() → [Ljava.lang.String;(数组类名特殊格式)
getSimpleName()返回简单类名(无包名)String.class.getSimpleName() → String
getPackage()获取包信息String.class.getPackage().getName() → java.lang
isInterface()判断是否为接口Runnable.class.isInterface() → true
isEnum()判断是否为枚举类Month.class.isEnum() → true
isArray()判断是否为数组类型String[].class.isArray() → true
isPrimitive()判断是否为基本类型int.class.isPrimitive() → true
2. 创建类实例
  • 方法newInstance()

  • 示例

    Class<String> cls = String.class; 
    String obj = cls.newInstance(); // 等价于 `new String()`(调用无参构造方法)
    
  • 限制:仅能调用public无参构造方法,不支持有参或非public构造方法。

3. 与instanceof的区别
特性instanceofClass实例比较(==
核心功能判断对象是否为某类型或其子类型(支持多态)精确判断是否为同一类型(不考虑继承关系)
示例Integer num instanceof Number → true(多态匹配父类)num.getClass() == Number.class → falseIntegerNumber子类,类型不同)
四、动态加载机制
  1. JVM 特性:按需加载,首次使用时加载类到内存(如首次调用类的方法、创建实例或访问静态成员时)。

  2. 应用场景:运行期根据条件加载不同实现类(如日志框架优先级选择)

    // 示例:优先使用Log4j,不存在则用JDK日志
    boolean isLog4jPresent = false; 
    try { 
        Class.forName("org.apache.log4j.Logger"); 
        isLog4jPresent = true; // 加载Log4j类,存在则使用 
    } catch (ClassNotFoundException e) { 
        // 未找到Log4j,使用JDK日志 
    }
    
五、特殊类型的 Class 实例
  1. 基本类型:JVM 预定义Class实例,如int.classboolean.classvoid.class

  2. 数组类型

    • 引用类型数组:类名为[L全类名;(如String[].class → [Ljava.lang.String;)。
    • 基本类型数组:类名为[基本类型缩写(如int[] → [Iboolean[] → [Z)。

Java 反射之访问字段

一、核心知识点:通过反射获取字段信息
  1. 获取Field实例的方法

    • getField(name):获取某个public 字段(包括父类)。
    • getDeclaredField(name):获取当前类的任意字段(不包括父类,可获取 private/protected/default 字段)。
    • getFields():获取所有public 字段(包括父类)。
    • getDeclaredFields():获取当前类的所有字段(不包括父类)。
  2. Field对象包含的字段信息

    • getName():返回字段名称(如"name")。
    • getType():返回字段类型(Class实例,如String.class,数组类型用[B表示byte[])。
    • getModifiers():返回字段修饰符(int值),可通过Modifier类判断(如Modifier.isPrivate(m))。
二、核心操作:读写字段值
  1. 获取字段值(Field.get(Object instance)

    • 示例:Object value = field.get(p),获取实例p的字段值。
    • 访问限制:若字段为非 public(如 private),需先调用field.setAccessible(true)突破访问限制(可能受SecurityManager限制,如无法修改 Java 核心类字段)。
  2. 设置字段值(Field.set(Object instance, Object value)

    • 示例:field.set(p, "新值"),修改实例p的字段值。
    • 注意:同样需通过setAccessible(true)访问非 public 字段,支持修改基本类型和引用类型字段。
三、代码示例与注意事项
  1. 示例代码逻辑

    • 获取字段:通过Class实例(如Student.classp.getClass())调用字段获取方法。
    • 突破封装:反射可访问私有字段,但会破坏类的封装性,仅用于工具 / 框架等非常规场景。
  2. 安全限制

    • setAccessible(true)可能被SecurityManager阻止(如 Java 核心包java.*/javax.*的类),确保 JVM 安全。
四、小结
  1. 反射访问字段的核心步骤

    • 通过Class获取Field实例(区分是否包含父类、是否 public)。
    • 通过Field获取字段元信息(名称、类型、修饰符)。
    • 通过get()/set()读写字段值,非 public 字段需setAccessible(true)
  2. 适用场景与风险

    • 用途:底层框架、工具开发(未知对象结构时动态操作字段)。
    • 风险:破坏封装性,代码繁琐,受安全策略限制,需谨慎使用。

调用方法

二、核心内容总结
1. 获取 Method 对象的方法

通过Class实例获取Method对象,支持不同范围和权限的方法获取:

  • getMethod(String name, Class... parameterTypes) :获取类及其父类的public 方法(需指定参数类型)。
  • getDeclaredMethod(String name, Class... parameterTypes) :获取当前类的任意方法(包括 private,不包括父类)。
  • getMethods() :获取类及其父类的所有 public 方法(返回Method[])。
  • getDeclaredMethods() :获取当前类的所有方法(包括 private,返回Method[])。
2. Method 对象的信息获取

通过Method实例可获取方法的元数据:

  • getName() :返回方法名称(如"getScore")。
  • getReturnType() :返回返回值类型(Class实例,如String.class)。
  • getParameterTypes() :返回参数类型数组(Class[],如{String.class, int.class})。
  • getModifiers() :返回修饰符(int类型,通过位运算解析 public/private 等)。
3. 调用方法的方式
  • 实例方法调用:使用Object invoke(Object instance, Object... args)instance为方法所属对象,args为参数。
    示例

    Method m = String.class.getMethod("substring", int.class);
    String result = (String) m.invoke("Hello", 2); // 调用"ello"
    
  • 静态方法调用invoke的第一个参数为null(无需实例)。
    示例

    Method m = Integer.class.getMethod("parseInt", String.class);
    Integer num = (Integer) m.invoke(null, "123"); // 调用静态方法
    
  • 非 public 方法调用:需先通过setAccessible(true)破除访问限制(可能受SecurityManager限制)。
    示例

    Method privateMethod = MyClass.class.getDeclaredMethod("privateMethod");
    privateMethod.setAccessible(true);
    privateMethod.invoke(instance);
    
4. 多态特性

反射调用方法时遵循多态原则:调用实际对象类型的覆写方法(即使通过父类Class获取方法)。
示例
子类Student覆写父类Personhello()方法,通过Person.class.getMethod("hello")获取方法后,调用Student实例仍执行子类实现。

三、关键代码示例
  1. 获取不同权限的方法

    Class<Student> clazz = Student.class;
    clazz.getMethod("publicMethod"); // 公共方法(含父类)
    clazz.getDeclaredMethod("privateMethod"); // 当前类私有方法
    
  2. 处理方法重载:通过参数类型区分同名方法,如获取substring(int start)substring(int start, int end)需指定不同参数类型。

  3. 异常处理:调用invoke可能抛出IllegalAccessException(权限问题)、InvocationTargetException(方法内部异常)等,需显式处理或声明抛出。

四、小结
  • 反射调用方法的核心步骤:获取Class→获取Method→调用invoke(处理权限和参数)。
  • 方法访问范围getMethod用于公共方法(含继承),getDeclaredMethod用于当前类任意方法(需破除权限)。
  • 多态支持:反射调用严格遵循 Java 多态规则,实际调用对象的覆写方法。
  • 应用场景:动态调用未知方法、框架底层扩展(如 Spring 依赖注入)、单元测试工具等。

调用构造方法(Java 反射机制)

一、实例创建的常规方式与反射基础
  1. 常规实例创建
    使用 new 操作符直接创建实例:

    Person p = new Person();
    
  2. 反射创建实例(无参构造)
    通过 Class.newInstance() 方法调用 public 无参数构造方法

    Person p = Person.class.newInstance();
    

    局限:仅支持无参且 public 的构造方法,无法处理有参或非 public 构造方法。

二、Constructor 对象:反射调用任意构造方法

Constructor 类封装了构造方法的详细信息,可用于创建实例,支持 有参、非 public 构造方法

  1. 获取 Constructor 对象的方法

    • getConstructor(Class<?>... parameterTypes):获取 public 构造方法(指定参数类型)。
    • getDeclaredConstructor(Class<?>... parameterTypes):获取 任意访问权限 的构造方法(包括非 public)。
    • getConstructors():获取所有 public 构造方法(返回数组)。
    • getDeclaredConstructors():获取所有构造方法(包括非 public,返回数组)。

    注意:构造方法属于当前类,与父类无关,无多态特性。

  2. 调用构造方法创建实例
    使用 newInstance(Object... args) 传入参数创建实例:

    // 调用 Integer(int) 构造方法
    Constructor<Integer> cons1 = Integer.class.getConstructor(int.class);
    Integer n1 = cons1.newInstance(123); // 结果:123
    
    // 调用 Integer(String) 构造方法
    Constructor<Integer> cons2 = Integer.class.getConstructor(String.class);
    Integer n2 = cons2.newInstance("456"); // 结果:456
    
  3. 访问非 public 构造方法
    需通过 setAccessible(true) 强制设置访问权限(可能受安全策略限制):

    Constructor<MyClass> cons = MyClass.class.getDeclaredConstructor();
    cons.setAccessible(true); // 允许访问非 public 构造方法
    MyClass obj = cons.newInstance();
    
三、核心要点总结
  1. Constructor 的作用
    封装构造方法信息,支持调用 任意访问权限、任意参数的构造方法,弥补 Class.newInstance() 的局限。

  2. 关键方法对比

    方法名功能描述访问权限参数类型匹配
    getConstructor获取 public 构造方法public精确匹配参数
    getDeclaredConstructor获取任意构造方法(包括非 public)任意精确匹配参数
    getConstructors获取所有 public 构造方法(数组)public全部
    getDeclaredConstructors获取所有构造方法(包括非 public)任意全部
  3. 注意事项

    • 非 public 构造方法需通过 setAccessible(true) 解锁访问,可能抛出安全异常。
    • 构造方法调用结果始终返回实例,无返回值(与普通方法不同)。