异常

异常

Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • **Error**:Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Exception 分为两大类

  • 编译时期异常: checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。

  • 常见的编译异常有:

    • SQLException //操作数据库时,查询表可能发生异常
    • IOException //操作文件时,发生的异常
    • FileNotFoundException //当操作一个不存在的文件时,发生异常
    • ClassNotFoundException //加载类,而该类不存在时,异常
    • EOFException // 操作文件,到文件未尾,发生异常
    • ParseException //日期格式化异常
  • 运行时期异常: runtime异常(uncheck异常)。在运行时期,检查异常,在编译时期,运行异常不会编译器检测(不报错)。

  • RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

    • NullPointerException(空指针错误)
    • IllegalArgumentException(参数错误比如方法入参类型错误)
    • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
    • ArrayIndexOutOfBoundsException(数组越界错误)
    • ClassCastException(类型转换错误)
    • ArithmeticException(算术错误)
    • SecurityException (安全错误比如权限不够)
    • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

注意事项

  • 对于编译异常,程序中必须处理,比如 try-catch 或者 throws
  • 对于运行时异常,程序中如果没有处理,默认就是throws的方式处理
  • 在throws 过程中,如果有方法 try-catch,就相当于处理异常,就可以不必throws

image-20240303160129909

异常处理的两种方式

  1. try-catch-finally程序员在代码中捕获发生的异常,自行处理

  2. throws将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是JVM

捕获异常try…catch…finally

异常处理的第一种方式,自己处理异常

  • 如果异常发生了,则异常发生后面的代码不会执行,直接进入到catch块
  • 如果异常没有发生,则顺序执行try的代码块,不会进入到catch
  • 如果希望不管是否发生异常,都执行某段代码(比如关闭连接,释放资源等),则使用finally
  • try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
  • 执行完try…catch后,后续代码继续执行
image-20240303161538145

Throwable 类常用方法

String getMessage(): 返回异常发生时的简要描述

String toString(): 返回异常发生时的详细信息

String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同

void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
try{
readFile("d:\\a.tx");//可能产生异常的代码
System.out.println("资源释放"); //如果捕获到异常,该代码不执行,直接进入catch

}catch(IOException e){
//try中抛出什么异常对象,catch就定义什么异常变量,用来接收这个异常对象
//异常的处理逻辑,异常异常对象之后,怎么处理异常对象
//System.out.println("catch - 传递的文件后缀不是.txt");
//System.out.println(e.getMessage());//文件的后缀名不对
//System.out.println(e.toString());//重写Object类的toString java.io.IOException: 文件的后缀名不对
System.out.println(e);//java.io.IOException: 文件的后缀名不对
e.printStackTrace();
/*
java.io.IOException: 文件的后缀名不对
at com.itheima.demo02.Exception.Demo01TryCatch.readFile(Demo01TryCatch.java:55)
at com.itheima.demo02.Exception.Demo01TryCatch.main(Demo01TryCatch.java:27)
*/

}finally{
System.out.println("资源释放");//无论是否出现异常,都会执行
}
System.out.println("后续代码");//继续执行
}

一次捕获多次处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
int[] arr = {1,2,3};
System.out.println(arr[3]);
//ArrayIndexOutOfBoundsException: 3

List<Integer> list = List.of(1, 2, 3);
System.out.println(list.get(3));
//IndexOutOfBoundsException: Index 3 out-of-bounds for length 3

}catch (ArrayIndexOutOfBoundsException e){
System.out.println(e);
}catch (IndexOutOfBoundsException e){
System.out.println(e);
}

注意事项:catch里边定义的异常变量,如果有子父类关系,那么子类的异常变量必须写在上边,否则就会报错ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException。

多个异常一次处理

1
2
3
4
5
6
7
8
9
10
11
12
try {
int[] arr = {1,2,3};
System.out.println(arr[3]);
//ArrayIndexOutOfBoundsException: 3

List<Integer> list = List.of(1, 2, 3);
System.out.println(list.get(3));
//IndexOutOfBoundsException: Index 3 out-of-bounds for length 3

}catch (Exception e){
System.out.println(e);
}

注意:不管是多个异常一次处理还是一次捕获多次处理,当捕获异常后程序会跳转到与抛出的异常类型匹配的 catch 块。

声明异常throws

异常处理的第二种方式,交给调用者处理,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),最终交给JVM处理–>中断处理

  • 如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理

  • 在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

  • 如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可

  • 方法内部如果抛出了多个异常对象,那么throws后边必须也声明多个异常

  • throws关键字后边声明的异常必须是Exception或者是Exception的子类

  • 注意事项

    • 对于编译异常,程序中必须处理,比如 try-catch 或者 throws
    • 对于运行时异常,程序中如果没有处理,默认就是throws的方式处理
    • 在throws 过程中,如果有方法 try-catch,就相当于处理异常,就可以不必throws
1
2
3
4
5
6
7
public static void f1() /*throws ArithmeticException*/{
//1.对于编译异常,程序中必须处理,比如 try-catch 或者 throws
//2.对于运行时异常,程序中如果没有处理,默认就是 throws 的方式处理
int nl = 10;
int n2 = 0;
double res = n1 / n2:
}

编译异常必须处理,要么try-catch 要么 throws

1
2
3
4
public static void readFile(String file) throws FileNotFoundException{
//读文件的操作可能产生FileNotFoundException类型的异常
FilelnputStream fis = new FilelnputStream("d://aa.txt")
}

