前言:本篇博客结合培训班视频编写,希望以最通俗易懂的文字,讲明白异常机制。大部分在介绍概念的时候,我都会用自问自答的方式来描述,原因是这样子更加印象深刻。
动力节点:https://www.bilibili.com/video/BV1Rx411876f?p=629
一、异常概述
二、异常继承结构
三、编译时异常和运行时异常
四、异常的处理
- 1)处理方式一:捕获
- 2)处理方式二:抛出
五、异常类的常用方法
- 1)printStackTrace()方法
- 2)getMessage()方法
六、finally的使用
七、自定义异常
八、注意点总结
回到目录
1)什么是异常?
通俗地说,就是程序编译或者执行过程中发生了不正常的情况。
2)java提供异常机制有什么用?
当程序执行过程中出现了不正常情况(异常)的时候,java会把异常的信息打印到控制台上,供程序员参考,程序员就能根据异常信息,对程序进行修改,让程序更加健壮。假如没有异常机制,我们的代码错了也不知道,那显然是不行的。
我们先看看以下的代码:
public class ExceptionTest02 {
public static void main(String[] args) {
int a = 10;
int b = 0;
int c = a/b;
System.out.println(a + "/" + b + "=" + c);
}
}
显然,我们的代码是不正确的,因为数学中,作为分母的 b 是不能为 0 的,我们执行这段代码,发现控制台输出了以下信息。
有了这些信息,我们就得知,原来我们的代码在第5行出错了,且原因是除数为 0,然后我们就能对代码进行修改,让程序更加健壮。
public class ExceptionTest02 {
public static void main(String[] args) {
int a = 10;
int b = 0;
if(b == 0){
//如果b为0,直接return
System.out.println("除数不能为0");
return;
}
int c = a/b; //b不为0就会执行到这里进行运算
System.out.println(a + "/" + b + "=" + c);
}
}
3)为什么我们代码写错的时候,会有异常出现呢?
在java中,万物皆对象,异常也不例外,各种异常都被封装成了类(也就是异常在java中以类的形式存在),当代码写错时,java虚拟机(JVM)会根据异常类创建一个异常对象,将这个对象“抛出”,并且终止我们程序的执行。
在异常类中,都有一个参数类型为字符串的构造方法,参数是字符串类型,传入的是异常的原因
比如我们上面的代码中,假如b为 0 时
int c = a/b; //JVM执行到这里会new异常对象:new ArithmeticException("/ by zero");
然后JVM将异常信息打印在控制台上(先不用关注其底层如何实现,知道是JVM在抛异常对象即可)。
回到目录
下图中,红线均表示继承。
对图(1)的解释
1)Throwable是一个类,该单词的中文翻译是“可抛出的”,也就是继承了该类的Error(错误)和Exception(异常)都
是可以抛出的。这里的抛出是指throw和throws关键字,后面讲。
2)Error是“错误”,Exception是“异常”,错误和异常都会结束程序的执行,但是错误不能被处理,异常可以被处理
。这里的处理是指捕获或者抛出,后面讲。
3)Error子类中,不仅仅包括上图中的两个,但这个不是我们学习的重点,没必要了解太多。Exception子类中,有
一个子类是RuntimeException,RuntimeException下的所有子类都称为运行时异常;除RuntimeException的以外
其他子类,都称为编译时异常。这两种异常同样后面讲。
4)同样的,运行时异常的子类不仅仅只有这些,这里只是列举一些常见的异常。这些我们需要认识,当我们遇到异常
时,就能比较快地知道哪里错误了。
回到目录
在前面我们已经提到,异常在java以类的形式存在,当异常出现的时候,是由JVM去创建一个异常对象并且抛出,我们还知道,创建对象这个过程发生在运行阶段。也就是说,编译时异常和运行时异常都是发生在运行阶段,那么编译时异常这个名称是怎么来的呢?
编译时异常:如下图。显然,在我们写代码的时候,我们的编译器就能识别出某个地方有异常需要先处理。也就是在编译前就能知道异常的存在,如果不处理,编译将不能通过,所以称为“编译时异常”。
运行时异常:看我们前面那个除以 0 的代码,显然,我们在编写代码的时候,编译器并没有发现 int c = a/b; 这一行代码存在异常(也就是这行代码下并没有红色波浪线)。需要在我们执行程序的时候,它才能确定这行代码存在异常,因此称为“运行时异常”。
有些小伙伴可能前面的基础不够扎实,还不知道什么是编译,这里你先简单理解成:
我们在编写java代码的时候,其实是在一个 “类名,java” 的文件上编写,编译能根据这个文件生成另外一个文件,称为字节码文件,文件名是 “类名.class”,只有字节码文件,我们的java虚拟机才能运行,“.java”是运行不了的。
那么也就是说,如果是编译时异常,我们将不能得到 .class 文件,而运行时异常可以,我们来看下图。
图(3)
怎么样?对比其他资料上那些经过多次复制粘贴文字表述,这里是不是清晰了很多。那些学过异常,然后知识不牢固的,相信你看到这里,对编译时异常和运行时异常会有更清晰的了解了。
回到目录
在写异常的处理之前,请大家先记住一件事:编译时异常可以捕获或者抛出(需要处理),运行时异常不需要处理。
现在,先讲前半句,也就是编译时异常的处理方式:
假设有这么个案例,我是做销售的,某次业务中不小心使得公司亏损了1000块钱,我们把这件事当做是java中的异常,我应该怎么做?
1)处理方式一:捕获
第一个方式是我自己掏钱把1000块补上,我自己处理这件事。
在java中,假设一个方法出现了编译时异常,同样的,它也可以通过try...catch...来解决这个异常,这种方式称为
捕获。
针对图(2)中的异常,我们捕获处理的代码如下:(注意看注释,且在执行以下代码之前,请在F盘中创建一个a.txt
文件)
public class ExceptionTest01 {
public static void main(String[] args) {
//try是"尝试"的意思,也就是尝试一下try后面 { } 中的内容。如果有异常,则进行捕捉,即catch环节
//;如果没有异常,则不需要catch,跳过catch之后,继续从System.out.println("skr");开始,往下
//执行
try {
FileInputStream fis = new FileInputStream("F:\\a.txt");
} catch (FileNotFoundException e) {
//catch是"捕捉"的意思,也就是在try中判断出现异常之后,catch将异常抓住并进行处理。e.printStac
//kTrace()就是将异常信息打印在控制台上,其中printStackTrace()所有Throwable子类都具有的方法
e.printStackTrace();
}
System.out.println("skr");
}
}
可能执行上面的代码,你还是不太能理解,执行结果是直接在控制台上打印“skr”,这是因为,我们的 F:\a.txt 文件是确确实实存在于我们的硬盘上的,也就是能找到文件,也就不会出现异常。
我们修改一下代码,改成 F:\b.txt ,此时,文件已经不存在,我们运行一下,结果如下:
图(4)
可以看到,它帮我们将异常信息,包括原因、位置等都打印出来了。还有一个细节,我们注意到,"skr"也被打印出来了!也就是说,异常发生之后,程序还在执行!这和我们前面的说法不同,我们前面是说,发生异常后,程序终止执行,这是为什么呢?带着这个问题,我们来看编译时异常的另一种处理方法。
2)处理方式二:抛出
回到案例,我除了自己掏钱补上,其实我还有另一种处理方式,我可以上报我的组长,让他想办法。
同样的,在java中,如果一个方法出现了异常,那它可以不进行捕捉处理,它可以抛给它的调用者(调用它的另外一
个方法),实现的方式是使用throws关键字。
我们继续来看下面的代码,仍然是注意看注释。
public class ExceptionTest01 {
//3)经理不乐意了,那让董事长(Java虚拟机)去解决吧!继续throws
public static void main(String[] args) throws FileNotFoundException {
lisi();
System.out.println("skr");
}
//2)组长李四犯愁了,他也不想掏腰包,于是继续throws,又抛给了经理main方法
public static void lisi() throws FileNotFoundException {
zhangsan();
}
//1)定义方法,假设张三犯错导致公司亏损,那他赶紧告诉了组长李四
public static void zhangsan() throws FileNotFoundException{
FileInputStream fis = new FileInputStream("F:\\a.txt");
}
}
由于我们的 F:\a.txt 文件是存在的,因此不会出现异常,因此继续执行打印 “skr”;
我们再将 F:\a.txt 修改成 F:\b.txt ,发现报错了,且没有打印 skr 。这是因为,各个方法都不对异常进行处理,Java虚拟机只能将异常信息打印在控制台上,并直接终止程序的运行。
图(5)
假如我们一开始就捕捉处理,就不需要一层层地向上抛了,如下代码
public class ExceptionTest01 {
public static void main(String[] args) {
lisi();
System.out.println("skr");
}
public static void lisi() {
zhangsan();
}
public static void zhangsan(){
try {
FileInputStream fis = new FileInputStream("F:\\a.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
【小总结】
编译时异常有两种处理方式,一种方式是在方法内部自行捕捉处理,这种方式如果出现了异常,会打印异常信息,但不会导致程序的终止,因为异常不是被Java虚拟机发现的;另一种方式是在声明方法的时候,加上throws,将可能出现的异常抛给调用者,如果调用者一直没有处理,直到抛给Java虚拟机,Java虚拟机如果发现有异常,那么就会打印异常信息,并终止程序。
我们用了大篇幅来介绍编译时异常的处理,那么运行时异常呢?其实运行时异常我们不用处理,原因很简单。因为运行时异常即使不处理,也能通过编译。
学到这里,相信你有以下的困惑:
1)为什么异常要分成编译时异常和运行时异常,直接全部作为编译时异常,那我们的程序不就绝对安全了吗?
想法是正确的,确实,这样能保证我们的程序绝对安全,但是这样的话,我们的程序将导到处都是处理异常的代码,
可读性很差。
2)编译时异常有两种处理方式,那我应该选择哪一种呢?
当异常有必要上报,让调用者知道的时候,就需要用throws,你会觉得这是废话,我来解释一下。
我们来看下图。前面我们已经说到,异常在java中是类的形式存在,异常出现后有异常信息的打印是因为创建了异常
对象,然后调用异常对象的方法进行打印的,这里再强调一遍,我们看下面的代码就好理解了。
显然,我们创建完FileNotFoundException对象,就是想要上抛,让我们程序员在创建FileInputStream对象的时候
,能够清楚地知道该构造方法可能会因为找不到文件而出现异常,所以我们程序员才能使用try catch进行处理。
这里为什么使用throws而不使用try catch也就很明显了,假如我们在这里使用try catch的话,那不是搬石砸脚吗?
我们创建了异常对象,却又要捕捉处理它,那不是没事找事做吗?
具体的话,其实等待了实际的开发,我们才能够真正领会到,所以这里不理解也没关系。
回到目录
在前面的学习中,我们捕捉到异常对象之后,打印异常信息都是使用printStackTrace()方法,其实还有其他常用的方法,现在我们来了解一下。
1)printStackTrace()方法
这个方法较为常用,也就是我们上面一直在用的方法。原因是该方法能够打印异常原因,还能打印异常的跟踪路径,如何理解呢?看看这代码和图你就明白了。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest01 {
public static void main(String[] args) throws FileNotFoundException {
m2();
}
public static void m2() throws FileNotFoundException {
m1();
}
public static void m1() throws FileNotFoundException {
FileInputStream fis = new FileInputStream("F:\\b.txt");
}
}
还是找不到文件这个异常,只不过现在我们是要学习如何看异常信息。
上图写的很清楚了,在红框中,是我们需要关注的,且应该直接找最上面那一行。因为最上面那一行是异常出现的根源,它告诉我们,出现异常的根源是在 ExceptionTest01类的 m1() 方法出错,在类的第12行代码。
于是我们去检查第12行,结合异常原因,发现原来是 new FileInputStream() 时,传入的路径 F:\b.txt 有问题,文件不存在。
2)getMessage()方法
getMessage()方法比较少用,但是在某些时候,只能使用该方法,这里不再扩展,我们看一下怎么用就行。
getMessage()方法以字符串形式返回异常原因,所以我们还需要加上打印的代码。
public class ExceptionTest01 {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("F:\\b.txt");
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
}
}
}
这个方法不难,大家直接复制粘贴看看效果就行了。
我们已经知道,try中的代码,如果出现异常,就会到catch步骤;如果没有出现异常,就跳过catch步骤。finally呢?finally就是无论try中的代码有没有出现异常,finally{ }中的代码都会执行。finally一般用于关闭资源。如下代码
public class ExceptionTest01 {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("F:\\a.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally{
//这就是关闭资源,如果你已经学过了IO流,这个不难看懂;没学过也不要紧,
//你只要知道,无论有没有异常发生,finally中的代码都会执行即可
fis.close();
}
System.out.println("skr");
}
}
另外,更离谱的是,即使return结束方法了,finally还是能执行,我们看以下图片
图(8)
finally在学习IO流的时候,就会经常用到。
SUN公司提供的JDK内置异常类在实际的开发中是不够用的,因为实际开发中有很多的业务,这些业务出现异常之后,JDK中没有。所以我们就需要自定义异常。
怎么自定义异常呢?不会就模仿!
我们来看看 ArithmeticException 类的代码是怎么写的。(不需要记代码!稍微瞄一眼就行!我们模仿就好了,以下是SUN公司写的源代码。)
public class ArithmeticException extends RuntimeException {
private static final long serialVersionUID = 2256477558314496007L;
public ArithmeticException() {
super();
}
public ArithmeticException(String s) {
super(s);
}
}
我们发现,ArithmeticException类只有两个构造方法,那么会不会是模仿的关键呢?我们试一下:
自定义一个异常,当银行卡中没有钱,你还要取钱的时候,就会报错。
public class NoMoneyException extends RuntimeException {
public NoMoneyException () {
super();
}
public NoMoneyException (String s) {
super(s);
}
}
我们来测试一下这个自定义的异常类管不管用,代码如下
public class ExceptionTest03 {
private int balance = 100; //余额
public static void main(String[] args) {
ExceptionTest03 test03 = new ExceptionTest03();
test03.getMoney(200);
}
public void getMoney(int money){
//参数是要取得钱
if(money > balance){
throw new NoMoneyException("取的钱比余额多,取钱失败");
}
balance = balance - money;
System.out.println("取了" + money + "钱,卡内剩下" + balance + "钱。");
}
}
我们调用 getMoney() 方法,传入100时,正常打印;传入100以上的钱时,抛出异常,效果如下。说明我们自定义的异常类成功啦!
图(9)
细心的小伙伴应该能够注意到,我们这里继承的是 RuntimeException ,也就是我们自定义的异常类是运行时异常类,那么如何自定义编译时异常类呢?
答案是【继承Exception类】即可,这个小伙伴们自己试一下哈,继承 RuntimeException 改成继承 Exception,我们就能看到 throw new NoMoneyException(“取的钱比余额多,取钱失败”); 这行代码下划红色波浪线了。
在学完以上的知识点之后,我们最后再来看看几个注意点。
1) throws 关键字后面可以接多个异常类
//如下,getMoney()方法同时抛出了两个异常
public void getMoney(int money) throws NoMoneyException,IOException{
}
2) 任何一个调用者都可以 try catch 对异常进行捕捉
在前面张三、组长李四、经理main的案例代码中,其实在zhangsan()、lisi()、main()这三个方法中,都能对异常
进行try catch。不是在 zhangsan() 中才能处理。
3) main() 方法最好不要使用throws
main()方法如果还用throws方法往上抛的话,一旦出现异常,那么程序就会停止执行了。这种在main()方法上加
throws,在实际开发中基本不存在。
因为异常往上抛的目的,是为了提醒程序员在某处可能会出现异常,在可能出现时,及时处理。
4) try中某行出现异常,该行以下的代码都不会再执行
复制执行以下代码,“张三”和“王五”会被打印,“李四”不会。
public class ExceptionTest01 {
public static void main(String[] args){
try {
System.out.println("张三");
FileInputStream fis = new FileInputStream("F:\\b.txt");
System.out.println("李四");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("王五");
}
}
5) catch中的类可以是父类,但是一般不写父类,因为子类会更加精确
其实,我们在 catch( ) 的小括号的是,是可以写成 Exception e 的(这里应用了java的多态特性,Exception
e = new FileNotFoundException("")),而且,无论是写 catch (FileNotFoundException e),还是写
catch (Exception e),其最终打印的异常信息是一样的。
你会觉得,那我就写 Exception e 就好了,多省事。
这样写的可读性很差,程序员看代码的时候,不能精确地知道该异常是什么类型,理解起来就需要更多时间。
6) catch可以多行,可以捕捉多个异常
既然throws后可以多个异常,那try catch相应的,也需要有多个catch来处理这些异常,如下。
public static void main(String[] args) {
ExceptionTest03 test03 = new ExceptionTest03();
try {
test03.getMoney(200);
}catch (NoMoneyException e) {
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
}
7) 父类已经捕捉,子类就没有必要在写了
在上述的代码中,NoMoneyException 和 IOException 都是继承自 Exception 类,那么我们可以直接写父类
Exception。代码如下。与问题(5)同理,为了提高代码可读性,我们一般不这么做。
另外,如果我们捕捉了 Exception 之后,再捕捉 IOException ,就会报错,大家自己试一下。
public static void main(String[] args) {
ExceptionTest03 test03 = new ExceptionTest03();
try {
test03.getMoney(200);
}catch (Exception e) {
e.printStackTrace();
}
}
8) throw和throws的区别
这个问题,其实 图(6) 已经很清楚了,throw是用在方法内,用来创建异常对象之后,将对象抛出到方法体外;throws是用在方法声明语句上,是将 throw 抛过来的异常对象,抛给该方法的调用者。