乐趣区

Java核心技术笔记 异常、断言和日志

《Java 核心技术 卷Ⅰ》第 7 章 异常、断言和日志

处理错误
捕获异常
使用异常机制的技巧
记录日志

处理错误
如果由于出现错误而是的某些操作没有完成,程序应该:

返回到一种安全状态,并让用户执行一些其他操作;或者
允许用户保存所有操作,并以妥善方式终止程序

检测(或引发)错误条件的代码通常离:

能让数据恢复到安全状态
能保存用户的操作结果并正常退出程序

的代码很远。
异常处理的任务:将控制权从错误产生地方转移给能够处理这种情况的错误处理器。
异常分类
在 Java 中,异常对象都是派生于 Throwable 类的一个实例,如果 Java 中内置的异常类不能满足需求,用户还可以创建自己的异常类。
Java 异常层次结构:

Throwable

Error

Exception

IOException

RuntimeException

可以看到第二层只有 Error 和 Exception。
Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误,应用程序不应该抛出这种类型的对象,这种内部错误的情况很少出现,出现了能做的工作也很少。
设计 Java 程序时,需关注 Exception 层次结构,这个层次又分为两个分支,RuntimeException 和包含其他异常的 IOException。
划分两个分支的规则是:

由程序错误导致的异常属于 RuntimeException

程序本身无问题,由于像 I / O 错误这类问题导致的异常属于其他异常 IOException

派生于 RuntimeException 的异常包含下面几种情况:

错误类型转换
数组访问越界
访问 null 指针

派生于 IOException 的异常包含下面几种情况:

试图在文件尾部后面读取数据
试图打开一个不存在的文件
试图根据指定字符串查找 Class 对象,而这个字符串表示的类并不存在

Java 语言规范将派生于 Exception 类和 RuntimeException 类的所有异常统称非受查(unchecked)异常,所有其他异常都是受查(checked)异常。
编译器将核查是否为所有的受查异常提供了异常处理器。
声明受查异常
一个方法不仅要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
异常规范(exception specification):方法应该在其首部声明所可能抛出的异常。
public FileInputStream(String name) throws FileNotFoundException
如果这个方法抛出了这样的异常对象,运行时系统会开始搜索异常处理器,以便知道如何处理这个异常对象。
当然不是所有方法都需要声明异常,下面 4 种情况应该抛出异常:

调用一个抛出受查异常的方法时
程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常
程序出现错误,一般是非受查异常
Java 虚拟机和运行时库出现的内部错误

出现前两种情况之一,就必须告诉调用者这个方法可能的异常,因为如果没有处理器捕获这个异常,当前执行的线程就会结束。
如果一个方法有多个受查异常类型,就必须在首部列出所有的异常类,异常类之间用逗号隔开:
class MyAnimation
{

public Image loadImage(String s) throws FileNotFoundException, EOFException
{

}
}
但是不需要声明 Java 的内部错误,即从 Error 继承的错误。
关于子类和超类在这部分的问题:

子类方法声明的受查异常不能比超类中方法声明的异常更通用(即子类能抛出更特定的异常或者根本不抛出任何异常)
如果超类没有抛出任何受查异常,子类也不能

如果类中的一个方法声明抛出一个异常,而这个异常是某个特定类的实例时:

这个方法可能抛出一个这个类的异常(比如 IOExcetion)
或抛出这个类的任意一个子类的异常(比如 FileNotFoundException)

如何抛出异常
假设程序代码中发生了一些很糟糕的事情。
首先要决定应该抛出什么类型的异常(通过查阅已有异常类的 Java API 文档)。
抛出异常的语句是:
throw new EOFException();
// 或者
EOFException e = new EOFException();
throw e;
一个名为 readData 的方法正在读取一个首部有信息 Content-length: 1024 的文件,然而读到 733 个字符之后文件就结束了,这是一个不正常的情况,希望抛出一个异常。
String readData(Scanner in) throws EOFException
{

while(…)
{
if(!in.hasNext()) // EOF encountered
{
if(n < len)
throw new EOFException();
}

}
return s;
}
EOFException 类还有一个含有一个字符串类型参数的构造器,这个构造器可以更加细致的描述异常出现的情况。
String gripe = “Content-length:” + len + “, Received:” + n;
throw new EOFException(gripe);
对于一个已经存在的异常类,将其抛出比较容易:

找到一个合适的异常类
创建这个类的一个对象
将对象抛出

一旦抛出异常,这个方法就不可能返回到调用者,即不必为返回的默认值或错误代码担忧。
创建异常类
实际情况中,可能会遇到任何标准异常类不能充分描述的问题,这时候就应该创建自己的异常类。
需要做的只是定义一个派生于 Exception 的类,或者派生于 Exception 子类的类。
习惯上,定义的类应该包含两个构造器:

一个是默认的构造器
另一个是带有详细描述信息的构造器(超类 Throwable 的 toString 方法会打印出这些信息,在调试中有很多用)

class FileFormatException extends IOException
{
public FileFormatException() {}
public FileFormatException(String gripe)
{
super(gripe);
}
}
捕获异常
捕获异常
要想捕获一个异常,必须设置 try/catch 语句块。
try
{
code

}
catch(ExceptionType e)
{
handler for this type
}
如果 try 语句块中任何代码抛出了一个在 catch 子句中说明的异常类,那么:

程序将跳过 try 语句块的其余代码
程序将执行 catch 子句中的处理器代码

如果没有代码抛出任何异常,程序跳过 catch 子句。
如果方法中的任何代码抛出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立即退出。
// 读取数据的典型代码
public void read(String filename)
{
try
{
InputStream in = new FileInputStream(filename);
int b;
while((b = in.read()) != -1)
{
// process input

}
}
catch(IOException exception)
{
exception.printStackTrace();
}
}
read 方法有可能抛出一个 IOException 异常,这种情况下,将跳出整个 while 循环,进入 catch 子句,并生成一个栈轨迹。
还有一种选择就是什么也不做,而是将异常传递给调用者。
public void read(String filename) throws IOException
{
InputStream in = new FileInputStream(filename);
int b;
while((b = in.read()) != -1)
{
// process input

}
}
编译器严格地执行 throws 说明符,如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。
两种方式哪种更好?
通常,应该捕获那些知道如何处理的异常,将那些不知道怎么样处理的异常进行传递。
这个规则也有一个例外:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常;并且不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。
捕获多个异常
为每个异常类型使用一个单独的 catch 子句:
try
{
code

}
catch(FileNotFoundException e)
{
handler for missing files
}
catch(UnknownHostException e)
{
handler for unknown hosts
}
catch(IOException e)
{
handler for all other I/O problems
}
异常对象可能包含与异常相关的信息,可以使用 e.getMessage() 获得详细的错误信息,或者使用 e.getClass().getName() 得到异常对象的实际类型。
在 Java SE 7 中,同一个 catch 子句中可以捕获多个异常类型,如果动作一样,可以合并 catch 子句:
try
{
code

}
catch(FileNotFoundException | UnknownHostException e)
{
handler for missing files and unknown hosts
}
catch(IOException e)
{
handler for all other I/O problems
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
再次抛出异常与异常链
在 catch 子句中可以抛出一个异常,这样做的目的是改变异常的类型。
try
{
access the database
}
catch(SQLException e)
{
throws new ServletException(“database error:” + e.getMessage());
}
ServletException 用带有异常信息文本的构造器来构造。
不过还有一种更好的处理方法,并将原始异常设置为新异常的“原因”:
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServletException(“database error”);
se.initCause(e);
throw se;
}
当捕获到异常时,可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
finally 子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。
如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收(比如数据库连接的关闭),那么就会产生资源回收问题。
一种是捕获并重新抛出所有异常,这种需要在两个地方清除所分配的资源,一个在正常代码中,另一个在异常代码中。
Java 有一种更好地解决方案,就是 finally 子句。
不管是否有异常被捕获,finally 子句的代码都会被执行。
InputStream in = new FileInputStream(…);
try
{
// 1
code that might throw exception
// 2
}
catch(IOException e)
{
// 3
show error message
// 4
}
finally
{
// 5
in.close();
}
// 6
上面的代码中,有 3 种情况会执行 finally 子句:

代码没有抛出异常,执行序列为 1、2、5、6

抛出一个在 catch 子句中捕获的异常

如果 catch 子句没有抛出异常,执行序列为 1、3、4、5、6
如果 catch 子句抛出一个异常,异常将被抛回这个方法的调用者,执行序列为 1、3、5(注意没有 6)

代码抛出了一个异常,但是这个异常没有被捕获,执行序列为 1、5

try 语句可以只有 finally 子句,没有 catch 子句。
有时候 finally 子句也会带来麻烦,比如清理资源时也可能抛出异常。
如果在 try 中发生了异常,并且被 catch 捕获了异常,然后在 finally 中进行处理资源时如果又发生了异常,那么原有的异常将会丢失,转而抛出 finally 中处理的异常。
这个时候的一种解决办法是用局部变量 Exception ex 暂存 catch 中的异常:

在 try 中进行执行的时候加入嵌套的 try/catch,并在 catch 中暂存 ex 并向上抛出
在 finally 中处理资源的时候加入嵌套的 try/catch,并且在 catch 中进行判断 ex 是否存在来进一步处理

InputStream in = …;
Exception ex = null;
try
{
try
{
code that might throw exception
}
catch(Exception e)
{
ex = e;
throw e;
}
}
finally
{
try
{
in.close();
}
catch(Exception e)
{
if(ex == null)throw e;
}
}
下一节会介绍,Java SE 7 中关闭资源的处理会容易很多。
带资源的 try 语句
对于以下代码模式:
open a resource
try
{
work with the resource
}
finally
{
close the resource
}
假设资源属于一个实现了 AutoCloseable 接口的类,Java SE 7 位这种代码提供了一个很有用的快捷方式,AutoCloseable 接口有一个方法:
void close() throws Exception
带资源的 try 语句的最简形式为:
try(Resource res = …)
{
work with res
}
try 块退出时,会自动调用 res.close()。
try(Scanner in = new Scanner(new FileInputStream(“…”), “UTF-8”))
{
while(in.hasNext())
System.out.println(in.next());
}
这个块正常退出或存在一个异常时,都会调用 in.close() 方法,就好像使用了 finally 块一样。
还可以指定多个资源:
try(Scanner in = new Scanner(new FileInputStream(“…”), “UTF-8”);
PrintWriter out = new PrintWriter(“…”))
{
while(in.hasNext())
System.out.println(in.next().toUpperCase());
}
不论如何这个块如何退出,in 和 out 都会关闭,但是如果用常规手动编程,就需要两个嵌套的 try/finally 语句。
之前的 close 抛出异常会带来难题,而带资源的 try 语句可以很好的处理这种情况,原来的异常会被重新抛出,而 close 方法带来的异常会“被抑制”。
分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。
可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
一种更灵活的方法是使用 getStackTrace 方法,会得到 StackTraceElement 对象的一个数组,可以在程序中分析这个对象数组:
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
analyze frame
StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法,同时还含有能获得类名和方法名的方法,toString 方法会产生一个格式化的字符串,其中包含所获得的信息。
静态的 Thread.getAllStackTraces 方法,可以产生所有线程的堆栈轨迹。
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
StackTraceElememt[] frames = map.get(t);
analyze frames
}
java.lang.Throwable

Throwable(Throwable cause)
Throwable(String message, Throwable cause)

Throwable initCause(Throwable cause):将这个对象设置为“原因”,如果这个对象已经被设置为“原因”,则抛出一个异常,返回 this 引用。

Throwable getCause():获得设置为这个对象的“原因”的异常对象,如果没有则为 null

StackTraceElement[] getStackTrace():获得构造这个对象时调用堆栈的跟踪

void addSuppressed(Throwable t):为这个异常增加一个抑制异常

Throwable[] getSuppressed():得到这个异常的所有抑制异常

java.lang.StackTraceElement

String getFileName()
int getLineNumber()
String getClassName()
String getMethodName()

boolean isNativeMethod():如果这个元素运行时在一个本地方法中,则返回 true

String toString():如果存在的话,返回一个包含类名、方法名、文件名和行数的格式化字符串,如 StackTraceTest.factorial(StackTraceTest.java:18)

使用异常机制的技巧
1. 异常处理不能代替简单的测试。
在进行一些风险操作时(比如出栈操作),应该先检测当前操作是否有风险(比如检查是否已经空栈),而不是用异常捕获来代替这个测试。
与简单的测试相比,捕获异常需要花费更多的时间,所以:只在异常情况下使用异常机制。
2. 不要过分细分化异常。
如果可以写成一个 try/catch(s) 的语句,那就不要写成多个 try/catch。
3. 利用异常层次结构。
不要只抛出 RuntimeException 异常,应该寻找更适合的子类或创建自己的异常类。
不要只抛出 Throwable 异常,否则会使程序代码可读性、可维护性下降。
4. 不要压制异常。
在 Java 中,倾向于关闭异常。
public Image loadImage(String s)
{
try
{
codes
}
catch(Exception e)
{}
}
这样代码就可以通过编译了,如果发生了异常就会被忽略。当然如果认为异常非常重要,就应该对它们进行处理。
5. 检测错误时,“苛刻”要比放任更好。
6. 不要羞于传递异常。
有时候传递异常比捕获异常更好,让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。
断言
这部分和测试相关,以后有需要的话单独开设一章进行说明。
记录日志
不要再使用 System.out.println 来进行记录了!
使用记录日志 API 吧!
基本日志
简单的日志记录,可以使用全局日志记录器(global logger)并调用 info 方法:
Logger.getGlobal().info(“File->Open menu item selected”);
默认情况下会显示:
May 10, 2013 10:12:15 ….
INFO: File->Open menu item selected
如果在适当的地方调用:
Logger.getGlobal().setLevel(Level.OFF);
高级日志
可以不用将所有的日志都记录到一个全局日志记录器中,也可以自定义日志记录器:
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”);
未被任何变量引用的日志记录器可能会被垃圾回收,为了避免这种情况,可以用一个静态变量存储日志记录器的一个引用。
与包名类似,日志记录器名也具有层次结构,并且层次性更强。
对于包来说,包的名字与其父包没有语义关系,但是日志记录器的父与子之间共享某些属性。
例如,如果对 com.mycompany 日志记录器设置了日志级别,它的子记录器也会继承这个级别。
通常有以下 7 个日志记录器级别 Level:

SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST

默认情况下,只记录前三个级别。
另外,可以使用 Level.ALL 开启所有级别的记录,或者使用 Level.OFF 关闭所有级别的记录。
对于所有的级别有下面几种记录方法:
logger.warning(message);
logger.info(message);
也可以使用 log 方法指定级别:
logger.log(Level.FINE, message);
如果记录为 INFO 或更低,默认日志处理器不会处理低于 INFO 级别的信息,可以通过修改日志处理器的配置来改变这一状况。
默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。
但是如果虚拟机对执行过程进行了优化,就得不到准确的调用信息,此时,可以调用 logp 方法获得调用类和方法的确切位置,这个方法的签名为:
void logp(Level l, String className, String methodName, String message)
记录日志的常见用途是记录那些不可预料的异常,可以使用下面两个方法提供日志记录中包含的异常描述内容:
if(…)
{
IOException exception = new IOException(“…”);
logger.throwing(“com.mycompany.mylib.Reader”, “read”, exception);
throw exception;
}
还有
try
{

}
catch(IOException e)
{
Logger.getLogger(“com.mycompany.myapp”).log(Level.WARNING, “Reading image”, e);
z
}
调用 throwing 可以记录一条 FINER 级别的记录和一条以 THROW 开始的信息。
剩余部分暂时不做介绍,初步了解到这即可,一把要结合 IDE 一起来使用这个功能。如果后续的高级知识部分有需要的话会单独开设专题来介绍。
Java 异常、断言和日志总结

处理错误
异常分类
受查异常
抛出异常
创建异常类
捕获异常
再次抛出异常与异常链

finally 子句
在资源的 try 语句
分析堆栈轨迹元素
使用异常机制的技巧
基本日志与高级日志

个人静态博客:
气泡的前端日记:https://rheabubbles.github.io

退出移动版