
一、异常的概念
1.概念:程序过程中发生的不正常的行为
程序异常就是相当于报病,就要去治疗它。
之前曾遇到过的常见的异常
1.算术异常
10÷0错误,0不能作为除数

2.数组下标越界异常
数组只有4个元素,你要打印最后一个元素arr[3],意外写成arr[4],就越界了

3.空指针异常
数组为空,却要求数组的长度

2.体系结构

Throwable是异常体系的祖宗,代表能 抛出的东西。他的子类就是Error和Exception。
分别是虚拟机的错误和异常。(GitMind思乎这个做思维导图还挺好用的,可以免费让AI生成,但是只有几次机会后面就要钱了
)
3、异常的分类
按处理方式分:
①(编译时异常)检查异常Checked Exception:
是程序运行中可能遇到的外部问题,即是RuntimeException以外的异常,必须显式处理,若不处理程序就会不通过,编译失败,如图上IOException、ClassNotFoundException等等
②(运行时异常)非受检查异常Unchecked Exception:
本质是代码编写错误导致运行时问题(如算术、数组下标越界、空指针异常),继承自RuntimeException,不强制处理

之前的第一篇文章可以看见一个Java文件是先编译在运行,所以也可以知道他的异常两个板块也就是编译时异常和运行时异常。(是我的图就是改了个名字)
编译器:只判断编译时异常
运行时:同时判断运行时异常和虚拟机错误
运行时异常上面已经举了三个例子了
接下来是编译时异常 ,我们以IOException输入输出异常为例
java
public class IOExceptionDemo {
//演示IOException(受检异常/编译时异常)
public static void main(String[] args) {
//以其中FileNotFoundException文件未找到为例
FileInputStream fis = new FileInputStream("不存在的文件.txt");//读文件用的
}
}

编译报错的原因:
→受检异常(编译时异常)
→不捕获/不抛出,直接编译不通过
(必须try-catch或throws,下一个板块讲,否则编译失败)
再拿CloneNotSupportedException举例
java
//Student实现接口Cloneable
class Student implements Cloneable{
private String name;
public Student(String name) {
this.name = name;
}
//这里会编译报错:CloneNotSupportedException
//不支持克隆异常
@Override
protected Object clone(){
return super.clone();
}
}
public class CloneDemo {
public static void main(String[] args) {
Student student1 = new Student("小马");
}
}

编译报错的原因:
→受检异常(编译时异常)
→不捕获/不抛出,直接编译不通过
(必须try-catch或throws,下一个板块讲,否则编译失败)
三、如何处理异常
1.防御式编程
①.事前防御型(LBYL)
**事前防御型:**Look Before You Leap在操作前充分检查
频繁用到 return ,直接结束后面的
缺陷:正常流程和错误流程代码混在一起,代码整体比较混乱
我们以前面的 算术异常 为例:
事前就要考虑除数的情况,遇到0 直接就结束return
java
public class Demo4 {
public static void main(String[] args) {
//事前防御
//1.算数异常
int a = 0, b = 0;
Scanner scanner = new Scanner(System.in);
a = scanner.nextInt();//输入10
b = scanner.nextInt();//输出0
//事前考虑0为除数情况
if (b == 0) {
return;
} else {
System.out.println(a / b);
}
}
}
②事后认错型(EAFP)
异常处理的核心=事后认错型
事后认错型:"事后获取原谅比事前获取许可更容易",
也就是先操作,遇到问题再处理
用到try-catch
try:我先大胆的干这件事
catch:万一出事,我在这里认错、处理
try {//可能会出现的错误代码
} catch (要抓的异常 变量) {
//出错了就跑这里
//处理错误,程序不会崩
}
我们依然以算术异常为例
java
public class Demo5 {
//事后防御
//先大胆的干,错了在处理
public static void main(String[] args) {
int a, b = 0;
Scanner scanner = new Scanner(System.in);
a = scanner.nextInt();//10
b = scanner.nextInt();//0
try {
System.out.println(a / b);
}catch (ArithmeticException arithmeticException) {
System.out.println("除数不能为0");//除数不能为0
}
}
}
执行结果:
优势:正常流程和错误流程是分开的,程序员更关注正常流程,代码更清晰,容易理解代码
2.异常的抛出
什么叫抛出异常:
代码发现错了→自己不处理→给别人处理
1.throw(手动抛)
throw + 一个异常对象
throw new XXXException("异常产生的原因");
注意事项:
①throw必须写在方法体内部
②抛出对象必须是Exception或者Exception的子类对象
③如果抛出运行时异常RunTimeException或其子类,则可以不用处理,直接交给JVM来处理
④如果抛出是编译时异常,用户必须处理,否则编译无法通过
⑤异常一旦抛出,其后代码就不会执行
①运行时异常
用数组是否越界举例
java
public class Demo6 {
public static void main(String[] args) {
//运行时异常
int[] arr = {1,2,3,4};
Scanner scanner = new Scanner(System.in);
System.out.println("选择你要打印的数组下标:");
int index = scanner.nextInt();
//throw
System.out.println(isIndexOutBounds(arr,index));
}
//定义一个数组是否越界的方法
public static int isIndexOutBounds(int[] arr,int index) {
//数组是否未传递,空指针异常
if(arr == null) {
throw new NullPointerException("传递数组为null");
}
//下标是否越界
if (index < 0 || index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("数组下标越界" + index);
}else {
return index;
}
}
}
当我输入大于数组下标的元素时,会抛出 我给定的异常(自己指定的异常),后面内容不再执行直接结束。

