BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Java编码易疏忽的十个问题

| 作者 傅健 关注 0 他的粉丝 发布于 2012年9月5日. 估计阅读时间: 15 分钟 | 都知道硅谷人工智能做的好,你知道 硅谷的运维技术 也值得参考吗?QCon上海带你探索其中的奥义

在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)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

就这么低级的文章也算深度内容? by ww ww

就这么低级的文章也算深度内容?

Re: 就这么低级的文章也算深度内容? by fu jian

深度仅仅是相对而言。若您水平较高,自然对你而言毫无深度,INFOQ更注重分享,又不是专门对文章进行评级的。

Re: 就这么低级的文章也算深度内容? by 胡 匪

文章标题是《易疏忽的十个问题》,只要有一条对别人有意义,那也不是低级的文章

不错 by 依 杰

我觉得不错啊,这些问题我平时编码的时候都在注意

Re: 就这么低级的文章也算深度内容? by ww ww

嗯,是我理解错了,向作者道歉

看了头两条我直接崩溃了 by Zheng Ken

以至于我3以后全忽略了。
这是闹哪样。。。第一个那不很正常吗,不知道在说什么
第二个你那么改也不对把,闰年怎么算?

Re: 看了头两条我直接崩溃了 by fu Jia

确实,呵呵,都没有考虑这个闰年的事情,谢谢。你估计看完发现更多的错误。呵呵

Re: 看了头两条我直接崩溃了 by fu Jia

第一条的意思是写完代码,要扫视下IMPORT部分,曾经犯过一个错:导入一个另外的同名类了。然后那个类方法还都差不多,就是内部行为不同,所以IDE没有报错。行文比较潦草还请多包涵。这个东西不误导大家就求神拜佛了。

Re: 就这么低级的文章也算深度内容? by fu jian

呵呵,客气了,假设很多人都看此文,仅希望能得益与一条,更希望大家指出不足,然后校正,以免影响人。呵呵,像下面一位仁兄说的闰年没有考虑等,哈

还行! by Yu Wu

大多时候,问题一时摸不到头,都是自己不小心造成的。

呵呵,写的不错 by wangzm zhimeng

写的不错了,很多人总是用挑剔的眼光视人的。
文章很多内容对多数人还真不见得都知道,尤其是单例的例子,很多其他编程实践也看得出作者用心了,重在分享,加油。

Re: 呵呵,写的不错 by fu jian

谢谢肯定,呵呵

第五条的解释是否准确呢? by Chen Eric

根据java的内存模型,如果不加volatile的话,在初始化时,instance = new CacheImpl ();对于构造函数的调用会通过内联(inline)来处理。进而使得getInstance()返回的对象中的属性仅仅是java的默认值,而不是构造函数中设置的值。这是因为对构造函数的写操作和对instance这个引用的写操作的reorder造成的。为了避免编译器的重新排序,所以需要加上volatile。
www.cs.umd.edu/~pugh/java/memoryModel/DoubleChe...

Re: 第五条的解释是否准确呢? by fu Jia

据我了解,这个问题有2个原因,我文章中阐述的是最常见的一个原因:即可见性问题,工作内存和主存不定一致;您说的是第二点:即重排序问题。因为volatile还有层含义是控制重排序,所以您说的也是一个方面,即有可能读出的是介于NULL和完成初始化之间的一个中间错误状态。这个可以参考 JAVA并发编程实践,里面有一个对DCL问题的阐述,呵呵,里面很详细。所以我觉得我们的介绍合在一起就全面了,当时没有写上去是直接用最常见的原因解释了,没有做详细解释。

好,值得看 by shi jie

发现有些人真心恶心,你自己会,那就说你会了,不要自己不写人家写了还那么多事,处事得体一些都那么大人了,任何能分享的人和事我都觉得是好事

Re: 第五条的解释是否准确呢? by hy jiacan

嗯,我写单实例的时候就一直没有考虑到这个问题,以后会注意了。

感谢分享 by Zhang Gavin

感谢并鼓励作者

非常不错的分享。 by Changchun Hu

非常不错的分享。谢谢!

Re: 就这么低级的文章也算深度内容? by 王 耀兴

这个对我们初级程序员很有用啊!对我们也算深度文章了!

这种文章也上infoq...崩溃, by 林 足雄

只适合新手...

挺好的 by wang zhu

以后注意了!

第8条 by 余 杰

最近喜欢实例1那种习惯,实例3偶尔也有用到。
实例2真心不喜欢,过段时间怕自己都忘记什么意思。。

Re: 第五条的解释是否准确呢? by Liu Nathan

如果使用synchronized(instance){} 就可以保证可见性了吧。而且在JDK5之后JMM已经解决了DCL问题

支持 by Lee Lucas

不错! 还是很好的! 支持作者、创造很多的好文章

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

24 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT