首先看一下Java中的异常体系:
Error:一般是指与虚拟机相关的问题,如 OOM,ThreadDeath。
RuntimeExprion:NullPointerException,ClassCastException ,IllegalArgumentException ,ArrayStoreException ,IndexOutOfBoundsException ,NumberFormatException,SecurityException......。
非RuntimeExprion:ClassNotFoundException、CloneNotSupportedException、FileNotFoundException、InterruptedException、IOException、SQLException、TimeoutException、UnknownHostException......。
}catch(Exception e){
System.out.println("e was thrown");
throw e;
}
重抛异常会把异常抛给上一级的异常处理程序,同一个 try 块中的 catch 子句将被忽略。异常的所有信息都得意保存。
重抛当前异常,printStackTrace() 打印出的是原来异常抛出点的调用栈信息,而并非重抛点的异常调用栈信息。如果想要更新为重抛点的信息,可以调用 fillInStackTrace() 把当前异常调用栈信息填入原来的异常调用栈信息,注意 fillInStackTrace() 返回Throwable类型,并且是个耗时的操作,如果不关心异常的堆栈信息,可以通过重写fillInStackTrace()方法来实现。代码如下:
package pers.ltx.deep.ExceptionTest.ExceptionChain;
public class Demo1 {
public static void main(String[] args) {
try {
new Demo1().g();
}catch (Exception e){
e.printStackTrace();
}
}
public void f() throws Exception1 {
throw new Exception1();
}
public void g() throws Exception {
try {
f();
}catch (Exception1 e1){
throw e1;
//throw (Exception1)e1.fillInStackTrace();
}
}
}
class Exception1 extends Exception{
/*
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
*/
}
class Exception2 extends Exception{
}
输出:
加上 fillInStackTrace() 则变为:
public void g() throws Exception {
try {
f();
}catch (Exception1 e1){
//throw e1;
throw (Exception1)e1.fillInStackTrace();
}
}
输出:
异常链:在捕获一个异常后需要重新抛出一个新的异常,同时保留原始的异常信息,这称为异常链。
注意:Throwable子类中,只有三种基本的异常类提供了带 cause 参数的构造器 Error、Exception、RuntimeException,其余情况只能靠initCause()方法。源码如下:
如果直接在 g() 中抛出一个新的异常 Exception2,会造成之前异常信息的丢失,只会输出新抛出的异常的调用栈信息:
package pers.ltx.deep.ExceptionTest.ExceptionChain;
public class Demo1 {
public static void main(String[] args) {
try {
new Demo1().g();
}catch (Exception e){
e.printStackTrace();
}
}
public void f() throws Exception1 {
throw new Exception1();
}
public void g() throws Exception {
try {
f();
}catch (Exception1 e1){
throw new Exception2();
//throw (Exception2)new Exception2().initCause(e1);
}
}
}
class Exception1 extends Exception{
}
class Exception2 extends Exception{
}
输出:
如果想打印出整条异常链则可以这样操作:
public void g() throws Exception {
try {
f();
}catch (Exception1 e1){
throw (Exception2)new Exception2().initCause(e1);//利用 initCause()方法
//throw (Exception2)new Exception(e1);//或者利用构造器
}
}
输出:
我们不应该把异常直接暴露给用户,对用户可以对异常进行封装然后弹出。对异常封装后再抛出,再通过异常链传递,可以使系统更健壮。
finally 子句总能被执行。如果 try 块中有 return ,则会先把返回值保存在临时变量中,接着执行 finally ,finally 中代码怎样执行都不会影响到返回值。但是如果 finally 中也有 return ,则会忽略 try catch 块中的return ,并且忽略其中的异常直接返回,也就是说会吞食掉异常。
比如下面代码会返回 2 :
public int test(){
try {
throw new Exception();
}catch (Exception e){
return 1;
}finally {
return 2;
}
}
比如下面代码实际上并不会抛出异常,并且返回 2 :
public int test() throws Exception{
try {
throw new Exception();
}finally {
return 2;
}
}
finally 可以用于关闭资源:
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(""));
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (in!=null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
不过这代码写起来也酸爽了吧,没关系此时可以用 try-with-resource 语法糖。
try(InputStream in = new BufferedInputStream(new FileInputStream(""))) {
//.....
}catch (IOException e){
e.printStackTrace();
}
反编译看一下:
可以发现,编译器自动帮我们生成了finally块,并且在里面调用了资源的close方法。
使用 try-with-resource,必须实现 AutoClosable
接口,并且重写 close
方法。比如这样:
class Connection implements AutoCloseable{
public void sendData(){
System.out.println("Connection");
}
@Override
public void close() throws Exception {
System.out.println("close");
}
}
关于 try-with-resource,这里不再详细解释了,有兴趣的可以自己学习一下。
Java 的异常也有瑕疵,对于程序中的异常,绝不应该去忽视,但在 Java 中还是有可能被轻易的忽视掉。比如上文 finally 中的异常吞食,再如下面代码:
public class ExceptionMissed {
void fun1() throws Exception1 {
throw new Exception1("fun1");
}
void fun2() throws Exception2 {
throw new Exception2("fun2");
}
public static void main(String[] args) {
try {
ExceptionMissed em = new ExceptionMissed();
try {
em.fun1();
}finally {
em.fun2();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
输出:
可以发现 Exception1 不见了。
1.派生类构造器必须包含基类构造器中的异常说明,派生类构造器抛出的异常可以是基类构造器所抛出异常的父类。异常限制对构造器不起作用。
2.Constructor call must be the first statement in a constructor。所以,构造器中针对 super() 的异常只能异常声明,而不能捕获。
3.派生类重写的基类方法,抛出的异常数量、种类可以比父类少,但异常的范围必须比基类方法的异常声明范围小。
异常说明不属于方法类型的一部分,不能根据异常说明进行重载。此外,一个出现在基类中的异常不一定出现在派生类中,即派生类中非构造方法的异常声明一定不大于基类中非构造方法的异常声明。这点与继承规则完全不同。在继承中,派生类的方法可以比基类的方法多。
如果在构造器内发生了异常,清理行为也许不能正常工作,所以在编写构造器时需要格外小心。也许你认为加一个 finally 就可以解决问题,但是如果对象没有被成功的创建,这时是不需要进行清理的,然而用 finally 无论怎样都会执行清理过程。
InputStream in = null;
public InputFile(){
try {
in = new BufferedInputStream(new FileInputStream(""));
}catch (FileNotFoundException e){
//发生 FileNotFoundException 异常时,
// in 并没有完成创建,所以不需要进行 close。
}catch (Exception e){
//进入此语句,说明 in 已经完成创建,
//此时需要 close
try {
in.close();
} catch (IOException ex) {
System.out.println("in.close() successful");
}
}finally {
//此处不需要进行 close
}
}
基本规则:在创建失败时不需要进行清理,在创建成功后立即进入 try 语块保证能正确进行清理。
Java 无谓地发明了“检查异常”,这还只是一次尝试,目前为止并没有别的语言采用这种方式。
“检查异常”的好处很明显,要求针对该异常必须捕获或者在当前方法上进行异常声明,起到了规范效果。
try {
throw new Exception();
}catch (Exception e){
throw (RuntimeException)new RuntimeException().initCause(e);
}
一些异常常见误区,可以点击这里。
1. 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常)。
2. 解决问题,并重新产生异常。
3. 进行少许修补,然后绕过异常发生的地方继续执行。
4. 用别的数据进行计算,代替方法预计会返回的值。
5. 把当前运行环境下的能做的事情尽量做完,然后把相同的异常重抛到更高层。
6. 把当前运行环境下的能做的事情尽量做完,然后把不同的异常抛到更高层。
7. 终止程序
8. 简化异常。
9. 让类库和程序更安全。