Code Effective学习笔记--第8章防御式编程

这一章聚焦如何通过断言和Java的异常处理机制这些防御式编程的方法来提高程序的健壮性和安全性,这是防御式编程技术的方面。但是健壮性和安全性到了一定的程度其实是矛盾的,健壮性意味着对于任何的输入,程序都不会终止而且都能给出返回,但代价就是牺牲返回的准确性;而正确性意味着子程序只要返回就要返回最精确的对象,要么就不返回让程序终止,这里就是能够反映防御式编程艺术的一方面。

什么是防御式编程

顾名思义,防御式编程就是以一种防御式的思想来设计子程序,其核心在于编写能够抵御错误和异常的代码,确保程序在面对不确定性和潜在错误时仍能保持稳定和可靠,它“防御”的就是各种对于子程序的潜在“攻击”,这个攻击可能是错误的输入类型、超过限制范围的输入值、外部系统、网络的异常等等,还有一类攻击来自于子程序的内部,这主要是由于程序设计问题导致的异常,包括栈溢出、无限循环等等,而这类攻击的“目的”都是使得子程序返回非预定的内容,使得整个程序奔溃(或者叫失控)。

作者重点介绍了输入检查、断言、异常处理、隔离、代码调试等防御式编程的方法。

无效输入的检测

子程序好比是一个工厂,输入是这个工厂与外部交互的第一步,我们要首先判断清楚这里输入的是正常的原料还是垃圾。

检查公共接口来源于外部的所有值:这里外部指的是示例的外部,比如文件、用户、网络或其他外部接口获取到的数据。包括数据的类型、范围、格式等等。

检查非公共接口的输入值:这里的输入值一般来自于其他子程序的输出。

一旦检测到无效参数就涉及到如何对错误进行处理,这在后面介绍,先介绍一下如何对上面的无效输入进行检测,这就涉及到断言。

断言

断言就是程序员在开发过程中用于检测某些条件是否成立的方法,一旦检测出异常程序员便有机会通过调整代码来解决相关的异常。

断言的语法:

assert num!=0:"num cannot be zero"
//assert 判断条件:如条件为假时返回的信息

由于断言的定位是给程序员一个在开发环境中用于检测异常的工具而且它对于程序有一定程度的性能损耗,所以在生产环境中会自动忽略。

断言使用的指导原则:

用断言来处理永远不应该发生的情况,而预期会发生的情况应该通过错误处理代码来处理:这里所说的不应该发生的情况主要是由于程序设计本身原因造成的异常,他们假定在进入生产环境之前就会被程序员所解决。而预期将会发生的情况主要是程序本身不可控的事项,比如网络、存储、IO系统等程序外部产生的异常,由于他们无法控制所以需要设计错误处理代码来解决相关问题。

避免将要执行的代码放到断言中:由于断言语句在生产环境中会被忽略,如果其中涉及要执行代码,则会被同时忽略。

使用断言来声明和验证子程序的前置和后置条件:前置与后置条件是“契约式设计”的重要组成部分。其中前置条件指的是子程序或者类的调用代码在调用相关子程序与类实例前需要满足的前提条件;而后置条件指的是子程序或者类的实例在返回前所需要承担的责任。这里需要强调一下这里的前置和后置条件的数据是来自于内部可信方法的,如果是来自于外部源头,则属于不可控的异常需要设计错误处理方法。

对于健壮性要求高的代码需要在使用断言的同时再处理错误:当软件比较复杂,尤其是一个功能的使用场景非常多时,程序员往往不能通过断言去检测出所有的异常(有的异常可能在单元测试当中没有被测出来),这个时候就需要通过错误处理方法再兜个底。

public class BuddleSort{public static void sort(int[] arr){assert arr!=null: "arr can not be null";if (arr==null) {throw new IllegalArgumentException("arr can not be null");}for(int i=0; i<arr.length-1;i++){for(int j=0;j<arr.length-1-i;j++){if(arr[j]>arr[j+1]){arr[j] = arr[j] + arr[j+1];arr[j+1] = arr[j]-arr[j+1];arr[j] = arr[j]-arr[j+1];}}}}
}

比如这个冒泡排序的算法,同时使用错误处理(抛出异常)和断言机制。

错误处理方法

