第七章:异常、断言和日志
错误无处不在,我们希望程序能在遇到错误时,通过错误处理器,返回一种安全状态,或者妥善的保存用户数据然后退出,由此保证程序的健壮性。
7.1.1 异常分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t16JgjmG-1625397745294)(image-20210611081945608.png)]
大致继承层次如上。但是并非不能直接继承自Throwable。
Error层次结构描述系统级别的内部错误或者资源耗尽错误,程序员无法抛出此类错误,而且也无法处理。
RuntimeException:由编程错误导致的异常
IOException:由I/O错误导致的异常
非检查型异常:派生自Error类(在我们的控制之外)或者RuntimeException类(一开始编写时就应该避免的,例如数组越界)
非检查型异常:其他所有
7.1.2 声明检查型异常
抛出异常的情况:
- 调用了一个抛出检查型异常的方法
- 检测到一个错误,并利用throw语句抛出一个检查型异常
- 程序出现错误(例如a[-1]会抛出一个非检查型异常)
- Java虚拟机或者运行时库出现内部错误
在方法头部的多个异常用逗号隔开。注意,只能声明抛出检查型异常,而不能声明抛出非检查型异常
class Test{
public void f() throws ArrayIndexOutOfBoundsException {//错误,这是一个RuntimeException
...do something
}
}
注意:
class A{
public void f() throws IOException {
//...
}
}
class B extends A {
public void f() throws Exception{
//报错,子类覆写方法的Exception不能是超类的异常的超类
//类比于访问控制符
//如果超类没有抛出任何异常,子类就不能抛出异常
}
}
7.1.3 如何抛出异常
有手就行。除此之外可以调用含字符串的构造器更好的描述异常。
7.1.4 创建异常类
派生自Exception,或者它的子类即可
class MyException extends Exception{
public MyException() {
}
public MyException(String message){
super(message);
}
}
7.2 捕获异常
7.2.1 捕获异常
try catch语句。有手就行。
7.2.2 捕获多个异常
一try多catch,catch中用 " | " 分开。有手就行
7.2.3 再次抛出异常与异常链
可以在catch语句中再次抛出异常
出来新建,还可以调用initCause方法把原始异常设置为新异常的"原因"
try{
ExceptionTest.f(1);
}catch (Exception original){
var e = new RuntimeException("error in:");
e.initCause(original);
throw e;
//捕获之后,使用下面这条语句来获取原始异常
Throwable original = caughtException.getCause();
强烈建议使用这种异常链式,这样在传递的时候不会丢失底层异常的具体信息。
7.2.4 finally语句
无论是否有异常,无论catch语句怎么操作(例如抛出新的异常)finally块的语句都会执行。这多用于关闭资源,连接等最终处理操作。
final语句的功能应该限定于清理资源,而不应该写任何逻辑。
package com.package1;
import java.io.IOException;
public class Test {
public static void main(String[] args) {
System.out.println(FinalTest.f());
}
}
class FinalTest{
public static int f(){
try{
return Integer.parseInt("这里是要报错的");
//按道理来讲,会报一个转换错误,但是final语句直接return了,这里没用了
}
finally {
return 0;
}
}
}
7.2.5 try-with-Resources语句
是一种扩展的写法
open a resource
try{
work with the resource
}
finally{
close the source
}
//假如这个资源实现了AutoCloseable接口(有close方法,抛出Exception,它还有一个Closeable接口,抛出的是IOException)可以这样写:
try (Resource res = ...){
work with res;
}
//在退出try代码块时会自动执行close方法.
try(var in = new Scanner(new FileInputStream("/user/share/dict/word"),StandardCharset.UTF_8),
var out = new PrintWriter("out.txt",StandardCharset.UTF_8)){
while(in.hasNext()){
out.println(in.next().toUpperCase());
}
}
// 无论这个块如何退出,in和out都会被关闭
try-with-Resource语句也可以有自己的catch和final语句,这些语句会在close之后执行
7.2.6 分析堆栈轨迹元素
堆栈轨迹(stack trace),程序执行过程中某个特定点上的所有挂起方法调用的一个列表(那一层层的嵌套报错)
package com.package1;
import java.io.PrintWriter;
import java.io.StringWriter;
public class Test {
public static void main(String[] args) {
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
//可以调用Throwable类的pringtStackTrace方法访问堆栈轨迹的文本描述信息
String description = out.toString();
System.out.println(description);
System.out.println("finished");
}
}
//"C:\Program Files\Java\jdk-15.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=49790:C:\Program Files\JetBrains\IntelliJ IDEA 2020.3.2\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\NieGuevara\Desktop\JavaRelearn\out\production\JavaRelearn com.package1.Test
//java.lang.Throwable
// at com.package1.Test.main(Test.java:9)
//finished
//还可以使用StackWalker类
//它会生成一个StackWalker.StackFrame实例流,每一个实例描述一个栈帧(stack frame)
StackWalker walker = StackWalker.getInstance();
walker.foeEach(frame -> analyze frame);
//如果想使用懒方式处理
walker.walk(stream -> process stream);
package com.package1;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
try(var in = new Scanner(System.in)){
System.out.println("Enter n");
int n = in.nextInt();
factorial(n);
}
}
public static int factorial(int n){
System.out.println("factorial("+ n + ")");
var walker = StackWalker.getInstance();
walker.forEach(System.out::println);
int r;
if(n <= 1){
r = 1;
}
else{
r = n * factorial(n-1);
}
System.out.println("return " + r);
return r;
}
}
//result:
Enter n
5
factorial(5)
com.package1.Test.factorial(Test.java:19)
com.package1.Test.main(Test.java:13)
factorial(4)
com.package1.Test.factorial(Test.java:19)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.main(Test.java:13)
factorial(3)
com.package1.Test.factorial(Test.java:19)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.main(Test.java:13)
factorial(2)
com.package1.Test.factorial(Test.java:19)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.main(Test.java:13)
factorial(1)
com.package1.Test.factorial(Test.java:19)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.factorial(Test.java:25)
com.package1.Test.main(Test.java:13)
return 1
return 2
return 6
return 24
return 120
Process finished with exit code 0
7.3 使用异常的技巧
1.异常处理不能代替简单的测试
if(!s.empty()){
s.pop();
}
//运行时间明显少于
try{
s.pop();
}catch(EmptyStackException){
...
}
只在异常情况下使用异常
2.不要过分细化异常
不要写多个并列或者嵌套的异常
3.充分利用异常层次结构
不要只抛出超类的那些异常,应该细化
4. 不要压制异常
意即不要catch了之后什么也不干
5.在检测错误时,苛刻比放任更好
能抛出异常,就直接抛出异常,而不是返回一个null,这有很大可能在之后抛出一个NullPointerException异常
6.不要羞于传递异常
自己不能处理就继续上抛
7.4 使用断言
7.4.1 断言的概念
在测试期间向代码插入一些检查,而在生产代码中自动删除这些检查。
assert condition;
//或者
assert condition : expression;//表达式将会传入AssertError的构造器,并转换成一个消息字符串.
//如果结果为false,则抛出一个AssertionError异常
Java中assert失败不会自动打印断言的相关信息,而需要程序员自己写expression
public class Test {
public static void main(String[] args) {
int x = 1;
assert x < 0 : "x应该小于0";
}
}
但是是不报错的,因为默认assert是被ban了
7.4.2 启用和禁用断言
可以在运行程序时使用 -enableassertions 或者 -ea 启用断言
而且,不用重新编译程序来启用或者禁用断言。启用和禁用断言是类加载器的功能。
也可以在某个类或整个包中启用断言
java -ea:MyClass -ea:com.werun.njq MyApp;
可以使用-disableassertion 或者 -da 在某个特定类或者包中禁用断言
对于系统类,可以使用-esa , -enablesystemassertions 开启断言
7.4.3 使用断言完成参数检查
- 断言是致命的
- 断言检查只是在开发和测试阶段打开
7.5 日志
这里仅介绍java标准日志,log4j2,Logback什么的先不做介绍
7.5.1 基本日志
package com.package1;
import java.util.logging.Logger;
public class Test {
public static void main(String[] args) {
Logger.getGlobal().info("打印一条日志");
}
}
//6月 16, 2021 7:55:02 下午 com.package1.Test main
//信息: 打印一条日志
调用 Logger.getGlobal().setLevel(level.OFF);会关闭日志
private static final Logger myLogger = Logger.getLogger("com.package1.Test");
//获得自己的日志记录器而非使用全局的(或造成混乱)
//声明为static防止被垃圾回收(如果未被任何变量引用,是有可能的)
包于包之间是没有任何语义关系的,但是对于日志记录器,在子包里面的日志记录器会继承母包的日志级别:
- SEVERE(最高级别)
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINSET
默认只用前三个。也可以设置不同的级别。可以用Level.OFF关闭所有,或者Level.ALL开启所有
logger.setLevel(Level.FINE);
logger.warning(message);
logger.log(Level.FINE,message);
默认的日志处理器会抑制低于INFO级别的日志
package com.package1;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Test {
public static void main(String[] args) {
myLogger.setLevel(Level.FINE);
//即便如此,INFO以下的信息仍然不会输出,需要配置Handler。
f(1);
}
private static int f(int n){
myLogger.entering("com.package1.Test","f",n);
//生成的日志是FINER级别的,
myLogger.logp(Level.FINER,"com.package1.Test","f","logp会获得调用类和方法的准确位置");
n++;
myLogger.exiting("com.package1.Test","f",n);
return n;
}
private static final Logger myLogger = Logger.getLogger("com.package1.Test");
}
记录异常也是一种通常的用法
if(...){
var e = new IOException("...");
logger.throwing("com.werun.njq","f",e);
//级别为FINER
throw e;
}
//or:
try{
...
}catch(IOException e){
Logger.getLogger("com.werun.njq").log(Level.Warning,"Reading image",e);
}
7.5.3 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各个属性,默认情况下配置文件位于
conf/logging.properties
7.5.4 本地化
采用资源包实现本地化(亦或者说国际化)
在请求一个日志记录器时,可以指定一个资源包
具体见书309
7.5.5 处理器
默认情况下日志记录器将记录发送到ConsoleHandler,并由它输出到system.err流
7.5.6 过滤器
实现Filter接口并重新定义isLoggable方法
7.5.7 格式化器
扩展Formatter类并覆盖format方法