Java语言中Throwable是所有异常的根类,Throwable 派生了两个直接子类Error 和 Exception。Error 表示应用程序本身无法克服和恢复的一种严重问题,触发Error时会终止线程甚至是虚拟机。Exception 表示程序还能够克服和恢复的问题,Exception按照处理时机可以分为编译时异常和运行时异常。
编译时异常都是可以被修复的异常,代码编译期间Java程序必须显式处理编译时异常,否则无法编译通过。运行时异常通常是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下软件系统可能会继续运行,严重情况下软件系统才会死掉。
异常的处理 1、异常处理的方案Java中异常处理通常有两种方案,捕获处理异常和抛出异常。
对编译时异常处理方法有两种,当前方法知道如何处理该异常,则捕获处理。当前方法不知道如何处理,则在定义该方法时声明抛出该异常。
运行时异常只有当代码在运行时才发现的异常,编译时不需要捕获处理。如除数是0、数组下标越界等等,其产生频繁,处理麻烦,若显示声明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
2、异常处理关键字Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字。一般try,catch,finally结合使用用于捕获异常。throws,throw单独使用用于抛出异常。
try关键字后紧跟一个花括号扩起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块,在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上到下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后面的,否则编译器会报错。finally块跟catch块之后,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。
throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常。throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。
3、异常处理栗子(1)编译时异常
public static void main(String[] args) {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile(); // 这段代码直接运行,这里编译不通过,报编译时异常。
}
}
编译时异常在代码编译期间就会报错(注意编译期间报错的不一定都是编译时异常),这种异常在编码期间需要手动捕获或者抛出处理~
/**
* Create by SunnyDay on 2022/04/21 18:26
*/
public class ExceptionDemo {
public static void main(String[] args) throws IOException {
tryCatch();
throwsException(); // 这里选择继续抛出给main
}
/**
* 捕获异常栗子
*/
private static void tryCatch() {
try {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("finally 块");
}
}
/**
* 方法签名处抛出异常栗子。
* 注意若是此方法被其他方法A调用,那么A需要捕获或者抛出处理。
*/
private static void throwsException() throws IOException {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile();
}
}
}
(2)运行时异常
private static void runtimeException(String name) {
name.length(); //name 为空时java.lang.NullPointerException.直接crash。
}
运行时异常一般为开发人员代码考虑不周引起的,一般不需要主动来捕获或者抛出的~ 不过若是需要也可以主动捕获处理~如下。
/**
* 不过一般不会建议采取捕获处理的方式,完全可通过name的判空处理。
*/
private static void runtimeException(String name) {
// 捕获,出现异常也不会导致crash,不影响try catch 块之外的逻辑。
try {
name.length(); //java.lang.NullPointerException
} catch (Exception e) {
e.printStackTrace();
}
}
(3)throw关键字
前面两个栗子就把异常相关关键字熟悉了下,不过还有个throw未涉及到,这里就再补充下~
/**
* 栗子:throw的用法。
*
* @function 当方法调用者不小心传递参数type为null时 直接抛出异常,程序crash.
*/
private static int getCode(String type) {
if (type == null) throw new IllegalArgumentException("参数不能为空");
int code;
if ("0".equals(type)) {
code = 0;
} else if ("1".equals(type)) {
code = 1;
} else {
code = 2;
}
return code;
}
(4)访问异常信息
如果需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获得。当JVM决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,这时我们可通过该参数来获得异常的相关信息。常用方法如下:
- getMessage():返回该异常的详细描述字符串。
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
看似两三个栗子吧异常过了一遍,其实异常相关的注意点还是很多的,这些需要我们在开发、学习中总结~
(1)不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此异常退出。一般表现为方法执行非正常完成。
(2)try块与if语句不一样,try块后的花括号不可以省略,即使try块里只有一行代码,也不可省略这个花括号。与之类似的是,catch块后的花括号也不可以省略。
(3)try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。
(4)触发异常时虚拟机会生成对应的异常,并会自上而下遍历我们catch中定义的异常条目,寻找匹配的异常条目。catch 中定义异常条目时要遵循只能扩大或者不相关的原则,否则编译失败。
(5)在Java 7以前,每个catch块只能捕获一种类型的异常;但从Java 7开始,一个catch块可以捕获多种类型的异常。多异常捕获需要注意:
-
捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。
-
捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
private static void mutipleException(){
// 多异常
try {
}catch (ArrayIndexOutOfBoundsException|NumberFormatException e){
e = new IllegalArgumentException("") // 编译报错
}
// 单个异常
try {
}catch (Exception e){
e = new IllegalArgumentException(""); // 编译通过
}
}
(6)在某些不幸的情况下,catch 代码块也触发了异常,那么 finally 代码块同样会运行,并会抛出 catch 代码块触发的异常。在某些极端不幸的情况下,finally 代码块也触发了异常,那么只好中断当前 finally 代码块的执行,并往外抛异常。
/**
* try中捕获异常A,catch中又触发异常B,这时finally执行完后系统只会抛出异常B。
* 这种case也可以看做try catch的弊端,丢失了try中的异常。
* */
private static void test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
String a = null;
a.length(); // finally 执行完毕后这里最终由系统抛出NullPointerException
} finally {
System.out.println("finally");
}
}
/**
* try中捕获异常A,catch中又触发异常B,这时finally执行又触发异常C系统只会抛出异常C。
* 这种case也可以看做try catch的弊端,丢失了try,catch中的异常。
*/
private static void test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
String a = null;
a.length(); // NullPointerException
} finally {
Integer.parseInt("aaa"); //代码执行到这里只会抛出NumberFormatException。上述两异常忽略。
System.out.println("finally");
}
}
(7)不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。也即try、catch中的return语句不会影响finally代码块的执行。
public static void main(String[] args) {
System.out.println("test return value:"+test());
}
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
return 0;
} finally {
System.out.println("finally");
}
System.out.println("test finish");
return 1;
}
log:
finally
test return value:0
可见finally最终打印出来了,证明了我们的观点,那么为啥方法最终打印的返回值是0,而不是1呢?其实流程是这样的~
首先代码执行到try块触发ArithmeticException异常,然后catch块捕获住处理,不过异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话,那么有 finally 就必须先执行完 finally 代码块里面的代码然后再返回到catch中抛出或者return。最终执行catch return语句方法结束。后续的代码不会再执行了。
不妨可以修改代码验证下,如下catch 代码块执行完后会继续走try catch finally 之外的代码~
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
System.out.println("catch");
} finally {
System.out.println("finally");
}
System.out.println("test finish");
return 1;
}
log:
catch
finally
test finish
test return value:1
来个栗子再让我们更好巩固下,彻底理解他 emmm~ 如下方法的返回值是几?
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
代码执行到try的 int a这里会触发ArithmeticException,这时由异常处理器捕获,catch中return 2,但是异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话,那么有 finally 就必须先执行完 finally 代码块里面的代码然后再返回到catch中抛出或者return。此时会先执行finally中的return3。finally这里正好碰到了return语句,正常结束方法。
若是finally只是处理一些资源关闭的代码,这里未return 3,那么本方法的返回值就是2喽~
(8)除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
System.out.println("try");
System.exit(1);
} catch (Exception e) {
System.out.println("catch");
System.exit(0);
} finally {
System.out.println("finally");
}
return 0;
}
log:
catch
Process finished with exit code 0
如上,首先触发ArithmeticException异常,此时会走到catch代码块,执行了打印语句后执行System.exit(0) 直接退出JVM。
(9)在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,(throw语句将在后面介绍),一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效。
(10)在try块、catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。
- 异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可放在try块里,也可放在catch块里,还可放在finally块里。
- 异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。
前面了解到try catch 中的异常存在丢失的情况,为了解决这个问题,java7引入了Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。
Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Supressed 异常。当然,该语法糖的主要目的并不是使用 Supressed 异常,而是精简资源打开关闭。因为在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状态情况下都能关闭。这种做法使代码太臃肿了~
Java 7 的 try-with-resources 语法糖,极大的简化了上诉代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close *** 作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于上面手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常 “被消失”。
(1)自动关闭资源
系统提供了一些类自动实现了AutoCloseable ,这时我们直接使用try-with-resources 语法糖即可,不用再使用finally做繁琐的关闭处理的工作了~
public static void main(String[] args) throws Exception {
/**
* 1、try()中进行变量定义(创建、赋值),类必须实现了AutoCloseable接口(或者是AutoCloseable实现类)。
* 2、try后的代码块中可进行逻辑的 *** 作。
* 3、自动关闭资源的try语句相当于包含了隐式的finally块,执行了close回调,因此这个try语句可以既没有catch块,
* 也没有finally块。
* 4、注意AutoCloseable#close()方法抛出了Exception
* */
try (
BufferedReader br = new BufferedReader(new FileReader("F://a.txt"));
PrintStream pr = new PrintStream(new FileOutputStream("F://b.txt"))
) {
br.readLine();
pr.write("emmm".getBytes());
}
}
BufferedReader、PrintStream都实现了Closeable接口,把它们放在try语句中声明、初始化,try语句会自动关闭它们。当然我们也可以自定义类实现接口即可,在接口中实现资源的处理工作。接下来验证下异常的捕获~
(2)避免异常的丢失
/**
* Create by SunnyDay on 2022/04/22 17:37
*/
public class Demo implements AutoCloseable {
private String desc;
public Demo(String name) {
this.desc = name;
}
public static void main(String[] args) throws Exception {
try (
Demo demo1 = new Demo("1");
Demo demo2 = new Demo("2")) {
int a = 10/0; // 执行代码 触发异常
}
}
@Override
public void close() throws Exception {
// 这里直接抛出一个异常,验证 finally中触发了异常工作。
throw new IllegalArgumentException();
}
}
log: 打印所有的异常信息
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Demo.main(Demo.java:21)
Suppressed: java.lang.IllegalArgumentException
at Demo.close(Demo.java:29)
at Demo.main(Demo.java:22)
Suppressed: java.lang.IllegalArgumentException
at Demo.close(Demo.java:29)
at Demo.main(Demo.java:22)
异常实现原理
class 文件被编译成字节码时,每个方法都附带一张异常表。异常表中的每一个条目代表一个异常处理器(包括from、to、target、所捕获的异常类型)
- from 、to 表示表示异常处理器监控范围,即用try标记的范围。
- target表示异常处理器的起始位置,即catch起始位置。
- 异常类型即为xxxException。
/**
* Create by SunnyDay on 2022/04/22 18:45
*/
public class Test {
public static void main(String[] args) {
// 异常条目1(try catch finally块就是一个异常处理器)
try {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("finally1");
}
// 异常条目1
try {
int a = 1/0;
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("finally2");
}
}
}
//javap 命令 查看class文件:javap -c -l Test.class main方法的异常表如下:
Exception table:
from to target type
0 22 33 Class java/io/IOException
0 22 49 any
33 38 49 any
60 64 75 Class java/lang/Exception
60 64 91 any
75 80 91 any
当程序触发异常时,Java 虚拟机会生成抛出一个异常实例,并且从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会d出当前方法对应的Java 栈帧,并且在调用者(caller)中重复上述 *** 作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java栈上所有方法的异常表。
finally 代码块的编译比较复杂,当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。
针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。
如果 catch 代码块捕获了异常,并且触发了另外一个异常,那么 finally 捕获并重抛的异常是哪个呢?答案是后者,也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。
UncaughtExceptionHandler待续~
End参考:深入拆解 Java 虚拟机
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)