这篇文章汇聚33个BUG!来挑战一下,看看你能找出来几个?

9,351 阅读29分钟

你好呀,我是歪歪。

前几天看到“Qunar技术沙龙”公众号推送了一篇关于他们举办了一场“Code Review大赛”的文章。

看到 Code Review 我很感兴趣啊,“啪”的一下就点进去了啊,很快啊。

难能可贵的是,“Qunar技术沙龙”本着开放共享原则,把他们的 CR 大赛真题进行了免费开放。

我当时“啪”的一下就先把真题拿到手了,然后对着真题“刷刷刷”就是一顿输出。

不得不说,这套题出的是真的很不错,所以写篇文章带大家感受一波。

但是我觉得为了让你有更好的体验,我强烈建议你先在拿到这套真题,自己做一遍,然后在来和我对一下答案。

那么问题就来了:怎么拿到真题呢?

在公众号"why技术"后台回复关键字“CR”即可拿到。

如果你还是觉得麻烦,那么你也可以点开下面这张图片,然后往右边滑动,这样你就可以先只看到题目,而不看我的解析,(但是有可能突然看到一些奇怪的图片),可以自己先在心里做一遍。

还得提前说明一下的是,我写文章的时候,官方并没有给出每道题的标准答案是什么,官方题解也只是出了七道,后面还会慢慢做出来,所以我给出的解析也不一定是全部正确的。

还有,我能提醒你的是,一个题目里面可能有多个问题点,你需要尽量多的识别出来,宁可错杀,不可放过。

因为官方还未完全公布正确答案,那这样就更加有意思了,因为当你发现你的答案和我的答案不一样的时候,就是我们相互学习的时候。我特别希望你能指正我的答案,也特别感谢你能在评论区分享你的、和我不一样的答案。

我们碰撞一下,共同进步。

另外,官方题解在这里:

《看ChatGpt解去哪儿网第四届CR大赛题目》

目前只解答了 7 道题目,后面的解答还在路上。但是官方的题解中整了个活儿,就是把整段代码喂给 ChatGPT 让它帮忙诊断有什么问题,可以去看看。

我只能用一个字来形容它:

第 1 题

首先,拿到这题第一眼,我就看到一个非常别扭的地方,方法入参:cazeList?

不应该是 caseList 吗?

其实我理解 core review 不只是应该关注业务逻辑,也应该关注到这些命名规范、变量拼写的“小地方”。

所以,秉着宁可错杀,不可放过的原则,这算是一个需要修改的点。

然后第二个点:入参不判断非空的吗?

你看第 9 行,拿着 caseList 这个入参就在开始 get(0) 了。caseList 是传入进来的,万一传了一个 null 怎么办?

不要给我说外面可能判断了,不会传递 null。不要基于假设去分析问题,再次强调:宁可错杀,不可放过。

在这里加上一个非空判断,代码的健壮性更强。

接着看代码,在第 9 行做了一个批量插入的动作。被 try-catch 代码块包裹了起来。

在 catch 代码块里面执行了表不存在则创建,然后再插入的动作。这个代码能用,但是我个人觉得:不要在 catch 代码块里面写过多的逻辑,也就是尽量不要基于异常去做程序逻辑控制。

在插入之前先执行创建表的动作,然后再执行批量插入,这样的正向逻辑它不香吗?

总结一下第一题:

  1. 入参名字拼写错误,cazeList 应该是 caseList。
  2. caseList 没有做非空检验。
  3. 应该把 catch 代码块里创建表的动作,放到 try 逻辑里面去。先尝试创建表,再批量插入。

第 2 题

这题没啥好说的,直接秒杀。

五秒之内没有看出问题,说明“八股文”背的不牢。

我问你:Spring 事务注解生效的前提是什么?

是不是事务方法的调用方得是经过 Spring 代理后的对象?

那么上面代码中的事务会生效吗?

肯定不会的。

经典的 this 调用导致事务注解失效的场景,一眼秒杀之。

第 3 题

这题也应该属于可以“秒杀”的题目。

你看一下 getInstance 方法的实现,这是什么老伙计?

这不就是我们可爱的基于双重锁检查实现的单例模式吗?