前面说了,错误处理方法是程序员面对不可控的异常(就是文本里说的预期会发生)需要做的各种应对预案,作者列举了几种方式:

  • 返回中立值:比如屏幕的背景颜色显示默认值。
  • 换用下一条有用数据:比如在通过数据流读取大量数据进行统计分析的时候一两条信息读取的失败就直接忽略即可。
  • 返回与上次相同的答案:比如一个一秒测量一次的温度计程序
  • 换用最接近的合法值:对于一些返回超出范围的数据由范围极值取代
  • 在文件中记录告警信息:这严格来说不算是处理,就是一个记录
  • 返回一个错误代码:错误代码可以映射不同的处理方式
  • 调用错误处理子程序或者对象:将错误处理集中在一个全局的处理中心,这个方法具有高内聚的特性,但是一旦处理中心被攻击,将会对所有程序产生影响。
  • 在错误的地方显示错误消息:这里说的显示消息是将错误信息直接呈现给用户,这往往不被推荐。
  • 用最妥当的方式在局部处理错误:要求子程序在产生或者catch错误以后不要再抛出,而是直接处理。
  • 关闭程序:如果所有方式都不合适,那就关闭程序重新启动吧。

Java标准的异常处理机制

异常的介绍

Java通过将各种异常(Exception)封装形成不同的类来对其进行标准化的处理,下面先说一下Java异常的分类方式:

Throwable类是所有错误和异常的基类。它定义了错误和异常的基本结构和行为,包括如何报告错误信息、如何获取错误的堆栈跟踪等。它提供的基本方法包括:

  • getMessage():返回异常的详细信息字符串。
  • toString():返回异常类名和异常信息。
  • printStackTrace():将异常的堆栈跟踪信息打印到标准错误流。
  • fillInStackTrace():填充异常的堆栈跟踪信息。

这些方法也是Exception和Error类的基础方法,在自定义异常种类的时候无需去重构这些方法,重点只要把几个构造函数重构即可:

public class CustomException extends Exception {public CustomException() {super(); // 调用Exception的无参构造函数}public CustomException(String message) {super(message); // 调用Exception的构造函数,接收一个错误消息}public CustomException(String message, Throwable cause) {super(message, cause); // 调用Exception的构造函数,接收一个错误消息和一个引起异常的原因}public CustomException(Throwable cause) {super(cause); // 调用Exception的构造函数,仅接收一个引起异常的原因}
}

这里的cause是底层的异常,message是错误信息的描述。

异常的处理方法

对于异常(Checked Exception)的处理方式其实就两种:一是捕获后直接通过错误处理方法处理掉,另一种是抛给上层调用者(当然非受控异常也可以直接忽略掉,交给日志反映)。下面分别展示两种方式:

//这里Divide方法选择将非受控异常ArithmeticException抛出给上层调用者public class SafeDivision {public static int Divide(int a, int b){assert b!=0: "Divisor cannot be zero";if(b==0){throw new ArithmeticException("Divisor cannot be zero");}return a/b;}
}
//上层的Calculator在调用SafeDivision.Divide方法时选择捕获相关方法进行处理(这里是返回0值)public class Calculator {public static int add(int a, int b){return a+b;}public static int sub(int a, int b){return a-b;}public static int mul(int a, int b){return a*b;}public static int divide(int a, int b){try {return SafeDivision.Divide(a, b);} catch (ArithmeticException e) {return 0;}}
}

使用异常的注意事项

使用异常能够让程序的其他部分无法忽略错误的存在:对于程序的调用者来说必须明确它收到的异常要如何处理,否则程序便会直接关闭。

只有在真正可能会发生异常的情况下才抛出异常:如果其他编码实践可以解决相关的异常情况(尤其是非受控异常),那就尽量不要使用异常,因为它会使得程序调用者不得不去关注子程序会抛出的异常类型,这在一定程度上有降低封装性的可能。

不要使用异常来推卸责任:不要只做抛出,只要抽象程度一致了,就应该在本级把异常给处理掉

尽量不要在构造函数和析构函数中抛出异常:由于像C++调用析构函数的前提是构造函数被成功调用过,当构造函数抛出异常的时候可能实例已经有部分被构建,但由于构造函数未被执行完整,导致无法调用析构函数,造成潜在的资源泄漏风险。

在合适的抽象层抛出异常:这里就需要通过自定义异常来实现同级的抽象。

在异常消息中包括导致异常的所有消息:即异常的message参数要在初始化时仔细设置。

避免空的catch块:这会绕过异常处理的强制性屏障,造成危险。

了解库代码的所抛出的异常:因为库代码是子程序组成的基石,我们只有对他们的异常情况够了解才能够实现真正的防御式编程。

可以创建一个集中式的异常报告机制:标准化所有异常的规范动作以及返回信息。

