写点什么

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:0011155

评论

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

阿里巴巴商品详情数据接口Python

tbapi

阿里巴巴 阿里巴巴API接口 阿里巴巴商品详情接口

面试官:如何防止短信盗刷和短信轰炸?

王磊

Java 面试

深入解析Linux进程内存:VSS、RSS、PSS、USS及查看方式

百度搜索:蓝易云

云计算 Linux 运维 后端 云服务器

文心一言 VS 讯飞星火 VS chatgpt (195)-- 算法导论14.3 3题

福大大架构师每日一题

福大大架构师每日一题

削峰有高招!评价QPS降低85%的背后逻辑-京东零售技术实践

京东零售技术

前端 移动开发 大促 黄金流程

华为视频走近《鲁豫有约一日行》,看技术如何赋能艺术创作

最新动态

网络安全训练营第 0 期 -- 毕业总结

楷鹏:)

Vector Magic for mac(矢量图片转换工具) 1.2.0免激活版

iMac小白

Real Glow for mac( AE真实发光效果模拟插件) v1.0.0汉化版

iMac小白

如何在春节实现弯道超车,你知道吗?

测吧(北京)科技有限公司

测试

对话苏光牛:国内数据库市场已进入关键转折点,2024年或是分水岭

华为云开发者联盟

数据库 后端 华为云 华为云GaussDB 华为云开发者联盟

DR5插件加强版 for Mac(ps磨皮滤镜) v5.0中文版

iMac小白

聚道云软件连接器助力餐饮企业实现数字化管理

聚道云软件连接器

案例分享

耗时一个月我问遍了身边的大佬,零基础自学Java的路线,适用程序员入门&进阶,Java学习路线,2024新版

小明Java问道之路

快速上手极狐GitLab设计管理功能

极狐GitLab

代码验证斯特林公式的准确性

fliter

什么是HTTP长轮询?

百度搜索:蓝易云

Linux 运维 前端 HTTP 云服务器

VMware Fusion Pro 13 for Mac(VM虚拟机)中文破解版

iMac小白

Flicker Free for mac(专业AE视频去闪烁插件)兼容Big Sur v1.1.8激活版

iMac小白

Mac苹果电脑照片管理必备软件:Lightroom Classic 2022 for Mac最新激活版(LrC中文破解)

iMac小白

mac电池最大充电限制工具AlDente Pro for Mac v1.22.2激活版下载

iMac小白

Real Glow for mac( AE真实发光效果模拟插件) v1.0.0汉化版

iMac小白

安全SCDN可以应对攻击吗

德迅云安全杨德俊

基于BiLSTM-CRF模型的分词、词性标注、信息抽取任务的详解,侧重模型推导细化以及LAC分词实践

汀丶人工智能

自然语言处理 nlp 信息抽取 词性标注 分词算法

PS滤镜插件套装 Nik Collection 6 for Mac v6.1.0中文激活版下载

iMac小白

@所有人 您需要的 幻兽帕鲁服务器搭建教程 已上线

六月的雨在InfoQ

阿里云 ECS 计算巢 幻兽帕鲁

LoRA:语言模型微调的计算资源优化策略

Baihai IDP

人工智能 深度学习 程序员 AI LLM

行业知识图谱是什么

悦数图数据库

知识图谱

行业首家!TCL实业通过国家大模型算法备案

Geek_2d6073

业务架构师之路

Geek_28a0f5

架构师 业务 业务架构师

Shell错误:/bin/bash^M: bad interpreter: No such file or directory

百度搜索:蓝易云

云计算 Linux 运维 windows 云服务器

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