前端视角 Java Web 入门手册 2.5:Java Core ——异常处理

235 阅读7分钟

异常(Exception)是程序在运行过程中发生的事件,通常导致程序的正常流程中断。Java通过异常处理机制,允许开发者捕获并处理这些异常,确保程序能够在遇到错误时表现得更加优雅和可靠。

异常分类

在Java中 Throwable 是 Java 中所有错误和异常的超类,下层是 Exception 和 Error

Exception

Exception 是程序正常运行中,可以预料的意外情况,比如数据库连接中断,空指针,数组下标越界。异常出现可以导致程序非正常终止,也可以预先检测被捕获处理掉,使程序继续运行,Exception 有两类

CheckedException

CheckedException是部分方法在声明时候使用 throws 关键字声明可能出现的异常,编译器会强制要求程序员对此类异常进行捕获或声明抛出,需要使用 try/catch、try-with-resources 等处理异常或者继续抛出异常,否则编译器就会报错。常见的已检查异常包括 IOExceptionSQLException

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try {
            File file = new File("input.txt");
            Scanner scanner = new Scanner(file);
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                System.out.println(line);
            }
            scanner.close();
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        }
    }
}

Scanner 主动声明编译异常

public Scanner(File source) throws FileNotFoundException {
    this((ReadableByteChannel)(new FileInputStream(source).getChannel()));
}

如果使用 Scanner 没有用 try/catch 包裹,编译器会报错

RuntimeException

RuntimeException也被称为 UncheckedException,通常由程序逻辑错误引起,比如空指针、下标越界等,运行时异常应该在程序测试期间被暴露出来,由程序员去解决,而避免使用捕获的方式(实际开发时很难做到这点,还是会用 try/catch 保障程序稳定性)

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length()); // 会抛出 NullPointerException
    }
}

Error

Error 表示更严重的错误,一般是 Java 运行时系统内部错误或资源耗尽错误,常见的有内存溢出、JVM 虚拟机非正常运行等,Error 会导致程序无法运行,应用程序不会主动抛出 Error,也无法处理 Error,出现后除了告知用户就是尽力使程序安全终止

异常处理机制

Java 提供了一套完整的异常处理机制,通过使用 try-catch-finally 语句块和 throw/throws 关键字,开发者可以有效地捕获和处理异常

try...catch...finally

finally 用于执行一定要执行的代码,无论是否发生异常。常用于释放资源,如关闭文件、数据库连接等。finally 可以省略

public class FinallyExample {
    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader("file.txt"));
            String line = reader.readLine();
            System.out.println(line);
        } catch (IOException e) {
            System.out.println("文件读取出错: " + e.getMessage());
        } finally {
            try {
                if (reader != null) reader.close();
            } catch (IOException e) {
                System.out.println("关闭文件出错: " + e.getMessage());
            }
        }
    }
}

try with resources

try-with-resources 是 Java 7 引入的一个语法糖,用于自动关闭实现了 AutoCloseable 接口的资源,如文件、数据库连接等,用以代替传统的 try-catch-finally 方法来关闭资源,使代码更简洁、可读性更高

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        // 括号中声明并初始化需要关闭的资源
        try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("文件读取或关闭出错: " + e.getMessage());
        }
    }
}

当 try 块结束时 Java 会自动调用 BufferedReader 对象的 close() 方法来关闭它。如果读取文件时发生异常,catch 块中的代码会处理异常,并且 BufferedReader 对象也会被自动关闭

throw 与 throws

throw 用于在方法内部主动抛出异常,throws 在方法签名中声明该方法可能抛出的异常,提醒调用者需要处理这些异常

public class ThrowsExample {
    public static void main(String[] args) {
        try {
            processFile("file.txt");
        } catch (IOException e) {
            System.out.println("处理文件出错: " + e.getMessage());
        }
    }

    public static void processFile(String fileName) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(fileName));
        String line = reader.readLine();
        System.out.println(line);
        reader.close();
    }
}

自定义异常

除了Java提供的内置异常,开发者还可以根据业务需求创建自定义异常类,以更准确地表达错误状态和处理逻辑。

  1. 继承异常类:自定义异常类需要继承自 Exception 类(受检查异常)或 RuntimeException 类(运行时异常)。通常如果希望调用者必须处理该异常则继承 Exception 类;如果希望异常可以在运行时被抛出而不需要显式处理则继承 RuntimeException
  2. 添加构造方法:为自定义异常类添加构造方法,以便在抛出异常时可以传递错误信息
// 创建一个已检查异常
public class InvalidUserInputException extends Exception {
    public InvalidUserInputException(String message) {
        super(message);
    }
}

// 创建一个未检查异常
public class DataProcessingException extends RuntimeException {
    public DataProcessingException(String message) {
        super(message);
    }
}

使用自定义异常

public class CustomExceptionExample {
    public static void main(String[] args) {
        try {
            validateAge(15);
        } catch (InvalidUserInputException e) {
            System.out.println("验证失败: " + e.getMessage());
        }
    }

