Java高级程序设计

异常处理

异常处理

异常处理(Exception handling)是指在进行运算时,出现例外的情况(需要特殊处理的非常规或例外的情况)对应的处理,这种情况经常会破坏程序正常的流程。

它通常由特殊的编程语言结构、计算机硬件机制(如:中断或者如信号等操作系统IPC设施)所构成的。具体实现由硬件和软件自身定义而决定。一些异常,尤其是硬件,将会在被中断后进行恢复。

--wikipedia

看个例子

public class Calculator {

    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 2));
    }
    
}

看个例子

public class Calculator {
    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 0));
    }
}
❯ java Calculator
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Calculator.div(Calculator.java:4)
	at Calculator.main(Calculator.java:8)

ArithmeticException

public class ArithmeticException extends RuntimeException

Thrown when an exceptional arithmetic condition has occurred. For example, an integer "divide by zero" throws an instance of this class.

Since: JDK1.0

https://docs.oracle.com/javase/8/docs/api/java/lang/ArithmeticException.html

异常导致失去控制

public class Calculator {
    public int div(int a, int b) {
        int c = a / b;
        return c;
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 0));
    }
}

return没有执行,div方法没执行完

C

int main()
{
   int i[2];
   i[3] = 10;
   return 0;
}
❯ gcc main.c
...
1 warning generated.
❯ ./a.out
[1]    20757 abort      ./a.out

异常处理

C以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。

如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。

-- 「On Java 8」

C++ 异常处理

#include <iostream>
using namespace std;

int main()
{
   int x = -1;
   try {
      if (x < 0) {
         throw x;
      }
   }
   catch (int x ) {
      cout << "Exception Caught \n";
   }
   return 0;
}

Java 异常处理

public class Calculator {
    public int div(int a, int b) {
        try{
            return a / b;
        }catch(Exception e){
            System.out.println("exception!");
            return 0; //这不合适,先将就一下
        }
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 0));
    }
}

运行

❯ java Calculator
exception!
0

程序运行完了!执行流程是符合预期的!

return 0是不合理的... 为什么?

从字节码层面看

javap -v -p -s -sysinfo -constants Calculator

  public int div(int, int);
    Code:
         0: iload_1
         1: iload_2
         2: idiv
         3: ireturn
         4: astore_3
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String exception!
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: iconst_0
        14: ireturn
      Exception table:
         from    to  target type
             0     3     4   Class java/lang/Exception
      LineNumberTable:
        line 4: 0

异常的语义

“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在那里将作出正确的决定。

在哪里做出正确的决定?

public class Calculator {
    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        try{
            System.out.println(new Calculator().div(1, 0));
        }catch(Exception e){
            System.out.println("exception!");
        }
    }
}

再改一下

public class Calculator {
    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        try {
            System.out.println(new Calculator().div(Integer.parseInt(args[0]), Integer.parseInt(args[1])));
        } catch (Exception e) {
            System.out.println("exception!");
        }
    }
}

main()方法中try...catch...意味着此处定义了异常处理语义。

再改

public class Calculator {
    public int div(int a, int b) throws Exception {
        if (b == 0) {
            throw new Exception("divided by zero");
        }
        return a / b;
    }
    public static void main(String[] args) {
        try {
            System.out.println(new Calculator().div(Integer.parseInt(args[0]), 
                                                    Integer.parseInt(args[1])));
        } catch (Exception e) {
            System.out.println("exception!");
        }
    }
}

从字节码层面看

  public int div(int, int) throws java.lang.Exception;
    descriptor: (II)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=3
         0: iload_2
         1: ifne          14
         4: new           #2                  // class java/lang/Exception
         7: dup
         8: ldc           #3                  // String divided by zero
        10: invokespecial #4                  // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
        13: athrow
        14: iload_1
        15: iload_2
        16: idiv
        17: ireturn
      LineNumberTable:
        ...
    Exceptions:
      throws java.lang.Exception

