从事 Java 开发的小伙伴对于“异常”应该不陌生,因为每天都会遇到不少异常,或捕获,或抛出。那究竟什么是异常? 异常即非正常的,不同于平常、一般化的情况 。在平时生活中,医生会说你身体的某个部位有异常,该异常会有什么什么的影响,是由某某原因引起的;我每天都准时打卡,按时上下班,那么我本月的考勤是正常的,反之,但凡有迟到、旷工、早退的情况之一的,我本月的考情就会有 异常 。
而在程序中,代码在运行中如果出现运行错误,程序会终止运行,这时 由于错误导致程序运行终止的情况就是程序出现了异常 。异常并不是指语法错误,因为如果语法错了,编译就通不过,不会产生JVM能够识别的字节码文件,是没法运行起来的,所以只有运行中的程序才会有异常一说。
异常处理是衡量一门语言是否成熟的标准之一, C 系列的语言诸如: Java 、 C++ 、 C 等都支持异常处理,有自己的一套异常处理机制。异常处理机制可以让程序有更好的容错性,使代码更加健壮;但 C 语言却没有异常处理机制, C 程序员一般都是利用方法的返回值来实现异常处理,使用 if + 条件 来判断正常和异常情况,使用特定返回值来表示异常情况。
// 汽车
class Car {
// 汽车是否正常运行
public static final boolean IS_OK = true;
// 汽车运行
public boolean run(int wheelNum) {
if (wheelNum == 4) {
return true;
}
return false;
}
}
// 上班族
class Officer {
private Car car;
public Officer(Car car) {
this.car = car;
}
// 开车去上班
public boolean goWorkByCar(int wheelNum) {
if (car == null) {
// 只能选择其他出行方式了
goWorkByOther();
return false;
}
boolean result = car.run(wheelNum);
if (result) {
System.out.println("升职,加薪,迎娶白富美");
return true;
}
System.out.println("迟到,扣薪,领导天天喷");
return false;
}
// 其他方式上班
public void goWorkByOther() {
System.out.println("其他方式上班");
}
}
// 运行demo
public class WorkDemo {
public static void main(String[] args) {
Car car = new Car();
Officer officer = new Officer(car);
officer.goWorkByCar(4);
}
}
上述案例中,只是简单粗暴的把汽车的状态分为 true 和 false 两种,细化不够,不能体现出汽车的详细状态;业务逻辑也很局限,如果要拓展,就要花费更大的成本。
针对于上述的问题, Java 基于面向对象的思想提出了解决方案:
Java 异常体系: Java API 文档中的详细介绍如下
JVM 相关的不可修复的错误,如:系统崩溃、内存溢出、 JVM 内部错误等,由 JVM 抛出,我们一般情况下不需要处理,几乎其所有的子类都是以 “Error” 作为类名后缀;比如: StackOverflowError ,当应用程序递归太深而发生内存溢出时,就会抛出该错误。 “Exception” 作为类名的后缀; Java 体系中, Throwable 类是所有 错误 和 异常 的父类; 当出现了没见过的异常时,可以将异常类的类名拿到 Java API 文档中去查找,通过文章介绍即可获得异常的详细信息,以及其在 Java 中的继承、实现体系;常见的 Exception 有:
null 的时候,对该对象做操作时会出现该异常; public class ExceptionDemo {
public static void main(String[] args) {
Integer.valueOf("laofu");
}
}
运行结果如下:
如果出现异常,会立刻中断运行中的程序,所以必须处理异常,而处理方式有两种:
出现异常之后,程序会中断执行,所以异常必须处理;处理的方式有两种: 捕获 和 抛出, 两种方式二选一即可。我们先来运行一个案例来证明:
public static void main(String[] args) {
System.out.println("老夫开始啦");
int result = 10 / 2;
System.out.println("10 / 2 = " + result);
System.out.println("老夫去也");
}
运行结果如下:
老夫开始啦 10 / 2 = 5 老夫去也
通过查看运行结果,是我们期望的运行结果,代码运行成功;那么接下来我们对上述案例稍作修改,再来看其运行结果如何:
public static void main(String[] args) {
System.out.println("老夫开始啦");
int result = 10 / 0;
System.out.println("10 / 2 = " + result);
System.out.println("老夫去也");
}
运行结果如下:
老夫开始啦
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.main(Main.java:5)
通过查看运行结果,运行结果并不是我们想要的,代码中出现了异常,代码被中断运行。对比两次的运行结果,我们可以得出结论: 出现异常之后,程序会中断执行,异常必须处理。
try-catch 异常捕获:使用try-catch捕获单个异常,语法如下:
try{
编写可能会出现异常的代码
} catch(异常类型 e) {
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
注意:try和catch都不能单独使用,必须连用
所以可以对上述出现异常的代码使用 try-catch 来处理:
public class ExceptionDemo {
public static void main(String[] args) {
System.out.println("老夫开始啦");
try {
int result = 10 / 0;
System.out.println("10 / 2 = " + result);
} catch (ArithmeticException e) {
System.out.println("异常信息:" + e.getMessage());
System.out.println("异常对象:" + e.toString());
System.out.println("异常栈追踪:");
e.printStackTrace();
}
System.out.println("老夫去也");
}
}
运行结果如下:
老夫开始啦异常信息:/ by zero
异常对象:java.lang.ArithmeticException: / by zero
异常栈追踪:老夫去也java.lang.ArithmeticException: / by zero
at Main.main(Main.java:6)
通过查看运行结果,不难发现,使用 try-catch 之后,程序遇到异常时不再中断执行,而是跳过异常代码及其之后的在 try-catch 中的剩余代码语句,来到 catch 代码块中执行我们定义的异常处理代码;
在上述案例中是打印出了异常信息,异常对象信息,异常栈追踪;其中的3个方法都是 Throwable 类的方法:
String getMessage() :获取异常的详细描述信息; String toString() :获取异常的类型、异常描述信息; void printStackTrace() :打印异常的跟踪栈信息并输出到控制台,但不能在 System.out.println() 中使用该方法;其中包含了异常的类型、异常的原因、异常出现的位置;在开发和调试阶段,该方法都很有用,方便调试和修改;
使用try-catch捕获多个异常:语法如下:
try{
编写可能会出现异常的代码
} catch (异常类型A e){ // 当try中出现A类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
} catch (异常类型B e){ // 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
注意
catch 语句,只能捕获一种类型的异常,如果需要捕获多种异常类型,就得使用多个 catch 语句; try-catch 中的代码在只会出现一种类型的异常,只能一个 catch 捕获,不可能同时匹配多个 catch ; catch 语句的代码中出现异常时,会从上到下依次匹配 catch 语句,所以多个 catch 语句应该按照从子类到父类的顺序依次定义; catch 之后,便不会匹配剩余的 catch ,而是会跳出 try-catch ,执行之后的代码;
运行结果如下:
老夫开始啦 NumberFormatException 老夫去也 java.lang.NumberFormatException: For input string: "laofu" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at Main.main(Main.java:7)
抛出异常有两种方式:
private static int divide(int num1, int num2) throws Exception {} throw语句:运用于方法内部,抛出一个具体的异常对象,中止方法的执行,其语法格式如下:
throw new 异常类("异常信息");
一般的,当一个方法出现异常的情况,我们不知道该方法应该返回什么时,此时就可以返回一个错误,在 catch 语句块中使用 throw 继续向上抛出异常。 return 是返回一个值, throw 是返回一个错误,返回给该方法的调用者。比如: String 类的 charAt 方法就是一个很好的例子:
如果每一个方法都放弃处理异常都直接通过 throws 声明抛出,最后异常会抛到 main 方法,如果此时 main 方法还不处理,会继续抛出给 JVM , JVM 底层的处理机制就是打印异常的跟踪栈信息; runtime 异常,默认就是这种处理方式。
一同:方法的签名必须相同。
一大:子类方法的访问权限必须大于等于父类方法的访问权限。
异常( Exception )根据其在编译时期还是运行时期去检查异常可分为: checked异常 和 runtime异常,
RuntimeException 自身及其子类异常都属于 runtime异常 ; runtime异常 之外的其他异常(包括 Exception 自身)都属于 checked异常 ;
Java 中有着不同的定义好的异常类,分别表示着某一种具体的异常情况,在开发中总是有些异常情况是 Java SE 库中没有定义好的,此时就可以根据自己业务的异常情况来定义异常类;我们把这样的异常类称为自定义异常类。
java.lang.Exception ; java.lang.RuntimeException ; 一般在开发中,自定义的异常都是运行时异常。
异常转译:位于最外层的业务系统不需要关心底层的异常细节,我们通过 捕获原始的异常 , 将其转换为一个新的不同类型的异常 , 然后再向上抛出 ;这个过程称为 异常转译 。
在上述例子中:我的车坏了,在 catch 中重新抛出一个新的异常( OfficerException )给我的调用者(老板),不能把车的异常信息抛给老板看,因为老板不关心这些细节,关心的我是否迟到。
异常链:把原始异常包装为新的异常类,形成多个异常的有序排列;异常链由于更加清楚、准确的定位异常出现的位置;在下述案例中,异常一层层抛出,直至异常被处理,在这个过程中,异常链就产生了:
1.增强的throw :对比 Java 6 和 Java 7 中对于抛出异常的改进来体现
2.多异常捕获:重写捕获多个异常案例来体现
3.自动资源关闭:资源类必须直接或者间接实现 java.lang.AutoCloseable 接口
finally 语句块表示 无论如何 (也包括发生异常时)都会 最终执行 的代码块,比如:当我们在 try 语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),在使用完之后,都得最终关闭打开的资源。
1. try...finally:此时没有 catch 来捕获异常,因为此时根据应用场景会抛出异常,我们程序员自己不处理;
2. try...catch....finally:程序员自己需要处理异常,最终得手动关闭资源。
需要注意的是: finally不能单独使用 。
当只有在 try 或者 catch 中 调用退出JVM的相关方法 , 此时 finally 才不会执行 ,否则 finally 修饰的代码块永远会执行。比如: System.exit(0); // 退出JVM
如果finally和return语句同时存在,永远返回finally中的结果,但是应该避免这种情况的出现。详情看如下的案例:
如果 finally 和 return 语句同时存在,永远返回 finally 中的结果
还有另一种情况也很有趣,一起来看看:
为什么会出现这种情况呢?首先 finally 肯定是会被执行的,所以 a++ 之后a的值变成了 14 ,但是 finally 中没有返回值,值为 14 的变量 a 并没有被返回;然后接着执行 return a; 这里的 a 的值在方法执行之初就已经确定了,故返回的值是 13 。
try-catch 的存在也会影响性能,尽量缩小 try-catch 的代码范围; Java doc ,如果自定义了异常或某一个方法抛出了异常,应该在文档注释中详细说明; NullPointerException 等; try-catch 块,切忌将几百行代码放到一个 try-catch 块中; try-catch ); RuntimeException 类型的,并且要尽量避开已存在的异常; 1. 五个关键字: try、catch、finally、throw、throws ;
2. 异常体系的两个继承结构:
Throwable 类有两个子类: Error 和 Exception 。 Exception 类有一个特殊的子类: RuntimeException 。 RuntimeException 类及其子类称之为 runtime异常 。
Exception 类和子类中除了 RuntimeException 体系的其他类称之为 checked异常 。
3. 自定义异常类
4. Error 和 Exception 的区别和关系
5. checked 异常和`runtime异常的区别
6. finally 关键字及其相关知识
7. finally 和 return 的执行顺序
8. throw 和 throws 的区别
完结。老夫虽然不正经,但老夫一身的才华!