第七章 异常、断言和日志

第七章 异常、断言和日志

7.1 处理错误

假设在一个Java程序运行期间出现了一个错误。这个错误可能由于文件包含错误信息,或者网络连接出现问题造成的,也有可能是因为使用了无效的数组下标,或者试图使用一个没有被赋值的对象引用而造成的。用户期望在出现错误时,程序能够采取合理的行为。如果由于出现错误而使得某些操作没有完成,程序应该:

  • 返回到一种安全状态,并能够让用户执行其他的命令;或者
  • 允许用户保存所有工作的结果,并以妥善的方式终止程序。

为了能够处理程序中的异常情况,必须考虑到程序中可能会出现的错误和问题。那么需要考虑哪些问题呢?

  • 用户输入错误
  • 设备错误
  • 物理限制
  • 代码错误

正如第五章中所提到的那样,在Java中,如果某个方法不能采用正常的途经完成它的任务,可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回正常值。此外,也不会从调用这个方法的代码继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)。

异常有自己的语法和特定的继承层次结构。下面首先介绍语法,然后给出有效地使用这种语言特性的技巧。

7.1.1 异常分类

在Java程序设计语言中,异常对象都是派生于Throwable类的一个类实例。如果Java中内置的异常类不能满足需求,用户还可以创建自己的异常类。

需要注意的是,所有的异常都是Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。

Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。你的应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力妥善地终止程序之外,你几乎无能为力。这种情况很少出现。

在设计Java程序时,要重点关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。一般规则是:由编程错误导致的异常属于RuntimeException;如果程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

派生于RuntimeException的异常包括以下问题:

  • 错误的强制类转换
  • 数组访问越界
  • 访问null指针。

不是派生于RuntiomeException的异常包括:

  • 试图超越文件末尾继续读取数据
  • 试图打开一个不存在的文件
  • 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。

如果出现了RuntimeException异常,那么就一定是你的问题,这个规则很有道理。应该通过检测数组下标是否越界来避免ArrayIndexOutBoundsExceptiont异常;应该在使用变量之前通过检测它是否为null来杜绝NullPointerException异常的发生。

如何处理不存在的文件呢?难道不能先检查文件是否存在再打开它呢?嗯,这个文件有可能在你检查它是否存在之后就立即被删除了。因此,“是否存在”取决于环境,而不只是取决于你的代码。

Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型(unchecked)异常,所有其他的异常称为检查型(checked)异常。这是很有用的术语,在后面还会用到。编译器将检查你是否为所有的检查型异常提供了异常处理器。

7.1.2 声明检查型异常

如果遇到了无法处理的情况,Java方法可以抛出一个异常。这个道理很简单:方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。例如,一段读取文件的代码知道有可能读取的文件不存在,或者文件内容为空,因此,试图处理文件信息的代码就需要通知编译器可能抛出IOException类的异常。

要在方法的首部指出这个方法可能抛出一个异常,所以要修改方法首部,以反映这个方法可能抛出的检查异常。例如,下面是标准类库中的FileInputStream类的一个构造器的声明。

1
public FileInputStream(String name) throws FileNotFoundException

这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能抛出一个FileNotFoundException异常。如果发生了这种情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException类对象。如果这个方法真的抛出了这样一个异常对象,运行时系统就会开始搜索直到如何处理FileNotFoundException对象的异常处理器。

在自己编写方法时,不必声明这个方法可能抛出的所有异常。至于什么时候需要在方法中throws子句声明异常,以及要用throws子句声明哪些异常,需要记住在遇到下面四种情况会抛出异常:

  • 调用了一个抛出检查型异常的方法,例如FileInputStream构造器。
  • 检测到一个错误,并且利用throw语句抛出一个检查型异常。
  • 程序出现错误,例如,a[-1] = 0会抛出一个非检查型异常。
  • Java虚拟机或运行时库内出现内部错误。

如果出现了前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。为什么?因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会终止。

有些Java方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范(exception specification)声明这个方法可能抛出异常。

1
2
3
4
5
6
7
8
class MyAnimation
{
...;
public Image loadImage(String s) throws IOException
{
...;
}
}

如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。

1
2
3
4
5
6
7
8
class MyAnimation
{
...;
public Image loadImage(String s) throws FileNotFoundException, EOFException
{
...;
}
}

但是,不需要声明Java的内存错误,即从Error继承的异常。任何程序代码都有可能抛出那些异常,而我们对此完全无法控制。同样,也不应该声明从RuntimeException继承的那些非检查型异常

1
2
3
4
5
6
7
8
class MyAnimation
{
...;
void drawImage(int i)throws ArrayIndexOutOfBoundsException //bad style
{
...;
}
}

这些运行时错误完全在我们的控制之中。如果特别担心数组下标错误,就应该多花些时间修正这些错误,而不只是声明这些错误有可能发生。

总之,一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在你的控制之外(Error),要么是由从一开始就应该避免的情况所导致的(RuntimeException)。如果你的方法没有声明所有可能发生的检查异常,编译器就会发出一个错误消息。

当然,从前面的示例中可以知道:不只是声明异常,你还可以捕获异常。这样就不会从这个方法抛出这个异常,所以也没有必要使用throws。

如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类。例如,FileInputStream构造器声明有可能抛出一个IOExcetion异常,在这种情况下,你并不知道具体是哪种IOException异常。它既可能是IOException异常,也可能是某个子类的对象,例如,FileNotFoundException。

7.1.3 如何抛出异常

现在假设在程序代码中发生了糟糕的事情。一个名为readData的方法正在读取一个文件,文件首部包含以下信息,承诺文件长度1024个字符:

1
Content - leangth: 1024

然而,读到733个字符之后文件就结束了。你可能认为这是一种不正常的情况,希望抛出一个异常。

首先要决定应该抛出什么类型的异常。可能某种IOException是个不错的选择。仔细地阅读Java API文档之后会发现:EOFException异常的描述是:“指示输入过程中意外遇到了EOF”。完美,这正是我们要抛出的异常。下面是抛出这个异常的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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类还有一个带一个字符串参数的构造器。你可以很好地利用这个构造器,更细致地描述异常。

1
2
String gripe = "Content - length:" + len + ",Received:" + n;
throws new EOFException(gripe);

在前面已经看到,如果一个已有的异常类能够满足你的要求,抛出这个异常非常容易。在这种情况下:

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

一旦方法抛出异常,这个方法就不会返回到调用者。也就是说,不必操心建立一个默认的返回值或错误码。

7.1.4 创建异常类

你的代码可能会遇到任何标准异常类都无法描述清楚的问题。在这种情况下,创建自己的异常类就是一件顺理成章的事情了。我们需要做的只是一个派生于Exception的类,或者派生于Exception的某个子类,如IOException。习惯做法是,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器(超类Throwable的toString方法会返回一个字符串,其中包含这个详细信息,这在调式中非常有用。)

