文章目录
一、异常的概念
异常(Exception) 是指程序在运行过程中出现的错误情况,这些错误可能导致程序的正常流程中断。异常可以是由程序自身的逻辑错误引起,也可以是由外部因素(如文件未找到、网络中断等)导致。
二、异常的分类
Error(错误)
**描述:**错误通常指的是由Java虚拟机(JVM)自身引起的严重问题,这些问题往往超出了程序的控制。错误通常无法通过程序代码来恢复,因此是不建议进行捕获和处理的(虽然它们可以写在 catch 后的括号中)。
**示例:**常见的错误有 OutOfMemoryError(内存不足)、StackOverflowError(栈溢出)和 NoClassDefFoundError(类定义未找到)。
**处理建议:**对于错误,通常不需要程序员处理,发生后程序很可能会直接终止。正确的做法是优化代码以避免这些错误的发生。
Exception(异常)
**描述:**异常是程序运行过程中可以被捕获并处理的问题。当程序遇到异常时,可以通过适当的处理机制(如 try-catch 块)来防止程序崩溃。
- 分类:
(一)检查型异常(Checked Exceptions):又名,编译时异常/受查异常
描述:检查型异常是在编译时被检查 的异常。程序员必须在代码中进行处理,否则编译器会报错。它们通常表示外部环境的问题,如文件未找到、网络连接失败等。
**示例:**IOException(输入输出异常)、SQLException(SQL异常)、 ClassNotFoundException(类未找到异常),CloneNotSupportedException(克隆异常)。
处理方式:使用 try-catch 块捕获异常,或在方法签名中使用 throws 声明该异常。
(二)非检查型异常(Unchecked Exceptions):又名,运行时异常
描述:非检查型异常是在运行时发生 的异常,它们通常是由程序的逻辑错误引起的,如数组越界、空指针引用等。编译器不会强制要求程序员处理这些异常。
**示例:**NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组下标越界)、IllegalArgumentException(非法参数异常)。
处理方式:虽然不强制要求捕获,但建议在适当的地方进行处理,以提高程序的健壮性。
异常其实就是一个一个的类,所有的异常都继承于顶级父类Throwable
1.常见的几个运行时异常

(1)NullPointerException
NullPointerException 空指针异常
当应用程序试图使用 null 作为对象的引用时,会抛出 NullPointerException。这通常发生在尝试访问对象的方法或属性时。
java
public class Example {
public static void main(String[] args) {
String str = null;
System.out.println(str.length()); // 尝试使用空引用
}
}
(2)ArithmeticException
ArithmeticException 数学运算异常(算数异常)
当发生算术运算的异常情况时,会抛出 ArithmeticException。最常见的情况是除以零。
例如:
java
public class Example {
public static void main(String[] args) {
int result = 10 / 0;
}
}
(3)ArrayIndexOutOfBoundsException
ArrayIndexOutOfBoundsException 数组下标越界异常
当尝试访问数组中不存在的索引时,会抛出 ArrayIndexOutOfBoundsException。该异常表示访问的索引超出了合法范围(0到数组长度-1)。
java
public class Example {
public static void main(String[] args) {
int[] arr = new int[5];
System.out.println(arr[-1]);
}
}
(4)ClassCastException
ClassCastException 类型转换异常
当尝试将对象强制转换为不兼容的类型时,会抛出 ClassCastException。这通常发生在使用类型转换时。
java
public class Example {
public static void main(String[] args) {
Object obj = new String();
Integer num = (Integer)obj;
}
}
(5)NumberFormatException
NumberFormatException 数字格式不正确异常
当尝试将字符串转换为数字格式时,如果字符串的格式不符合数字的要求,就会抛出 NumberFormatException。例如,尝试将一个非数字字符串转换为整数。
例如:
java
public class Example {
public static void main(String[] args) {
String str = "abc";
int num = Integer.parseInt(str);
}
}
2.常见的几个编译时异常
1、SQLException
SQLException 是在与数据库交互时可能发生的异常。它可以指示由于数据库访问错误或其他数据库问题(如查询语法错误、连接失败等)而导致的错误。
2、IOException
IOException 是与输入/输出操作相关的异常,例如文件读取或网络通信时出错。它是处理文件和流操作时的常见异常。
3、FileNotFoundException
FileNotFoundException 是 IOException 的一个子类,在尝试打开一个不存在的文件时抛出。它通常在文件操作时进行检查。
4、ClassNotFoundException
ClassNotFoundException 是当应用程序尝试通过字符串名称加载类,但没有找到对应类时抛出的异常。这通常发生在使用反射或动态加载类时。
5、EOFException
EOFException 是在读取文件时遇到文件末尾(End of File)而未能成功读取数据时抛出的异常。通常在使用数据输入流(DataInputStream)时会遇到此异常。
6、CloneNotSupportedException
一个类想要调用Object类的clone()方法实现克隆,但没有实现Cloneable接口(Cloneable是一个标记接口,没有任何方法,仅用于标识该类支持克隆)。
三、处理异常
1.防御式编程
错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时了解到我们是哪里出现了错误,因此我们得到了两个得到错误位置的方式。
🥏(1)事前防御型(LBYL)

