第七章 - 异常,断言和日志
目录1. 异常
1.1 异常的类型
-
异常对象都是派生于Throwable类的一个类实例,Throwable下可以分为两个分支:
-
Error
:Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。如果出现了这种错误,一般除了通知用户并终止程序外无能为力 -
Exception
:Exception层次结构是要重点关注的,这个层次结构又分为两个分支-
RuntimeException
:由编程错误导致的异常属于RuntimeException- 错误的强制类型转换
- 数组访问越界
- 访问null指针
-
IOException
:程序本身没有问题,由于像I/O错误这类问题导致的异常- 试图超越文件末尾继续读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在
-
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型 异常,所有其他的异常称为检查型 异常。不应该声明从RuntimeException继承的那些非检查型异常,这些运行时错误完全在我们的控制之中,应该多花些时间修正这些错误,而不只是声明这些错误有可能发生。
-
1.2 抛出异常
- 要抛出异常,需要先新建一个异常的对象,然后再抛出,在抛出时可以添加字符串用于描述异常。
1.2.1 抛出已有的异常类
如果一个已有的异常类能满足要求,抛出这个异常只需要:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
String ReadData(Scanner in) throws EOFException
{
//...
while(...){
if(!in.hasNext()){
if(n < len){
String gripe = "Content length:" + len + ", Received:" + n;
throw new EOFException(gripe);
}
}
}
return s;
}
如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用。同理,如果超类方法没有抛出任何检查型异常,子类也不能抛出任何检查型异常。
1.2.2 自定义异常类
创建自定义异常类的习惯做法是,定义一个派生于Exception或其子类的类,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器,用于描述异常
class FileFormatException extends IOException{
public FileFormatException() {};
public FileFormatException(String gripe){
super(gripe);
}
}
1.3 捕获异常
-
如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并在控制台上打印一个消息,其中包括这个异常的类型和一个堆栈轨迹。
-
想要捕获一个异常,需要设置try-catch语句块。如果try语句块中的任何代码抛出了catch子句中指定的一个异常类,那么程序将跳过try语句块的其余代码并执行catch子句中的处理器代码。如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。如果方法中的任何代码抛出了catch子句中没有声明的一个异常类型,那么这个方法就会立刻退出。
-
一个try语句块中可以捕获多个异常类型
try{ } catch(FileNotFoundException e){ } catch(UnknownHostException e){ } catch(IOException e){ }
异常对象可能包含有关异常性质的信息。要想获得这个对象的更多信息,可以尝试使用e.getMessage()得到详细的错误信息,或者使用e.getClass().getName()
-
try语句可以只有finally子句,而没有catch子句
InputStream in = ...; try{ try{ //... } finally{ in.close(); } } catch(IOException e){ } //内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个指责,就是确保报告出现的错误。这种结构的语句功能更强,结构更清晰
-
当finally子句包含return语句时,有可能发生意外。假设利用return语句从try语句块中退出。在方法return前,就会执行finally子句块。如果finally块也有一个return语句,这个返回值将会屏蔽原来的返回值。总之,finally子句的体主要用于清理资源,不要把改变控制流的语句(return,throw,break,continue)放在finally子句中
1.3.1 堆栈轨迹
-
堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。当Java程序因为一个未捕获的异常而终止时,就会显示堆栈轨迹。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息
var t = new Throwable(); var out = new StringWriter(); t.printStackTrace(new PrintWriter(out)); String description = out.toString();
1.4 try-with-Resources语句
-
try块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样
//还可以指定多个资源 try(Scanner in = new Scanner( new FileInputStream("/user/share/..."),StandardCharset.UTF-8); PrintWriter out = new PrintWriter("out.txt",StandardCharsets.UTF-8)) { while(in.hasNext()) System.out.println(in.next().toUpperCase()); }
1.5 使用异常的技巧
-
异常处理不能代替简单的测试
捕获异常所花费的时间大大超过了前者,最好只在异常情况下使用异常
-
不要过分的细化异常
有必要将整个任务包在一个try语句块中,这样当任何一个操作出现问题时,就可以取消整个任务
-
充分利用异常层次结构
不要只抛出RuntimeException异常。应该寻找一个合适的子类或创建自己的异常类。不要只捕获Throwable异常,否则这会使得代码更难度,更难维护
-
不要压制异常
如果要throw一个发生概率极小异常,那么可能会导致编译器对调用了该方法的所有方法报错。因此可以捕获,但不处理这个异常,以此关闭这个异常的目的。不过这样以来即便异常发生了也不会被记录,要做好承担后果的准备
-
早抛出,晚捕获
最好在出错的地方抛出一个合适的异常,而不是返回一个值;也没必要捕获并处理所有的异常,适当的传递可以让程序更有效率
2. 断言
-
断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除致谢检查
-
使用断言有两种方式
-
assert [condition]
:condition为要被测试的语句 -
assert [condition] : [expression]
:expression将传入AssertionError对象的构造器,并转换成一个消息字符串。表达式的唯一目的就是产生一个消息字符串,除此之外它不具备任何功能,Java设计者有意如此以防止在断言中进行异常处理操作。
两个语句都会计算条件,如果结果为false,则抛出一个AssertError异常
-
-
断言的应用场景十分有限,它只应该用于在测试阶段测试程序内部错误的位置
3. 日志
- 日志就像是程序测试时插入的println语句,但是相比之下日志更加规范且功能强大
3.1 创建日志
-
要生成简单的日志记录,可以使用全局日志记录器并调用info方法
Logger.getGlobal.info("File->Open menu item selected"); //在默认情况下会打印以下记录 //十一月 05, 2021 6:35:40 下午 Test main //信息: File->Open menu item selected Logger.getGlobal().setlevel(Level.OFF); //这条指令将会取消掉所有日志
-
也可以自定义自己的日志记录器
//可以调用getLogger方法创建或获取日志记录器 private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
未被任何变量引用的日志记录器可能会被垃圾回收,为了防止这种情况发生,最好用静态变量储存日志记录器的引用
3.2 日志级别
-
与包名类似,日志记录器名也有层次结构,而且层次性更强。日志记录器的父与子之间将共享某些属性,例如,如果对日志记录器"com.mycompany"设置了日志级别,它的子日志记录器也会继承这个级别。通常有以下七个日志级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
默认情况下,只记录前3个级别。如有需要可以手动设置记录的级别
logger.setLevel(Level.FINE); //现在FINE及所有更高级别的日志都会记录 //此外可以使用Level.ALL开启所有级别的日志记录,使用Level.OFF关闭所有级别的日志记录 //可以对同一日志的不同级别添加信息 logger.warning(message); logger.fine(message); //还可以使用log方法指定添加信息的级别 logger.log(Level.FINE,message);
3.3 日志常用方法
-
用于跟踪执行流的方法
//这些调用将生成FINIER级别而且以字符串ENTRY和RETURN开头的日志记录 void entering(String className, String methodName); void entering(String className, String methodName,Object param); void entering(String className, String methodName,Object param[]); void exiting(String className,String methodName); void exiting(String className, String methodName, Object result);
-
记录异常的方法
//throwing调用可以记录一条FINER级别的日志记录和一条以THROW开始的消息 void throwing(String className, String methodName,Throwable t); void log(Level l,String message,Throwable t);
3.4 处理器,过滤器,格式化器
- 本笔记不记录关于处理器,过滤器,格式化器的配置或扩展相关知识,仅记录最基础的相关概念。相关详细内容见书P309~313
3.4.1 处理器
-
日志记录器会将记录发送到父处理器,而最终的祖先处理器有一个ConsoleHandler,由它将记录输出到System.err流。对于一个要记录的日志记录,它的日志级别必须高于日志记录器和处理器二者的阈值。默认的控制台处理器的日志级别为INFO
-
要想将日志记录发送到其它地方,就要添加其它的处理器。API为此提供了两个处理器,FileHandler和SocketHandler。SocketHandler可以将记录发送到指定的主机和端口;Filehandler可以将记录收集到文件中
FileHandler handler = new FileHandler(); logger.addHandler(handler);
这些记录会被发送到用户主目录的javan.log文件中,默认情况下记录会格式化为XML
3.4.2 过滤器
-
在默认情况下,会根据日志记录的级别进行过滤。每个日志记录器和处理器都有一个可选的过滤器来完成附加的过滤。要定义一个过滤器,需要实现Filter接口并定义以下方法:
boolean isLoggable(LogRecord record);
要想将过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了。同一时刻最多只能有一个过滤器
3.4.3 格式化器
-
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但也可以通过扩展Formatter类并覆盖下列方法来自定义格式
String format(LogRecord record);
可以调用setFormatter方法将格式化器安装到处理器中
4. 调试技巧
-
可以使用下面的方法打印和记录任意变量的值
System.out.println("x=" + x); //或 Logger.getGlobal().info("x=" + x);
如果x是一个数值,则会被转换成字符串。如果x是一个对象,那么Java会调用这个对象的toSring方法(这就是为什么在自定义类中写toString这么重要,它可以提供有效信息)
如果还想获得隐式参数对象的状态,可以打印this对象的状态
Logger.getGlobal().info("this=" + this);
-
可以在每一个类中放置一个单独的main方法,这样就可以提供一个单元测试桩(stub),能够独立测试类
public class MyClass{ //类主体 public static void main(String[] args){ //测试代码 } }
可以建立一些对象,然后调用所有的方法,检查每个方法能否争取的完成工作。可以保留这些main方法,在运行应用程序的时候,Java虚拟机只调用启动类的main方法。
-
利用Throwable类的printStackTrace方法,可以从任意的异常对象获得堆栈轨迹,下面的代码可以捕获任意的异常,答应这个异常对象和堆栈轨迹,然后重新抛出异常,以便找到对应的处理器
try{ //... } catch(Throwable t){ t.printStackTrace(); throw t; }
也可以在代码的某个位置插入下面这条语句就可以获得堆栈轨迹:Thread.dumpStack();
-
一般来说,堆栈轨迹显示在System.err上。如果想要记录或显示堆栈轨迹,可以将它捕获到一个字符串中
StringWriter out = new StringWriter(); new Throwable().printStackTrace(new PrintWriter(out)); String description = out.toString();
-
在System.err中显示未捕获的异常的堆栈轨迹并不好。最好将这些信息记录到一个文件中,可以使用静态方法Thread.setDefaultUncaughtExceptionHandler改变未捕获异常的处理器
Thread.setDefaultUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler(){ public void uncaughtException(Thread t,Throwable e){ //保存到文件中 }; });