当面试官问你双重锁检查的时候,考点是什么?

第一个看锁对象,这里是 class 对象,全局一把锁,没毛病。

第二个问你为什么要判断两次 instance == null。

因为有可能 A,B 两个线程都执行到了 16 行去抢锁。这个时候 A 先抢到了,B 就被阻塞。A 线程创建完一个实例之后释放了锁,这个时候 B 继续执行,如果不再次判断 instance == null,那么就会再次执行初始化的动作。

第三个考察点 instance 为什么要加 volatile?

因为一个对象的创建是分为三步的:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置instance指向刚分配的内存地址

如果 2 和 3 步被重排序了,那么就会出现一个有内存地址,但是还没有初始化完成的对象。

如果这个对象被其他线程访问到了,那么就是有问题的。所以我们加一个 volatile 来就禁止指令重排序,来规避这个问题。

这些都是老八股了,就不细说了。

拿着刚刚说的三个点,去和题目里面的代码对比,就会发现题目里面的代码是没有加 volatile,所以这就是要修复的地方。

另外,多说一句。加 volatile 这个方案,得 JDK5 版本以上使用,因为在 JDK5 之后,使用的新的 JSR-133 内存模型规范,在这个规范里面增强了 volatile 的语义。

该死,这句话是怎么出现在我的脑子里面的?

它就像是突然蹦出的一样,这就是传说中的刻在基因里面的“八股文”吧。

第 4 题

这题第一个非常明显的优化点在第 20 行。

要是看到这句话之后你还没反应过来是咋回事,那完犊子了,多半是要回去等通知了。

对于“流”的操作,关闭流的代码必须放在 finally 代码块里面啊,或者用 try-with-resource 语法糖,这是早期学 JavaSE 的时候就要重点掌握的东西了。

第二个要一眼秒杀的优化点是 22 行。

谁教你这样打印日志的?这样打印可以说完全没有任何卵用啊,因为丢失了“故事现场”。

应该这样写:

log.error("出现异常啦",e);

保留整个异常堆栈,方便后续定位问题,

第三个问题是 fileName,文件名称用的是时间戳,这样是有并发风险的。虽然看描述,这是一个给运营人员使用的功能,没啥并发量。在这个场景下,属于“错杀”。

但是,“并发”这根弦还是应该绷紧的。万一是外部客户导出功能呢?万一外部客户很多呢?万一很多外部客户就是在同一时间发起了统计呢?

第四个问题是我没搞懂第 11 行里面在干啥,因为题目中没有给出这部分的代码。

我理解应该是一个从某个数据源根据指定参数查询数据的功能。

为什么要把数据查出来之后再和 TOP_CAPCITY 比较并截取 list 呢?

如果运营商很多,比如有 10w 个运营商,但是运营只想看前 10 的运营商,也就是 TOP_CAPCITY=10 的情况。

这样把 10w 个运营商查询出来完全没有意义,而且还容易产生大对象,对 GC 也不友好啊。

所以,为什么不在查询数据源的时候,把 TOP_CAPCITY 作为参数传进去呢?

我认为这是第四个优化点。

总结一下第四题:

  1. 输出流关闭未放到 finally 代码块,或者用 try-with-resource。
  2. 日志打印不规范。
  3. 文件名称用的是时间戳,有并发风险。
  4. 查询数据的时候应该把 TOP_CAPCITY 作为条件,而不是全部查询出来再截取。

第 5 题

针对 future 进行异常捕获的时候,只捕获了 ExecutionException 异常。我觉得至少还得捕获一个 TimeoutException 吧。

因为这个 future 是 dubbo 接口异步获取的,所以 get 操作获取超时的场景应该还是比较常见吧。

另外一个就是 19 行和 29 行的日子打印,我不知道为什么没有采用 {} 占位符的方式去做,而是采用了 + 拼接。这个点由于 TITLE_Arg1 参数到底长啥样的,代码里面没有体现出来,所以只是提上一嘴。

但是同样是日志打印,34 行的日志打印和前面的一样,丢失了“故事现场”,没有把 e 打出来。

第 6 题

这一题,我首先想到的第一个是没有对 redPlanIdList 对象进行非空判断。

