java异常处理(学习笔记)

时间:2022-07-28
本文章向大家介绍java异常处理(学习笔记),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

异常处理

异常处理已经成为衡量一门语言是否成熟的标准之一,目前主流的编程语言大都提供了异常处理机制。增加异常处理机制后的程序有更好的容错性和健壮性。

关键字

java的异常机制主要依赖于try,catch,finally,throw和throws五个关键字。其中try关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理该这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常,而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。

异常类型

java将异常分为两种,Checked异常和Runtime异常,java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常,而Runtime异常则无需处理。

异常处理机制

java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统就会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

使用try…catch来捕获异常

java提出了一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。下面时java异常处理机制的语法结构。

try{
	//业务实现代码
	......
}
catch (Exception e){
	alert 输入不合法
	goto retry
}

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给java运行时环境,这个过程就叫做抛出异常。 当java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给catch块处理,这个过程被称为捕获异常,如果java运行时环境找不到捕获异常的catch块,则运行时环境终止,java程序也将退出。

不管程序代码是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则java运行时环境无法找到处理该异常的catch块,程序退出。

异常类的继承体系

当java运行时环境接收到异常对象时,如何为该异常对象寻找catch块呢?注意上面实例程序中catch关键字的形式:(Exception e),这意味着每个catch块都是专门用于处理该异常类及其子类的异常实例。

当java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,java运行时环境将调用该catch块来处理该异常,否则再次拿该异常对象和下一个catch块里的异常类做比较。

当程序进入负责异常处理的catch块时,系统生成的异常对象将会传给catch块后的异常形参,从而允许catch块通过该对象来获得异常的详细信息。当系统发生不同的意外情况时,系统会生成不同的异常对象,java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

通过在try块后提供多个catch块可以无须在异常处理块中使用if、switch判断异常类型,但依然可以针对不同的异常类型提供相应的处理逻辑,从而提供更精致更有条理的异常处理逻辑。在通常情况下,如果try块只执行一次,则try块后只有一个catch块会被执行,除非在循环中使用了continue开始下一次循环而下一次循环又运行了try块,这才可能导致多个catch块被执行。

需要注意,try块中声明的变量是局部变量,在catch块中不能访问该变量。

java提供了丰富的异常类,这些异常类之间有严格的继承关系。java把所有的非正常情况分为两种:异常(Exception)和错误(Error),它们都继承Throwable父类。

Error错误

Error错误一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致程序的中断。通常应用程序无法处理这些错误,因此应用程序不应该使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

常见的几种异常

IndexOutOfBoundsException:数组越界异常 NumberFormatException:数字格式异常 ArithmeticException:运算条件异常(例如除数为0) InputMismatchException:Scanner输入的类型不匹配 NullPointerException:空指针异常

捕获异常时应该注意的事项

进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面,也就是先捕获小异常,再捕获大异常

多异常捕获

使用一个catch块捕获多种类型的异常时需要注意以下两个地方:

  • 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。
  • 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量进行重新赋值。(而捕获一种类型的异常时,则没有final修饰,因此异常变量可以重新赋值)

访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。

所有的异常对象都包含了如下几个常用方法:

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
  • printfStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流
  • getStackTrace():返回异常的跟踪栈信息

使用finally回收资源

有些时候,程序在try块中打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源必须显式回收

java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存

如果程序在try块中进行资源回收的话,当try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。如果在catch块中进行资源回收,但catch块中的语句完全有可能不执行,这些都导致无法正常回收物理资源。 为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。(如果在catch块中使用System.exit(1)语句来退出虚拟机,则finally块将失去执行的机会)

完整的java异常处理语法结构如下:

try{
	//业务实现代码
}catch(SubException e){
	//异常处理块1
}catch(SubException2 e){
	//异常处理块2
}catch(SubException3 e){
	//异常处理块3
}
...
finally{
	//资源回收块
}

异常处理语法结构中只有try块是必需的,也就是说,如果没有try块,则不能有后面的catch块和finally块,catch块和finally块都是可选的,但至少出现其中之一,也可以同时出现。捕获父类异常的catch块必须位于捕获子类异常后,多个catch块必须位于try块之后,finally块必须位于所有的catch块后。

在通常情况下,不要在finally块中使用return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效

当java程序执行try块、catch块时遇到了return或throw语句时,会去寻找是否存在finally块,如果没有,则程序会立即终止;如果存在finally块,系统会立即执行finally块——只有当finally块执行完成后,才会跳回来执行try、catch块中的return或throw语句,如果在finally块中就终止了程序,则不会跳回去执行。

异常处理的嵌套

异常处理流程代码可以放在任何能放可执行代码的地方,因此完整的异常处理流程既可以放在catch块里,还可以放在finally块里。虽然异常处理嵌套的深度没有明确的限制,但通常没必要使用超过两层的嵌套异常处理,会导致程序可读性降低。

java9增强的自动关闭资源的try语句

java7增强了try语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源,try语句在该语句结束时自动关闭这些资源。为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。自动关闭资源的try语句相当于包含了隐式的finall块,因此这个try语句可以既没有catch块也没有finally块

java7几乎把所有的“资源类”进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。