隔离程序

这里说的隔离就是通过对公共接口输入数据的清洗,将输入数据转换为合适的类型,使得将子程序与外部的错误隔离开。

这里作者重点说明了隔离的使用使得断言和错误处理方法的区别变得更加清晰。对于隔离内的子程序由于已被清理过,默认不存在不可控的输入问题,所以可以通过断言来检测异常,而隔离外的程序应该使用错误处理方法。

调试辅助代码

调试辅助代码是在软件开发过程中,为了帮助开发者更好理解和定位代码中的问题而在其中插入额外代码段来辅助程序员的代码。这里有一种比较激进的辅助方式值得说一说就是“进攻式编程”。

进攻式编程

进攻式编程主张在开发阶段尽可能让所有的错误都显示出来(甚至是夸张的显示出来),使得程序员不得不进行优化,而在生产环境中将所有相关的错误都恢复。下面作者列举了进攻式编程的六种方式:

  • 确保断言语句是的程序终止运行;
  • 完全填充所有分配到的内存,以便检测出内存分配方面存在的错误
  • 完全填充所有分配到的文件或流,以便排查任何形式的格式错误
  • 确保每一个case语句的default分支或者else分支都能产生极其严重的错误
  • 在删除对象之前使其填满垃圾数据
  • 让程序将错误日志抄送程序员电子邮件

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

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

相关文章

Tftp服务器环境搭建

1、什么是Tftp TFTP&#xff08;Trivial File Transfer Protocol&#xff0c;简单文件传输协议&#xff09;是一种基于UDP&#xff08;User Datagram Protocol&#xff09;的文件传输协议&#xff0c;它被设计为一个非常简单的文件传输机制&#xff0c;特别适用于那些对复杂性有…

make2exe:自动集成测试

模板Makefile&#xff0c;生成多个C/C模块的集成测试程序。

免费【2024】springboot 基于微信小程序的宠物服务中心

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…

JavaDS —— 二叉搜索树、哈希表、Map 与 Set

前言 我们将学习 Map 与 Set 这两个接口下的 TreeMap 与 TreeSet &#xff0c;HashMap 与 HashSet &#xff0c;在学习这四个类使用之前&#xff0c;我们需要先学习 二叉搜索树与 哈希表的知识。 二叉搜索树 在学习二叉树的时候&#xff0c;我们就已经了解过二叉搜索树的概念…

酒店智能门锁接口pro[0922]D801 对接收银-SAAS本地化-未来之窗行业应用跨平台架构

proUSB接口函数[0922中性版]-D801 调用函数库&#xff1a; 提供Windows下的32位动态连接库proRFL.DLL&#xff0c;函数使用详细说明 //-----------------------------------------------------------------------------------// 功能&#xff1a;读DLL版本&#xff0c;不涉…

【Linux C | 网络编程】进程池退出的实现详解(五)

上一篇中讲解了在进程池文件传输的过程如何实现零拷贝&#xff0c;具体的方法包括使用mmap&#xff0c;sendfile&#xff0c;splice等等。 【Linux C | 网络编程】进程池零拷贝传输的实现详解&#xff08;四&#xff09; 这篇内容主要讲解进程池如何退出。 1.进程池的简单退…

Java并发编程(上)

并发&#xff1a;多个线程&#xff08;进程&#xff09;竞争一个资源 并行&#xff1a;多个线程&#xff08;进程&#xff09;同时运行不同资源 线程和进程的关系简单地说&#xff0c;进程是一个容器&#xff0c;一个进程中可以容纳若干个线程&#xff0c;一个进程里面&#…

微信小程序入门

创建一个入门程序 这是index.vxml代码 <!--index.wxml--> <navigation-bar title"Weixin" back"{{false}}" color"black" background"#FFF"></navigation-bar> <view class"container" ><view&…

苹果CMS:资源采集站如何设置定时采集详细教程讲解

我们搭建好站点之后&#xff0c;会自定义一些采集&#xff0c;但是需要每天去手动执行&#xff0c;有时候甚至会忘记&#xff0c;那我们如何处理呢&#xff1f;今天我们就来介绍一下如何设置定时器。 如果按照官方例子来设置定时器会遇到一个问题就是采集的资源未绑定类型&…

WAF+API安全代表厂商|瑞数信息入选IDC报告《生成式AI推动下的中国网络安全硬件市场现状及技术发展趋势》

