异常的处理

异常处理

异常处理的地位

我们首先要认识到一点:没有 没有异常的程序。 以后,在工作当中你们会发现,你们80%的精力其实都是用在处理20%发生异常的地方。且,这些处理都会与你们的KPI绑定的。

异常处理也是程序员的基本能力之一。一旦搞不定,那你基本上是没有办法混的。

也许在面试中异常的问题并不算常见题目,但是别忘了,还有试用期。

什么是异常

既然要解决异常,那么我们必须先认识敌人呀。

1、不是所有的报错都是异常。 2、异常也不仅仅是出现在运行期,编译期也有异常处理的需求。

比如:

int num = 3.14;
sout(n);

这句代码会报错,但是它不是异常,它是语法错误。

也许在生活中,我们会把所有的不正常情况都当做异常。但是,在Java中异常是一种专门的情况。它是针对一种叫做“Exception”的类进行处理的。或者我们说,只有在报错信息中出现了:XxxxxxException的情况,才属于今天我们说的异常处理的范围

演示:

异常的分类

根据异常发生的时机

异常分为:

运行时异常 --- 编译通过,然后在运行的时候发生的异常。 这种异常一旦发生,且没有处理,会终止我们的程序,并且在控制台上打印出报错信息。

编译时异常 --- 编译不通过,当然也就没有办法运行。它往往是在编译期,强制要求解决这里有可能发生的某种或某几种异常。不解决,不通过。 这种在编译期报的异常,我们分辨的方式,就是指到红线处,它会告诉我们:未处理的异常XxxxException。

异常类

这些XxxxException其实都是类名,且被放入到了JDK的包当中。这说明:先人预先就帮我们设计好了各种各样异常类,每种异常类都对应一种异常情况。所以,其实我们的异常处理,就是对这些异常类的对象进行处理。

异常类的层次结构

在整个层次结构中,最顶级的基类是:Throwable。 它的名字见名知意,就是可抛的。

它有两个子类: a、Exception b、Error 这说明在Java当中,异常是异常,错误是另一种情况了。Error和Exception是完全不同的东西。

不同之处: Exception是指程序中发生的,可以用代码去解决的问题。

Error是指程序中发生的不能用代码去解决的问题,往往是硬件问题或运行环境问题。

关注Exception及其子类: Exception下面的子类非常丰富,且子类下面还有子类。这说明JDK的设计非常强大,已经把能够想到的大部分异常都提前设计出来了。

在所有的这些子类中,分为: 1、RuntimeException及其子类 --- 运行时异常

2、其他 --- 编译时异常

异常处理

运行时异常

方案一 用提前判断,将异常的发生扼杀在摇篮里面。让异常根本不出现。 这是最好的处理手段,只是这种方式需要经验,因为你要提前预判这里会出现异常。而经验是需要积累的,这个就是大家缺乏的。

总结一下,目前我们有哪些已经知道的可以解决运行时的经验: 1、为防止数组下标越界,提前用[0-数组.leng)的判断;--- 针对ArrayIndexOutofBoundsException

2、从别人的代码传递过来的对象,在调用该对象之前,做非空判断; --- 针对NullPointerException

3、强制类型转换之前,做instanceof判断;--- 针对ClassCastException

方案二 如果我们由于经验不足或所学习的API不够,导致了运行时异常的发生,且没有办法修改成提前判断,那么就要用这种方法了。它叫做异常捕获机制

在讲解捕获机制之前,我们先要聊聊异常的传播机制

异常传播

在前面我们遇到运行时异常的时候,大家都能在控制台看到红色的报错信息。

在这个报错信息中,总共有3个重点内容: 1、Exception in thread “main” --- 异常发生在了main线程当中。 由于我们现在都是单线程的程序,所以只有一个main线程。因此,目前你们看到的报错都是以这个开头的,无法提供有意义的信息。除非以后有了多线程,那么它可以告诉我们是哪个线程发生了异常。

2、异常类的类名:由于Java的JDK当中为不同的异常情况设计了各自对应的异常类。所以,通过类名的见名知意,我们就可以知道发生了什么异常。当然,英文好的同学占便宜,直接看类名就知道发生了什么。不好的同学,可以通过查阅API文档或百度,同样能获取信息。

3、异常发生的位置。对于我们来说胡老师之前给了大家一个认知:从上往下找第一行你写的代码,就是异常真正发生的位置。

这个时候大家会有疑惑,为啥会有这么多异常发生的位置?这个就跟异常的传播机制有关系了。

当程序运行过程中,发生了某种异常,那么JVM就会自动根据异常的类型产生一个异常对象。 然后,产看当前方法有没有对该异常对象进行处理。如果没有处理,那么先记录位置,再结束当前方法,带着这个异常对象返回方法调用处。 再观察,方法调用处有没有对这个异常对象进行处理。如果没有,记录位置,然后结束这个方法,返回它的调用处。 就这样一层一层的返回,最后返回到main方法。如果main方法,也没有解决,那么结束main方法。返回main方法的调用者JVM,它只能打印错误信息,然后结束程序。

