限时领|《AI 百问百答》专栏课+实体书(包邮)! 了解详情
写点什么

Java 编码易疏忽的十个问题

  • 2012-09-04
  • 本文字数:3624 字

    阅读完需:约 12 分钟

在 Java 编码中,我们容易犯一些错误,也容易疏忽一些问题,因此笔者对日常编码中曾遇到的一些经典情形归纳整理成文,以共同探讨。

1. 纠结的同名

现象

很多类的命名相同(例如:常见于异常、常量、日志等类),导致在 import 时,有时候张冠李戴,这种错误有时候很隐蔽。因为往往同名的类功能也类似,所以 IDE 不会提示 warn。

解决

写完代码时,扫视下 import 部分,看看有没有不熟悉的。替换成正确导入后,要注意下注释是否也作相应修改。

启示

命名尽量避开重复名,特别要避开与 JDK 中的类重名,否则容易导入错,同时存在大量重名类,在查找时,也需要更多的辨别时间。

2. 想当然的 API

现象

有时候调用 API 时,会想当然的通过名字直接自信满满地调用,导致很惊讶的一些错误:

示例一:flag 是 true?

复制代码
boolean flag = Boolean.getBoolean("true");

可能老是 false。

示例二:这是去年的今天吗(今年是 2012 年,不考虑闰年)?结果还是 2012 年:

复制代码
Calendar calendar = GregorianCalendar.getInstance();
calendar.roll(Calendar.DAY_OF_YEAR, -365);

下面的才是去年:

复制代码
calendar.add(Calendar.DAY_OF_YEAR, -365);

解决办法

问自己几个问题,这个方法我很熟悉吗?有没有类似的 API? 区别是什么?就示例一而言,需要区别的如下:

复制代码
Boolean.valueOf(b) VS Boolean.parseBoolean(b) VS Boolean.getBoolean(b);

启示

名字起的更详细点,注释更清楚点,不要不经了解、测试就想当然的用一些 API,如果时间有限,用自己最为熟悉的 API。

3. 有时候溢出并不难

现象

有时候溢出并不难,虽然不常复现:

示例一:

复制代码
long x=Integer.MAX_VALUE+1;
System.out.println(x);

x 是多少?竟然是 -2147483648,明明加上 1 之后还是 long 的范围。类似的经常出现在时间计算:

数字 1×数字 2×数字 3…示例二:

在检查是否为正数的参数校验中,为了避免重载,选用参数 number, 于是下面代码结果小于 0,也是因为溢出导致:

复制代码
Number i=Long.MAX_VALUE;
System.out.println(i.intValue()>0);

解决

  1. 让第一个操作数是 long 型,例如加上 L 或者 l(不建议小写字母 l,因为和数字 1 太相似了);
  2. 不确定时,还是使用重载吧,即使用 doubleValue(),当参数是 BigDecimal 参数时,也不能解决问题。

启示

对数字运用要保持敏感:涉及数字计算就要考虑溢出;涉及除法就要考虑被除数是 0;实在容纳不下了可以考虑 BigDecimal 之类。

4. 日志跑哪了?

现象

有时候觉得 log 都打了,怎么找不到?

示例一:没有 stack trace!

复制代码
} catch (Exception ex) {
log.error(ex);
}

示例二:找不到 log!

复制代码
} catch (ConfigurationException e) {
e.printStackTrace();
}

解决

  1. 替换成 log.error(ex.getMessage(),ex);
  2. 换成普通的 log4j 吧,而不是 System.out。

启示

  1. API 定义应该避免让人犯错,如果多加个重载的 log.error(Exception) 自然没有错误发生
  2. 在产品代码中,使用的一些方法要考虑是否有效,使用 e.printStackTrace() 要想下终端 (Console) 在哪。

5. 遗忘的 volatile

现象

在 DCL 模式中,总是忘记加一个 Volatile。

复制代码
private static CacheImpl instance; //lose volatile
public static CacheImpl getInstance() {
if (instance == null) {
synchronized (CacheImpl.class) {
if (instance == null) {
instance = new CacheImpl ();
}
}
}
return instance;
}

解决