1
2
3
4
5
6
7
8
public FileFormatException extends IOException
{
public FileFormatException(){}
public FileFormatException(String gripe)
{
super(gripe);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
String readData(BufferedReader in) throws FileFormatException
{
...;
while(...)
{
if(ch == -1)
{
if(n < len) throws new FileFormatException();
}
...;
}
return s;
}

7.2 捕获异常

7.2.1 捕获异常

如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并在控制台上打印一个消息,其中包括这个异常的类型和一个堆栈轨迹。

想要捕获一个异常,需要设置try/catch语句块。最简单的try语句块如下所示:

1
2
3
4
5
6
7
8
9
try
{
code;
more code;
more code;
}catch(ExceptionType e)
{
handler for this type;
}

如果try语句块中的任何代码抛出了catch子句中指定的一个异常类,那么:

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

如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。

如果方法中的任何代码抛出了catch子句没有声明的一个异常类型,那么这个方法就会立即退出(希望它的调用者为这种类型的异常提供catch子句)。

为了展示捕获异常的处理过程,下面给出了一个很典型的读取数据的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void read(String filename)
{
try
{
FileInputStream in = new FileInputStream(filename);
int b;
while((b = in.read())!= -1)
{
process input;
}
}catch(IOException exception)
{
exception.printStackTrace();
}
}

需要注意的是,try子句中的大多数代码都很容易理解:读取并处理字节,直到遇到文件结束符为止。正如在JavaAPI中看到的那样,read方法有可能抛出一个IOException异常。在这种情况下,将跳出整个while循环,进入catch子句,并生成一个堆栈轨迹。对于一个“玩具类”的简单程序来说,这样处理异常看上去很有道理。还有其他选择么?

通常,最好的选择是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法调用者去操心这个问题!如果采用这种处理方法,就必须声明这个方法可能会抛出一个IOException。

1
2
3
4
5
6
7
8
9
public void read(String filename) throws IOException
{
FileInputStream in = new FileInputString(filename);
int b;
while((b = in.read()) != -1)
{
process input;
}
}

请记住,编译器将严格地执行throws说明符。如果调用了一个抛出检查型异常的方法,就必须处理这个异常,或者继续传递这个异常。哪种方法更好呢?一般经验是,要捕获那些你知道如何处理的异常,而继续传播那些你不知道怎么处理的异常。如果想传播一个异常,就必须在方法的首部添加一个throws说明符,提醒调用者这个方法可能会抛出异常。查看API文档,可以看到每个方法可能会抛出哪些异常,然后决定是由自己处理,还是添加到throws列表中。对于后一种选择,不用感到难堪。将异常交给胜任的处理器进行处理要比压制这个异常更好。

同时记住,这个规则也有一个例外。前面曾经提到过:如果编写一个方法覆盖超类的方法,而这个超类的方法没有抛出异常(如JComponent中的paintComponent),你就必须捕获你的方法代码中出现的每一检查型异常。不允许在子类的throws说明符中出现超类方法未列出的异常类。

7.2.2 捕获多个异常

在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。要为每个异常类型使用一个单独的catch子句,如下列:

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
code that might throw exceptions
}catch(FileNotFoundException e)
{
emergency action for missing files
}catch(UnknowHostException e)
{
emergency action for unknown hosts
}catch(IOException e)
{
emergency action for all other I/O problems
}

异常对象可能包含有关异常性质的信息。想要获得这个对象的更多信息,可以尝试使用e.getMessage();得到详细的错误消息(如果有的话),或者使用e.getClass().getName()得到异常对象的实际类型。

在Java7中,通过一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:

1
2
3
4
5
6
7
8
9
10
try
{
code that might throw ceceptions
}catch(FileNotFoundException | UnknownHostException e)
{
emergency action for missing files and unknown hosts
}catch(IOException e)
{
emergemcy action for all other I/O problems
}

只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。

7.2.3 再次抛出异常与异常链

可以在catch子句中抛出一个异常。通常,希望改变异常的类型时会这样做。如果开发了一个供其他程序员使用的子系统,可以使用一个指示子系统故障的异常类型,这很有道理。ServletException就是这样一个异常类型的例子。执行一个servlet的代码可能不想知道发生错误的细节原因,但希望明确地直到Servlet是否有问题。

可以如下捕获异常并将它再次抛出:

1
2
3
4
5
6
7
try
{
access the database;
}catch(SQLException e)
{
throw new ServletException("databese error :" + e.getMessage());
}

在这里,构造ServletException时提供了异常的消息文本。

不过,可以有一种更好的处理方法,可以把原始异常设置为新异常的“原因”:

1
2
3
4
5
6
7
8
9
try
{
access the database
}catch(SQLException original)
{
ServletException e = new ServletException("database error");
e.initCause(original);
throw e;
}

捕获到这个异常时,可以使用下面这条语句获取原始异常:

1
Throwable original = caughtException.getCause();

强烈建议使用这种包装技术。这样可以在子系统中抛出高层异常,而不会丢失原始异常。

有时候你可能只想记录一个异常,再将它重新抛出,而不做任何改变:

1
2
3
4
5
6
7
8
try
{
access the database;
}catch(Exception e)
{
logger.log(level, message, e);
throw e;
}

7.2.4 finally 子句

代码抛出一个异常时,就会停止处理这个方法中 剩余的代码,并退出这个方法。如果这个方法已经获得了只有它自己知道一些本地资源,而且这些资源必须清理,这就会有问题。一种解决方案是捕获所有异常,完成资源的清理,再重新抛出异常。但是,这种解决方案比较繁琐,这是因为需要在两个点地方清理资源分配。一个在正常的代码中;另一个在异常代码中。finally子句可以解决这个问题。

不管是否有异常被捕获,finally子句中的代码都会执行。在下面的示例中,所有情况下程序都将关闭输入流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FileInputStream in = new FileInputStream(...);
try
{
//1
code that might throw exceptions;
//2
}catch(IOException e)
{
//3
show error message;
//4
}finally
{
//5
in.close();
}
//6

这个程序可能在以下3中情况执行finally子句:

  • 代码没有抛出异常。这种情况下,程序首先执行try语句块中的全部代码,然后执行行finally子句中的代码。随后,继续执行finally子句之后的第一条语句。也就是说,执行顺序为1,2,5,6。
  • 代码抛出一个异常,并在一个catch子句中捕获。在上面的示例中就是IOException异常。在这种情况下,程序将执行try语句块中的所有代码,直到抛出异常为止。此时,将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。如果catch子句没有抛出异常,程序将执行finally子句之后的第一条语句。在这种情况下,执行顺序是1,3,4,5,6。如果catch子句抛出了一个异常,异常将被抛回到这个方法的调用者。执行顺序则只是1,3,5。
  • 代码抛出了一个异常,但没有任何catch子句捕获这个异常。在这种情况下,程序将执行try语句块中的所有语句,直到抛出异常为止。此时,将跳过try语句块的剩余代码,然后执行finally子句中语句,并将异常抛回给这个方法的调用者。在这里,执行的顺序只是1,5。

try语句可以只有finally子句,而没有catch子句。例如,下面这条try语句:

1
2
3
4
5
6
7
8
9
InputStream in = ...;
try
{
code that might throw exceptions;
}
finally
{
in.close();
}

无论在try语句块中是否遇到异常,finally子句中的in.close()语句都会执行。当然,如果真的遇到一个异常,这个异常将会被重新抛出,并且必须由另一个catch子句捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
InputStream in = ...;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}catch(IOException e)
{
show error message
}