然后一看 Safes.of 这是什么写法,没见过啊。

于是用关键字搜索了一把:

相关的结果很少很少,只有看到这个靠谱一点:

这是别人自己封装的一个方法,并不是什么组件里面的。

好吧,就姑且当这个方法是对 null 对象进行了处理吧,把 null 转化为了空集合,避免了空指针异常。

然后我们接着往下看,在第 7 行使用了 parallelStream,并行流来对 redPlanIdList 对象进行操作。

但是你看一下第五行,我们知道 redPlanIdList 这个对象是一个 List,但是它具体是什么 List 呢,是 ArrayList 还是 CopyOnWriteArrayList 呢?

不得而知。

那我们假设是非线程安全的 ArrayList 呢?

有的同学看到这里的时候,是不是要开始抢答了:这题我会,在多线程里面使用了 ArrayList,有线程安全问题...

不对啊,抢答的不对啊。

即使这个地方的 redPlanIdList 是非线程安全的 ArrayList 也没有关系。因为在并行流里面,只是遍历了它,并没有对它进行新增操作。

所以 redPlanIdList 并没有线程安全问题。

有问题的是 15 行,result 是 ArrayList,调用了 addAll 方法,有线程安全问题。需要使用线程安全的集合类。

或者,把 parallelStream 修改为单线程的 stream,对吧?

对吗?

从程序逻辑上讲是对的,使用单线程没有任何毛病。

但是从需求背景上讲,这个地方完完全全就可以、也应该使用多线程的方式去提升响应速度嘛。

所以,总结一下这题:在多线程里面调用了 arrayList 的 addAll 方法,有线程安全问题。需要使用线程安全的集合类。

另外,追问一个八股文:请问 ArrayList 的线程不安全具体体现在什么地方?

针对这个问题,早年间,我还写过相关的文章,如果你不清楚的话,可以去翻翻:《ArrayList 的线程不安全》

这里就直接说答案了。

ArrayList 的线程不安全体现在多线程调用 add 方法的时候。具体有两个表现:

  1. 当在需要进行数组扩容的临界点时,如果有两个线程同时来进行插入,可能会导致数组下标越界异常。
  2. 由于往数组中添加元素不是原子操作,所以可能会导致元素覆盖的情况发生。

第 7 题

这题,我靠,第一眼,不得秒了它?

遍历 ArrayList 的同时,还在调用它的 remove(int index) 方法。这写法,江湖大忌啊。

在这个题目里面,由于调用了 remove(int index) 方法,所以导致队列的长度在变化,因此第 7 行会抛出数组下标越界的异常:

此外,如果把题目中的 for 循环修改为这样:

for (String s : testStrArray) {
    testStrArray.remove(s);
}

也是要完犊子的写法,会抛出 ConcurrentModificationException,老八股了,不细说了。

牢记一句话:针对 ArrayList 的删除,用迭代器来实现,稳得一笔。

除了这个显而易见的问题之外,在上面的题目中还有一处可以优化的地方。

就是 emp、empany、q、t 这几个变量名字取的真丑。你取名的时候,取做 isAllMatch、isAnyMatch、findFirstOfStr 它不香吗?

第 8 题

问题很好发现,精度丢失。

改正嘛...

为什么不问问神奇的 ChatGPT 呢?

第 9 题

这个 B...

我是说这个类名称,B,是不是有点太随意了?

当然我猜测这可能是主办方为了脱敏,随便取了一个 B,不过这是不是有点太随意了?

然后看看第 10 行,我理解不应该把异常抛给前端吧,除非你项目里面有全局异常处理器,但是题目中没体现,所以也提一嘴,保平安。

接着 15 行,又是流没有关闭,前面已经出现过一次了,不赘述。

再看看 16 行,HashSet 这玩意也不是线程安全的啊。所以在线程池里面,多线程调用其 add 方法,得改。

另外,你这个 HastSet 对象的名称叫做 visted。

我知道你是想表达这个 Set 里面放的是被访问过的 url。

但是 visted,什么鬼?不应该叫做 visited 吗,老铁。

既然都说到拼写错误了,那么你在看看这个方法名:proceessLog。

处理日志,应该是 processLog 吧。