那么这个机制告诉了我们,只要我们能够在这个异常对象交会给JVM之前,任意位置把它处理掉,我们的程序就都不会死,然后继续运行就救回来了。

异常捕获

在java当中提供了专门的语法来对异常对象进行捕获。这就是著名的try-catch。

try-catch是由两个语法块组成的:try块 和 catch块。 语法格式如下:

   try{
       /*
         书写正常逻辑的代码,当然这段代码是有可能发生的异常的。
       */ 
   
   }catch(异常类型 变量名){
      //处理语句
   }
​

A、如果try块中的代码没有发生异常,那么整个try块执行完毕以后,就会跳到catch块的后面,顺序往下正常执行;

B、如果发生了异常,try块剩余的代码不执行。用异常对象与catch块()中的类型进行类型匹配。 匹配成功,代表这个catch就是处理这个异常的,那么进入catch块执行里面的代码。结束后,退出catch块顺序往下执行。 匹配不成功,代表这个异常对象没有处理代码,那么结束本方法,带着异常对象传播到方法调用处。

在catch块的()里面如果我们声明捕获异常的父类类型,那么try块中发生的所有子类异常对象都能被这个catch块捕获。

用父类全部捕获,唯一的问题是如果在try当中发生了不同类型的异常,我们需要有不同的处理,那么就自己写if-else用instanceof判断了。

所以,try-catch支持一个try后面接多个catch的语法。

   try{
   
   }catch(异常类1 变量名){
   
   }catch(异常类2 变量名){
   
   }....

注意: 1、多个catch块只代表这个try块有可能发生多种异常,但每次只可能发生一种; 2、如果你捕获的异常具有继承关系,那么请把捕获父类异常的catch块写到后面; 3、如果两个catch块中的处理具有一致性,那么可以采用新语法: catch(异常类1|异常类2 变量名);

还有一种情况,如果try-catch-catch当中具有无论是否发生异常,都必须要执行的共有代码。那么,我们可以再增加一个块,它叫做“finally”。

try{
​
}catch(){
​
}catch(){
​
}finally{
   无论是否发生异常都必须要执行的代码。
}
​

对于我们来说finally非常强大,在代码级别,它前面的所有的流程控制,包括:break、continue、return,都必须先把finally执行了,再来跳转。所以,我强调了必须。 唯一能阻止它的代码是System.exit(0);

那么哪种代码是有这个荣幸被放到finally当中呢?往往是资源的清理,管道的关闭,说白了就是资源的回收动作。

总结: 1、完整的异常捕获语句是try-catch-finally;

2、try块不能单独存在,catch和finally必须和try一起; 特例:try-finally

3、catch在捕获有继承关系的异常类的时候是有顺序的。

异常捕获的语法其实是死的,只要掌握了今天讲的内容就够了。真正的灵活性,是体现在具体的应用中。

1、哪些代码会发生异常;--- 这个要靠经验了,比如:现在对于你们来说肯定是输入的时候是最常见的。对于以后的你们主要是调用别人的方法时发生编译时异常。

2、在哪里加异常捕获。 从本质上说,在异常对象交给main方法之前,甚至就算是在main方法里面加也可以保证程序不死。 你所要考虑的关键是处理完以后,回到哪个流程是自然的。

编译时异常

编译时异常,其实是在定义方法的时候,通过throws语句,后面跟上异常类型,警告本方法的调用者,这个方法有可能发生某种异常。调用者必须在编译期解决。

跟编译时异常有关系的两个关键字是throw 和 throws。

throw

我们前面说过,一旦发生了异常,JVM会给我们产生一个异常对象。那么,我们自己能不能根据某个条件满足或不满足,自己产生异常对象呢?

我们已经知道了异常类的类名,然后我们也学过了new的语法,理论上我们的前置知识点已经足够了呀。

new 异常类类名();

在代码中光new异常对象是不够的,因为这个指令仅仅是要求在内存中产生了一个异常对象。但是,它没有进行异常传播。

好,“throw”关键字,就用来将它后面的异常对象,抛出去。即,纳入到异常传播中。 这个时候分两种情况,如果throw后面跟的是运行时异常对象。由于编译期不检查,所以编译直接通过。

但是,如果你throw后面跟的是编译时异常对象,那么由于需要在编译期检查。检查在编译期警告调用者,所以要求在这个方法的声明最后加上throws,后面跟上异常类的名字。

throws

throws的出现才让我们到这里才算看到了一个完整的方法声明:

访问修饰符 可选修饰符 返回类型 方法名(形参列表) throws 异常类

区别

1、书写位置不同 throw是写在方法实现当中的; throws是写在方法声明后面的;