近日&#xff0c;全球领先的权威资讯机构IDC正式发布《IDC Market Presentation&#xff1a;生成式AI推动下的中国网络安全硬件市场现状及技术发展趋势&#xff0c;2024》报告。报告中IDC 评估了众多厂商的安全硬件产品能力&#xff0c;并给出了产品对应的推荐厂商供最终用户参…

04 | 深入浅出索引(上)

此系列文章为极客时间课程《MySQL 实战 45 讲》的学习笔记&#xff01; 索引的常见模型 可以提供查询效率的数据结构有很多&#xff0c;常见的有三种&#xff1a;哈希表、有序数组、搜索数。 哈希表是一种以 key-value 形式存储的数据结构。输入一个 key&#xff0c;通过固定…

强烈推荐java人,2024年大厂面试背这份(八股文+场景题结合)!很管用!

2024 年的行情&#xff0c;和 3~4 年前不同&#xff0c;通过海量简历投递和海量面试找工作的时代已经过去了。 在如今面试机会较少&#xff0c;并且面试难度较大的情况下。 充分做好面试的准备才是快速通过面试最有效的方法&#xff01; 切忌把真实面试当靶场&#xff0c;最…

信息学奥赛初赛天天练-48-CSP-J2020完善程序2-变量交换、冒泡排序、贪心算法、最小区间覆盖

PDF文档公众号回复关键字:20240728 2020 CSP-J 完善程序2 1 完善程序 (单选题 &#xff0c;每小题3分&#xff0c;共30分) 最小区间覆盖 给出 n 个区间&#xff0c;第 i 个区间的左右端点是 [ai,bi]。现在要在这些区间中选出若干个&#xff0c;使得区间 [0, m] 被所选区间的…

前端框架 element-plus 发布 2.7.8

更新日志 功能 组件 [级联选择器 (cascader)] 添加持久化属性以提升性能 (#17526 by 0song)[日期选择器 (date-picker)] 类型添加月份参数 (#17342 by Panzer-Jack)[级联选择器 (cascader)] 添加标签效果属性 (#17443 by ntnyq)[加载 (loading)] 补充加载属性 (#17174 by zhixi…

第九讲 后端1

后端&#xff08;Backend&#xff09; 用带噪声的数据估计内在状态&#xff08;Estimated the inner state from noisy data&#xff09;——状态估计问题渐进式&#xff08;Incremental&#xff09;&#xff1a;保持当前状态的估计&#xff0c;在假如新信息时&#xff0c;更新…

【算法专题】双指针算法之18. 四数之和(力扣)

欢迎来到 CILMY23的博客 &#x1f3c6;本篇主题为&#xff1a;双指针算法之18. 四数之和&#xff08;力扣&#xff09; &#x1f3c6;个人主页&#xff1a;CILMY23-CSDN博客 &#x1f3c6;系列专栏&#xff1a;Python | C | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算…

ProxmoxPVE虚拟化平台--U盘挂载、硬盘直通

界面说明 ### 网络设置 ISO镜像文件 虚拟机中使用到的磁盘 挂载USB设备 这个操作比较简单&#xff0c;不涉及命令 选中需要到的虚拟机&#xff0c;然后选择&#xff1a; 添加->USB设置选择使用USB端口&#xff1a;选择对应的U盘即可 硬盘直通 通常情况下我们需要将原有…

【Linux 16】进程间通信的方式 - 共享内存

文章目录 &#x1f308; 一、共享内存概述⭐ 1. 什么是共享内存⭐ 2. 如何实现共享内存⭐ 3. 操作系统允许存在多个共享内存⭐ 4. 操作系统如何管理共享内存⭐ 5. 获取共享内存的唯一标识符 key⭐ 6. 为什么要由用户提供 key &#x1f308; 二、查看共享内存⭐ 1. 使用 ipcs -m…

TCP 协议的 time_wait 超时时间

优质博文&#xff1a;IT-BLOG-CN 灵感来源 Time_Wait 产生的时机 TCP四次挥手的流程 如上所知&#xff1a;客户端在收到服务端第三次FIN挥手后&#xff0c;就会进入TIME_WAIT状态&#xff0c;开启时长为2MSL的定时器。 【1】MSL是Maximum Segment Lifetime报文最大生存时间…

root 用户和权限

目录 1. 超级管理员 root 2. 切换用户 Switch User 2.1 普通用户切换到 root 用户 2.2 root 用户切换到普通用户 3. sudo 命令 3.1 配置认证 无论是 Windows&#xff0c;MacOS&#xff0c;Linux 均采用多用户的管理模式管理权限&#xff1b; 1. 超级管理员 root 在 Li…