多了一个字母,有一点代码洁癖的人,看得难受。

最后,最严重的一个问题,你看看第 10 行的这个线程池:newCachedThreadPool。

这玩意的调用的线程池的构造方法是这样的:

这是一个可以不断开新线程的线程池啊,然后你再读题:在文件中有大量的 URL。

“大量”,完犊子,线程过多,性能反而拉胯。

另外,线程池提交用的是 execute 方法,应该用 try-catch 代码块把线程池里面的逻辑给包起来,在 catch 里面记录日志,免得后面出问题了,日志都找不到。

第 10 题

官方给的题目中,第 9 题和第 10 题重复了。

我大抵是病了,横竖都睡不着,坐起身来点起了一支烟,这悲伤没有由来,黯然看着面前的两套考卷,一套是我的,另一套也是我的。

我翻开考卷一查,这题目没有答案,歪歪斜斜的每页上都写着“找出BUG”四个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是:卷起来。

第 11 题

这题,读完题就得秒啊,得秒之。

都已经说的很明白了:

  1. is 开头的 boolean 类型的属性,用的是 FastJson 序列化。
  2. 反序列化的工具有可能是 FastJson,也有可能是 gson。

首先第一点,即使你之前不知道 FastJson 对于 is 开头的 boolean 类型属性的处理方式,但是你从题目上也能读出来,题目上特意写了这句话,说明里面是有故事的。

第二个,序列化工具是 FastJson,但是反序列化可能是 FastJson 也可能是 gson。

这不就离了大谱了吗?

上面的代码我也拿出来给你跑一下,就很清楚了:

FastJson 序列化的时候,把 isWmsSend 变成了 wmsSend。然后用 Gson 反序列化的时候,发现没有 isWmsSend 这个属性呀,再一看,是基本类型的 boolean,那就给默认值,false 吧。

所以 isWmsSend 应该用包装类 Boolean,尽量不使用基本类型 boolean。因为包装类的默认值是 null,基本类型的默认值是 false。

在你的业务代码中,null 和 false 代表的可完全不是一个东西。

因为反序列化的问题导致把 true 变成了 false,这一听就是一个大锅啊,可不敢乱背。

第 12 题

看到这个题的第一眼,我的注意力是放在注释上的。

你看第 13 行到第 15 行的注释是放在代码后面的。但是第 23 行到 28 行的注释又是放在代码上面的。

这是两种不同的风格,我个人喜欢的是第二种。但是不管你喜欢哪一种,你至少统一一下吧。

还有,这里又出现了 proccess 这个单词。前面第 9 题的时候,写的是 proceess。

我能说什么呢?

我只能单打一个:6。

我承认,这两个点确实有点吹毛求疵了,但是作为有点代码洁癖的我来说,必须要说出来才爽。

然后,再说说这个代码真正有问题的地方。

第一个其实很明显,一千万的数据量,一页读取一千条数据,用传统的分页,到后面的深度分页,你会发现越来越慢,因为这个是有性能问题的。

应该采取游标的方式,比如返回本次查询的 max(id),然后下一次分页的时候带着 id 去查询。

这个也算是一个八股文吧,如果你不了解的话,去找资料背一背。

第二个问题是没有对 objectList 进行非空判断,防止空指针。

第三个问题,应该在 26 行的 for 循环里面搞个线程池,来提升数据处理速度。你这 1000w 的数据量了,单线程得搞到什么时候啊。

可能营销活动都要结束了,用户还没收到推送。到时候活动效果不好,又是程序员背锅。

第四个问题是 35 行这个方法。虽然我不懂 UserDao 这个写法是什么意思,应该是公司内部封装的持久化框架。

但是我看到这里是把 tableName 作为参数传递到了方法里面,这个时候就得提高警惕了:同志们,谨防 SQL 注入啊。安全的那帮哥们就喜欢扫这样的代码,拿出来公示。

第 13 题

这个题很有意思啊,因为它的考点比较多,也算比较“偏一点”。

另外,终于把 process 写对了,舒服了。

首先第一个,第 7 行加锁,第 8 行万一抛出异常了怎么办?

