》》》我的博客主页
》》》我的gitee链接
关注我,在学习Java的道路上共同进步!!!
在Java中,将程序执行过程中发生的不正常行为称为异常。
平常容易见到的异常 3个例子:
实际上异常也是一个类,异常类 (Exception) 和 错误类 (Error) 都继承于同一个父类 Throwable。
Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
Error 也是程序执行过程中发生的不正常行为,但与 Exception 不同的是:
异常 (Exception) 是指可以被程序员通过代码进行处理的问题,比如 算术异常,空指针异常,数组索引越界异常 等。
错误 (Error) 指的是 JVM 或 底层系统 出现的严重问题,这些问题发生在程序员无法用代码控制的底层范围。比如栈溢出错误 StackOverflowError,内存溢出错误 OutOfMemoryError 等。
运行时异常 : 也叫 非受检查异常,RuntimeException 就算运行时异常的一大类。编译时不检查,只有运行时才会被发现的异常就是运行时异常。算术异常 ArithmeticException, 空指针异常 NullPointerException, 数组越界异常 ArrayIndexOutOfBoundsException 等这些都是属于 运行时异常。
编译时异常 :也叫 受检查异常 (编译时需要接受检查),也就是编译期间要处理的异常,否则代码不能编译通过。IOException, ClassNotFoundException 和 CloneNotSupportedException 这3个类算 编译时异常。
当然,每个异常子类当中都有许许多多的子类,这里不详细展开。
在 Java 中,有关异常处理的 5 个关键字是:throw , throws , try , catch , finally
在这一节会慢慢详细展开说明这 5 个关键字的用法。
错误的代码是难以避免的,我们要做的就是通过代码得到及时的反馈,及时发现问题才能够及时修改。
事前防御型编程 像是 一步三回头
代码进行下一步的时候,上一步已经检查完毕。
举一个游戏匹配之前的代码例子:
boolean ret = false;
ret = 登陆游戏();
//每执行下一步前先对上一步进行检查
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
...
事前防御型编程的缺点:正常流程和错误处理流程的代码混在一起,代码整体看起来比较混乱。
把正常流程的代码放在一个代码块里,把错误处理流程的代码放在另一个代码块里,然后通过 catch 关键字捕捉 正常流程代码中可能会发生的异常。
还是举游戏匹配之前的代码例子,但这回用事后认错型的编程方式:
try {
//把正常流程的代码放在这个代码块里
登陆游戏();
开始匹配();
游戏确认();
...
} catch (登陆游戏异常) {//通过catch捕捉在 try 块中的代码可能会出现的异常
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} ...
相比于事前防御型编程的优势:正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码。
抛出异常 是 异常处理 的前提,那如何让程序抛出异常呢?
自动抛出就不必多说,注意力集中在通过 throw 手动抛出异常。
之前提过,异常也是一个类,我们要手动抛出异常就要 创建一个异常对象,然后用 throw 关键字 抛出这个异常对象。
throw 关键字一般用于抛出我们自定义的异常。
异常抛出之后,由谁捕获 抛出去的异常?这里有两种情况:
使用方式:throws 使用在方法的声明之后 且 可以声明多个异常,但必须是 Exception 或 Exception 的子类。
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
异常声明的作用:告诉 该方法的调用者 调用这个方法会抛出 XXX异常,如果 该方法的调用者 没有 主动捕获和处理 这个方法抛出的异常,就会交给JVM处理。
注意点: throws 通常是声明 编译时异常 的,因为 编译时异常 需要在编译期间就要处理,如果该方法没有去处理异常,那就要声明异常 让该方法的调用者去处理,以此类推,直到被JVM捕获为止。
用代码举个例子:
我写了一个 func() 方法 并用 throws 声明 调用 func() 方法会抛出 CloneNotSupportedException 异常,
然后我直接在 main 方法调用 func() 方法 但没去捕获 CloneNotSupportedException 异常,这很明显出现了问题。
因为我们既没有 手动处理 ,也没有在 main 方法上继续用 throws 声明异常让JVM自动处理。
编译器知道 func() 方法可能会抛出 CloneNotSupportedException 异常,但我们没有采取任何处理措施,导致程序无法继续执行。
解决办法1:在 main 方法声明 CloneNotSupportedException 异常,然后让JVM捕获 CloneNotSupportedException 异常 ,这就有点像推卸责任。
那JVM怎么处理 CloneNotSupportedException 异常?就是告诉你出现了 CloneNotSupportedException 异常。
解决办法2:我们程序员自己通过 try - catch 主动捕获 CloneNotSupportedException 异常并处理。
处理异常的时候会根据具体的业务需求,将其转换成适合当前业务需求的异常类型,然后再将其抛出。
1.throw 和 throws 的区别?
throw 是用于在程序中手动抛出一个异常对象。
throws 是在方法定义的时候声明这个异常,只是让方法的调用者知道该方法可能会抛出哪些异常。那么在方法的调用者中,必须对可能抛出的异常进行处理 或者 在调用这个方法的方法中继续声明这个异常,否则会在编译时产生错误、、、
在这一小节详细说明 try - catch 的用法。
从一个代码例子入手:下面这种情况就是交给 JVM 处理,一旦交给 JVM 处理,程序就会异常终止。
在catch 的 () 里面 写 捕获到的异常,捕获的异常要与可能发生的异常是匹配的,如果捕获到该异常了才会执行catch当中的内容。
如果捕获的异常 没有与可能发生的异常 发生匹配,那就是异常没有捕获到,导致异常终止。
如果我不知道会捕获哪个异常怎么办?多写几个 catch 可以捕获多种异常。
需要注意一点:可以通过 catch 捕获多种异常,但是同一时刻 只能抛出一个异常,catch 不能在同一时刻捕获到多个异常。
我怎么知道捕获的是哪一行的代码异常呢?调用该异常对象的 printStackTrace()方法。
捕获多种异常还有一种写法,但比较少用,还是更推荐上面一种写法。
try - catch 注意事项:
如果抛出异常类型与 catch 时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到 JVM 收到后中断程序 ---- 异常是按照类型来捕获的
try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获 ---- 即多种异常,多次捕获
如果异常之间具有父子关系,一定是子类异常在前 catch,父类异常在后 catch,否则语法错误
不能直接用异常父类 接收 所有的异常子类。因为此时异常不精准
由于 Exception 类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常。但如果想要通过一个 catch 捕获所有的异常,即多个异常,一次捕获,这是不推荐的。
finally 写在 try - catch 的后面,下面用一个代码例子说明 finally 的作用:
此时,第 8 行代码出现了异常,finally 部分的代码成功执行,现在修改 array ,使得 array 引用指向确切的数组,这时算数组的长度就不会有异常,那 finally 部分的代码会执行吗?
通过观察可以知道,即使没有执行 catch 部分的代码, finally 部分的代码照样执行,这里我要对 finally 做进一步的解释:
无论 try 块中的代码有没有发生异常,finally 中的语句终将被执行。
finally 一般用来释放资源,如果有需要释放资源的东西,即使代码发生了异常,finally也一定会被执行,相当于又充当了一个保底的作用。
比如当我们使用 Scanner 对象从标准输入读取用户的输入时,它底层会打开一个与系统输入流(System.in)相关联的资源,当不用 Scanner 对象的时候最好要通过 scanner.close(); 关闭资源,因为无论 try 块中的代码有没有发生异常,finally 中的语句终将被执行,所以 scanner.close(); 最适合写在 finally 中。
还可以这么写:这样写的话,finally 就不需要手动关闭资源了。
建议不要在 finally 当中 写 return
如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上层(调用者)传递(谁调用它,谁来处理异常)
如果传到main方法,main方法也没有处理异常,
如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理, 程序就会异常终止(和我们最开始没有使用 try-catch 时是一样的)
异常的处理流程总结:
虽然 Java 已经提供了很多异常类,但毕竟不能满足所有的需求,在实际的业务开发当中需要调用我们自定义的异常类来满足我们实际的业务需求。
例如:实现一个用户登录功能。
public class Login {
public String userName = "admin";
public String password = "123456";
public void loginInfo(String paUserName,String paPassword) {
if(!paUserName.equals(this.userName)) {
//想要抛出一个用户名异常
}
if(!paPassword.equals(this.password)) {
//想要抛出一个密码异常
}
System.out.println("登录成功");
}
public static void main(String[] args) {
Login login = new Login();
login.loginInfo("admin","123456");
}
}
此时的需求是:
如果用户名不相同,希望抛出一个用户名异常;
如果密码不相同,希望抛出一个密码异常。
可是用户名异常和密码异常Java没有,怎么办?
这时,我们可以基于已有的异常类进行扩展(继承), 创建和我们业务相关的异常类。
如何创建自定义异常类?
就像上面所说的,如果要写一个自定义异常,一定要继承一个异常。
两种继承的方式:
现在分别定义 用户名异常 和 密码异常,
然后分别实现一个带有 String 类型参数的构造方法,参数含义:出现异常的原因
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
现在有了自定义异常类,就能抛出我们实际情况的异常了,用户登录功能的完整代码如下:
//自定义用户名异常类
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
//自定义密码异常类
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
public class Login {
public String userName = "admin";
public String password = "123456";
public void loginInfo(String paUserName,String paPassword)
throws UserNameException, PasswordException {//因为这个方法可能会抛出这两个异常,所以要声明异常
if(!paUserName.equals(this.userName)) {
throw new UserNameException("用户名错误");
}
if(!paPassword.equals(this.password)) {
throw new PasswordException("密码错误");
}
System.out.println("登录成功");
}
public static void main(String[] args) {
try {
Login login = new Login();
login.loginInfo("admi","123456");
}catch (UserNameException e) {
e.printStackTrace();//输出异常的堆栈信息
}catch (PasswordException e) {
e.printStackTrace();//输出异常的堆栈信息
}
}
}