Java异常、断言和日志
处理错误
如果由于出现错误而是的某些操作没有完成,程序应该:
返回到一种安全状态,并让用户执行一些其他操作;或者
允许用户保存所有操作,并以妥善方式终止程序
检测(或引发)错误条件的代码通常离:
能让数据恢复到安全状态
能保存用户的操作结果并正常退出程序
的代码很远。
异常处理的任务:将控制权从错误产生地方转移给能够处理这种情况的错误处理器。
异常分类
在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语句
分析堆栈轨迹元素
使用异常机制的技巧
基本日志与高级日志