庆哥是如何理解Java异常的
作者 | ithuangqing
来源 | 编码之外(ID:ithuangqing)
ps:本文节选自《跟着庆哥零基础入门Java》中的异常章节!
什么是异常?
最简单的,看一个代码示例:
public static void main(String[] args) {
int a = 1;
int b = 0;
System.out.println(a / b);
}
这段代码有什么问题?简单,除数不能为0对吧,我们打印输出:
显而易见,程序出问题了,不能正常执行了,这里出现了一些爆红的信息,这些就是异常提示,这就是Java中提供的异常机制,当你的程序存在问题的情况下,会给你打印输出一些信息,这个就叫做异常信息。
字面意思上去理解,所谓“异常”也就是“不正常”,放在代码程序中就是那些导致不能正确执行的问题,比如上述代码,Java就会给你打印出为啥此段代码不能正确执行,给你输出不正常的信息,这样你就可以根据异常信息去修改代码,从而提高代码的健壮性!
详细聊聊异常
以上我们简单看了下一个具体的异常,下面我们就“何为异常”再直白的探讨一下,异常作为一种代码的处理机制,现在基本上大多数的编程语言都包含有这个异常机制,但是,我门熟知的伟大的C语言是没有异常处理机制的。
大多数的高级语言,比如Java,python,C++这些都包含非常完善的异常处理机制,既然有这个玩意,那自然有它的好处,一般来说吧,拥有异常机制可以是我们的代码:
拥有更好的容错性 更加的健壮
那啥意思嘞?啥是容错性,啥又是健壮呢?
首先是容错性,这个通俗来讲,就是可承受错误的范围和概率,比如说我们的程序要是没有异常机制的话,那很多错误是无法承受的,可能一旦出现错误,就会导致我们的系统崩溃出大问题,这个带来的后果可能比较严重,但是具有异常机制,就可以帮助我们去处理一些错误,以至于即使出现错误也不会造成这么严重的后果。
那什么又是健壮呢?这个一般就是说我们的代码比较安全,不容易出现bug,基本上把该想到的情况都想到了,代码编写比较严谨,不容易出错,质量好,这个一般就可以说我们的代码比较健壮。
当然,以上只是我粗浅的理解,希望能够帮助大家对异常机制的理解。
那再来说异常,其实就是不好的东西,比如我们的代码有bug,程序出错等等,这些都是有可能发生的,谁也不能保证自己写的代码一定是正确的,对吧。
异常也就是代码中可能出现的意外情况,这就要求我们在编写代码的时候尽量考虑全面,但是即使你考虑的再全面也不可能将所有的意外情况都考虑进去,所以,实际当中意外情况会有发生的概率,对于这种我们无法考虑周到的意外情况,就需要我们的异常机制去处理了。
Java中的异常
接下来我们来看看Java中的异常,想必大家多多少少都会听说过这样一个异常叫做空指针异常,我们来看代码演示:
NullPointerException nullPointerException = new NullPointerException("空指针异常");
System.out.println(nullPointerException);
可以发现,在Java真实存在NullPointerException这个类,而我们可以通过这个类去创建具体的异常对象,比如这里的空指针异常对象,我们打印输出看看:
如此来看,在Java中,异常是以类的形式存在的,而且我们可以通过这些异常类去创建相应的异常对象,那么我们再来看这段代码:
public static void main(String[] args) {
int a = 1;
int b = 0;
System.out.println(a / b);
}
这里会出现异常,其实实际上就是在运行到“ System.out.println(a / b);”的时候Jvm虚拟机就会在底层为我们创建出一个异常对象,从而将相关的异常信息打印输出。
所以:
Java中异常是的的确确存在的类
Java的异常处理机制
接下来我们来说说Java的异常机制。我们还是来看上面那个代码,也就是这个:
public static void main(String[] args) {
int a = 1;
int b = 0;
System.out.println(a / b);
}
这段代码我们如果运行的话是会出错的,也就是这样:
这里会给到我们一个异常信息,告诉我们说除数不能为0,然后程序就自动退出了,接下来我们再为这段代码添加一个打印输出:
很显然这里并不会执行后面的这句输出语句,因为前面已经出现异常程序退出了,但是如果我们要求这里的输出必须执行该怎么办呢?
在Java中是提供了相对应的异常处理机制的,以上在a/b的时候出现了异常,在Java中我们是可以通过如下的方式去捕获到这个异常的。
try-catch捕获异常
具体的操作就是使用try- catch去捕获我们的异常并作出相应处理,具体看代码:
public static void main(String[] args) {
int a = 1;
int b = 0;
try {
System.out.println(a / b);
} catch (Exception e) {
System.out.println(e+":除数不能为0!");
}
System.out.println("执行到这里……");
}
我们在之前已经说过,在Java中,异常是以类的形式存在的,在我们写的程序代码中,只要出现了异常,JVM就会给我们创建一个异常对象出来,这个时候,我们是需要对这个异常对象做处理的,如果你放任不管的话,最终导致的结果就是你的Java程序会退出。
所以啊,有异常你不处理,你的程序就会退出,那咋办,处理啊,找到这个异常,处理它,那怎么找到呢?
我们可以使用try去包裹可能出现的异常代码,比如上述所讲的代码,在执行到a/b的时候可能出现异常,也就是b不能为0,这里简单说下,这里的a和b是我们提前定义还好的,如果是让用户输入a和b的值呢?
我们简单改写下代码:
Scanner StringA = new Scanner(System.in);
Scanner StringB = new Scanner(System.in);
int a = Integer.parseInt(StringA.next());
int b = Integer.parseInt(StringB.next());
System.out.println(a/b);
System.out.println("执行到这里……");
这里的意思是我们从键盘输入去获取这个a和b,那么当我们输入的是这样的a和b的时候执行是没什么问题的:可是一旦用户不小心把b的值输成0,那么问题就来了:
所以这里绕了一圈就是告诉大家b/a这步操作是可能出现异常的,我们把这个操作叫做可能出现的异常代代码块,于是我们就可以使用try去操作这段代码:
try {
System.out.println(a / b);
} catch (Exception e) {
System.out.println(e+":除数不能为0!");
}
这里要注意的就是,这个try和catch是一起配合使用的,catch是捕获的意思,我们使用try包裹可能出现的异常代码快,然后使用catch去捕获这个异常对象,然后做出相应的处理,比如这里,我们使用try包裹了a/b的操作,那么当b不小心被赋值为零的时候,那么这里在运行的时候就会出现异常,由于在Java中异常是以类的形式存在,所以这里会抛出一个异常对象。
那么我们仔细看这个catch后面有个括号,就是异常对象参数,意思就是如果出现的这个异常对象属于我括号里的这个异常,那么就进入这个catch块去处理这个异常。
说白了就是,程序一旦出现异常,随之而来就是会产生一个异常对象,而异常是以类的形式存在,那么你就得为这个异常对象定义一个catch块,这个异常对象会根据catch后的参数去找属于自己的那一个catch块,找到就进入该catch块,没有的话程序就有因为异常而终止了。
而除数为0这是一个叫做ArithmeticException的异常,也就是算术异常,而这个异常是继承自Exception,也就是说Exception的范围比ArithmeticException要大,所以Exception的catch块可以处理身为子类的ArithmeticException异常。
异常类的继承
以上我们说了ArithmeticException这个异常是继承自Exception的,后者的范围更广,然后我们再看代码:
try {
System.out.println(a / b);
} catch (ArithmeticException e) {
System.out.println(e + ":除数不能为0!出现算术异常");
} catch (Exception e) {
System.out.println(e+"出现异常");
}
也就是说,一个try下面可以对应多个catch块,而每一个catch都有自己对应的一个可以处理的异常类型,我们看上面的改进,我们添加了负责处理ArithmeticException的catch块,那么结果是如呢?
可以看到,这里是直接进入了ArithmeticException的catch块,也就是说,异常对象一旦被一个catch捕获,就不会再进入下一个异常了。
有人可能会说可以这样吗?
也就是把Exception放在第一个catch块,实际上这里是不行的,为啥?我们来看下再这里的一个异常继承关系:
可以看到,ArithmeticException其实是Exception的子类的,如果你把Exception放在第一个catch块的话那么所以的异常对象都将直接被这个catch块捕获,因为所有的异常对象都是Exception或者其子类的实例对象,这就很关键啊,意味着你后面定义再多的catch块也没有用啊,因为永远不会执行到这里,上面已经被Exception这个老大哥给截胡了,在Java中永远执行不到的代码就会被定义为错误的,所以是不能把Exception给放到第一个catch块的。
这里有这么一个原则:
先捕获处理小范围的异常,再捕获处理大范围的异常,也就是先小后大
意思也就是先把子类异常放在前面的catch块,这么以来,Exception的捕获基本上都是在最后一个catch了。
多异常的处理
这个是在Java 7之后增加的,也就是说啊在Java7之前嘞,一般来说一个catch块只能捕获处理一个异常,但是在Java7之后就升级了,可以一个catch块捕获处理多个异常。
那这个是怎么操作的呢?来看看代码就一目了然了:
是不是还是比较清楚的,这里的编写也很简单,就是通过符号“|”把不同的异常对象类型给分隔开,记住这里只需要在最后定一个异常就行,也就是这里的“e”,同时由于是捕获多个异常,这里的e其实是默认final修饰的,因此就不能再对e进行任何赋值操作了,比如一下这样就是错误的:
这就是多个异常的捕获了。
获取异常信息
先明白这点:
当产生一个异常对象,被相对应的catch块捕获之后,这个catch块后的异常形参变量也就接受到了这个异常对象。
因此,我们就可以通过这个异常参数去获得一些异常信息,一般我们常用的一些方法如下:
getMessage():这个方法会返回异常的详细描述信息 printStackTrace():这个方法会打印出异常的跟踪栈信息 printStackTrace(PrintStream p):这个则会将异常的跟踪栈信息输出到指定的输出流中去 getgtacktreace():返回异常的跟踪站信息
那具体的访问,我们看下代码便知:
这里其实也比较简单,就是几个常见的异常信息获取方法的使用。
finally
这个可以说叫做善后的,啥意思嘞?简单来说,就是你的异常对象无论进入哪个catch块执行,那么到最后这个finally里的代码一定会被执行。
这个一般用在哪里呢?通常被用于释放资源,一般比如说数据库连接操作,网络连接或者常见的IO流的操作,这些就需要进行资源的回收,那么这个时候就可以使用finally里,因为它必定会被执行。
看到这里不知道大家有没有疑惑啊,不是说Java会自动回收资源吗?这个感觉要手动操作啊,这里其实你要区分资源的分类,Java的垃圾回收针对的堆内存中的对象所占用的内存,而这里说的IO流操作,数据库连接什么都是属于物理资源,而物理资源必须是需要手动回收的。
看看代码:
这个时候我们看异常的处理就比较完整了,也就是包括try,然后是catch,再加上一定会被执行的finally块。
那么这里就需要特别说一下了:
对于异常处理来说,try块是必须的,没有try块啥也不是,而catch和finally则不是必须的,但是,也必须选择其一,也就是说,你不能只有个try,既没有catch也没有finally,然后就是注意catch块了,可以有多个,但是要遵循“先小后大”的原则
接下来我们来看个测试,看代码:
我们在这里加入了return语句,一般来说吧,只要程序的方法中碰到了return,那么就会立即结束该方法,但是现在呢?我们看下结果:
这说明,finally语句一定会被执行!另外再给大家说一个注意点:
如果你在finally中定义了return语句,那么这个将导致你在try中定义的return语句失效,所以记住一点,不要在finally中使用return哦。
到这里我们清楚了,对于finally语句来说是一定会被执行的(其实有例外,比如你调用了System.exit(1)退出虚拟机),我们常在finally中去做释放资源的操作,但是你有没有发现,这样的操作觉得比较麻烦😡,那有没有简单的一些做法呢?
其实在Java7中对这个try语句进行了增强,可以让我们不需要在finally中进行资源的关闭操作,可以自动帮我们关闭需要释放的资源,但是这里有个前提就是你所需要关闭的资源类要么实现AutoCloseable接⼝,要么实现Closeable接⼝,实际上在Java7中几乎把所有的资源类都进行了改写,主要就是都实现了AutoCloseable或者Closeable接⼝,可以让其实现资源的自动关闭,这些资源类一般就是文件IO的各种类,或者是JDBC的Connection接口等等。
那到了Java9之后又对这个try语句进行了增强,在java7的改进中你需要在try后的圆括号内声明并创建资源,到了Java9,你不需要这样做了,只需要自动关闭的资源有final修饰或者是有效的final即可,这里先尽做了解,后期会详细探讨。
Checked异常和Runtime异常
接下来我们来看看关于异常的分类,Java中的异常可以分为两个大类:
Checked异常 Runtime异常
那怎么区分这两类异常呢?所有的RuntimeException类及其⼦类的实例被称为Runtime异常;不 是RuntimeException类及其⼦类的异常实例则被称为Checked异常。
那对于Checked异常就是可检查异常,也就是说在Java中认为这种异常是可以被提前处理的,所以一旦出现这种异常你就得处理它,如果不处理它,那是编译都无法通过的。
那怎么去处理这个Checked异常呢?我们前面也说了,可以使用try- catch的方式去捕获处理异常,当然,我们还有一种方式就是抛出异常,暂且不管,这个等会会讲。
对于Runtime异常也就是运行时异常了,这个我们不需要在编译阶段就处理它,如果要处理的话,可以使用try- catch,就比如上面我们一直演示的那个除数为0的案例。
throws
我们可以使用throws来声明抛出异常,啥意思嘞,这个抛出异常咋回事?字面意思去理解,就是这个异常不管了,扔出去,对吧,抛出抛出,那如何扔出去呢?使用这个throws关键字即可。
也就是说当你不知道该如何处理某一类型的异常的时候,你就可以选择将该异常抛出,实际上抛出异常也不是说就不管异常了,而是将该异常交给上一级调用者去处理。如果一直往上抛出异常,最终就把这个烫手山芋交给了JVM,那JVM是怎么处理这个异常呢?
一般就是:
打印异常的跟踪栈信息,并中止程序
下面我们来看下代码:
我们这里在main方法上使用throws抛出了这个异常,那就是把这个异常扔给了我们的JVM,而JVM的处理上面也说了,我们看下结果:
打印出跟踪栈信息,然后中止程序,这里其实是个运行时异常,也就是Runtime异常,接下来我们看下对于Checked异常的抛出,我们首先编写一段含有Checked异常的代码,如下:
这里就会产生一个编译时异常,那么IDEA给我们的提示可以用try/catch捕获处理,当然,也可以使用throws关键字抛出,我们这里将其抛出:
接下来我们在main方法中去调用这个方法:
发现了吗?我们在main方法中调用它依然是需要处理出现的异常的,本身CheckedTest将异常抛出,就是希望由调用者去处理该异常,所以这里我们在main方法中去调用该方法的时候也要一并去处理该方法产生的异常,要不你继续将其抛出交给JVM,要不使用try/catch捕获!
Checked异常的限制
这里给大家看一个示例:
发现没有,当我们使用throws去抛出一个异常时,父类中的方法抛出一个异常,而其子类中重写该父类抛出异常的方法的时候,重写后的方法抛出的异常的范围是不能比父类中方法抛出的异常的范围大的,这句话可能有点绕,但是配合看图应该能明白什么意思。
这其实就是Checked异常所带来的一个限制。
手动抛出异常
以上我们使用throw是来抛出异常其实都是Java自动帮我们去抛出异常对象的,除此之外,我们还可以自己手动的去抛出异常,这里需要使用到的一个关键字叫做throw,注意这里是没有s的,和以上我们说的throws是不一样的。
想一下这里为什么要手动抛出异常呢?因为异常本身就不是确定的,什么意思呢?就是同一件事情,在不同的人看来可能性质就不一样,比如你明天要外出,可是明天突然就下雨了,那么这个下雨对你来说就是一种异常,是你不想要的,但是对于那些尤为某种情况希望明天下雨的来说,这件事情就不是一件异常事件。
对应到我们的程序中,异常也是要根据具体情况来定义的,因此这种异常是系统无法帮我们来判定的,这就需要我们自行去抛出异常。
具体就是使用throw来手动抛出异常,怎么操作的看代码:
try {
//规定第一次输入的值不能大于10,也就是这里的stringA不能大于10
Scanner stringA = new Scanner(System.in);
Scanner stringB = new Scanner(System.in);
int a = Integer.parseInt(stringA.next());
int b = Integer.parseInt(stringB.next());
if (a > 10) {
throw new Exception("输入的第一个数字不能大于10");
} else {
System.out.println(a + b);
}
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println("第一次输入请输入一个小于10的数字!");
}
同样的,当你手动的抛出一个异常的时候也是需要对这个异常进行处理的,我们这里使用try/catch来捕获处理该异常,看结果:
这里说一点,就是无论你是手动抛出异常还是系统给我们抛出异常,在java中对异常的处理方式是不变的。也就是说碰到Checked异常,要不使用throws将其抛出,要么使用try/catch语句块捕获处理。
自定义异常
一般来说吧,我们不会去手动抛出异常,当然,这里说的异常指的是系统级别的异常,那除此之外,我们还可以自己自定义异常,代码如下:
class MyException extends Exception {
public MyException(){}
public MyException(String msg) {
super(msg);
}
}
以上我们就自定义了一个异常,自定义异常我们需要注意以下两点:
创建一个无参构造器 创建一个带有字符串参数的有参构造器
这里的字符串参数其实就是异常的具体描述信息,比如我们之前这样定义一个异常:
一般的我们要是自定义异常的话最好就是有一个“见名知意”的程度,就是我看到你这个自定义异常类名,大概知道这是一个什么异常。
小结
以上我们就Java中的异常进行了学习,不知道你发现没有,我们对异常的学习其实主要就是在围绕以下五个关键字:
try catch finally throws throw
然后还有就是要注意Checked异常和Runtime异常,以上都是关于异常的基本知识,掌握这些,足以应付我们在日常工作学习中异常操作,至于更深层次的学习则需要我们在实际应用的去不断的探索了,关于Java中的异常,我们就先介绍到这里。
这是俺之前写的文章:
欢迎加我微信,一起交流学习
如果能给个赞和在看那就更好了,转发是最大的支持!