2、后面跟的内容不同 throw后面跟的是一个异常对象; throws后面跟的是一个或多个异常类型;

3、作用不同 throw语句一旦被执行到,一定会产生一个异常对象,且抛出去;

throws只是在编译期警告调用者,有可能发生异常,需要对方提前解决。但是运行起来,不一定会报异常。

4、关系 书写了throw语句,且后面跟的是编译时异常,那么就要书写throws语句。

相关知识点的变化: 1、方法声明的完整语法; 2、重写方法的新要求。

为什么要有编译时异常?

运行时异常,大家都能理解的。运行期可能发生各种异常情况,然后通过中断程序,打印信息的方式告知给程序员。然后程序员去修改代码解决问题。

编译时异常,很奇葩。你看看它的描述是叫做有可能发生某种异常;另外,它发生了异常要调用者解决?

我们的同学往往一门心思都在不报错,能运行就开心了。但是,在实际开发当中,当好人是没有用的,该谁解决就是谁。

并不是哪里发生异常就由哪里解决。这个存在一个职责问题。谁导致的,谁解决。这就是为什么编译时异常存在的原因。

往往,当我们调用别人的代码的时候,别人的方法在throws异常。这个时候,我们要观察,这个异常是不是由我们造成的,如果是,那么检查代码,然后用try-catch通过编译。如果不是,那么继续抛。

另外,如果你写的代码有可能因为外部调用者的参数导致错误,也别自己解决,你该抛就抛。

自定义异常(扩展知识点)

为什么要有自定义异常

我们看到在JDK当中,已经设计好了非常多的异常类型,代表了各种情况。那么,定义自定义异常还有必要吗? 这个主要是从两个方面考虑: 1、JDK当中设计的异常类,往往都是处于整个Java程序运行或编译的可能出现的问题进行设计的。但是,实际开发中,还有可能出现一种叫做“业务异常”的情况。 这种异常是跟我们要处理的业务有关的,是在业务场景中被判断为一种异常,而JDK不可能设计出对应具体业务的异常类,所以我们只能自定义了。当然,这种情况,也可以不选择自定义异常,就用Exception,然后通过参数告知具体发生的业务异常信息。

2、自定义异常更多的是发生在公司的项目架构设计需要中。你们以后到了公司中,大部分项目都会采用一种所谓的“三层架构”的方式进行开发。 这种架构会把程序中的类分成3种: 表现层的类 --- 这种类的任务是负责进行程序外观的展现; 业务层的类 --- 这种类的任务是专门对用户提交的数据进行业务处理的; 持久层的类 --- 这种类的任务就是负责使用持久层技术完成数据的增删改查。

因此,三层的工作方式是: 表现层 <---> 业务层 <---> 持久层 这呈现出了一个调用一个,一个返回一个的效果。

那么,在开发过程中,每一层的方法都有可能发生异常。如果,按照职责,那么每一层都会出现异常处理。为了维护和结构清晰,我们通常都会采用往上抛的方式,然后所有的异常处理行为都集中在最上层也就是表现层。

但是,下层抛上来的异常是由各种类型的,那么上层如何接?写多个catch块肯定不合适,因此只能抓大的Exception。问题是,Exception是先人写的,又没有我们需要的自定义行为,比如书写日志。

因此,提出自定义异常,那么我们就可以直接在它里面设计书写日志的行为。但是,自定义异常又不是其他异常的父类。所以,我们需要让下层把发生的异常转换成自定义异常对象。然后,再往上抛,表现层只捕获自定义异常,然后直接调用书写日志这种方法即可。

演示:虽然代码编译没有通过,但是我们要看的是这个用法。

自定义异常的语法

1、书写一个类叫做XxxxException;

2、一定要通过继承,把这个类纳入到异常类的层次结构中去。 继承Exception,继承Throwable。

3、至少定义3种构造方法,无参的,带异常对象参数的,带字符串参数的。

4、另外再在自定义异常中增加各种工具方法,比如书写日志。

总结

1、理论部分: 什么是异常? 异常与错误的区别? 异常的类层次结构? 编译时异常与运行时异常

2、应用部分: try-catch-finally的语法; throw、throws的语法;

你们在应用中遇到的主要问题是:越到异常,如何解决?不同的解决方案,效果是什么?

运行时异常,一般都是提前判断。目前除非是输入问题,在我们还没有学习更简便的检查判断之前,你们可以使用try-catch。

编译时异常,目前你们还没有见过呢。要学到后面,输入输出/数据库/反射/多线程/网络编程,你们调用这些API的时候,才会遇到这些API声明有编译时异常。 而这些时候的编译时异常,几乎99.99%都是因为你的传参有可能导致人家内部发生问题,所以你要用try-catch处理。 等你到了设计和实现一个方法给别人用的时候,你才能根据职责选择自己处理还是抛出去。这个时候才用得上throw,throws。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值