所以这个锁弄的有大问题啊,按照规范来说,加锁操作是要放在 try 代码块外的第一行。这之间不应该有任何其他的代码,以免加锁成功了,但是未解锁,出现死锁的现象。

第二个问题,很明显,你看第 15 行,processWorkB 方法有两个入参,但是第 10 行调用的时候只给了一个?

这个应该是题出的失误了,不做追究。要是有人真写出了这样的代码并没有发现问题,说明他是个奇才。关键是这玩意,代码都编译不过啊。

第三个问题,还是 processWorkB 方法,没有对 param 参数进行非空判断。

来我问你一个问题:如果 param 是 null 会出现什么情况?

肯定是空指针异常了啊。

但是,你有没有想过一个问题:switch/case 里面为什么不做成支持 null 的模式呢?

如果表达式为 null ,我们就拿着 null 去 case 里面匹配,这样理论上做也是可以做的。

关于这个问题,我再这篇文章里面做过探讨,有兴趣可以去看看:《被阿里一道基础面试题给干懵了,一气之下写出万字长文。》

第四个问题:在 processWorkA 里面,隐藏的比较深,但是确实是一个我在实际编码的过程中踩到过的一个坑。

简单来说就是三目表达式里面的拆箱问题。

巧了,这个问题,我之前也提到过一次:《三目表达式的自动拆箱问题》

网上的相关解析也很多,如果不了解的话可以定向攻破一下。

具体到这个代码中的问题,就是中间的 params1*params2 之后的结果,类型是 int 型。由于三目表达式类型要对齐的特性,所以 params3 会被拆箱为 int 类型。

结果,params3 参数又是一个 null。

哦豁,空指针就来了。

第 14 题

题目太长,我实在是看的费劲儿。

就......不解析了吧。

你发现了问题,可以再评论区教教我。

第 15 题

这一题,说实话,我第一眼没看出来问题。

隔了一会,第二眼看,也没看出来啥问题。

要不是这是一道题目,代表它确实有问题,不然在实际 review 的时候,我应该就放过这个部分了。

当我带着它一定是有问题的心态去看问题的时候,终于还是发现了端倪。

第 6 行,多线程操作,一般出题人会在多线程里面埋坑,只要找出多线程里面线程不安全的操作即可。

而多线程里面的核心逻辑是给 Status 的 reason 字段赋值。再定眼一看:Status 是个枚举啊。

枚举是单例啊。

单例对象的一个字段,在多线程里面被疯狂操作...

你明白我意思吧?

我给你搞段代码验证一下:

首先,把 List 里面的对象搞得多多的。

然后,closeStatus 方法里面打印 log 的时候,入参的 id 肯定要和 Status 枚举中的 reason 这个字符串里面拼接的 id 一样,对吧?

所以,我在 log 里面打印日志的时候,判断 reason 不包含当前的 id。

如果有输出,则说明有问题,能反应过来吧?

程序跑起来,确实有输出:那就说明确实有问题:

id:4548 当前的状态是:关闭 变更原因为:关闭当前状态3370
id:796 当前的状态是:关闭 变更原因为:关闭当前状态3300
id:8621 当前的状态是:关闭 变更原因为:关闭当前状态8000
id:9791 当前的状态是:关闭 变更原因为:关闭当前状态7528
id:8283 当前的状态是:关闭 变更原因为:关闭当前状态7842

不要在多线程里面对单例对象进行修改操作,你把握不住。

第 16 题

这题,拿到手第一眼,非常扎眼的一个问题就是 SearchCategory 对象里面的属性没有被 private 修饰,不满足 Java 对于对象进行封装的思想。

第二个考察点其实也不难,第 22 行,两边都是 Integer 对象,针对封装对象的对比,应该使用 !equals 而不是 !=。

别问为什么,问就是 Integer 缓存了 -128~127 之间的数字,在这个范围内用 == 和这个范围外用 ==,运行结果不一样。

老八股了,不细说了。

第三个考察点,稍微稍微隐蔽一点:SearchCategory 没有重写 hashcode 和 equals 方法。

categorySet 是一个 Set 集合对象。第 25 行里面,在把 SearchCategory 往 categorySet 里面放。

自定义对象往 Set 集合里面放,如果不重写 hashcode 和 equals 方法,那么会有重复元素滴。