内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种解决方法不仅清楚,而且功能更强大:将会报告finally子句中出现的错误。

7.2.5 try-with-Resources 语句

在Java7中,对于以下代码模式:

1
2
3
4
5
6
7
8
open a resource
try
{
work with the resource
}finally
{
close the resource
}

假设资源属于一个实现了AutoCloseable接口的类。Java7为这种代码模式提供了一个很有用的快捷方式。AutoCloseable接口有一个方法:

1
void close() throws Exception

try-with-resources语句的最简形式为:

1
2
3
4
try(Resource res =...)
{
work with res
}

try块退出时,会自动调用res.close()。下面给出一个典型的例子,这里要读取一个文件中的所有单词:

1
2
3
4
5
try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"),StandardCharsets.UTF_8))
{
while(in.hasNext())
System.out.println(in.next());
}

这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了finally一样。还可以指定多个资源。例如:

1
2
3
4
5
6
try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words",StandardCharsets.UTF_8);
PrintWriter out = new PrintWriter("out.txt",StandardCharsets.UTF_8))
{
while(in.hasNext())
out.println(in.next().toUpperCase());
}

不论这个块如何退出,in和out都会关闭。如果你用常规方式手动编程,就需要两个嵌套的try/finally语句。

在Java9中,可以在try首部中提供之前声明的事实最终变量:

1
2
3
4
5
6
7
8
public static void printAll(String[] lines, PrintWriter out)
{
try(out)
{//effectively final cariable
for(String line: lines)
out.println(line);
}//out.close() called here
}

