[ 转 ]编写高质量代码:改善Java程序的151个建议

        记得3年前刚到公司,同桌同事见我无事可做就借我看《编写高质量代码:改善Java程序的151个建议》这本书,当时看了几页没上心就没研究了。到上个月在公司偶然看到,于是乎又找来看看,我的天,真是非常多的干货,对于我这种静不下心的人真是帮助莫大呀。

        看完整本书,也记了不少笔记,我就分享一部分个人觉得有意义的内容,也为了方便以后自己温习。

--警惕自增陷阱

i++表示先赋值后自增,而++i表示先自增后赋值。下面的代码返回结果为0,因为lastAdd++有返回值,而返回值是自增前的值(在自增前变量的原始值会存在一个内存空间,当自增结束后就会将内存空间的值返回)。

Java代码
 
 
   
   
  1. public static void main(String[] args) 
  2. { 
  3.     int lastAdd = 0; 
  4.     for(int i=0;i<10;i++)
  5.     { 
  6.         lastAdd = lastAdd++; 
  7.     } 
  8.     System.out.println(lastAdd); 
  9. }

--显式申明Serializable UUID

类实现Serializable是为了可持久化,有了它就能为系统的分布式和异构部署提供先决条件。

每一个实现了Serializable的类都可以定义一个静态UID,来标识当前类的序列。

Java代码
  
  
  1. private static final long serialVersionUID = -3898355999058501880L;
这是有作用的:假设项目A有一个User JavaBean实现了Serializable但没有显示定义serialVersionUID,而项目B引入项目A的User类,再通过WebService获取到User对象,这样的实现是可行的。但假设项目A的User类新增了一个属性,而项目B没有及时更新(异构部署经常遇到这样的情况),那么项目B通过Web service获取User可能就会报InvalidClassException。原因是如果未显示申明Serializable的serialVersionUID属性,则JVM会根据包名、类名和类里的元素等因子生成一个唯一的UID,因为项目A的User属性有变化,所以项目A的User与项目B的User的serialVersionUID不一样,序列化工作就被JVM拒绝了。

 

解决方法就是编写一个固定的serialVersionUID,只要两边的值相同,则JVM就会进行正常的序列化、反序列化,只是反序列化的时候某些属性不存在而已。

--transient关键字 

Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的

  
  
  1. class A implements Serializable 
  2. {
  3.      private String name;
  4.      transient private String address;
  5. }

那么你在序列化(IO流等)A类时,给它的name和address属性赋值,但是你在反序列化A时,拿到了name属性,但是却拿不到address属性。

--易变业务使用脚本语言编写

脚本语言三大特征:灵活,脚本语言的对象类型可动态变化;便捷,脚本语言属于解释性语言,编写后不需要编译,直接通过解析器即可执行;简单,脚本语言简单易学。

Java支持第三方脚本语言,即可以通过java来调用指定的脚本。所以当某些业务公式动态变化时,通过脚本语言来解析非常有效。

--慎用Java动态编译

Java6已经支持了动态编译java代码,即生成一段java的字符串或java文件,然后用java api动态编译执行这段java字符串代码。

这样做有一些风险:框架慎用(struts、spring等框架),动态创建的对象很难受框架控制;不要在高性能的地方使用,动态编译解析会耗费很多时间;考虑安全问题,允许用户自己编译java代码以后,很容易受到危险代码的攻击注入。

如果要做动态代码,一定要记录编写、编译、执行过程等所有工作日志,便于追踪管理。另外动态还是希望尽量将工作交给脚本语言完成吧。

--奇偶数用偶判断而不用奇判断

以下代码写法是正确的:

  
  
  1. i%2==0?”偶数”:”奇数”

以下代码的写法是错误的:

  
  
  1. i%2==1?”奇数”:”偶数”

 

原因是java取余方式是这样的:

  
  
  1. public static int remainder(int dividend,int divisor)
  2. {  
  3.     return dividend dividend/divisor*divisor;
  4. }

--小数运算想办法先转换为整数进行运算

java小数位运算是会丢失精度的,比如(10.00-9.60)返回值不是0.4而是0.40000000000000036,这是由浮点数存储小数丢失精度造成的。

解决丢失精度的方法有两种:第一种用BigDecimal来做精度运算;第二种是将小数先转换为整数,运算结束后再转为小数。

 

Java代码 
 
 
   
   
  1. public static void main(String[] args) 
  2. { 
  3.     double i = 10.00D; 
  4.     double j = 9.60D; 
  5.     System.out.println(i-j); //结果:0.40000000000000036
  6.     System.out.println((i*100-j*100)/100); //结果:0.4 
  7. }
