关于Java的异常,我们认为符合大致分为以下几种情况:
Java
异常类体系结构如下图所示:
Exception或者Exception自类的异常对象是可以进行捕获和处理的,例如下面这段算术异常,我们就可以手动捕获并打印处理掉:
try {
for (int i = 0; i < 30_0000; i++) {
int num = 10 / 0;
System.out.println("Fnum:" + num);
}
} catch (Exception e) {
System.out.println("outerTryCatch 报错");
}
Exception
包含受检异常和非受检异常,他们区别是:
关于受检异常和非受检异常我们后文给出示例。
这种错误一般都是OOM
、Java虚拟机运行错误(Virtual MachineError)
、或者类定义错误(NoClassDefFoundError)
,基本都是服务崩溃,无法捕获处理,只能由开发人员在发现问题后通过日志定位异常代码修复重启才能保证服务正常运行。
受检异常一般在调用时,用户就需要对其进行处理,如果不处理则不会通过编译。
例如FileInputStream
,如果我们没有对其构造方法抛出的错误(即受检异常)
进行处理,我们是无法通过编译的。
查看该构造方法定义,可以FileInputStream受检的异常FileNotFoundException已经定义在方法签名上。
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
非受检异常一般是运行时异常,常见的有空指针异常、非法参数异常、算数异常、数组越界等,这类异常可以按需进行捕获处理。
例如下面这段代码就会抛出算术异常,我们即使没有使用try代码块,代码也能通过编译并运行(直到报错)。
int num = 10 / 0;
System.out.println("Fnum:" + num);
它们唯一的区别就在定义的位置:
throws
放在函数上,throw
放在函数内。throws
可以跟多个错误类,throw
只能跟单个异常对象返回异常的错误的简要信息,它不会打印异常堆栈,只是打印异常的原因,例如这段代码,它的打印结果就是/ by zero
,并不会输出运行报错的具体调用堆栈信息。
try {
for (int i = 0; i < 30_0000; i++) {
int num = 10 / 0;
System.out.println("Fnum:" + num);
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
我们在catch模块调用异常类的toString方法,它会返回异常发生时的异常信息,同样不会打印堆栈结果,所以对于只需要看到异常信息的话,可以使用toString。
catch (Exception e) {
System.out.println(e.toString());
}
输出结果:
java.lang.ArithmeticException: / by zero
返回异常本地化信息。
catch (Exception e) {
System.out.println(e.getLocalizedMessage());
}
输出结果:
/ by zero
在控制台上打印堆栈追踪信息,我们可以详细看到异常的调用堆栈信息和错误原因。
catch (Exception e) {
e.printStackTrace();
}
输出结果:
java.lang.ArithmeticException: / by zero
at com.sharkChili.base.Main.outerTryCatch(Main.java:17)
at com.sharkChili.base.Main.main(Main.java:7)
我们自定义一个受检异常
public class ArithmeticException extends Exception {
@Override
public String getMessage() {
return "自定义算术异常";
}
}
编写一个除法的函数
private int calculate(int number,int divNum) throws ArithmeticException {
if (divNum==0){
throw new ArithmeticException();
}
return number / divNum;
}
测试代码如下,因为是受检异常,所以运行时需要手动捕获处理一下。
@Test
public void calculateTest(){
int number=20;
try {
int result = calculate(number,0);
System.out.println(result);
} catch (ArithmeticException e) {
logger.error("calculateTest计算失败,请求参数:[{}],错误原因[{}]",number,e.getMessage(),e);
}
}
输出结果:
[main] ERROR com.guide.exception.ExceptionTest - calculateTest计算失败,请求参数:[20],错误原因[自定义算术异常]
try-catch
感知和捕获异常的工作过程如下:
try
块代码执行报错若有catch
模块catch
则会对其进行捕获处理。例如我们上文捕获了ArithmeticException
,那么catch (ArithmeticException e)
实质上会做一个Exception e = new ArithmeticException()
的动作,进而按照用户的想法进行错误捕获逻辑处理。
对于多异常需要捕获处理时,我们建议符合以下三大原则:
exception
放在最下方。|
进行归类整理。如下所示,我们自定义一个自定义错误函数
private int calculate(int number,int divNum) throws ArithmeticException,FileNotFoundException, UnknownHostException, IOException {
if (divNum==0){
throw new ArithmeticException();
}
return number / divNum;
}
假定UnknownHostException
是用户配置问题,我们无法处理,那么就抛出,其他错误一一捕获,所以我们的代码可能是这样。
@Test
public void calculateTest(){
int number=20;
int result = 0;
try {
result = calculate(number,0);
} catch (ArithmeticException e) {
e.printStackTrace();
} catch (FileNotFoundException e){
}catch (IOException e) {
e.printStackTrace();
}
System.out.println(result);
}
实际在为了让代码更加整洁高效。生成的catch
块也只是一个公共的代码块,所以我们最终的代码应该是下面这个样子
@Test
public void calculateTest()throws UnknownHostException{
int number=20;
int result = 0;
try {
result = calculate(number,0);
} catch (ArithmeticException|FileNotFoundException e) {
logger.error("calculateTest执行报错,用户执行出错或者文件数据获取失败,请求参数[{}],错误信息[{}]",number,e.getMessage(),e);
} catch (IOException e) {
logger.error("calculateTest执行报错,文件操作异常,请求参数[{}],错误信息[{}]",number,e.getMessage(),e);
}catch (Exception e){
logger.error("calculateTest执行报错,请求参数[{}],错误信息[{}]",number,e.getMessage(),e);
}
System.out.println(result);
}
注意,使用|
运算符之后e
会变为final
变量,用户无法改变引用的指向(这个似乎对我们没有说明影响)
。
使用runtime
异常类时,在函数内throw
则函数上不用throws
,编译可以正常通过。如下代码,即使没有throws ArithmeticException(ArithmeticException为runtime的子类)
,编译照样通过,代码可以正常运行,直到报错。
class Demo{
int div(int a,int b)throws Exception//throws ArithmeticException{
if(b<0)
throw new Exception("出现了除数为负数了");
if(b==0)
throw new ArithmeticException("被零除啦");
return a/b;
}
}
RuntimeException
可以编译通过且不用捕获的原因也很简单,设计者认为运行时异常是特定情况下运行时错误,是否处理需要让开发人员自行判断。
finally
无论异常是否执行,在try
块结束后必定会运行的,需要注意的是如果程序出现异常,finally
中有return
语句的话,catch
块的return
将没有任何作用,代码如下所示
public static int func() {
try {
int i = 1 / 0;
} catch (Exception e) {
return 2;
} finally {
return 1;
}
}
finally最常用于资源释放:
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
补充一句,对于资源释放,我们现在更简易使用try-with-resources
代码简洁易读。上述那段关闭流的代码十分冗长,可读性十分差劲,对于继承Closeable
、AutoCloseable
的类都可以使用以下语法完成资源加载和释放
try (Scanner scanner = new Scanner(new File("D://read.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
不一定,如下代码所示,当虚拟机执行退出的话,finally
是不会被执行的
@Test
public void finallyNoRun() {
try {
System.out.println("try code run.....");
System.exit(0);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("finally run...");
}
}
输出结果如下,可以看到finally方法并没有被运行到。
try code run.....
函数执行的try块返回值会被缓存的本地变量中,当finally进行return操作就会覆盖这个本地变量。
public void finallyReturnTest() {
System.out.println(finallyReturnFun());
}
private String finallyReturnFun() {
try {
int ten = 10;
return "tryRetunVal";
} catch (Exception e) {
System.out.println("报错了。。。。");
} finally {
return "finallyReturnVal";
}
}
输出结果为finallyReturnVal,因为finally优先于try块中的代码,当finally进行return操作就会覆盖这个本地变量。
finallyReturnVal
捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请
将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
自定义一个异常
public class MyIllegalArgumentException extends Exception {
public MyIllegalArgumentException(String msg) {
super(msg);
}
@Override
public String getMessage() {
return super.getMessage();
}
}
测试代码
public void checkTest() {
String param = null;
try {
check(null);
} catch (MyIllegalArgumentException e) {
logger.info("参数校验异常,请求参数[{}],错误信息[{}]", param, e.getMessage(), e);
}
}
private void check(String str) throws MyIllegalArgumentException {
if (str == null || str.length() <= 0) {
throw new MyIllegalArgumentException("字符串不可为空");
}
}
输出结果:
[main] INFO com.guide.exception.ExceptionTest - 参数校验异常,请求参数[null],错误信息[字符串不可为空]
不要使用try块语句控制业务执行流程,原因如下:
try-catch
阻止JVM
试图进行的优化,所以当我们要使用try
块时,使用的粒度尽可能要小一些。try
块解决问题。错误示例,不仅可读性差,而且性能不佳。
public static void stackPopByCatch() {
long start = System.currentTimeMillis();
try {
//插入1000w个元素
Stack stack = new Stack();
for (int i = 0; i < 1000_0000; i++) {
stack.push(i);
}
//使用pop抛出的异常判断出栈是否结束
while (true) {
try {
stack.pop();
} catch (Exception e) {
System.out.println("出栈结束");
break;
}
}
} catch (Exception e) {
}
long end = System.currentTimeMillis();
System.out.println("使用try进行异常捕获,执行时间:" + (end - start));
start = System.currentTimeMillis();
Stack stack2 = new Stack();
for (int i = 0; i < 1000_0000; i++) {
stack2.push(i);
}
//使用for循环控制代码流程
int size = stack2.size();
for (int i = 0; i < size; i++) {
stack2.pop();
}
end = System.currentTimeMillis();
System.out.println("使用逻辑进行出栈操作,执行时间:" + (end - start));
}
输出结果,可以看到用try块控制业务流程性能很差:
出栈结束
使用try进行异常捕获,执行时间:2613
使用逻辑进行出栈操作,执行时间:1481
JSON
工具,因为某些get
方法可能会抛出异常。
public void logShowTest() {
Map inputParam = new JSONObject().fluentPut("key", "value");
try {
logShow(inputParam);
} catch (ArithmeticException e) {
logger.error("inputParam:{} ,errorMessage:{}", inputParam.toString(), e.getMessage(), e);
}
}
private int logShow(Map inputParam) throws ArithmeticException {
int zero = 0;
if (zero==0){
throw new ArithmeticException();
}
return 19 / zero;
}
输出结果:
[main] ERROR com.guide.exception.ExceptionTest - inputParam:{"key":"value"} ,errorMessage:自定义算术异常
com.guide.exception.ArithmeticException: 自定义算术异常
at com.guide.exception.ExceptionTest.logShow(ExceptionTest.java:166)
如下代码所示,可以看到频繁抛出和捕获对象是非常耗时的,所以我们不建议使用异常来作为处理逻辑,我们完全可以和前端协商好错误码从而避免没必要的性能开销
private int testTimes;
public ExceptionTest(int testTimes) {
this.testTimes = testTimes;
}
public void newObject() {
long l = System.currentTimeMillis();
for (int i = 0; i < testTimes; i++) {
new Object();
}
System.out.println("建立对象:" + (System.currentTimeMillis() - l));
}
public void newException() {
long l = System.currentTimeMillis();
for (int i = 0; i < testTimes; i++) {
new Exception();
}
System.out.println("建立异常对象:" + (System.currentTimeMillis() - l));
}
public void catchException() {
long l = System.currentTimeMillis();
for (int i = 0; i < testTimes; i++) {
try {
throw new Exception();
} catch (Exception e) {
}
}
System.out.println("建立、抛出并接住异常对象:" + (System.currentTimeMillis() - l));
}
public void catchObj() {
long l = System.currentTimeMillis();
for (int i = 0; i < testTimes; i++) {
try {
new Object();
} catch (Exception e) {
}
}
System.out.println("建立,普通对象并catch:" + (System.currentTimeMillis() - l));
}
public static void main(String[] args) {
ExceptionTest test = new ExceptionTest(100_0000);
test.newObject();
test.newException();
test.catchException();
test.catchObj();
}
输出结果:
输出结果
建立对象:3
建立异常对象:484
建立、抛出并接住异常对象:539
建立,普通对象并catch:3
上文提到try-catch时会捕获并创建异常对象,所以如果在for循环内频繁捕获异常会创建大量的异常对象:
public static void main(String[] args) {
outerTryCatch();
innerTryCatch();
}
//for 外部捕获异常
private static void outerTryCatch() {
long memory = Runtime.getRuntime().freeMemory();
try {
for (int i = 0; i < 30_0000; i++) {
int num = 10 / 0;
System.out.println("Fnum:" + num);
}
} catch (Exception e) {
}
long useMemory = (memory - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
System.out.println("cost memory:" + useMemory + "M");
}
//for 内部捕获异常
private static void innerTryCatch() {
long memory = Runtime.getRuntime().freeMemory();
for (int i = 0; i < 30_0000; i++) {
try {
int num = 10 / 0;
System.out.println("num:" + num);
} catch (Exception e) {
}
}
long useMemory = (memory - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
System.out.println("cost memory:" + useMemory + "M");
}
输出结果如下,可以看到for循环内部捕获异常消耗了22M的堆内存。原因很简单,for外部捕获异常时,会直接终止for循环,而在for循环内部捕获异常仅结束本次循环的,所以如果for循环频繁报错,那么在内部捕获异常尽可能创建大量的异常对象。
cost memory:0M
cost memory:22M
Java基础常见面试题总结(下):https://javaguide.cn/java/basis/java-basic-questions-03.html#项目相关
Effective Java中文版(第3版):https://book.douban.com/subject/30412517/
阿里巴巴Java开发手册:https://book.douban.com/subject/27605355/
Java核心技术·卷 I(原书第11版):https://book.douban.com/subject/34898994/
Java 基础 - 异常机制详解:https://www.pdai.tech/md/java/basic/java-basic-x-exception.html#异常是否耗时为什么会耗时
面试官问我 ,try catch 应该在 for 循环里面还是外面?:https://mp.weixin.qq.com/s/TQgBSYAhs_kUaScw7occDg