类层次









https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html

Throwable

public class Throwable extends Object implements Serializable

The Throwable class is the superclass of all errors and exceptions in the Java language.

https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html

Error

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

https://docs.oracle.com/javase/8/docs/api/java/lang/Error.html

public static void print(String myString) {
    print(myString);
}
Exception in thread "main" java.lang.StackOverflowError
at StackOverflowErrorExample.print(StackOverflowErrorExample.java:6)

Exception

The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.

The class Exception and any subclasses that are not also subclasses of RuntimeException are checked exceptions. Checked exceptions need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.

RuntimeException

if(t == null)
    throw new NullPointerException();

如果必须对传递给方法的每个引用都检查其是否为null(因为无法确定调用者是否传入了非法引用),这听起来着实吓人。幸运的是,这不必由你亲自来做,它属于 Java 的标准运行时检测的一部分。如果对null引用进行调用,Java 会自动抛出NullPointerException异常,所以上述代码是多余的,尽管你也许想要执行其他的检查以确保NullPointerException不会出现。

https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html

如何避免 NullPointerException?

Java 提供了多种机制来避免或更好地处理空指针异常:

  1. Optional (Java 8+) - 优雅地处理可能为空的值
  2. Helpful NPE Messages (Java 14+) - 精确的错误信息
  3. Objects.requireNonNull() (Java 7+) - 参数校验
  4. @NonNull / @Nullable 注解 - 编译期检查

Optional (Java 8+)

Optional 是一个容器对象,明确表示一个值可能存在或不存在。

// 传统方式 - 容易出现 NPE
public String getUserName(User user) {
    if (user != null && user.getName() != null) 
        return user.getName();
    return "Unknown";
}

// 使用 Optional - 更优雅
public String getUserName(Optional<User> user) {
    return user.map(User::getName).orElse("Unknown");
}

Optional 常用方法

Optional<String> opt = Optional.ofNullable(getString());

// 获取值,如果为空则使用默认值
String v1 = opt.orElse("default");
// 获取值,如果为空则通过 Supplier 提供
String v2 = opt.orElseGet(() -> getDefaultValue());
// 获取值,如果为空则抛出异常
String v3 = opt.orElseThrow(() -> new IllegalArgumentException());
// 如果有值则执行操作
opt.ifPresent(v -> System.out.println(v));
// 转换值
Optional<Integer> length = opt.map(String::length);

Optional 链式调用解析

user.map(User::getName).orElse("Unknown");
  1. user - 类型是 Optional<User>,可能存在也可能不存在

  2. .map(User::getName) - 转换操作

    • 如果 user 存在:调用 getName() 并返回 Optional<String>;如果 user 不存在:跳过,直接返回空的 Optional<String>
  3. .orElse("Unknown") - 获取最终值

    • 如果有值:返回该值;如果为空:返回默认值 "Unknown"

Optional 执行流程示例

// 情况1:user 存在且有名字
Optional.of(new User("Alice")).map(User::getName).orElse("Unknown");
// 结果: "Alice"

// 情况2:user 不存在
Optional.empty().map(User::getName)      // 跳过此步骤
    .orElse("Unknown");
// 结果: "Unknown"

等价的传统写法

String result = (user != null && user.getName() != null) 
    ? user.getName() : "Unknown";

Helpful NullPointerExceptions (14+)

Java 14 改进了 NPE 的错误信息,能精确指出哪个变量是 null

public class HelpfulNPE {
    static class A { B b; }
    static class B { C c; }
    static class C { String d; }
    public static void main(String[] args) {
        A a = new A();
        a.b.c.d = "value"; // a.b 为 null
    }
}

Helpful NullPointerExceptions

Java 13 及之前

Exception in thread "main" java.lang.NullPointerException
    at HelpfulNPE.main(HelpfulNPE.java:7)

Java 14+(默认启用):

Exception in thread "main" java.lang.NullPointerException: 
Cannot read field "c" because "a.b" is null
    at HelpfulNPE.main(HelpfulNPE.java:7)