    public static void validateAge(int age) throws InvalidUserInputException {
        if (age < 18) {
            throw new InvalidUserInputException("年龄不能小于18岁!");
        }
    }
}

NPE 克星——Optional

使用 null 对象的方法或属性时候就会产生空指针异常,也就是大名鼎鼎的 NPE NullPointerException

Optional 类是在 Java 8 中引入的,作为java.util包的一部分,旨在用更优雅地处理可能为空的值,从而减少代码中的空指针异常

Optional<String> optional = Optional.ofNullable(null);
String value = optional.orElseGet(() -> "计算得到的默认值"); // "计算得到的默认值"

创建 Optional 对象

// 如果传入的值为null,则会抛出 NullPointerException
Optional<String> nonEmptyOptional = Optional.of("Hello");

// 如果传入的值为null,则返回一个空的Optional
Optional<String> nullableOptional = Optional.ofNullable(null); 

// 创建一个空的Optional,不包含任何值
Optional<String> emptyOptional = Optional.empty();

检查值是否存在

如果 Optional 中包含一个值,isPresent()返回 true,否则返回 false

Optional<String> optional = Optional.of("Java");
if (optional.isPresent()) {
    System.out.println("值存在");
}

在 Java 11 后可以使用 isEmpty(),相当于 isPresent()的反义

Optional<String> optional = Optional.empty();
if (optional.isEmpty()) {
    System.out.println("Optional is empty");
}

获取值

使用 get()可以获取 Optional 中的值,如果没有值则抛出 NoSuchElementException

Optional<String> optional = Optional.of("World");
String value = optional.get(); // "World"

直接使用get()可能导致异常,一般会使用orElse()设置默认值

Optional<String> optional = Optional.ofNullable(null);
String value = optional.orElse("默认值");

或者使用orElseGet()通过 Supplier 函数计算默认结果

Optional<String> optional = Optional.ofNullable(null);
String value = optional.orElseGet(() -> "计算得到的默认值");

操作 Optional 对象

map:如果 Optional 中有值,应用提供的映射函数并返回包含结果的新的 Optional,否则返回空的 Optional

Optional<String> optional = Optional.of("Java");
Optional<Integer> lengthOptional = optional.map(String::length); // Optional[4]

flatMap:与 map 类似,但映射函数返回一个 Optional,会直接展开该 Optional,避免形成嵌套

import java.util.Optional;

public class OptionalFlatMapExample {
    public static void main(String[] args) {
        Optional<String> optionalEmail = findNameById(1)
            .flatMap(name -> findEmailByName(name));

        // 用户邮箱: alice@example.com
        optionalEmail.ifPresent(email -> System.out.println("用户邮箱: " + email));
    }

    public static Optional<String> findNameById(int id) {
        if (id == 1) {
            return Optional.of("Alice");
        } else {
            return Optional.empty();
        }
    }

    public static Optional<String> findEmailByName(String name) {
        if ("Alice".equals(name)) {
            return Optional.of("alice@example.com");
        } else {
            return Optional.empty();
        }
    }
}

如果函数使用的是 map 而不是 flatMap

Optional<Optional<String>> nestedOptionalEmail = findNameById(1)
.map(name -> findEmailByName(name));

// 输出: Optional[Optional[alice@example.com]]
System.out.println(nestedOptionalEmail); 

这样会得到一个嵌套的 Optional<Optional<String>>,这在实际使用中并不方便。而 flatMap 可以避免这种情况,直接返回 Optional<String>

filter:如果 Optional 中有值,并且该值满足给定的谓词条件,则返回 Optional 本身;否则返回空的 Optional

Optional<String> optional = Optional.of("Java");
Optional<String> filtered = optional.filter(s -> s.startsWith("J")); // Optional["Java"]
Optional<String> emptyFiltered = optional.filter(s -> s.startsWith("X")); // Optional.empty()

isPresent:如果 Optional 中有值,执行提供的消费者操作

Optional<String> optional = Optional.of("Java");
optional.ifPresent(System.out::println); // 输出: Java

最佳实践

  • 不要使用异常捕获进行流程控制
  • 不要捕获 Throwable 或 Error
  • 捕获具体的子类而不是捕获 Exception 类,抛出具体定义的检查性异常而不是 Exception
  • 不要在 fianlly 块中使用 return。try 块中的 return 语句执行成功后并不立马返回,而是继续执行 finally 里面的语句,如果 finally 中包含 return 语句,会导致 try 中的 return 失效
  • 早 throw 晚 catch。在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常。同时在 catch 块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息
  • 自定义异常不要丢失堆栈追踪,使用日志框架(如 Log4j、SLF4J)记录异常信息,而不是仅在控制台输出,有助于后续的监控和分析
catch (ClassNotFoundException e) {
   throw new MyException("Something wrong: " + e.getMessage());  // 错误方式
}

catch (ClassNotFoundException e) {
   throw new MyException("Something wrong: " + e);  // 正确方式
}