代码精进之路
范学雷
前 Oracle 首席软件工程师,Java SE 安全组成员,OpenJDK 评审成员
38234 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
结束语 (1讲)
代码精进之路
15
15
1.0x
00:00/00:00
登录|注册

02 | 把错误关在笼子里的五道关卡

分享讨论
代码示例
怎样打造更好的关卡或者设置更好的笼子
编写优秀的代码依赖于每个关卡的优秀级别
代码分析
代码评审
回归测试
编译器
提高程序员的修养
一个人犯了错误并不可怕,怕的是不承认错误
同样的错误不能犯第二次
人人都会犯错误的共识
人类犯错误的复杂性
多余的代码导致的安全漏洞
C语言的伪代码漏洞情况
苹果公司的安全漏洞案例
思考优秀代码的角度
经济、规范、安全的代码
一起来动手
代码制造的流水线
把错误关在笼子里
人人都会犯错误
无心的过失
优秀的代码
把错误关在笼子里的五道关卡

该思维导图由 AI 生成,仅供参考

上一讲中,我们一起讨论了什么是优秀的代码。简而言之,优秀的代码是经济、规范、安全的代码。在平时的工作中,我们要朝着这个方向努力,时常站在团队、流程、个人能力的角度去思考优秀代码。
作为一名软件工程师,我们都想写出优秀的代码。可是,怎么才能编写出经济、规范、安全的代码呢?这是个大话题,相信你之前也有过思考。

无心的过失

