有效处理 Java 异常三原则


Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮、易于调试。


异常之所以是一种强大的调试手段,在于其回答了以下三个问题:

  • 什么出了错?

  • 在哪出的错?

  • 为什么出错?


在有效使用异常的情况下,异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出,如果你的异常没有回答以上全部问题,那么可能你没有很好地使用它们。


有三个原则可以帮助你在调试过程中最大限度地使用好异常,这三个原则是:

  • 具体明确

  • 提早抛出

  • 延迟捕获


为了阐述有效异常处理的这三个原则,本文通过杜撰个人财务管理器类JCheckbook进行讨论,JCheckbook用于记录及追踪诸如存取款,票据开具之类的银行账户活动。


具体明确


Java定义了一个异常类的层次结构,其以Throwable开始,扩展出Error和Exception,而Exception又扩展出RuntimeException.如图1所示.



图1.Java异常层次结构


这四个类是泛化的,并不提供多少出错信息,虽然实例化这几个类是语法上合法的(如:new Throwable()),但是最好还是把它们当虚基类看,使用它们更加特化的子类。Java已经提供了大量异常子类,如需更加具体,你也可以定义自己的异常类。


例 如:java.io package包中定义了Exception类的子类IOException,更加特化确的是 FileNotFoundException,EOFException和ObjectStreamException这些IOException的子 类。


每一种都描述了一类特定的I/O错误:分别是文件丢失,异常文件结尾和错误的序列化对象流。异常越具体,我们的程序就能更好地回答”什么出了错”这个问题


捕获异常时尽量明确也很重要。


例如:JCheckbook可以通过重新询问用户文件名来处理FileNotFoundException,对于 EOFException,它可以根据异常抛出前读取的信息继续运行。

如果抛出的是ObjectStreamException,则程序应该提示用户文件 已损坏,应当使用备份文件或者其他文件。


Java让明确捕获异常变得容易,因为我们可以对同一try块定义多个catch块,从而对每种异常分别进行恰当的处理。


File prefsFile = new File(prefsFilename);

 

try{

    readPreferences(prefsFile);

}

catch (FileNotFoundException e){

    // alert the user that the specified file

    // does not exist

}

catch (EOFException e){

    // alert the user that the end of the file

    // was reached

}

catch (ObjectStreamException e){

     // alert the user that the file is corrupted

}

catch (IOException e){

    // alert the user that some other I/O

    // error occurred

}


JCheckbook 通过使用多个catch块来给用户提供捕获到异常的明确信息。


举例来说:如果捕获了FileNotFoundException,它可以提示用户指定另一 个文件,某些情况下多个catch块带来的额外编码工作量可能是非必要的负担,但在这个例子中,额外的代码的确帮助程序提供了对用户更友好的响应。


除前三个catch块处理的异常之外,最后一个catch块在IOException抛出时给用户提供了更泛化的错误信息.这样一来,程序就可以尽可能提供具体的信息,但也有能力处理未预料到的其他异常。


有时开发人员会捕获范化异常,并显示异常类名称或者打印堆栈信息以求"具体"。


千万别这么干!


用户看到java.io.EOFException或者堆栈信息 只会头疼而不是获得帮助。应当捕获具体的异常并且用"人话"给用户提示确切的信息。不过,异常堆栈倒是可以在你的日志文件里打印。记住,异常和堆栈信息是用来帮助开发人 员而不是用户的。


最后,应该注意到JCheckbook并没有在readPreferences()中捕获异常,而是将捕获和处理异常留到用户界面层来做,这样就能用对话框或其他方式来通知用户。这被称为"延迟捕获",下文就会谈到。


提早抛出


异常堆栈信息提供了导致异常出现的方法调用链的精确顺序,包括每个方法调用的类名,方法名,代码文件名甚至行数,以此来精确定位异常出现的现场。


java.lang.NullPointerException

at java.io.FileInputStream.open(Native Method)

at java.io.FileInputStream.<init>(FileInputStream.java:103)

at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)

at jcheckbook.JCheckbook.startup(JCheckbook.java:116)

at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)

at jcheckbook.JCheckbook.main(JCheckbook.java:318)


以上展示了FileInputStream类的open()方法抛出NullPointerException的情况。


不过注意 FileInputStream.close()是标准Java类库的一部分,很可能导致这个异常的问题原因在于我们的代码本身而不是Java API。所以问题很可能出现在前面的其中一个方法,幸好它也在堆栈信息中打印出来了。


不幸的是,NullPointerException是Java中信息量最少的(却也是最常遭遇且让人崩溃的)异常。它压根不提我们最关心的事情:到底哪里是null。所以我们不得不回退几步去找哪里出了错。