--不要让类型默默转换

看下面的代码,long j计算结果是负数,明显数据越界了,原因是在执行i*1000时还是以int整数类型进行运算的,所以超出int范围。

而long k = 1L*i*1000;在运算前显式申明了1L表示这是一个long字段,后面的数据就会以long进行运算,所以结果就会正确。

 

Java代码 
 
 
   
   
  1. public static void main(String[] args) 
  2. { 
  3.     int i = 3*1000*1000; 
  4.     System.out.println(i);//3000000 
  5.     long j = i*1000; 
  6.     System.out.println(j);//-1294967296 
  7.     long k = 1L*i*1000; 
  8.     System.out.println(k);//3000000000 
  9. }

--注意数字边界值测试

如下代码,(1000+1000)<=2000为true这是正确的,而(1000+2147483647)<=2000也是true就奇怪了!

原因也是类型转换问题,int的最大值是2147483647,如果再加一个值就会变成负数,所以“负数<=2000”就顺理成章了。


Java代码 
 
 
   
   
  1. public static void main(String[] args) 
  2. { 
  3.     int limit = 2000; 
  4.     int cur = 1000; 
  5.     int i = 1000; 
  6.     System.out.println((cur+i)<=limit);//true 
  7.     int j = 2147483647; 
  8.     System.out.println((cur+j)<=limit);//true 
  9. }

 注:这只是冰山一角,涉及到数字运算的都要考虑边界问题!

--不要让四舍五入亏了一方

传统的四舍五入规则是:尾数小于等于4则舍去尾数,尾数大于等于5则进一位。这是一个很经典的理论,但是对于银行来说如果用传统的四舍五入会造成亏本。

从银行家的理论来说:

四舍:如果小数位尾数为0~4的0.0000、0.0001、0.0002、0.0003、0.0004都是舍去变成0.0000,也就是这小数位的钱不给用户,银行赚这一笔钱。

五入:如果小数位尾数为5~9的0.0005、0.0006、0.0007、0.0008、0.0009都是进一位成0.0010,也就是银行会额外补贴钱给用户,银行会亏钱。

理论上0~9的数字是均匀分布的,所以对银行来说每10笔钱通过四舍五入银行赚取的利息盈利是:

0.0000+0.0001+0.0002+0.0003+0.0004-0.0005-0.0004-0.0003-0.0002-0.0001 = -0.0005

假设一个银行有5000万资金存储,上面的亏损算法会发现银行每年很可能白白送出10万的利息给用户。

显然从公平角度来说,银行亏了,银行为了四舍五入额外支出了一大笔钱出去(银行本应赚更多的)。

随后银行家提出了“银行家舍入法”:当舍去位的数值小于5时,直接舍去该位;当舍去位的数值大于等于6时,在舍去该位的同时向前位进一;当舍去位的数值等于5时,如果前位数值为奇,则在舍去该位的同时向前位进一,如果前位数值为偶,则直接舍去该位。

所以如果程序涉及公平交易原则应该采用“银行家舍入法”,java可以采用BigDecimal计算数值,再通过RoundingMode.ROUND_HALF_EVEN进行舍入即可。

--优先使用整型池

创建Integer对象尽量使用Integer.valueOf()方法,查看其实现代码可以发现IntegerCache将-128~127的整数都缓存了,所以如果用户创建的是这个区间的数字则会直接从缓存空间中取值,极大地提升了数字创建性能。

 

Java代码
 
 
   
   
  1. public static Integer valueOf(int paramInt) 
  2. { 
  3.     if ((paramInt >= -128) && (paramInt <= 127)) 
  4.         return IntegerCache.cache[(paramInt + 128)]; 
  5.     return new Integer(paramInt); 
  6. } 

  7. private static class IntegerCache 
  8. { 
  9.     static final Integer[] cache = new Integer[256]; 
  10.     static 
  11.     { 
  12.         for (int i = 0; i < cache.length; ++i) 
  13.         cache[i] = new Integer(i - 128); 
  14.     } 
  15. }
--接口不要存在任何实现代码

接口是一种契约,不要做任何代码实现。

如下代码是不好的编程习惯,接口是一种契约,一种规范,不应该在接口中提供可变的实现。

Java代码
 
 
   
   
  1. //不规范的接口
  2. Interface A
  3. {
  4.     public static final B b = new B()
  5.     {
  6.         public void doSomeThing()
  7.         {
  8.             System.out.print("这是接口");
  9.         }
  10.     }
  11. }
  12. Interface B
  13. {
  14.     public void doSomeThing();
  15. }
