Java的基本理念是“结构不佳的代码不能运行”。
发现程序中的错误的理想时机是编译阶段(即生成.class文件的阶段),但现实是很多的错误只有在运行阶段才能被发现。比如说,程序需要打开一个文件,你不运行程序去试图打开文件,怎么会知道文件是不存在,打不开还是有什么其它的错误。又比如说程序中一个方法的参数是对象引用,然后向该对象引用发送消息,你不运行程序怎么知道传递给参数的实参是是不是为null。因此就有了通过异常来处理错误的说法。
所以的错误和异常都是Throwable的子类,其中Error及其子类一般是系统层面或者JVM层面的错误,不需要程序员的过多关注。Exception及其子类表示程序中的异常,它们是Java异常处理系统的主要部分。其中又分为被检查的异常和运行时异常,运行时异常(RuntimeException)不需要程序手动地捕获(Catch)或者是抛出(Throws),在产生运行时异常后,运行的程序通常会终止然后该异常被JVM捕获,进而执行JVM所提供的异常处理程序(通常是打印调用栈轨迹)。
1. 捕获和处理异常
-
捕获和处理异常的一般形式
try{ // 可能产生异常的代码 } catch(第一种异常) { // 对于第一种异常的处理 // 注意:第一种异常不能是第二种异常的基类,因为加入第一种异常是第二种异常的基类的话, // 第二个Catch语句一定得不到执行(此时编译器会报错),因此如果要捕捉Exception异常,该 // catch一定放在最后一位 } catch(第二种异常){ // 对于第二种异常的处理 } finally{ // 无论是否产生异常,无论异常是否被捕获,这里的代码都会得到执行 }
-
自定义异常类的一般形式
// 自定义异常类 class MyException extends Exception{ public MyException() { } // message表示异常类的具体信息 public MyException(String message) { super(message); } }
-
生成,捕获和处理异常
public class FullConstructors { public static void f() throws MyException { System.out.println("Throwing MyException from f()"); // 抛出受检查的异常,此时要么捕捉(catch),要么声明该方法将抛出这个异常 throw new MyException(); } public static void g() throws MyException { System.out.println("Throwing MyException from g()"); throw new MyException("Originated in g()"); } public static void main(String[] args) { try { f(); } // 由于捕捉了异常,因此程序可以继续往下执行 catch (MyException e1) { // e1.printStackTrace()的作用为打印”从方法调用处到异常 // 抛出处的方法调用序列(方法调用栈) // 同时,这里将方法调用序列的信息发送到了标准输出流,所以你会看到 // 下面依次输出的情况,且在idea中输出内容都是白色的。默认是将方法调用 // 序列的信息发送到标准错误流,这时控制台会先依次输出标准输出流的内容,再 // 依次输出标准错误流的内容,且在idea中标准错误流(默认方法调用序列)是红色的 e1.printStackTrace(System.out); } try { g(); } catch (MyException e2) { e2.printStackTrace(System.out); } } } /* output Throwing MyException from f() exceptions.MyException(由基类Exception中默认的getMessage()方法得到) at exceptions.FullConstructors.f(FullConstructors.java:21) at exceptions.FullConstructors.main(FullConstructors.java:30) Throwing MyException from g() exceptions.MyException: Originated in g() at exceptions.FullConstructors.g(FullConstructors.java:25) at exceptions.FullConstructors.main(FullConstructors.java:36) *///
-
捕获异常搭配日志工具的使用
public class LoggingExceptions { // "LoggingExceptions"可以当做这个logger的名字 private static Logger logger = Logger.getLogger("LoggingExceptions"); static void logException(Exception e) { StringWriter trace = new StringWriter(); // 将方法调用序列重定向到trace中 e.printStackTrace(new PrintWriter(trace)); // 将trace中的方法调用序列信息用严重级别(sever)的日志消息打印 logger.severe(trace.toString()); } public static void main(String[] args) { try { throw new NullPointerException(); } catch (NullPointerException e) { // 将异常e的产生方法调用序列用日志打印出来 logException(e); } } } /* output 二月 27, 2022 11:07:19 下午 exceptions.LoggingExceptions logException 严重: java.lang.NullPointerException at exceptions.LoggingExceptions.main(LoggingExceptions.java:22) *///
用日志打印的好处是会显示具体的时间,在哪个包下,日志的名称,日志的级别这些具体信息,有利于程序员的进一步调试。
-
接收异常后重新抛出异常,重抛异常通常会交给上一级异常处理程序处理,如果想要更新重新抛出异常的信息(将新抛出点作为异常的发生地点)可通过fillInStackTrace()方法
-
在异常捕获的过程中,finally字句是一定会执行的,那么通常使用finally字句是用来做什么的呢?有时候我们需要释放在try语句中出内存之外资源之外的其它的东西,如文件流,网络连接等,这些东西无论是否捕获异常或是在哪个catch语句中捕获都需要一定得到释放。因此就到了finally字句的用武之处,在新版本的JDK中是加入了try-with-resource语句的,它可以自动帮我们关闭文件流,资源等(实际上也是通过finally语句实现的,只不过编译器帮我们完成了,俗称“语法糖”。
2. 异常的限制
当覆盖方法时,被覆盖的方法只能抛出在基类方法中异常声明(throws)中的那些异常或其子类,这个限制很有用也很实际。当我们使用多态性质时,即基类对象引用->子类对象,向基类对象引用发送消息调用方法,实际上执行的子类中已覆盖的方法(因此子类方法抛出的异常应该被基类对象引用的方法抛出)。
基类的方法声明将抛出异常,但它实际上没有抛出异常,这个时候就强制用户去捕获它的覆盖类中可能抛出的该异常(说白了还是因为要满足多态的性质)。
异常限制对构造器不起作用,子类的构造器可以抛出任何异常,而不必只满足于基类所抛出的异常,但是子类构造器必须抛出基类构造器所抛出的异常(不能捕获基类构造器的异常,因为构造器中的super()语句必须是构造器中的第一句非注释语句)。这也是很显然易见的,我们不能通过基类引用调用子类构造器,因此不必满足于基类构造器的异常说明,但同时子类的构造器必须调用父类的构造器无论是显示的调用还是隐式调用,抛出的异常都需要处理。
注意:尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是有方法的名字和参数的类型的组成的。因此,不能够根据异常说明来重载方法。此外,一个在基类方法中出现的异常说明,不一定会出现在子类该方法的异常说明中。
总结:日常使用Java开发时,我们大多时候是调用某个API,那个API会抛出异常,然后我们catch它,顺便打印一下方法调用序列。或者程序中有某个运行时bug,如数组越界,爆栈,空引用的方法调用等,程序会自动停止,JVM执行异常处理程序(打印方法调用序列)。这个性质可谓是我们做OJ的神器,再也不用担心Runtime Error了。只要到我们成为类库设计者时,我们的代码要被千千万万的程序员使用时,我们就要考虑某个方法在什么时候抛出什么异常。