java -XX:+ShowCodeDetailsInExceptionMessages HelpfulNPE

Objects.requireNonNull() (Java 7+)

用于参数校验,提前抛出清晰的异常信息。

import java.util.Objects;

public class User {
    private String name, email;
    public User(String name) {
        // 如果 name 为 null,立即抛出 NPE
        this.name = Objects.requireNonNull(name, "name cannot be null");
    }
    public void setEmail(String email) {
        this.email = Objects.requireNonNull(email, "email required");
    }
}

@NonNull / @Nullable 注解

配合 IDE 和静态分析工具,在编译期发现潜在的空指针问题。

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class UserService {
    public void processUser(@NotNull User user) {
        System.out.println(user.getName()); // IDE 会警告可能的 null
    }
    @Nullable
    public User findUser(String id) {
        return userMap.get(id); // 返回值可能为 null
    }
}

Checked Exception

Checked exceptions represent errors outside the control of the program. For example, the constructor of FileInputStream throws FileNotFoundException if the input file does not exist.

private static void checkedExceptionWithThrows() throws FileNotFoundException {
    File file = new File("not_existing_file.txt");
    FileInputStream stream = new FileInputStream(file);
}

Use a try-catch block to handle a checked exception.

继承中的异常规则

子类重写方法时,对异常的限制:

class Parent {
    void method() throws IOException, SQLException { }
}

class Child extends Parent {
    
    void method() { } // 可以不抛异常
    
    @Override
    void method() throws FileNotFoundException { }  // 可以抛出父类异常的子类
    
    // void method() throws InterruptedException { }  // 编译错误,不能抛出新的 checked exception
    
    void method() throws RuntimeException { } // 可以抛出任意 unchecked exception
}

Lambda 中的 Checked Exception

Lambda 表达式不能直接抛出 checked exception(以后再说)。

List<String> files = Arrays.asList("a.txt", "b.txt");

files.forEach(f -> new FileInputStream(f));  // 编译错误: Lambda 不能抛出 checked exception (IOException)

// 解决方案1:try-catch 包装为 unchecked exception
files.forEach(f -> {
    try {
        new FileInputStream(f);
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
});

Checked Exception 的争议

Pros

  • 编译期发现潜在问题,强制处理错误,提高代码健壮性
  • 异常成为方法签名的一部分,提供文档化

Cons

  • 代码冗长,try-catch 到处都是,很多时候只是简单吞掉或向上抛出
  • 破坏函数式编程(Lambda、Stream)
  • C#、Kotlin、Scala 等现代语言都不支持 checked exception

Checked Exception 的争议

"...on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth."

--Martin Fowler

(author of UML Distilled, Refactoring, and Analysis Patterns)

现代趋势:倾向于使用 OptionalResult 类型等函数式方法,而非 checked exception。

异常的性能开销

创建异常对象开销较大,因为需要填充完整的堆栈跟踪信息。

// 非常慢!
for (int i = 0; i < 1000000; i++) {
    try {
        throw new Exception();
    } catch (Exception e) { }
}
  • 异常用于真正的异常情况,不要用于控制流程
  • 高频场景考虑使用返回值:Optional<User> 而非抛出异常
  • 使用日志而非频繁抛出异常进行调试

为什么异常开销大?

throw new Exception();  // 这一行发生了什么?
  1. 创建 Exception 对象(分配内存)
  2. 遍历当前线程的整个调用栈
  3. 记录每一层的调用信息(类名、方法名、文件名、行号)并将这些信息存储在异常对象中

核心原因Throwable 构造函数会调用 fillInStackTrace() 方法,这是一个 native 方法,需要遍历整个调用栈。调用链越深,开销越大。

性能对比

// 测试1:创建 100万个异常对象, 约 3000-5000ms
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    try {
        throw new Exception();
    } catch (Exception e) { }
}
System.out.println("抛出异常: " + (System.currentTimeMillis() - start) + "ms");