如果这个你一时间没反应过来的话,那我问你一个老八股:HashMap 的 key 是自定义对象的时候,应该注意什么?

这样一听,是不是就熟悉多了。

你在结合我之前写过的这篇文章:《悄悄给你说几个HashCode的破事。》

轻松拿下。

第 17 题

这题我没搞懂第 6 行和第 9 行,调用了 queryUserStock 方法,但是这个方法又在同一个类里面。

为什么要通过自己注入自己的方式去调用呢?

又不涉及到比如事务、缓存这些切面相关的东西。所以我觉得这两行代码没用。

然后是第 4 行里面的入参 userId 是基本类型 long,而到了 14 行里面就变成了包装类型 Long。

所以,从这个代码片段来说,15 行的判断永远为 true,因为传递进来的 userId 是基本类型,默认值为 0,不可能为 null。

应该保持类型一致,都用包装类型。

还有一个点是 11 行,对于返回的集合没有做非空判断,万一空指针了呢,对吧?

对个毛线,我在这里虚晃一枪,看看你有没有带着自己的思考看文章。

14 行的方法是不会返回空指针的,如果没查到数据,MyBatis 会自动帮我们封装为空集合,而不是 null。

所以外面拿到空集合也没有任何毛病。

另外,再这里我再次重申一下:官方也没有给出全部的题目解析,所以我给的答案不一定是正确的,你要带着批判的眼光来看。

我等着你发现错误,指正我呢。

第 18 题

题目做到这里后,一看到多线程,就开始条件反射式的想到了线程安全问题。

定眼一看,33 行,CopyOnWriteArrayList 是线程安全的,看来这次的考察点不一样了。

从上往下看,首先是自定义线程池,我一眼就看出了问题:

(r, executor) -> log.error("...")

自定义拒绝策略中,仅仅打印了一行日志,没有做其他任何操作,有坑,容易喜提生产事件:

详情见连接:《虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。》

所以这个拒绝策略需要重新写一个。

第二个问题:21 行调用了 filter 方法,在这个方法里面用到了线程池,但是你仔细分析一下 34 行的这个 for 循环,要循环多少次?

是不是要循环“一页的大小”,也就是第 19 行的 500 次。但是这个线程池的容量才多少?

这个线程池同时间做多也只能容纳 120 个任务,所以这个地方参数不合理,需要调整线程池参数。

另外,34 行这个循环对象也没有判空啊。这是一个 JSONObject 对象,一不留神,取出一个 null 怎么办?

第三个问题,35 行,用了 submit 提交任务,这个方法是有返回值的,但是代码里面又没有使用这个返回值,那么为什么不用 execute 方法呢?

第四个问题,47 行,await 方法没有指定等待时间,容易死等。

比如就这个代码,线程池满了之后,并不会执行 countDown 方法,喜提一个永远在阻塞的线程。

这题靠多线程,算是撞我枪口上了,秒了,再见。

第 19 题

这题没啥特别好说的,第 18 行 repaceFirst 的第一个入参是正则表达式。

所以,如果供应商的名字里面有一些什么 *?.| 这些玩意,就会出现问题。

其实 Sting 的一些关于内容替换的方法里面好多都是支持正则的,所以如果你不清楚这个“坑”的话,那后面需要注意一下了。

这个题就不多说了,写个案例,一目了然:

别问我为什么一眼就看到了 repaceFirst,因为我在它身上吃过亏。还有 split,这个“坑爹”的方法,也是一样,有正则坑。

第 20 题

这题...

恕我学艺不精,确实没发现有什么问题啊?

难道是 13 行这个 false?它代表用 JDK 的动态代理,要实现接口,但是这个代码也确实实现了接口呀?

肉眼没看出来,我把代码放到自己的 idea 里面去跑也是能正常跑的:

如果这题在 AOP 上有考点的话,而且我觉得肯定是有考点的,我确实拿不到这个分。

如果考的是不要把 http 接口写在项目启动类上的这种编码结构上的规范的话。好吧,只能说考查角度刁钻。

不知道你有没有看出啥问题,可以在评论区教教我。

第 21 题

只有几行代码,秒之。