--避免在构造函数中实例化其它类

如下代码,如果这样写,会造成死循环实例化A、B对象,直到溢出。

 

Java代码
 
 
   
   
  1. public class A
  2. {
  3.     public A()
  4.     {
  5.         new B();
  6.     }
  7. }
  8. public class B
  9. {
  10.     public B()
  11.     {
  12.         new A();
  13.     }
  14. }
--构造代码块的功能
Java代码 
 
 
   
   
  1. public class Demo
  2. {
  3.     {
  4.         System.out.println("这是代码块");
  5.     }
  6.     public Demo()
  7.     {
  8.         System.out.println("Demo();");
  9.     }
  10.     public Demo(String demo)
  11.     {
  12.         System.out.println("Demo(String demo)");
  13.     }
  14.     public static void main(String[] args)
  15.     {
  16.         new Demo();
  17.         new Demo("");
  18.     }
  19. }

 

以上代码的运行结果是:

这是代码块

Demo();

这是代码块

Demo(String demo)

代码块的作用是在提取所有构造函数的相同代码,让代码在构造函数执行前执行,而且不用担心构造函数内通过this()调用别的构造的函数,这样只会触发一次代码块。

--善用匿名类的代码块

接上面的例子,我们在实例化Demo对象时创建一个匿名类,并且定义一个代码块

 

Java代码 
 
 
   
   
  1. public static void main(String[] args)
  2. {
  3.     new Demo()
  4.     {
  5.         {
  6.             System.out.println("这也是代码块哦");
  7.         }
  8.     };
  9. }

 最后执行结果是这样的:

这是代码块

Demo();

这也是代码块哦

要注意的是new Demo()和new Demo(){}所生成的类是不一样的,通过getClass比较就会知道。

--让工具类不可实例化

java有很多utils工具类,开发人员也可以自己开发工具类,一般工具类下都是static类型的可直接访问的属性和方法,完全不需要实例化。

所以既然不能实例化,就做一个无法实例化的构造函数吧:

  
  
  1. public class DateUtils 
  2. {
  3.     private DateUtils()
  4.     {
  5.         throw new RuntimeException("别实例化");
  6.     }
  7. ......
  8. }

之所以private构造函数中还要抛出异常,是因为开发人员可以通过java反射压制权限来实例化private的类,抛出异常则让别人完全玩不转了。

--另类的package-info.java文件

package-info.java是一个特殊的java文件,正常的Java文件命名是不能有“-”的,但是这个文件除外!

有兴趣的可以参考:http://blog.csdn.net/lazythinker/article/details/9180271

--字符串的replace和replaceAll的区别

两者都是替换全部满足条件的字符串,replace(param1,params)的第一个参数支持传入普通的字符串,而replaceAll(regex,params)的第一个参数要求传入正则表达式。

--合理运用String、StringBuffer和StringBuilder

String适用于不经常改变的字符串常量,不适合字符串拼接的代码,这样性能很低。String a = "我"+"爱"+"你";//不推荐的写法

StringBuffer适合需要经常变动或拼接的字符串,而且是线程安全的,如果你是多线程程序组装字符串则用这种写法非常好。

StringBuilder也适合需要经常变动或拼接的字符串,只是线程不安全,但运行效率比StringBuffer高很多,一般常见的拼接是用这个对象。

注:字符串拼接实现中,+加号最耗时,concat比+好点,StringBuilder的append最快。

--一个系统前后台包括文件使用统一编码,以免乱码

强烈建议使用UTF编码

--中文汉字排序很困难

要想用中文首字母排序很复杂,因为中国文字博大精深!一般的排序算法是根据文字的unicode码来确定的,所以基本都是错误的。

推荐使用Collator进行排序

  
  
  1. List<String> words = new ArrayList<String>(Arrays.asList(new String[]{"赵","钱","孙","李"}));
  2. Comparator paramComparator = Collator.getInstance(Locale.CHINA);
  3. Collections.sort(words, paramComparator);
  4. for (String string : words) 
  5. {
  6.     System.out.println(string);
  7. }

//返回结果 李 钱 孙 赵

--性能考虑,数组优先

使用基础数据类型的数组效率要于对象类型的List,因为前者直接从速度快的栈内存中取数据,而List还得装拆箱。在极限效率下,数组(基础数据类型的数组)非常好用。

--变长数组

java的数组是定长的,但是可以通过Arrays.copyOf扩充数组长度,其实List的自动扩充原理与之相似.

  
  
  1. int[] i = {1,2,3,4,5};
  2. System.out.println(i.length); //返回5
  3. i = Arrays.copyOf(i, 20);
  4. System.out.println(i.length); //返回20

