Java-Learn

"Java 学习中 —— 异常、断言、日志和调试"

Posted by shihunyewu on May 29, 2018

第 11 章 异常、断言、日志和调试

如果由于程序出现了错误而使得某些操作没有完成,程序应该:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令
  • 允许用户保存所有操作的结果,并以适当的方式终止程序

11.1 异常

11.1.1 异常分类

  • Throwable 接口
    • Error,表示 java 在运行时的内部错误或者是程序错误,coder 对此基本无能为力,能做的只是安全的退出
    • Exception
      • IOException,程序没有问题,由于设备、文件和用户输入等其他不可控因素引起的异常
      • RuntimeException,程序有问题,一般是可以通过程序设计来避免。
        • 类型转换错误,可以在程序中先判断变量类型
        • 数据访问越界,可以在程序中规定访问范围
        • 访问空指针,可以在访问前先判空
        • … 其中 Error 和 RuntimeException 统称为未检查异常

11.1.2 声明已检查异常

这一点通常大多数的 IDE 都能够做到自动检测方法中已检查的异常,然后强制编程者在方法头部用throw XXException标出。

注意,当子类的覆盖了父类的一个方法,子类方法中声明的已检查异常不能比超类方法中声明的异常更通用。比如说超类抛出 IOException,子类只能抛出 IOException 的子类;另外,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常

11.1.3 抛出异常

关键是找到一个合适的异常类,问题描述得越精确越好。如果没有对应的异常类,可以自己创建异常类。

11.1.4 创建异常类

自创建的异常需要继承 Exception 或者是其子类,再或者是实现 Throwable 接口。一般来说自定义的异常需要两个构造函数

  • 参数为空的构造函数
  • 参数为 String 描述信息的构造函数

11.2 捕获异常

11.2.1 再次抛出异常

  • 关注点不同再次抛出的异常不同 现在的情景是你开发了一个供其他成员使用的子系统,那么用于表示系统故障的异常类型可能产生多种解释,但是该子系统的用户只关注这个子系统是否有问题,因此可以将子系统中的 IOException 这样的异常捕获后,重新封装一个子系统有关的异常。下面提供一个 Servlet 的案例
    try{
    access the database
    }catch(SQLException e){
      throw new ServletException("database error:" + e.getMessage());
    }
    

    虽然是 SQLException 异常,但是封装成了 ServletException 异常,告诉用户这是 Servlet 这个子系统的内部错误。

启示 我们在开发子系统的时候可以用这种方式来让用户区分他们自己编程造成的异常和子系统引起的异常。

finally 子句

finally 子句无论如何都会执行 即使是 try 子句中存在 return 语句,finally 也会执行,而且当 finally 也包含有 return 语句的时候,会覆盖掉 try 中的 return。

建议

独立使用try/catchtry/finally语句块,例如

try{
	try{
    	code
    }finally{
    	in.close();
    }
}
catch(IOException e){
	show error
}

这样写一方面逻辑清晰,即 in.close() 语句紧跟文件操作语句,让人很容易注意到我们做了文件关闭。另一方面,finally 子句关闭文件的时候也可能出现错误,catch 语句可以同时捕获 finally 中的异常。

11.2.4 带资源的 try 语句

try(Resource res = ...){
	work with res
}

try 子句执行完成后,会自动调用 res.close();这样不必再单独写 finally 子句来关闭资源了。close() 会将异常交给 try 语句。

11.2.5 堆栈跟踪

在调试的时候可以使用

11.3 使用异常机制的技巧

  1. 尽量提前检测,防止异常发生。在无法检测异常的地方,才使用捕获异常
  2. 不要过分细化每个异常,可以将可能发生异常的代码放在同一个 try 中,然后使用多个 catch 语句来捕获异常
  3. 利用异常结构,抛出更加有意义的异常,可以将异常封装成自己的异常
  4. 如果异常几乎不会发生,那么可以不抛出异常,并不对异常做处理。
  5. 检测错误的时候,尽量抛出异常,尽量不返回一个异常值
  6. 需要传递异常的时候就传递异常,有时候需要让更高层的用户知道异常

11.4 使用断言

断言主要用于测试阶段,在运行程序的时候可以通过参数来选择使用断言还是不使用。默认情况下断言是被禁用的,可以通过 -ea 选项开启,如下

java -ea MyApp
何时使用断言?

断言表示的是一种不可以恢复的异常,主要在测试阶段来测试参数或者是变量是否会出现非法的取值。开发者大多数使用断言来进行自我检查。

11.5 使用日志

日志相比于 println() 的好处:

  • 日志分级别,可以打印不同级别的信息
  • 日志可以过滤信息
  • 日志可以选择不打印到屏幕,不输出到文件
  • 日志可以选择不同的输出格式到文件
  • 日志一般有配置文件对其进行格式化

11.6 调试技巧

  • 打印变量值
  • 每个类中添加一个可以独立运行的 main 函数,用 main 函数来对类的功能进行单元测试
  • 可以使用一些测试框架,比如 junit
  • 日志代理
  • 利用 Throwable 的 printStackTrace 打印堆栈信息
  • 将错误信息使用 System.err 打印到屏幕,也可以将其重定向到文件
  • 使用 -verbose 参数启动虚拟机观察类加载过程
  • 使用虚拟机提供的调试工具,比如
    • jconsole,图形化界面,展示虚拟机运行期间的情况
    • jmap,堆的转储,显示了堆中的每个对象。
  • 使用 -Xprof(没听说过)