通过逐步回退跟踪堆栈信息并检查代码,我们可以确定错误原因是向readPreferences()传入了一个空文件名参数。既然readPreferences()知道它不能处理空文件名,所以马上检查该条件:


public void readPreferences(String filename)

throws IllegalArgumentException{

    if (filename == null){

         throw new IllegalArgumentException("filename is null");

    }  //if

 

   //...perform other operations...

 

   InputStream in = new FileInputStream(filename);

 

   //...read the preferences file...

}


通过提早抛出异常(又称"迅速失败"),异常得以清晰又准确。


堆栈信息立即反映出什么出了错(提供了非法参数值),为什么出错(文件名不能为空值),以及哪里出的错(readPreferences()的前部分)。这样我们的堆栈信息就能如实提供:


java.lang.IllegalArgumentException: filename is null

at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)

at jcheckbook.JCheckbook.startup(JCheckbook.java:116)

at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)

at jcheckbook.JCheckbook.main(JCheckbook.java:318)


另外,其中包含的异常信息("文件名为空")通过明确回答什么为空这一问题使得异常提供的信息更加丰富,而这一答案是我们之前代码中抛出的NullPointerException所无法提供的。


通过在检测到错误时立刻抛出异常来实现迅速失败,可以有效避免不必要的对象构造或资源占用,比如文件或网络连接。同样,打开这些资源所带来的清理操作也可以省却。


延迟捕获


菜鸟和高手都可能犯的一个错是,在程序有能力处理异常之前就捕获它。Java编译器通过要求检查出的异常必须被捕获或抛出而间接助长了这种行为。自然而然的做法就是立即将代码用try块包装起来,并使用catch捕获异常,以免编译器报错。


问题在于,捕获之后该拿异常怎么办?最不该做的就是什么都不做。


空的catch块等于把整个异常丢进黑洞,能够说明何时何处为何出错的所有信息都会永远丢失。把异常写到日志中还稍微好点,至少还有记录可查。


但我们总不能指望用户去阅读或者理解日志文件和异常信息。


让readPreferences()显示错误信息对话框也不合适,因为虽然JCheckbook目前是桌面应用程序,但我们还计划将它变成基于HTML的Web应用。那样的话,显示错误对话框显然不是个选择。


同时,不管HTML还是C/S版本,配置信息都是在服务器上读取的,而错误信息需要显示给Web浏览器或者客户端程序。 readPreferences()应当在设计时将这些未来需求也考虑在内。适当分离用户界面代码和程序逻辑可以提高我们代码的可重用性。


在有条件处理异常之前过早捕获它,通常会导致更严重的错误和其他异常。


例如,如果上文的readPreferences()方法在调用FileInputStream构造方法时立即捕获和记录可能抛出的FileNotFoundException,代码会变成下面这样:


public void readPreferences(String filename){

   //...

   InputStream in = null;

   // DO NOT DO THIS!!!

try{

    in = new FileInputStream(filename);

}

catch (FileNotFoundException e){

    logger.log(e);

}

in.read(...);

//...

}


上面的代码在完全没有能力从FileNotFoundException中恢复过来的情况下就捕获了它。如果文件无法找到,下面的方法显然无法读取它。


如果 readPreferences()被要求读取不存在的文件时会发生什么情况?当然,FileNotFoundException会被记录下来,如果我们当时去看日志文件的话,就会知道。


然而当程序尝试从文件中读取数据时会发生什么?既然文件不存在,变量in就是空的,一个 NullPointerException就会被抛出。


调试程序时,本能告诉我们要看日志最后面的信息。那将会是NullPointerException,非常让人讨厌的是这个异常非常不具体。错误信息不仅误导我们什么出了错(真正的错误是FileNotFoundException而不是NullPointerException),还误导了错误的出处。


真正的问题出在抛出NullPointerException处的数行之外,这之间有可能存在好几次方法的调用和类的销毁。我们的注意力被这条小鱼从真正的错误处吸引了过来,一直到我们往回看日志才能发现问题的源头。


既然readPreferences() 真正应该做的事情不是捕获这些异常,那应该是什么?看起来有点有悖常理,通常最合适的做法其实是什么都不做,不要马上捕获异常


把责任交给 readPreferences()的调用者,让它来研究处理配置文件缺失的恰当方法,它有可能会提示用户指定其他文件,或者使用默认值,实在不行的话也许警告用户并退出程序。


把异常处理的责任往调用链的上游传递的办法,就是在方法的throws子句声明异常。在声明可能抛出的异常时,注意越具体越好。这用于标识出调用你方法的程序需要知晓并且准备处理的异常类型。例如,“延迟捕获”版本的readPreferences()可能是这样的:


public void readPreferences(String filename)

throws IllegalArgumentException,

FileNotFoundException, IOException{

    if (filename == null){

           throw new IllegalArgumentException("filename is null");

     }  //if

 

     //...

 

     InputStream in = new FileInputStream(filename);

 

//...

}


技术上来说,我们唯一需要声明的异常是IOException,但我们明确声明了方法可能抛出FileNotFoundException。 IllegalArgumentException不是必须声明的,因为它是非检查性异常(即RuntimeException的子类)。然而声明它是为 了文档化我们的代码(这些异常也应该在方法的JavaDocs中标注出来)。


当然,最终你的程序需要捕获异常,否则会意外终止。


但这里的技巧是在合适的层面捕获异常,以便你的程序要么可以从异常中有意义地恢复并继续下去,而不导致更深入的错误;要么能够为用户提供明确的信息,包括引导他们从错误中恢复过来。如果你的方法无法胜任,那么就不要处理异常,把它留到后面捕获和在恰当的层面处理。


结论


经验丰富的开发人员都知道,调试程序的最大难点不在于修复缺陷,而在于从海量的代码中找出缺陷的藏身之处。只要遵循本文的三个原则,就能让你的异常协助你跟踪和消灭缺陷,使你的程序更加健壮,对用户更加友好。


来源:ImportNew-郑玮


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304765.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

人口危机 资本剥削导致生育率低迷

不久前&#xff0c;相关部门公布数据&#xff0c;2020年出生并已经到公安机关进行户籍登记的新生儿共1003.5万。对此&#xff0c;铁流先不做评论&#xff0c;我们先看数据。以下是1949年和1981年以来人口出生情况&#xff1a;1949年: 1275万......1981年&#xff1a;2064万1982…

连不上网_手机连不上网?四种方法教你如何解决,建议收藏以备不时之需

随着科技的进步&#xff0c;现在家家户户大街小巷都是Wifi信号&#xff0c;所以无线基地已经成为生活中不可缺少的一环&#xff0c;但也因为这样无线干扰的情况&#xff0c;常常听到有人抱怨怎么无线又突然断线啦&#xff1f;我的wifi又连不上了&#xff1f;为什么无线上网速度…

关于机器学习,你应该至少学习这8个落地案例|干货集锦

机器学习、深度学习、强化学习、迁移学习&#xff0c;这些你到底了解多少&#xff1f;各种深度学习框架如TensorFlow、Caffe、MXNet等又该如何选择&#xff1f;如何将机器学习整合到正在开发的应用中&#xff1f;机器学习在金融、电商、外卖、教育等领域有哪些落地案例&#xf…

c++经典编程题_全国青少年软件编程等级考试C语言经典程序题10道十

全国青少年软件编程等级考试C语言经典程序题10道十【程序91】题目&#xff1a;时间函数举例11.程序分析&#xff1a;2.程序源代码&#xff1a;#include "stdio.h"#include "time.h"void main(){ time_t lt; /*define a longint time varible*/lttime(NULL)…

使用SQL Server分区表功能提高数据库的读写性能

首先祝大家新年快乐&#xff0c;身体健康&#xff0c;万事如意。一般来说一个系统最先出现瓶颈的点很可能是数据库。比如我们的生产系统并发量很高在跑一段时间后&#xff0c;数据库中某些表的数据量会越来越大。海量的数据会严重影响数据库的读写性能。这个时候我们会开始优化…

回顾周杰伦17年间的歌词,才知道他都唱了些什么

每当提到周杰伦的歌时&#xff0c;你首先会想到的是什么呢&#xff1f;双截棍&#xff1f;中国风&#xff1f;还是是方文山&#xff1f;或者更会有人回答说&#xff1a;根本听不清的歌词…… 回想起来&#xff0c;周杰伦的歌可以说陪伴了我们一代甚至是几代人的成长。无论是《晴…

最近公共祖先_[LeetCode] 236. 二叉树的最近公共祖先

题目链接&#xff1a; https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree难度&#xff1a;中等通过率&#xff1a;57.2%题目描述:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为&#xff1a;"对于有根树…

开启事物_《原神》全新角色甘雨登场 「浮生孰来」活动祈愿开启_新闻资讯_最新手游时评_原创手游资讯...

璃月地区的方方面面&#xff0c;都由「璃月七星」治理。由他们决议得出的策略牵动着市场的走向&#xff0c;因此每一条都要慎重再三。而会议之后&#xff0c;是谁摘录出要点&#xff0c;编入长长的条例中的&#xff0c;人们便不得而知了。璃月的千头万绪&#xff0c;就是这样被…

AI浪潮席卷而来,现在加入还来得及吗?

