在
Java
中,将程序执行过程中发生的不正常行为称为异常
。
异常的体系结构
1.
Throwable
:
是异常体系的顶层类,其派生出两个重要的子类
,
Error(错误)
和
Exception(异常)
2.
Error
:
指的是
Java
虚拟机无法解决的严重问题,比如:
JVM
的内部错误、资源耗尽等
,典型代表:
StackOverflowError
和
OutOfMemoryError
,一旦发生回力乏术。
3.
Exception
:
异常产生后程序员可以通过代码进行处理,使程序继续执行。
异常的分类
1.
编译时异常
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)
2. 运行时异常
在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception)
RunTimeException以及其子类对应的异常,都称为运行时异常。
运行时指的是程序已经编译通过得到
class
文件了
,
再由
JVM
执行过程中出现的错误
.
1. 算术异常
System.out.println(10/0);
2. 数组越界异常
int[] arr = new int[10];
System.out.println(arr[20]);
3. 空指针异常
int[] arr = null;
System.out.println(arr.length);
异常的处理
防御式编程
1.
LBYL
: Look Before You Leap.
在操作之前就做充分的检查
.
即:
事前防御型
2.
EAFP
: It's Easier to Ask Forgiveness than Permission. "
事后获取原谅比事前获取许可更容易
".
也就是先操 作,
遇到问题再处理
.
即:
事后认错型
优势:正常流程和错误流程是分离开的
,
程序员更关注正常流程,代码更清晰,容易理解代码。
异常处理的核心思想就是
EAFP
。
在
Java
中,
异常处理主要的
5
个关键字:
throw
、
try
、
catch
、
final
、
throws
。
1. 异常的抛出
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测。
在
Java
中,可以借助
throw
关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:
public class Test {
public static void main(String[] args) {
int[] arr = null;
if(null==arr){
System.out.println("可以执行语句");
throw new NullPointerException("空指针异常");//throw 中断了执行流
//throw 中断了执行流,导致这一行永远不会被执行。
//System.out.println("不可达语句");//编译器会报错:unreachable statement
}
//如果没有抛出异常或异常被捕获处理,那么执行流会到达这一行并打印。
//如果异常未处理,则程序在抛出异常后终止,这一行也不会被执行
System.out.println("不执行语句");
}
}
【
注意事项
】
1. throw
必须写在方法体内部
2.
抛出的对象必须是
Exception
或者
Exception
的子类对象
3.
如果抛出的是
RunTimeException
或者
RunTimeException
的子类,则可以不用处理,直接交给
JVM
来处理
4.
如果抛出的是编译时异常,用户必须处理,否则无法通过编译
5.
异常一旦抛出,其后的代码就不会执行
异常的捕获
异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理
2. 异常声明throws
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助
throws
将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常
。
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
public void methodName() throws ExceptionType1, ExceptionType2 {
// 方法实现
}
【
注意事项
】
1. throws
必须跟在方法的参数列表之后
2.
声明的异常必须是受检异常即编译时异常
3.
方法内部如果抛出了多个异常,
throws
之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
4. 调用声明抛出异常的方法时,方法的调用者需要负责处理这些异常(通过
try-catch
或继续声明
throws
)
3. try-catch捕获并处理
throws
对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch
。
关于异常的处理方式异常的种类有很多 , 我们要根据不同的业务场景来决定 .对于比较严重的问题 ( 例如和算钱相关的场景 ), 应该让程序直接崩溃 , 防止造成更严重的后果对于不太严重的问题 ( 大多数场景 ), 可以记录错误日志 , 并通过监控报警程序及时通知程序猿对于可能会恢复的问题 ( 和网络相关的场景 ), 可以尝试进行重试 .在我们当前的代码中采取的是经过简化的第二种方式 . 我们记录的错误日志是出现异常的方法调用信息 , 能很快速的让我们找到出现异常的位置 . 以后在实际工作中我们会采取更完备的方式来记录异常信息
【
注意事项
】
1. try
块内抛出异常位置之后的代码将不会被执行
2.
如果抛出异常类型与
catch
时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序
----
异常是按照类型来捕获的
3. try中可能会抛出多个不同的异常对象,则必须用多个
catch
来捕获
----
即多种异常,多次捕获
如果异常之间具有父子关系,一定是子类异常在前
catch
,父类异常在后
catch
,否则语法错误
如果多个异常的处理方式是完全相同
,中间可以用
|
隔开
可以通过一个
catch
捕获所有的异常,即多个异常,一次捕获
(
不推荐
)
由于
Exception
类是所有异常类的父类
.
因此可以用这个类型表示捕捉所有异常
.
即: catch 进行类型匹配的时候
,
不光会匹配相同类型的异常对象
,
也会捕捉目标异常类型的子类对象
.
4. finally
在写程序时,
有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源
:网络连接、数据库
连接、
IO
流等,
在程序正常或者异常退出时,必须要对资源进进行回收
。另外,因为
异常会引发程序的跳转,可能
导致有些语句执行不到
,
finally
就是用来解决这个问题的。
注意:
finally
中的代码一定会执行的,一般在
finally
中进行一些资源清理的扫尾工作
。
语法格式:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
Scanner scan = new Scanner(System.in);
try{
int n = scan.nextInt();
System.out.println("抛出异常之前的代码可以被执行");
System.out.println(10/n);
System.out.println("抛出异常之后的代码不会被执行");
}catch(ArithmeticException a){
a.printStackTrace();//打印异常的详细信息
System.out.println("这是算术异常");
}
//多个异常的处理方式是完全相同,中间可以用 | 隔开
catch(ArrayIndexOutOfBoundsException | NullPointerException n){
n.printStackTrace();
System.out.println("这是数组越界异常和空指针异常");
}
//通过一个catch捕获所有的异常,即多个异常
catch (Exception e){
e.printStackTrace();
System.out.println("这是异常");
}
//finally中的代码一定会执行的,不管是否发生异常
finally {
System.out.println("finally中的代码一定会被执行");
scan.close();
}
System.out.println("该代码在没有抛出异常或异常被捕获执行,异常未处理不执行");
public static int meth(){
try{
System.out.println(10/0);
return 0;
}catch(ArithmeticException a){
return 1;
}finally{
return 2;
}
}
public static void main(String[] args) {
System.out.println(meth());// 输出:2
}
finally
执行的时机是在方法返回之前
(try
或者
catch
中如果有
return
会在这个
return
之前执行
finally).
但是如果
finally
中也存在
return
语句
,
那么就会执行
finally
中的
return,
从而不会执行到
try
中原有的
return.
在 finally
中使用 return
是不推荐的,因为:
- 容易混淆逻辑:掩盖了
try
或catch
的返回结果。 - 难以调试:可能会导致不可预测的行为,特别是在处理资源释放时。
异常的处理流程
关于 " 调用栈 "方法之间是存在相互调用关系的 , 这种调用关系我们可以用 " 调用栈 " 来描述 . 在 JVM 中有一块内存空间称为" 虚拟机栈 " 专门存储方法之间的调用关系 . 当代码中出现异常的时候 , 我们就可以使用 e.printStackTrace(); 的方式查看出现异常代码的调用栈 .
如果本方法中没有合适的处理异常的方式
, 就会沿着调用栈向上传递,
如果向上一直传递都没有合适的方法处理异常
,
最终就会交给
JVM
处理
,
程序就会异常终止
(
和我们最开始未使用
try
catch
时是一样的
)
【 异常处理流程总结 】程序先执行 try 中的代码如果 try 中的代码出现异常 , 就会结束 try 中的代码 , 看和 catch 中的异常类型是否匹配 .如果找到匹配的异常类型 , 就会执行 catch 中的代码如果没有找到匹配的异常类型 , 就会将异常向上传递到上层调用者 .无论是否找到匹配的异常类型 , finally 中的代码都会被执行到 ( 在该方法结束之前执行 ).如果上层调用者也没有处理的了异常 , 就继续向上传递 .一直到 main 方法也没有合适的代码处理异常 , 就会交给 JVM 来进行处理 , 此时程序就会异常终止
自定义异常类
Java
中虽然已经内置了丰富的异常类
,
但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构.
注意事项自定义异常通常会继承自 Exception 或者 RuntimeException继承自 Exception 的异常默认是受查异常继承自 RuntimeException 的异常默认是非受查异常
实现一个用户登录功能:
输入用户名和密码,比较是否与我们原有的用户名和密码相同,如果用户名不同,抛出我们自定义的NameException异常,并重新输入用户名,如果密码不同,抛出我们自定义的PasswordException异常,并重新输入密码,直到相同,跳出循环。
//自定义异常
//继承RuntimeException
public class NameException extends RuntimeException{
//构造方法
public NameException() {
}
public NameException(String message) {
super(message);
}
}
//自定义异常
//继承RuntimeException
public class PasswordException extends RuntimeException{
//构造方法
public PasswordException() {
}
public PasswordException(String message) {
super(message);
}
}
import java.util.Scanner;
public class Login{
private String name = "admin";
private String password = "123456";
public void Loginifor(String name,String password){
if(!this.name.equals(name)){
throw new NameException("用户名输入错误,请重新输入用户名:");//抛出异常
}
if(!this.password.equals(password)){
throw new PasswordException("密码输入错误,请重新输入密码:");//抛出异常
}
System.out.println("登录成功");
}
public static void main(String[] args) {
Login login = new Login();
Scanner scan = new Scanner(System.in);
System.out.print("请输入用户名:");
String name1 = scan.nextLine();
System.out.print("请输入密码:");
String password1 = scan.nextLine();
while(true){
try{
login.Loginifor(name1,password1);//可能会抛出异常
break;//抛出异常后,该代码不被执行 输入正确后,跳出循环
}catch (NameException n){ //捕获异常
//处理异常
n.printStackTrace();//打印异常
name1=scan.nextLine();//重新输入用户名
}catch (PasswordException p){
p.printStackTrace();
password1=scan.nextLine();
}
}
}
}