错误和异常处理.ppt_第1页
错误和异常处理.ppt_第2页
错误和异常处理.ppt_第3页
错误和异常处理.ppt_第4页
错误和异常处理.ppt_第5页
已阅读5页,还剩59页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

错误和异常处理 郭东伟http www skywind name 目录 一 软件的正确性与鲁棒性 复杂的软件系统由多个层次构成 在运行时会涉及到多个不同的软硬件系统 如一个普通的航空订票系统 它的最高端是由一些GUI组件所组成 用来在用户的屏幕上显示内容并与用户交互 这些高端组件与那些封装了数据库API的数据存取对象相互作用 再往底层一些 那些数据库API与数据库引擎相交互 然而数据库引擎自己又会调用系统服务来处理底层的硬件资源 比如物理内存 文件系统和网络系统和安全模型 这期间无论哪一个环节出现问题 都可能导致软件变得不可靠 为什么我们要小心翼翼 输入参数问题类型不对 非法指针 空值数据不一致 不符合由文档或者逻辑得出的谓词 内部流程数组越界 非法访问数据转型 cast 错误数据一致性被破坏不可预料的内部错误硬件失效 除数为零 浮点溢出 为什么我们要小心翼翼 调用其他方法无法确保被调用方法在此过程中是否会出问题如何检查被调用方法的状态 资源申请与释放申请资源是否能够成功 申请的资源在不用时能否保证被释放 即使是其他部分出问题的时候 如何防止资源被重复释放 外部资源可能处于不可用状态 临时的或永久的 如何检查及处理 为什么我们要小心翼翼 当出现上述问题时候 如何应对 能否就地解决 如何向上汇报 是否有关注者 如何使错误处理流程尽量不干扰正常程序流程 当软件出现问题时 如何修复 并使之回到良好的运行状态上 原因归类 错误Error语法 逻辑 算法错误输入数据错误 容易发现的 应该预料到 一般能在调用的上一级解决异常Exception难以预料原因 一般来说直接原因源自第三方无法打开必须的文件网络传输错误通常无法在本地解决 以至于不能预计会在调用层次的哪一级 直到用户交互 解决严重失效Fault不能预料出现 而且可能在任何地点出现内存溢出外部硬件错误通常无法在程序内解决 错误和异常 错误能够预料到能够直接修复结果 而不是过程异常有些不能预料一般不能直接修复强调过程 而不是结果一定语境下可以混用 重新审视DbC 调用者 应用程序等 和被调用者 库函数 组件库等 是平等的 都有权利和义务的说明 检查前置条件是一种权利 而传统上模块中检查错误被认为是一种义务 If pointer NULL assert pointer NULL 当出现错误 也就是契约 特别是前置条件 遭到破坏时 首先检查调用者 重新审视DbC 明确错误和异常的关系前置条件遭到破坏 是调用者的错误满足前置条件下 虽经努力 由于其他原因 无法正常得到结果 是一种异常 需要抛出 没有其他原因 必须满足后置条件 否则是提供者的错误 设计接口必须设计契约 intReadFile constchar fn FILE fp fopen fn assert fp intReadFile FILE fp assert fp 测试与错误 软件测试目的是发现错误 而不是消灭错误 软件测试只能表明错误的存在 而不能表明错误的不存在 测试是为了发现程序中的错误而执行程序的过程 好的测试方案是极可能发现迄今为止尚未发现的错误的测试方案 成功的测试是发现了至今为止尚未发现的错误的测试 没有发现错误的测试也是有价值的 完整的测试是评定软件质量的一种方法 二 异常处理方法 异常处理的内容传统异常处理技术返回错误代码使程序处于错误状态终止程序调用预定的错误处理程序结构化异常处理 C 和Java throwtry catch能够以类的方式定义异常的类别和层次 异常处理的内容 一般情况下 很多运行期错误会在底层代码中被检测出来 但是它们不能 或者说不应该 试图自己处理这些错误 解决这些严格的运行期错误的责任应该由高端组件来承担 错误的发生点并不是错误的处理和解决点为了解决一个错误 高端组件必须得到错误发生的通知 本质上 错误处理包括错误检测和通知高端组件 这些高端组件依次处理错误并且试图从错误中恢复 传统处理错误技术 Returnavaluerepresenting error 返回错误代码Returnalegalvalueandleavetheprograminanillegalstate 返回合法值 但使程序处于错误状态 如设置一个全局错误变量 Terminatetheprogram 终止程序Callafunctionsuppliedtobecalledincaseof error 当错误发生时 调用预定的错误处理程序 返回表示错误的值 在小型程序上比较简单易用错误码很难统一 因为一个库的实现者可能选择返回值0来代表一个错误 然而另一个实现者却选择0来代表成功并且用那些非0值代表出现错误 微软从来就没有一个统一的错误码查询表 对于每个调用都要检查返回值 并将错误代码向上回传 可使代码倍增 有时根本不能返回值 如构造函数 使程序处于错误状态 C的头文件中定义了一种机制用来检查和给一个全局整型标记errno赋值 在一个多线程环境中 被一个线程赋予了一个错误码的errno有可能不经意的被另一个线程所改写 而调用者还未对errno进行检查 上面两种方法的共同点 二者都提供一种机制来报告错误 但是二者却都不能保证错误被处理 例如 一个函数没有成功打开一个文件可以通过给errno赋予一个合适的值来表明错误的发生 然而 它不能阻止另一个函数试图写入和关闭那个文件 更进一步 如果errno表明一个错误并且程序员检测到而且按照预期处理了它 那么errno还必须被显式的复位 否则会引起其他函数误以为错误还没有被处理 从而去校正那个问题 引起不可预知的结果 终止程序 最为残酷的处理运行期错误的方法是简单的终止程序 这种解决方案去除了上面两种方法的一些缺点突然终止可能使一些资源不能得到正确的释放 如自由存储 文件句柄 I O设备 特别在面向对象设计中 有些关键程序不应该在任何运行期错误存在的情况下突然终止 如股票交易程序 Web服务器等 终止程序在极限环境下或者在调试阶段是可以被接受的 包括调试对话框 调用预定的错误处理程序 调用预定的错误处理程序 如果缺少关于异常情况的信息 该错误处理程序仍然无能为力 也只能采用类似前三种的方法 OnErrorGotoXXXOnErrorResumeNext 三 结构化错误处理技术 结构化异常处理是一种把控制权从异常发生的地点转移到一个匹配的handler的机制 它将发现错误和处理错误的部分相互分离 即将错误处理代码从 正常 代码中分离出来 使程序更易阅读 异常对象可以是内在类型变量或用户定义类型的对象 可提供更多信息 异常处理使系统从错误中恢复过来 提高了程序的容错能力 异常处理机制由四部分 try块 一个或多个和try块相关的处理器handler catch块 throw语句 以及异常对象自己 优点 异常处理流程与正常流程分离不使用返回值或全局对象强制处理异常可以使用类来表示异常 支持类体系定义 异常处理机制 异常处理的默认相应方式是终止程序 而传统的程序可以 装糊涂 继续运行 异常处理可以处理同步异常 如数组范围检查和I O出错 不能处理异步异常 单击鼠标 交互行为 网络消息达到 I O完成等 异常处理机制是基于堆栈回退的非局部控制机制 可以完成局部变量的销毁工作 如果程序的某些部分出现无法处理的情况 可通知高层的环境去处理它以便从这个 异常 中恢复过来 异常多用于程序不同组件之间 异常处理五阶段 发生表征捕捉分发处理善后恢复 resumingexception 终止 terminatingexception 异常处理五阶段 intmain void try if some error Stage1 thowE Stage2 catch E 异常四要素 异常对象 可以是任何类型 一般采用用户定义对象类型表示异常 因为要使它们易于分辨 同时能够包含更多异常信息 try块放上要检查异常的代码每个catch块指定捕获的异常和处理这些异常的异常处理器throw用来抛出异常对象或者说明抛出哪些异常对象 示例 异常对象 includeintmain char buf try buf newchar 512 if buf 0 throw Memoryallocationfailure catch char str std cout Exceptionraised str n return0 StackUnwinding 栈回退 当一个异常被抛出 运行时机制首先在当前的作用域寻找合适的handler 如果不存在这样一个handler 那么将会离开当前的作用域 进入更外围的一层继续寻找 这个过程不断的进行下去直到合适的handler被找到为止 此时堆栈已经被解开 并且所有的局部对象被销毁 如果始终都没有找到合适的handler 那么程序将会终止 捕获异常 在异常处理过程中 对于一个异常 首先在抛出异常的当前函数中查找 如果throw的类型没有匹配在try块中 当前函数带着一个异常退出 下一步是检查调用者 如果在调用者中对当前函数的调用位于一个try块中 则可用与该try块关联的catch子句列表中的某一个来处理 并进入该子句进行异常处理 在这一过程中 因发生异常而逐步退出复合语句和函数定义 被称为栈展开 stackunwinding 随着栈展开 在退出的复合语句和函数定义中声明的局部变量的生命期也结束了 异常的类型结构 一个异常就是某个表示异常类的对象 异常类型可以是任何类型 但习惯上将它们组织成一定结构如层次结构 从而对异常可以分类 如某数学库的异常可能在C 中 异常类型可以是基本类型或者预定义类 std exception 的派生类或者自定义类型在Java中 异常类型必须是预定义类型 Exception 的派生类 四 不同语言的实现细节 C 中的异常构造函数与异常析构函数与异常异常声明和描述没有类体系的异常类型Java中的异常finally语句checked和unchecked异常 C 中的异常 构造函数与异常析构函数与异常异常声明和描述没有类体系的异常类型 构造函数与异常 语法上讲 构造函数可以抛出异常由于构造函数没有返回值 有人认为 构造函数抛出异常是表示构造不成功的方法 构造函数抛出异常后 该对象没有构造成功 不执行此级别的析构函数如果继承链上某父类之前已经构造成功 则执行这些祖先对象的析构函数如果构造函数已经执行一半 之前申请的资源就不会被释放 析构函数与异常 对象销毁时 自动执行析构函数无论是delete 堆对象 还是作用域结束 或发生异常 栈对象 注意 C 保证局部对象被适当的销毁仅当在抛出的异常被处理的情况下 一个未被捕获的异常是否引起局部对象的销毁由实现决定的 可以用来进行资源的保护Auto ptr的使用析构函数中不能抛出异常 非语法限制 如果此析构函数运行在错误处理过程中 则程序立即崩溃 好的构造和析构模式 永远不在构造函数和析构函数里面抛出异常 或者执行可以跑出异常的动作 除非加好catch 最简单的构造函数复杂对象 实现一个init函数 手工执行 如果需要 再实现一个destroy函数 C 中的异常描述 在函数声明中列出该函数 自身 可能抛出的异常 并保证该函数 自身 不会抛出任何其他类型的异常 抛出特定类型的异常intdivide int int throw Zerodivide MemOverflow 抛出任何可能异常 原始的C类型声明 intdivide int int 永远不会抛出任何异常boolequals int int throw 函数实现时 必须与声明时的异常描述一致虚函数覆盖时 其异常描述必须小于原函数额外地运行时检测如果在运行时 函数抛出了一个没有被列在它的异常规范中的异常时 则系统调用标准库中定义的函数unexpected unexpected 缺省操作是调用teminate 异常类型和使用 在C 中 异常类型可以是任何类型 包括基本类型和对象类型建议使用完整的异常类体系throw什么 throw匿名临时对象throwStackOverFlow size 100 不是堆对象 也不是预先建立的对象catch什么 catch StackOverFlow e 支持多态避免复制 避免忘记析构 C 异常讨论 很多开发者认为 C 的异常机制不好用 甚至是失败的 应该避免使用的 性能的下降不可预知性 不可控虚假的安全感 Java中的异常处理 finally块确保在可能异常的程序块后一定被执行进行资源恢复等清理工作finally不是析构函数 try catch Aa catch Bb finally 一定被执行 构造函数和异常 在构造函数中可以抛出异常 表示对象构造不成功但如果在构造函数中申请外部资源 事情仍然很麻烦 Java中的异常声明 Java异常分为两大类 checked异常和unChecked异常 所有继承java lang Exception的异常都属于checked异常 所有继承java lang RuntimeException的异常都属于unChecked异常 当一个方法去调用一个可能抛出checked异常的方法 必须通过try catch块对异常进行捕获进行处理或者重新抛出 unChecked异常也称为运行时异常 通常RuntimeException都表示用户无法恢复的异常 如 虽然用户也可以像处理checked异常一样捕获unChecked异常 但是如果调用者并没有去捕获unChecked异常时 编译器并不会强制你那么做 关于checked异常的讨论 使用checked异常的初衷是使程序员关注异常 并进行及时地处理 Java推荐人们在应用代码中应该使用checked异常使用checked异常 应意味着有许多的try catch在你的代码中 导致代码冗长 以及一些其他问题 目前很多大师和流行的开源Java软件都开始避免checked异常BruceEckel语 当少量代码时 checked异常无疑是十分优雅的构思 并有助于避免了许多潜在的错误 但是经验表明 对大量代码来说结果正好相反 使用checked异常的一些误区 checked异常导致了太多的try catch代码有很多checked异常对开发人员来说是无法合理地进行处理的 比如SQLException 而开发人员却不得不去进行try catch 而这时 通常是简单的把异常打印出来或者是干脆什么也不干 特别是对于新手来说 过多的checked异常让他感到无所适从 实际上这掩盖了错误checked异常导致了许多难以理解的代码产生当开发人员必须去捕获一个自己无法正确处理的checked异常 通常的是重新封装成一个新的异常后再抛出 这样做并没有为程序带来任何好处 反而使代码难以理解 需要处理非常多的try catch 真正有用的代码被包含在try之内 代码的可读性遭到破坏 使得理解这个方法变得困难起来 checked异常导致破坏接口方法一个接口上的一个方法已被多个类使用 当为这个方法额外添加一个checked异常时 那么所有调用此方法的代码都需要修改 使用checked异常的一些建议 当所有调用者必须处理这个异常 可以让调用者进行重试操作 或者该异常相当于该方法的第二个返回值 使用checked异常 这个异常仅是少数比较高级的调用者才能处理 一般的调用者不能正确的处理 使用unchecked异常 有能力处理的调用者可以进行高级处理 一般调用者干脆就不处理 这个异常是一个非常严重的错误 如数据库连接错误 文件无法打开等 或者这些异常是与外部环境相关的 不是重试可以解决的 使用unchecked异常 因为这种异常一旦出现 调用者根本无法处理 如果不能确定时 使用unchecked异常 并详细描述可能会抛出的异常 以让调用者决定是否进行处理 五 异常使用反例 anti pattern 1OutputStreamWriterout 2java sql Connectionconn 3try 4Statementstat conn createStatement 5ResultSetrs stat executeQuery 6 selectuid namefromuser 7while rs next 8 9out println ID rs getString uid 10 姓名 rs getString name 11 12conn close 13out close 14 15catch Exceptionex 16 17ex printStackTrace 18 反例I 丢弃异常 丢弃异常 代码 15行 18行 这段代码捕获了异常却不作任何处理 可以算得上软件质量的杀手 如果你看到了这种丢弃 而不是抛出 异常的情况 可以百分之九十九地肯定代码存在问题 在极少数情况下 这段代码有存在的理由 但最好加上完整的注释 以免引起别人误解 异常 几乎 总是意味着某些事情不对劲了 或者说至少发生了某些不寻常的事情 我们不应该对程序发出的求救信号保持沉默和无动于衷 调用一下printStackTrace算不上 处理异常 不错 调用printStackTrace对调试程序有帮助 但程序调试阶段结束之后 printStackTrace就不应再在异常处理模块中担负主要责任了 丢弃异常的情形非常普遍 在JDK的内部类中都可以看到类似的使用方法 可见 丢弃异常这一坏习惯是如此常见 它甚至已经影响到了Java本身的设计 反例I 丢弃异常 1 处理异常 针对该异常采取一些行动 例如修正问题 提醒某个人或进行其他一些处理 要根据具体的情形确定应该采取的动作 再次说明 调用printStackTrace不是 处理异常 2 重新抛出异常 处理异常的代码在分析异常之后 认为自己不能处理它 重新抛出异常也不失为一种选择 3 把该异常转换成另一种异常 大多数情况下 这是指把一个低级的异常转换成应用级的异常 其含义更容易被用户了解的异常 4 不捕获自己不能处理的异常 结论一 既然捕获了异常 就要对它进行适当的处理 不要捕获异常之后又把它丢弃 不予理睬 反例II 不指定具体的异常 15行 你能够处理所有异常么 catch语句表示我们预期会出现某种异常 而且希望能够处理该异常 异常类型的作用就是告诉Java编译器我们想要处理的是哪一种异常 catch Exceptionex 就相当于说我们想要处理几乎所有的异常 我们真正想要捕获的异常是什么呢 一个是SQLException 这是JDBC操作中常见的异常 另一个可能的异常是IOException 因为它要操作OutputStreamWriter 显然 在同一个catch块中处理这两种截然不同的异常是不合适的 如果用两个catch块分别捕获SQLException和IOException就要好多了 这就是说 catch语句应当尽量指定具体的异常类型 而不应该指定涵盖范围太广的Exception类 另一方面 除了这两个特定的异常 还有其他许多异常也可能出现 例如 如果由于某种原因 executeQuery返回了null 该怎么办 答案是让它们继续抛出 即不必捕获也不必处理 实际上 我们不能也不应该去捕获可能出现的所有异常 程序的其他地方还有捕获异常的机会 直至最后由JVM处理 结论二 在catch语句中尽可能指定具体的异常类型 必要时使用多个catch 不要试图处理所有可能出现的异常 反例III 占用资源不释放 占用资源不释放 代码 3行 14行 异常改变了程序正常的执行流程 这个道理虽然简单 却常常被人们忽视 如果程序用到了文件 Socket JDBC连接之类的资源 即使遇到了异常 也要正确释放占用的资源 为此 Java提供了一个简化这类操作的关键词finally finally是样好东西 不管是否出现了异常 Finally保证在try catch finally块结束之前 执行清理任务的代码总是有机会执行 遗憾的是有些人却不习惯使用finally 当然 编写finally块应当多加小心 特别是要注意在finally块之内抛出的异常 这是执行清理任务的最后机会 尽量不要再有难以处理的错误 结论三 保证所有资源都被正确释放 充分运用finally关键词 反例IV 不说明异常的详细信息 不说明异常的详细信息 代码 3行 18行 仔细观察这段代码 如果循环内部出现了异常 会发生什么事情 我们可以得到足够的信息判断循环内部出错的原因吗 不能 我们只能知道当前正在处理的类发生了某种错误 但却不能获得任何信息判断导致当前错误的原因 printStackTrace的堆栈跟踪功能显示出程序运行到当前类的执行流程 但只提供了一些最基本的信息 未能说明实际导致错误的原因 同时也不易解读 因此 在出现异常时 最好能够提供一些文字信息 例如当前正在执行的类 方法和其他状态信息 包括以一种更适合阅读的方式整理和组织printStackTrace提供的信息 结论四 在异常处理模块中提供适量的错误原因信息 组织错误信息使其易于理解和阅读 反例V 过于庞大的try块 过于庞大的try块 代码 3行 14行 经常可以看到有人把大量的代码放入单个try块 实际上这不是好习惯 这种现象之所以常见 原因就在于有些人图省事 不愿花时间分析一大块代码中哪几行代码会抛出异常 异常的具体类型是什么 把大量的语句装入单个巨大的try块就象是出门旅游时把所有日常用品塞入一个大箱子 虽然东西是带上了 但要找出来可不容易 一些新手常常把大量的代码放入单个try块 然后再在catch语句中声明Exception 而不是分离各个可能出现异常的段落并分别捕获其异常 这种做法为分析程序抛出异常的原因带来了困难 因为一大段代码中有太多的地方可能抛出Exception 结论五 尽量减小try块的体积 反例VI 输出数据不完整 输出数据不完整 代码 7行 11行 不完整的数据是Java程序的隐形杀手 仔细观察这段代码 考虑一下如果循环的中间抛出了异常 会发生什么事情 循环的执行当然是要被打断的 其次 catch块会执行 就这些 再也没有其他动作了 已经输出的数据怎么办 使用这些数据的人或设备将收到一份不完整的 因而也是错误的 数据 却得不到任何有关这份数据是否完整的提示 对于有些系统来说 数据不完整可能比系统停止运行带来更大的损失 较为理想的处置办法是向输出设备写一些信息 声明数据的不完整性 另一种可能有效的办法是 先缓冲要输出的数据 准备好全部数据之后再一次性输出 结论六 全面考虑可能出现的异常以及这些异常对执行流程的影响 改写后的代码 OutputStreamWriterout java sql Connectionconn try Statementstat conn createStatement ResultSetrs stat executeQuery selectuid namefromuser while rs next out println ID rs getString uid 姓名 rs getString name catch SQLExceptionsqlex out println 警告 数据不完整 thrownewApplicationException 读取数据时出现SQL错误 sqlex catch IOExceptionioex thrownewApplicationException 写入数据时出现IO错误 ioex 改写后的代码 finally if conn null try conn close catch SQLExceptionsqlex2 System err this getClass getName mymethod 不能关闭数据库连接 sqlex2 toString if out null try out close catch IOExceptionioex2 System err this getClass getName mymethod 不能关闭输出文件 ioex2 toString 六 资源申请与释放 一个函数申请了资源 如打开了一个文件 在自由存储空间分配了一些存储 设置了某种访问锁等 为保证系统将来的运行 一个关键问题是应该正确的释放这些资源 假如fopen调用后 fclose调用前 出现了异常 怎么办 voiduse file constchar fn FILE fp fopen fn w useffclose fp 正确关闭文件的一个解决方法 voiduse file constchar fn FILE f fopen fn r try usef catch fclose f throw fclose f 总这样做 可能太罗嗦 不清晰 代价高 利用构造函数和析构函数 类File ptr的构造和析构函数负责文件的 申请与释放classFile ptr FILE p public File ptr constchar n constchar a p fopen n a File ptr FILE pp p pp File ptr fclose p operatorFILE returnp 使用File ptr voiduse file constchar fn File ptrf fn r usef 无论是正常结束还是异常退出 都能保证会调用File ptr对象f的析构函数 向上回退穿过堆栈 去为某个异常查找处理器的过程 通常称为 堆栈回退 在 堆栈回退 过程中 所有局部对象的析构函数会正常被调用 资源申请即初始化 利用局部对象管理资源的技术即所说的 资源申请即初始化 它依赖于构造函数和析构函数的性质 只有在一个构造函数执行完之后 对象才建立起来 只有在此后 堆栈回退时 其析构函数才被调用 一个由子对象构成的对象的构造一直持续到它的所有子对象构造完 数组的构造一直持续到它的每个元素构造完 堆栈回退时 只有那些构造完成的元素的析构函数才被调用 关于异常处理机制的讨论 虚假的安全感 byTomCargill Whileentirelyinfavorofrobu

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论