值得注意的是copyOf是浅拷贝,所以不要指望通过这种方式copy一个全新数组出来。

--List初始化时最好定义一个长度

比如new ArrayList(100);因为如果不赋值,默认是10的长度,假设一个List第一次会有上百条数据添加,则它会调用多次数组扩容代码,明显浪费时间。

--通过Arrays.asList转换的集合无法增删数据

因为Arrays.asList创建的ArrayList是Arrays的内部类,而不是util.ArrayList,而该内部类没有重写add和remove方法。

 

Java代码   
 
 
   
   
  1. Integer i = {1,2,3,4,5}; List list = Arrays.asList(i); 
  2. list.add(6); //java.lang.UnsupportedOperationException

 如果需要一个变长的List可以这样写:List list = new ArrayList(Arrays.asList(i));

--ArrayList和LinkedList的最优迭代循环方式有区别

ArrayList继承了RandomAccess接口,这个接口没有任何特殊实现,仅仅表明这个对象属于随机存储,随机存储的数据不依赖自己两边的数据(即下标为1和下标为2的数据没有任何关联关系),继承了RandomAccess接口的List通过list.get(i)取值非常快,因为它不需要考虑自己两边的数据关系。

LinkedList属于链式结构,每一个节点都强关联了上下级关系,比如下标为1的数据不仅会存储自己的值,还是将自己上一节点(下标1)和下一节点(下标2)信息存储,这种实现非常适合于iterator式的取值方式。

下面是一个例子测试用for和foreach哪种速度更快。

 

Java代码 
 
 
   
   
  1. //arrayList 100000条 //linkedList 100000条
  2. Integer ii = 0;
  3. begin = System.currentTimeMillis();
  4. for (Integer integer : arrayList)
  5. {
  6.     ii = ii+integer;
  7. }
  8. end = System.currentTimeMillis();
  9. System.out.println("foreach arrayList:"+(end-begin)); //8 ii = 0;
  10. begin = System.currentTimeMillis();
  11. for (Integer integer : linkedList)
  12. {
  13.     ii = ii+integer;
  14. }
  15. end = System.currentTimeMillis();
  16. System.out.println("foreach linkedList:"+(end-begin));//5
  17. ii = 0;
  18. begin = System.currentTimeMillis();
  19. for (int j=0;j<arrayList.size();j++)
  20. {
  21.     ii = ii+arrayList.get(j);
  22. }
  23. end = System.currentTimeMillis();
  24. System.out.println("for arrayList:"+(end-begin));//4
  25. ii = 0;
  26. begin = System.currentTimeMillis();
  27. for (int k=0;k<linkedList.size();k++)
  28. {
  29.     ii = ii+linkedList.get(k);
  30. }
  31. end = System.currentTimeMillis();
  32. System.out.println("for linkedList:"+(end-begin));//8725

从上面的结果来看,用foreach的时候linkedList确实要比arrayList快一点,但不明显,除非集合数量级上百万;而用for循环就要千万慎重了,linkedList.get(i)效率非常非常差!

综上所述,在遍历列表时,继承了RandomAccess的集合用普通for循环,其它用foreach循环。如果只能选择一种循环,那就选择foreach!


Java代码
 
 
   
   
  1. public static void forList(List<Integer> list)
  2. {
  3.     int ii = 0;
  4.     if(list instanceof RandomAccess)
  5.     {
  6.         for(int i=0;i<list.size();i++)
  7.         {
  8.             ii = ii + list.get(i);
  9.         }
  10.     }
  11.     else
  12.     {
  13.         for (Integer integer : list)
  14.         {
  15.             ii = ii + integer;
  16.         }
  17.     }
  18. } 
--indexOf适用于小数据集合查找,Collections.binarySearch二分法适用于大数据集合(前提是集合进行过排序)
--List的并集、交集、差集

  
  
  1. 并集list1.addAll(list2);
  2. 无重复并集:list2.removeAll(list1);list1.addAll(list2);
  3. 交集list1.retainAll(list2);//此时list1包含了交集数据,其它数据会从list1中删除
  4. 差集list1.removeAll(list2);

--集合打乱排序方法只需一步

Collections.shuffle(list);调用这个方法即可将list序列打乱。该方法常用于:随机显示操作、抽奖、安全传输(先打乱顺序,再加密传输以迷惑对手)。

--大数据少用HashMap

