疯狂Java讲义(十)----第一部分

 本章内容:

        异常机制已经成为判断一门编程语言是否成熟的标准,除传统的像C语言没有提供异常机制之外,目前主流的编程语言如Java、C#、Ruby、Python等都提供了成熟的异常机制。异常机制可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。
        Java的异常机制主要依赖于try、catch、finally、throw和 throws五个关键字,其中try关键字后紧跟一个花括号扩起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码catch后对应异常类型和一个代码块,用于表明该catch 块用于处理这种类型的代码块多个catch 块后还可以跟一个finally块, finally块用于回收在try块里打开的物理资源,异常机制会保证 finally块总被执行throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象
        Java 7进一步增强了异常处理机制的功能,包括带资源的try语句、捕获多异常的catch两个新功能,这两个功能可以极好地简化异常处理。
        开发者都希望所有的错误都能在编译阶段被发现,就是在试图运行程序之前排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决。Java将异常分为两种, Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。Checked异常可以提醒程序员需要处理所有可能发生的异常,但Checked异常也给编程带来一些烦琐之处,所以Checked 异常也是Java领域一个备受争论的话题。

1. 异常概念 :

        异常处理已经成为衡量一门语言是否成熟的标准之一,目前的主流编程语言如C++、C#、Ruby、Python等大都提供了异常处理机制。增加了异常处理机制后的程序有更好的容错性,更加健壮
        与很多图书喜欢把异常处理放在开始部分介绍不一样,本书宁愿把异常处理放在“后面”介绍。因为异常处理是一件很乏味、不能带来成就感的事情,没有人希望自己遇到异常,大家都希望每天都能爱情甜蜜、家庭和睦、风和日丽、春暖花开……但事实上,这不可能!(如果可以这样顺利,上帝也会想做凡人了。)
        对于计算机程序而言,情况就更复杂了--—没有人能保证自己写的程序永远不会出错!就算程序没有错误,你能保证用户总是按你的意愿来输入﹖就算用户都是非常“聪明而且配合”的,你能保证运行该程序的操作系统永远稳定﹖你能保证运行该程序的硬件不会突然坏掉?你能保证网络永远通畅?………太多你无法保证的情况了!
        对于一个程序设计人员,需要尽可能地预知所有可能发生的情况,尽可能地保证程序在所有糟糕的情形下都可以运行。考虑前面介绍的五子棋程序:当用户输入下棋坐标时,程序要判断用户输入是否合法,如果保证程序有较好的容错性,将会有如下的伪码。

        上面代码还未涉及任何有效处理,只是考虑了4种可能的错误,代码就已经急剧增加了。但实际上,上面考虑的4种情形还远未考虑到所有的可能情形(事实上,世界上的意外是不可穷举的),程序可能发生的异常情况总是大于程序员所能考虑的意外情况。
        而且正如前面提到的,高傲的程序员们开发程序时更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事”。
        如果每次在实现真正的业务逻辑之前,都需要不厌其烦地考虑各种可能出错的情况,针对各种错误情况给出补救措施——这是多么乏味的事情啊。程序员喜欢解决问题,喜欢开发带来的“创造”快感,都不喜欢像一个“堵漏”工人,去堵那些由外在条件造成的“漏洞”。

        对于上面的错误处理机制,主要有如下两个缺点。

  • 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
  • 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。

程序员希望有一种强大的机制来解决上面的问题,希望上面程序换成如下伪码。

        上面伪码提供了一个非常强大的“if块”——程序不管输入错误的原因是什么,只要用户输入不满足要求,程序就一次处理所有的错误。这种处理方法的好处是,使得错误处理代码变得更有条理,只需在一个地方处理错误。
        现在的问题是“用户输入不合法”这个条件怎么定义?当然,对于这个简单的要求,可以使用正则表达式对用户输入进行匹配,当用户输入与正则表达式不匹配时即可判断“用户输入不合法”。但对于更复杂的情形呢﹖恐怕就没有这么简单了。使用Java的异常处理机制就可解决这个问题。

