一、什么是异常,异常能干什么?
Java当中的异常是为了提高Java的安全性所提出的一种机制,在类库中已经有一些已经被定义好的异常模型,能够在程序编译或者运行过程中出现问题时给出相应的异常,并且在该异常没有经过程序自定义方案处理时,该异常由JVM接收并且终止程序的运行,防止因为程序出错造成更加严重的后果。
二、Java中的异常处理机制
Java中的所有异常,本质上是一个个拥有特定名称的被封装类,这些异常类都继承自Throwable类,分为编译时异常和运行时异常两类;编译时异常又称受检异常,能够在程序编译阶段就被检测出来错误,运行时异常又称非受检异常,只有在程序运行时出现错误才给出异常栈信息。当Java程序中出现相应的异常时,会自动依据该异常生成一个对应的实例化对象,该实例化对象依据方法的调用依次沿栈向上传递,如果到最终的main方法也没有对该异常进行相应的人工处理,则会交由JVM处理,而JVM处理的方法是给出异常信息,并且终止程序的执行。
三、Java中的异常处理模型
try {
可能出现的异常种类代码;
...
} [catch (异常类 实例化对象) {
对该异常的处理;
} catch (异常类 实例化对象) {
对该异常类的处理;
}...][finally {
不论是捕捉到异常,都执行的语句,常用来回收程序运行中的资源
}]
🐻例如,对于常见的类定义异常ArithmeticException,可能会有以下情况导致出现:
public class ArithMeticExceptionTestDrive { public static int division(int x,int y) { int result = x / y; return result; } public static void main(String[] args) { System.out.println(division(10, 0)); System.out.println("division方法执行结束,程序运行结束"); } } /* Exception in thread "main" java.lang.ArithmeticException: / by zero at com.shuai.www.ArithMeticExceptionTestDrive.division(ArithMeticExceptionTestDrive.java:5) at com.shuai.www.ArithMeticExceptionTestDrive.main(ArithMeticExceptionTestDrive.java:9) */
并且在此程序中,会发现程序在division方法所在行被中断了运行,这是因为dividion方法在默认抛出异常后,main方法并没有进行相应的处理,最终该异常被提交给了JVM进行处理,而JVM保持着它一贯的作风:给出了异常栈,终止了程序的运行。
如果程序因为一些无法被避免的且能够经过处理恢复的异常而中断运行,那么该程序一定是很失败的,为了解决这种问题,应该怎么做呢?
🐻为了让出现的能够被解决的异常在解决后能够继续运行程序而不是终端程序的运行成为失败的程序,应该怎么做?
这是就可以使用Java提供的异常处理机制模型,将上述的程序修改为:
public class ArithMeticExceptionTestDrive { public static int division(int x,int y) { int result = x / y; return result; } public static void main(String[] args) { try { System.out.println(division(10, 0)); System.out.println("异常里的语句"); } catch (ArithmeticException e) { System.out,println("捕捉到了算数异常信息,进行处理"); e.printStackTrace();//打印栈追踪信息,即输出异常信息 } System.out.println("division方法执行结束,程序运行结束"); } } /* 运行结果: java.lang.ArithmeticException: / by zero at com.shuai.www.ArithMeticExceptionTestDrive.division(ArithMeticExceptionTestDrive.java:5) at com.shuai.www.ArithMeticExceptionTestDrive.main(ArithMeticExceptionTestDrive.java:10) division方法执行结束,程序运行结束 */
会发现程序在给出相应的异常栈后并没有终止,原因是division函数在执行过程中抛出异常后有main方法的catch捕获到了该异常并且进行了相应的处理,从而使得该程序能够继续向下运行。同时当try语句块中出现异常后,try语句块后面的语句就不再被执行。同时需要注意,不管在何种情况下,finally语句块中的语句都会被执行:
public class FinallyTestDrive { public static int func() { try { int a = 1; return a; } finally { return 2; } } public static void main(String[] args) { System.out.println(func()); } } //输出:2
既然所有得异常类都继承自Throwable父类,那么在进行信息捕获时为什么不使用对象向上转型利用父类对象接收呢?
🐻可以使用父类对象接收异常对象的向上转型进行异常栈的打印,但该父类对象应该是Exception而不是Throwable。
原因是Throwable表示的范围包括error和Exception,如果使用Throwable处理,虽然没有语法上的问题,但会存在逻辑问题,因为此时出现的或者说用户能够处理只有Exception类型,而Throwable还包括了error问题,用户是处理不了的。用Exception接收异常类型:
public class ArithMeticExceptionTestDrive { public static int division(int x,int y) { int result = x / y; return result; } public static void main(String[] args) { try { System.out.println(division(10, 0)); } catch (Exception e) { e.printStackTrace();//打印栈追踪信息,即输出异常信息 } System.out.println("division方法执行结束,程序运行结束"); } } /* 运行结果: java.lang.ArithmeticException: / by zero at com.shuai.www.ArithMeticExceptionTestDrive.division(ArithMeticExceptionTestDrive.java:5) at com.shuai.www.ArithMeticExceptionTestDrive.main(ArithMeticExceptionTestDrive.java:10) division方法执行结束,程序运行结束 */
会发现和上述使用具体的异常类捕捉异常对象时的效果是相同的,Exception实例化的父类对象也成功捕捉了子类异常对象并且给出了相应的处理,从而使得程序继续运行。
而使用Exception时需要注意一个问题:在处理多个异常类时,使用具体类捕捉的异常对象一定要在使用Exception类捕捉的异常对象的前面catch,而这种写法一定是错误的-->原因是后
面的NullPointerException异常类永远捕捉不到异常对象,异常对象在使用Exception类的catch块中已经向上转型被Exception类捕获了,程序开发中应当避免这种异常捕获机制写法的低级错误。而对于一些自定义类,如果依据需求需要在某些特定的情况下给出异常栈,应该怎么做?🐻使用throw关键字,可以手动进行异常对象得抛出。
例如对于上述程序,现在需求要求除数不能小于2,这个时候就可以这样进行书写:
public class ArithMeticExceptionTestDrive { public static int division(int x,int y) { if(y < 2) { throw new ArithmeticException("除数不能小于2");//在除数小于2时手动抛出异常信息 } return x/y; } public static void main(String[] args) { try { System.out.println(division(10, 1)); } catch (ArithmeticException e) { e.printStackTrace();//打印栈追踪信息,即输出异常信息 } System.out.println("division方法执行结束,程序运行结束"); } } /* 运行结果: java.lang.ArithmeticException: 除数不能小于2 at com.shuai.www.ArithMeticExceptionTestDrive.division(ArithMeticExceptionTestDrive.java:6) at com.shuai.www.ArithMeticExceptionTestDrive.main(ArithMeticExceptionTestDrive.java:12) division方法执行结束,程序运行结束 */
这个时候就限制了除数得范围,解决了需求。
而对于一个实现复杂的方法,为了程序的健硕,需要考虑到所有的异常处理,难道调用者在调用时还要翻看方法的具体实现或者打电话给方法的实现者来询问都抛出了哪些异常吗?当然不能这么做!
🐻使用throws关键字,可以在方法的定义语句处声明出该方法包含的所有异常类型。
这样做的好处就在于方法的调用者可以清晰的看到方法中可能出现的异常,从而在调用该方法时做出相应的异常处理方法,提高程序的健硕性和开发效率,对于可能出现多个异常的方法,在声明异常时异常之间用','隔开即可。
public class ArithMeticExceptionTestDrive { public static int division(int x,int y) throws ArithmeticException { //声明了该方法中出现的异常类型 if(y < 2) { throw new ArithmeticException("除数不能小于2"); } return x/y; } public static void main(String[] args) { try { System.out.println(division(10, 1)); } catch (ArithmeticException e) { e.printStackTrace();//打印栈追踪信息,即输出异常信息 } System.out.println("division方法执行结束,程序运行结束"); } } /* 运行结果: java.lang.ArithmeticException: 除数不能小于2 at com.shuai.www.ArithMeticExceptionTestDrive.division(ArithMeticExceptionTestDrive.java:6) at com.shuai.www.ArithMeticExceptionTestDrive.main(ArithMeticExceptionTestDrive.java:12) division方法执行结束,程序运行结束 */
而有时为了应对需求,类中的异常类可能无法满足每套程序的要求。这个时候应该怎么解决?
🐻Java早就考虑到了这个问题,于是出现了自定义异常类。自定义异常类的定义规则:
- 继承子父类Exception或者RuntimeException。
- 在构造方法中调用父类的构造方法
自定义异常类的实现例子:
class BomException extends Exception { public BomException() { super(); } public BomException(String msg) { super(msg); } } public class CustomizeExceptionTestDrive { public static void func() throws BomException { throw new BomException("异常"); } public static void main(String[] args) { try { func(); } catch (BomException e) { e.printStackTrace(); } System.out.println("程序执行结束"); } } /* 输出: com.shuai.www.BomException: 异常 at com.shuai.www.CustomizeExceptionTestDrive.func(CustomizeExceptionTestDrive.java:13) at com.shuai.www.CustomizeExceptionTestDrive.main(CustomizeExceptionTestDrive.java:17) 程序执行结束 */
在自定异常类时建议直接将该异常类继承Exception而非RuntimeException,强制使用该异常类的方法的调用者必须实现异常处理,增强程序的健硕性。
四、总结
Java中的所有异常类都继承自Throwable父类;该父类被两大类继承,分别为error错误类群和Exception异常类群。error类群中出现的是用户无法解决的;Exception类又被受检异常类(编译时异常类)和非受检异常类(运行时异常类)继承。在程序出现异常问题时,对应的异常类会自动生成一个异常对象,该异常对象会沿栈(方法层级)依次传递,如果在此过程中被某个方法中的catch语句块捕获,则输出给用户对应的异常栈,并继续程序的运行;如果没有被catch捕获,则最终会被JVM捕获,JVM保持一贯作风:输出异常栈信息,终止程序的运行。