- 👑专栏内容:Java
- ⛪个人主页:子夜的星的主页
- 💕座右铭:前路未远,步履不停
目录
- 一、异常的体系结构
- 1、异常的体系结构
- 2、异常的分类
- 二、异常的处理
- 1、异常的抛出
- 2、异常的捕获
- 2.1、异常声明`throws`
- 2.2、`try-catch`捕获并处理
- 2.3、`finally`
- 3、异常的处理流程
- 3.1、什么是 "调用栈"
- 3.2、异常处理流程总结
- 三、自定义异常类
在程序运行过程中,会遇见一些奇奇怪怪的问题,有时候通过代码是很难控制的。在 Java 中,将程序执行过程中发生的不正常行为称之为异常。
一、异常的体系结构
异常种类有很多,为了对不同异常或者错误进行很好的分类管理。
Java内部维护了一个异常的体系结构:
1、异常的体系结构
Throwable
是 Java异常体系的顶层类,是Java语言中所有错误和异常的超类。它派生出两个重要的子类:Error
和 Exception
。
Error
指的是Java虚拟机(JVM)无法解决的严重问题,通常不应由合理的应用程序尝试捕获。大多数此类错误是JVM遇到的异常情况。此类异常一旦发生,一般没有办法通过修改代码解决。
Exception
指的是程序员可以通过代码进行处理,使程序继续执行的异常。我们平时所说的“异常”通常是指Exception
。
2、异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:运行时异常和编译时异常(受检查异常)。
- 编译时异常
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception
)。这些异常在编译期间会被检查,也就是说,如果一个方法可能抛出某个受检查异常,那么在调用这个方法的地方必须对这个异常进行处理。
这种处理可以是捕获异常并对其进行处理,或者是将异常声明在方法的throws
子句中,告诉方法的调用者需要处理这个异常。
- 运行时异常
运行时异常,也称为非受检查异常(Unchecked Exceptions
),与编译时异常不同,运行时异常在编译期间不需要显式处理。这意味着如果你的代码抛出了一个运行时异常,你不需要用try-catch
块捕获它,也不需要在方法声明中用throws
子句声明它。
二、异常的处理
异常处理主要的5个关键字:throw
、try
、catch
、final
、throws
1、异常的抛出
throw
关键字是用来显式地抛出一个异常的。使用throw
可以抛出一个现有的异常实例或创建一个新的异常实例并抛出。具体的语法如下:
throw new XXXException("需要显示的错误信息");
我们一般通过throw
抛出一些自己自定义的异常。
public class ArrayElementRetriever {// getElement方法尝试从一个整数数组中获取指定索引处的元素public static int getElement(int[] array, int index) {// 检查传入的数组是否为null。如果是null,抛出NullPointerException。if (null == array) {throw new NullPointerException("传递的数组为null");}// 检查索引是否越界。如果索引小于0或大于等于数组长度,抛出ArrayIndexOutOfBoundsException。if (index < 0 || index >= array.length) {throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");}// 如果没有异常发生,则返回数组中指定索引处的元素。return array[index];}public static void main(String[] args) {int[] array = {1, 2, 3};// 尝试获取数组中的元素。这里故意传递一个越界的索引。getElement(array, 3);}
}
【注意事项】
throw
必须写在方法体内部。- 抛出的对象必须是
Exception
或者Exception
的子类对象。 - 如果抛出的是
RunTimeException
或者RunTimeException
的子类,则可以不用处理,直接交给JVM来处理(JVM会立即终止你的程序)。 - 如果抛出的是编译时异常,用户必须处理,否则无法通过编译。
- 异常一旦抛出,其后的代码就不会执行。
2、异常的捕获
异常的捕获,也就是异常的具体处理方式。异常的捕获主要有两种方式:异常声明throws
以及 try-catch
捕获处理。
2.1、异常声明throws
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws
将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。具体语法如下:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
public class ExceptionThrower {// 定义一个方法,声明它可能抛出一个自定义的编译时异常public void doSomethingRisky() throws CustomException {// 假设这里有一些逻辑,最后确定需要抛出异常throw new CustomException("描述错误情况");}public static void main(String[] args) {ExceptionThrower thrower = new ExceptionThrower();try {// 尝试调用可能抛出异常的方法thrower.doSomethingRisky();} catch (CustomException e) {// 处理异常System.out.println("捕获到异常: " + e.getMessage());}}
}// 自定义编译时异常
class CustomException extends Exception {public CustomException(String message) {super(message);}
}
【注意事项】
throws
关键字用于方法声明中,紧跟在方法的括号之后。throws
后面可以跟一个或多个异常类型,使用逗号分隔。- 如果一个方法声明抛出一个异常,那么调用这个方法的代码必须处理这个异常,要么是通过
try-catch
捕获它,要么是通过在其自身声明中使用throws
继续向上抛出。 - 使用
throws
声明的异常类型应该是具体的异常类,而不是抽象类或接口。 - 对于运行时异常(
RuntimeException
及其子类),通常不在方法声明中使用throws
进行声明,因为它们是非受检查的异常,但你也可以这样做以提供更好的程序文档。
2.2、try-catch
捕获并处理
throws
对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch
。具体语法格式如下:
try{// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}[catch(异常类型 e){// 对异常进行处理
}finally{// 此处代码一定会被执行到
}]// 后序代码// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
注意:
[]
中表示可选项,可以添加,也可以不用添加try
中的代码可能会抛出异常,也可能不会
public class demo{public static void main(String[] args) {System.out.println("before");try{System.out.println(10/0);}catch (ArithmeticException e){System.out.println("捕获到了 ArithmeticException 这个异常");}System.out.println("affer");}
}
try
块内抛出异常位置之后的代码将不会被执行,如果抛出异常类型与catch
时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序。
public class demo1 {public static void main(String[] args) {System.out.println("before");try{System.out.println(10/0);}catch (NullPointerException e){System.out.println("捕获到了 ArithmeticException 这个异常");}System.out.println("affer");}
}
try
中可能会抛出多个不同的异常对象,则必须用多个catch
来捕获,即多种异常,多次捕获
public class demo1 {public static void main(String[] args) {int[] arr = {1, 2, 3};try {System.out.println("before");// arr = null;System.out.println(arr[100]);System.out.println("after");} catch (ArrayIndexOutOfBoundsException e) {System.out.println("这是个数组下标越界异常");e.printStackTrace();} catch (NullPointerException e) {System.out.println("这是个空指针异常");e.printStackTrace();}System.out.println("after try catch");}
}
如果多个异常的处理方式是完全相同, 也可用写成下面这个样子:
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {}
虽然可用像上面这样写,但是不建议。因为没办法知道到底是因为那个异常导致的。衡量一个代码的好与坏,除了时间复杂度和空间复杂度之外,真正要看的是代码的可读性。
如果异常之间具有父子关系,一定是子类异常在前catch
,父类异常在后catch
,否则语法错误:
public class demo1 {public static void main(String[] args) {int[] arr = {1, 2, 3};try {System.out.println("before");arr = null;System.out.println(arr[100]);System.out.println("after");} catch (Exception e) { // Exception可以捕获到所有异常e.printStackTrace();}catch (NullPointerException e){ // 永远都捕获执行到e.printStackTrace();}System.out.println("after try catch");}
}
可以通过一个catch
捕获所有的异常,即多个异常,一次捕获(不推荐)
public class demo1 {public static void main(String[] args) {int[] arr = {1, 2, 3};try {System.out.println("before");arr = null;System.out.println(arr[100]);System.out.println("after");} catch (Exception e) {e.printStackTrace();}System.out.println("after try catch");}
}
由于 Exception
类是所有异常类的父类,因此可以用这个类型表示捕捉所有异常。
catch
进行类型匹配的时候,不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象。如刚才的代码,NullPointerException
和 ArrayIndexOutOfBoundsException
都是 Exception
的子类,因此都能被捕获到。
2.3、finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。
另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally
就是用来解决这个问题的。finally
的语法格式如下:
try{// 可能会发生异常的代码
}catch(异常类型 e){// 对捕获到的异常进行处理
}finally{// 此处的语句无论是否发生异常,都会被执行到
}// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public class demo1 {public static void main(String[] args) {try{int[] arr = {1,2,3};arr[100] = 10;arr[0] = 10;}catch (ArrayIndexOutOfBoundsException e){System.out.println(e);}finally {System.out.println("finally中的代码一定会执行");}System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");}
}
【问题】 既然 finally
和 try-catch-finally
后的代码都会执行,那为什么还要有finally
呢?
虽然finally
块和try-catch
结构后的代码在很多情况下都会执行,finally
仍然非常重要,原因主要包括:
-
确保资源释放: 最常见的
finally
用途是进行资源清理,比如关闭文件流或数据库连接。这些操作即使在发生异常时也必须执行,以防止资源泄漏。如果只将清理代码放在try-catch
之后,那么在异常未被当前的catch
捕获时,这些清理代码将不会执行。 -
处理未捕获的异常: 如果try块中抛出了一个未被任何catch捕获的异常,try-catch之后的代码不会执行,但finally块仍然会执行。这为处理所有情况提供了一个统一的地方,确保即使在出现意外异常时也能进行必要的清理。
-
覆盖返回值: 在
try
或catch
块中有返回语句时,finally
块仍会在方法返回之前执行,甚至有能力修改返回值(但是并不推荐这样,因为会使代码难以理解)。 -
明确的意图: 使用
finally
可以明确表示某段代码无论发生何种情况都必须执行,这对于代码的可读性和维护性是有好处的。它让其他开发者清楚地知道,无论try
块中发生了什么,finally
块中的代码都是必须执行的。
3、异常的处理流程
3.1、什么是 “调用栈”
方法之间是存在相互调用关系的,这种调用关系我们可以用 “调用栈” 来描述。 在 JVM 中有一块内存空间称为"虚拟机栈" 专门存储方法之间的调用关系。当代码中出现异常的时候,我们就可以使用 e.printStackTrace();
的方式查看出现异常代码的调用栈。这个调用栈追踪显示了异常发生时的方法调用顺序,从最近的方法调用开始,一直追溯到异常被抛出的源头。调用栈追踪提供的信息通常包括:
- 异常类型和描述信息。
- 异常发生的代码位置,包括类名、文件名和行号。
- 方法调用序列,从发生异常的方法开始,一直到程序入口。
public class demo1 {public static void main(String[] args) {try {func();} catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace();}System.out.println("after try catch");}public static void func() {int[] arr = {1, 2, 3};System.out.println(arr[100]);}
}
如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理,程序就会异常终止(和我们最开始未使用 try catch
时是一样的)。
3.2、异常处理流程总结
- 程序先执行 try 中的代码。
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配。
- 如果找到匹配的异常类型, 就会执行 catch 中的代码。
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者。
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行)。
- 如果上层调用者也没有处理的了异常, 就继续向上传递一直到 main 方法也没有合适的代码处理异常, 。就会交给 JVM 来进行处理, 此时程序就会异常终止。
三、自定义异常类
自定义异常是一种用户定义的异常,它可以帮助处理程序特定的错误情况。创建自定义异常通常有助于更清晰地表达程序的意图和处理程序的特定错误。
自定义异常通常通过继承Java的Exception
类或其子类来创建。如果希望自定义异常是受检异常(即必须显式处理的异常),则应继承Exception
类。如果希望它是非受检异常,则应继承RuntimeException
。自定义异常类应该至少提供一个构造方法,它通常会调用父类的构造方法来初始化异常消息。可以根据需要提供多个构造方法,以便在抛出异常时可以传递不同类型的信息。
public class LogIn {private String userName = "admin";private String password = "123456";public static void loginInfo(String userName, String password) {if (!userName.equals(userName)) {}if (!password.equals(password)) {}System.out.println("登陆成功");}public static void main(String[] args) {loginInfo("admin", "123456");}
}
此时我们在处理用户名密码错误的时候可能就需要抛出两种异常。我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类。
具体方式:
- 自定义异常类,然后继承自
Exception
或者RunTimeException
- 实现一个带有
String
类型参数的构造方法,参数含义:出现异常的原因
class UserNameException extends Exception {public UserNameException(String message) {super(message);}
}
class PasswordException extends Exception {public PasswordException(String message) {super(message);}
}
此时我们的 login 代码可以改成:
public class LogIn {private String userName = "admin";private String password = "123456";public static void loginInfo(String userName, String password)throws UserNameException,PasswordException{if (!userName.equals(userName)) {throw new UserNameException("用户名错误!");}if (!password.equals(password)) {throw new PasswordException("用户名错误!");}System.out.println("登陆成功");}public static void main(String[] args) {try {loginInfo("admin", "123456");} catch (UserNameException e) {e.printStackTrace();} catch (PasswordException e) {e.printStackTrace();}}
}
注意:
- 自定义异常通常会继承自
Exception
或者RuntimeException
- 继承自
Exception
的异常默认是受查异常 - 继承自
RuntimeException
的异常默认是非受查异常.