查漏补缺,持续学习
一、Throwable结构
在Java中,Throwable是所有错误与异常的超类。Throwable包含两个子类:Error(错误)和Exception(异常)
异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理
Error
通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception
通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常
Error: 是程序无法处理的错误,表示运行应用程序中较严重问题. 大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。(如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等)这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。在 Java中,错误通过Error的子类描述。
当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError,Java虚拟机(JVM)一般会选择线程终止
Exception: 是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
- 不可查异常定义【包括RuntimeException和错误】:执行方法期间抛出的RuntimeException的任何子类都无需在throws子句中进行声明,因为它是uncheckedExcepiton。常见五种RuntimeException如下,RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException
- java.lang.ArithmeticException(算术异常)
- java.lang.ClassCastException(类型转换异常)
- java.lang.IllegalArgumentException(不合法的参数异常)
- java.lang.IndexOutOfBoundsException(数组下标越界异常)
- java.lang.NullPointerException(空指针异常)
- 受检异常定义: Exception 中除 RuntimeException 及其子类之外的异常。特点: Java编译器要求程序必须try-cache捕获或声明throws子句抛出这种异常
- java.io.IOException(IO流异常)
- java.lang.ClassNotFoundException(没找到指定类异常)
- java.lang.NoSuchFieldException(没找到指定字段异常)
- java.lang.NoSuchMetodException(没找到指定方法异常)
- java.lang.IllegalAccessException(非法访问异常)
- java.lang.InterruptedException(中断异常)
二、异常处理机制
在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
2.1 抛出异常
2.1.1 抛出异常的声明
如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句来声明抛出异常。例如汽车在运行时可能会出现故障,汽车本身没办法处理这个故障,那就让开车的人来处理。
throws语句用在方法定义时声明该方法要抛出的异常类型,如果抛出的是Exception异常类型,则该方法被声明为抛出所有的异常。多个异常可使用逗号分割。throws语句的语法格式为:
methodname throws Exception1,Exception2,..,ExceptionN { }
方法名后的throws Exception1,Exception2,…,ExceptionN 为声明要抛出的异常列表。当方法抛出异常列表的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由他去处理。
使用throws关键字将异常抛给调用者后,如果调用者不想处理该异常,可以继续向上抛出,但最终要有能够处理该异常的调用者。
2.1.2 Throws抛出异常的规则:
1. 如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
2. 如果一个方法可能出现可查异常(checked exception),要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
3. 只有当抛出了异常时,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
4. 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
2.1.3 使用Throws抛出异常
throw总是出现在方法体中,用来抛出一个Throwable类型的异常。程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块,或者层层上抛,最终JVM会进行处理。
我们知道,异常是异常类的实例对象,我们可以创建异常类的实例对象通过throw语句抛出。该语句的语法格式为:
throw new exceptionname;
例如抛出一个IOException类的异常对象:
throw new IOException;
要注意的是,throw 抛出的只能够是可抛出类Throwable 或者其子类的实例对象。下面的操作是错误的,因为String 不是Throwable 类的子类。
throw new String("exception");
如果抛出了可查异常,则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。
如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是Error或RuntimeException,则该方法的调用者可选择处理该异常。
public class TestException {
public static void main(String[] args) {
int a = 6;
int b = 0;
try {
if (b == 0) {
throw new ArithmeticException();
//"除数为0"等ArithmeticException,是RuntimException的子类。而运行时异常将由运行时系统自动抛出,不需要使用throw语句,这里把throw new ArithmeticException()去掉也是不影响运行结果的。
}
System.out.println("a/b的值是:" + a / b);
} catch (ArithmeticException e) {
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}
运行结果:
程序出现异常,变量b不能为0。
程序正常结束。
2.1.4 补充
为什么要在声明方法抛出异常?
答:方法是否抛出异常与方法返回值的类型一样重要。假设方法抛出异常却没有声明该方法将抛出异常,那么客户程序员可以调用这个方法而且不用编写处理异常的代码。那么,一旦出现异常,那么这个异常就没有合适的异常控制器来解决。
为什么抛出的异常一定是可检查异常(除了Exception中的RuntimeException及其子类以外,其他的Exception类及其子类)?
答:RuntimeException与Error可以在任何代码中产生,它们不需要由程序员显示的抛出,一旦出现错误,那么相应的异常会被自动抛出。遇到Error,程序员一般是无能为力的;遇到RuntimeException,那么一定是程序存在逻辑错误,要对程序进行修改;只有可检查异常才是程序员所关心的,程序应该且仅应该抛出或处理可检查异常。而可检查异常是由程序员抛出的,这分为两种情况:客户程序员调用会抛出异常的库函数;客户程序员自己使用throw语句抛出异常。
注意: 在finally块中不能抛出异常。JAVA异常处理机制保证无论在任何情况下必须先执行finally块然后再离开try块,因此在try块中发生异常的时候,JAVA虚拟机先转到finally块执行finally块中的代码,finally块执行完毕后,再向外抛出异常。如果在finally块中抛出异常,try块捕捉的异常就不能抛出,外部捕捉到的异常就是finally块中的异常信息,而try块中发生的真正的异常堆栈信息则丢失了。
2.2 捕获异常
2.2.1 try-cache语句
try {
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
} catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}
关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域。
Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。
匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。
注意:一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。
Java通过异常类描述异常类型,对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。
2.2.2 try-cache-finally语句
try-catch语句还可以包括第三部分,就是finally子句。它表示无论是否出现异常,都应当执行的内容。try-catch-finally语句的一般语法形式为:
try {
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
} catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}finally {
// 无论是否发生异常,都将执行的语句块
}
try、catch、finally语句块的执行顺序:
1)当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
2)当try捕获到异常,catch语句块里没有处理此异常的情况:此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
3)当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句。finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。
2.2.3 如果执行了finally,返回值的问题?
public static void main(String[] args) {
System.out.println(test());
}
public static int test(){
try {
System.out.println("try block");
int i = 1 / 0;
return 0;
} catch (Exception e) {
System.out.println("catch block");
return 1;
} finally {
System.out.println("finally block");
return 2;
}
}
运行结果输出:
try block
catch block
finally block
2
对于上面的代码,相信大部分人都能知道输出值是 2,打印结果也确实是 2,就算把 int i = 1 / 0这一行注释掉,打印结果也是 2。
所以在这里我们可以下结论 : finally里的 return语句会把 try/catch块里的 return语句效果给覆盖掉。
假如我们不在 finally中 return,结果会怎样?我们再看看下面的例子 :
public static void main(String[] args) {
System.out.println(test());
}
public static int test(){
int i = 999;
try {
System.out.println("try block");
i = 1 / 0;
return i;
} catch (Exception e) {
System.out.println("catch block");
i = 100;
return i;
} finally {
System.out.println("finally block");
i = 200;
}
}
打印结果:
try block
catch block
finally block
100
虽然调用了 finllay
改变了i的值,但是最后输出还是 100
,为什么呢?我们可以通过分析字节码文件得到结果 :
public static int test();
Code:
0: sipush 999
3: istore_0
。。。
29: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
32: ldc #14 // String catch block
34: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: bipush 100
39: istore_0
40: iload_0
41: istore_2
42: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #12 // String finally block
47: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: sipush 200
53: istore_0
54: iload_2
55: ireturn
。。。
从字节码文件中可以看出,在 37:39
中把 100
存入 index 0
的位置,就是设置 i
的值为 100
,在 40:41
中把 index 0
的值赋值给了 index 2
的位置,在 50:53
中把 200
存入 index 0
的位置,就是设置 i
的值为 200
,最后在 54:55
中把 index 2
的值加载出来并返回,即最后返回的是 index 2
的值 100
。
对于这种情况我的理解就是在 return
的的时候会把返回值压入栈,并把返回值赋值给栈中的局部变量, 最后把栈顶的变量值作为函数返回值。所以在 finally
中的返回值就会覆盖 try/catch中
的返回值,如果 finally
中不执行 return
语句,在 finally
中修改返回变量的值,不会影响返回结果。
3、常见异常
RuntimeException子类
IOException
其他
四、相关问题
1. 为什么要创建自己的异常
答:当Java内置的异常都不能明确的说明异常情况的时候,需要创建自己的异常。
2. 应该在声明方法抛出异常还是在方法中捕获异常?
答:捕捉并处理知道如何处理的异常,而抛出不知道如何处理的异常。
3. throw和throws的区别是什么?
答:throw抛出异常,方法的调用者必须处理该异常,就是必须try catch处理这个异常,或者继续throws继续网上抛; throw抛出异常,程序就终止了,调用该方法的地方报错,不继续向后执行