代码精进之路
范学雷
Oracle首席软件工程师,Java SE安全组成员,OpenJDK评审成员
立即订阅
6350 人已学习
课程目录
已完结 47 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 你写的每一行代码,都是你的名片
免费
第一模块:代码“规范”篇 (16讲)
01 | 从条件运算符说起,反思什么是好代码
02 | 把错误关在笼子里的五道关卡
03 | 优秀程序员的六个关键特质
04 | 代码规范的价值:复盘苹果公司的GoToFail漏洞
05 | 经验总结:如何给你的代码起好名字?
06 | 代码整理的关键逻辑和最佳案例
07 | 写好注释,真的是小菜一碟吗?
08 | 写好声明的“八项纪律”
09 | 怎么用好Java注解?
10 | 异常处理都有哪些陷阱?
11 | 组织好代码段,让人对它“一见钟情”
12丨组织好代码文件,要有“用户思维”
13 | 接口规范,是协作的合约
14 | 怎么写好用户指南?
15 | 编写规范代码的检查清单
16丨代码“规范”篇用户答疑
第二模块:代码“经济”篇 (14讲)
17 | 为什么需要经济的代码?
18丨思考框架:什么样的代码才是高效的代码?
19 | 怎么避免过度设计?
20 | 简单和直观,是永恒的解决方案
21 | 怎么设计一个简单又直观的接口?
22丨高效率,从超越线程同步开始!
23 | 怎么减少内存使用,减轻内存管理负担?
24 | 黑白灰,理解延迟分配的两面性
25 | 使用有序的代码,调动异步的事件
26 | 有哪些招惹麻烦的性能陷阱?
27 | 怎么编写可持续发展的代码?
28 | 怎么尽量“不写”代码?
29 | 编写经济代码的检查清单
30丨“代码经济篇”答疑汇总
第三模块:代码“安全”篇 (14讲)
31 | 为什么安全的代码这么重要?
32 | 如何评估代码的安全缺陷?
33 | 整数的运算有哪些安全威胁?
34 | 数组和集合,可变量的安全陷阱
35 | 怎么处理敏感信息?
36 | 继承有什么安全缺陷?
37 | 边界,信任的分水岭
38 | 对象序列化的危害有多大?
39 | 怎么控制好代码的权力?
40 | 规范,代码长治久安的基础
41 | 预案,代码的主动风险管理
42 | 纵深,代码安全的深度防御
43 | 编写安全代码的最佳实践清单
44 | “代码安全篇”答疑汇总
加餐 (1讲)
Q&A加餐丨关于代码质量,你关心的那些事儿
结束语 (1讲)
结束语|如何成为一个编程好手?
代码精进之路
登录|注册

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

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

无心的过失