子父类的异常

  • 如果父类抛出了多个异常,子类重写父类方法时,可以抛出和父类相同的异常,或者抛出父类异常的子类,或者不抛出异常。
  • 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
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
//父类
public class Fu {
public void show01() throws NullPointerException,ClassCastException{}
public void show02() throws IndexOutOfBoundsException{}
public void show03() throws IndexOutOfBoundsException{}
public void show04() {}
}
//子类
class Zi extends Fu{
//子类重写父类方法时,抛出和父类相同的异常
public void show01() throws NullPointerException,ClassCastException{}
//子类重写父类方法时,抛出父类异常的子类
public void show02() throws ArrayIndexOutOfBoundsException{}
//子类重写父类方法时,不抛出异常
public void show03() {}


/*
父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。
*/
//此时子类产生该异常,只能捕获处理,不能声明抛出
public void show04() {
try {
throw new Exception("编译期异常");
} catch (Exception e) {
e.printStackTrace();
}
}
}

throw关键字

可以使用throw关键字在指定的方法内部抛出指定的异常

抛出异常的过程通常用于表示出现了错误或异常情况,并将异常传递给调用者或上层代码进行处理。

  • throw关键字必须写在方法的内部
  • throw关键字后边new的对象必须是Exception或者Exception的子类对象
  • throw关键字后边创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理,默认交给JVM处理(打印异常对象,中断程序)
  • throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么try…catch
  • throw关键字抛出指定的异常对象,我们就必须处理这个异常对象
1
2
3
4
5
6
7
8
9
10
11
12
13
  public static void readFile(String fileName) throws Exception{
if(!fileName.equals("c:\\a.txt")){
throw new FileNotFoundException("传递的文件路径不是c:\\a.txt");
}
/*
如果传递的路径,不是.txt结尾
那么我们就抛出IO异常对象,告知方法的调用者,文件的后缀名不对
*/
if(!fileName.endsWith(".txt")){
throw new IOException("文件的后缀名不对");
}
System.out.println("路径没有问题,读取文件");
}

编译型异常,必须自己处理,此处使用throws交给调用者处理

FileNotFoundException extends IOException extends Excepiton,如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static int getElement(int[] arr,int index){

/*
我们可以对传递过来的参数数组,进行合法性校验
如果数组arr的值是null
那么我们就抛出空指针异常,告知方法的调用者"传递的数组的值是null"
*/
if(arr == null){
throw new NullPointerException("传递的数组的值是null");
}

/*
我们可以对传递过来的参数index进行合法性校验
如果index的范围不在数组的索引范围内
那么我们就抛出数组索引越界异常,告知方法的调用者"传递的索引超出了数组的使用范围"
*/
if(index<0 || index>arr.length-1){
throw new ArrayIndexOutOfBoundsException("传递的索引超出了数组的使用范围");
}

int ele = arr[index];
return ele;
}

NullPointerException是一个运行期异常,我们不用处理,默认交给JVM处理

ArrayIndexOutOfBoundsException是一个运行期异常,我们不用处理,默认交给JVM处理

自定义异常

当程序中出现了某些“错误”,但该错误信息并没有在Throwable子类中描述处理,这个时候可以自己设计异常类,用于描述该错误信息。

注意:

  • 自定义异常类一般都是以Exception结尾,说明该类是一个异常类
  • 自定义异常类,必须的继承Exception或者RuntimeException
  • 继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try…catch
  • 继承RuntimeException:那么自定义的异常类就是一个运行期异常,无需处理,交给虚拟机处理(中断处理)

自定义异常的练习

要求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

首先定义一个登陆异常类RegisterException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RegisterException extends /*Exception*/ RuntimeException{
//添加一个空参数的构造方法
public RegisterException(){
super();
}
/*
添加一个带异常信息的构造方法
查看源码发现,所有的异常类都会有一个带异常信息的构造方法,
方法内部会调用父类带异常信息的构造方法,
让父类来处理这个异常信息
*/
public RegisterException(String message){
super(message);
}
}

Exception源码

image-20240303163828965

测试

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
public class Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};

public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(RegisterException e){
//处理异常
e.printStackTrace();
}
}

//判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new RegisterException("亲"+name+"已经被注册了!");
}
}
return true;
}
}

finally 中的代码一定会执行吗?

不一定的,在某些情况下,finally 中的代码不会被执行。

就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。

1
2
3
4
5
6
7
8
9
10
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
// 终止当前正在运行的Java虚拟机
System.exit(1);
} finally {
System.out.println("Finally");
}

输出:

1
2
Try to do something
Catch Exception -> RuntimeException

另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:

  1. 程序所在的线程死亡。
  2. 关闭 CPU。

如何使用 try-with-resources 代替try-catch-finally?

try-with-resources 是 Java 7 引入的一种自动资源管理机制,主要用于简化对实现了 AutoCloseableCloseable 接口的资源的处理。它通过在 try 语句中声明资源,自动确保在语句结束时资源被正确关闭,不再需要显式地在 finally 块中关闭资源。

  1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
  2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

《Effective Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,

Java 中类似于InputStreamOutputStreamScannerPrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}

使用 Java 7 之后的 try-with-resources 语句改造上面的代码

1
2
3
4
5
6
7
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}

异常
http://example.com/2023/06/26/Java/Java进阶/异常/
作者
PALE13
发布于
2023年6月26日
许可协议