异常

  • 所有的异常都是由Throwable继承而来, 下一层有两个分支, ErrorException
    • Error类描述了Java运行时系统的内部错误和资源耗尽问题, 一般不要抛出这个类型
    • Exception层次分为两个分支, RuntimeException和其他异常
      • 编程错误导致的是RuntimeException
      • 由于IO错误导致的是其他异常
      • 继承自RuntimeException异常包括: 错误的强制类型转换; 数组越界; 访问null指针
      • 不继承自RuntimeException异常包括: 打开不存在的文件; 越过文件末尾读取数据; 根据给定字符串查找Class, 但是这个类并不存在
    • 所有派生于ErrorRuntimeException的异常是非检查型异常, 其他异常都是检查型异常

抛出异常的情况

  1. 调用了某个会抛出异常的方法, 比如FileInputStream构造器
  2. 检测到一个错误, 使用throw语句抛出异常
  3. 程序出现错误, 比如a[-1]抛出一个非检查型异常
  4. JVM内部错误
  • 一个方法必须声明所有可能抛出的检查型异常, 也可以捕获一个异常, 这样也不需要抛出了
  • 如果子类覆盖了超类的一个方法, 子类抛出的异常不能比超类的更通用, 如果超类没有抛出异常, 则子类也不能抛出异常

创建异常

  • 定义一个派生于Exception或者他子类的一个类, 包含两个构造器, 一个是无参构造器, 另一个是包含详细信息的构造器
    1
    2
    3
    4
    5
    6
    class FileFormatException extends IOException {
    public FileFormatException(){};
    public FileFormatException(String gripe) {
    super(gripe);
    }
    }

捕获异常

  • 如果try语句块中任何代码抛出了catch指定的一个异常类
    1. 跳过try语句块剩余执行内容
    2. 执行catch语句块代码
  • 如果没有抛出异常, 则直接跳过catch部分
  • 如果抛出了异常, 但是不在catch中, 则方法会直接退出
  • 一般是捕获知道如何处理的异常, 抛出不知道如何处理的异常
  • 一个try语句块可能抛出多种不同的异常, 每个异常需要一个catch语句块
    • 如果两个异常的捕获动作一样的话, 可以使用catch(Exception e1 | Exception e2)合并
    • 捕获多个变量时, 异常变量银行了final

再次抛出

  • 有时候只想记录一个异常, 再次抛出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try {
    ...
    } catch (Exception e) {
    logger.log(level, message, e);
    throw e;
    }
    // 如果这段代码存在于 public void update() throws SQLException中
    // 在Java 7之前会报错, 因为 throw e可能抛出其他类型的异常, 而不是SQLException
    // 现在改变了, 编译器会跟踪到e来自try代码块, try代码块中仅有的检查型异常是SQLException实例, 并且
    // e在catch块中没有改变, 那么外围方法声明为throws SQLException就是合法的

finally子句

  • 代码抛出异常, 剩下的代码就不会运行, 如果这时候已经获取到了一些资源, 在退出之前需要释放.

    • 可以先捕获所有异常, 然后释放资源, 再重新抛出异常
    • 也可以使用finally子句, 无论是否抛出异常, finally子句部分一定都会执行
    • Java 7之后可以使用try-with-resources, 这个更常用
  • try语句可以只有finally, 没有catch

  • finally语句中不要放throw, continue, break, return这种改变程序执行顺序的语句

try-with-resources

  • AutoCloseable接口有一个方法 void close() throwa Exception
  • Closeable接口是AutoCloseable接口的子接口, 同样只包含close(), 但是抛出的是IOException
  • try-with-resources语句的最简单形式是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try (Resources res = ...) {
    work with res
    }
    // 这里try块退出时, 会自动调用res.close()
    try (var in = new Scanner(Path.of("1.txt"), StandardCharsets.UTF_8);
    var out = new PrintWriter("2.txt", StandardCharsets.UTF_8)) {
    while (in.hasNext()) {
    out.println(in.next().toUpperCase());
    }
    }
    // 上述代码不论是如何退出的, 都一定会自动关闭in和out
  • 如果是try-catch-finally语句, 在try中抛出了异常, 然后finally调用in.close()又抛出了异常就会产生问题
    • 此时使用try-with-resources就可以解决这个问题
    • 原来的异常会重新抛出, close()产生的异常会被抑制, 自动捕获, 由addSuppressed() 添加到原来的异常方法中
    • 可以调用getSuppressed(), 会生成一个数组, 包含其中从close()方法中抛出的被抑制的异常
  • try-with-resources语句本身可以有catch, finally语句, 这些子句会在关闭资源以后才执行

