Java进阶_02_异常


本文介绍Java中的异常机制。

1. 异常概述

程序在编写或者运行过程中会发生一些不正常的情况,比如除数为0,类型不能转换等情况。这种不正常的情况叫做:异常。

Java语言是很完善的语言,提供了异常的处理方式。当异常出现后,程序停止执行,JVM把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对程序进行修改,让程序更加的健壮。

adv_004.png (1016×579) (gitee.io)

在API官方文档中可以看到,java.lang包下提供了很多的异常类,如NumberFormatException、ClassCastException。所以异常是以类和对象的形式存在的。程序运行过程中如果遇到不正常情况,JVM会创建对应的异常对象,并将对象信息打印到控制台上。简单代码如下所示:

1
2
3
4
5
6
7
8
9
public class ExceptionTest01 {

public static void main(String[] args) throws Exception{

NullPointerException npe = new NullPointerException("创建空指针异常实例对象");

System.out.println(npe);
}
}

2. 异常的继承结构

根类是Object类,然后Throwable类继承Object类,是所有错误(Error)和异常(Exception)的父类。不管是错误还是异常,都是可抛出的。

Throwable类有Error和Exception两个直接子类。

错误只要发生,Java程序只有一个结果,就是终止程序的执行,退出JVM,错误是不能处理的。而异常允许处理,不处理也会终止执行。

异常分为编译时异常运行时异常,异常Exception类的直接子类均为编译时异常,编译时异常并不是指的是在编译阶段发生的异常,而是表示必须在编写程序的时候预先处理这种异常,如果不处理编译器会报错。除了直接子类还有RuntimeException类运行时异常,在编写阶段可以处理也可以不处理。

2.1 编译时异常和运行时异常

编译时异常和运行时异常,都是发生在运行阶段,编译阶段是不会发生的。编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器会报错,所以称为编译时异常。所有异常都是在运行阶段发生的,因为只有程序运行阶段才可以new对象,异常的“直观体现”就是new异常对象。

编译时异常一般发生的概率比较高,而运行时异常一般发生的概率比较低。编译时异常一般是可以预料到的,发生概率较高的,比如淋雨会导致生病,在运行前对其进行预处理(提前拿一把伞);而运行时异常则是不可预料到的,比如天降横祸,在运行前没必要对这种异常进行处理。

2.2 异常的处理方式

Java语言对异常的处理包括两种方式:

  • 在异常所在方法声明的位置上,使用throws关键字,称为抛出/上抛,注意抛出的异常范围不能比当前异常小,就是不能抛其子类异常。
  • 使用try…catch语句进行异常的捕捉,称为捕捉

注意,只要发生异常,如果采用throws上抛,那么异常语句之后的语句就不会执行;如果采用try…catch捕捉,那么try括号里面的异常语句之后的语句不会执行。

try…catch语法如下,注意,捕捉只能捕捉指定的异常及其子类型异常,其他类型异常不会捕捉到

1
2
3
4
5
try{
可能发生异常的语句;
}catch(捕捉的异常类型 变量名){
异常发生后执行的操作;
}

注意,异常发生后,如果选择了上抛,即抛给了方法的调用者,那么调用者需要对这个异常继续处理,同样有这两种处理方式。

注意,Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果:终止Java程序的执行。

一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM,JVM只能终止程序。而异常处理机制的作用就是增强程序的健壮性,让程序不终止。所以一般main方法中的异常建议使用 try…catch 进行捕捉,main方法就不要继续上抛了。

简单案例如下所示:

案例1,运行时异常,不需要提前处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExceptionTest01 {

public static void main(String[] args){

/*
程序执行到此处发生了ArithmeticException异常,底层new了一个
ArithmeticException异常对象,然后抛给了main方法,并且main方法并没有处理,所以将这个异常自动抛给了JVM,JVM终止程序的执行。
该异常是RuntimeException异常,运行时异常,不需要提前处理。但是不处理仅仅编译不会报错而已,该处理还是要处理。
*/
System.out.println(100 / 0);

// 这里的输出语句并没有执行。
System.out.println("Hello world!");

}
}

