《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 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
语句 - 分析堆栈轨迹元素
- 使用异常机制的技巧
- 基本日志与高级日志
个人静态博客:
- 气泡的前端日记: rheabubbles.github.io