2. 异常处理机制:

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

  (1) 使用try...catch捕获异常

        正如前一节代码所提示的,希望有一种非常强大的“if 块”,可以表示所有的错误情况,让程序可以一次处理所有的错误,也就是希望将错误集中处理。
        出于这种考虑,此处试图把“错误处理代码”从“业务实现代码”中分离出来。将上面最后一段伪码改为如下所示伪码。

        上面代码中的“if块”依然不可表示——一切正常是很抽象的,无法转换为计算机可识别的代码,在这种情形下,Java提出了一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try 块中定义,所有的异常处理逻辑放在 catch块中进行处理。下面是Java异常处理机制的语法结构。

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

 下面使用异常处理机制来改写前面第4章五子棋游戏中用户下棋部分的代码。

        上面程序把处理用户输入字符串的代码都放在 try块里进行,只要用户输入的字符串不是有效的生标值(包括字母不能正确解析,没有逗号不能正确解析,解析出来的坐标引起数组越界……),系统者将抛出一个异常对象,并把这个异常对象交给对应的catch块(也就是上面程序中粗体字代码块)处理,catch 块的处理方式是向用户提示坐标不合法,然后使用continue 忽略本次循环剩下的代码,开始执行下一次循环,这就保证了该五子棋游戏有足够的容错性-─用户可以随意输入,程序不会因为用户输入不合法而突然退出,程序会向用户提示输入不合法,让用户再次输入。

  (2) 异常类的继承体系

        当Java运行时环境接收到异常对象时,如何为该异常对象寻找catch 块呢?注意上面Gobang 程序中catch关键字的形式:(Exception e),这意味着每个catch块都是专门用于处理该异常类及其子类的异常实例
        当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;否则再次拿该异常对象和下一个catch 块里的异常类进行比较。Java异常捕获流程示意图如图10.1所示。

        当程序进入负责异常处理的catch块时,系统生成的异常对象ex将会传给catch块后的异常形参,从而允许catch块通过该对象来获得异常的详细信息。
        从图10.1中可以看出,try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

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

        Java提供了丰富的异常类,这些异常类之间有严格的继承关系,图10.2显示了Java常见的异常类之间的继承关系。

        从图10.2中可以看出,Java把所有的非正常情况分成两种:异常(Exception)和错误(Error),它们都继承Throwable父类
        Error错误一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其 throws子句中声明该方法可能抛出 Error及其任何子类。下面看几个简单的异常捕获例子。

        上面程序针对IndexOutOfBoundsException、NumberFormatException、ArithmeticException类型的异常,提供了专门的异常处理逻辑。Java运行时的异常处理逻辑可能有如下几种情形。

  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常。
  • 如果运行该程序时输入的参数不是数字,而是字母将发生数字格式异常,Java运行时将调用NumberFormatException对应的catch块处理该异常。
  • 如果运行该程序时输入的第二个参数是0,将发生除О异常,Java运行时将调用ArithmeticException对应的catch块处理该异常。
  • 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对应的catch 块处理该异常。

        上面程序针对NullPointerException异常提供了专门的异常处理块。上面程序调用一个null对象的after()方法,这将引发 NullPointerException异常(当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常),Java运行时将会调用NullPointerException对应的catch块来处理该异常;如果程序遇到其他异常,Java运行时将会调用最后的catch 块来处理异常。
        正如在前面程序所看到的,程序总是把对应 Exception类的catch 块放在最后,这是为什么呢?想一下图10.1所示的Java异常捕获流程,读者可能明白原因:如果把Exception类对应的catch块排在其他catch块的前面,Java运行时将直接进入该catch块(因为所有的异常对象都是Exception或其子类的实例),而排在它后面的catch块将永远也不会获得执行的机会
        实际上,进行异常捕获时不仅应该把 Exception类对应的 catch 块放在最后,而且所有父类异常的catch块都应该排在子类异常catch 块的后面(简称:先处理小异常,再处理大异常),否则将出现编译错误。看如下代码片段:

        上面代码中有两个catch 块,前一个catch 块捕获 RuntimeException异常,后一个catch 块捕获NullPointerException异常,编译上面代码时将会在②处出现已捕获到异常 java.lang.NullPointerException的错误,因为①处的RuntimeException已经包括了NullPointerException异常,所以②处的catch块永远也不会获得执行的机会。
 

  (3) Java 7新增的多异常捕获

        在Java 7以前每个catch块只能捕获一种类型的异常;但从Java 7开始,一个catch块可以捕获多种类型的异常
        使用一个catch块捕获多种类型的异常时需要注意如下两个地方。

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

