第12章 通过异常处理错误
java的基本理念是“结构不佳的代码不能运行"。
12.2 基本异常
异常情形:阻止当前方法或者作用域继续执行的问题。
抛出异常后的情形
- 使用new在堆上创建异常对象;
- 当前的执行路径被终止;
- 从当前环境中弹出对异常对象的引用。
异常参数:所有标准异常类都有两个构造器,一个默认构造器,一个接收字符串作为参数。
可以简单的把异常处理看成一种不同的返回机制,虽然返回类型不同;
Throwable对象是所有异常类型的根类。
12.3 捕获异常
监控区域 是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
try{
// 捕获异常
}catch (Exception e){
// 处理异常
}
异常处理两种基本模型
- 终止模型:发生错误,终止执行;
- 恢复模型:异常处理程序会修复错误,然后重新调用出问题的方法。
恢复模型的问题:会导致耦合,需要了解抛出的地点,会增加依赖于抛出位置的非通用性代码。
12.4 创建自定义异常
public class MyException extends Exception {
public MyException(){}
// 对于异常来说,最重要的就是类名,一般默认构造器就够用了
public MyException(String msg){
super(msg);
}
}
public static void main(String[] args) throws FileNotFoundException {
try {
throw new MyException();
} catch (MyException e) {
// 默认调用
e.printStackTrace();
}
try {
throw new MyException("说明信息");
} catch (MyException e) {
// 输出到指定流
PrintStream p = new PrintStream("E:\\a.txt");
e.printStackTrace(p);
}
}
/*
com.ding.five.MyException
at com.ding.five.Test.main(Test.java:17)
*/
a.txt
com.ding.five.MyException: 说明信息
at com.ding.five.Test.main(Test.java:24)
异常与记录日志
public class MyException extends Exception {
// 返回指定命名的日志对象,如果存在直接返回,否则创建后返回
private static Logger logger = Logger.getLogger("MyException");
public MyException(){
// 下面两步的操作是将错误信息输入到StringWriter中
StringWriter writer = new StringWriter();
printStackTrace(new PrintWriter(writer));
// 将错误信息打印
logger.info(writer.toString());
}
}
/*
九月 06, 2020 11:48:50 上午 com.ding.five.MyException <init>
信息: com.ding.five.MyException
at com.ding.five.Test.main(Test.java:18)
*/
记录异常日志工具类
public class MyException extends Exception {
}
public class Test {
public static void main(String[] args) {
try {
throw new MyException();
} catch (MyException e) {
deal(e);
}
}
public static void deal(Exception e){
Logger logger =Logger.getLogger("deal");
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
logger.info(writer.toString());
}
}
/*
九月 06, 2020 12:07:28 下午 com.ding.five.Test deal
信息: com.ding.five.MyException
at com.ding.five.Test.main(Test.java:16)
*/
进一步自定义异常
// 新增字段x,并重写了getMessage()方法
public class MyException extends Exception {
private int x;
public MyException(){}
public MyException(String msg){
super(msg);
}
public MyException(String msg,int x){
super(msg);
this.x = x;
}
@Override
public String getMessage() {
return "具体信息:" + x +" "+ super.getMessage();
}
}
public static void main(String[] args) {
try {
throw new MyException();
} catch (MyException e) {
e.printStackTrace();
}
try {
throw new MyException("错了");
} catch (MyException e) {
e.printStackTrace();
}
try {
throw new MyException("出错了",5);
} catch (MyException e) {
e.printStackTrace();
}
}
/*
com.ding.five.MyException: 具体信息:0 null
at com.ding.five.Test.main(Test.java:16)
com.ding.five.MyException: 具体信息:0 错了
at com.ding.five.Test.main(Test.java:22)
com.ding.five.MyException: 具体信息:5 出错了
at com.ding.five.Test.main(Test.java:28)
*/
12.5 异常说明
异常说明: 在形参后使用throws关键字列出所有可能抛出的异常;
受检异常: 在编译时被强制检查的异常;
特殊点: 可以声明方法将抛出异常,但实际上并不抛出;这种方式在定义基类和接口时非常重要,可以将来修改时,而不做改动。
12.6 捕获所有异常
// 通过捕获Exception可以捕获所有的异常,一般放在最后,防止抢了其他的异常处理程序的处理
public static void main(String[] args) {
try {
throw new Exception("My");
} catch (Exception e) {
// 获取详细信息(输入的字符串参数)
System.out.println("getMessage:"+e.getMessage());
// 获取用本地语言描述的详细信息
System.out.println("getLocalizedMessage:"+e.getLocalizedMessage());
// 获取类的简单描述和详细信息
System.out.println("e:"+e);
// 打印类的调用栈轨迹
System.out.println("printStackTrace:");
e.printStackTrace();
}
}
/*
getMessage:My
getLocalizedMessage:My
e:java.lang.Exception: My
printStackTrace:
java.lang.Exception: My
at com.ding.five.Test.main(Test.java:16)
*/
栈轨迹
public class Test {
public static void main(String[] args) {
b();
}
public static void a(){
try {
throw new Exception();
} catch (Exception e) {
// 返回由栈轨迹中的元素构成的数组
StackTraceElement[] stackTrace = e.getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.println(stackTraceElement.getMethodName());
}
}
}
public static void b(){
a();
}
}
/*
a
b
main
*/
重新抛出异常-fillInStackTrace()
public class Test {
public static void main(String[] args) throws InterruptedException {
try {
x();
} catch (Exception e) {
System.out.println("main调用x打印");
e.printStackTrace();
}
// 防止打印错乱
Thread.sleep(10);
try {
y();
} catch (Exception e) {
System.out.println("main调用y打印");
e.printStackTrace();
}
}
public static void a() throws Exception {
throw new Exception();
}
public static void x() throws Exception {
try {
a();
} catch (Exception e) {
System.out.println("x方法打印信息");
e.printStackTrace();
Thread.sleep(10);
// 调用x()方法追究路径到真正抛出异常为止
throw e;
}
}
public static void y() throws Exception {
try {
a();
} catch (Exception e) {
System.out.println("y方法打印信息");
e.printStackTrace();
Thread.sleep(10);
// 调用y()方法追究的路径到此为止
throw (Exception) e.fillInStackTrace();
}
}
}
/*
x方法打印信息
java.lang.Exception
at com.ding.five.Test.a(Test.java:27)
at com.ding.five.Test.x(Test.java:32)
at com.ding.five.Test.main(Test.java:12)
main调用x打印
java.lang.Exception
at com.ding.five.Test.a(Test.java:27)
at com.ding.five.Test.x(Test.java:32)
at com.ding.five.Test.main(Test.java:12)
y方法打印信息
java.lang.Exception
at com.ding.five.Test.a(Test.java:27)
at com.ding.five.Test.y(Test.java:43)
at com.ding.five.Test.main(Test.java:19)
main调用y打印
java.lang.Exception
at com.ding.five.Test.y(Test.java:48)
at com.ding.five.Test.main(Test.java:19)
*/
重新抛出异常-另外一种
public static void main(String[] args) throws InterruptedException {
try {
try {
throw new OneException();
} catch (OneException e) {
System.out.println("第一种异常");
e.printStackTrace();
Thread.sleep(10);
throw new TwoException();
}
} catch (TwoException e) {
System.out.println("第二种异常");
e.printStackTrace();
}
}
/*
第一种异常
com.ding.five.OneException
at com.ding.five.Test.main(Test.java:13)
第二种异常
com.ding.five.TwoException
at com.ding.five.Test.main(Test.java:18)
*/
异常链
希望在捕获一个异常后抛出另一个异常,并且把原始异常的信息保存下来,这就是异常链,在JDK1.4后,可以使用cause参数来实现这一功能。
这样通过把原始异常传递给新的异常,即使在当前位置创建并抛出了异常,也可以追踪到异常最初发生的位置。
Error、Exception、RuntimeException提供了接收cause的构造器,其他类型异常需要使用initCause()方法来实现这一功能。
注意: 书中为了演示这一知识,构造了一个复杂的案例,下面这个是自己构造的一个简单版
// 注意:这里抛出的最终异常是 TwoException,它保存了导致它的原始异常信息OneException,而后者在发生异常后才指定是由NullPointerException异常导致的。
public static void main(String[] args) throws TwoException {
try {
throw new OneException();
} catch (OneException e) {
e.initCause(new NullPointerException());
TwoException t = new TwoException();
t.initCause(e);
throw t;
}
}
/*
Exception in thread "main" com.ding.five.TwoException
at com.ding.five.Test.main(Test.java:16)
Caused by: com.ding.five.OneException
at com.ding.five.Test.main(Test.java:12)
Caused by: java.lang.NullPointerException
at com.ding.five.Test.main(Test.java:14)
*/
书上案例 对于该知识点而言太复杂了,但案例不错
// 动态字段类(为了简化,又Object改为String)
public class DynamicFields {
// 定义包含的字段
private String[][] fields;
// 初始化
public DynamicFields(int initSize){
fields = new String[initSize][2];
}
// 重写toString方法
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (String[] field : fields) {
sb.append(field[0]).append(":").append(field[1]).append("\r\n");
}
return sb.toString();
}
// 判断是否有某个字段,如果有,返回索引
private int hasKey(String key){
for (int i = 0; i < fields.length; i++) {
// 判断是否含有key
if (key.equals(fields[i][0])) {
return i;
}
}
return -1;
}
// 获得某个字段的索引
/*当存在hasKey方法时仍然创建该方法,目的是解耦,是否有某个字段是不应该产生异常的,
但是获得key的索引时是可能产生异常的,因为它可能不存在;同时还有另一层好处:封装,
如果没有该方法,直接调用hasKey()也能实现该功能,但这样的话,每个调用者都需要做判断
索引是否为-1,然后抛出异常
*/
private int getKeyNumber(String key) throws NoSuchFieldException{
int fieldNum = hasKey(key);
if (fieldNum ==-1) {
throw new NoSuchFieldException();
}
return fieldNum;
}
// 增加字段,并返回增加的字段索引
/*
注意:该方法并没有增加是否含有该字段的判断,是因为它是private的,
增加判断的代码在调用者处
*/
private int makeKey(String key){
for (int i = 0; i < fields.length; i++) {
if (fields[i][0] == null) {
fields[i][0] = key;
return i;
}
}
// 走到这说明数组已经不存在空值了
// 新建临时数组
String[][] temp = new String[fields.length+1][2];
// 将数组的值赋予临时数组
for (int i = 0; i < fields.length; i++) {
temp[i] = fields[i];
}
fields = temp;
// 循环调用
return makeKey(key);
}
// 获得某个字段的值
public String getValue(String key) throws NoSuchFieldException {
return fields[getKeyNumber(key)][1];
}
// 设置某个字段的值(不存在则新建),并返回旧值
public String setValue(String key,String value) throws DynamicFieldsException {
// 如果值为空,抛出动态字段的专属异常,通时执行了异常链,抛出空指针异常
if (value == null) {
DynamicFieldsException de = new DynamicFieldsException();
// 注意:这里演示了本节知识点
de.initCause(new NullPointerException());
throw de;
}
int filedNum = hasKey(key);
// 如果该字段不存在,则新建
if (filedNum == -1) {
filedNum = makeKey(key);
}
String result = null;
try {
result = getValue(key);
} catch (NoSuchFieldException e) {
// 注意:因为前边对key做了判断,不可能不存在key,所以该错误不会抛出
throw new RuntimeException(e);
}
fields[filedNum][1] = value;
return result;
}
}
public static void main(String[] args) {
DynamicFields dynamicFields = new DynamicFields(2);
System.out.println("初始化:"+ dynamicFields);
try {
// 设置值
dynamicFields.setValue("name","ding");
dynamicFields.setValue("age","17");
System.out.println("首次设置:"+dynamicFields);
String old = dynamicFields.setValue("name", "fu");
System.out.println("二次设置:"+dynamicFields);
System.out.println("old:"+old+" new:"+dynamicFields.getValue("name"));
// 抛出异常
dynamicFields.setValue("ag",null);
} catch (DynamicFieldsException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/*
初始化:null:null
null:null
首次设置:name:ding
age:17
二次设置:name:fu
age:17
old:ding new:fu
com.ding.five.DynamicFieldsException
at com.ding.five.DynamicFields.setValue(DynamicFields.java:87)
at com.ding.five.Test.main(Test.java:23)
Caused by: java.lang.NullPointerException
at com.ding.five.DynamicFields.setValue(DynamicFields.java:89)
... 1 more
*/
12.7 java标准异常
异常类非常繁多,对异常来说,关键是理解概念以及如何使用,异常的基本的概念是用名字代表发生的问题。
RuntimeException 运行时异常,它属于错误,会被自动捕获。如果没有被捕获将在程序退出前调用异常的printStackTrace()方法。
12.8 使用finally进行清理
try {
// 正常语句
} catch (Exception e) {
// 异常处理
} finally {
// 必定执行
}
public static void main(String[] args) {
int i = 0;
while (true){
try {
if (i++ == 0){
throw new Exception();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("总会执行");
if (i == 2) {
break;
}
}
}
}
finally的作用
释放需要清理的资源:已经打开的文件或网络资源,外部的某个开关等。
public static void main(String[] args) {
try {
Switch s = new Switch();
try {
s.on();
throw new Exception("内层错误");
}catch (Exception e){
e.printStackTrace();
}finally {
s.off();
System.out.println(s);
System.out.println("更高一层前执行");
}
throw new Exception("外层错误");
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("最终执行");
}
}
/*
java.lang.Exception: 内层错误
at com.ding.five.Test.main(Test.java:16)
false
更高一层前执行
java.lang.Exception: 外层错误
at com.ding.five.Test.main(Test.java:24)
最终执行
*/
在return中使用finally
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
f(i);
}
}
public static void f(int i){
try {
if (i == 0) {
System.out.println("0点返回");
return;
}else if (i == 1){
System.out.println("1点返回");
return;
}else {
System.out.println("其他点返回");
return;
}
} finally {
System.out.println("清理完毕。。。");
}
}
}
/*
0点返回
清理完毕。。。
1点返回
清理完毕。。。
其他点返回
清理完毕。。。
*/
缺憾:异常丢失
第一种丢失
public static void main(String[] args) {
try {
try {
throw new Exception("非常重要");
}finally {
throw new Exception("不重要");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/*
java.lang.Exception: 不重要
at com.ding.five.Test.main(Test.java:16)
*/
// 可以看到,第一个异常没有处理时再抛出第二个异常,前面的异常信息丢失了
第二种丢失-1
public static void main(String[] args) {
try {
throw new RuntimeException();
}finally {
return;
}
}
第二种丢失-2
public static void main(String[] args) {
try {
try {
throw new Exception();
}finally {
return;
}
}catch (Exception e){
e.printStackTrace();
}
}
上面两种方式不会返回任何信息。
12.9 异常的限制
- 当子类重写父类的方法时,只能抛出在基类中抛出的异常或者是这些异常的子类,或者不抛出异常;
- 子类的构造器抛出的异常必须包含父类的构造器异常说明;
- 异常说明并不属于方法类型的一部分。
- 当父类的方法和接口中的方法重名时,此时重写方法,异常信息,必须符合父类和接口的规则。
12.10 构造器
public class FileInput {
private BufferedReader in;
public FileInput(String name) throws Exception{
/*
注意此处的处理逻辑:没有找到文件时,文件不会被打开,所以,不需要关闭资源,
但是,如果是其他异常的话,此时,已经建立了连接,应该关闭资源
*/
try {
in = new BufferedReader(new FileReader(name));
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (Exception e){
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
throw e;
}finally {
// 清理动作不能在这里做
}
}
public String getLine(){
String s = null;
try {
s = in.readLine();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("读取失败");
}
return s;
}
public void dispose(){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("关闭连接失败");
}
}
}
public static void main(String[] args) {
/*
* 注意:
* 第一个try块,如果构造对象不成功的话,不存在调用清理方法,
* 当进入第二个try块时,此时对象已经构造成功,当发生异常时,
* 应当清理资源,所以该try接的finally会调用清理方法。
* */
try {
FileInput fileInput = new FileInput("e:/a.txt");
String s = null;
try {
s = fileInput.getLine();
} catch (Exception e) {
e.printStackTrace();
} finally {
fileInput.dispose();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("构造对象失败");
}
}
注意:
像上似方法,即使构造器没有抛出异常,也应该参考使用,如下:
public class A {
public void mainDeal(){}
public void dispose(){}
}
public static void main(String[] args) {
A a = new A();
try {
a.mainDeal();
}finally {
a.dispose();
}
}
12.11 异常匹配
抛出异常时,系统会按照书写顺序,查找匹配的处理程序,子类可以匹配基类的程序。当基类异常在前,子类异常再后,子类异常永远不会匹配,编译器会报错。
public static void main(String[] args) {
try {
throw new NoSuchFieldException();
} catch (NoSuchFieldException e) {
System.out.println("子匹配");
} catch (Exception e) {
System.out.println("基匹配");
}
try {
throw new NoSuchFieldException();
} catch (Exception e) {
System.out.println("基匹配");
}
}
/*
子匹配
基匹配
*/
12.12 其他可选方式
本章主要是一些关于异常的讨论,作者比较反对受检异常。作者认为应该统一异常的模型,而不是区分受检异常和运行时异常。
受检异常是抛出了可能产生的异常,并让程序员处理,但是其并不包含所有发生的异常,同时它会打断正常程序的思路,而且程序员可能并不知道应该如何在这里处理。
将受检异常转为运行时异常
public class Test {
public static void main(String[] args) {
try {
f();
}catch (RuntimeException e){
try {
// getCause()方法提取原来的异常
throw e.getCause();
} catch (NoSuchFieldException e1){
System.out.println("NoSuchFieldException");
}catch (FileNotFoundException e1){
System.out.println("FileNotFoundException");
}catch (RuntimeException e1){
System.out.println("RuntimeException");
}
catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
public static void f(){
try {
Random random = new Random();
int i = random.nextInt(3);
switch (i){
case 0:
throw new FileNotFoundException();
case 1:
throw new NoSuchFieldException();
case 2:
throw new RuntimeException();
}
}catch (Exception e){
// 将异常转为运行时异常
throw new RuntimeException(e);
}
}
}
12.13 异常使用指南
- 在恰当的级别处理问题(在知道该如何处理的情况下才捕获异常)。
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人)。
- 让类库和程序更安全(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资)。
12.14 总结
异常是java不可分割的一部分。异常处理的优点之一就是它可以时程序员在某处集中处理需要解决的问题,在另一处处理错误。
异常同时也被人认为是一种工具,使得程序可以在运行时报告错误,并恢复。但大多数情况下报告才是异常的精髓所在,很少使用到恢复功能。