如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块。

java9再次增强了这种try语句,java9不要求在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或是有效的final,java9允许将资源变量放在try后的圆括号内。

Checked异常和Runtime异常体系

java异常被分为两大类,Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常,其余异常实例则被称为Checked异常。

只有java语言提供了Checked异常,其他语言都没有提供Checked异常。java认为Checked异常是可以被修复的异常,所以必须显式处理,否则就会编译错误。

对于Checked异常的处理方式有如下两种:

  • 当前方法明确知道如何处理该异常,应该使用try…catch块来捕获该异常,然后在对应的catch块中修复。
  • 当前方法不知道如何处理这种异常,应该在定义方法时抛出该异常。

Runtime异常则更加灵活,Runtime异常无须显式的声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

使用throws声明抛出异常

使用throws抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理,如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。Jvm对异常的处理方法是,打印异常的跟踪栈信息,并终止程序运行。

throws声明抛出异常的语法格式如下

throws ExceptionClass1 , EsceptionClass2...

throws声明抛出的语法格式仅跟在方法签名之后,一旦使用throws语句声明抛出该异常,程序就无须使用try…catch块来捕获该异常了。

如果某段代码中调用了一个带throws声明的方法,该方法抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try…catch块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中。

方法重写时声明抛出异常的限制

使用throws声明抛出异常时有一个限制,就是方法重写时"两小"中的一条规则:子类方法声明抛出的异常应该是父类方法声明抛出的异常类型的子类或相同;子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

使用Checked异常至少存在如下不便之处:

  • 对于程序中的Checked异常,Java要求必须显式捕获并处理该异常,或者显式声明抛出该异常。
  • 如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。

在大部分时候推荐使用Runtime异常,尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。 当使用Runtime异常时,程序无须在方法声明中抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。 如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以使用try…catch块来捕获Runtime异常。

使用throw抛出异常

当程序出现错误时,系统自动抛出异常。java也允许程序自行抛出异常,自行抛出异常时使用throw语句来完成。

如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw的语法格式如下:

throw ExceptionInstance;

当java运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,java运行时环境对异常的处理没有任何差别。

如果throw语句抛出的异常时Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块中,也无须放在带throws声明抛出的方法中;程序既可以显式使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法的调用者处理。

自定义异常类

在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用信息。所以在选择抛出异常时,应该选择合适的异常类,从而明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。

用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参地构造器,另一个是带一个字符串参数地构造器,这个字符串作为该异常对象地描述信息(相当于getMessage()方法的返回值)。

创建自定义异常类时,最好让该异常类的类名可以准确描述该异常。

catch和throw同时使用

当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。

使用throw语句抛出异常

从java7开始,java编译器会执行更细致的检查,java编译器会检查throw语句抛出异常的实际类型,这样编译器就知道代码实际上可能抛出的异常,因此在方法签名中只要声明抛出该异常即可。

java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。

在面向对象的编程中,大多数复杂的操作都会被分解成一系列方法调用。这是因为:实现更好的可重用性,将每个可重用的代码单元定义成方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个对象来共同实现,在最终编程模型中,很多对象将通过一系列方法调用来实现通信,执行任务。

所以,面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者,直至最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。

第一行的信息详细显示了异常的类型和异常的详细信息,接下来跟踪栈记录程序中所有的异常发生点,各行显示被调用方法中执行的停止位置,并标明类、类中的方法名、与故障点对应的文件的行。一行行的往下看,跟踪栈总是最内部的被调用方法逐渐上传,直至最外部的起点。

异常处理规划

成功的异常处理应该实现如下目标

  1. 使程序代码混乱最小化
  2. 捕获并保留诊断信息
  3. 通知合适的人员
  4. 采用合适的方式结束异常活动

不要过度使用异常

过度使用异常主要有两个方面

  1. 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有地错误处理
  2. 使用异常处理来代替流程控制

对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性,对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对外部的、不能确定和预知的运行时错误才使用异常

异常处理的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。

异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。因为java运行时接收到异常后,还需要进入相应的catch块来捕获该异常,所以运行效率差。

异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。对于一些完全可预知的,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统的称为异常。

不要使用过于庞大的try块

正确的做法应该是把大块的try块分割成多个可能出现异常的程序段落,并把他们放在单独的try块中,从而分别捕获并处理异常。

避免使用Catch All语句

所谓的Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。

但在编写关键程序时就应该避免使用这种异常处理方式,这种异常处理方式有以下不足:

  • 所有的异常采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制。
  • 这种捕获方式可能将程序中的错误,Runtime异常等可能导致程序终止的情况全部捕获到,从而”压制“了异常。如果出现了一些关键的异常,则会被忽略。

不要忽略捕获到的异常

不要忽略异常!既然已经捕获到异常,那么catch块就应该做一些有用的事情——处理并修复这个错误。catch块整个为空或者仅仅打印出错误信息都是不妥的。

通常建议对异常采取适当措施:

  • 处理异常
  • 重新抛出新异常
  • 在合适的层处理异常

总结

这章的内容比较抽象,花了一天的时间看完了这一章,有的内容还没有完全理解,学长说要结合IO流的相关例子来理解异常,继续学习吧!