12 异常

异常

概念

为什么需要异常机制?

可以集中处理处理程序中出现的错误。
使程序员把精力花在要解决的问题上。
成功的情况只有一种,错误的情况是无限的(未知的才是最可怕的)。

和约定返回一个错误码相比,有什么有优势?

  1. 可以用更优美的方式处理。
  2. 对于错误信息,我可以直接抛出,没有必要“先判断错误类型,然后返回该错误信息”(如C),因为我函数的返回类型有可能是有意义的,这样可以在外层统一处理所有被抛出的异常。
  3. 对于检查异常,会强迫程序员去处理,这样减缓了程序员的马虎不小心。
  4. 对于非检查异常,即便在运行时期发生了,未处理的程序员也可以通过异常信息很好的定位错误的位置。

基本异常

  1. 普通问题:在当前方法内可以处理异常。
  2. 异常情形:必须要抛出的情况。

标准异常的构造器:

  1. 默认构造器
  2. 接受字符串为参数的构造器

如何抛出异常?

使用throws+异常抛出
在方法后面加throws是声明这个方法可能会抛出
实际上抛出不抛出由这个方法的实现而定

抛出异常和方法返回的相似之处:

他们都会从当前方法跳出,仅此而已。至于跳出后的落点,完全不一样。(异常在异常处理程序中落脚)

捕获异常

当前方法抛出了异常如果不处理,那么需要在方法后当前方法加throws是声明这个方法继续抛出,当前方法结束并且跳出,那么总要在某个地方处理被抛出的异常,否则就会一直跳出,知道终止程序。

如果不希望终止程序,如何处理

像C一样在源代码的后面放上一堆判断错误类型的代码并处理吗?不需要
在try{}里编写的代码会捕获异常,然后结合异常处理程序catch{}来处理异常。

自定义异常

实现Exception即可。

如何打印异常信息?
Throwable中有printStackTrace()方法,打印从方法调用处到异常抛出处的方法调用序列
Exception实现了Throwable
默认printStackTrace()是将信息输出到System.err中
使用printStackTrace(System.out)可以把异常序列输出到控制台。

捕获所有异常

什么是轨迹栈?

轨迹栈可以理解为异常在被捕获打印出来的时候都经历了哪些方法,并且按照从里到外的顺序记录着(实际上就是方法栈的顺序)。

如果我在捕获之后重新抛出,轨迹先改变了吗?

我的理解是,这个异常对象没有改变重新被抛出,它记录的轨迹栈就没有变化,

轨迹线没有变化如何理解?

看案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}

public static void g() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}

public static void main(String[] args) {
try {
g();
} catch (Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}
/* Output:
originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
*///:~

可以看到,两个地方打印出来的异常轨迹线都是一样的。

为什么?

我的理解是:
我们知道有方法栈这么一个概念,
最开始main()入栈,
在main()中调用g(),g()入栈,
g()中调用f(),f()入栈
f()中抛出异常,该异常的轨迹栈就是当前的方法栈。

如何修改轨迹栈?

使用e.fillInStackTrace()方法,它的作用是重写把当前的方法栈写入到原异常的轨迹栈中。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}

public static void h() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
throw (Exception) e.fillInStackTrace();
}
}

public static void main(String[] args) {
try {
h();
} catch (Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}
/* Output:
originating the exception in f()
Inside h(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.h(Rethrowing.java:20)
at Rethrowing.main(Rethrowing.java:35)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.h(Rethrowing.java:24)
at Rethrowing.main(Rethrowing.java:35)
*///:~

使用e.fillInStackTrace()方法和抛出一个新的异常结果是一样的。

如何在打印一个异常的同时把它之前出现的异常信息也同时打印出来呢?

使用异常链
Error,Exception,RuntimeException可以使用构造器传入一个异常,构成异常链。
其它异常要使用initCause(e)来构成异常链。

标准JAVA异常

异常的分类:

Error:属于系统错误
Exception:属于编程错误

所有的Exception都需要手动抛出吗?

目前看来,所有的异常在被抛出后有两种解决方案:

  1. try-catch 捕获处理
  2. 继续抛出

如果是这样,每一个对象在使用前都要处理“为NULL的异常”,或者继续抛出异常,但在代码里总需要有处理的地方,这还是会十分麻烦。
因此RuntimeException类型的异常JAVA会帮我们自动判断并抛出。

JAVA帮我们抛出的异常如果没有处理会怎样?

会一直抵达main()方法并抛出
最后JAVA发现是这个类型的异常后会在退出程序前调用printStachTrace()方法。

也就是说运行时异常可以不用在代码中声明抛出和写处理程序,而检查异常这两个都是必有的。

finally

对于一些代码,无论try中的异常是否抛出,我们都希望在最后执行,怎么处理?

使用finally{}
对于没有垃圾回收机制的语言来说,这种情况一般是处理内存的回收。
对于JAVA而言,用来清理除了内存之外的资源,譬如文件,网络连接,开关等等。

异常的限制

重写方法时,只能抛出在该方法的基类版本中指定的异常。

为啥?
更好的支持多态。

构造器和异常

我们之前说过,在finally中的处理是执行完异常代码后,清理除内存之外的资源,但是有一个问题是:
这些资源会被正确的清理吗?

怎么理解这句话,这么一个情况,如果资源还没有被打开?需要被清理吗?不需要清理反而清理了,是不是就有问题了?

什么时候资源不会被打开呢?

在构造器方法里抛出了异常,这时候资源对象还未被创建。

如何处理最安全?

如果是单独捕获构造器抛出的异常并处理,也不是非常安全,因为有可能其它语句也会抛出这个异常,这时候就无法区分。
最好的解决办法是,使用嵌套try-finally

为什么要使用嵌套?

try和finally是绑定的,对于构造器的finally,我们希望区别开来:
在创建需要清理的对象之后,立即进入一个try-finally的语句。
这样如果创建对象失败了,后面的的语句也就不会执行了。
这也是使用嵌套而不写分开写成多个try-finally的原因。

无论构造器会不会抛出异常都可以这样使用:
基本规则是:在创建需要清理的对象之后,立即进入一个try-finally的语句。

检查异常

C如何处理错误?

判断错误的类型并使用代码处理
程序员往往忽略这些错误的处理,之后出现问题的时候再调试找出问题。

C++如何处理错误?

C++有异常声明,表明当前方法有异常被抛出,至于处不处理是程序员的事。
有兴趣的再去更深入的了解

被检查异常会有什么问题吗?

强迫程序员必须要某个地方解决这个异常,但有时候这个异常的产生是由底层的代码抛出的,没有足够的条件去处理,这时候为了使程序继续运行,往往假装解决了,这时候异常就消失不见了,潜在的危急十分恐怖。
“吞食即伤害”
成功的情况往往只有一种,错误的情况是无限的(未知的才是最可怕的)。

类型检查的弊端?

java是一种静态类型语言,意味着在编译时期会对类型做检查,这虽然减少了很多错误的出现,但也一定程序上限制了程序员编码的自由,因此后面设计的泛型和反射都是弥补这种缺失的自由的。

被检查异常无法时处理的时候怎么办?

  1. 在main()后抛出被检查的异常,异常信息会被打印到控制台中,作后续处理。
  2. 将被检查异常转为非检查异常,使用异常链记录之前的被检查的异常,在必要的时候使用getCause()重新抛出这些异常。

本文标题:12 异常

文章作者:Sun

发布时间:2018年10月25日 - 12:10

最后更新:2018年12月12日 - 16:12

原始链接:https://sunyi720.github.io/2018/10/25/THING IN JAVA/12 异常/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。