为什么需要异常机制:

Java的基本理念是“结构不佳的代码不能运行” --- Java编程思想

最理想的是在编译时期就发现错误,但一些错误要在运行时才会暴露出来。对于这些错误我们当然不能置之不理。对于错误而言的两个关键是发现和处理错误。Java提供了统一的异常机制来发现和处理错误。

不考虑异常存在来看一下这个场景:

public void showObject(Object obj) {
    if (obj == null) {
        System.out.println("error obj is null");
    } else {
        System.out.println(obj.toString());
    }
}

对于showObject来说obj为null是一个错误,要在输出之前做错误判断,发生错误的话把错误打印出来作为错误报告和处理。这里把错误的发现、报告处理和正常业务逻辑放在了一起。但一些错误往往比这复杂且不只一个,如果我们为每一个错误都去定义一个独特错误报告的形式且都将错误处理代码和正常业务代码紧紧的耦合在一起那我们代码会变得难以维护。

合理的使用异常不仅能使我们的代码更加健壮,还能简化开发提升开发效率。

一、抛出异常(发现错误):

1、异常也是对象:

java使用使用异常机制来报告错误。异常也是普通的类类型。Java自身已经定义好了使用java时可能会产生的异常,使用java时java会自动去检查异常的发生,当异常发生时,java会自动创建异常对象的实例并将其抛出。我们经常看到的NullPointerException便是java已经定义好的异常。

除了java自身定义的异常外我们可以自定义异常,但自定义的异常需要我们自己去检查异常情形的发生,并自己创建异常对象和抛出。当然也可以创建java自定义的异常并抛出,抛出异常使用throw关键字:

throw new Exception();

我们使用的第三方库大多封装了自己的异常,并在异常情形发生时将自定义异常通过throw抛出。所有的异常类型都继承自Throwable类,所有Throwable类型的对象都可被抛出。

2、抛出异常:

异常发生,系统自动创建异常实例并抛出,或我们自己创建异常实例抛出异常时,代码的正常执行流程将会被终止,转而去执行异常处理代码。

二、异常捕获(处理错误):

1、监控区域:

当异常抛出时,自然抛出的异常应该得到处理,这就需要将抛出的捕获异常。但一个异常类型可能在很多地方被抛出,那么怎么去对特定的地方编写特定的异常处理程序那?java采用一个最方便和合理的方式,即对可能产生异常的代码区域进行监控,并在该区域后添加处理程序。

监控的代码区域放在try{}中,而异常处理代码紧跟在try后的catch中:

try {
    /***/
} catch (ExceptionType e) {
    /*
    ***
    */
}

catch类似方法的申明括号中为异常的类型和该类类型的实例。表明当前catch块处理的是什么类型的异常,而e便是该异常抛出的实例对象。

当try内的代码抛出异常时,就会停止当前流程,去匹配第一个catch中申明的异常类型与抛出类型相同的catch,如果匹配到则执行其内代码。一个try中可能会抛出多种类型的异常,可以用多个catch去匹配。

2、捕获所有异常:

注意catch中声明的异常如果为当前抛出异常的父类型也可以匹配。所以一般将基类的异常类型放在后面。

因为所有可以进行捕获的异常都继承自Exception,所有可以catch中申明Exception类型的异常来捕获所有异常,但最后将其放在最后防止将其他异常拦截了。

三、重新抛出异常

一、异常的两种类型

先看一下异常的类层次结构图:
image

我们可以将异常分为检查和非检查两种类型:

  • 检查异常:该类异常包含Exception及其子类异常,这些类型的异常的抛出必须有相应的catch进行捕获,否则无法通过编译。
  • 非检查异常:该类型的异常包含RuntimeException、Error和两者的子类型,这类异常可以没有对应的try-catch进行捕获也可通过编译。当异常发生时没有相应的捕获则异常会自动向上一级抛出,如此如果一直到main方法中还未被捕获则会调用该异常的printStacjTrace方法输出异常信息,并终止main的运行。其中Error为Java自身错误,这类错误发生时我们并不能在业务代码中去解决,如内存不足,所以这类异常不需要去捕获。
1、异常声明:

catch中的语句执行完成后会继续执行try-catch后的其他语句。所以当try-catch后还有语句时,一定要保证但异常发生时在catch中已经对异常进行了正确处理,后面的代码可以得到正常的运行,如果不能保证则应该终止代码向后的执行或再次抛出异常。

一些异常在当前的方法中不需要或无法进行处理时,可以将其抛出到上一层。要在方法中将异常抛出需要在方法中对要抛出的异常进行声明,这样方法的调用者才能知道哪些异常可能会抛出,从而在调用方法时添加异常处理代码。

非检查异常抛出到上一级时可以不用进行声明,合理的使用非检查异常可以简化代码。

(1) 异常声明

在方法声明的参数列表之后使用throws进行异常声明,多个异常类型使用逗号隔开:

void t () thrwos ExcptionTyep1, ExceptionType2 {
    
}

在方法中声明了的异常在方法中可以不进行捕获,直接被抛出到上一级。异常声明父类异常类型可以匹配子类异常类型,这样当有多个子类异常抛出时,只用声明一个父类异常即可,子类异常将被自动转换为父类型。

四、创建自定义异常:

要创建自己的异常必须得继承自其它的异常,一般继承Exception创建检测异常,继承RumtimeException创建非检查异常。

一般情况下异常提供了默认构造器和一个接受String参数的构造器。对于一般自定义的异常来说,只需要实现这两个构造方法就足够了,因为定义异常来说最有意义的是异常的类型,即异常类的名字,但当异常发生时只需看到这个异常的类型就知道发生了什么,而其他一些操作在Throwable中已经有定义。所以除非有一些特殊操作,不然在自定义异常时只需只需简单的实现构造方法即可。

五、异常信息:

所以异常的根类Throwable定义了我们需要的大多数方法:

// 获取创建异常时传入的字符串
String getMessage()
// 使用System.err输出异常发生的调用栈轨迹
void printStackTrace()
// 使用传入的PrintStream打印异常调用栈
void printStackTrace(PrintStream s)
// 使用PrintStreamOrWriter打印异常调用栈
void printStackTrace(PrintStreamOrWriter s)

获取调用栈实例

StackTraceElement[] getStackTrace()

该方法放回StackTraceElement数组,StackTraceElement为调用方法栈的实例,改类型有以下常用方法:

// 返回栈代码所在的文件名
String getFileName()
// 返回异常抛出地的行号
int getLineNumber()
// 返回栈的类名
String getClassName()
// 放回栈的方法名
String getMethodName()

六、异常链:

1、重新获取异常

当我们捕获到一个异常时可能想将他在次抛出,但这样直接抛出的话异常的栈信息是该异常原来的栈信息,不会是最新的再次抛出的异常的栈信息。如下:

class SimpleException extends Exception {
    public SimpleException() {
        
    }
    public SimpleException(String msg) {
        super(msg);
    }
}

public class Test {

    public void s() throws SimpleException {
        throw new SimpleException();
    }
    
    public void s2() throws SimpleException {
        try {
            s();
        } catch(SimpleException e) {
            throw e;
        }
    }
    
    public static void main(String[] args) {
        Test t = new Test();
        try {
            t.s2();
        } catch (SimpleException e) {
            e.printStackTrace();
        } 
    }
}

上面代码输出为:

com.ly.test.javatest.exceptiontest.SimpleException
    at com.ly.test.javatest.exceptiontest.Test.s(Test.java:19)
    at com.ly.test.javatest.exceptiontest.Test.s2(Test.java:24)
    at com.ly.test.javatest.exceptiontest.Test.main(Test.java:33)

可以看到异常抛出最终地为 com.ly.test.javatest.exceptiontest.Test.s(Test.java:19),但如果我们想让异常抛出地变为s2那?毕竟我们在这里自己抛出了异常。
Thrwoable类的fillInStackTrac创建一个新的Throwable对象,并将当前栈信息做新创建的Throwable异常的异常栈信息,然后返回。

2、异常链:

上面的做法又有另外一个问题,如果我们使用fillInStackTrace获得新的异常,那原来的异常信息也就丢失了,如果我们想抛出新的异常当又得包含原来的异常那?

