title: 异常、断言和日志
tag: 标签名
categories: 分类
comment: 是否允许评论(true or false)
description: 描述
top_img: https://z3.ax1x.com/2021/10/06/4xq2s1.png
cover: https://z3.ax1x.com/2021/10/06/4xq2s1.png
处理错误
假设一个Java程序运行期间出现了一个错误。这个错误的原因有很多种,可能是由于文件包含错误信息,或者网络连接出现问题,也有可能是使用了无效的数组下标。用户期望在出现错误的时,能够采取合理的行为。如果由于出现错误而使得某些操作没有完成,程序应该:
- 返回一种安全的状态,并能够让用户执行其他的命令
- 允许用户保存所有工作的结果,并且以妥善的方式终止程序。
异常分类
任何异常对象都是派生与Throwable类的一个类实例。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。
在Java程序设计中,重点关注Exception层次结构。它的下面又分为两个结构,一个为RuntimeException,另一个分支为包含其他异常。一般的规则为:由于编程错误导致的异常属于RuntimeException;如果程序本身没有问题,但是由于像I/O错误这里问题的异常属于其他异常。
派生与RuntimeException的异常包括:
- 错误的强制类型转换
- 数组范围越界
- 访问null指针
不是派生于RuntimeException的异常包括:
- 试图超越文件末尾继续读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
“如果出现RuntimeException异常,那么就一定是比的问题”,这个规则很有道理。应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常;应该在使用变量之前通过检测它是否为Null来杜绝NullPointException异常的发生。
Java语法规范将派生与Error类或RuntimeException类的所有异常称为非检查型异常,所有其他的异常称为检查型异常。
声明检查型异常
如果遇到了无法处理的异常,Java方法可以抛出一个异常。方法不仅需要告诉编译器将要返回什么值,还有告诉编译器有可能发生什么错误。如:一段读取文件的代码知道有可能读取的文件不存在,或者文件内容为空。
要在方法的首部指出这个方法可能抛出一个异常,所以要修改方法首部,以反映这个方法可能抛出的异常。
public FileInputStream(String name) throws FileNotFoundException
这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能出错而抛出一个FileNotFoundException异常。如果出现了这种糟糕的情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException对象。如果这个方法真的抛出了这样的一个异常,运行时系统就会开始搜索如何处理FileNotFoundException对象的异常处理器。
在自己编写方法时,不必声明这个方法可能抛出的所有异常。在什么时候需要在方法中用throws子句声明异常,以及要用throws子句声明哪些异常,需要记住下面4种情况抛出的异常:
- 调用一个抛出检测型异常的方法
- 检测到一个错误,并且利用throw语句抛出一个检查型异常
- 程序出现错误
- Java虚拟机或运行时库出现内部错误。
如果出现前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常得到方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前的执行的线程就会终止。
有些Java方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范声明这个方法可能抛出异常。
class MyAnimation{
...
public Image loadImage(String s)throws IOException{
...
}
}
如果一个方法有可能抛出多个检查异常类型,那么就必须在方法得到首部列出所有的异常类。每个异常类之间用逗号隔开。
class MyAnimation{
...
public Image loadImage(String s)throws FileNotFoundException,EOFException{
...
}
}
总之,一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在你的控制之外(Error),要么是由从以一开始就应该避免的情况所导致的(RuntimeException)。如果你的方法没有声明所有可能发生的检查型异常,编译器就会发出一个错误信息。
如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类。
如何抛出异常
String readData(Scanner in)throws EOFException{
...
while(...){
if(!in.hasNext()) // EOF encounted
{
if(n < len)
throw new EOFException();
}
...
}
return s;
}
如果一个已有异常的类能够满足你的要求,抛出这个异常非常容易。在这种情况下:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
创建异常类
你的代码可能会遇到任何标准异常类都无法描述清楚的问题。在这种情况下,创建自己的异常类就是一件顺理成章的事情了。我们需要做的就是定义一个派生于Exception的类,或者派生于Exception的某个子类。习惯做法是,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器。
class FileFormatException extends IOException{
public FileFormatException(){}
public FileFormatException(String gripe){
super(gripe);
}
}
// 现在,就可以抛出你自定定义的异常类型了。
String readDate(BufferReader in)throws FileFormatException{
...
while(...){
if(ch == -1)// EOF encountered{
if(n < len)
throw new FileFormatException();
}
...
}
return s;
}
API
Throwable()
// 构造一个新的Throwable对象,但没有详细的描述信息
Throwable(String message)
// 构造一个新的Throwable对象,带有指定的详细描述信息。所有派生的异常类都支持一个默认构造器和一个带有详细描述信息的构造器
String getMessage()
// 获得Throwable对象的详细描述信息
捕获异常
捕获异常
如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并且在控制台上打印一个消息,其中包括这个异常的类型和要给堆栈轨迹。
要想捕获一个异常,需要设置try/catch语句块。最简单的try语句块如下所示:
try{
code
more code
more code
}catch(ExceptionType e){
handler for this type
}
如果try语句块种的任何代码抛出了catch子句种指定的一个异常类,那么
- 程序将跳过try语句块的其余代码
- 程序将执行catch子句中的处理器代码
如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
如果方法中的任何代码抛出了catch子句中没有声明的一个异常类型,那么这个方法就会立即退出。
下面是一个典型的读取数据的代码:
public void read(String filename){
try{
var in = new FileInputStream(filename);
int b;
while((b = in.read())!= -1){
process input
}
}
catch (IOException exception){
exception.printStackTrace();
}
}
// 读取并处理字节,直到遇到文件结束符为止。read方法有可能抛出一个IOException异常,在这种情况下,将跳出整个while循环,进入catch子句,并生成一个堆栈轨迹。
// 最好的方式就是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法的调用者去操心这个问题。
public void read(String filename)throws IOException{
try{
var in = new FileInputStream(filename);
int b;
while((b = in.read())!= -1){
process input
}
}
// 编译器将严格地执行throws说明符。如果调用了一个抛出检查型异常的的方法,就必须处理这个异常,或者继续传递这个异常。
请记住,如果编写一个方法覆盖超类的方法,而这个超类方法没有抛出异常,你就必须捕获你的方法代码中出现的一个检查型异常。不允许在子类的throws说明符中出现超类方法未列出的异常类。
捕获多个异常
一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。要为每个异常类型使用一个单独的catch子句,如下所示:
try{
code that might throw exceptions
}catch(FileNotFoundException e){
emergency action for missing files
}catch(UnkownHostException e){
emergency action for unknown hosts
}catch(IOException e){
emergency action for all other I/O problems
}
// 同一个catch子句中可以捕获多个异常类型
try{
code that might throw exceptions
}
catch (FileNotFoundException | UnkownHostException e){
emergency action for missing files and unkown hosts
}
catch(IOException e){
emergency action for all other I/O problems
}
再次抛出异常与异常链
可以在catch子句中抛出一个异常。通常,希望改变异常的类型时会这样做。如果开发了一个供其他程序使用的子系统,可以使用一个指示子系统故障的异常类型,这很有道理。
try{
access the database
}
catch(SQLException e){
throw new ServletException("database error:"+e.getMessage());
}
// 不过,可以有一种更好的处理方法,可以把原始异常设置为新异常的“原因”:
try{
access the database
}
catch(SQLException original){
var e = new ServletException("database error");
e.initCause(Original);
throw e;
}
// 捕获到这个异常时,可以使用下面这条语句获取原始异常
Throwable original = caughtException.getCause();
有时你可能只想记录一个异常,再将它重新抛出,而不做任何改变:
try{
access the database
}
catch(Exception e){
logger.log(level,message,e);
throw e;
}
fianlly子句
代码抛出一个异常,就会停止处理这个方法中剩余的代码,并退出这个方法。如果这个方法已经获得了只有它自己知道一些本地资源,而且这些资源必须清理,者就会有问题。
不管是否有异常被捕获,finally子句中的代码都会执行。下面的示例中:
var in = new FileInputStream(...);
try{
// 1
code that might throw exception
// 2
}
catch(IOException e){
// 3
show error message
// 4
}
finally{
//5
in.close();
// 6
}
有下列几种情况执行finally子句:
- 代码没有抛出异常。在这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码。随后,继续执行finally子句之后的第一条语句。执行的顺序为1.2.5.6
- 代码抛出了一个异常,并在一个catch子句中捕获。如果catch子句没有抛出异常,程序将执行finally子句之后的第一条语句。这种情况下,执行的顺序是1.3.4.5.6。如果catch子句抛出了一个异常,异常将抛回到这个方法的调用者。执行顺序则只是1、3、5。
- 代码抛出了一个异常,但没有任何catch子句捕获这个异常。在这种情况下,程序将执行try语句块中的所有语句,直到抛出异常为止。这里,执行顺序只是1、5
try语句可以只有finally子句,而没有catch子句。
InputStream in = ...;
try{
code that might throw exceptions
}finally{
in.close();
}
// 无论在try语句块中是否遇到异常,finally子句中in.close()语句都会执行。
InputStream in = ...;
try{
try{
code that might throw exceptions
}finally{
in.close();
}
}
catch(IOException e){
show error message;
}
// 内嵌try语句只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种解决方案不仅清楚,而且功能更强:将会报告fianlly子句中出现的错误。
// 当fianlly子句包含return语句时,有可能会有意向不到的结果。假设利用return语句从try语句块中间退出。在方法返回前,会执行finally子句块。如果finally块也有一个return语句,这个返回值将会遮蔽原来的值,如下列例子:
public static int parseInt(String s){
try{
return Integer.parseInt(s);
}
finally{
return 0; // ERROR
}
}
// 这个方法在正在返回之前会调用finlly中的return语句,这样就会使得方法最后返回0,而忽略原先的返回值。
// finally子句的体要用于清理资源。不要把改变控制流的语句(return、throw、break、continue)放在finally子句中。
try-with-Resources语句
try-with-Resources语句(带资源的try语句)的最简形式为:
try(Resource res = ...){
work with res
}
// try 块退出是,会自动调用res.close().
try(var in = new Scanner(new FileInputStream("/usr/share/dict/words"),StandardCharsets.UTF_8)){
while(in.hahsNext()){
System.out.println(in.next());
}
}
// 这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了fianlly块一样。
在Java9中,可以在try首部中提供之前声明的事实最终变量:
public static void printAll(String[] lines,PrintWriter out){
try(out){ // effectively final variable
for(String line: lines){
out.println(line);
}
}// out.close()
}
分析堆栈轨迹元素
堆栈轨迹元素是程序执行过程中某个特定点上所有挂起的方法调用一个列表。当Java程序因为一个未捕获的异常而终止时,就会显示堆栈轨迹。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
使用异常的技巧
-
异常处理不能代替简单的测试
-
不要过分地细化异常
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 } } // 这种编码方式会使得代码量急剧膨胀 // 正确的做法如下: try{ for(int i = 0;i < 100; i++){ n = s.pop(); out.writeInt(n); } }catch(IOException e){ // problem writing to file }catch(EmptyStackException e){ // stack was empty }
-
充分利用异常层次结构
不要只抛出RuntimeException异常。应该寻找一个适合的子类或创建自己的异常类
不要只捕获Throw异常,否则,这会使你的代码更加难读,更加难维护。
-
不要压制异常
-
在检测错误时,"苛刻"要比放任更好。
-
不要羞于传递异常
很多程序员都感觉应该捕获抛出的全部异常。如果调用了一个抛出异常的方法,例如,FileInputStream构造器或readLine方法,它们会本能地捕获这些可能产生的异常。其实,最好继续传递这个异常,而不是自己捕获:
public void readStuff(String filename)throws IOException { var in = new FileInputStream(filename,StandardCharsets.UTF_8); ... }
使用断言
断言的概念
Java语言引入了关键字assert。这个关键字有两种形式:
assert condition; 和 assert condition:expression;
这两个语句都会计算条件,如果结果为false,则抛出一个AssertionError异常。在第二个语句中,表达式将传入AssertionError对象的构造器,并转换成一个消息字符串。
要想断言x是一个非负数,只需要简单地使用下面这条语句
assert x >= 0;
// 或者将x的实际值传递给AssertionError对象,以便以后显示
启用和禁用断言
在默认情况下,断言是禁用的。可以在运行程序时用-enableassertions或-ea选项启用断言:
也可以在某个类或整个包中启用断言,例如
java -ea:MyClass -ea:com.mycompany.mylib MyApp
// 这条命令将为MyClass类以及com.mycompany.mylib包和它的子包中的所有类打开断言。
使用断言完成参数检查
什么时候应该选择使用断言?应该记住下面几点:
- 断言失败时致命的、不可恢复的错误。
- 断言检查只是在开发和测试阶段打开。
日志
使用日志API的优点:
- 可以容易地取消全部日志记录,或者仅仅取消某个级别以下的日志,而且可以很容易地再次打开日志开关
- 可以很简单地禁止日志记录,因此,将这些日志代码留在程序中的开销很小。
基本日志
要生成简单的日志记录,可以使用全局日志记录器并调用其info方法:
Logger.getGobal().info("File -> Open menu item selected");
高级日志
在一个专业的应用程序中,你可以定义自己的日志记录器,可以调用getLogger方法创建或获取日志记录器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
通常,有以下7个日志级别:
- SEVERE
- WARING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
在默认情况下,实际上只记录前3个级别。也可以设置一个不同的级别,如
logger.setLevel(Level.FINE);
// 现在,FINE以及所有更高级别的日志都会记录
使用Log4j
Log4j是一种非常流行的日志框架,最新版本是2.x
Log4j是一个组件化设计的日志系统,它的架构大致如下:
当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。如
- console:输出到屏幕
- file:输出到文件
- socket:通过网络输出到远程计算机
- jdbc:输出到数据库
在输出日志的过程中,通过Filter来过滤哪些log需要被输出,哪些log不需要被输出。例如,仅输出ERROR级别的日志。最后,通过Layout来格式化日志信息,如,自动添加日期、时间、方法名称等信息。
我们在实际使用的时候,并不需要关心Log4j的API,而是通过配置文件类配置它。
以XML为例:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<!-- 定义日志格式 -->
<Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
<!-- 定义文件名变量 -->
<Property name="file.err.filename">log/err.log</Property>
<Property name="file.err.pattern">log/err.%i.log.gz</Property>
</Properties>
<!-- 定义Appender,即目的地 -->
<Appenders>
<!-- 定义输出到屏幕 -->
<Console name="console" target="SYSTEM_OUT">
<!-- 日志格式引用上面定义的log.pattern -->
<PatternLayout pattern="${log.pattern}" />
</Console>
<!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
<RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
<PatternLayout pattern="${log.pattern}" />
<Policies>
<!-- 根据文件大小自动切割日志 -->
<SizeBasedTriggeringPolicy size="1 MB" />
</Policies>
<!-- 保留最近10份 -->
<DefaultRolloverStrategy max="10" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<!-- 对info级别的日志,输出到console -->
<AppenderRef ref="console" level="info" />
<!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
<AppenderRef ref="err" level="error" />
</Root>
</Loggers>
</Configuration>
虽然配置Log4j比较繁琐,但一旦配置完成,使用起来就非常方便。对上面的配置文件,凡是INFO级别的日志,会自动输出到屏幕,而ERROR级别的日志,不但会输出到屏幕,还会同时输出到文件,并且,一旦日志文件达到指定大小,Log4j就会自动切割新的日志文件,并且最多保留10份。
割日志 -->
虽然配置Log4j比较繁琐,但一旦配置完成,使用起来就非常方便。对上面的配置文件,凡是INFO级别的日志,会自动输出到屏幕,而ERROR级别的日志,不但会输出到屏幕,还会同时输出到文件,并且,一旦日志文件达到指定大小,Log4j就会自动切割新的日志文件,并且最多保留10份。