关于 java:4. 异常处理与调试

一、异常核心语法

1.1 try-catch-finally:异常捕获与处理结构

1)作用

  • 用于捕获和处理程序运行过程中可能发生的异常

  • 防止程序因异常中断,提高代码的鲁棒性(健壮性)

2)基本语法结构:

try {
    // 可能抛出异常的代码块
} catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
} finally {
    // 无论是否发生异常,都会执行(如关闭资源)
}

3)示例讲解:

public class Demo {
    public static void main(String[] args) {
        try {
            int a = 10 / 0; // 运行时异常:ArithmeticException
        } catch (ArithmeticException e) {
            System.out.println("错误:除数不能为零!");
        } finally {
            System.out.println("程序结束,释放资源。");
        }
    }
}

运行结果:

错误:除数不能为零!
程序结束,释放资源。

4)多个 catch

try {
    String s = null;
    System.out.println(s.length());
} catch (NullPointerException e) {
    System.out.println("空指针异常!");
} catch (Exception e) {
    System.out.println("其他异常:" + e.getMessage());
}

建议先写具体异常,再写父类(Exception),否则子类异常无法被捕获。

5)finally 详解

  • 无论 try 块是否抛出异常,finally 总会执行

  • 通常用于释放资源,如关闭文件、数据库连接

FileInputStream fis = null;
try {
    fis = new FileInputStream("data.txt");
    // 读取文件
} catch (IOException e) {
    System.out.println("读取失败");
} finally {
    if (fis != null) {
        try {
            fis.close(); // 一定要关闭资源
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.2 throw:手动抛出异常对象

1)作用

throw 用于在代码中主动抛出一个异常实例,可以抛出任何 Throwable 的子类。

2)语法格式:

throw new 异常类型("异常描述");

3)示例:

public void checkAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年龄不能为负数");
    }
}
  • 程序执行到 throw 语句时会立即抛出异常并中断执行

  • 如果这个异常没有在方法内被 try-catch 捕获,必须用 throws 声明

1.3 throws:方法声明异常

1)作用

  • 用于方法签名中,声明该方法可能抛出哪些异常

  • 告诉调用者:你要么用 try-catch 处理,要么继续 throws

2)语法格式:

返回类型 方法名(...) throws 异常类型1, 异常类型2 {
    // 可能抛出异常的代码
}

3)示例:

public void readFile(String path) throws IOException {
    FileReader fr = new FileReader(path); // FileReader 会抛 IOException
}

调用时:

try {
    readFile("test.txt");
} catch (IOException e) {
    System.out.println("文件读取失败:" + e.getMessage());
}

4)throw vs throws 区别对比表:

特性 throw throws
用途 抛出异常对象 声明异常可能被抛出
位置 方法体内 方法声明处
后面跟的内容 异常对象(new) 异常类(不带 new)
示例 throw new IOException("失败") throws IOException

1.4 综合例子:使用 throw + throws + try-catch

public class User {
    public void login(String username) throws Exception {
        if (username == null || username.isEmpty()) {
            throw new Exception("用户名不能为空");
        }
        System.out.println("登录成功");
    }

    public static void main(String[] args) {
        User user = new User();
        try {
            user.login(""); // 会抛出异常
        } catch (Exception e) {
            System.out.println("捕获到异常: " + e.getMessage());
        } finally {
            System.out.println("登录尝试结束");
        }
    }
}

输出:

捕获到异常: 用户名不能为空
登录尝试结束

1.5 实际用法

场景 应用
开发中 try-catch 处理用户输入、文件读取等不确定行为
SDK 调试 通过日志堆栈 catch (Exception e) 观察调用流程
逆向分析中 Hook 异常处理函数,绕过 throw 抛出的错误(例如:校验失败)
安全测试中 利用错误提示、异常堆栈进行路径发现或代码注入入口分析

1.6 小结

try-catch-finally
│
├─ try:放入可能出错的代码
├─ catch:处理指定异常类型
├─ finally:一定执行,用于释放资源
│
throw:主动抛出异常对象
throws:方法声明可能抛出哪些异常

二、自定义异常类

2.1 自定义异常类的作用

在 Java 中,除了使用系统提供的异常(如 NullPointerException, IOException),我们还可以根据自己的业务逻辑需求定义新的异常类

自定义异常的典型用途:

  • 表示业务逻辑错误(例如:余额不足、权限异常)

  • 抛出更清晰可读、可追踪的错误

  • 与项目的模块/组件解耦,提高代码可维护性

  • 在调试或逆向中,定位异常的抛出源

2.2 自定义异常类的本质

Java 中所有异常类,最终都继承自:

java.lang.Throwable
    ├── Error         // 错误(虚拟机错误等)
    └── Exception     // 异常
         ├── RuntimeException(运行时异常)
         └── 其他受检异常(IOException 等)

