写点什么

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

评论

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

VMware Aria Automation 8.18 发布,新增功能概览

sysin

vmware automation aria

主流电商平台售后API接口代码【详细】

快递鸟

API 接口

加强科技平台企业赋能 加快发展新质生产力

不在线第一只蜗牛

低代码 数字化

ClkLog企业版(CDP)预售开启,更有鸿蒙SDK前来助力

ClkLog

开源 sdk HarmonyOS 用户画像

青椒云云桌面针对中小企业上云的服务怎么样?

青椒云云电脑

云桌面 云电脑 云桌面厂商

阿里云上的Salesforce这一年:深耕本地化,释放新价值

Alter

阿里云 CRM Salesforce

Databend 产品月报(2024年10月)

Databend

一文彻底弄懂Java的IO操作

不在线第一只蜗牛

Java Python io

确保数据安全!使用Spring Boot 实现强大的API输入验证

江南一点雨

ETLCloud怎么样?深度解析其在数据管理中的表现

RestCloud

数据库 ETL 数据管理 数据集成

HarmonyOS 5.0应用开发——音频播放组件的封装

高心星

鸿蒙 HarmonyOS 鸿蒙5.0 HarmonyOS NEXT

运营TikTok需要什么网络环境

Ogcloud

云手机 海外云手机 tiktok云手机 tiktok运营 tiktok运营干货

ElevenLabs X-to-Voice:社交账号自动生成能说话的个人页面;OpenAI 正式推出 ChatGPT 搜索

声网

有Redis为什么还要本地缓存?谈谈你对本地缓存的理解?

王磊

VMware ESXi 8.0U3 xFusion (超聚变) 定制版更新 OEM BIOS 2.7 支持 Windows Server 2025

sysin

macos esxi OEM Xfusion

云电脑与远程控制软件的区别

青椒云云电脑

云桌面 云电脑 云桌面厂家

如何在鲲鹏平台上快速上手应用开发?鲲鹏DevKit给你答案

华为云开发者联盟

性能调优 鲲鹏 鲲鹏DevKit

VMware ESXi 8.0U3 Dell (戴尔) 定制版更新 OEM BIOS 2.7 支持 Windows Server 2025

sysin

macos esxi OEM unlocker dell

RUM最佳实践:内网IP地址映射地图地理位置场景

观测云

RUM

高并发编程/一张图精通CompletableFuture整体执行流程与设计(高手篇)

肖哥弹架构

Java 并发编程 高并发

企业选择云桌面系统的主要原因是什么?

青椒云云电脑

云桌面 云桌面系统

云桌面解决方案-青椒云

青椒云云电脑

云桌面 云桌面厂家 云桌面解决方案

游戏显卡VS专业显卡:两者有什么区别?

Finovy Cloud

显卡

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