这种方式我们其实会发现一些缺陷,我们的正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱。
(2)事后认错型(EAFP)

当然对于事后认错型我们发现我们的代码中使用了两个关键字:try和catch.
然而,对于事后认错型,我们有五个主要的关键字:throw、try、catch、final、throws。
2.异常的抛出
java
语法
throw new XXXException("异常产生的原因");

注意
- throw必须写在方法体内部
- 抛出的对象必须是Exception 或者 Exception 的子类对象
- 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
- 异常一旦抛出,其后的代码就不会执行
3.异常的捕获
既然我们已经得到了信息接下来就是要捕获并处理他,而异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理。
(1)异常声明throws
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。
语法:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{}

而我们知道了异常的声明方式,我们就可以成功的解决这个问题了

注意事项
1、throws必须跟在方法的参数列表之后

2、声明的异常必须是 Exception 或者 Exception 的子类

3、调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出


4、方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型 具有父子关系,直接声明父类即可。
多个异常类型

具有父子关系

我们要注意如果多种类型具有父子类关系,再次抛出只需要声明父类


(2)try-catch捕获并处理
throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。
语法:
try{
// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时, 或者是try中抛出异常的基类 时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}catch(要捕获的异常类型 e){
// 对异常进行处理
}finally{
// 此处代码一定会被执行到
}
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执

这就是我们对异常的处理,但是我们发现原本编译器其实会告诉我们异常的信息,当现在我们并不知道异常是什么,那么我们该怎样得到异常信息呢?其实很简单只需要我们在catch处加上异常信息的打印即可。

当然由于异常的种类有很多, 因此我们要根据不同的业务场景来决定我们是否使用异常处理
对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果
对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿
对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试
1、try-catch 语句
try-catch语句用于捕捉和处理在try块中可能出现的异常。其基本语法结构如下:
java
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 异常处理代码
}
例如:
java
try {
int result = 10 / 0; // 可能抛出 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("ArithmeticException: " + e.getMessage());
}
注意 :
1. try块内抛出异常位置之后的代码将不会被执行
2. 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序----异常是按照类型来捕获的
2、try-catch-finally 语句
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
语法:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
java
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 异常处理代码
} finally {
// 无论是否发生异常,都会执行的代码
}
3、多个 catch 结构
1)介绍
多个catch结构用于处理try块中可能抛出的不同类型的异常。当一个try块中的代码可能抛出多种异常时,可以使用多个catch块来分别捕获和处理这些异常。这样可以提高程序的健壮性和可维护性。
多个catch结构的基本语法如下:
java
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e1) {
// 处理 ExceptionType1 异常
} catch (ExceptionType2 e2) {
// 处理 ExceptionType2 异常
} catch (ExceptionType3 e3) {
// 处理 ExceptionType3 异常
}
// 可选的 finally 块
finally {
// 清理操作,无论是否发生异常都会执行
}


但是注意我们是不能够捕获到多个异常的

需要注意的是,在多个catch块中,子类异常必须放在父类异常之前。例如,如果同时有IOException和Exception,则IOException应该放在前面。否则,Exception会捕获所有异常,导致子类IOException的catch块无法被执行。
可以使用一个通用的异常捕获块(如Exception)来捕获所有未被具体捕获的异常,通常放在最后的 catch 语句块中。
如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误,我们知道NullPointerException是Exception的子类所以当为空指针异常时我们可以写成这样:

但是如果我们想要这两异常都存在,我们一定要让子类异常在前catch,父类异常在后catch


可以通过一个catch捕获所有的异常,即多个异常,一次捕获(不推荐)
当然对于上述这种方式我们是不推荐的

4、一个 catch 块捕获多个异常
如果多个异常的处理方式是完全相同, 对于这个代码我们是可以进行简化的:
但是我们并不是很建议这样来写,因为我们并不能明确的知道到底是什么导致的异常

从Java 7开始,可以在一个catch块中捕获多个异常,使用管道符号**|**分隔多个异常类型。这对于处理多个异常类型时执行相同的处理逻辑非常有用。
java
try {
// 可能抛出异常的代码
} catch (IOException | NullPointerException e) {
// 处理 IOException 或 NullPointerException
System.out.println("Exception: " + e.getMessage());
}
5、try-finally 语句
这种方式只使用了 try 和 finally 块,没有 catch 块用来捕获和处理异常。
这通常用于清理资源,例如关闭文件、数据库连接等。
java
try {
// 可能抛出异常的代码
} finally {
// 无论是否发生异常,都会执行的代码
}
但是我们发现 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?
这种写可以确保如果异常发生,可以执行 finally 语句块中的语句,执行完 finally 块中的语句后,程序就因异常结束了,后面的外部语句就不会被执行了。
例如:
java
public class Example {
public static void main(String[] args) {
try {
int res = 1 / 0;
} finally {
System.out.println("finally 语句块");
}
System.out.println("外部语句");
}
}
运行结果:

可以看到因为异常没有被捕获,所以只执行了 finally 语句块后程序就因为异常而崩溃了。
6、补充
1)可能出现异常的语句要放到 try 块中
只有放到 try 语句块中的代码抛出的异常才能被 catch 语句块捕获到。
java
public class Example {
public static void main(String[] args) {
int res = 2 / 0; // 这里抛出的异常不会被捕获
try {
} catch(ArithmeticException e) {
System.out.println(e.getMessage());
}
}
}

这样直接执行的结果是程序结束,异常没有被捕获:
2)catch 捕获异常后,就会跳入到 catch 块中执行
在 try 块中的某一句代码抛出异常后,该异常被 catch 捕获,则会直接跳入 catch 语句块中执行。try 块的剩余语句不会再执行了。
java
public class Example {
public static void main(String[] args) {
try {
int res = 1 / 0;
System.out.println("..."); // 这一句不会执行
} catch(ArithmeticException e) {
System.out.println(e.getMessage());
}
}
}
可以发现 try 块中剩余的语句没有被执行。
3)catch 不会被执行的情况
没有发生异常或者没有捕获到异常,catch 块都不会被执行。
java
public class Example {
public static void main(String[] args) {
try {
String str = "1";
int num = Integer.parseInt(str);
System.out.println(num);
} catch (NumberFormatException e) {
System.out.println("异常发生:" + e.getMessage());
}
}
}
可以看到异常没有发生,则 catch 块中的语句没有被执行。
4)finally 语句块无论是否有异常都会被执行
finally 语句块中的代码无论是否发生异常,都会执行,通常用于清理资源,如关闭文件或数据库连接。
java
import java.util.Scanner;
public class Example {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
String str = input.next();
try {
int num = Integer.parseInt(str);
System.out.println(num);
} catch (NumberFormatException e) {
System.out.println("异常发生:" + e.getMessage());
} finally {
System.out.println("finally 代码块...");
}
}
}
可以看到无论是否发生异常,finally 代码块都会执行。
5)与 finally 块有关的题目
由于 finally 块无论是否发生异常都须被执行。
java
public class Example {
public static int method() {
try {
String name = null;
name.length();
} catch (NullPointerException e) {
return 1;
} finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(method());
}
}
运行结果:2
由于 finally 块无论是否发生异常都会被执行,所以在 catch 块中不会返回,到了 finally 块中才会返回,所以最终 method() 方法返回值为 2。
4.异常的处理流程
如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递,最后就会交给JVM,导致程序终止。

总结
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
- 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.
受查异常必须处理

在main里面调用function,如果throws声明了异常(先thorw抛出异常用catch捕获异常),那就得在main里用try-catch处理这个异常,不处理得话也可以在main后面跟一个一样的声明,这样的话就交给了jvm处理,交给jvm直接中止程序
抛出了异常才会捕获到,即只有try里调用方法的时候thorw抛出异常的时候才会进入catch捕获异常(catch的参数列表是抛出的异常那个类的对象)
通过--变量.printStackTrace()打印错误信息
5.自定义异常
Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构.
在 Java 中,定义一个自定义异常(Custom Exception)的本质就是定义一个类。
但是,这个类必须遵循一个关键的继承规则
自定义异常可以继承两个主要基类之一


是否可以声明

当然,非受查异常也是可以声明的,所以我们一般在写代码的时候尽量写声明,这样能有效知道我们都有那些异常类型
注意
继承
java.lang.Exception:
- 创建的是受检查异常(Checked Exception)。
- 用于表示程序可以预料和恢复的业务错误(例如:文件不存在、网络连接超时、账户余额不足)。
继承
java.lang.RuntimeException:
- 创建的是不受检查异常(Unchecked Exception)。
- 用于表示程序逻辑错误或不可恢复的错误(例如:参数无效、索引越界)。
不能任何异常都用一个Exception解决,一开始就被Exception捕获了后面就没意义了

不是所有异常都必须手动捕获。 只有受检查异常(Checked Exceptions) 必须手动处理或声明抛出;而不受检查异常(Unchecked Exceptions) 可以选择性地处理
只有受检查异常 要求您进行手动捕获(try-catch)或声明抛出(throws)。不受检查异常 (RuntimeException 及其子类)不需要强制手动捕获。