我们自定义的异常通常继承自:

1)Exception(受检异常)

  • 必须用 try-catchthrows 处理

2)RuntimeException(非受检异常)

  • 编译器不强制捕获

  • 更灵活,适合应用内部逻辑异常

2.3 自定义异常类的语法

1)继承 Exception(受检异常)

public class MyCheckedException extends Exception {
    public MyCheckedException() {
        super();
    }

    public MyCheckedException(String message) {
        super(message);
    }

    public MyCheckedException(String message, Throwable cause) {
        super(message, cause);
    }
}

2)继承 RuntimeException(非受检异常)

public class MyRuntimeException extends RuntimeException {
    public MyRuntimeException(String message) {
        super(message);
    }
}

2.4 使用自定义异常的例子

示例 1:余额不足异常

// 自定义异常类
public class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

示例 2:在业务代码中使用

public class BankAccount {
    private double balance = 100.0;

    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException("余额不足,取款失败!");
        }
        balance -= amount;
    }
}

示例 3:调用者处理异常

public class Test {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        try {
            account.withdraw(150.0); // 触发异常
        } catch (InsufficientBalanceException e) {
            System.out.println("异常捕获:" + e.getMessage());
        }
    }
}

2.5 规范建议(编写自定义异常)

建议项 内容
类名 Exception 结尾(如 LoginFailedException
构造方法 提供 String messageThrowable cause 构造器
继承方式 业务类建议继承 Exception,内部错误建议继承 RuntimeException
包名 放在 com.xxx.exception 包下,统一管理

2.6 自定义异常在调试/逆向中的价值

在调试中:

  • 通过日志或堆栈跟踪定位自定义异常的抛出点

  • 比系统异常更具语义性,便于快速理解错误

在逆向中:

  • 某些 SDK 或加密逻辑会用自定义异常抛出校验错误

  • 通过 Frida/日志/trace 定位异常类名和抛出位置

  • 分析异常触发条件,进而绕过或构造伪装数据

2.7 小结

自定义异常类
├─ 为什么要自定义?
├─ 继承 Exception / RuntimeException
├─ 如何定义:构造器 + 命名规范
├─ 如何使用:抛出 throw + 声明 throws
├─ 实际场景:业务逻辑错误、逆向定位

三、常见异常类型

3.1 NullPointerException(空指针异常)

1)定义

空指针异常是指:访问了一个为 null 的引用对象的方法或字段时引发的异常。

java.lang.NullPointerException

2)常见触发场景

触发语句 说明
obj.toString() obj 为 null,调用方法抛异常
obj.field 访问成员变量时,obj 为 null
arr[0] arr 是 null,访问数组元素抛异常
list.get(0) list 是 null,而非空但索引越界时会抛 IndexOutOfBoundsException

3)示例代码

public class Demo {
    public static void main(String[] args) {
        String s = null;
        System.out.println(s.length()); // NullPointerException
    }
}

4)调试方式

  • 查看异常栈信息(Exception stack trace)

  • 从堆栈中定位异常行号和方法名

  • 使用 IDE 的断点或日志逐步排查 null 来源

栈追踪示例:

Exception in thread "main" java.lang.NullPointerException
    at Demo.main(Demo.java:4)

5)如何防止空指针

方法 示例
非空判断 if (obj != null)
Optional Optional.ofNullable(obj).orElse(defaultValue)
IDE 工具提醒 IntelliJ IDEA 有 null 检测功能
Lombok 的 @NonNull 注解 编译时校验是否为 null

3.2 ClassCastException(类强制类型转换异常)

1)定义

该异常表示:试图将某个对象强制转换为不是其实际类型的类时引发的异常

java.lang.ClassCastException

2)常见触发场景

Object obj = new Integer(5);
String str = (String) obj; // 报 ClassCastException

虽然 objObject 类型,但实际它是 Integer,不能强制转成 String

3)示例代码

public class Demo {
    public static void main(String[] args) {
        Object obj = "Hello";
        Integer num = (Integer) obj; // ClassCastException
    }
}

输出异常信息:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    at Demo.main(Demo.java:4)

4)如何避免 ClassCastException

方法 示例
使用 instanceof 判断 if (obj instanceof Integer)
使用泛型(推荐) ListList 更安全
明确接口和实现类边界 不随意强转接口实现
开发规范约束 明确参数传递和接收类型

3.3 调试角度分析

异常类型 常见错误栈信息 调试关键点
NullPointerException 指向具体行的空对象访问 追踪为 null 的变量,检查赋值
ClassCastException 指出无法转换的两个类 看实际对象的类型(getClass())和要转的类

3.4 在逆向/安全分析中的价值

1)辅助分析代码结构

  • App crash 日志中出现 ClassCastException 可能表示有 代码逻辑混淆或伪装

  • 出现 NullPointerException 时,反编译代码可定位关键对象未初始化