如果try块抛出一个异常,而且close方法也抛出一个异常,这就带来一个难题。try-with-resources语句可以很好地处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可以调用getSuppressed方法,它会生成从close方法抛出并被抑制的异常数组。

你肯定不想采用这种常规方式编程。只要需要关闭资源,就要尽可能使用try-with-resources语句。

7.2.6 分析堆栈轨迹元素

堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。你肯定已经看到过这种堆栈轨迹列表,当Java程序因为一个未捕获的异常而终止时,就会显示堆栈轨迹。

可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。

1
2
3
4
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();

一种更灵活的方法是使用StackWalker类,它会生成一个StackWalker.StackFrame实例流,其中每个实例分别描述一个栈帧(stack frame)。可以利用以下调用迭代处理这些栈帧:

1
2
StackWalker walker = StackWalker.getInstance();
walker.forEach(frame -> analyze frame)

如果想要以懒方式处理Stream,可以调用:

1
walker.walk(stream -> process stream)

7.3 使用异常的技巧

  • 1.异常处理不能代替简单的测试:作为一个示例,在这里编写了一段代码,试着将一个空栈弹出10000000次。首先,在弹栈之前先查看栈是否为空。
1
if(!s.empty()) s.pop()

接下来,我们强制要求不论栈是否为空都执行弹出操作,然后捕获EmptyStackException异常来指示我们不能这样做。

1
2
3
4
5
6
7
try
{
s.pop();
}catch(EmptyStackException e)
{

}

在我们的测试机器上,调用isEmpty的版本运行时间为646毫秒。捕获EmptyStackException的版本运行时间为21739毫秒。可以看出,与完成简单的测试相比,捕获异常所花费的时间大大超过了前者,因此使用异常的基本规则是:只在异常情况下使用异常。

  • 2.不要过分地细化异常:很多程序员习惯将每一条语句都分装在一个独立的try语句块中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PrintStream out;
Stack s;

for(i = 0; i < 100; i++)
{
try
{
n = s.pop();
}catch(EmptyStackException e)
{
//stack was empty
}
try
{
out.writeInt(n);
}catch(IOException e)
{
//problem writing to file
}
}

这种编程方式将导致代码量的急剧膨胀。首先来看你希望这段代码完成的任务。在这里,我们希望从栈中弹出100个数,将它们存入一个文件中。如果出现了问题,我们什么也做不了。如果栈是空的,他不会变成非空状态;如果文件包含错误,这个错误也不会魔法般地消失。因此,有必要将整个任务包在一个try语句块中,这样,当任何一个操作出现问题时,就可以取消整个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
{
for(i = 0; i < 100; i++)
{
n = s.pop();
out.writeInt(n);
}
}catch(IOException e)
{
//problem writing to file
}
catch(EmptyStackException e)
{
//Stack was empty
}

这段代码看起来清晰多了。这样也满足了异常处理的一个承诺:将正常处理与错误处理分开。

  • 3.充分利用异常层次结构:不要只抛出RuntimeException异常。应该寻找一个合适的子类或创建自己的异常类。不要只捕获Throwable异常,否则,这会使你的代码更难读、更难维护。考虑检查型异常与非检查型异常的区别。检查型异常本来就很庞大,不要为逻辑错误抛出这些异常。如果能够将一种异常转换成另一种更加合适的异常,那么不要犹豫。例如,在解析某个文件中的一个整数时,可以捕获NumberFormatException异常,然后将它转换为IOException的一个子类或MySubsystemException。
  • 4.不要压制异常:在Java中,往往非常希望关闭异常。如果你编写了一个方法要调用另一个方法,而那个方法有可能100年才抛出一个异常,但是,如果没有在你的方法的throws列表中声明这个异常,编译器就会报错。你不想把它放在throws列表中,以为这样一来,编译器会对调用了你的方法的所有方法报错。因此,你会将这个异常关闭:
1
2
3
4
5
6
7
8
9
10
public Image loadImage(String s)
{
try
{
code that threatens to throw checked exception
}catch(Exception e)
{

}
}