当查找数组索引合法时,执行不抛出异常

②编译时异常
我们前面举例编译时异常 的两个IOException和CloneNotSupportedException不能直接通过throw解决,因为他是编译时异常,用户必须处理,需要配合 throws,否则无法通过编译
①编译时异常throws配合处理

处理之后就可以通过编译,抛出异常了
java
public class IOExceptionDemo {
//演示IOException(受检异常/编译时异常)
public static void main(String[] args) throws FileNotFoundException{
//以其中FileNotFoundException文件未找到为例
//FileInputStream读文件用的
FileInputStream fis = new FileInputStream("不存在的文件.txt") ;
throw new FileNotFoundException("文件未找到");
}
}

②编译时异常try-catch配合处理依旧必须处理方法throws
java
public class IOExceptionDemo {
//演示IOException(受检异常/编译时异常)
public static void main(String[] args){
//编译时throw抛出异常,用try-catch处理
try {
checkFileExists();
}catch (FileNotFoundException fileNotFoundException) {
System.out.println("文件没找到");//文件没找到
}
}
//方法内部手动 throw 抛出编译时异常,需要方法上声明throws
public static void checkFileExists() throws FileNotFoundException {
//模拟文件不存在场景
FileInputStream fis = new FileInputStream("不存在文件.txt");
throw new FileNotFoundException("文件未找到:" + fis);
}
}

2.throws(方法声明抛)
throws写在方法名的后面
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
用前面报错的CloneNotSupportedException来看

