暂时跳过的部分:
- 7.2.6 堆栈轨迹, All
Ch.VII 异常, 断言与日志:
7.1 处理错误:
Java的异常处理与C++非常的相似
首先, 是Java内置的异常类的继承结构:
其中:
-
Error 类描述了 Java 运行时系统内部错误和资源耗尽的错误 (注意这玩意不是异常, 是错误)
出现这种情况, 通常无力回天, 只能通告给用户, 并尽力使程序安全的终止
异常是可以被处理的,而错误是没法处理的
-
Exception规定的异常是程序本身可以处理的异常, 其中的两个大类:
- RuntimeException:
程序错误导致的异常 - IOException
由于I/O 错误这类问题导致的异常
- RuntimeException:
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常, 无法预先捕捉处理,而所有其他的异常称为受查(checked)异常, 可以预先捕捉处理
-
Checked Exception
可检查的异常,这是编码时非常常用的,所有checked exception都是需要在代码中处理的
它们的发生是可以预测的,正常的一种情况,可以合理的处理比如IOException,或者一些自定义的异常
-
Unchecked Exception
RuntimeException及其子类都是unchecked exception。这种异常是运行时发生,无法预先捕捉处理
比如NPE空指针异常,除数为0的算数异常ArithmeticException等等
类比一下C++中的异常继承结构:
与C++异常类的比较:
- RuntimeException相当于C++中的logic_error, 表示程序中的逻辑错误
- 而其他所有的非RuntimeException异常, 都相当于C++中的runtime_error异常, 是所有由于不可预测的原因所引发的异常
所以这俩是相反的…
声明受查(checked)异常:
其实前头已经见识过了如何声明一个方法可能抛出的异常:
//前头自建的clone方法:
class Employee implements Cloneable
// 这里仅仅是将Object中的clone覆盖为public方法, 并没有添加其他的功能
public Employee clone() throws CloneNotSupportedException{
return (Employee) super.clone();
}
如果一个方法可能抛出多个异常, 则用,
逗号连接:
public Image loadlmage(String s) throws FileNotFoundException, EOFException{
...
}
方法能够抛出的异常类型:
方法只能也只应该抛出可能的受查异常
对于非受查异常, 要么无法控制(Error), 要么需要避免发生(RuntimeException)
void drawlmage(inti) throws ArraylndexOutOfBoundsException // bad style
异常与继承:
如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用
也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常
特别需要说明的是, 如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常
关于Java & C++中throw的区别:
在C++中, 异常的throw是在程序运行中执行的
即在try语句块中, 程序判定需要抛出异常时就会调用throw
而在Java中, 异常时在编译时执行并抛出的
异常的抛出:
对于Java内置的异常, 其抛出非常简单:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 使用throw将对象抛出
throw new EOFException; //此处将抛出一个EOFException异常
对于自定义异常, 抛出方式与Java内置异常相同, 主要在其定义:
-
选择合适的异常超类
-
定义两个构造器: 默认的无参构造器, 带有详细描述信息的构造器
其中后者的详细信息将会被基类Throwable的toString打印, 非常方便
7.2 异常捕获:
与C++相同, Java也有try & catch
语句块, 用来处理异常, 且语法与C++相同, 这里就不做阐述了
异常处理与异常抛出的关系:
如果一个方法在内部可能出现异常, 但是在内部自行消化解决了, 就不需要在方法头用throw声明可能抛出的异常, 但是如果没有处理, 或者希望方法的调用者来处理, 则需要使用throw说明可能抛出的异常, 而这样, 调用者就需要自己编写try & catch语句块
捕获多个异常:
语法仍然与C++相同
但是Java SE 7 中支持一个新操作, 对相似的异常进行合并, 用一个catch进行处理:
try{
code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException e){ //两个异常合并处理
emergency action for missing files and unknown hosts
}
catch (IOException e){
emergency action for all other I/O problems
}
当多个异常的处理方式相同, 且捕获的异常类型之间不存在子类关系时, 推荐这么整
异常的在抛出:
就是在catch中再抛出一个异常, 主要是为了改变异常的类型
一种比较高级的应用是将接受到的异常进行一定程度的包装, 而后返回一个更上层的异常:
try{
access the database;
}
/**
* 创建一个新的上层异常, 而后将当前接受到的异常设置为上层异常的原因
* 当外部捕获到异常时, 可以通过getCause()函数获得原始异常
*/
catch(
SQLException e){
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
Throwable e = se.getCause(); //获得异常产生原因, 即方法内部的原始异常
finally字句:
无论是否发生异常,finally 代码块中的代码总会被执行, 所以通常在finally代码块中设置清理类型等收尾善后性质的语句
比如打开的文件需要关闭, 等等…
finally代码块解决了这些善后问题, 否则想要处理, 就需要在正常代码与catch的异常处理代码中同时设置善后代码
finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
代码优化: 将try/catch & try/finally 解耦合:
其实就是将try/catch & try/finally分开处理, 使得代码更加清晰
其中外层的try与catch匹配, 内层的try与finally匹配, 各自负责单一的职能
InputStrean in = . . .;
try{
try{
//code that might throw exceptions
}
finally{
in.close();
}
}
catch (IOException e){
//show error message
}
带资源的try语句:
这里主要针对上头可能发生的问题:
当try抛出异常后, 执行finally语句块时, 又发生了异常, 咋整?
如果在finally中不对finally中出现的异常做处理, 这个异常就会将上一个异常抑制, 使得上一个异常无法发出…
所以这里引入带资源的try语句
, 语法如下:
就是在try语句后头加上一个使用的资源 (此处的资源是指那些必须在程序结束时显式关闭的资源,比如数据库连接,网络连接等) 这里的资源必须实现了Closeable或AutoCloseable接口
使用的资源会在try退出后自动被关闭
public static String processFile() throws IOException {
try (
BufferedReader br = new BufferedReader(new FileReader("D:data.txt"))
)
{
return br.readLine();
}
}
如果需要使用多个资源, 需使用分号分隔 (注意不是逗号):
try (//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.bin"));
//创建对象输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.bin"));
)
{
//序列化java对象
oos.writeObject(s);
oos.flush();
//反序列化java对象
s2 = (Student) ois.readObject();
}
分析堆栈轨迹元素:
这部分先放着
7.3 使用异常机制的技巧:
这里介绍了程序设计中处理异常的几个设计技巧, 还是蛮有用的
-
异常处理相当耗时, 所以能用简单的测试完成的任务不要使用异常处理
执行上百万次下头的操作:
//方法1 if (!s.empty()) s.pop(); //方法2 try { s.pop(); } catch (EmptyStackException e) { //void }
方法1耗时: 646 ms
方法2耗时: 21739 ms -
使用更为合适的异常
如果Java内置异常类中没有合适的, 应当创建自己的异常, 而不是执着与RuntimeException
-
捕获更为精确的异常:
InputStream is = null; try { is = new FileInputStream("file.txt"); } catch (Exception e) { e.printStackTrace(); }
实际应该捕获
FileNotFoundException
,却捕获了泛化的Exception
, 使得异常发生的具体原因未知 -
优先关闭没啥用的异常, 而不是压制(放着不管)
当一个异常基本不会发生, 或是没啥影响时, 总是想把它直接忽略, 但是这样无法通过编译
可以直接在try&catch中接收, 然后啥也不干:
public Image loadImage(String s) { try { // code that threatens to throw checked exceptions } catch (Exception e) {} // 就是啥也不干 }
-
对于一套处理流程, 将其包装在一个try&catch中, 比每个语句单独一个try&catch要有效的多
比如:
PrintStream out; Stack s; for (i = 0;i < 100; i++) { try { n = s.popO; } catch (EmptyStackException e) { //stack was empty } try { out.writelnt(n); } catch (IOException e) { // problem writing to file } }
这样不但使得代码量迅速膨胀, 且还达不到良好的处理效果: 实际上这是一套处理流程, 一旦中间某处出现问题, 整个流程都应当取消
优化后如下:
try { for (i = 0; i < 100; i++) { n = s.popO; out.writelnt(n); } } catch( IOException e) { // problem writing to file } catch( EmptyStackException e) { // stack was empty }
7.4 使用断言:
与C++的断言机制相同, Java断言同样是用在开发期的DEBUG, 而release版本中可以移除所有的断言代码, 提升程序的运行速度
断言库语法:
assert 布尔表达式;
assert 布尔表达式:消息;
这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常
在第二种形式中,表达式将被传人 AssertionError 的构造器, 并转换成一个消息字符串, 而后可以通过try&catch捕捉异常, 从中导出异常信息
public static void main(String[] args) {
testClass outObj = new testClass();
outObj.anonymousClassTest();
int x=1;
try {
assert x<0 : "This is an assert";
}
catch (AssertionError assR){
System.out.println(assR.getMessage());
}
}
程序输出:
This is an assert
断言の合适使用:
断言的合适使用时机:
-
断言失败是致命的, 不可恢复的错误
所以如果是可恢复的错误, 应该使用异常来处理
-
断言检查只用于开发和测阶段
所以不应该把断言的内容暴露给用户
断言只应该用于在测试阶段确定程序内部的错误位置
7.5 记录日志:
日志API的优点:
- 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
- 可以很简单地禁止日志记录的输出, 因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器, 用于在控制台中显示, 用于存储在文件中等。
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
- 日志记录可以采用不同的方式格式化,例如,纯文本或 XML。
- 应用程序可以使用多个日志记录器, 它们使用类似包名的这种具有层次结构的名字,例如, com.mycompany.myapp
- 在默认情况下,日志系统的配置由配置文件控制。 如果需要的话, 应用程序可以替换这个配置。
由于这里采用的是java自带的日志系统, 而广泛使用的是log4j, 更为NB的是slf4j, 所以, 这里的笔记采用混合学习的方式
Log4j & Slf4J 的安装:
-
首先需要使用Maven来管理项目
不知道为啥, 但是Baidu大部分教程都是用这玩意的
-
在pom.xml中添加相应的依赖,然后点击右下角的import changes 即可自动导入相应的包
pom.xml添加的内容如下:
注意是添加在最后的</project>
上头<dependencies> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.2</version> </dependency> </dependencies>
添加后大概是这个样子:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>MarvenTestProject</artifactId> <version>1.0-SNAPSHOT</version> //新添加的内容: <dependencies> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.2</version> </dependency> </dependencies> </project>
-
在src>main>resources下添加 log4j.properties文件,内容如下:
其中部分路径可修改
### 设置### log4j.rootLogger = debug,stdout,D,E,I ### 输出信息到控制抬 ### log4j.appender.stdout = org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target = System.out log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n ### 输出DEBUG 级别以上的日志到=E://logs/log.log ### log4j.appender.D = org.apache.log4j.DailyRollingFileAppender log4j.appender.D.File = E://logs/log.log log4j.appender.D.Append = true log4j.appender.D.Threshold = DEBUG log4j.appender.D.layout = org.apache.log4j.PatternLayout log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n ### 输出ERROR 级别以上的日志到=E://logs/error.log ### log4j.appender.E = org.apache.log4j.DailyRollingFileAppender log4j.appender.E.File =E://logs/error.log log4j.appender.E.Append = true log4j.appender.E.Threshold = ERROR log4j.appender.E.layout = org.apache.log4j.PatternLayout log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n ### 输出INFO 级别以上的日志到=E://logs/info.log ### log4j.appender.I = org.apache.log4j.DailyRollingFileAppender log4j.appender.I.File =E://logs/info.log log4j.appender.I.Append = true log4j.appender.I.Threshold = INFO log4j.appender.I.layout = org.apache.log4j.PatternLayout log4j.appender.I.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
-
添加用于测试的class文件testDemo, 并添加如下代码
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class testDemo { private static Logger logger = LoggerFactory.getLogger(testDemo.class); public static void main(String[] args) { System.out.println("This is println message. yeah"); // 记录debug级别的信息 logger.debug("This is debug message."); // 记录info级别的信息 logger.info("This is info message."); // 记录error级别的信息 logger.error("This is error message."); } }
-
构建文件
期间会下载所需要的包, 可能会比较耗时一点
但Maven就这个功能最强大, 可以自动下载项目依赖库, 免去了配置的麻烦问题最后的输出结果:
This is println message. yeah
[DEBUG] 2020-01-22 10:14:02,955 method:HelloMaven.main(HelloMaven.java:15)
This is debug message.
[INFO ] 2020-01-22 10:14:02,958 method:HelloMaven.main(HelloMaven.java:17)
This is info message.
[ERROR] 2020-01-22 10:14:02,958 method:HelloMaven.main(HelloMaven.java:19)
This is error message.
如果在编译时出现这个:
Warning:java: 源值1.5已过时, 将在未来所有发行版中删除
这玩意是Maven的锅, 可以参考这个教程解决
总之到这里总是可以跑起来了
后来发现也不用去下SLF4j的jar包啥的, IDEA好像会自动完成
还是啥也不懂啊…
Log4j 使用教程:
上头的程序跑起来之后, 多少是有点B数了…现在可以开始正式的学习
定义配置文件:
当然, 完全可以在代码中配置Log4j环境, 但是使用配置文件可将Log4j 的环境与代码分离, 使得Log4j使用起来比较方便灵活
Log4j支持两种配置文件的方法:
-
XML格式的文件
类似于Maven的pom.xml, 这个先放着
-
Java专用配置文件
.properties
上头使用的就是这个, 下头学的也是这玩意
配置rootLogger
其语法为:
log4j.rootLogger = [ level ] , appenderName, appenderName, …
其中:
-
level 是日志记录的优先级
通过这里定义级别, 可以控制程序中相应级别的日志信息的输出
总共有7个等级, 从上到下范围一次增大
即范围大的包含范围小的, 当INFO打开时, DEBUG信息不输出- ALL level:打开所有日志记录开关;是最低等级的,用于打开所有日志记录
- DEBUG:输出调试信息;指出细粒度信息事件对调试应用程序是非常有帮助的
- INFO: 输出提示信息;消息在粗粒度级别上突出强调应用程序的运行过程
- WARN: 输出警告信息;表明会出现潜在错误的情形
- ERROR:输出错误信息;指出虽然发生错误事件,但仍然不影响系统的继续运行
- FATAL: 输出致命错误;指出每个严重的错误事件将会导致应用程序的退出
- OFF level:关闭所有日志记录开关;是最高等级的,用于关闭所有日志记录
通常只使用ERROR、WARN、INFO、DEBUG这四个等级的Log
-
appenderName 指定日志输出位置的别名
别名需要在下头自定义
可以同时指定多个位置, 并用逗号,
分割
如之前的配置信息:
log4j.rootLogger = debug,stdout,D,E
表明日志等级设置为debug, 且将Log输出到stdout, D, E中, 后头的这仨是log4j.appender.stdout
与 log4j.appender.D
与log4j.appender.E
配置日志信息输出目的地log4j.Appender
主要语法:
log4j.appender.appenderName = fully.qualified.name.of.appender.class
log4j.appender.appenderName.option1 = value1
…
log4j.appender.appenderName.option = valueN
-
第一句是创建一个别名, 并将其指向log4j中提供的几个appender之一
其中,Log4j提供的appender有以下几种:
-
org.apache.log4j.ConsoleAppender
输出到控制台
-
org.apache.log4j.FileAppender
输出到特定文件
-
org.apache.log4j.DailyRollingFileAppender
每天创建一个文件并将日志输出至其中
-
org.apache.log4j.RollingFileAppender
文件大小到达指定尺寸的时候产生一个新的文件
-
org.apache.log4j.WriterAppender
将日志信息以流格式发送到任意指定的地方
-
-
而后是设置输出的各个属性:
比较常用的是这几个:
#定义日志文件的存储路径 log4j.appender.myFile.File=src/log/logProperties/log4j.log #定义日志文件的大小 log4j.appender.myFile.MaxFileSize=1024kb #定义日志文件最多生成几个(从0开始算1个,即此处最多3个文件) #超过该大小则会覆盖前面生成的文件 log4j.appender.myFile.MaxBackupIndex=2 #针对可能出现的中文乱码问题, 将编码设置为UTF-8: log4j.appender.myFile.encoding=UTF-8; #或者可以使用GBK: log4j.appender.myFile.encoding=gbk;
配置日志信息的格式:
log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class
log4j.appender.appenderName.layout.option1 = value1
…
log4j.appender.appenderName.layout.option = valueN
其中log4j提供有几种预定义好的格式:
- org.apache.log4j.HTMLLayout
以HTML表格形式布局 - org.apache.log4j.PatternLayout
可以灵活地指定布局模式 - org.apache.log4j.SimpleLayout
包含日志信息的级别和信息字符串 - org.apache.log4j.TTCCLayout
包含日志产生的时间、线程、类别等等信息
其中org.apache.log4j.PatternLayout支持使用类似C语言的printf格式进行自由设置:
%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r 输出自应用启动到输出该log信息耗费的毫秒数
%c 输出所属的类目,通常就是所在类的全名
%t 输出产生该日志事件的线程名
%m 输出日志所在方法的名字
%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格
比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。
其他的辅助语法类似于printf
如上头的例子中:
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ]
输出效果:
2020-01-22 10:11:43 [ main:0 ] - [ DEBUG ] This is debug message.
在代码中使用log4j:
在配置完成后, 就可以在代码中愉快的使用log4j 了
首先是创建日志获取器:
private static final Logger logger = LoggerFactory.getLogger(HelloMaven.class);
由于未被任何变量引用的日志记录器可能被当做垃圾直接回收了, 所以最好加上static 与 final
而后是使用配置信息文件:
这一步如果是用Maven的话, 只要在resource中添加了相应的配置文件log4j.properties
就可以自动完成
否则需要手动添加:
PropertyConfigurator.configure ( String configFilename)
其中文件名中的所有反斜杠\
都要变成俩除号//
, Java中的惯例
之后就可以在需要的位置插入Log信息:
通常只使用这4个等级:
Logger.debug ( Object message ) ;
Logger.info ( Object message ) ;
Logger.warn ( Object message ) ;
Logger.error ( Object message ) ;
Log的输出会调用类的toString() 函数输出信息, 所以之前的toString是相当有用的
Slf4j 使用教程:
实际上变更的不多, 大部分操作和log4j还是相同的
主要的差别有以下几个:
-
创建日志记录器
//注意此时import的是这俩 //由于IDEA的自动import功能, 如果原先有import log4j的包, 需要先删掉 import org.slf4j.Logger; import org.slf4j.LoggerFactory; //创建记录器: //这里的记录器都是Logger类 //但是使用的是记录器的工厂方法LoggerFactory, 后头的都相同 private static final Logger logger= LoggerFactory.getLogger(HelloMaven.class);
-
Log信息的输出:
就是这四个不同等级的输出会有一定的差别, 看API了解一下就OK
Logger.debug ( Object message ) ; Logger.info ( Object message ) ; Logger.warn ( Object message ) ; Logger.error ( Object message ) ;
这里强调一下Slf4j中比较重要的特性:
其在字符串编排上引入了类似C语言printf的占位符功能, 免去了大量的字符串拼接, 使得日志的性能有几乎两倍的提升
//slf4j log.debug("Found {} records matching filter: '{}'", records, filter); //log4j log.debug("Found " + records + " records matching filter: '" + filter + "'");
其中的占位符就是
{}
, 而后将按顺序将后头的String对象填入其中 (注意后头的还是String类)
本部分还需要补充实战期间的技巧整合…
<未完待续>
7.6 调试技巧:
开发过程中常用的DEBUG技巧 (这里不包括IDEA的调试技巧)
-
在需要的位置打印变量的值:
直接输出到控制台, 或是输出到日志
-
在每个类中单独放置一个main方法用于单元测试
IDEA可以非常方便的运行单独的main, 所以可以将必要的测试代码丢到里头
或者是使用单元测试框架, 一步到位才是真谛
-
利用日志代理:
实际上就是创建一个假的由于拦截调用的和输出日志信息的子类
可以在这个子类中输出调用信息和堆栈轨迹
-
通过printStackTrace的方法
捕获异常之后, 可以仅仅输出其堆栈轨迹, 而后重新将其抛出, 交个上层处理等
-
利用Java虚拟机的功能, 可以查看更多程序运行的信息
IDEA debugger 使用:
总的教程可以看这里, 写的还是蛮好的:
既然IDEA如此强大, 我还要啥自行车