案例2,,编译时异常,不处理编译器会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ExceptionTest02 {

public static void main(String[] args) {

// main()方法中调用doSome()方法
// 因为doSome()方法声明抛出的是编译时异常,必须对其处理,所以main方法收到抛出的异常后必须对异常进行处理。
// 如果不处理,编译器就会报错。
doSome(); // 报错,因为main方法没有处理异常。
}

/**
* doSome方法在声明的位置上使用了:throws ClassNotFoundException,
* 这个代码表示 doSome()方法在执行过程中,有可能会出现 ClassNotFoundException 异常。
* 这个异常的直接父类是:Exception,所以该异常属于编译时异常,在编译阶段必须对其处理。
* @throws ClassNotFoundException
*/
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome()!!!");
}
}

处理异常案例3,针对编译时异常上抛和捕捉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ExcepTest03 {

/*
处理方法1,上抛异常
*/
// public static void main(String[] args) throws ClassNotFoundException {
// doSome();
// }

/*
处理方法2,捕捉异常
*/
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

/**
* doSome方法在声明的位置上使用了:throws ClassNotFoundException,
* 这个代码表示 doSome()方法在执行过程中,有可能会出现 ClassNotFoundException 异常。
* 这个异常的直接父类是:Exception,所以该异常属于编译时异常,在编译阶段必须对其处理。
* @throws ClassNotFoundException
*/
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome()!!!");
}
}

2.3 throws抛出异常

抛出异常指的是将异常抛给调用者,自身并不处理异常,相当于甩锅。对于运行时异常,可处理也可不处理;对于编译时异常,必须处理,无论是抛出还是捕捉,如果是捕捉,捕捉一次即可,如果是抛出,则需要一直抛出直到main方法或者直到捕捉。

案例1,采用一直上抛,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class ExceptionTest04 {

public static void main(String[] args) throws FileNotFoundException{
m1(); // main()方法接收了m1()方法抛出的异常,需要处理。
}


public static void m1() throws FileNotFoundException{
System.out.println("m1 begin");
m2(); // m1()方法接收了m2()方法抛出的异常,需要处理
System.out.println("m1 end");
}

public static void m2() throws FileNotFoundException{
System.out.println("m2 begin");
m3(); // m2()方法接收了m3()方法抛出的异常,需要处理。
System.out.println("m2 end");
}

public static void m3() throws FileNotFoundException {

// FileInputStream构造方法抛出了编译时异常,此时调用者 m3 必须处理异常
new FileInputStream("D:\\studytest\\nptest\\a.csv");
System.out.println("qwefasdf"); // 如果发生异常,该语句不会执行
}
}

2.4 try…catch捕捉异常

捕捉异常指的是将异常捕捉并进行处理,不会让调用者知道,相当于对于调用者来说是“透明的”。最终JVM也就不会知道发生了异常,也就不会终止程序,保证了程序的健壮性。

案例2,采用捕捉,中间任何一步捕捉即可,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class ExceptionTest05 {

public static void main(String[] args) {

m1();
}

public static void m1(){
System.out.println("m1 begin");
m2();
System.out.println("m1 end");
}

public static void m2(){
System.out.println("m2 begin");
try{
m3();
System.out.println("qwefasdf"); // 如果发生异常,该语句不会执行
}catch(FileNotFoundException ffe){
System.out.println(ffe);
}
System.out.println("m2 end");
}

public static void m3() throws FileNotFoundException{
new FileInputStream("D:\\studytest\\nptest\\a.csv");
}
}

2.5 上抛和捕捉如何选择?

如果希望调用者来处理,那么就选择throws上抛,否则try…catch捕捉自己处理。

3. throws和throw

