1. 处理错误
如果出现错误而导致一个操作无法完成,程序应该返回到安全状态,并允许用户执行其他的命令或保存所有工作,并妥善地终止程序。
Java 允许方法有一个备选的退出路径,如果方法没有正常运行,则选择该路径。方法将 抛出(throw) 一个封装了错误信息的对象(而不是正常情况下的返回一个值),然后异常处理机制开始搜索能够处理这种异常状况的 异常处理器(exception handler)。
1) 异常分类
异常对象都是扩展自 Throwable 类的类的实例,其下一级有 Error 类和 Exception 类两个分支,前者描述 Java 运行时系统的内部错误和资源耗尽问题,后者的下一层有 IOException 类和 RuntimeException 类两个分支,前者由输入输出等错误导致,后者由编程错误导致。
如果出现 RuntimeException,那么一定是程序员的问题。
RuntimeException 包括的部分问题:
- 错误的强制类型转换。
- 数组访问越界。
- 访问
null指针。
RuntimeException 不包括的部分问题:
- 试图越过文件末尾继续读取数据。
- 试图打开一个不存在的文件。
- 试图查找一个不存在的类。
非检查型(unchecked) 异常:扩展自 Error 或 RuntimeException 的异常。
检查型(checked) 异常:所有其他异常。
2) 声明检查型异常
public FileInputStream(String name) throws FileNotFoundException以上声明表示此构造器方法将要根据给定参数构造一个对象,但也可能出错并抛出 FileNotFoundException。如果真的出错,构造器将不会构造一个对象,而是抛出一个 FileNotFoundException 类对象,此时系统将搜索知道如何处理它的异常处理器。
以下 4 种情况将导致抛出异常:
- 调用抛出检查型异常的方法,例如,上文的
FileInputStream构造器。 - 检测到错误,并用
throw语句抛出一个检查型异常。 - 程序出错,例如,调用
a[-1] = 0将抛出一个非检查型异常。 - JVM 或运行时库出现内部错误。
在前两种情况下,要在方法的首部声明异常。
如果一个方法可能抛出多种检查型异常,则需要在其首部列出所有异常(用逗号分隔)。
不要声明非检查型异常,因为 RuntimeException 可以避免,Error 则在控制以外。
异常不仅可以声明,还可以 捕获(catch),如果选择捕获,异常将不会被方法抛出,也就不需要在其首部声明。
总之,要在方法首部声明所有可能抛出的检查型异常。
3) 如何抛出异常
readData() 读取一个文件,文件首部承诺其长度为 1024 个字符,然而读到 733 个字符后文件就结束了,此时希望抛出一个异常。阅读 Java API 文档后发现,EOFException 的描述匹配此情况。
throw new EOFException();String readData(Scanner in) throws EOFException {
// ...
while () {
if (!in.hasNext()) {
if (n < len) throw new EOFException();
}
// ...
} return s;
}EOFException 有一个带字符串参数的构造器,可以用它详细地描述异常:
String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFException(gripe);如果方法抛出了异常,它将不会返回到调用者,所以无需设定一个默认的返回值或错误码。
4) 创建异常类
如果找不到适用的异常类,可以自定义异常类。该类需属于 Exception 分支。
class FileFormatException extends IOException {
public FileFormatException() {
// ...
}
public FileFormatException(String gripe) {
super(gripe);
}
}2. 捕获异常
1) 捕获异常概述
try {
// 可能产生异常的代码
}
catch(*ExceptionType e) {
// 对此类异常的处理
}如果 try 语句块中的代码抛出了符合在 catch 子句中指定类型的异常,那么程序将:
- 跳过
try的其余代码。 - 执行
catch中的处理器代码。
如果方法抛出了不符合在 catch 中指定类型的异常,那么它将直接退出。
public void read(String filename) {
try {
var in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1)
// 处理输入
}
catch (IOException e) {
e.printStackTrace();
}
}情况简单时,可以如上处理。不过最好的选择是什么也不做,将异常传递给调用者,交给他处理。如果选择传递,则必须声明这个方法可能抛出的异常。
public void read(String filename) throws IOException {
var in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1)
// 处理输入
}捕获那些你知道如何处理的异常,传递那些你不知道如何处理的异常。
子类方法的 throws 列表中不允许出现同名超类方法未列出的异常,如果超类中的方法没有抛出异常,则子类方法必须捕获异常。
2) 捕获多个异常
一个 try 可以捕获多种异常并作出不同处理,为每种异常使用一个单独的 catch。
try {
// ...
}
catch (FileNotFoundException e) {
// ...
}
catch (UnknownHostException e) {
// ...
}
catch (IOException e) {
// ...
}异常对象可能包含关于异常的信息,调用 e.getMessage() 获取详细的错误消息,调用 e.getClass().getName() 获取异常对象的类型。
一个 catch 可以捕获多种异常,多个异常类型用分隔符(|)间隔(这样一来,对于这些异常都采取相同的处理)。
try {
// ...
}
catch (FileNotFoundException | UnknownHostException e) {
// ...
}
catch (IOException e) {
// ...
}3) 再次抛出异常与异常链
如果需要改变异常的类型,可以在 catch 中抛出一个异常。
P300-301
4) finally 子句
方法抛出异常时,其中的剩余代码就不再执行。但是有时它已经获得了一些资源,需要完成资源清理,finally 子句 可以解决此问题。
无论是否有异常抛出,finally 都将执行。
FileInputStream in = new FileInputStream()
try {
// 1
// 可能抛出异常的代码
// 2
}
catch (IOException e) {
// 3
// 显示错误信息
// 4
}
finally {
// 5
in.close();
}
// 6执行以上代码时 3 种可能的情况:
- 没有抛出异常。程序首先执行
try的全部,然后执行finally,执行顺序 1、2、5、6。 - 抛出一个异常,并被一个
catch捕获。程序首先执行try直至异常抛出,然后执行对应catch,最后执行finally。如果catch没有再次抛出异常,将执行finally之后的第一条语句,执行顺序 1、3、4、5、6;如果catch又抛出一个异常,异常将被抛回方法调用者,执行顺序 1、3、5。 - 抛出一个异常,但没有被任何
catch捕获。程序首先执行try直至异常抛出,然后执行finally,异常将被抛回方法调用者,执行顺序 1、5。
try 可以只有 finally 而没有 catch。
不要把改变控制流程的语句(return,throw,break,continue)放在 finally 中。
public static int parseInt(String s) {
try {
return Integer.parseInt(s);
}
finally {
return 0;
}
}调用 parseInt("5") 似乎将返回 5,不过在这个方法真正返回之前,finally 将执行,导致返回 0。更糟的情况下,调用 parseInt("zero"),方法 Integer::parseInt 将抛出 NumberFormatException,然后执行 finally 返回 0,return 语句直接吞掉了这个异常,导致调用者对此毫不知情(看上去正常地返回了 0)。
5) try-with-Resources 语句
// 打开资源
try {
// 处理资源
}
finally {
// 关闭资源
}如果该资源属于实现了 AutoCloseable 接口的类,对于以上模式有一个简便写法。
void close() throws Exceptiontry-with-resources 语句的最简形式为:
try (*ResourceType res = ...) {
// 处理资源
}try 块退出时将自动调用 res.close()。
例如,读取一个文件中的所有单词:
try (var in = new Scanner(Path.of("in.txt"), StandardCharsets.UTF_8)) {
while (in.hasNext())
System.out.println(in.next());
}无论这段代码如何退出,都将调用 in.close(),就好像使用了 finally。
可以指定多个资源:
try (var in = new Scanner(Path.of("in.txt"), StandardCharsets.UTF_8); var out = new PrintWriter("out.txt", StandardCharsets.UTF_8)) {
while (in.hasNext()) out.println(in.next().toUpperCase());
}无论这段代码如何退出,in、out 都将关闭,如果用常规方式,需要两个嵌套的 try / finally。
可以在 try 首部提供事先声明的事实最终变量:
public static void printAll(String[] lines, PrintWriter out) {
try (out) {
for (String line : lines) out.println(line);
}
}6) 分析栈轨迹元素
栈轨迹(stack trace):一个程序执行过程中某个特定点上所有挂起的方法调用的列表。
调用方法 Throwable::printStackTrace 访问栈轨迹的文本描述信息:
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
StackWalker类见 P305
3. 使用异常的技巧
- 异常处理不能代替简单的测试。
假设有一段代码尝试将一个空栈弹出 10000000 次,第一种做法是先检查是否为空:
if (!s.empty()) s.pop();第二种做法是不论如何先执行弹出操作,再捕获 EmptyStackException 来提醒我们本不该这样做:
try {
s.pop();
}
catch (EmptyStackException e) {
}后者的时间开销为前者的数十倍,可见完成简单的测试比先运行再捕获异常效率高很多。
- 不要过分地细化异常。
很多程序员将每一条语句都分装在单独的 try 语句块中:
for (int i = 0; i < 100; i ++)
{
try
{
n = s.pop();
}
catch (EmptyStackException e)
{
}
try
{
out.writeInt(n);
}
catch (IOException e)
{
}
}这种风格将导致代码量的急剧膨胀,合理的做法是将整个任务包在一个 try 语句块中:
try {
for (int i = 0; i < 100; i ++) {
n = s.pop();
out.writeInt(n);
}
}
catch (EmptyStackException e) {
}
catch (IOException e) {
}- 合理利用异常层次结构。
不要只抛出 RuntimeException,而应寻找一个合适的子类或创建自己的异常类。
不要只捕获 Throwable 异常,否则代码可读性和可维护性将降低。
如果能将一种异常转换为另一种更合适的异常,那么不要犹豫。
-
不要压制异常。
-
在检测错误时,“苛刻”要比放任更好。
当栈为空时,使 Stack.pop() 抛出异常比使其返回 null 更好。
- 不要羞于传递异常。
很多情况下,传递异常给高层方法比自己捕获更好。
-
使用标准方法报告
null指针和越界异常。 -
用
Object类包含的方法完成参数检验。 -
不要向最终用户显示栈轨迹。
栈轨迹可能包含实现细节,暴露给攻击者将带来风险,应将其记入日志以便之后获取,而只向用户显示一条总结消息。
4. 使用断言
1) 断言的概念
假设需要调用 Math.sqrt(x),首先检查 x 非负:
if (x < 0) throw new IllegalArgumentException("x < 0");测试完成后这段代码仍然留在程序中,如果有大量的测试代码,程序运行速度将变慢。
断言(assertion) 机制允许在测试期间向代码插入一些检查,而在生产代码中自动将其删除。
关键字 assert 的两种格式:
assert *condition;
// 或
assert *condition : *expression;两种语句都将计算 *condition,如果结果为 false,则抛出 AssertionError。第二种语句中,*expression 将传入 AssertionError 对象的构造器,并转换为一个消息字符串。
对上例,断言 x 非负写为:
assert x >= 0;或将 x 的值传递给 AssertionError 对象,以便之后显示:
assert x >= 0 : x;2) 启用和禁用断言
默认情况下断言是禁用的,可以在程序运行时用 -enableassertions 或 -ea 选项启用断言。
可以为特定的类或包启用断言:
java -ea:MyClass -ea:com.mycompany.mylib MyApp用 -disableassertions 或 -da 选项禁用断言。
3) 使用断言完成参数检查
P313-314
4) 使用断言提供假设文档
P314-315
5. 日志
1) 基本日志
对于简单的日志记录,可以使用全局日志记录器(global logger)并调用其方法 info():
Logger.getGlobal().info("File->Open menu item selected");在默认情况下将打印此记录:
May 10, 2013 10:12:15 PM LoggingImageViewer fileOpen
INFO: File->Open menu item selected在适当位置(如 main() 最前端)调用 Logger.getGlobal().setLevel(Level.off) 将抑制所有日志。
2) 高级日志
定义自己的日志记录器,可以调用 getLogger() 创建或获取一个日志记录器:
private static final Logger myLogger = Logger.getLogger("com.company.app")日志的 7 个级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
默认只记录前 3 个级别,也可以手动设置记录级别,如 logger.setLevel(Level.FINE),现在将记录 FINE 及更高级别。Level.ALL 将开启全部级别,Level.OFF 将关闭全部级别。
每个级别都对应有日志记录方法,如 logger.info(message)、logger.fine(message),也可以使用 log() 并指定级别,如 logger.log(Level.FINE, message)。