栈轨迹

  • 栈轨迹是程序执行中某个特定点所有挂起的方法调用的一个列表
  • Throwable类的printStackTrace()可以打印
    1
    2
    3
    4
    5
    6
    7
    var t = new Throwable();
    var out = new StringWriter();
    t.printStackTrace(new PrintWriter(out));
    String des = out.toString();
    // Java 9之前, Throwable.printStackTrace()会生成一个StackTraceElement[]
    // 数组中包含了和StackWalker.StackFrame类似的信息, 效率较低
    // 因为会得到整个栈, 但是调用者只需要几个栈帧, 并且只允许访问挂起方法的类名, 不能访问类对象
  • 还可以使用StackWalker类, 生成一个StackWalker.StackFrame实例流, 其中每个实例表示一个栈帧
    1
    2
    StackWalker walker = StackWalker.getInstance();
    walker.forEach(frame -> ananlyze frame)
  • 栈轨迹一般显示在System.err上, 如果想要记录栈轨迹, 可以捕获到字符串中
  • 也可以记录到文件中, 不过如果是错误的话, 就会发送到System.err中, 所以就不能使用下面的代码
    • java MyApp > errors.txt, 而是应该使用 java MyApp 2> errors.txt
    • 如果需要在同一个文件中同时保存System.out, System.err可以使用如下代码
    • java MyApp 1> errors.txt 2>&1
  • 可以使用静态方法Thread.setDefaultUncaughtExceptionHandler改变没有捕获异常的处理器
  • 启动JVM可以使用-verbose看到类加载器加载过程
  • Xlint选项可以告诉编译器找出常见的代码问题, 比如javac -Xlint sourceFiles
  • JDK提供了jconsole可以显示JVM性能统计结果

使用异常技巧

  1. 异常不能代替简单测试, 使用捕获异常会导致程序耗时大大增加, 因此只在异常情况下使用异常
  2. 不要过分细化异常, 否则一个异常一个catch会导致代码量激增
  3. 合理使用异常层次
    • 不要只抛出RuntimeException, 应该需要寻找一个合适的子类, 或者创建自己的异常类
    • 不要只捕获Throwable异常, 否则代码会很难读懂
    • 考虑检查型异常和非检查型异常, 检查型异常本质上开销较大
  4. 不要压制异常, 可以使用
  5. 使用标准方法报告null指针和越界异常
  6. 不要向用户展示最终的栈轨迹

断言

  • 断言允许在测试期间在代码中插入一些检查, 在生产代码中自动删除这些
    1
    2
    3
    4
    assert condition;
    assert condition : expression;
    // 两个写法都会计算condition, 如果为false, 会抛出AssertionError异常
    // 第二个语句会将expression传入到AssertionError构造器中, 转换为一个消息字符串
  • 默认情况下禁用断言, 运行是使用java -enableassertions MyApp 或者java -ea MyApp启用
  • 不需要重新编译启用断言, 因为断言是类加载器的功能, 禁用断言的时候类加载器会自动删去断言的代码, 不会降低速度
  • 可以在特定的类或整个包中打开断言java -ea:MyClass -ea:com.mycompany.mylib MyApp
    • 这样会在MyClass类, com.mycompany.mylib包及其子包中的所有类打开断言
    • 同样可以使用-da 或者-disableassertions禁用断言
    • -ea-da不能应用于没有类加载器的系统类, 需要使用-esa或者enablesystemassertions开启系统类断言