第 7、8 行,用 redis 加锁,set 方法和给值设置过期时间的方法非原子性,如果 set 完成之后,expire 无法执行, 会出现死锁。

第 11 行,捕获异常后没有通过日志打印异常具体的信息,不方便追溯问题。

第 22 题

其实题目做到这里的时候,都已经有点掉入思维陷阱里面了。

啥意思呢。

比如这个题,我只是瞟了一眼,我就知道 34 行的这个方法里面肯定是有问题的。

因为如果没有问题,按照出题人的习惯,这个方法的实现会略去。

所以,当我带着这个想法去看这部分代码的时候,getCerts 之后直接调用了 get(0),不用判断集合对象是否为空吗?

但是,你注意啊,其实在 23 行的时候判断了。

所以这个地方其实不是空指针的问题。

我觉得应该是需要结合到业务场景来看的,certs 是一个集合,里面放的是乘机人的证件,一个人可能有多个证件,比如身份证、护照、工牌、打工人认证、一叠好人卡等等这些玩意,所以用集合来装,和合理。

但是,get(0) 你怎么给我保证获取出来的不是好人卡,而是身份证呢?

代码中都没有体现这个逻辑,所以我怀疑出题人在这里是埋坑了。

第二个问题就很明显了,你仔细看看题目中的三次 stream 操作。前两次操作的都是同一个集合,然后用结果去覆盖了 passengers 参数。

看代码,是会一点点流式操作的,但是不多,不然也不会分开写了,三次操作完全可以合并为一个。

第 23 题

这题一看...

代码太多,看得我眼睛充血,脑壳疼。

再一看题,按照“充血模式”开发,补全一些代码。

这玩意感觉得结合着业务来才行啊,算了,我放弃,好吧。

这题我不做了。

你来。

第 24 题

感觉问题在 21 行的这个 Lists.transform 方法上,但是我没有用过这个方法,不清楚是干啥的。

于是在网上查了一圈:

简单来说就是第 24 行这个 for 循环是没有用的。你可以理解未返回的这个 addresses 仅仅是一个视图,对视图进行修改,没有任何卵用。

然后是同样没有看到对于异常的处理机制,如果没有全局异常处理机制的话,有可能会直接抛给用户不友好的错误提醒。

第 25 题

第 29 行,又看到事务了,又是事务不生效,就不多说了。这个事务不生效这一点确实是一大考点,一定要把这个玩意搞的滚瓜烂熟才行。

然后是 41 行到44 行,这是个什么神奇的写法,链式 set? 没见过。

所以我感觉这个地方也是有问题的,应该是想用 Builder 模式来构建对象吧。

另外,saveData 方法里面有一些数据库操作,难免会出现一点异常情况,在这个方法里面我没有看到对于异常的相关捕获和处理。

最后一个点,38 行,除非你用了 MDC 链路追踪,不然这行日志没有任何意义啊,至少再加上一个订单号吧。才能一眼看出来是哪个订单号被成功更新了。

附加题

官方的题目只公开了 25 题,但是歪歪歪师傅也想给你出几道附加题,让你来体验一下。

我丑话说在前头:做不出来的,都是假粉丝。

再多说一句,我都说了是附加题了,这些题肯定就很偏了,看着图个乐就行,真正的代码 review 注意点,还是得看前面的题目。

附加题一

请找出下面代码的问题:

《附加题一的答案》

附加题二

请找出下面代码的问题:

《附加题二的答案》

附加题三

请解释下面代码不能正常结束的原因:

《附加题三的答案》

附加题四

asyncResult 是一个 Future 对象,下面框起来的代码可以认为是无限期等待,请指出其和 future.get() 的区别:

《附加题四的答案》

附加题五

请找出下面代码的问题:

《附加题五的答案》

附加题六

请解释下面代码不能正常结束的原因:

《附加题六的答案》

附加题七

以下程序是否会抛出空指针异常,并解释为什么:

《附加题七的答案》

附加题八

请找出下面代码的问题:

《附加题八的答案》

再说一次,附加题,考点很偏,看着图个乐就行。别把自己给 PUA 了。

最后,求个赞,不过分吧?

“ 本文正在参加「金石计划」 ”