CORE JAVA 第七章 异常、断言和日志
第七章 异常、断言和日志
对于异常情况,例如,可能造成程序崩溃的错误输入,Java使用一种称为异常处理(exception handing)的错误捕获机制处理。
在测试期间,需要进行大量的检测以验证程序操作的正确性。然而,这些检测可能非常耗时,在测试完成后也不必保留它们,因此,可以将这些检测删掉,并在其他测试需要时将他们粘贴回来,这是一件很乏味的事。断言,可以有选择地启用检测。
当程序出现错误时,并不总是能够与用户或终端进行沟通。此时,可能希望记录下出现的问题,以备日后进行分析。本章的第三部分将讨论标准Java日志框架。
7.1 处理错误
假设在一个Java程序运行期间出现了一个错误。如果由于出现错误而使得某些操作没有完成,程序应该:
- 返回到一种安全状态,并能够让用户执行一些其他的命令;或者
- 允许用户保存所有操作的结果,并以妥善的方式终止程序。
要做到这些并不是一件很容易的事。其原因是检测(或引发)错误条件的代码通常离那些能够让数据恢复到安全状态,或能够保存用户的操作结果,并正常地退出程序的代码很远。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
正如第五章中所叙述的那样,在Java中,如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值,而是抛出一个封装了错误信息的对象。这个方法将会立刻退出,调用这个方法的代码也将无法继续执行,取而代之的是异常处理机制开始搜索能够处理这种异常状况的异常处理器。
7.1.1 异常分类
异常对象都是派生于Throwable类的一个实例。如果Java中内置的异常类不能够满足需求,用户可以创建自己的异常类。
所有的异常都是由Throwable继承而来,但在下一层分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了。这种情况很少出现。
Exception层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。由于程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
派生于RuntimeException的异常包含下面几种情况:
- 错误的类型转换。
- 数组访问越界。
- 访问null指针。
不是派生于RuntimeException的异常包含下面几种情况:
- 试图在文件尾部后面读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
“如果出现RuntimeException异常,那么一定就是你的问题”是一条相当有道理的规则。应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常;应该通过在使用变量之前检测是否为null来杜绝NullPointerException异常的发生。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查异常,所有其他的异常称为受查异常。编译器将核查是否为所有的受查异常提供了异常处理器。
7.1.2 声明受查异常
如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。这个道理非常简单:一个方法不仅要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
方法应该在其首部声明所有可能抛出的异常。例如,下面是标准类库中提供的FileInputStream类的一个构造器的声明:
public FileInputStream(String name) throws FileNotFoundException
这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能抛出一个FileNotFoundException异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException类对象,运行时系统就会开始搜索异常处理器,以便知道如何处理FileNotFoundException对象。
在自己编写方法时,不必将所有可能抛出的异常都声明。在下面4种情况时应该抛出异常:
- 调用一个抛出受查异常的方法,例如,FileInputStream构造器。
- 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。
- 程序出现错误,例如,a[-1] = 0会抛出一个ArrayIndexOutOfBoundsException这样的非受查异常。
- Java虚拟机和运行时库出现的内部错误。
如果出现前两种情况之一,则必须告诉调用这个方法的程序员有可能抛出异常。如果没有处理器捕获这个异常,当前执行的线程就会结束。
对于那些可能被他人使用的方法,应该根据异常规范,在方法的首部声明这个方法可能抛出的异常。
class MyAnimation
{
……
public Image loadImage(String s) throws IOException
{
……
}
}
如果一个方法有可能抛出多个受查异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。
class MyAnimation
{
……
public Image loadImage(String s) throws FileNotFoundException,EOFException
{
……
}
}
但是,不需要声明Java的内部错误,即从Error继承的错误。同样,也不应该声明从RuntimeException继承的那些非受查异常。
class MyAnimation
{
……
void drawImage(int i) throws ArrayIndexOutOfBoundsException // bad style
{
……
}
}
总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。如果方法没有声明所有可能发生的受查异常,编译器就会发出一个错误消息。
除了声明异常之外,还可以捕获异常。这样会使异常不被抛到方法外,也不需要throws规范。稍后将会讨论如何决定一个异常是被捕获,还是被抛出让其他的处理器进行处理。
警告:如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)。
如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
如果类中的一个方法声明将会抛出异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。
注释:在Java中,没有throws说明符的方法将不能抛出任何受查异常。
7.1.3 如何抛出异常
抛出异常的语句:
throw new EOFException();
或者:
EOFException e = new EOFException();
throw e;
下面将这些代码放在一起:
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);
对于一个已经存在的异常类,将其抛出非常容易:
- 找到一个合适的异常类。
- 创建这个类的一个对象。
- 将对象抛出。
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。
7.1.4 创建异常类
在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。创建自己的异常类需要做的是定义一个派生于Exception的类,或者派生于Exception子类的类。
习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable的toString方法会打印出这些详细信息,这在调试中非常有用)。
class FileFormatException extends IOException
{
public FileFormatException() {}
public FileFormatException(String gripe)
{
super(gripe);
}
}
现在,就可以抛出自己定义的异常类型了。
7.2 捕获异常
7.2.1 捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
要想捕获一个异常,必须设置try/catch语句块。最简单的try语句块如下所示:
try
{
// codes
}
catch (ExceptionType e)
{
// handler for this type
}
如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
- 程序将跳过try语句块的其余代码
- 程序将执行catch子句中的处理器代码
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出。
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递(传递给调用者)。
如果想传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。请记住,编译器严格的执行throws说明符。如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。
仔细阅读一下Java API文档,以便知道每个方法可能会抛出哪种异常,然后再决定是自己处理,还是添加到throws列表中。对于后一种情况,也不必犹豫。将异常直接交给能够胜任的处理器进行处理要比压制对它的处理更好。
这个规则也有一个例外。如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,则这个方法就必须捕获方法代码中出现的每一个受查异常。
7.2.2 捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列方式为每个异常类型使用一个单独的catch子句:
try
{
// codes that might throw exceptions
}
catch (FileNotFoundException e)
{
……
}
catch (UnkownHostException e)
{
……
}
catch (IOException e)
{
……
}
e.getMessage()
得到详细的错误信息;e.getClass().getName()
得到异常对象的实际类型。
在Java SE 7中,同一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:
try
{
// codes that might throw exceptions
}
catch (FileNotFoundException | UnkownHostException e)
{
……
}
catch (IOException e)
{
……
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
注释:捕获多个异常时,异常变量隐含为final变量。不能为其赋不同的值。
注释:捕获多个异常,生成的字节码只包含一个对应公共catch子句的代码块。
?
7.2.3 再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。
下面给出了捕获异常并将它再次抛出的基本方法:
try
{
//
}
catch(SQLException e)
{
throw new ServletException("data error : " + e.getMessage());
}
不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“原因”:
try
{
//
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
当捕获到异常的时候,就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
提示:如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并将它包装成一个运行时异常。
有时你可能只想记录一个异常,再将它重新抛出,而不做任何改变:
try
{
//
}
catch (Exception e)
{
logger.log(level, message, e);
throw e;
}
7.2.4 finally子句
不管是否有异常被捕获,finally子句中的代码都被执行。finally子句可以恰当地关闭一个文件,或关闭与数据库的连接。
在下面的示例中,程序将在所有情况下关闭文件。
InputStream in = new FileInputStream(……);
try
{
//1
//code that might throw exceptions
//2
}
catch (IOException e)
{
//3
//show error message
//4
}
finally
{
//5
in.close();
}
// 6
三种情况:省略
try语句可以只有finally子句,而没有catch子句。
事实上,我们认为在需要关闭资源时,用这种方式使用finally子句是一种不错的选择。
提示:强烈建议解耦合try/catch和try/finally语句块,这样可以提高代码清晰度。例如:
InputStream in = ……;
try
{
try
{
//code that might throw exceptions
}
finally
{
in.close();
}
}
catch (IOException e)
{
// show error message
}
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告finally子句中出现的错误。
有时候,finally子句也会带来麻烦。例如,清理资源的方法也可能抛出异常。假设在try语句块中的代码抛出了一些非IOException的异常,这些异常只有方法的调用者才能给予处理。执行finally语句块,并调用close方法。而close方法本身也有可能抛出IOException异常。当出现这种情况时,原始的异常将会丢失,转而抛出close方法的异常。
如果想做适当的处理,重新抛出原来的异常,代码会变得极其繁琐。如下:
InputStream in = ……;
Exception ex = null;
try
{
try
{
//code that might throw exceptions
}
catch (Exception e)
{
ex = e;
throw e;
}
}
finally
{
try
{
in.close();
}
catch (Exception e)
{
if (ex == null) throw e;
}
}
下一节将了解到,Java SE 7中关闭资源的处理会容易很多。
7.2.5 带资源的try语句
对于以下代码模式:
open a resource
try
{
work with the resource
}
finally
{
close the resource
}
假设资源属于一个实现了AutoCloseable接口的类,Java SE 7为这种代码模式提供了一个很有用的快捷方式。AutoCloseable接口有一个方法:
void close() throws Exception
注释:另外,还有一个Closeable接口。这是AutoCloseable的子接口,也包含一个close方法,不过,这个方法声明为抛出一个IOException。
带资源的try语句(try-with-resources)的最简形式为:
try (Resource res = ……)
{
work with res
}
try块退出(正常退出或存在异常)时,会自动调用res.close()
。就好像使用了finally块一样。
上一节已经看到,如果try块抛出一个异常,而且close方法也抛出一个异常,这就会带来一个难题。带资源的try语句可以很好地处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会被“抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可以调用getSuppressed方法,它会得到从close方法抛出并被抑制的异常列表。
只要需要关闭资源,就要尽可能使用带资源的try语句。
注释:带资源的try语句自身也可以有catch子句和一个finally子句。这些子句将在关闭资源之后执行。
7.2.6 分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。当Java程序正常终止,而没有捕获异常时,这个列表就会显示出来。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(mew PrintWriter(out));
String description = out.toString();
一种更灵活的方式是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组。例如:
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
analyze frame
StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。
静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈轨迹。下面给出使用这个方法的具体方式:
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet())
{
StackTraceElement[] frames = map.get(t);
analyze frames
}
7.3 使用异常机制的技巧
-
异常处理不能代替简单的测试
试着上百万次地对一个空栈进行退栈操作。在实施退栈之前,首先要查看栈是否为空:
if (!s.empty()) s.pop();
接下来,强行进行退栈操作。然后,捕获EmptyStackException异常来告知我们不能这样做。
try
{
s.pop();
}
catch (EmptyStackException e)
{
}
在测试的机器上,调用isEmpty的版本运行时间为646毫秒。捕获EmptyStackException的版本运行时间为为21739毫秒。
可以看出,与执行简单的测试相比,捕获异常所花费的时间大大超过了前者,因此使用异常的基本规则是:只在异常情况下使用异常机制。
- 不要过分地细化异常
不要将每一条语句都分装在一个独立的try语句块中。有必要将整个任务包装在一个try语句块中,这样,当任何一个操作出现问题的时候,整个任务都可以取消。这样也满足了异常处理机制的其中一个目标,将正常处理与错误处理分开。
- 利用异常层次结构
不要只抛出RunTimeException异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Throwable异常。否则,会使程序代码更难读、更难维护。
已检查异常本来就很庞大,不要为逻辑错误抛出这些异常。
将一种异常转换成另一种更加适合的异常时不要犹豫。
- 不要压制异常
在Java中,往往强烈地倾向关闭异常。
- 在检测错误时,“苛刻”比放任更好
在用无效的参数调用一个方法时,返回一个虚拟的数值,还是抛出一个异常?例如,当栈空时,stack.pop是返回一个null,还是抛出一个异常?我们认为:在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException异常更好。
- 不要羞于传递异常
如果调用了一个抛出异常的方法,这些方法就会本能地捕获这些可能产生的异常。其实,传递异常比捕获这些异常更好。
让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。
规则5、6可以归纳为“早抛出,晚捕获”。
7.4 使用断言
在一个具有自我保护能力的程序中,断言很常用。
7.4.1 断言的概念
假设确信某个属性符合要求,并且代码的执行依赖于这个属性。例如,需要计算double y = Math.sqrt(x)
。
我们确信,这里的x是一个非负数值。原因是:x是另外一个计算的结果,而这个结果不可能是负值;或者x是一个方法的参数,而这个方法要求它的调用者只能提供一个正整数。
然而,还是希望进行检查,以避免让“不是一个数”的数值参与计算操作。当然,也可以抛出一个异常:
if (x < 0) throw new IllegalArgumentException("x < 0");
但是这段代码会一直保留在程序中,即使测试完毕也不会自动地删除。如果在程序中含有大量的这种检查,程序运行起来会相当慢。
断言机制允许在测试期间向代码中插入一些检测语句。当代码发布时,这些插入的检测语句将会被自动地移走。
Java语言引入了关键字assert。这个关键字有两种形式:
assert 条件;
assert 条件:表达式;
这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
表达式部分的唯一目的是产生一个消息字符串。AssertionError对象并不存储表达式的值,因此,不可能在以后得到它。
要想断言x是一个非负数值,只需要简单地使用下面这条语句:
assert x >= 0;
或者将x的实际值传递给AssertionError对象,从而可以在后面显示出来:
assert x>= 0 : x;
个人理解:
用易理解的话来说 ,断言主要使用在代码开发和测试时期,用于对某些关键数据的判断,如果这个关键数据不是你程序所预期的数据,程序就提出警告或退出。
7.4.2 启用和禁用断言
在默认情况下, 断言被禁用。可以在运行程序时使用-enableassertions
或-ea
选项启用:
java -enableassretions MyApp
需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器的功能。当断言被禁用时,类加载器将跳过断言代码,因此,不会降低程序运行的速度。
也可以在某个类或整个包中使用断言,例如:
java -ea:MyClass -ea:com.mycompany.mylib……MyApp
选项-ea
将开启默认包中的所有类的断言。
选项-disableassertions
或-da
禁用某个特定类和包的断言:
java -ea:…… -da:MyClass MyApp
然而,启用和禁用所有断言的-ea
和-da
开关不能应用到那些没有类加载器的“系统类”上。对于这些系统类,需要使用-enablesystemassertions/-esa
开关启用断言。
7.4.3 使用断言完成参数检查
- 断言失败是致命的、不可恢复的错误。
- 断言检查只用于开发和测试阶段。
因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作为程序向用户通告问题的手段。断言只应用于在测试阶段确定程序内部的错误位置。
例子:检查方法的参数。如果在开头的文档中规定参数引用不能为null,就可以在方法的开头使用断言。
这种约定称为前置条件。如果调用者在调用这个方法时没有提供满足这个前置条件的参数,所有的断言都会失败,并且这个方法可以执行它想做的任何操作。事实上,由于可以使用断言,当方法被非法调用时,将会出现难以预料的结果。有时候会抛出一个断言错误,有时候会产生一个null指针异常,这完全取决于类加载器的配置。
7.4.4 为文档假设使用断言
很多程序员使用注释说明假设条件:
if (i % 3 == 0)
……
else if (i % 3 == 1)
……
else // (i % 3 == 2)
……
在这个示例中,使用断言会更好一些。
if (i % 3 == 0)
……
else if (i % 3 == 1)
……
else
{
assert i % 3 == 2;
}
这个示例说明了程序员如何运用断言来进行自我检查。
!7.5 记录日志
断言是一种测试和调试阶段所使用的战术性工具;而日志记录是一种在程序的整个生命周期都可以使用的策略工具。
记录日志API;记录日志API的优点;
7.5.1 基本日志
要生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其info方法:
Logger.getGlobal().info("File -> Open menu item selected");
如果在适当的地方(如main开始)调用
Logger.getGlobal().setLevel(Level.OFF);
将会取消所有的日志。
7.5.2 高级日志
从前面已经看到“虚拟日志”,下面继续看一下企业级日志。在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器。
可以调用getLogger方法创建或获取记录器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
提示:未被任何变量引用的日志记录器可能会被垃圾回收。为了防止这种情况发生,要用一个静态变量存储日志记录器的一个引用。
与包名类似,日志记录器名也具有层次结构。日志记录器的父与子之间将共享某些属性。例如,如果对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别。
通常,有以下7个日志记录器级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
在默认情况下,只记录前三个级别。也可以设置其他的级别。例如:
logger.setLevel(Level.FINE);
现在,FINE和更高级别的记录都可以记录下来。
另外,还可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别的记录。
对于所有的级别有下面几种记录方法:
logger.warning(message);
logger.fine(message);
同时,还可以使用log方法指定级别:
logger.log(Level.FINE, message);
默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。但是,如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用logp方法获得调用类和方法的确切位置,这个方法的签名为:
void logp(Level l, String className, String methodName, String message)
entering和exiting方法可以跟踪执行流。这些调用将生成FINER级别和以字符串ENTRY和RETURN开始的日志记录。
记录日志的常见用途是记录那些不可预料的异常。可以使用throwing和log方法提供日志记录中包含的异常描述内容。
调用throwing可以记录一条FINER级别的记录和一条以THROW开始的信息。
7.5.3 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。比如,修改默认的日志记录级别。
警告:日志管理器在VM启动过程中初始化,这在main执行之前完成。
日志记录并不将消息发送到控制台上,这是处理器的任务。
7.5.4 本地化
将日志消息本地化,以便全球的用户都可以阅读它。
7.5.5 处理器
在默认情况下,日志处理器将记录发送到ConsoleHandler中,并由它输出到System.err流中。
………………
7.5.6 过滤器
7.5.7 格式化器
7.5.8 日志记录说明
- 为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,是一种好的编程习惯。可以通过调用下列方法得到日志记录器。
Logger logger = Logger.getLogger("com.mycompany.myprog");
- 默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。
- 所有级别为INFO、WANING和SEVERE的消息都将显示在控制台上。因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE是一个很好的选择。
7.6 调试技巧
假设编写了一个程序,并对所有的异常进行了捕获和恰当的处理,然后,运行这个程序,但还是出现问题,现在该怎么办呢?
在启动调试器前,本节先给出一些有价值的建议。
- 打印或记录任意变量的值。
- 在每一个类中放置一个单独的main方法。这样就可以对每一个类进行单元测试。利用这种技巧,只需要创建少量的对象,调用所有的方法,并检测每个方法是否能够正确地运行就可以了。
- 使用Junit。Junit是一个非常常见的单元测试框架。
- 日志代理可以截取方法调用,并进行日志记录,然后调用超类中的方法。
- 利用Throwable类提供的printStackTrace方法。可以从任何一个异常对象中获得堆栈情况。
……