下面程序示范了Java 7提供的多异常捕获。

        上面程序中第一行粗体字代码使用了IndexOutOfBoundsException|NumberFormatException|ArithmeticException来定义异常类型,这就表明该catch 块可以同时捕获这三种类型的异常。捕获多种类型的异常时,异常变量使用隐式的final修饰,因此上面程序中①号代码将产生编译错误;捕获一种类型的异常时,异常变量没有final修饰,因此上面程序中②号代码完全正确。

  (4) 访问异常信息

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

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

下面例子程序演示了程序如何访问异常信息。

        上面程序调用了Exception对象的 getMessage()方法来得到异常对象的详细信息,也使用了printStackTrace()方法来打印该异常的跟踪信息。运行上面程序,会看到如图10.3所示的界面。

        从图10.3中可以看到异常的详细描述信息:"a.txt(系统找不到指定的文件)",这就是调用异常的getMessage()方法返回的字符串。下面更详细的信息是该异常的跟踪栈信息,关于异常的跟踪栈信息后面还有更详细的介绍,此处不再赘述。

  (5) 使用finally回收资源

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

        在哪里回收这些物理资源呢?在try块里回收?还是在catch块中进行回收?假设程序在try块里进行资源回收,根据图10.1所示的异常捕获流程——如果try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。如果在catch块里进行资源回收,但catch块完全有可能得不到执行,这将导致不能及时回收这些物理资源。
        为了保证一定能回收try 块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch 块被执行,甚至在try块或catch 块中执行了return语句,finally块总会被执行。完整的Java异常处理语法结构如下:

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

        上面程序的try块后增加了finally 块,用于回收在 try块中打开的物理资源。注意程序的catch 块中①处有一条return语句,该语句强制方法返回。在通常情况下,一旦在方法里执行到return语句的地方,程序将立即结束该方法;现在不会了,虽然return语句也强制方法结束,但一定会先执行finally块里的代码。运行上面程序,看到如下结果:

        上面执行结果表明finally 块没有被执行。如果在异常处理代码中使用System.exit(1)语句来退出虚拟机,则finally块将失去执行的机会

        在通常情况下,不要在 finally块中使用如return或throw等导致方法终止的语句,( throw语句将在后面介绍),一旦在 finally块中使用了return或 throw语句,将会导致try 块、catch块中的return、throw语句失效。看如下程序。

        上面程序在 finally块中定义了一个return false语句,这将导致try块中的return true失去作用。运行上面程序,将打印出false的结果。
        当Java程序执行try 块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally 块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块——只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了returm或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。

   

  (6) 异常处理的嵌套

        正如FinallyTest.java程序所示,finally块中也包含了一个完整的异常处理流程,这种在try块、catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。
        异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可放在 try块里,也可放在catch块里,还可放在 finally块里。
        异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。

  (7) Java 9增强的自动关闭资源的try语句

        在Java 7以前,上面程序中粗体字代码是不得不写的“臃肿代码”,Java 7的出现改变了这种局面。Java 7增强了try 语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
        需要指出的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现 AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法

        下面程序示范了如何使用自动关闭资源的 try语句。

        上面程序中粗体字代码分别声明、初始化了两个 IO流,由于 BufferedReader、PrintStream都实现了Closeable接口,而且它们放在try语句中声明、初始化,所以 try语句会自动关闭它们。因此上面程序是安全的。
        自动关闭资源的try语句相当于包含了隐式的 finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块

        如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块。
        Java 9再次增强了这种 try语句,Java 9不要求在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或者是有效的final (effectively final),Java 9 允许将资源变量放在 try后的圆括号内。上面程序在Java 9中可改写为如下形式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

撩得Android一次心动

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值