毋庸置疑,加上一个吧,synchronized 锁的是一块代码(整个方法或某个代码块),保证的是这”块“代码的可见性及原子性,但是 instance == null 第一次判断时不再范围内的。所以可能读出的是过期的 null。

启示

我们总是觉得某些低概率的事件很难发生,例如某个时间并发的可能性、某个异常抛出的可能性,所以不加控制,但是如果可以,还是按照前人的“最佳实践”来写代码吧。至少不用过多解释为啥另辟蹊径。

6. 不要影响彼此

现象

在释放多个 IO 资源时,都会抛出 IOException ,于是可能为了省事如此写:

复制代码
public static void inputToOutput(InputStream is, OutputStream os,
boolean isClose) throws IOException {
BufferedInputStream bis = new BufferedInputStream(is, 1024);
BufferedOutputStream bos = new BufferedOutputStream(os, 1024);
….
if (isClose) {
bos.close();
bis.close();
}
}

假设 bos 关闭失败,bis 还能关闭吗?当然不能!

解决办法

虽然抛出的是同一个异常,但是还是各自捕获各的为好。否则第一个失败,后一个面就没有机会去释放资源了。

启示

代码 / 模块之间可能存在依赖,要充分识别对相互的依赖。

7. 用断言取代参数校验

现象

如题所提,作为防御式编程常用的方式:断言,写在产品代码中做参数校验等。例如:

复制代码
private void send(List< Event> eventList) {
assert eventList != null;
}

解决

换成正常的统一的参数校验方法。因为断言默认是关闭的,所以起不起作用完全在于配置,如果采用默认配置,经历了 eventList != null 结果还没有起到作用,徒劳无功。

启示

有的时候,代码起不起作用,不仅在于用例,还在于配置,例如断言是否启用、log 级别等,要结合真实环境做有用编码。

8. 用户认知负担有时候很重

现象

先来比较三组例子,看看那些看着更顺畅?

示例一:

复制代码
public void caller(int a, String b, float c, String d) {
methodOne(d, z, b);
methodTwo(b, c, d);
}
public void methodOne(String d, float z, String b)
public void methodTwo(String b, float c, String d)

示例二:

