Java笔记_10
异常
- 所有的异常都是由
Throwable
继承而来, 下一层有两个分支,Error
和Exception
Error
类描述了Java
运行时系统的内部错误和资源耗尽问题, 一般不要抛出这个类型Exception
层次分为两个分支,RuntimeException
和其他异常- 编程错误导致的是
RuntimeException
- 由于
IO
错误导致的是其他异常 - 继承自
RuntimeException
异常包括: 错误的强制类型转换; 数组越界; 访问null
指针 - 不继承自
RuntimeException
异常包括: 打开不存在的文件; 越过文件末尾读取数据; 根据给定字符串查找Class
, 但是这个类并不存在
- 编程错误导致的是
- 所有派生于
Error
和RuntimeException
的异常是非检查型异常, 其他异常都是检查型异常
抛出异常的情况
- 调用了某个会抛出异常的方法, 比如
FileInputStream
构造器 - 检测到一个错误, 使用
throw
语句抛出异常 - 程序出现错误, 比如
a[-1]
抛出一个非检查型异常 JVM
内部错误
- 一个方法必须声明所有可能抛出的检查型异常, 也可以捕获一个异常, 这样也不需要抛出了
- 如果子类覆盖了超类的一个方法, 子类抛出的异常不能比超类的更通用, 如果超类没有抛出异常, 则子类也不能抛出异常
创建异常
- 定义一个派生于
Exception
或者他子类的一个类, 包含两个构造器, 一个是无参构造器, 另一个是包含详细信息的构造器1
2
3
4
5
6class FileFormatException extends IOException {
public FileFormatException(){};
public FileFormatException(String gripe) {
super(gripe);
}
}
捕获异常
- 如果
try
语句块中任何代码抛出了catch
指定的一个异常类- 跳过
try
语句块剩余执行内容 - 执行
catch
语句块代码
- 跳过
- 如果没有抛出异常, 则直接跳过
catch
部分 - 如果抛出了异常, 但是不在
catch
中, 则方法会直接退出 - 一般是捕获知道如何处理的异常, 抛出不知道如何处理的异常
- 一个
try
语句块可能抛出多种不同的异常, 每个异常需要一个catch
语句块- 如果两个异常的捕获动作一样的话, 可以使用
catch(Exception e1 | Exception e2)
合并 - 捕获多个变量时, 异常变量银行了
final
- 如果两个异常的捕获动作一样的话, 可以使用
再次抛出
- 有时候只想记录一个异常, 再次抛出
1
2
3
4
5
6
7
8
9
10try {
...
} catch (Exception e) {
logger.log(level, message, e);
throw e;
}
// 如果这段代码存在于 public void update() throws SQLException中
// 在Java 7之前会报错, 因为 throw e可能抛出其他类型的异常, 而不是SQLException
// 现在改变了, 编译器会跟踪到e来自try代码块, try代码块中仅有的检查型异常是SQLException实例, 并且
// e在catch块中没有改变, 那么外围方法声明为throws SQLException就是合法的
finally
子句
-
代码抛出异常, 剩下的代码就不会运行, 如果这时候已经获取到了一些资源, 在退出之前需要释放.
- 可以先捕获所有异常, 然后释放资源, 再重新抛出异常
- 也可以使用
finally
子句, 无论是否抛出异常,finally
子句部分一定都会执行 Java 7
之后可以使用try-with-resources
, 这个更常用
-
try
语句可以只有finally
, 没有catch
-
finally
语句中不要放throw, continue, break, return
这种改变程序执行顺序的语句
try-with-resources
AutoCloseable
接口有一个方法void close() throwa Exception
Closeable
接口是AutoCloseable
接口的子接口, 同样只包含close()
, 但是抛出的是IOException
try-with-resources
语句的最简单形式是:1
2
3
4
5
6
7
8
9
10
11try (Resources res = ...) {
work with res
}
// 这里try块退出时, 会自动调用res.close()
try (var in = new Scanner(Path.of("1.txt"), StandardCharsets.UTF_8);
var out = new PrintWriter("2.txt", StandardCharsets.UTF_8)) {
while (in.hasNext()) {
out.println(in.next().toUpperCase());
}
}
// 上述代码不论是如何退出的, 都一定会自动关闭in和out- 如果是
try-catch-finally
语句, 在try
中抛出了异常, 然后finally
调用in.close()
又抛出了异常就会产生问题- 此时使用
try-with-resources
就可以解决这个问题 - 原来的异常会重新抛出,
close()
产生的异常会被抑制, 自动捕获, 由addSuppressed()
添加到原来的异常方法中 - 可以调用
getSuppressed()
, 会生成一个数组, 包含其中从close()
方法中抛出的被抑制的异常
- 此时使用
try-with-resources
语句本身可以有catch, finally
语句, 这些子句会在关闭资源以后才执行
栈轨迹
- 栈轨迹是程序执行中某个特定点所有挂起的方法调用的一个列表
Throwable
类的printStackTrace()
可以打印1
2
3
4
5
6
7var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String des = out.toString();
// Java 9之前, Throwable.printStackTrace()会生成一个StackTraceElement[]
// 数组中包含了和StackWalker.StackFrame类似的信息, 效率较低
// 因为会得到整个栈, 但是调用者只需要几个栈帧, 并且只允许访问挂起方法的类名, 不能访问类对象- 还可以使用
StackWalker
类, 生成一个StackWalker.StackFrame
实例流, 其中每个实例表示一个栈帧1
2StackWalker walker = StackWalker.getInstance();
walker.forEach(frame -> ananlyze frame)
- 栈轨迹一般显示在
System.err
上, 如果想要记录栈轨迹, 可以捕获到字符串中 - 也可以记录到文件中, 不过如果是错误的话, 就会发送到
System.err
中, 所以就不能使用下面的代码java MyApp > errors.txt
, 而是应该使用java MyApp 2> errors.txt
- 如果需要在同一个文件中同时保存
System.out, System.err
可以使用如下代码 java MyApp 1> errors.txt 2>&1
- 可以使用静态方法
Thread.setDefaultUncaughtExceptionHandler
改变没有捕获异常的处理器 - 启动
JVM
可以使用-verbose
看到类加载器加载过程 Xlint
选项可以告诉编译器找出常见的代码问题, 比如javac -Xlint sourceFiles
JDK
提供了jconsole
可以显示JVM
性能统计结果
使用异常技巧
- 异常不能代替简单测试, 使用捕获异常会导致程序耗时大大增加, 因此只在异常情况下使用异常
- 不要过分细化异常, 否则一个异常一个
catch
会导致代码量激增 - 合理使用异常层次
- 不要只抛出
RuntimeException
, 应该需要寻找一个合适的子类, 或者创建自己的异常类 - 不要只捕获
Throwable
异常, 否则代码会很难读懂 - 考虑检查型异常和非检查型异常, 检查型异常本质上开销较大
- 不要只抛出
- 不要压制异常, 可以使用
- 使用标准方法报告
null
指针和越界异常 - 不要向用户展示最终的栈轨迹
断言
- 断言允许在测试期间在代码中插入一些检查, 在生产代码中自动删除这些
1
2
3
4assert condition;
assert condition : expression;
// 两个写法都会计算condition, 如果为false, 会抛出AssertionError异常
// 第二个语句会将expression传入到AssertionError构造器中, 转换为一个消息字符串 - 默认情况下禁用断言, 运行是使用
java -enableassertions MyApp
或者java -ea MyApp
启用 - 不需要重新编译启用断言, 因为断言是类加载器的功能, 禁用断言的时候类加载器会自动删去断言的代码, 不会降低速度
- 可以在特定的类或整个包中打开断言
java -ea:MyClass -ea:com.mycompany.mylib MyApp
- 这样会在
MyClass
类,com.mycompany.mylib
包及其子包中的所有类打开断言 - 同样可以使用
-da
或者-disableassertions
禁用断言 -ea
和-da
不能应用于没有类加载器的系统类, 需要使用-esa
或者enablesystemassertions
开启系统类断言
- 这样会在
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sangs Blog!