HashMap的数据结构是由多个Entry<key,value>对象组成,而如果用List表示key、value则需要两个集合:List(key)和List(value)。HashMap多了很多Entry对象,占用更多内存。

同时HashMap也是动态扩容,至少以2倍阀值进行扩容,ArrayList则是1.5倍,虽然ArrayList扩容也会内存溢出,但是HashMap更容易在扩容时造成内存溢出。

--HashMap查找元素快的原因是因为它根据不同HashCode快速查找,如果Map Key对象重写了HashCode方法并返回相同值,那就谈不上快了。
--多线程考虑使用Vector和HashTable
--一个枚举对象下的数量限制在64以内

数量在64以内的枚举元素采用的是RegularEnumSet,大于64元素采用的是JumboEnumSet对象。RegularEnumSet通过一个long类型存储枚举,因为一个long有64位,所以每个枚举存储在每个位数上。而JumboEnumSet采用的是long数组存储,将元素分割成多个long。

--反射访问Field和Method时,将Accessible设置为true

setAccessible(true)不是说取消访问限制,而是是否进行安全检查。因为Java类下有不同访问权限,所以获取某个字段或方法时都会默认确认是否有调用权限,而setAccessible(true)恰恰是告诉JVM不做权限验证,直接访问字段或方法。

经过测试,在大量反射下将Accessible设置为true会提高性能20倍!所以当实例化对象、获取属性、调用方法时尽量设置不进行安全验证,这样能极大提高性能。

--使用forName动态加载类文件

Class.forName("xx.xx.xxClass")的作用是告知JVM动态加载该类(注意是加载而不是实例化),并且加载Class时会调用类中的static静态块代码,而static块中往往会做注册自己、初始化信息等操作。但是forName仅仅是初始化Class,并不会new类的实例。

--支持用反射

反射效率低是真,但并不是低到不能用,除非是及其要求性能的地方,这点性能基本不会影响整个项目,所以提高性能完全可以关注其他地方。

--不要在异常finally块中定义返回值
Java代码
 
 
   
   
  1. try
  2. {
  3.     if(x = 1)
  4.     {
  5.         return throw new RuntimeException();
  6.     }
  7.     else
  8.     {
  9.         return 1;
  10.     }
  11. }
  12. catch(Exception e)
  13. {
  14.     return 2;
  15. }
  16. finally
  17. {
  18.     return 3;
  19. }

 如上代码,虽然有很多返回值,但如果真的执行,该方法的返回值永远是3,因为finally是最终执行代码,将其它返回值全部覆盖了!

--异常也慢,但还是支持多用异常

活用异常能让代码更面向对象,如下代码异常从userManager抛出,而不是通过返回值告诉你登录失败,这样的设计可读性更佳!

Java代码 
 
 
   
   
  1. try
  2. {
  3.     userManager.login(account,passowrd)
  4. }
  5. catch(AccountException e)
  6. {
  7. //账号出错
  8. }
  9. catch(PasswordException e)
  10. {
  11. //密码出错
  12. }

 创建IOException要比创建String慢5倍,因为它会记录StackTrace信息,但只是相对慢而已,如果不是极限性能代码,支持多用异常,这样能保证代码的健壮性。

--覆写Exception的fillInStackTrace方法

原生的fillInStackTrace方法会记录所有异常栈信息,如果不想记录异常栈就可以使用自定义异常并且覆写fillInStackTrace方法即可减小性能开支,据说覆写该方法能提高10倍性能。

Java代码 
 
 
   
   
  1. public class MyException extends Exception
  2. {
  3.     @Override
  4.     public synchronized Throwable fillInStackTrace()
  5.     { 
  6.         return this;
  7.     }
  8. }
--线程优先级推荐只使用3个

Thread定义了三个线程级常量:Thread.MAX_PRIORITY=10;Thread.MIN_PRIORITY=1;Thread.NORM_PRIORITY=5;在开发多线程程序并且设置优先级时推荐使用者三个常量。

原因是级别高获得线程的几率大一些而已,而级别太近的线程体现不出优先级,而1,5,10级别差别较大,所以线程优先级会体现更明显。

--Clone对象效率不一定比new对象高

如果要复制一个普通且简单的Java对象,推荐使用New的形式重新创建,Clone并没有想象中的那么高。Clone适合于复杂的Java对象。

--推荐第三方优秀包

不要闭门造车,很多第三方工具类帮我们提供了解决方案,用一些靠谱的工具包比自己写工具代码靠谱。

Google-Guava:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等

Apache扩展包:著名的apache-common系列

Joda日期时间处理工具:http://www.joda.org/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值