复制代码
public boolean remove(String key, long timeout) {
Future< Boolean> future = memcachedClient.delete(key);
public boolean delete(String key, long timeout) {
Future< Boolean> future = memcachedClient.delete(key);

示例三:

复制代码
public static String getDigest(String filePath, DigestAlgorithm algorithm)
public static String getDigest(String filePath, DigestAlgorithm digestAlgorithm)

解决

  1. 保持参数传递顺序;
  2. remove 变成了 delete,显得突兀了点, 统一表达更好;
  3. 保持表达,少缩写也会看起来流畅点。

启示

在编码过程中,不管是参数的顺序还是命名都尽量统一,这样用户的认知负担会很少,不要要用户容易犯错或迷惑。例如用枚举代替 string 从而不让用户迷惑到底传什么 string, 诸如此类。

9. 忽视日志记录时机、级别

现象

存在下面两则示例:

示例一:该不该记录日志?

复制代码
catch (SocketException e)
{
LOG.error("server error", e);
throw new ConnectionException(e.getMessage(), e);
}

示例二:记什么级别日志?

在用户登录系统中,每次失败登录:

复制代码
LOG.warn("Failed to login by "+username+");

解决

  1. 移除日志记录:在遇到需要 re-throw 的异常时,如果每个人都按照先记录后 throw 的方式去处理,那么对一个错误会记录太多的日志,所以不推荐如此做;但是如果 re-throw 出去的 exception 没有带完整的 trace( 即 cause),那么最好还是记录下。
  2. 如果恶意登录,那系统内部会出现太多 WARN,从而让管理员误以为是代码错误。可以反馈用户以错误,但是不要记录用户错误的行为,除非想达到控制的目的。

启示

日志改不改记?记成什么级别?如何记?这些都是问题,一定要根据具体情况,需要考虑:

  1. 是用户行为错误还是代码错误?
  2. 记录下来的日志,能否能给别人在不造成过多的干扰前提下提供有用的信息以快速定位问题。

10. 忘设初始容量

现象

在 JAVA 中,我们常用 Collection 中的 Map 做 Cache, 但是我们经常会遗忘设置初始容量。

复制代码
cache = new LRULinkedHashMap< K, V>(maxCapacity);

解决

初始容量的影响有多大?拿 LinkedHashMap 来说,初始容量如果不设置默认是 16,超过 16×LOAD_FACTOR, 会 resize(2 * table.length), 扩大 2 倍:采用 Entry[] newTable = new Entry[newCapacity]; transfer(newTable),即整个数组 Copy, 那么对于一个需要做大容量 CACHE 来说,从 16 变成一个很大的数量,需要做多少次数组复制可想而知。如果初始容量就设置很大,自然会减少 resize, 不过可能会担心,初始容量设置很大时,没有 Cache 内容仍然会占用过大体积。其实可以参考以下表格简单计算下, 初始时还没有 cache 内容, 每个对象仅仅是 4 字节引用而已。

  • memory for reference fields (4 bytes each);
  • memory for primitive fields

Java type Bytes required boolean 1 byte char 2 short int 4 float long 8 double 启示

不仅是 map, 还有 stringBuffer 等,都有容量 resize 的过程,如果数据量很大,就不能忽视初始容量可以考虑设置下,否则不仅有频繁的 resize 还容易浪费容量。

在 Java 编程中,除了上面枚举的一些容易忽视的问题,日常实践中还存在很多。相信通过不断的总结和努力,可以将我们的程序完美呈现给读者。


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-09-04 00:0011817

评论

发布
暂无评论
发现更多内容

架构师训练营第十章作业

叮叮董董

腾讯一面面试官让我关闭连接

我是程序员小贱

Go make 和 new 的区别

曲镇

make Go 语言

Code Review 失败后总结的几个实践技巧

Phoenix

团队管理 团队协作 技术人 代码质量

架构师训练营--第10周作业

Just顾

40张图入门Linux——(前端够用,运维入门)

执鸢者

Linux 大前端

招银网络问了啥?这么尬?妥妥的安排

我是程序员小贱

troubleshoot之:使用JFR解决内存泄露

程序那些事

Java 内存泄露 性能调优

环信助力OFashion迷橙开辟海外直播带货新通路

DT极客

炸裂!40+图万字长文拿下HTTP

我是程序员小贱

计算机网络

架构师训练营第十章总结

叮叮董董

第十周学习总结

赵龙

链表应用之设计高性能访客记录系统

架构师修行之路

数据结构 链表 架构师

关于微服务架构(中台架构、领域驱动设计、组件设计原则)的一点思考

jason

有意思:Go函数的闭包

申屠鹏会

闭包 函数 Go 语言

Newbe.Claptrap 框架如何实现 Claptrap 的多样性?

newbe36524

容器 微服务 .net core ASP.NET Core

远程办公暴露过程管理的不足

持续交付实践指南

管理 软件工程 远程办公

Week 10

一叶知秋

微服务与DDD学习总结

qihuajun

我期待,这是个多彩的世界

瓜藤老祖

大三儿 乐队的夏天 九连真人

socket通信,你还会实现么?

小隐乐乐

热乎的宇宙条总部面经,已拿offer,速来围观

我是程序员小贱

架构师训练营第十周作业

qihuajun

可读代码编写炸鸡十 - 保持单纯

多选参数

代码质量 代码 代码优化 可读代码编写 可读代码

如何优雅的编写GO程序?

八两

优雅 语法 Go 语言

[翻译]分布式系统的模式-综述

流沙

架构 分布式系统

一文读懂GaussDB(for Mongo)的计算存储分离架构

华为云开发者联盟

数据库 mongodb 数据 GaussDB 存储分离

第十周命题作业

赵龙

芯片破壁者(十一):回看日本半导体的倾塌

脑极体

六张图从HTTP/0.9进化到HTTP3.0

执鸢者

大前端 网络 HTTP

Dubbo源码分析--dubbo-config配置层的套路

jason

Java编码易疏忽的十个问题_Java_傅健_InfoQ精选文章