异常
一、异常的概念
异常是指在程序运行时出现的不正常的情况,可能会导致程序崩溃。
异常处理是指在程序运行时出现不正常的情况时,对该情况进行相应的处理(提示),得以让程序继续执行,而不会崩溃。避免损失。
public class TestMain {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
while(true) {
System.out.println("欢迎使用除法计算器:请输入要计算数字:");
System.out.println("第一个数字:");
int num1 = input.nextInt();
System.out.println("第二个数字:");
int num2 = input.nextInt();
int result = num1 / num2;
int y = num1 % num2;
System.out.println("结果为:" + result + ", 余数为:" + y);
}
}
}
当除数为0时,程序崩溃。
二、异常的分类
顶层父类为Throwable。
直接子类:Error和Exception。
错误:硬件问题、JVM问题、系统问题等。这些问题程序无法解决,不需要异常处理。
异常:
- 运行时异常RuntimeException,在编译时,不会提示异常,此类异常不属于需要异常处理的范畴,通常出现这些问题都是程序员写代码的bug。这一类的异常也叫未检查(非受检)异常,意味着检查一下就可以避免,此类异常有除数为0、空指针异常、下标越界异常、类型转换异常都继承自RuntimeException。
- 已检查(受检、受查)异常,在编译时,会提示有异常需要处理。程序员在程序中即使检查过了,还是有可能出现异常,因为该异常是由用户引发的。此类异常就需要异常处理。
三、异常的产生
1、程序运行时出现了异常。
2、手动使用throw 异常对象来抛出一个异常。
结果:当出现异常时,程序会终止,相当于return。
public class TestMain2 {
public static void main(String[] args) {
min(null);
}
public static int min(int [] arr) {
if(arr == null || arr.length == 0) {
// 报个错
throw new RuntimeException("数组为空");
}else {
// 循环比较,并返回最小值
int min = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i] < min) {
min = arr[i];
}
}
return min;
}
}
}
四、异常的传递
throw:是在方法的执行过程中产生一个异常。
throws:是方法的声明上声明一个方法中可能要抛出的异常。
在调用一个方法时,如果该方法声明有异常,调用者可以将异常再次抛出,即为异常的传递。
注意: 如果所有的方法均不处理异常,都进行抛出,最终将抛到JVM中,由JVM处理。JVM处理的方式是直接停止程序,并打印异常堆栈信息。
public class TestMain2 {
public static void main(String[] args) throws Exception {
test();
}
public static void test() throws Exception {
min(null);
}
public static int min(int [] arr) throws Exception { // 声明异常
if(arr == null || arr.length == 0) {
// 报个错
// 产生异常
throw new Exception("数组为空");
}else {
// 循环比较,并返回最小值
int min = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i] < min) {
min = arr[i];
}
}
return min;
}
}
}
如果在方法中产生了多种类型的异常,方法异常声明时,需要声明多个类型的异常,采用逗号隔开。
public static int min(int [] arr, String dateStr) throws ParseException, SQLException {
}
注意:在方法重写时,如果方法声明了异常,那么重写后也必须声明对应的异常。
五、异常的处理
如果在遇到异常时,不需要抛出异常,可以处理异常。
语法:
try{
// 尝试可能会出现异常的代码
}catch(Exception e){
// 当出现异常时处理的代码
}finally{
// 无论是否出现异常,必定会执行的代码
}
组合方式:
1、try-catch
2、try-finally
3、try-catch-finally
注意:catch可以有多个。即catch可以捕获多种异常,注意异常捕获也是自上而下执行,所以类型越宽泛,越应该放到最后,Exception要放到最后,因为Exception是所有异常的父类。
5.1 try
在try中,一旦代码出现了异常,则自异常处开始,后面的代码均不会执行。
注意:在程序中,如果代码多个地方可能会出现异常,但是这些代码之间没有直接关联,那么不应该将其放到一个try中,应该使用多个try,保证其他代码的正常执行。
public class TestMain4 {
public static void main(String[] args) {
int n = 0;
String str = null;
System.out.println("程序开始");
System.out.println("程序执行第1行");
try {
int m = 20 / n;
System.out.println(m);
} catch (Exception e) {
}
System.out.println("程序执行第6行");
try {
System.out.println(str.length());
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("程序执行第11行");
System.out.println("程序结束");
}
}
5.2 catch
catch表示捕获异常,即程序如果出现异常,则会执行catch中的内容。
注意:在catch中一般会打印异常信息,并进行相应的处理。
public class TestMain4 {
public static void main(String[] args) {
int n = 0;
String str = null;
int m;
System.out.println("程序开始");
System.out.println("程序执行第1行");
try {
m = 20 / n;
System.out.println(m);
} catch (Exception e) {
// 打印错误的堆栈信息
e.printStackTrace();
// 得到错误信息
String message = e.getMessage();
System.out.println(message);
// 给m赋值,出现异常时,默认值
m = 5;
}
System.out.println("程序执行第6行");
System.out.println("程序结束");
}
}
当一个方法中出现了多种异常时,应该使用多个异常处理的方式,进行不同的处理。
public static void main(String[] args) {
try {
min(null, "2020-10-10");
} catch (ParseException e) {
// 当出现第一种异常时,在此处处理
e.printStackTrace();
} catch (SQLException e) {
// 当出现第二种异常时,在此处处理
e.printStackTrace();
}
}
}
如果所有的异常均使用一种方式处理,也可以直接处理父类异常。
try {
min(null, "2020-10-10");
} catch (Exception e) {
// 直接处理Exception
e.printStackTrace();
}
注意:如果不确定处理处理的异常之外,是否还有别的异常没有处理,建议在异常处理时,添加上Exception,而且要放到最后。
public static void main(String[] args) {
System.out.println("程序开始");
try {
min(null, "2020-10-10");
} catch (ParseException e) {
// 当出现第一种异常时,在此处处理
e.printStackTrace();
} catch (SQLException e) {
// 当出现第二种异常时,在此处处理
e.printStackTrace();
}catch(Exception e) {
// 当出现其他的所有异常时,在此处处理
e.printStackTrace();
}
}
5.3 finally
finally表示最终必定执行的。一般作用是关闭资源等。
public class TestMain5 {
public static void main(String[] args) {
System.out.println(m1(5));
}
public static int m1(int n) {
try {
if(n > 0) {
return 5;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("我是finally"); // 会执行,在return前执行
}
return 3;
}
}
在finally中不应该写return,因为此时无论代码如何执行,都会执行finally中的return,没有意义。
public class TestMain5 {
public static void main(String[] args) {
System.out.println(m1(5));
}
public static int m1(int n) {
try {
if(n > 0) {
return 5;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("我是finally");
return 4; // 一定会返回4,没有意义
}
}
}
以下代码结果是:
public class TestMain6 {
public static void main(String[] args) {
System.out.println(m1());
}
public static int m1() {
int m = 3;
try {
int n = 5 / 0;
m = 4;
return m;
} catch (Exception e) {
e.printStackTrace();
m = 5;
return m;
}finally {
m = 6;
System.out.println("finally");
}
}
}
结果为:5
在catch中给m赋值为5,return是将m的值5放入栈顶,准备返回5。接下来执行了finally中的代码,将m赋值为6,此处m改变为6,但是return的还是5。
public class TestMain7 {
public static void main(String[] args) {
System.out.println(m1());
}
public static Student m1() {
Student stu = null;
try {
int n = 5 / 0;
stu = new Student("张三", 20); // 0x1fff
return stu;
} catch (Exception e) {
e.printStackTrace();
stu = new Student("李四", 21); // 0x2fff
return stu;
}finally {
stu = new Student("王五", 22); // 0x3fff
System.out.println("finally");
}
}
}
结果是 new Student(“李四”, 21)对象,原因是在finally中改变了stu指向的地址,而在catch中已经将原地址放到栈顶,等待返回,所以stu的值该变了,对返回结果没有影响。
public class TestMain7 {
public static void main(String[] args) {
System.out.println(m1());
}
public static Student m1() {
Student stu = null;
try {
int n = 5 / 0;
stu = new Student("张三", 20); // 0x1fff
return stu;
} catch (Exception e) {
e.printStackTrace();
stu = new Student("李四", 21); // 0x2fff
return stu;
}finally {
stu.setName("王五");
stu.setAge(22);
System.out.println("finally");
}
}
}
结果是 new Student(“王五”, 22)对象,原因是在finally中改变的是stu的地址指向对象的属性,而在catch中已经将地址放到栈顶,等待返回,所以stu的地址没有改变,但是属性值改变了,返回结果显示是原地址对应的改变后的属性值。
六、自定义异常
一般在项目中,需要自定义一些业务相关的异常。定义时继承Exception类即可,如果需要传递异常的信息,可以定义构造方法调用父类的构造方法。
public class ArrayNullException extends Exception{
public ArrayNullException(String message) {
super(message);
}
}
public static int min(int [] arr, String dateStr) throws ArrayNullException {
if(arr == null || arr.length == 0) {
// 报个错
// 产生异常
throw new ArrayNullException("数组为空");
}else {
// 循环比较,并返回最小值
int min = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i] < min) {
min = arr[i];
}
}
return min;
}
}
七、项目中异常的使用方式
7.1 抛出异常还是处理异常?
调用者能够处理就抛出,调用者不能处理就自己处理。
例如:一个用户登录场景,用户输入用户名和密码,将信息从界面传递到后台,后台调用登录方法。该方法中假设有两个异常:1、业务异常:用户输入的用户名或密码不正确。2、系统异常:数据库连接失败。
这两种异常用户名或密码不正确异常应该抛出,交给调用者,然后由调用者显示给用户,以期待用户再次输入获取不一样的结果。而数据库连接失败异常应该在方法中直接处理(写入日志,让管理员在修复时有迹可循),不应该抛出。
7.2 业务场景很多,应该定义多少个异常类?
项目中业务场景会很多,该定义多少个业务异常类?实际在项目开发中,一般所有的业务异常只需要定义一个业务异常类,所有的场景使用不同的异常信息(按照业务规则来定义的一个编码,称为消息code)来描述,例如:404,500