克隆方法后面throws抛出异常,即可编译成功,不会报错
注意事项:
①throws必须跟在方法参数列表之后
②声明异常必须是Exception或其子类
③方法中若是抛出多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
javaimport java.io.IOException; public class Demo7 { //抛出多个无继承异常,同时抛出FileNotFoundException, IOException //两者没有继承关系 public static void doSomething() throws ArithmeticException, IOException { //模拟实现两种不同类型的异常 if (Math.random() > 0.5) { throw new IOException("网络连接中断,IO操作失败"); } else { //算数异常,0不能是被除数 throw new ArithmeticException("除数不能为0"); } } //异常是父子关系,直接声明父类 public static void test() throws RuntimeException{ //算术异常ArithmeticException继承于运行时异常RunTimeException throw new ArithmeticException("除数不能为0"); } public static void main(String[] args) throws IOException{ doSomething(); test(); } }④调用抛出异常的方法时,调用者必须对异常进行处理,或者继续使用throws抛出
将光标放在抛出异常方法上,alt+enter 快速处理。
关于这个抛出异常,可以直接通过报错,直接一键添加。
3.异常的捕获
异常的捕获 = 抓住异常,
1.try - catch 捕获并处理
throws对异常并未真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。
真正对异常处理,就需要try-catch
throw / throws 对比 try- catch
try- catch:自己抓住,自己处理→ 事后认错
throw / throws:自己不处理,扔给别人→ 甩锅
语法格式:
try {
//将可能出现的异常的代码放在这里
} catch (要捕获的异常类型 e) {
//如果try中代码抛出异常,此处catch捕获异常类型与try中异常类型一致时,或者try中抛出了异常的基类,就会被捕获
//对异常就可以正常处理,处理完成之后,就跳出try-catch 结构,继续处理后续代码
}【catch (异常类型 e) {
//对异常进行处理
}finally {
//此处代码一定会被执行到
} 】
//后续代码
//当一场被捕获,异常就被处理,后续代码一定会执行
//如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码不会被执行
(①【】中表示可选项,可以添加,也可以不添加
② try 中代码可能抛出代码,也可能不会,
看 try代码中产生的异常类型 是否有 与之匹配的catch代码 )
try中抛出单个异常(异常的处理方式)
java
public class Demo9 {
public static void main(String[] args) {
//try-catch的方法注意事项
try {
String[] strings ={"abc","lll","g","o"};
//数组越界异常
System.out.println(strings[strings.length]);
}catch (RuntimeException r) {
//ArrayIndexOutOfBoundsException数组异常
// 是 RunTimeException运行时异常的子类
//异常的处理方式
// System.out.println(r.getMessage());//只打印异常信息
//输出:Index 4 out of bounds for length 4
System.out.println(r);//只打印异常类型:异常信息
//输出:java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
r.printStackTrace();//打印具体的异常
//输出:抛出异常
}
}
}
try中抛出多个不同异常对象,必须用多个catch来捕获,→ 多种异常,多次捕获
java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* @ClassName Demo8
* @Description TODO:异常的捕获
* @Author yangyutong
* Version 1.0
*/
public class Demo8 {
//方法内部手动 throw 抛出编译时异常,需要方法上声明throws
public static void checkFileExists() throws IOException {
//模拟文件不存在场景
FileInputStream fis = new FileInputStream("不存在文件.txt");
throw new FileNotFoundException("文件未找到:" + fis);
}
//定义一个数组是否越界的方法
public static int isIndexOutBounds(int[] arr,int index) {
//数组是否未传递,空指针异常
if(arr == null) {
throw new NullPointerException("传递数组为null");
}
//下标是否越界
if (index < 0 || index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("数组下标越界" + index);
}else {
//下标合法时返回下标值
return index;
}
}
/**
* try-catch 异常捕获并处理
* @param args 命令行参数
*/
public static void main(String[] args) {
//捕获异常并处理
try {
String s = null;
//空指针异常,运行到这一行,直接执行第二个catch,后面代码不执行
s.length();
//调用可能抛出编译时异常的方法
checkFileExists();
System.out.println("执行到这个地方,完成checkFileExists方法的调用");
} catch (IOException e) {
System.out.println("FileNotFoundException文件未找到异常");
//打印异常的详细调用栈信息,便于定位问题
e.printStackTrace();
} catch (RuntimeException r) {
System.out.println("空指针异常");//输出:空指针异常
//打印异常的详细调用栈信息,便于定位问题
r.printStackTrace();
}
}
}
输出结果:

多个异常处理方式完全相同,可以这样写:
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
...
}
或者直接继承他们的父类:
catch (RunTimeException r) {
...
}
注意事项:①try块内抛出异常位置,之后的代码不会被执行
②如果抛出异常类型与catch异常类型不匹配,即异常不会被成功捕获,也不会被处理,继续往外抛,直到JVM虚拟机收到后,中断程序 (异常都是按照类型捕获的)
javapublic class Demo9 { // try中异常 与 catch 不匹配 public static void main(String[] args) { try { String[] strings ={"abc","lll","g","o"}; //数组越界异常 System.out.println(strings[strings.length]); }catch (IOException i) { //try里的异常与catch 没有相符的 System.out.println("输入输出异常"); } } }try和catch异常没有匹配,直接报错
2.finally
在特定的代码中,不论程序是否发生异常,都需要执行,用来解决程序某些语句执行不到的情况
比如程序中打开的资源:网络连接、数据库连接、IO流等
在程序正常或者异常退出时,必须要对资源进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
比如:
javapublic class Demo10 { public static void main(String[] args) { try { //此处代码执行过程中,可能会出现异常、return等情况, // 导致后续的资源释放无法直接执行 //这也是为啥需要finally块来兜底执行资源释放逻辑原因 //定义两个测试用的整型变量 int a = 10; int b = 20; //当 a<b ,空指针异常 //这里的异常会导致try块后续代码直接终止执行 if (a < b) { throw new NullPointerException("a 小于 b"); } //若前面异常,后面的代码将无法执行 release(); return; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } finally { //必须执行 //无论是否抛出异常、是否执行return // 也无论catch块是否捕获到异常 //finally必执行,适合放置资源释放逻辑 release(); } } // 释放资源.... private static void release() { System.out.println("release 释放资源"); } }
语法格式(try - catch - finally)或 try- finally:
try{
//可能会发生异常的代码
}catch (异常类型 e) {
//对捕获异常进行处理
}finally {
//此处语句不论是否发生异常,都会被执行到
}
//没有抛出异常,或者异常已经被捕获处理,这里的代码也会执行
不管程序是否抛出异常,finally都会执行
无异常情况
java
public class Demo11 {
//初始化资源
private static String resource = "初始化资源";
public static void main(String[] args) {
//场景一:try块中无异常,正常执行
try {
System.out.println("try块执行:无异常,处理业务逻辑");
resource = "初始化后的资源";
} finally {
//无论try块是否正常,都需要执行finally:释放资源
releaseResource ();
}
}
//释放资源
private static void releaseResource() {
System.out.println("执行资源释放:" + resource + "已释放");
resource = null;//释放资源
}
}
即使有return,finally 也会执行
java
public class Demo12 {
//不论是否有return,finally必执行
public static int func() {
int a = 10;
Scanner scanner = new Scanner(System.in);
int b = scanner.nextInt();
// a÷b 是否算数异常
try {
//除数不能为0
if (b == 0) {
throw new ArithmeticException("算数异常!");
}
return b;
} catch (ArithmeticException e) {
System.out.println("算数异常");
} finally {
//关闭输入
scanner.close();
return 20;
}
}
public static void main(String[] args) {
System.out.println(func());//20
}
}
执行结果:

总结(trhow、throws、try-catch、finally的区别):
| 关键字 | 核心作用 | 位置 |
|---|---|---|
| throw | 手动抛出一个异常对象 | 方法体内 |
| throws | 声明方法可能抛出异常类型 | 方法声明上 |
| try - catch | 捕获并处理异常 | 方法体内 |
| finally | 任何情况必执行的代码块 | 在 try / catch后 |
4.异常的处理流程
程序先执行 try 中的代码
如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.如果找到匹配的异常类型, 就会执行 catch 中的代码如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).如果上层调用者也没有处理的了异常, 就继续向上传递.
一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止
四、自定义异常
java异常分类很多,但是实际中我们的代码题和企业开发所遇到的是特有情况,不能完全表示,所以就需要自定义异常类,也比较的方便
比如用户登录功能:
就可以用自定义异常来实现
我们需要先定义一个登录界面
用户名的异常类和密码的异常类
在执行登录界面
登录界面:
java
//用户信息
public class Login {
private String username = "admin";
private String password = "123123.";
//判断是否有误
public void login (String username, String password) throws UsernameNotFoundException, PasswordErrorException{
//用户名是否存在
if (!username.equals(this.username)) {
// System.out.println("用户名不存在");
// return;
throw new UsernameNotFoundException("用户名不存在");
}
//密码是否正确
if (!password.equals(this.password)) {
throw new UsernameNotFoundException("密码错误");
}
System.out.println("登录成功");
}
}
登录异常类:
java
//登录异常父类
public class LoginException extends Exception{
public LoginException(String message) {
//异常提示信息
//向用户和开发者说明登录失败的具体原因
super(message);
}
}
用户名不存在异常:
java
//用户名不存在异常类
public class UsernameNotFoundException extends LoginException{
public UsernameNotFoundException(String message) {
super("用户名不存在");
}
}
密码错误异常:
java
//密码错误异常类
public class PasswordErrorException extends LoginException{
public PasswordErrorException(String message) {
super("密码错误");
}
}
测试登录界面:
java
public class Test {
public static void main(String[] args) {
//捕获并处理异常
try {
Login userA = new Login();
userA.login("yyy", "123890");
System.out.println("登陆成功");
} catch (UsernameNotFoundException u) {
System.out.println("用户名错误");
} catch (PasswordErrorException e) {
System.out.println("密码错误");
}finally {
System.out.println("登录操作结束");
}
}
}
注意事项:
自定义异常通常继承自Exception或者RunTimeException
继承Exception的默认是编译时异常
继承自RunTimeException的默认是运行时异常

