异常
Java 异常机制
参照:https://mp.weixin.qq.com/s/QCzZOOQ7QrZFkNtsIQok9w
一.Java异常架构
1.Java异常架构图
2.Java异常架构分析
2.1 Throwable
- Throwable 是 Java 语言中所有错误与异常的超类。
- Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
- Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
2.2Error(错误)
-
定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
-
特点:此类错误一般表示代码运行时 JVM 出现问题。通常有
-
Virtual MachineError(虚拟机运行错误)
- 比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线;NoClassDefFoundError(类定义错误)等。
- NoClassDefFoundError(类定义错误)引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
-
这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!
-
2.3Exception(异常)
- 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
运行时异常
- 定义:RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。
- 特点:Java 编译器不会检查它。这个异常是不受检异常,也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。
- 比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。
- 此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理(try-catch),也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。
编译时异常
- 定义: Exception 中除 RuntimeException 及其子类之外的异常。
- 特点: Java 编译器会检查它。这个异常是受检异常,如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。
- 除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。
NoClassDefFoundError 和 ClassNotFoundException 区别?
- NoClassDefFoundError 是一个 Error 类型的非受查异常,是由 JVM 引起的,不应该尝试捕获这个异常。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致; - ClassNotFoundException 是一个Exception类型的受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
二.Java异常处理
1.Java异常处理机制的关键字:
-
try、catch、finally、throw、throws
- try -- 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
- catch -- 用于捕获异常。catch用来捕获try语句块中发生的异常。
- finally -- finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
- throw -- 用于抛出异常。
- throws -- 用在方法签名中,用于声明该方法可能抛出的异常。主方法上也可以使用throws抛出。如果在主方法上使用了throws抛出,就表示在主方法里面可以不用强制性进行异常处理,如果出现了异常,就交给JVM进行默认处理,则此时会导致程序中断执行。
-
throw 和 throws 的区别是什么?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。只能抛出受查异常
- 一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
2.Java异常处理的方式
2.1声明异常
- 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws关键字声明可能会抛出的异常。
注意:
-
非检查异常(Error、RuntimeException 或它们的子类)一般不使用 throws 关键字来声明要抛出的异常。
-
为什么抛出的异常一定是可检查异常(除了Exception中的RuntimeException及其子类以外,其他的Exception类及其子类)?
- 答:RuntimeException与Error可以在任何代码中产生,它们不需要由程序员显示的抛出,一旦出现错误,那么相应的异常会被自动抛出。遇到Error,程序员一般是无能为力的;遇到RuntimeException,那么一定是程序存在逻辑错误,要对程序进行修改;只有可检查异常才是程序员所关心的,程序应该且仅应该抛出或处理可检查异常。而可检查异常是由程序员抛出的,这分为两种情况:客户程序员调用会抛出异常的库函数;客户程序员自己使用throw语句抛出异常。
- 注意:不受检异常,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。不是程序员所关心的,但是你也可以利用他们抛出
原文链接:https://blog.csdn.net/qq_29229567/article/details/89397648
-
2.2抛出异常
如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。
throw关键字作用是:在方法内部抛出一个Throwable
类型的异常。任何Java代码都可以通过throw语句抛出异常。
2.3捕获异常
程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。
- 注意:不受检异常,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。不是程序员所关心的,但是你也可以利用他们抛出
3. try-catch-finally 中哪个部分可以省略?
- catch 可以省略
- 原因:
更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
4.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
执行语句时,如果try块中,没有抛出异常,或者没有出现异常的语句,则不会走catch块,直接走finally块,
如果try块中出现了异常语句,那么它后面的语句都不执行,直接跳到catch,再这前提下
-
- 如果catch有return,finally没有return,则从catch块返回
- 如果catch有return,finally有也return,则从finally块返回
- 如果catch没有return,finally没有return,则从三个块外面的return返回,不再try的return返回
-
答:会执行,在catch块的return 前执行。
-
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值,然后 finally 代码块执行完毕之后再向调用者返回刚刚记录下的值,然后如果在 finally 中修改了返回值,则调用者返回的是未修改前被记录下的值,和修改后的值就会不一致。给程序带来很大困惑。
-
代码示例1:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
*try中先把要返回的结果存放到不同于a的局部变量中去,执行完finally之后,再从中取出返回结果,
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
//执行结果:30
- 代码示例2:
public class FinallyTest
{
public static void main(String[] args) {
System.out.println(new FinallyTest().test());;
}
static int test()
{
int x = 1;
try
{
x++;
return x;
}
finally
{
++x;
}
}
}
//结果是2
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
return a;
}
}
//执行结果:40
-
代码3
public static void main(String[] args) { // 调用 测试方法 String result = get(); // 打印 测试方法返回的结果 System.out.println(result); } public static String get(){ int a = 0; try { System.out.println("try……"); // System.out.println("err before"); // int b = 1 / a; // System.out.println("err after"); return "111"; } catch (Exception e) { System.out.println("catch……"); } finally { System.out.println("finally……"); } return "222"; } } /*执行结果: try…… finally…… return "111"*/ public static void main(String[] args) { // 调用 测试方法 String result = get(); // 打印 测试方法返回的结果 System.out.println(result); } public static String get(){ int a = 0; try { //System.out.println("try……"); System.out.println("err before"); int b = 1 / a; System.out.println("err after"); return "111"; } catch (Exception e) { System.out.println("catch……"); } finally { System.out.println("finally……"); } return "222"; } } /*执行结果: err before catch…… finally…… 222
-
代码4
// 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。 try { throw new ExampleB("b") } catch(ExampleA e){ System.out.println("ExampleA"); } catch(Exception e){ System.out.println("Exception"); } //执行结果:ExampleA
(根据里氏代换原则[能使用父类型的地方一定能使用子类型],使用父类能够捕获子类的异常
- 抓取 ExampleA 类型异常的 catch 块,能够抓住 try 块中抛出的 ExampleB 类型的异常)
-
代码5(此题的出处是《Java 编程思想》一书)
class Annoyance extends Exception { } class Sneeze extends Annoyance { } class Human { public static void main(String[] args) throws Exception { try { try { throw new Sneeze(); } catch ( Annoyance a ) { System.out.println("Caught Annoyance"); throw a; } } catch ( Sneeze s ) { System.out.println("Caught Sneeze"); return ; } finally { System.out.println("Hello World!"); } } } /*执行结果: Caught Annoyance Caught Sneeze Hello World! */
**注意:为什么第二行可以打印出来?**
- **并不是子类捕获到父类的异常**
- ```java
try {
try {
throw new Sneeze();
} catch ( Annoyance a ) {//这一句使用的是父类的引用,但实际上是子类的对象,这是java中多态的经典表现
//Annoyance a =new Sneeze();
System.out.println("Caught Annoyance");
throw a;
}
} catch ( Sneeze s ) {
//所以当然可以捕获到自己抛出来的异常了。
System.out.println("Caught Sneeze");
return ;
}