开始之前,我先给你讲个曾经发生过的真实案例。2014 年 2 月,苹果公司的 iOS 和 OS X 操作系统爆出严重的安全漏洞,聪明的黑客们可以利用这一漏洞,伪装成可信网站或者服务,来拦截用户数据。而造成这一漏洞的原因,也让业界专家大跌眼镜。
下面我用 C 语言的伪代码来给你简单描述下当时的漏洞情况。
if ((error = doSomething()) != 0)
goto fail;
goto fail;
if ((error= doMore()) != 0)
goto fail;
fail:
return error;
其实这段代码非常简单,它有两个判断语句,如果判断条件成立,那就执行“goto fail”语句,如果不成立,那就跳过判断语句继续执行。上面的“goto fail”语句,它的意思是略过它之后的所有语句,直接跳转到标有“fail”语句的地方,也就是第 6 行。
我们来分析下,第一个判断条件(第一行和第二行),如果 error 不等于零,那就跳转到 fail 语句,这逻辑上没什么问题。而第三行,没有任何附加条件,就能直接跳转到 fail 语句,也就是说,它下面的代码永远也无法执行,这里是不是有问题?是的,漏洞就是出在这里。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了编写优秀代码的关键,通过苹果公司的安全漏洞案例强调了即使在大公司也可能犯低级错误。作者认为好的程序员也会犯错,并呼吁对自己和他人更宽容。文章提出了三个观点:好的程序员不会写坏的代码,同样的错误不能犯第二次,以及一个人犯了错误并不可怕,怕的是不承认错误。此外,文章还探讨了五道关卡将错误关在笼子里的方法,包括程序员的修养、编译器的作用、回归测试、代码评审和代码分析。作者强调了优秀代码的编写不仅依赖于个人的努力,还需要高质量的流水线和每个关卡的积极反馈。文章内容深入浅出,为读者提供了思考优秀代码编写的新视角。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《代码精进之路》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(65)

  • 最新
  • 精选
  • X.F
    置顶
    有一个问题我搞错了,部分留言的回复我已经不能修改了。 请见谅! 一个类,如果写了构造方法,不论有没有构造参数,就没有缺省的构造方法了(无参的构造方法)。所以,我们的例子中,只要写了有参的构造方法,并且serverNames没有初始化为空,final的serverNames就不会是空值了。
    2019-01-18
    16
  • 老码不识途
    1. 第7行:ServerNameSpec建议增加访问修饰符:public 2. 第11行:返回的是 UnmodifiableList 类型的List,但是在15行中使用了add方法,会抛:UnsupportedOperationException异常; 3. 第20行:没有缩进;也没有使用大扩号来包裹代码块。 4. 第23行:serverNames没有使用泛型,所以直接使用SNIServerName会编译不过。

    作者回复: 找到了好几个问题,很赞! > 1. 第7行:ServerNameSpec建议增加访问修饰符:public 要是公开类,需要加public;包内部的类,可以使用缺省的修饰符。 > 2. 第11行:返回的是 UnmodifiableList 类型的List,但是在15行中使用了add方法,会抛:UnsupportedOperationException异常; 👍,是的。如果肉眼看不到,这是一个测试可以测出的错误。 > 3. 第20行:没有缩进;也没有使用大扩号来包裹代码块。 是的,需要使用大扩号和缩进,两个都要用。 > 4. 第23行:serverNames没有使用泛型,所以直接使用SNIServerName会编译不过。 是的,这是一个编译器可以发现的错误。

    2019-01-07
    25
  • vector
    评论区高手如云,学到了好多。我想请教下老师,以Spec结尾命名这个类,是有什么说法吗,看到jdk源码也有好多这样的包名和类名,是特定的,指定的意思吗?

    作者回复: Spec一般是Specification的缩写,表示这个类表示某种实际的规范,包含特定的参数。比如RSAPublicKeySpec,表示一个RSA公开密钥是有那几个参数组成的。

    2019-01-07
    10
  • 悲劇の輪廻
    new ServerNameSpec的时候参数serverName给null,Collections.unmodifiableList(null)就报空指针了?

    作者回复: Collections.unmodifiableList规范里没写会报异常,Collections.unmodifiableList(null)就不应该抛出异常。 但是,事实的确抛出了异常。我写了个一行的代码: List<String> list = Collections.unmodifiableList(null); 运行时的异常开起来象是: Exception in thread "main" java.lang.NullPointerException at java.base/java.util.Collections$UnmodifiableCollection.<init>(Collections.java:1028) at java.base/java.util.Collections$UnmodifiableList.<init>(Collections.java:1301) at java.base/java.util.Collections.unmodifiableList(Collections.java:1288) Collections类规范写了,“The methods of this class all throw a NullPointerException if the collections or class objects provided to them are null. ” 但是这种表达方式并不直观,很难找到,不好用。对我来说,这是JDK/OpenJDK的一个bug。 如果你想给OpenJDK报一个bug, 请往core-libs-dev@openjdk.java.net发邮件,或者使用https://bugs.java.com/提交bug。 意外的收获!大概我们都太熟悉这个方法了,一直都没注意到这个问题。

    2019-01-15
    8
  • 王智
    1. class使用public修饰比较好,一个类有一个主类 2. final变量应该初始化 3. Collections.unmodifiableList()生成的List无法修改 4. if条件尽量使用括号,下面的return应该缩进 5. List没有指定泛型,遍历就不是SNIServerName 类型,应该是Object 6 builder.append追加两次可以改成一次,节省运算 以上就是我的见解,可能不正确,还请谅解

    作者回复: (修改后的回复,我遗漏了已经没有缺省构造函数的问题,谢谢@kenes孙) > 1. class使用public修饰比较好,一个类有一个主类 嗯,要是公开类,需要使用public修饰符。 要是包内部类,缺省的也可以,只能在包内使用。 > 2. final变量应该初始化 final变量在构造函数里初始化也可以。 > 3. Collections.unmodifiableList()生成的List无法修改 对的,这是一个绕弯的问题,你找到了👍! > 4. if条件尽量使用括号,下面的return应该缩进 是的,括号和缩进都要有! > 5. List没有指定泛型,遍历就不是SNIServerName 类型,应该是Object 对的,我们再声明时,就应该把泛型类型这个问题处理好。 > 6 builder.append追加两次可以改成一次,节省运算 非常好的观点! > 以上就是我的见解,可能不正确,还请谅解 留言区就是大家畅所欲言的、开放的地方。“人人都会犯错误”那一小节,我就是想让大家彻底放下犯错误的任何包袱。想说什么就说什么,😀放开点。

    2019-01-07
    5
    7
  • 18118762952
    看了一下SNIServerName 的取值返回在0-255之间,否则会抛异常,512的容量肯定大了,需要设置在0-255之间。 这是我非常非常非常喜欢的一点!这一个问题找的太好了!我们要去看不熟悉的类的规范,一定是看过才会找到这个问题。 上面的这点没太看明白,设置512是大了浪费还是其他,这里可能有多个对象的追加, 可以解释下吗 另外类似这种初始化设置一般设置多少合适?

    作者回复: 抱歉,我应该解释下这个背景。 SNIServerName一般用在TLS的连接中,用来指明所连接的服务器的域名。比如,我们使用https://www.example.com/,那么SNIServerName就应该设置成"www.example.com”。建立连接时,这个域名会被校验,已确保的确时连接到了“www.example.com”, 而不是一个冒牌的网站。 为了更多的灵活性,TLS的规范定义可以使用多个域名。实际上,一般情况下(比如HTTPS)一个连接仅使用一个域名。ServerNameSpec就是用来描述规范定义的,而SNIServerName就是用来描述一个域名的。 由于一般情况下,只有一个域名,所以初始化内存够一个域名用的效率就会好一些。这个问题之所以找的好,是因为,如果不去阅读相关的规范,是不会想到这一点的,这是非常专业的内容。 类似的初始化设置,如果初始化的容量和需要的容量一样大小、或者大一点点,都没有问题。但是,如果容量并不显而易见,需要计算,比如要遍历一个大的列表,我们就大致估计一个数值就好了,不要去遍历列表。如果也估计不出来,使用512算是一个常见的选择。

    2019-02-12
    6
  • 落叶飞逝的恋
    关于课后思考题,除了其他同学的回答的,我再加一个,就是这段代码没注释。目前可能这段代码比较短,通过代码阅读可以知晓这段代码做什么功能。但如果实际项目的一大段代码没注释,那真的很痛苦!!!

    作者回复: 赞!这真的是一个很大的问题!

    2019-01-14
    2
    6
  • Sisyphus235
    日常开发免不了大量业务逻辑,在做 code review 的时候,他人的业务逻辑部分不熟悉会很影响 code review,否则只能逐行看代码质量,而不能从设计模式和更高层面做 review,请问大家如何处理这个问题?

    作者回复: 在我的工作场景下,这是很常见的事情。Code review最先看的不是code的细节,而是业务逻辑和设计这些更高层面的东西,然后才会去看代码的实现是不是准确。业务逻辑和设计的review,可以使用单独的文档(比如OpenJDK的CSR,JEP等),也可以使用内嵌的规范(比如Java的API规范),也可以使用代码内的注释。看不清业务逻辑的代码,要加上合理的注释;要不然,这早晚都是维护者要填的坑。

    2019-05-21
    4
  • 夏落若
    自己看出的问题加上看留言别人发现的问题,总结一下所有问题如下: 第7行,class使用public修饰 第8行, final变量应该初始化,且不能在构造函数中修改serverNames的引用 空构造函数,调用add会报错 第11行,Collections.unmodifiableList()生成的List无法修改 第15行,List没有指定泛型,遍历就不是SNIServerName 类型,应该是Object 第19行,if条件尽量使用括号,下面的return应该缩进 第23行,for循环可能会空引用。循环之前需要判断serverNamers是否是null 第24,25行,builder.append追加两次可以改成一次,节省运算

    作者回复: (修改后的回复,我遗漏了已经没有缺省构造函数的问题,谢谢@kenes孙) 总结的好! > 第7行,class使用public修饰 嗯,如果是公开的类,就要有访问权限。 如果是包内部的类,使用缺省的访问权限也可以。 > 第8行, final变量应该初始化,且不能在构造函数中修改serverNames的引用 final变量,在构造函数中初始化就行。 > 空构造函数,调用add会报错 空构造函数没有声明,使用了带参的构造函数,缺省的空构造函数就没有了。 > 第11行,Collections.unmodifiableList()生成的List无法修改 这个真不是,在构造函数中,可以初始化final变量。不过,也依赖于你最后怎么改的这个代码。你要是第8行初始化了,这里就不能再次初始化了。 > 第15行,List没有指定泛型,遍历就不是SNIServerName 类型,应该是Object 嗯,我们应该在第8行声明时,就把类型这个问题解决掉。 > 第19行,if条件尽量使用括号,下面的return应该缩进 是的,缩进和括号,都要有! > 第23行,for循环可能会空引用。循环之前需要判断serverNamers是否是null 对的,需要检查空值。 > 第24,25行,builder.append追加两次可以改成一次,节省运算 嗯,可以写成一行;写两行也没什么毛病。 你看你看,你上面的找问题,其实就是代码评审最关键的部分。人人都可以做,人人都可以找出问题。鉴于人人都会犯错误,也不要求每个问题都找对,所有的问题都找到。看代码的眼睛越多,代码的错误隐藏的机会就越小。 如果我们这样看别人的代码,看自己的代码,不用多长时间,代码质量就会有显著的提升,编码越来越轻松。加油!

    2019-01-09
    4
  • kenes
    已经有了有参的构造函数,就没有了默认的无参构造函数,所以根本new不了,自然就没有空构造函数调用add的问题了吧……

    作者回复: 你是对的,这一点我遗漏了。 谢谢! 我要回头改改给其他人的留言,免得误导了大家。

    2019-01-13
    3
收起评论
显示
设置
留言
65
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部