Error、Exception和RuntimeException都含有一个接受Throwable对象的构造方法,在创建新的异常时时传入原来异常,即可保存原来异常。需要时使用getCause来获取到。除了使用构造方法传入异常,还可使用initCase方法传入异常。这其中的潜台词是“改异常是由什么异常造成的”。如下:

public class Test {

    public void s() throws Exception {
        throw new Exception();
    }
    
    public void s2() throws Exception {
        try {
            s();
        } catch(Exception e) {
            Exception ne = (Exception)e.fillInStackTrace();
            ne.initCause(e);
            throw ne;
        }
    }
    
    public static void main(String[] args) {
        Test t = new Test();
        try {
            t.s2();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}

六、总是执行的finally

看一下面的代码:

public class Test {
    public static void s() throws IOException {
        throw new IOException();
    }
    public static void main(String[] args) {
        String fileName = "C:\\temp\\test.txt";
        File file = new File(fileName);
        InputStream in = null;
        
        try {
            in = new FileInputStream(file);
            s();
            int tempbyte = in.read();
            in.close();
        } catch (IOException e) {
            if (in != null) {
                System.out.println("in");
            }
            e.printStackTrace();
        }
    }
}

可以看到要对in进行close但正常的流程中发生了异常,导致正常流程中的in.close无法执行,便跳到cattch中去执行,上面于是又在catch中写了一个关闭。着只是一个简单清理操作,但如果需要执行的清理操作不止一行而是非常多那?也是在正常流程和catch中写两遍吗,这样是非常不友好的,所以java提供了finally,如下

public class Test {
    public static void s() throws IOException {
        throw new IOException();
    }
    public static void main(String[] args) {
        String fileName = "C:\\temp\\test.txt";
        File file = new File(fileName);
        InputStream in = null;
        
        try {
            in = new FileInputStream(file);
            s();
            int tempbyte = in.read();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                System.out.println("in");
            }
        }
    }
}

finally中的代码无论异常是否发生都会被执行,即使try中包含return语句,也会在放回之前执行finally语句。

对于清理操作和一些异常发生也必得到执行的代码都应该放到finally中。

清理未创建的资源:

上面介绍使用finally来释放资源,但看下面这个情形:

public void test() {
    try {
        in = new BufferedReader(new FileReader());
        String s = in.readLine();
    } catch (FileNotFoundException e) {
        
    } catch (Exception e) {
        try {
            in.close();
        } catch (IOException e2) {
            System.out.println("in class false");
        }
    } finally {
        //in.close();
    }
}

这个例子可以看到如果new FileReader抛出了FileNotFoundException,那么in是不会被创建的,如果此时还在finally中执行in.close()那么自然是行不同的。但如果抛出了IOExceptin异常,那么说明in成功创建但在readLine时发生错,所以在catch中进行close时in肯定已经被创建。这种情形资源的释放应该放到catch中。

七、异常丢失:
1、在fianlly中return
public class Test {
    
    public static void main(String[] args) {
        try {
            int i = throwException();
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static int throwException () throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            throw e;
        } finally {
            return 1;
        }
    }
}

上面代码输出:1

2、在finally中抛出异常
public class Test {
    
    public static void main(String[] args) {
        try {
            int i = throwException();
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static int throwException () throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            throw e;
        } finally {
            throw new NullPointerException();
        }
    }
}

上面代码输出为:

java.lang.NullPointerException
    at com.ly.test.javatest.Test.throwException(Test.java:20)
    at com.ly.test.javatest.Test.main(Test.java:7)

可以看到main中捕获到的是NullPointerException,首先抛出的Exception异常丢失了。

在开发中非特殊情形应避免以上两种情况的出现。

八、异常限制:

父类构造器中声明的异常在基类的构造器中必须也声明,因为父类的构造器总是会显示会隐式(默认构造器)的被调用,而在子类构造器中是无法捕获父类异常的。但子类可以添加父类中没有声明的异常。

重载方法时子类只可抛出父类中声明的异常,因为我们会将子类对象去替换基类,这时如果重载的方法添加类新的异常声明,那么原来的异常处理代码将无法再正常工作。但子类方法可以减少或不抛出父类方法声明的异常。


三江小渡
50 声望8 粉丝