throw用于 创建完异常对象 手动抛出异常,可以认为throw是将异常抛给所在方法。

thows用于方法声明的时候抛出异常,属于异常处理的一种方法。

注意,仅仅创建完异常对象,此时JVM会认为是普通对象,并不会终止程序。

4. try…catch深入

catch括号中的异常类型可以是try语句块中的异常类型也可以是该类型的父类型(多态)。如果try语句块中的语句抛出多个异常,catch语句也可以捕捉多个异常,附加多个catch语句块即可建议catch的时候,异常类型尽量小,精确地一个一个处理,这样有利于程序的调试。

1
2
3
4
5
6
7
try{

}catch(FileNotFoundEception fee){

}catch(IOException ioe){

}...

注意,当有多个catch捕捉异常的时候,当出现了异常,就会自上而下匹配catch异常,所以当catch有多个的时候,自上而下设置的异常类型应该范围由小到大,否则,上面的大范围异常就会捕捉,而下面的小范围异常就会用不到,编译器报错。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void m3(){

try{
FileInputStream fis = new FileInputStream("D:\\studytest\\nptest\\asdf.csv");
fis.read();
}catch(FileNotFoundException ffe){
System.out.println("文件不存在,可能路径有误!");
}catch(IOException ioe){
System.out.println("该文件报错了");
}

System.out.println("asdfzxcv");
}

**上面的FileNotFoundException类型异常的范围小于IOException,所以当fis.read()发生异常的时候,自上而下匹配catch异常,匹配到第二个IOException。而如果IOException写在FileNotFoundException上面的话,当FileInputStream发生异常的时候就会匹配到IOException,因为IOException的范围大,包括了FileNotFoundException,就会报错Exception 'java.io.FileNotFoundException' has already been caught**。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void m3(){

try{
FileInputStream fis = new FileInputStream("D:\\studytest\\nptest\\asdf.csv");
fis.read();
}catch(IOException ioe){
System.out.println("该文件报错了");
}catch(FileNotFoundException ffe){
System.out.println("文件不存在,可能路径有误!");
}

System.out.println("asdfzxcv");
}

另外,多个catch异常可以压缩成一个catch语句块,前提是几个catch异常不能有交集,如下所示:

1
catch(FileNotFoundException | NullPointerException e)

5. 异常对象的常用方法

异常对象有两个非常重要的方法:getMessage()和printStackTrace()。

  1. getMessage()

    获取异常简单的描述信息,即构造方法中的String参数。

  2. printStackTrace()

    打印异常追踪的堆栈信息,即平时运行程序中出现的红色异常信息。注意,异常堆栈信息是另一个线程负责输出,所以最终的输出顺序可能和代码的输出顺序不一致。一般情况下try…catch捕捉异常时的操作就是采用该方法打印异常堆栈信息。

adv_005.png (1336×674) (gitee.io)

6. finally关键字

其实在捕捉异常的时候,还有finally语句块,该语句块中的代码是最后执行的,并且是一定会执行的,即使出现了异常。注意,finally子句必须和try…catch语句块一起使用,不能单独编写。

finally语句通常使用在哪些情况下呢?通常在finally语句块中完成资源的释放/关闭,因为finally中的代码比较有保障。即使try语句块中的代码出现问题,finally语句块也会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ExceptionTest07 {

public static void main(String[] args) {

FileInputStream fis = null;
try {
fis = new FileInputStream("D:\\studytest\\nptest\\asdf.csv");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("finally语句块");
}
}
}

另外,try可以和finally一起使用,但是try不能单独使用,**注意,只要有finally语句块,那么该语句块中的代码一定会执行,无论try中的语句是怎样的,除非try中有退出JVM的代码:System.exit(0);**,如下所示:

1
2
3
4
5
6
7
8
9
10
11
public class ExceptionTest08 {

public static void main(String[] args) {

try{
System.exit(0);
}finally{
System.out.println("finally");
}
}
}