2)模拟异常绕过检查

try {
    if (!licenseValid) {
        throw new NullPointerException("验证失败");
    }
} catch (Exception e) {
    // 验证失败后中断
    return;
}

可以 Hook 掉这个 throw,或者强改 licenseValid 为 true,绕过验证。

3.5 小结

异常 本质 触发时机 预防方法
NullPointerException 空引用访问 访问 null 的变量、方法或数组 非空检查、Optional
ClassCastException 类型错误 强转成非实际类型 instanceof 判断、泛型

四、堆栈追踪分析

4.1 什么是堆栈追踪(Stack Trace)?

堆栈追踪是 Java 程序在运行中发生异常或错误时,JVM 自动打印的一系列方法调用栈信息,描述了从异常发生点逐层向上传递调用关系

示例异常信息(Stack Trace):

Exception in thread "main" java.lang.NullPointerException
    at com.example.MyClass.myMethod(MyClass.java:10)
    at com.example.App.main(App.java:5)

4.2 Stack Trace 的结构组成

以典型的一条栈帧为例:

at com.example.MyClass.myMethod(MyClass.java:10)
部分 含义
at 表示当前调用栈的一帧
com.example.MyClass 异常发生的类
myMethod 异常发生的方法
(MyClass.java:10) 源码文件及第几行发生了异常(10 行)

4.3 堆栈追踪常见异常样式(举例对照)

1)NullPointerException(空指针)

Exception in thread "main" java.lang.NullPointerException
    at com.demo.DemoClass.printName(DemoClass.java:15)
    at com.demo.DemoClass.main(DemoClass.java:7)

分析:

  • 异常发生在 DemoClass.java 的第 15 行

  • 是从 main() 调用 printName() 时触发的

2)ClassCastException(类强制转换)

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    at com.test.CastTest.main(CastTest.java:8)

分析:

  • 明确告诉你:实际是 String,你试图转成 Integer

  • 报错发生在第 8 行

4.4 如何进行堆栈追踪分析?

步骤 1:从上到下阅读栈帧(第一条才是出错点)

  • 第一条是异常抛出的具体位置

  • 后面的每条是“谁调用了它”

步骤 2:对照源码或反编译代码,定位具体代码行

  • 使用 IDE 跳转对应行(Ctrl + 单击)

  • 或用 jadx, JD-GUI, Fernflower 反编译查看 .class

步骤 3:结合异常类型,分析出错条件

  • 是不是参数为 null?

  • 是不是数据转换错误?

  • 是不是调用了非法对象?

4.5 逆向调试场景中的应用

1)分析 App 崩溃日志

Caused by: java.lang.RuntimeException: 解密失败
	at com.app.secure.SecurityManager.decrypt(SecurityManager.java:87)
	at com.app.net.NetHandler.getUserInfo(NetHandler.java:122)

这说明:

  • decrypt() 方法出错,可能在使用 AES、RSA 时 key 异常

  • 你可以重点 hook 这一段代码,或者跟进加密过程逻辑

2)分析 Web 安全漏洞、异常行为

java.lang.IllegalArgumentException: 参数不合法
	at com.web.api.AuthHandler.checkToken(AuthHandler.java:52)
	at com.web.api.UserController.getUser(UserController.java:19)

说明:

  • checkToken 方法校验失败,抛出异常

  • 可用于判断接口是否有 token 依赖点,或可伪造点

3)结合 Frida 实现实时监控异常

可以使用 Frida 追踪所有异常抛出点:

Java.perform(function () {
    var Exception = Java.use("java.lang.Exception");
    Exception.$init.overload('java.lang.String').implementation = function (msg) {
        console.log("Exception 被抛出: " + msg);
        return this.$init(msg);
    };
});

4.6 分析:Caused bySuppressed

Caused by

有时异常是嵌套异常,会看到:

Exception in thread "main" java.lang.Exception: 顶层异常
Caused by: java.io.IOException: 文件不存在
	at com.file.Reader.read(Reader.java:45)
  • Caused by 表示底层真正触发的异常

  • 要分析最底层原因

Suppressed

当使用 try-with-resources 时,可能出现:

Suppressed: java.lang.Exception: 关闭资源失败

说明主异常之后还有资源释放过程中的异常

4.7 小结(堆栈分析三步法)

堆栈分析三步法:
1. 定位第 1 行出错代码(准确行号)
2. 判断异常类型及触发原因
3. 向上追踪调用链,分析调用过程

4.8 实战建议

场景 技巧
逆向异常分析 拿到 crash log,反编译对应类,查找触发条件
安全测试中断点排查 抓住抛异常的函数,设置 Frida Hook 或 JDWP 调试点
Web 渗透中判断逻辑 利用返回错误堆栈,看系统是怎么解析参数和验证身份

你可能感兴趣的:(java,开发语言)