写点什么

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

评论

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

4月券商App行情刷新及交易体验评测报告,7家券商入围领导者象限

博睿数据

性能测试 系统运维 博睿数据 券商排行

半年面试数百场,我总结出了这份10w字Java面试复盘笔记

Java全栈架构师

Java spring 程序员 架构 面试

阿里云联合鼎捷软件发布云上数字工厂解决方案,实现云MES系统本地化部署

阿里云弹性计算

数字化转型 MES系统 中小企业 云盒

计算机网络概述

工程师日月

计算机网络 5月月更

源码解读预告 |TiFlash DeltaTree 引擎设计及实现解析!

TiDB 社区干货传送门

AIRIOT物联网低代码平台如何配置OPC UA驱动?

AIRIOT

青藤入选信通院“数据安全推进计划”成员单位

青藤云安全

数据安全 信通院

数据库连接池 -Druid 源码学习(十)

wjchenge

Druid 数据库连接池

OceanBase 源码解读(十一):Location Cache 模块浅析

OceanBase 数据库

oceanbase 源码解读

ECSM隐私协议

潇潇雨歇

springboot集成activiti整套方案()

金陵老街

Vue ERP Activiti spring-boot

普渡科技宣布成立“PUDU-X”创新基金,为青年工程师筑梦未来

Geek_2d6073

Authing 身份云招聘:增长黑客

Authing

招聘 科技

大家谈的视频体验指标,都有哪些?如何测定?

声网

视频 Qoe Dev for Dev

linux之awk使用技巧

入门小站

天翼云推荐新人返好礼,最高返利千元

天翼云开发者社区

Authing 被世界经济论坛评选为 2022 技术先锋企业

Authing

身份云 科技 Idaas 科技企业

dfs专项练习题

工程师日月

DFS 5月月更

All in ONE!博睿数据重磅推出一体化智能可观测平台

博睿数据

博睿数据 IT运维 ONE平台

【活动报名】TiDB 社区天津站 Meetup 要来啦!

TiDB 社区干货传送门

SysAK 应用抖动诊断篇—— eBPF又立功了! | 龙蜥技术

OpenAnolis小助手

Linux 工具 内核 ebpf 龙蜥技术

论道原生:走进可观测性

Daocloud 道客

云原生 可观测性 OpenTelemetry

宜搭5月更新:跨应用数据读写能力升级,AI组件内测开放

一只大光圈

5.26直播预告|《观见话题》第一期:跨境组网与加速上云的硬核解法

观测云

【高并发】什么是ForkJoin?看这一篇就够了!

冰河

并发编程 多线程 高并发 协程 异步编程

生命科学领域下的医药研发通过什么技术?冷冻电镜?分子模拟?IND?

GPU算力

极速调取客户保单,YRCloudFile 助力保险存储架构升级

焱融科技

AI 存储 NAS 数字金融

【LeetCode】数组中的第K个最大元素Java题解

Albert

LeetCode 5月月更

美团二面:为什么Redis会有哨兵?

Java全栈架构师

Java 数据库 redis 程序员 面试

明天,龙蜥2位专家直播,第22届计算机系统会议等活动来了!

OpenAnolis小助手

Linux 开源 直播 内核 龙蜥技术

Redis「9」主从、高可用性方案

Samson

redis 学习笔记 5月月更

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