// 测试2:创建 100万个普通对象, 约 10-50ms    相差 100-500 倍!
start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    new Object();
}
System.out.println("创建对象: " + (System.currentTimeMillis() - start) + "ms");

堆栈跟踪示例(每一层都要记录)

public class StackTraceExample {
    public static void main(String[] args) { method1(); }
    static void method1() { method2(); }
    static void method2() { method3(); }
    static void method3() { new Exception().printStackTrace(); }
}
java.lang.Exception
    at StackTraceExample.method3(StackTraceExample.java:5)
    at StackTraceExample.method2(StackTraceExample.java:4)
    at StackTraceExample.method1(StackTraceExample.java:3)
    at StackTraceExample.main(StackTraceExample.java:2)

优化方案

public class FastException extends Exception {
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;  // 不填充堆栈,速度提升 10-100 倍
    }
}
// 不好 - 用异常控制流程
try { Integer.parseInt(input); return true; } 
catch (NumberFormatException e) { return false; }

// 好 - 用正则或其他方式
return input.matches("\\d+");

实际应用建议

// 避免高频场景抛异常
public User findUser(String id) throws UserNotFoundException {
    User user = cache.get(id);
    if (user == null) throw new UserNotFoundException(); // 太慢!
    return user;
}

// 应该使用 Optional
public Optional<User> findUser(String id) {
    return Optional.ofNullable(cache.get(id));
}

核心原则:异常是为异常情况设计的,不是为正常的控制流程设计的!

finally

有一些代码片段,可能会希望无论 try 块中的异常是否抛出,它们都能得到执行。可以在异常处理程序后面加上 finally 子句。

try {
    // The guarded region: Dangerous activities
} catch(A a1) {
    // Handler for situation A
} finally {
    // Activities that happen every time
}

Why finally?

对于没有垃圾回收和析构函数自动调用机制的语言来说,finally 非常重要。它能使程序员保证:无论 try 块里发生了什么,内存总能得到释放。但 Java 有垃圾回收机制,所以内存释放不再是问题。而且,Java 也没有析构函数可供调用。那么,Java 在什么情况下才能用到 finally 呢?

当要把除内存之外的资源恢复到它们的初始状态时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。

自定义异常

class DividedByZeroException extends Exception{
    DividedByZeroException(){
        super("divided by zero");
    }
}
public class Calculator {
    public int div(int a, int b) throws DividedByZeroException {
        if (b == 0) {
            throw new DividedByZeroException();
        }
        return a / b;
    }
    ...
}

多重捕获

public class SameHandler {
    void x() throws Except1, Except2, Except3, Except4 {}
    void process() {}
    void f() {
        try {
            x();
        } catch(Except1 e) {
            process();
        } catch(Except2 e) {
            process();
        } catch(Except3 e) {
            process();
        } catch(Except4 e) {
            process();
        }
    }
}

组合捕获

public class MultiCatch {
    void x() throws Except1, Except2, Except3, Except4 {}
    void process() {}
    void f() {
        try {
            x();
        } catch(Except1 | Except2 | Except3 | Except4 e) {
            process();
        }
    }
}

Since Java 7

异常匹配

class SuperException extends Exception { }
class SubException extends SuperException { }
class BadCatch {
  public void goodTry() {
    try { 
      throw new SubException();
    } catch (SuperException superRef) { ...
    } catch (SubException subRef) {
      ...// never be reached
    } // an INVALID catch ordering
  }
}

栈轨迹