当你的朋友圈刚被 AlphaGo、Master 刷屏&#xff0c;没几天就听说日本有公司开始用 AI 取代员工、百度将人工智能列为未来10年最重要战略。同时一夜之间&#xff0c;似乎所有大公司都开始做无人驾驶…… 人工智能正以前所未有的速度&#xff0c;渗透、改造着各行各业。而加速这…

使用 Benchmark.NET 测试代码性能

今天&#xff0c;我们将研究如何使用Benchmark.Net来测试代码性能。借助基准测试&#xff0c;我们可以创建基准来验证所做的更改是否按预期工作并且不会导致性能下降。并非每个项目都需要进行基准测试&#xff0c;但是如果您正在开发的是NuGet程序包或通用dll&#xff0c;则很有…

【直观理解】一文搞懂RNN(循环神经网络)基础篇

推荐阅读时间8min~15min 主要内容简介&#xff1a;神经网络基础、为什么需要RNN、RNN的具体结构、以及RNN应用和一些结论 1神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子&#xff0c;只要训练数据足够&#xff0c;给定特定的x&#xff0c;就能得到希望的y&#x…

给新手程序员的16个工作必备小妙招,省下时间去LOL吧!

写在前面&#xff1a; 这个文章核心并不是程序优化的具体技巧&#xff0c;而是拿到一个问题如何思考和利用工具的通用方法。比如即使我们不知道 profiler 这个东西&#xff0c;通过搜索"代码 每一行 时间"也可以很快知道有这样的工具叫做 profiler&#xff0c;并且学…

xftp怎么有root权限_许多人都不懂的Linux系统里的特殊权限!!你真的了解嘛?...

有的朋友一听Linux系统文件还有特殊权限&#xff0c;那头就要爆炸了&#xff0c;那还是接着看看/tmp目录和/usr/bin/passwd文件&#xff0c;怎么回事啊&#xff01;&#xff01;&#xff01;看见没有啊&#xff01;不是应该只有rwx 吗&#xff1f;还有其他的特殊权限( s 跟t )啊…

我是怎样爬下6万共享单车数据并进行分析的(附代码)

共享经济的浪潮席卷着各行各业&#xff0c;而出行行业是这股大潮中的主要分支。如今&#xff0c;在城市中随处可见共享单车的身影&#xff0c;给人们的生活出行带来了便利。相信大家总会遇到这样的窘境&#xff0c;在APP中能看到很多单车&#xff0c;但走到那里的时候&#xff…

使用 Tye 辅助开发 k8s 应用竟如此简单(三)

使用 Tye 辅助开发 k8s 应用竟如此简单&#xff08;一&#xff09;使用 Tye 辅助开发 k8s 应用竟如此简单&#xff08;二&#xff09;续上篇&#xff0c;这篇我们来进一步探索 Tye 更多的使用方法。本篇我们来了解一下如何在 Tye 中如何对数据库进行链接。Newbe.Claptrap 是一个…

mybatis collection标签_一对多的关系,在MyBatis中如何映射?

# 使用collection标签需求&#xff1a;根据用户id查询用户信息的同时获取用户拥有的角色&#xff0c;一个用户可以拥有1个或多个角色。一般情况下&#xff0c;不建议直接修改数据库表对应的实体类。所以这里我们延用之前博客中新建的类SysUserExtend&#xff0c;并添加如下代码…

上传文件白名单_十大常见web漏洞——文件上传漏洞

漏洞介绍在我们浏览网页时&#xff0c;文件上传是非常常见的&#xff0c;比如我们会上传头像、附件、视频等文件&#xff0c;文件上传漏洞通常由于网页代码中的文件上传路径变量过滤不严造成的&#xff0c;如果文件上传功能实现代码没有严格限制用户上传的文件后缀以及文件类型…

Java编程比C编程好吗?《精通Unix下C语言与项目实践》读书笔记(15)

《精通Unix下C语言编程与项目实践》读书笔记(new)文章试读 不拘一个遍程序系列&#xff1a;编程序不能一个脑袋钻到底&#xff0c;有时要学会变通&#xff0c;即所谓的曲线救国。一、二、三、四职场规划&#xff1a;一些杂七杂八的职场感悟吧。不值钱的软件人才 精力充沛与事业…

C# Lambda表达式详解,及Lambda表达式树的创建

每次写博客&#xff0c;第一句话都是这样的&#xff1a;程序员很苦逼&#xff0c;除了会写程序&#xff0c;还得会写博客&#xff01;当然&#xff0c;希望将来的一天&#xff0c;某位老板看到此博客&#xff0c;给你的程序员职工加点薪资吧&#xff01;因为程序员的世界除了苦…