异常(Exception)
异常概述
什么是程序的异常
在 Java 中,异常是指程序在运行过程中发生的非正常情况,它会中断程序的正常执行流程,例如:除零、数组越界、空指针访问、文件不存在等。
异常的抛出机制
Java中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出(throw)。然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获这个异常对象,那么这个异常对象将会导致程序终止。
对待异常
提前预防、及时处理、避免程序崩溃
Java异常体系
概览
Java 中所有异常和错误都继承自java.lang.Throwable。
Throwable├──Error(严重错误,程序通常无法处理,非受检异常) │ ├──VirtualMachineError│ │ ├──OutOfMemoryError│ │ └──StackOverflowError│ ├──LinkageError│ └──AssertionError│ └──Exception(程序可处理的异常) ├──RuntimeException(非受检异常) │ ├──NullPointerException│ ├──ArithmeticException│ ├──ClassCastException│ ├──IndexOutOfBoundsException│ ├──ArrayIndexOutOfBoundsException│ ├──StringIndexOutOfBoundsException│ └──IllegalArgumentException│ └──NumberFormatException│ └── ├──IOException│ ├──FileNotFoundException│ └──EOFException│ └──...├──SQLException├──ClassNotFoundException├──InterruptedException└──ParseExceptionThrowable
表示所有可以被抛出和捕获的异常或错误,只有Throwable及其子类才能被throw,只有Throwable才能被catch。
常用方法
| 方法 | 说明 |
|---|---|
getMessage() | 返回异常的详细信息 |
toString() | 返回异常类名 + 信息 |
printStackTrace() | 打印异常堆栈信息(常用) |
getCause() | 获取引发该异常的原因 |
fillInStackTrace() | 记录堆栈信息 |
getStackTrace() | 获取堆栈元素数组 |
Error 和 Exception
Throwable 可分为两类:Error 和 Exception。分别对应着java.lang.Error与java.lang.Exception两个类。
Error:JVM无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
- 例如:StackOverflowError(栈内存溢出)和 OutOfMemoryError(堆内存溢出,简称OOM)。
Exception:其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。
- 例如:空指针访问、试图读取不存在的文件、网络连接中断、数组角标越界等
编译时异常和运行时异常
Java程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。
根据异常可能出现的阶段,可以将异常分为:
- 编译时期异常(即checked异常、受检异常):在代码编译阶段,编译器就能明确警示当前代码可能发生(不是一定发生)xxx异常,并明确督促程序员提前编写处理它的代码。如果程序员没有编写对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异常)。
- 运行时期异常(即runtime异常、unchecked异常、非受检异常):在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xxx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。
- java.lang.RuntimeException类及它的子类都是运行时异常。比如:ArrayIndexOutOfBoundsException 数组下标越界异常,ClassCastException 类型转换异常。
常见的错误和异常
Error
最常见的就是VirtualMachineError,其两个经典的子类:StackOverflowError、OutOfMemoryError。
方法调用层级过深(通常是无限递归),每次调用都会在线程栈中压入栈帧,最终栈空间耗尽。
publicclassStackOverflowDemo{publicstaticvoidmain(String[]args){recursive();}publicstaticvoidrecursive(){recursive();// 没有终止条件}}内存溢出,JVM 某一块内存区域(最常见是堆内存)被耗尽,无法再分配对象。不断创建对象并保存引用,GC 无法回收。
importjava.util.ArrayList;importjava.util.List;publicclassOOMDemo{publicstaticvoidmain(String[]args){List<byte[]>list=newArrayList<>();while(true){list.add(newbyte[1024*1024]);// 每次分配 1MB}}}运行时异常
空指针异常
Stringstr=null;System.out.println(str.length());// NullPointerException数组越界
int[]arr=newint[5];for(inti=1;i<=5;i++){System.out.println(arr[i]);// ArrayIndexOutOfBoundsException}编译时异常
ClassNotFoundException
当 JVM 通过类名动态加载类时(如反射),如果在 classpath 中找不到该类,就会抛出此异常。虽然发生在运行时,但它是编译时异常。
publicclassTest{publicstaticvoidmain(String[]args){try{Class.forName("com.example.NotExistClass");}catch(ClassNotFoundExceptione){e.printStackTrace();}}}FileNotFoundException
当尝试打开一个不存在的文件时抛出
importjava.io.FileInputStream;importjava.io.FileNotFoundException;publicclassTest{publicstaticvoidmain(String[]args){try{FileInputStreamfis=newFileInputStream("a.txt");}catch(FileNotFoundExceptione){e.printStackTrace();}}}IOException
表示在读写数据过程中发生的异常
importjava.io.FileInputStream;importjava.io.IOException;publicclassTest{publicstaticvoidmain(String[]args){try{FileInputStreamfis=newFileInputStream("a.txt");intdata=fis.read();fis.close();}catch(IOExceptione){e.printStackTrace();}}}异常的处理
概述
Java 进行异常处理的主要目的是:提高程序的健壮性、可维护性和安全性。
具体体现在:
- 防止程序异常终止
程序在运行过程中可能出现各种错误(如数组越界、空指针、文件不存在等),如果不处理异常,程序会直接崩溃。
- 将正常逻辑与错误处理逻辑分离
异常处理机制使代码结构更清晰,便于阅读和维护。
- 提供统一的错误处理机制
通过异常类型和异常链,开发者可以精确定位错误原因。
- 增强程序的容错能力
允许程序在发生错误时采取补救措施,而不是直接终止。
Java 异常处理方式
使用try-catch-finally进行捕获处理
- Java程序的执行过程中如果出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为
抛出(throw)异常。 - 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为
捕获(catch)异常。 - 如果一个异常回到
main()方法,并且main()也不处理,则程序运行终止。
基本格式
try{...// 可能产生异常的代码}catch(异常类型1e){...// 当产生异常类型1型异常时的处置措施}catch(异常类型2e){...// 当产生异常类型2型异常时的处置措施}[...]finally{...// 无论是否发生异常,都无条件执行的语句}执行过程
在 Java 中,对于可能在执行过程中抛出异常的代码,无论该异常是受检异常还是非受检异常,都可以使用try语句块对其进行包裹,并在其后定义一个或多个catch子句,用于捕获并处理特定类型的异常对象。
如果程序在运行过程中,
try语句块中的所有代码均正常执行且未抛出任何异常,那么所有catch子句都不会被执行;程序的执行流程将直接跳过整个try-catch结构,继续向下执行其后的代码。如果程序在运行过程中,
try语句块中的某条语句抛出了异常对象,则:JVM 会立即终止
try块中当前异常语句之后的代码执行;JVM 会按照
catch子句的书写顺序(自上而下),依次判断异常对象的实际类型是否与catch中声明的异常类型匹配(包括父类匹配);⚠️若异常类型有父子关系,必须保证子异常类型在上,父异常类型在下;一旦找到第一个能够匹配该异常类型的
catch子句,便执行该catch中的异常处理代码;执行完匹配的
catch子句后,程序将继续执行整个try-catch结构之后的代码。
如果程序在运行过程中,
try语句块中抛出了异常对象,但:所有catch子句中声明的异常类型都无法匹配该异常对象,则:- JVM 将认为该异常在当前方法中未被处理;
- 当前方法的执行将被立即终止;
- 该异常对象会被 JVM 沿着方法调用栈向上抛出(异常传播),交由该方法的调用者处理;
- 如果调用者也未对该异常进行处理,则异常会继续向上传播;
- 若最终传播至虚拟机顶层仍未被捕获,JVM 将执行默认异常处理机制,导致程序异常终止(即程序“挂掉”)。
有关 finally
在 Java 的异常处理机制中,由于异常的发生会导致程序控制流发生转移,某些语句可能无法被正常执行。然而,在实际开发中,往往存在一些无论是否发生异常都必须执行的清理或释放操作,例如:关闭数据库连接、释放 I/O 流资源、断开 Socket 连接、释放锁(Lock)等。此类代码通常应放置在finally代码块中,以确保其执行。
需要注意的是,finally代码块在绝大多数情况下都会被执行,唯一的例外是显式调用System.exit(0)终止当前正在运行的 Java 虚拟机,此时程序会立即结束,finally块将不会得到执行。
无论try代码块中是否抛出异常,catch语句是否被执行,catch语句中是否再次抛出异常,或者catch语句中是否包含return语句,finally代码块中的语句都会在方法结束前被执行。
在语法结构上,catch语句和finally语句都是可选的,但finally不能单独使用,必须与try语句块配合出现。
声明抛出异常类型throws
- 核心意义:当前方法不处理异常,而是把异常抛给调用者处理。
基本格式
方法声明中使用 throws
访问修饰符 返回值类型 方法名(参数列表)throws异常类型1,异常类型2,...{// 方法体}调用该方法的代码必须try-catch捕获异常或继续使用throws向上抛出
使用举例
1、编译时异常
importjava.io.FileReader;importjava.io.IOException;publicclassTest{publicstaticvoidreadFile()throwsIOException{FileReaderfr=newFileReader("a.txt");fr.read();fr.close();}}publicstaticvoidmain(String[]args){try{readFile();}catch(IOExceptione){e.printStackTrace();}}- 不写
throws IOException→ 编译报错,编译器强制处理
2、运行时异常
throws 后面也可以写运行时异常类型,只是写或不写对于编译器和程序执行来说都没有任何区别。如果写了,唯一的区别就是调用者调用该方法后,使用 try…catch 结构时,IDEA 可以获得更多的信息,需要添加哪种 catch 分支。
publicclassTest{publicstaticvoiddivide(inta,intb)throwsArithmeticException{System.out.println(a/b);}}publicstaticvoidmain(String[]args){divide(10,0);// 运行时报异常}throws可写可不写、编译不会报错、异常在运行时抛出
💡方法重写(override)中 throws 的要求
核心规则:子类方法抛出的异常不能“更大”,对于“throws 运行时异常”没有要求。
- 子类可以不抛异常
- 子类可以抛父类方法异常的子类
- 子类不能抛父类方法没有声明的受检异常
- 子类不能抛比父类更大的异常
classFather{publicvoidtest()throwsIOException{}}classSonextendsFather{@Overridepublicvoidtest()throwsFileNotFoundException{}}classSonextendsFather{@Overridepublicvoidtest(){}}classSonextendsFather{@Overridepublicvoidtest()throwsException{// 编译错误}}💡接口中的 throws 规则
如果接口方法声明了异常,实现类:
- 可以不抛异常
- 只能抛接口方法声明异常的子类
- 不能抛接口未声明的受检异常
publicinterfaceA{voidmethod()throwsIOException;}classImplimplementsA{@Overridepublicvoidmethod()throwsFileNotFoundException{}}classImplimplementsA{@Overridepublicvoidmethod(){}}classImplimplementsA{@Overridepublicvoidmethod()throwsException{// 编译错误}}如何选择?
编译期异常处理策略的选择原则
编译期异常(Checked Exception),应根据具体的业务场景和代码结构,选择恰当的异常处理方式,主要包括try-catch-finally与throws两种机制。其选型原则如下:
涉及系统资源的操作必须就地处理异常
当程序中涉及对系统资源的访问与使用(如 I/O 流、数据库连接、网络连接等)时,应在当前方法中使用try-catch-finally(或 try-with-resources)进行异常处理,以确保资源在异常或正常执行路径下都能够被正确释放,避免资源泄漏问题。
方法重写受父类异常声明约束
若父类方法在声明时未通过
throws抛出异常,则子类在重写该方法时,不允许声明新的编译期异常。此时,子类方法中若发生异常,必须通过try-catch-finally方式在方法内部进行处理,而不能继续向上抛出。分层调用场景下的异常传递策略
在实际开发中,若方法
a依次调用方法b、c、d,且b、c、d之间存在明确的业务递进或逻辑依赖关系,通常采用以下异常处理策略:- 在底层方法(如
b、c、d)中,通过throws将异常向上抛出,使异常信息得以完整传递; - 在上层方法(如
a)中,统一使用try-catch-finally对异常进行集中处理,以便进行日志记录、事务回滚、异常封装或用户提示等操作。
- 在底层方法(如
手动抛出异常对象throw
Java 中异常对象的生成方式(两种)
1、JVM 自动生成并抛出异常
当程序在运行过程中出现不合法操作时,JVM 会自动创建异常对象并抛出。
2、程序员手动生成并抛出异常(throw)
当业务逻辑中发现不满足条件的情况,可以主动创建异常对象并抛出。
使用格式
thrownew异常类名(参数);throw语句抛出的异常对象,和 JVM 自动创建和抛出的异常对象一样。
如果是编译时异常类型的对象,必须在方法上声明
throws或者在方法中使用try-catch捕获,否则编译错误。如果是运行时异常类型的对象,可以直接抛出,不要求在方法签名中用
throws声明。可以抛出的异常必须是Throwable或其子类的实例。否则编译错误。
使用注意
无论是编译时异常还是运行时异常,如果在程序执行过程中未被try...catch结构合理捕获并处理,异常将沿着调用栈向上传播,最终可能导致当前线程终止,程序异常结束。
throw语句用于显式抛出一个异常对象,它会立即改变程序的正常执行流程。一旦执行到throw语句,当前代码块中位于其后的语句将不再被执行,例如:
thrownewRuntimeException("错误");System.out.println("这行不会执行");当某个方法内部通过throw抛出了异常,而该方法自身并未使用try...catch对异常进行处理时,throw的行为等价于提前结束方法执行。与return不同的是,throw并非返回一个普通结果,而是将异常对象抛给方法的调用者,由调用者决定是否捕获和处理该异常。
在实际开发中,throw通常与条件判断语句配合使用,用于在程序检测到不符合业务或逻辑约束的情况时,主动中断当前执行流程并报告错误,例如:
if(user==null){thrownewNullPointerException("用户不能为空");}此外,开发者可以通过继承Exception或其子类来自定义异常类型,以更准确地表达业务语义。自定义异常可以通过throw语句抛出,并在方法签名中使用throws声明该异常的传播行为,例如:
classMyExceptionextendsException{publicMyException(Stringmessage){super(message);}}voidtest()throwsMyException{thrownewMyException("自定义异常");}这种机制有助于提高程序的健壮性、可读性以及异常处理的规范性,使异常信息更符合实际业务场景。
throws 与 throw
throws:在方法声明处声明可能抛出的异常
throw:在方法体内主动的(显示的、手动)抛出异常
自定义异常
意义
1️⃣ 提高代码可读性和语义表达
使用系统异常(如RuntimeException、Exception)往往语义不明确:
thrownewRuntimeException("error");自定义异常可以清楚表达业务含义:
thrownewUserNotFoundException("用户不存在");👉 一看异常名就知道问题是什么
2️⃣ 区分业务异常与系统异常
- 系统异常:空指针、数组越界、IO 异常等(JDK 已提供)
- 业务异常:余额不足、订单不存在、权限不足等(需自定义)
3️⃣ 统一异常处理
在 Spring / Web 项目中,自定义异常便于:
- 统一捕获
- 统一返回错误码和错误信息
- 统一日志处理
4️⃣ 方便异常扩展
后期可以给异常加上:
- 错误码
- 业务状态
- 额外上下文信息
如何自定义异常类
- 继承一个异常类型
自定义一个编译时异常类型:自定义类继承java.lang.Exception
自定义一个运行时异常类型:自定义类继承java.lang.RuntimeException
推荐提供至少两个构造器,一个是无参构造,一个是(String message)构造器。推荐增加错误码字段。
自定义异常需要提供
serialVersionUID
注意点
- 自定义的异常对象只能通过 throw 手动抛出。抛出后由 try…catch 处理,也可以 throws 给调用者处理。
- 自定义异常最重要的是异常类的名字和 message 属性。当异常出现时,可以根据名字判断异常类型。比如:
TeamException("成员已满,无法添加");、TeamException("该员工已是某团队成员");
举例
publicclassAgeExceptionextendsRuntimeException{publicAgeException(Stringmessage){super(message);}}publicclassPerson{publicstaticvoidsetAge(intage){if(age<0||age>150){// 手动抛出自定义异常thrownewAgeException("年龄不合法:"+age);}System.out.println("年龄设置成功:"+age);}publicstaticvoidmain(String[]args){try{setAge(200);}catch(AgeExceptione){// try...catch 处理异常System.out.println("捕获异常:"+e.getMessage());}}}publicclassBalanceNotEnoughExceptionextendsException{publicBalanceNotEnoughException(Stringmessage){super(message);}}publicclassAccount{privatestaticintbalance=100;// 方法声明 throws,把异常交给调用者处理publicstaticvoidwithdraw(intmoney)throwsBalanceNotEnoughException{if(money>balance){thrownewBalanceNotEnoughException("余额不足,当前余额:"+balance);}balance-=money;System.out.println("取款成功,剩余余额:"+balance);}}publicclassTestAccount{publicstaticvoidmain(String[]args){try{Account.withdraw(200);}catch(BalanceNotEnoughExceptione){System.out.println("取款失败:"+e.getMessage());}}}