现在你的代码可以通过编译了。除非出现异常,否则它能很好地运行。但是一旦出现异常,这个异常会悄无声息地被忽略。如果你认为异常都非常重要,就应当是当地进行处理。

  • 5.在检测错误时,“苛刻”要比放任更好:当检测到错误的时候,有些程序员对抛出异常很担心。在用无效的参数调用一个方法时,返回一个虚拟值是不是比抛出一个异常更好?例如,党栈为空时,Stack.pop是该返回一个null,还是要抛出一个异常?我们认为:最好在出错的地方抛出一个EmptyStackException异常,这要好于以后抛出一个NullPointerException异常。
  • 6.不要羞于传递异常:很多程序员都感觉应该捕获抛出的全部异常。如果调用了一个抛出异常的方法,例如,FileInputStream构造器或readLine方法,它们就会本能地捕获这些可能产生的异常。其实,最好继续传递这个异常,而不是自己捕获:
1
2
3
4
5
public void readStuff(String filename)throws IOException
{
FileInputStream in = new FileInputStream(filename, StandardCharsets.UTF_8);
...;
}

更高层的方法通常可以更好地通知用户发生了错误,或者放弃不成功的命令。

7.4 使用断言

7.4.1 断言的概念

假设确信某个属性符合要求,并且代码的执行依赖于这个属性。例如,需要计算

1
double y = Math.sqrt(x);

你确信这里的x是一个非负数。原因是:x是另外一个计算结果,而这个结果不可能是一个负值;或者x是一个方法的参数,这个方法要求它的调用者只能提供一个正数输入。不过,你可能还是想再做一次检查,不希望计算中潜入让人困惑的“不是一个数”(NaN)浮点数。当然,也可以抛出一个异常:

1
if(x < 0) throw new IllagalArgumentException("x < 0");

但是即使测试完毕后,这个代码还会一直保留在程序中。如果在程序中含有大量这种检查,程序运行起来慢很多。

断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查。

Java语言引入了关键字assert。这个关键字有两种形式:

1
2
assert condition;
assert condition : expression;

这两个语句都会计算条件,如果结果为false,则抛出一个AssertionError异常。在第二个语句中,表达式将传入AssertionError对象构造器,并转换成一个消息字符串。

想要断言x是一个非负数,只需要简单地使用下面这条语句

1
2
3
assert x >= 0;
//或者将x的实际值传递给AssertionError对象,以便以后显示。
assert x >= 0 : x;

7.4.2 启用和禁用断言

在默认情况下,断言是禁用的。可以在运行程序时用 -enableassertions或 -ea 选项启用断言:

1
java -enableassertions MyApp

需要注意的是,不必重新编译程序来启用或禁用断言。启用或禁用断言是类加载器(class loader)的功能。禁用断言时,类加载器会取出断言代码,因此,不会降低程序运行的速度。也可以在某个类或整个包中启用断言,例如:

1
java -ea:MyClass -ea:com.mycompany.mylib MyApp

这条命令将为MyClass类以及com.mycompany.mylib包和它的子包中的所有类打开断言。选项 -ea将打开无名包中所有类的断言。也可以用选项 -disableassertions 或 - da 在某个特定类和包中禁用断言:

1
java -ea:... -da:MyClass MyApp

有些类不是由类加载器加载,而是直接由虚拟机加载的。可以使用这些开关有选择地启用或禁用那些类中的断言。

不过,启用和禁用所有断言的 -ea和 -da开关不能应用到那些没有类加载器的“系统类”上。对于这些系统类,需要使用 -enablesystemassertions/-esa 开关启用断言。也可以通过变成控制类加载器的断言状态。

7.4.3 使用断言完成参数检查

7.4.4 使用断言提供假设文档

7.5 日志

7.5.1 基本日志

7.5.2 高级日志

7.5.3 修改日志管理器配置

7.5.4 本地化

7.5.5 处理器

7.5.6 过滤器

7.5.7 格式化器

7.5.8 日志技巧

7.6 调试技巧

Donate comment here