开始之前,我先给你讲个曾经发生过的真实案例。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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《代码精进之路》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(49)

  • 范学雷 置顶
    有一个问题我搞错了,部分留言的回复我已经不能修改了。 请见谅!

    一个类,如果写了构造方法,不论有没有构造参数,就没有缺省的构造方法了(无参的构造方法)。所以,我们的例子中,只要写了有参的构造方法,并且serverNames没有初始化为空,final的serverNames就不会是空值了。
    2019-01-18
    8
  • 古城痴人
    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
    16
  • 王智
    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
    1
    5
  • vector
    评论区高手如云,学到了好多。我想请教下老师,以Spec结尾命名这个类,是有什么说法吗,看到jdk源码也有好多这样的包名和类名,是特定的,指定的意思吗?

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

    2019-01-07
    3
  • Kai
    小黄鸭就是把你的代码逻辑解释给一个玩具听

    作者回复: 谢谢了

    2019-01-07
    3
  • 悲劇の輪廻
    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
    2
  • 落叶飞逝的恋
    关于课后思考题,除了其他同学的回答的,我再加一个,就是这段代码没注释。目前可能这段代码比较短,通过代码阅读可以知晓这段代码做什么功能。但如果实际项目的一大段代码没注释,那真的很痛苦!!!

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

    2019-01-14
    1
    2
  • 夏落若
    自己看出的问题加上看留言别人发现的问题,总结一下所有问题如下:
    第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孙)
    总结的好!

    &gt; 第7行,class使用public修饰
    嗯,如果是公开的类,就要有访问权限。 如果是包内部的类,使用缺省的访问权限也可以。

    &gt; 第8行, final变量应该初始化,且不能在构造函数中修改serverNames的引用
    final变量,在构造函数中初始化就行。

    &gt; 空构造函数,调用add会报错

    空构造函数没有声明,使用了带参的构造函数,缺省的空构造函数就没有了。

    &gt; 第11行,Collections.unmodifiableList()生成的List无法修改
    这个真不是,在构造函数中,可以初始化final变量。不过,也依赖于你最后怎么改的这个代码。你要是第8行初始化了,这里就不能再次初始化了。

    &gt; 第15行,List没有指定泛型,遍历就不是SNIServerName 类型,应该是Object
    嗯,我们应该在第8行声明时,就把类型这个问题解决掉。

    &gt; 第19行,if条件尽量使用括号,下面的return应该缩进
    是的,缩进和括号,都要有!

    &gt; 第23行,for循环可能会空引用。循环之前需要判断serverNamers是否是null
    对的,需要检查空值。

    &gt; 第24,25行,builder.append追加两次可以改成一次,节省运算
    嗯,可以写成一行;写两行也没什么毛病。

    你看你看,你上面的找问题,其实就是代码评审最关键的部分。人人都可以做,人人都可以找出问题。鉴于人人都会犯错误,也不要求每个问题都找对,所有的问题都找到。看代码的眼睛越多,代码的错误隐藏的机会就越小。

    如果我们这样看别人的代码,看自己的代码,不用多长时间,代码质量就会有显著的提升,编码越来越轻松。加油!

    2019-01-09
    2
  • 大於一
    回归测试其实怎么测? 不懂

    作者回复: 回归测试,简单的说,就是每做一个变更,把测试都跑一遍,免得变更引起我们意想不到的麻烦。

    找找有没有这方面的专栏。如果没有,你在给我留言,我们看看怎么样介绍些这方面的内容。

    2019-01-08
    2
  • chon
    建议,每次新的课程开始时候,能对上一次课程的问题进行解答一下,便于加深印象,而不是在课程结束时候,再统一解答

    作者回复: 讨论区高手很多,很多留言很有参考价值,带来新的见解,也是我学习的机会。我也建议你看看讨论区不同人的不同观察角度。每一个问题,都没有标准的答案。用好讨论区,我希望讨论区的价值比专栏文章的价值还要大。当然,参与讨论的人越多钱,价值就越大,我们的收获就越多。

    让我们在讨论区把练手题的疑问解决掉。为了不影响大家思考、讨论,我会稍晚几天回复练手题的问题。

    2019-01-07
    2
  • hz
    15行和24行可能引发空指针异常

    作者回复: 为什么会引起空指针异常? 能多解释下吗?

    2019-01-07
    2
  • chengang
    除了@pyhhou提到命名规范也很重要,servernames和servername肉眼很难区分,512定义为常量是否更加合理

    作者回复:
    &gt; servernames和servername肉眼很难区分
    嗯,这个点很好!

    &gt; 512定义为常量是否更加合理
    这也是一个很棒的观察点,是要考虑这个问题的。

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

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

    2019-05-21
    1
  • Neal
    老师,我有个疑问: SNIServerName是抽象类,无法直接使用,而它只有SNIHostName一个子类,不熟悉的人使用的时候,要经历无法抽象类无法实例化,查找API的过程,浪费了调用者的时间,用SNIHostName是不是更好?

    作者回复: 要分场景,参数传入,可以使用抽象类,这样的接口设计未来可以容纳更多的抽象类实现。这就是我们说的可扩展性。实例化的时候,使用实现类。

    2019-03-07
    1
  • 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
    1
  • wupeng
    看了前面的评论 问一下 19行 serverNames.isEmpty() 已经对erverNames判空了, 下面24行 sn.toString();就不会出现空指针了吧? 麻烦回答下 很疑惑 谢谢

    作者回复: 第十九行的serverNames.isEmpty(),判断的是serverNames这个集合里有没有东西。一个集合里,也可以有空值,也就是说sn可能是个空值。比如说, {null, null}就不是空集合,里面有两个空值的条目。所以,我们调用sn的方法之前,还要判断sn是不是空值。

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

    作者回复: 你是对的,这一点我遗漏了。 谢谢!

    我要回头改改给其他人的留言,免得误导了大家。

    2019-01-13
    1
  • 空构造函数,调用add会报错的吧

    作者回复: 是的,这是一个隐蔽的较深的问题。高手!

    2019-01-08
    1
  • ZackZzzzzz
    除了已经有的评论,还忘了@Override annotation. 这些其实很多在Effective Java有讲

    作者回复: 是的,@Override一定不要丢了!👍

    2019-01-08
    1
    1
  • sdmanooo
    这个bug很可能在准备上线合并代码的时候给合并错了

    作者回复: 这也是其中一种猜想。

    2019-01-07
    1
收起评论
49
返回
顶部