7. 补充

在Java中有一些强制性的规则(有些规则是不能破坏的,一旦这么说了,就必须这么做!):

  • 方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
  • return语句一旦执行,整个方法必须结束,或者说return语句一定最后执行(亘古不变的语法!上面那个是退出JVM,和return无关)

下面的例子可能会违反前面所说的语句执行顺序,可以查看反编译后的代码。如下所示:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ExceptionTest09 {

public static void main(String[] args) {

System.out.println(m());
}

public static int m(){
int i = 100;
try{
return i;
}finally{
i++;
System.out.println("finally输出:" + i);
}
}
}

反编译后的代码(可在IDEA对应项目下的out目录下查找,就是.class文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExceptionTest09 {
public ExceptionTest09() {
}

public static void main(String[] args) {
System.out.println(m());
}

public static int m() {
byte i = 100;

byte var1;
try {
var1 = i;
} finally {
int i = i + 1;
System.out.println("finally输出:" + i);
}

return var1;
}
}

可以看到,源码中的return语句返回的是finally操作之前的变量。

  • return语句要最后执行,所以反编译(JVM内部实际代码)后将return语句放在了最后面。
  • 要自上而下执行,return返回的是未finally操作后的代码,所以只能是保存临时变量,并新建变量供finally操作。

8. final、fianlly、finalize()

  • final是一个关键字,表示最终的,不变的。
  • finally也是一个关键字,和try联合使用在异常处理机制中,finally语句块中的代码一定会执行的。
  • finalize()是Object类中的一个方法,作为方法名出现,用于垃圾回收回收时的一些操作。

9. 自定义异常

SUN提供的JDK内置的异常肯定是不够用的,在实际开发中,有很多业务出现异常之后,很多是JDK没有的,这时候就需要我们自定义异常。

查看一些异常类源码,可以发现,基本上只有无参构造方法以及有参构造方法两个方法,或者还有一些其他的私有方法以及属性等等。

9.1 自定义异常

有如下两个步骤:

  1. **编写一个类继承Exception(编译时异常)或者RuntimeException(运行时异常)**。
  2. 提供两个构造方法:无参构造方法,带有String参数的有参构造方法

自定义异常如下所示:

1
2
3
4
5
6
7
8
9
public class MyException01 extends Exception{
// 编译时异常

public MyException01(){}

public MyException01(String s){
super(s);
}
}

使用异常类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExceptionTest10 {

public static void main(String[] args) {

MyException01 me01 = new MyException01("用户名不能为空!");

me01.printStackTrace();

System.out.println(me01.getMessage());

try {
doSome();
} catch (MyException01 myException01) {
myException01.printStackTrace();
}
}

public static void doSome() throws MyException01{

throw new MyException01("用户名和密码不能为空!");
}
}

9.2 异常的作用

异常的作用就是为了当程序出现异常的时候,可以合理显著地提示程序员。而我们之前的操作基本上就是控制台打印一些提示信息(比如除数不能为0,栈已满等等)并return,这样做不是很合理,应该以异常的形式提示。

9.3 异常与方法重写

子类重写之后的方法不能比重写之前的方法抛出更多(更宽泛的异常),可以更小。注意:不抛异常 的 范围 比抛出异常 范围小(运行时异常除外)。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal{

public void doSome01(){}
public void doSome02() throws Exception{}
public void doSome03() throws Exception{}
}

class Cat extends Animal{

// 报错,因为父类没有抛异常,子类无法抛编译时异常,可以抛运行时异常
// public void doSome01() throws IOException {}

// 不报错,因为不抛异常的范围比抛出异常小。
public void doSome02() {}

// 不报错,子类抛出的异常范围小于父类
public void doSome03() throws IOException{}
}

一般情况下,父类的方法如何写,子类就如何写就行,不需要修改异常的抛出与范围。

10. 备注

参考B站《动力节点》。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录