前言
在编程中异常报错是不可避免的。特别是在学习某个语言初期,看到异常报错就抓耳挠腮,常常开玩笑说编程 1 分钟,改 bug1 小时。今天就让我们来看看什么是异常和怎么合理的处理异常吧!
异常与 error 介绍
下面还是先让我们来看一下基本概念吧!
异常 指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。异常机制本质就是当程序出现错误,程序安全退出的机制。在 Java 的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。
Java 是采用面向对象的方式来处理异常的。处理过程:
- 抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给 JRE。
- 捕获异常:JRE 得到该异常后,寻找相应的代码来处理该异常。JRE 在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
让我们来看看前面所讲到的异常类究竟是个什么东西!
其实所有的异常对象都是派生于 Throwable 类的一个实例。如果内置的异常类不能够满足需要,还可以创建自己的异常类。所有异常的根类为 java.lang.Throwable。看看它的家族长什么样。
Throwable 类下面主要是两大门派。Error和Exception。
- Error是程序无法处理的错误,表示运行应用程序中较严重问题,系统 JVM 已经处于不可恢复的崩溃状态中。例如,说内存溢出和线程死锁等系统问题。
-
Exception是程序本身能够处理的异常。
Exception 类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。通常 Java 的异常可分为:
- RuntimeException 运行时异常
- CheckedException 已检查异常
下面我们来研究研究这两个异常。
RuntimeException 和 CheckedException 异同
首先我们先来看看什么是 运行时异常。
这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常, 而是经常需要通过增加“逻辑处理来避免这些异常”。
比如以下常见的几种异常:
ArithmeticException 异常
int b=0;
System.out.println(1/b);
// 解决:if(b!=0){System.out.println(1/b);
}
NumberFormatException 异常
String str = "1234abcf";
System.out.println(Integer.parseInt(str));
// 解决:Pattern p = Pattern.compile("^\\d+$");
Matcher m = p.matcher(str);
if (m.matches()) { // 如果 str 匹配代表数字的正则表达式, 才会转换
System.out.println(Integer.parseInt(str));
}
ClassCastException 异常
Animal a=new Dog();
Cat c=(Cat)a;
// 解决:if (a instanceof Cat) {Cat c = (Cat) a;
}
这里再补充两点,方便大家更好的理解 java 异常的机制和处理过程。
- 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
- 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着 Java 程序的终止。
上面我们讲述了什么是运行时异常以及一些处理方式,下面就再来看看什么是 已检查异常 吧!
所有不是 RuntimeException 的异常,统称为 Checked Exception,又被称为“已检查异常”,如 IOException、SQLException 等以及用户自定义的 Exception 异常。这类异常在编译时就必须做出处理,否则无法通过编译。
通常异常的处理方式有两种:
- 使用“try/catch”捕获异常
- 使用“throws”声明异常。
下面就来详细的聊聊吧!
异常的处理
上面已经提了,异常处理通常有 2 种方式。先看看捕获异常吧。
捕获异常 是通过 3 个关键词来实现的:try-catch-finally。用 try 来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉 (catch) 并处理它,最后一步是通过 finally 语句为异常处理提供一个统一的出口,finally 所指定的代码都要被执行。
这个捕获异常其实也是我们在面试的时候会经常碰到的问题。下面我们分别再来对各个部分做一个简单的提示吧!
- try
一个 try 语句必须带有至少一个 catch 语句块或一个 finally 语句块。当异常处理的代码执行结束以后,不会再回到 try 语句去执行尚未执行的代码。
- catch
每个 try 语句块可以伴随一个或多个 catch 语句,用于处理可能产生的不同类型的异常对象。在此介绍一些常用的方法,这些方法均继承自 Throwable 类。
- toString ()方法,显示异常的类名和产生异常的原因。
- getMessage()方法,只显示产生异常的原因,但不显示类名。
- printStackTrace()方法,用来跟踪异常事件发生时堆栈的内容。
这里有一个需要特别注意的地方,那就是 catch 捕获异常时的捕获顺序:
如果异常类之间有继承关系,在顺序安排上就需注意。越是顶层的类,越放在下面,再不然就直接把多余的 catch 省略掉。也就是说先捕获子类异常再捕获父类异常。
- finally
finally 语句块中始终都要执行,除了遇到了 System.exit(0)结束程序运行。针对这个特性,所以我们通常在 finally 中关闭程序块已打开的资源,比如:关闭文件流、释放数据库连接等。
即使 try 和 catch 块中存在 return 语句,finally 语句也会执行。是在执行完 finally 语句后再通过 return 退出。
在这里就有一道非常经典的一个面试题。
public class Test {public static void main(String[]args) {System.out.println(new Test().test());;
}
static int test(){
int x = 1;
try{retun x;}finally{System.out.print("jdbk"+ ++x);
}
}
}
// 问输出结果?
先解释哈这里存在的玄妙吧!
看了上面的讲述,我们都知道了当 try 和 catch 中有 return 时,finally 仍然会执行,所以正常逻辑来说此题的答案应该是“jdbk2 2”, 但这里存在一个陷阱,那就是:
finally 是在 return 后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保 存起来,不管 finally 中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是 在 finally 执行前确定的。因此正确答案应该是:“jdbk2 1”。
还有一点需要注意的就是:finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值。
接下来再来讲讲 声明异常 吧,它相对来说就比较简单了。
在一些情况下,当前方法并不需要处理发生的异常,而是向上传递给调用它的方法处理。如果一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常,之间以逗号隔开。
public static void readFile(String fileName) throws FileNotFoundException,IOException {}
需要注意的地方就是:
- 在方法重写中声明异常时:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
- 声明异常我们一般在 server 层中。在 controller 层或则数据访问层一般是捕获异常。
自定义异常
我们为什么要自定义异常?还不是因为在程序中,可能会遇到 JDK 提供的任何标准异常类都无法充分描述清楚我们想要表达的问题。此时我们就可以创建自己的异常类,即自定义异常类。
那我们怎么自定义异常类呢?相信你看了上面的异常类的家族图应该就猜到了。不错,自定义异常类只需从 Exception 类或者它的子类派生一个子类即可。如果你继承 Exception 类,则为受检查异常,必须对其进行处理; 如果不想处理,可以让自定义异常类继承运行时异常 RuntimeException 类。通常我们自定义异常类应该包含 2 个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。这里举一个例子。
/**IllegalAgeException:非法年龄异常,继承 Exception 类 */
class IllegalAgeException extends Exception {
// 默认构造器
public IllegalAgeException() {}
// 带有详细信息的构造器,信息存储在 message 中
public IllegalAgeException(String message) {super(message);
}
}
public void setAge(int age) throws IllegalAgeException {if (age < 0) {throw new IllegalAgeException("人的年龄不应该为负数");
}
this.age = age;
}
最后给大家讲述一点使用异常机制的建议:
1. 要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2. 处理异常不可以代替简单测试 — 只在异常情况下使用异常机制。
3. 不要进行小粒度的异常处理 — 应该将整个任务包装在一个 try 语句块中。
4. 异常往往在高层处理。
最后,最近很多小伙伴找我要Linux 学习路线图,于是我根据自己的经验,利用业余时间熬夜肝了一个月,整理了一份电子书。无论你是面试还是自我提升,相信都会对你有帮助!
免费送给大家,只求大家金指给我点个赞!
电子书 | Linux 开发学习路线图
也希望有小伙伴能加入我,把这份电子书做得更完美!
有收获?希望老铁们来个三连击,给更多的人看到这篇文章
推荐阅读:
- 干货 | 程序员进阶架构师必备资源免费送
- 神器 | 支持搜索的资源网站