学习《Java核心技术》——第7章:异常、断言和日志
Java核心技术.卷I(第11版)
第7章:异常、断言和日志
- 处理错误
- 捕获异常
- 使用异常的技巧
- 使用断言
- 日志
- 调试技巧
1.处理错误
异常分类:
异常对象都是派生于Throwable类的一个类实例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QOnIE6Ni-1662469433341)(images/image-20220905201632403.png)]
Error:Java运行时系统的内部错误和资源耗尽错误
Exception分为:
-
RuntimeException包括:(一般是由编程错误导致的异常,这个取决于你代码的操作,也就是可以通过代码规避)
-
错误的强制类型转换
-
数组越界访问
-
访问null指针
-
-
其他异常(如IO异常,这个取决于环境,也就是通过代码也规避不了,比如检查文件存在后文件就删了,后面用到这个文件就报错)
-
试图超越文件末尾继续读取数据
-
试图打开一个不存在的文件
-
试图根据指定字符串查找Class对象,但这个字符串表示的类,不存在。
-
非检查型(unchecked)异常:派生于Error类或RuntimeException类的所有异常。为啥这样分???
检查型(checked)异常:所有其他的异常,如IO异常
声明检查型异常:
使用throws xxxException, xxxException1
声明,不需要声明Error,控制不了。
一个方法必须声明所有可能抛出的检查型异常,否则编译不通过。
子类覆盖父类方法,抛出的异常不能比父类的范围大,也可以不抛出异常;如果父类没有抛出任何检查型异常,那子类也不能抛出任何检查型异常。
如何抛出异常:
String readDate() throws EOFException{
throw new EOFException();
}
- 合适的异常类
- 创建异常类对象
- 将对象抛出
创建异常类:
继承Exception的某个子类,习惯做法是,一个默认构造器,一个包含详细描述信息的构造器
2.捕获异常
不捕获异常,发生异常后程序就会终止。
1.可以使用try/catch
语句块
try{
...;
}catch(ExceptionType e){
//处理异常
}
2.也可以将异常传递给调用者
public void read() throws IOException{
code;
}
一般而言,捕获知道要怎么处理的异常,继续传播不知道具体如何处理的异常。
需要注意的是,覆盖父类方法时,子类方法抛出的异常范围不能超过父类方法。如果父类方法不抛出异常,此时只能使用try/catch
语句捕获异常,不能使用throws
声明异常。
捕获多个异常:
try{
code;
}catch(XXXException e){
//处理异常1
}catch(XXXException e){
//处理异常2
}catch(XXX3Exception | XXX4Exception e){
//处理异常3|4
//同一个catch同时捕获多个异常类型
}catch(XXXException e){
//处理异常5
}
异常变量e,隐含为final变量。
再次抛出异常和异常链:
try{
}catch(XXXException e){
throw new XXXException();
}
更好的更推荐的办法,使用包装技术抛出异常,把原始异常设置为新异常的"原因":待具体
try{
}catch(XXX1Exception original){
var e = new XXX2Exception();
e.initCause(original);
throw e;
}
finally子句
一般用于对资源的清理(处理)
try{}catch(XXXexception e){}finally{}
不管是否捕获异常,finally子句中的代码都会被执行。
更实用的是try-with-resources
语句
也可以,只使用try/finally
来保证资源的正常处理
嵌套结构,内部处理资源,外部捕获异常,内外分明
//嵌套的结构
try{
try{
}finally{
}
}catch(XXXException e){
}
注意,如果finally子句包含return语句时,不管是抛出异常,还是方法返回一个结果对象,都会在这之前执行finally里的语句,此时finally里的return语句会覆盖原有的结果!
因此,finally子句的体一般用于清理资源。不要把改变控制流的语句(return,throw,break,continue)放在finally子句中。
try-with-Resources
语句:
如果资源属于一个实现了AutoCloseable接口的类,用这个语句会在try块退出时,自动关闭资源。
AutoCloseable接口有一个方法:void close() throws Exception
还有一个Closeable接口,是AutoCloseable的子接口,也只包含一个close方法,但是该方法声明为抛出一个IOException。
语法:
try(Resource res = ...){
work with res
}//try块退出时(正常执行完或者遇到异常),会自动调用res.close()
后面也可以跟catch、finally子句
分析堆栈轨迹(stack trace)元素:
Throwable类的printStackTrace方法
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
string description = out.tostring();
更灵活的方式,使用StackWalker类
它会生成一个和StackWalker.StackFrame实例流,其中每个实例分别描述一个栈帧(stack frame)。
StackWalker walker = StackWalker.getInstance();
walker.forEach(frame -> analyze frame);
//以懒方式处理Stream<StackWalker.StackFrame>,可以调用:
walker.walker(frame -> process stream);
利用这个类的一些方法,可以得到执行代码行的文件名和行号,类对象和方法名。toString方法包含这些信息。
3.使用异常的技巧
-
异常处理不能代替简单的测试,只在异常情况下使用异常
捕获异常花费时间巨大
-
不要过分细化异常,将正确处理和错误处理分开就行
比如保险起见,每条语句都用
try/catch
。这样代码量激增,且意义不大。 -
充分利用异常层次结构
指使用更加具体意义更加明确的异常
-
不要压制异常
有异常就要处理,即使可能性特别小,后果就是一旦异常不处理,就会终止程序
-
在检测错误时,要严格一些
在出错的地方,就抛出异常提示,比如pop一个空栈,应该抛出EmptyStackException,而不是返回null。
-
不要害怕传递异常
有利于向更高层次通知错误,向用户报告错误
4.使用断言
断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查。
assert condition;
assert condition : expression;
计算条件,如果false,则抛出一个AssertionError异常。
expression传入AssertionError对象的构造器,转换成一个消息字符串。
但是,AssertionError对象并不保存表达式的值。否则很容易让程序员尝试从断言中回复程序的运行,违背了断言的初衷。
启用和禁用断言:(属于类加载器的功能,不需要重新编译)
启用:在运行程序时用-enablearrertions
或-ea
选项启动断言,即
java -enableassertions MyApp
禁用:使用disableassertions
或-da
在某个特定类和包中禁用断言
由于,有的类不是由类加载器加载,而是直接由虚拟机加载,比如一些“系统类”,需要使用-enablesystemassertions/-esa
来开关启动断言。
也可以通过编程来控制类加载器的断言状态。
- edit configuration
- Modify options
- add VM options
使用断言完成参数检查:
在java中,有3种处理系统错误的机制:
- 抛出一个异常
- 日志
- 使用断言
使用断言的情况:(用于开发、测试阶段检查错误)
- 断言失败是致命的、不可恢复的错误
- 断言检查只是在开发和测试阶段打开,用于确定程序内部错误的位置
比如:规定方法的参数不能为空,则在方法开头,可以使用断言检查参数是否为空,这也叫前置条件(Precondition),不满足前置条件,就会触发断言。
使用断言提供假设文档
。。。。
就一些if/else if/else,不能穷举,所以会在最后的else后跟上行注释。
这样的情况可以的使用assert断言来检查,也相当于是注释了。
5.日志
类似sout
输出信息调试,但是sout
需要后面再注释掉。
建议使用日志API用于调试,很方便。
优点(原文):
-
可以很容易地取消全部日志记录,或者仅仅取消某个级别以下的日志,而且可以很容易地再次打开日志开关。
-
可以很简单地禁止日志记录,因此,将这些日志代码留在程序中的开销很小。
-
日志记录可以被定向到不同的处理器,如在控制台显示、写至文件,等等。
-
日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤器实现器指定的标准丢弃那些无用的记录项。
-
日志记录可以采用不同的方式格式化,例如,纯文本或XML
-
应用程序可以使用多个日志记录器,它们使用与包名类似的有层次结构的名字,例如,com.mycompany.myapp。
-
日志系统的配置由配置文件控制。
比如日志框架Log4J。
基本日志,Logger:
全局日志记录器Logger.getGlobal()
:
Logger.getGlobal().info("log info"); //打印记录
Logger.getGlobal().setLevel(Level.OFF);//禁用
高级日志:
就是拆分全局记录器,建立自己的日志记录器。
private static final Logger myLogger = logger.getLogger("com.xxx.xxx");
使用静态变量存储日志记录器的引用,防止没有引用没有被使用而被垃圾回收。
日志的7个级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
也可以自己设置新的级别logger.setLevel(Level.Fine)
logger.warining(msg);//设置warning级别日志
logger.fine(msg);
logger.log(Level.FINE, msg);//设置日志
具体看API
修改日志管理器配置:
配置文件默认在:conf/logging.properties
或jre/lib/logging.properties
(Java9之前)
日志记录器并不会把消息发送到控制台,那是处理器的任务,可以这样设置将消息打印到控制台:
java.util.logging.console Handler.level=FINE
日志管理器在虚拟机启动时初始化,也就是在main方法执行前。
本地化:
资源包(resource bundle),包括一组映射,分别对应各个本地化环境。
可以有多个资源包,不同包用于不同部分。
ResourceBundle类管理资源包
处理器:
-
ConsoleHandler
-
FileHandler
将记录收集到文件中
-
SocketHandler
将记录发送到指定的主机和端口
处理器也有日志级别。因此,在配置文件里,不光要修改日志记录器的级别也要修改处理器级别。
过滤器:
每个日志记录器和处理器都有一个可选的过滤器来完成附加的过滤。
实现Filter接口并定义boolean isLoggable(LogRecord record)
格式化器:
将日志记录以一定格式输出
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。
扩展Formatter类并覆盖String format(LogRecord record)
方法
日志技巧:
- 简单的应用,使用一个日志记录器就行
- 默认的日志配置会把等级等于或高于INFO的所有消息记录到控制台
- 所有级别为INFO、WARNING和SERVER的消息都将显示到控制台
。。。
6.调试技巧
- sout
- 在类里,psvm,测试这个类
- JUnit(http://junit.org),单元测试框架
- 日子代理(logging proxy),是一个子类的对象,可以解惑方法调用,记录日志然后调用父类的方法。
- 利用Throwable类的printStackTrace方法,可以从任意的异常对象获得堆栈轨迹。
- 将程序错误记录到文件中,
- 捕获错误流:
java myApp 2> err.txt
- 同时捕获System.err和system.out:
java myApp 1> errors.txt 2>&1
- 捕获错误流:
- 用静态方法Thread.setDefaultUncaughtExceptionHandler改变未捕获异常的处理器==?==
- 观察类的加载过程,启动Java虚拟机时可以使用
-verbose
标志。