class DividedByZeroException extends Exception{
    DividedByZeroException(){ super("divided by zero"); }
}
public class Calculator {
    public int div(int a, int b) throws DividedByZeroException {
        if (b == 0) throw new DividedByZeroException();
        return a / b;
    }
    public static void main(String[] args) {
        try {
            System.out.println(new Calculator().div(
                Integer.parseInt(args[0]), Integer.parseInt(args[1])));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

栈轨迹输出

DividedByZeroException: divided by zero
        at Calculator.div(Calculator.java:9)
        at Calculator.main(Calculator.java:15)

重新抛出

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

catch(Exception e) {
    System.out.println("An exception was thrown");
    throw e;
}

Try-With-Resources

import java.io.*;
public class MessyExceptions {
    public static void main(String[] args) {
        InputStream in = null;
        try{ 
          in = new FileInputStream(new File("MessyExceptions.java"));
             int contents = in.read();
             // Process contents
        } catch(IOException e) { // Handle the error
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch(IOException e) { // Handle the close() error
                }
            }
        }
    }
}

Try-With-Resources

import java.io.*;
public class TryWithResources {
    public static void main(String[] args) {
        try(
            InputStream in = new FileInputStream(new File("TryWithResources.java"))
        ) {
            int contents = in.read();
        } catch(IOException e) {
            // Handle the error
        }
    }
}

Since Java 7 with AutoCloseable

AutoCloseable vs Closeable

// Closeable (Java 1.5) - 只用于 I/O
public interface Closeable extends AutoCloseable {
    void close() throws IOException;
}

// AutoCloseable (Java 7) - 更通用
public interface AutoCloseable {
    void close() throws Exception;  // 可以抛出任何异常
}
  • Closeable 只抛出 IOException,要求幂等性(多次调用无副作用)
  • AutoCloseable 可以抛出任何 Exception,适用于更广泛的场景

资源关闭顺序

try (
    FileInputStream fis = new FileInputStream("file1.txt");  // 1 第1个打开
    FileOutputStream fos = new FileOutputStream("file2.txt"); // 2 第2个打开
    BufferedReader br = new BufferedReader(new FileReader("file3.txt")) // 3 第3个打开
) {
    // 使用资源
}
// 关闭顺序:br → fos → fis (与声明顺序相反!)

原因:后面的资源可能依赖前面的资源,所以要先关闭后面的。

类似栈结构:后进先出(LIFO)

字节码层面的实现

try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 使用 fis
}

// 编译后等价于(简化版)
FileInputStream fis = new FileInputStream("file.txt");
Throwable primaryEx = null;
try {
    // 使用 fis
} catch (Throwable t) {
    primaryEx = t; throw t;
} finally {
    if (fis != null) {
        if (primaryEx != null) {
            try { fis.close(); } 
            catch (Throwable closeEx) { primaryEx.addSuppressed(closeEx); }
        } else { fis.close(); }
    }
}

Java 9 的改进

// Java 7-8:必须在 try() 中声明
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    return br.readLine();
}

// Java 9+:可以使用 effectively final 变量
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
try (br) {  // 只要引用,不需要重新声明
    return br.readLine();
}

// Java 9+ 多个资源更简洁
InputStream is = new FileInputStream("file1.txt");
OutputStream os = new FileOutputStream("file2.txt");
try (is; os) {  // 用分号分隔
    // 使用资源
}

常见错误

// 错误:资源在 try 外部创建
FileInputStream fis = null;
try (fis = new FileInputStream("file.txt")) {  // 编译错误!
}

// 错误:null 资源
try (FileInputStream fis = null) {  // 运行时 NullPointerException
}

// 正确:确保资源非 null
FileInputStream fis = new FileInputStream("file.txt");
try (fis) {  // Java 9+
}

自定义 AutoCloseable 最佳实践

public class DatabaseConnection implements AutoCloseable {
    private Connection conn;
    private boolean closed = false;
    
    @Override
    public void close() {
        if (closed) return;  // 1. 保证幂等性:多次调用安全
        
        try {
            if (conn != null && !conn.isClosed()) {
                conn.close();
            }
        } catch (SQLException e) {
            logger.error("Failed to close connection", e);  // 2. 记录日志,但不要抛出异常

        } finally {
            closed = true;  // 3. 标记为已关闭
        }
    }
}