异常
1. 异常概念
1.1 生活中的异常
异常:异于平常,和平时正常得情况不一样,有问题,有故障,一般情况下是出现了某种错误。
例如:小明每天正常上班,需要做40分钟地铁,但是有一天发生了地铁故障/有人跳轨,结果导致地铁暂停
生活中如果遇到了此类问题,要进行解决。
小明的解决方法是:
-
出了地铁站,打车前往公司;
-
出了地铁站,规划其他方式前往公司!
-
再不行,和领导请假会晚一些。
1.2 程序中的异常
程序中的异常:在程序运行过程中,出现了异常事件,通常是由外部输入或硬件错误导致的,异常会导致程序中断。
Scanner input = new Scanner(System.in); System.out.print("请输入被除数:"); int num1 = input.nextInt(); System.out.print("请输入除数:"); int num2 = input.nextInt(); int result = num1 / num2; System.out.printf("%d / %d = %d", num1, num2, result);
正常情况下,上述程序可以完成连个数值的除法计算。
但如果出现了下方的情况,那就是程序中出现了异常(Exception)。
请输入被除数:4 请输入除数:0 Exception in thread "main" java.lang.ArithmeticException: / by zero at demo1.Demo1.main(Demo1.java:15)
Scanner input = new Scanner(System.in); System.out.print("请输入被除数:"); int num1 = input.nextInt(); System.out.print("请输入除数:"); int num2 = input.nextInt(); if (num2 == 0) { System.out.println("对不起!除数不能为0!"); return; } int result = num1 / num2; System.out.printf("%d / %d = %d", num1, num2, result);
请输入被除数:a Exception in thread "main" java.util.InputMismatchException at java.util.Scanner.throwFor(Scanner.java:864) at java.util.Scanner.next(Scanner.java:1485) at java.util.Scanner.nextInt(Scanner.java:2117) at java.util.Scanner.nextInt(Scanner.java:2076) at demo1.Demo1.main(Demo1.java:11)
Scanner input = new Scanner(System.in); System.out.print("请输入被除数:"); // hasNextInt() : boolean 判断当前输入的内容是否是整数 // hasNextDouble() : boolean 判断当前输入的内容是否是小数 if (input.hasNextInt()) { int num1 = input.nextInt(); System.out.print("请输入除数:"); if (input.hasNextInt()) { int num2 = input.nextInt(); if (num2 == 0) { System.out.println("对不起!除数不能为0!"); return; } int result = num1 / num2; System.out.printf("%d / %d = %d", num1, num2, result); } else { System.out.println("请输入整数!"); } } else { System.out.println("请输入整数!"); }
上述的方式的确可以解决掉问题,但使用 if 来解决异常情况,非常低端,我们无法做到对所有异常情况进行查缺补漏。并且 if 写多了,整个代码可读性变得非常差。
2. Java异常处理机制
在 Java 中提供了大量的异常类型来实现异常的分类,并且提供了几个关键字来实现异常处理。
- Exception:异常类型的顶级父类
- RuntimeException:运行时异常,在运算时可能出现的异常
- inputMismactchException:输入不匹配异常
- NullPointerException:空指针异常,引用数据类型的引用变量赋值为 null
- ArithmeticException:算术异常
- RuntimeException:运行时异常,在运算时可能出现的异常
- Error:错误,只有改代码或是更换硬件、环境才可以解决,程序无法自行修复
- IOError:输入输出流错误
- NoClassDefFoundError:找不到指定的类错误
- NoSuchMethodError:找不到指定的方法错误
- StackOverflowError:栈溢出错误
- OutOfMemoryError:内存溢出错误 OOM
try:尝试,尝试捕获程序中异常
catch:抓住,捕获异常
finally:最终的
throw:抛出,抛出异常
throws:声明异常
默认的情况下,JVM 在执行程序时,如果发生了异常,会立刻结束当前程序,然后根据异常的情况创建对应的异常对象,然后将该异常对象反馈(抛)给你,如果我们自身没有编写任何处理程序,JVM 会将异常对象的堆栈跟踪信息打印在控制台上。
3. try-catch
try { // try 块
// 可能会出现问题的代码
} catch (异常类型 形参名) { // catch 块会声明它可以抓住哪种异常
// 处理该异常的代码
}
使用 try-catch 来解决异常问题,会出现三种情况:
- 没有异常出现,程序正常执行,catch块内容不会生效
- 出现了 catch 块可以捕捉的异常,程序从出现异常位置结束,直接跳转到 catch 块中执行异常处理
- 出现了 catch 块无法捕捉的异常,此时相当于没有添加任何异常处理,JVM 默认在控制台上打印堆栈跟踪信息
4. 多重catch
try {
// 可能会出现问题的代码
} catch (异常类型 形参名) { // catch 块会声明它可以抓住哪种异常
// 处理该异常的代码
} catch (异常类型 形参名) { // catch 块会声明它可以抓住哪种异常
// 处理该异常的代码
} ....
// .........................................
try {
// 可能会出现问题的代码
} catch (异常类型1 | 异常类型2 形参名)
// 处理该异常的代码
它的处理逻辑和多重 if 类似,是自上而下执行 catch 检查,只要有一个 catch 块可以处理出现的异常,那么后续的 catch 块不再生效。
大多数情况下,我们往往会使用下方的写法:
- 所有的异常都可以被捕获到,不用担心有所遗漏
- 只写一个异常类型,非常简洁
try {
// 可能会出现问题的代码
} catch (Exception e) { // catch 块会声明它可以抓住哪种异常
// 处理该异常的代码
}
注意:如果 catch(Exception e) 出现在了多重 catch 块中,记得放在最后(联想多重 if 问题)。
5. try-catch-finally
以后一般会用 finally 解决流资源的关闭。
try {
// 可能会出现问题的代码
} catch () {
// 处理该异常的代码
} finally {
// 无论是否出现异常都要求执行代码
}
[面试题] 如果 finally 和 return 同时出现,执行顺序是什么样的?
会先执行 finally 然后在执行 return。
try {
// 可能会出现问题的代码
} finally {
// 无论是否出现异常都要求执行代码
}
当你不想处理异常时,但有些代码又必须执行,可以简化为 try-finally 的写法。
try 必须和 catch 或 finally 组合使用,不能单独使用
6. 常见的异常处理代码
-
在 catch 块中添加输出语句来提醒错误
System.out.printn();
-
在 catch 块中添加错误输出语句来提醒错误
System.err.println();
-
打印异常的堆栈跟踪信息
printStackTrace()
java.util.InputMismatchException 异常类型 下方是异常出现的定位,越靠上方,代表是越接近根源的位置(根本原因) at java.util.Scanner.throwFor(Scanner.java:864) at java.util.Scanner.next(Scanner.java:1485) at java.util.Scanner.nextInt(Scanner.java:2117) at java.util.Scanner.nextInt(Scanner.java:2076) 越靠下方的是越直接的位置(直接原因) at 全类名.方法名(哪一行) at demo4.Demo1.main(Demo1.java:12)
java.lang.ArithmeticException 异常类型: / by zero 异常提示信息 at demo4.Demo1.main(Demo1.java:16)
-
获取异常的提示信息
getMessage()
-
记录日志,将你的程序运行的过程存储成日志文件
-
根据你的需求,将一些错误的赋值归位
7. 抛出异常
在某个方法中,如果发现有异常的情况,可以添加抛出异常代码。
当程序中出现了 throw,等价于编写了一个 return,只不过 throw 是非正常结束当前方法
public xx xx() {
// throw new XXXException(异常提示信息);
throw 异常对象;
}
例如:
public class Student {
public void setGender(String gender) {
if (!("男".equals(gender) || "女".equals(gender))) {
// 抛出异常
throw new RuntimeException("性别非法!");
}
this.gender = gender;
}
}
Exception in thread "main" java.lang.RuntimeException: 性别非法!
at demo5.Student.setGender(Student.java:20)
at demo5.Test.main(Test.java:7)
return:结束当前方法,返回到方法的调用者位置。
throw:向调用者抛出异常对象,结束当前方法。如果抛出的异常没有处理,那么 JVM 会默认采用它的方案解决。
8. 声明异常
public xxx xx(形参) throws 异常类型1, 异常类型2, ... {
// 方法体
}
[面试题] throw 和 throws 有什么区别?
throw:
- 表示方法内抛出某种异常对象
- 如果异常对象是非 RuntimeException 则需要在方法声明时加上该异常的抛出,即需要加上 throws 语句 或者 在方法体内 try catch 处理该异常,否则编译报错。
- 执行到 throw 语句则后面的语句块不再执行
throws:
- 方法的参数后加上 throws 表示这个方法可能抛出某种异常,需要由方法的调用者进行异常处理。(提醒作用)
- throws 分为自行声明及被迫声明
- 自行声明:当你程序中出现的是运行时异常时,你想提醒调用我们方法的调用者注意处理该异常,可以添加。
- 被迫声明:当你程序中出现的是非运行时异常(受检异常),必须在方法上声明该异常类型,而且调用者在调用该方法时,也必须处理(try-catch)或再次声明(继续 throws)。
9. 自定义异常
创建属于自己的异常类型。
- 创建异常类型
- 继承合适的异常类型父类
- 定义无参和带参构造方法
public class IllegalGenderException extends RuntimeException {
public IllegalGenderException() {
super();
}
public IllegalGenderException(String message) {
super(message);
}
}
10. 捕捉异常
在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。
潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。
当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。
当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。