异常
异常的概念
在Java中,将程序执行过程中发生的不正常行为称为异常。(不包含语法错误和逻辑错误)
Java中的异常,其实是一个一个的类,可以是JVM自动抛出的,也可以是程序员通过throw语句
手动抛出的。
我们举几个例子,并观察其源码:
-
ArithmeticException
算术异常public static void main(String[] args) { System.out.println(10 / 0); }
-
NullPointerException
空指针异常public static void main(String[] args) { int[] array = null; array[0] = 10; }
异常的体系结构
Java维护了一个异常的体系结构,如下图所示:
观察图片,我们得出:
Throwable
是异常体系的顶层类,其他所有的类都直接或间接地继承它,它派生出两个子类:Error
和Exception
Error
是指Java虚拟机无法解决的严重问题,程序员是不能改变和处理的,遇到这样的问题,建议让程序直接终止,因此我们编写程序时不需要关心这一类错误。比如,JVM的内部错误、资源耗尽、线程死锁等。常见的:StackOverflowError栈溢出
Exception
即我们平时常说的狭义的异常(后面讲解的异常均指Exception)
,表示程序可以处理的异常可以捕获并可以尝试恢复,所以遇到这类异常我们要尽力处理,使程序恢复运行。主要指编码、环境、用户操作输入出现问题
异常的分类
Java的异常(广义)根据Javac对异常的处理要求,分为 检查异常 和 非检查异常
【检查异常】
检查异常即编译器要求必须处置的异常,其在编译期间抛出,如果我们不处置该类异常,代码将不会通过编译
检查异常 包含:除了Error
和RunTimeException
类及其子类外的所有类
检查异常 通常采用try-catch捕获
或者throws声明抛出
的方式处理
【非检查异常】
非检查异常即不强制要求处置的异常,非检查异常不会在编译期间检查,而是在运行时抛出
非检查异常 包含:Error
和 RunTimeException
类及其子类
检查异常 不应该使用Java的异常处理机制来处置,而应该采用修改代码的方式(因为这些异常的出现大部分是代码本身出错),当然,我们可以使用try-catch
和throws
来处置它们
根据异常发生时机不同,可将Exception
分为:编译时异常 和 运行时异常
-
编译时异常
在程序编译期间发生的异常,称为编译时异常,属于受检查异常(受查异常)。
常见的就是克隆对象时的
CloneNotSupportedException
异常class Obj implements Cloneable { public int a; public String s; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } public class TestException { public static void main(String[] args) { Obj obj = new Obj(); Obj obj1 = (Obj)obj.clone();//红线提示 } }
如图,受查异常会在运行前就有红线提示。(上代码解决方案是在main
方法的参数列表后加上throws CloneNotSupportedException
)
【注意】
不是所有的红线都是受查异常,例如我们将某个关键字写错或忘记写分号,这些都是基本的语法错误,不属于异常。
我们必须手动在代码里添加捕获语句来处理该异常,这类异常是我们主要处理的异常对象。
-
运行时异常
在程序执行期间发生的异常,称为运行时异常,属于非受检查异常(非受查异常)
RunTimeException
及其子类对应的异常,称为运行时异常,如常见的NullPointerException
、ArrayIndexOutBoundsException
等RunTimeException
异常会由Java虚拟机自动抛出并自动捕获,此类异常出现的绝大多数情况是代码本身有问题,我们应该从逻辑上解决这些问题,修改我们的代码。
异常的抛出
这里我们介绍throw
关键字。
前面提到,异常可以是JVM自动抛出的,也可以是程序员通过throw语句
手动抛出的,throw
一般用于抛出自定义异常(后面会讲)
throw
语句用来抛出一个异常类对象,告知调用者相关的错误信息
【语法格式】
保证
throw
后面是异常类对象即可
throw new 异常类名();
异常类名 引用变量名 = new 异常类名();
throw 引用变量名;
我们以抛出非自定义异常为例,看一段代码:
public class TestException1 {
public static void test() {
int[] array = new int[]{1, 2, 3, 4, 5};
System.out.println("请输入下标值:");
Scanner scanner = new Scanner(System.in);
int index = scanner.nextInt();
if(index > 5 || index < 0) {
throw new ArrayIndexOutOfBoundsException();
}else {
System.out.println(array[index]);
}
}
public static void main(String[] args) {
test();
}
}
如上代码,我们输入 7 ,抛出ArrayIndexOutOfBoundsException
异常
构造异常类时可以传入字符串来给出错误提示
public class TestException1 {
public static void test(int x) {
if(x == 0) {
throw new ArithmeticException("将会出现10 / 0的不正确语法!");
}else {
System.out.println(10 / x);
}
}
public static void main(String[] args) {
test(0);//触发if语句手动抛出ArithmaticException异常并打印错误信息
}
}
【注意事项】
throw
语句只能位于方法体内- 如果抛出的异常是非检查异常,则可以不用处理,直接交给JVM处理,一旦交给JVM处理,程序将会终止报错
- 如果抛出的异常是检查异常,则必须处理,否则编译不通过
异常的处理
异常的处理主要有两种方式:
一种是消极的处理方式throws
声明抛出;另一种是try-catch
捕获异常
throws
【语法格式】
修饰符 返回类型 方法名(参数列表)
throws
异常类型1,异常类型2… {}
当方法中抛出检查异常时,用户不想处理该异常,此时就可以使用throws
将异常抛给方法的调用者来处理(当前方法不处理异常,提醒方法的调用者处理该异常)
throws
语句又叫 异常的声明,它告诉调用者调用此方法可能会抛出对应的异常。实际上并没有处理异常,而是交给了此方法的调用者(上层)来处理该异常,如果不断地throws
,异常最终将会交给JVM处理,此时程序就会报错终止。
【举例】
//CloneObject.java
public class CloneObject implements Cloneable {
private int a;
private String s;
public CloneObject(int a, String s) {
this.a = a;
this.s = s;
}
@Override
protected Object clone() throws CloneNotSupportedException { //将CloneNotSupportedException异常交给调用者,此处为main方法
return super.clone();
}
}
//TestException1.java
public class TestException1 {
public static void main(String[] args) throws CloneNotSupportedException { //main方法将异常抛给上层,即JVM
CloneObject cloneObject1 = new CloneObject(12, "haha");
CloneObject cloneObject2 = (CloneObject) cloneObject1.clone();
}
}
【注意事项】
throws
语句必须位于方法的参数列表后throws
后的异常必须是Exception
类及其子类- 如果
throws
语句后面的多个异常存在父子关系,则只需要保留父类即可 throws
语句处理异常的方式是消极的,它并没有实际处理异常,而是不断地将异常抛给其上层调用者,最终会抛给JVM处理
try-catch
throws
没有真正处理异常,而是将异常抛给调用者,由调用者处理。try-catch
则是一种积极的处理异常的方式
【语法格式】
try {
} catch(异常类型 变量名) {
}
//后续代码…
try
内部书写可能出现异常的代码,catch
用于捕捉指定的异常,当try
内部语句出现异常,catch
语句将尝试捕获,如果异常类型匹配,则会捕获成功,执行catch
内部的语句,执行完毕后,继续执行后续的语句;如果捕获失败(异常类型不匹配),程序将会直接终止并报错。
另外,当try
内部出现异常,出现异常的语句之后的语句将不会执行,直接跳转到catch
语句尝试捕获
我们看一段代码:
public static void main(String[] args) {
int[] array = null;
try {
array[0] = 1;
System.out.println("异常语句后的语句...");
}catch (NullPointerException a) {
System.out.println("执行了catch语句...");
}
System.out.println("执行了后续语句...");
}
打印结果为:
分析: 我们创建了一个数组引用变量,它不指向任何对象(null),接着执行try
内部语句,将会出现NullPointerException
异常,此时后面的打印语句不会执行,直接跳转到catch
语句尝试捕获,类型匹配,捕获成功,执行catch
内部语句,最后执行后续语句,得到上图打印结果。
当catch
语句捕获到了异常,我们可能难以定位捕获异常的位置。
此时,我们可以通过printStackTrace
方法打印追踪栈的信息,帮助我们进行定位
public static void main(String[] args) {
int[] array = null;
try {
array[0] = 1;
System.out.println("异常语句后的语句...");
}catch (NullPointerException a) {
System.out.println("执行了catch语句...");
a.printStackTrace();//打印错误信息
}
System.out.println("执行了后续语句...");
}
这里有一个问题,printStackTrace
方法在println
之前,为什么先打印println
的信息?
这是因为,printStackTrace
内部的打印逻辑不同于println
,现阶段不必追究,知道这一现象即可。
try
中可能会抛出多个异常,此时要使用多个catch
来一一捕获,防止某个异常没有被捕获
我们看一段代码:
public static void main(String[] args) {
int[] array = null;
try {
array[0] = 1;
System.out.println(10 / 0);
System.out.println("异常语句之后的语句...");
}catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕获空指针异常的catch语句执行了...");
}catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("捕获算术异常的catch语句执行了...");
}
System.out.println("后续语句...");
}
问: 此时会打印几个异常的错误信息?
打印结果显示,只打印了一个异常的错误信息,原因很简单,当try
中出现了一个异常时,后面的语句将不再执行,也就是上述代码中会出现算术异常的10 / 0
语句不会执行。
对于可能出现多个异常的情景,有两种方便但不推荐的写法:
-
仅存在一个
catch
语句且定义待捕获的类为Exception
,通过一个catch
语句捕获所有的异常public static void main(String[] args) { try { System.out.println("可能出现异常的语句..."); } catch(Exception e) { System.out.println("捕获到了异常..."); } }
Exception
是所有异常(狭义)类的父类,这种写法虽然方便,但是具体抛出了什么异常,我们不清楚,所以不推荐这样写。不过,我们可以在已有的多个
catch
语句的后面再加上一个Exception的catch
语句以兜底。public static void main(String[] args) { int[] array = null; try { array[0] = 1; System.out.println(10 / 0); System.out.println("异常语句之后的语句..."); }catch (NullPointerException e) { e.printStackTrace(); System.out.println("捕获空指针异常的catch语句执行了..."); }catch (ArithmeticException e) { e.printStackTrace(); System.out.println("捕获算术异常的catch语句执行了..."); }catch (Exception e) { e.printStackTrace(); System.out.println("捕获到了异常..."); } System.out.println("后续语句..."); }
-
使用
|
连字符在一个catch
语句定义多个异常public static void main(String[] args) { try { System.out.println("可能出现异常的语句..."); } catch(NullPointerException | ArithmeticException e) { System.out.println("捕获到了异常..."); } System.out.println("后续语句..."); }
与一个
Exception
一样,我们不清楚具体抛出的异常。我们只需要了解可以使用|
的语法即可
如果异常之间具有父子关系,则必须保证子类异常在前面的catch
语句,父类异常在后面的catch
语句
具体实例参考Exception
兜底代码
【总结】
try
块内出现异常位置后的语句不会执行try
中可能会抛出多个异常,此时要使用多个catch
来一一捕获,防止某个异常没有被捕获,避免只使用一个catch
语句捕获所有的异常以及使用|
在一个catch
语句中出现多个异常类- 如果
catch
语句的异常类出现了父子关系,则必须保证子类在上,父类在下,这是强制性的 - 如果抛出异常的类型与
catch
不匹配,异常将会继续向外抛,知道抛给JVM中断程序
finally
编程过程中,有些代码,不论是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库、IO流等,在程序正常或异常退出时,必须对资源进行回收。另外,异常会引发程序的跳转,导致一些语句无法执行。
针对这些问题,Java提供了finally
关键字,它必须依附try-catch
语句,即try-catch-finally
【语法格式】
try {
} catch(异常类型 变量名) {
} finally {
}
finally
块内的语句一定会执行,不论是否发生异常、异常是否被捕获成功
问:finally
语句与try-catch
后面的语句都会执行,为什么还需要finally
呢?
我们看一段代码:
public class TestException1 {
public static int testFinally() {
Scanner scanner = new Scanner(System.in);
try {
int ret = scanner.nextInt();
return ret;
}catch (InputMismatchException e) {
e.printStackTrace();
}finally {
System.out.println("finally语句执行了...");
}
System.out.println("后续语句执行了...");
scanner.close();
return 0;
}
public static void main(String[] args) {
System.out.println(testFinally());
}
}
当输入的类型匹配时,例如输入 10:
此时,我们发现当没有抛出异常,try
块内的代码都会执行,执行返回语句返回 10,同时finally
的语句也会执行,但是后续语句没有执行,因为已经返回了。
当我们输入的类型不匹配时(即抛出了异常),如输入 “str”:
此时抛出异常被catch
捕获,再执行finally
和 后续语句。
通过上例,我们发现,异常引发的程序跳转使得后续代码可能执行,但是finally
代码必定会执行,所以这就是finally
语句出现的意义。
避免在finally
中出现return
语句
public static int testFinallyAndReturn() {
try {
return 10;
}catch (NullPointerException e) {
e.printStackTrace();
}finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(testFinallyAndReturn());
}
这段代码打印结果是什么?
finally
执行的时机是在方法返回之前(如果try-catch
中有return
会在此return
之前执行finally
;但是如果finally
中也存在return
,那么就会执行finally
中的return
,从而不会执行到try-catch
中原有的return
异常的处理流程
如果发生异常的方法中没有合适的异常处理方式,就会沿着调用栈向上(这里的上指的是调用者)传递。
方法之间是存在相互调用关系的,这种调用关系我们可以使用"调用栈"来描述。
JVM中有一块内存空间称为"虚拟机栈"专门存储方法之间的调用关系,前面介绍的
printStackTrace
方法就可以查看出现异常代码的调用栈
如上面举过例子的一段代码:
public static int testFinally() {
Scanner scanner = new Scanner(System.in);
try {
int ret = scanner.nextInt();
return ret;
}catch (InputMismatchException e) {
e.printStackTrace();
}finally {
System.out.println("finally语句执行了...");
}
System.out.println("后续语句执行了...");
scanner.close();
return 0;
}
public static void main(String[] args) {
System.out.println(testFinally());
}
黄框内部显示了出现异常代码的调用栈,最上面是栈顶。当出现异常时,此异常会一直向栈底回溯,这种行为叫做异常的冒泡,如果传递过程中一直没有合适的处理方式,异常最终会交给JVM处理,程序就会异常终止。
【异常处理流程总结】
- 程序先执行
try
中的代码 - 如果
try
中的代码出现异常,就会结束try
中的代码,catch
开始尝试捕获(看异常类型是否匹配) - 如果捕获成功(异常类型匹配),就会执行
catch
中的代码;如果捕获失败(异常类型不匹配),异常就会向上传递到上层调用者,如果一直到main
方法也没有合适的代码处理异常,就会交给JVM处理,此时程序就会异常终止 - 无论是否捕获成功,
finally
代码都会执行
自定义异常类
Java中虽然已经内置了丰富的异常类,但是并不能完全表示实际开发中所遇到的一些异常,此时我们就需要自定义异常类,维护符合我们实际情况的异常结构。
观察Java内置的InputMismatchException
异常源码:
观察框选部分,然后我们介绍规则。
【自定义异常的规则】
- 自定义异常要继承
Exception
或RunTimeException
,继承自Exception
默认为检查异常;继承自RunTimeException
默认为非检查异常 - 按照国际惯例,自定义异常类应该包含4个构造方法:
- 无参构造方法
- 带
String
参数的构造方法,并使用super
传递给父类 - 带
String
参数 和Throwable
参数的构造方法,并使用super
传递给父类 - 带
Throwable
参数的构造方法,并使用super
传递给父类
- 我们自定义异常类时,不必严格遵守国际惯例,只包含前两个无参和
String
参数的构造方法即可
了解完毕后,我们应用自定义异常实现段用户登录代码:
//UserNameException.java
public class UserNameException extends Exception {
public UserNameException() {
}
public UserNameException(String s) {
super(s);
}
}
//PassWordException.java
public class PassWordException extends Exception {
public PassWordException() {
}
public PassWordException(String s) {
super(s);
}
}
//Login.java
import java.util.Scanner;
public class Login {
private String userName;
private String passWord;
public void setUserName(String userName) {
this.userName = userName;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public void loginInfo(String userName, String passWord) throws UserNameException, PassWordException {
if(!this.userName.equals(userName)) {
throw new UserNameException("用户名错误!");
}
if(!this.passWord.equals(passWord)) {
throw new PassWordException("密码错误!");
}
System.out.println("登录成功!");
}
}
class Test {
public static void main(String[] args) {
Login login = new Login();
Scanner scanner = new Scanner(System.in);
login.setUserName("abc");
login.setPassWord("123");
try {
System.out.println("请输入用户名:");
String userName = scanner.nextLine();
System.out.println("请输入密码:");
String passWord = scanner.nextLine();
login.loginInfo(userName, passWord);
}catch (UserNameException e) {
System.out.println("用户名错误!");
e.printStackTrace();
}catch (PassWordException e) {
System.out.println("密码错误!");
e.printStackTrace();
}finally {
scanner.close();
}
}
}
熟悉了异常部分的知识,理解这些代码是容易的,希望大家多多练习!
完
JavaSE全篇马上就发布了!在此之前我们会补充一些知识:内部类、Comparable接口和Comparator接口、Object类