摘抄笔记--《编写高质量代码:改善Java程序的151个建议》
记得3年前刚到公司,同桌同事见我无事可做就借我看《编写高质量代码:改善Java程序的151个建议》这本书,当时看了几页没上心就没研究了。到上个月在公司偶然看到,于是乎又找来看看,我的天,真是非常多的干货,对于我这种静不下心的人真是帮助莫大呀。
看完整本书,也记了不少笔记,我就分享一部分个人觉得有意义的内容,也为了方便以后自己温习。
--警惕自增陷阱
i++表示先赋值后自增,而++i表示先自增后赋值。下面的代码返回结果为0,因为lastAdd++有返回值,而返回值是自增前的值(在自增前变量的原始值会存在一个内存空间,当自增结束后就会将内存空间的值返回)。
public static void main(String[] args) { int lastAdd = 0; for(int i=0;i<10;i++){ lastAdd = lastAdd++; } System.out.println(lastAdd); }
--显式申明Serializable UUID
类实现Serializable是为了可持久化,有了它就能为系统的分布式和异构部署提供先决条件。
每一个实现了Serializable的类都可以定义一个静态UID,来标识当前类的序列。
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型的变量是被包括进去的
class A implements Serializable {
private String name;
transient private String address;
}
那么你在序列化(IO流等)A类时,给它的name和address属性赋值,但是你在反序列化A时,拿到了name属性,但是却拿不到address属性。
--易变业务使用脚本语言编写
脚本语言三大特征:灵活,脚本语言的对象类型可动态变化;便捷,脚本语言属于解释性语言,编写后不需要编译,直接通过解析器即可执行;简单,脚本语言简单易学。
Java支持第三方脚本语言,即可以通过java来调用指定的脚本。所以当某些业务公式动态变化时,通过脚本语言来解析非常有效。
--慎用Java动态编译
Java6已经支持了动态编译java代码,即生成一段java的字符串或java文件,然后用java api动态编译执行这段java字符串代码。
这样做有一些风险:框架慎用(struts、spring等框架),动态创建的对象很难受框架控制;不要在高性能的地方使用,动态编译解析会耗费很多时间;考虑安全问题,允许用户自己编译java代码以后,很容易受到危险代码的攻击注入。
如果要做动态代码,一定要记录编写、编译、执行过程等所有工作日志,便于追踪管理。另外动态还是希望尽量将工作交给脚本语言完成吧。
--奇偶数用偶判断而不用奇判断
以下代码写法是正确的:
i%2==0?”偶数”:”奇数”
以下代码的写法是错误的:
i%2==1?”奇数”:”偶数”
原因是java取余方式是这样的:
public static int remainder(int dividend,int divisor){
return dividend – dividend/divisor*divisor;
}
--小数运算想办法先转换为整数进行运算
java小数位运算是会丢失精度的,比如(10.00-9.60)返回值不是0.4而是0.40000000000000036,这是由浮点数存储小数丢失精度造成的。
解决丢失精度的方法有两种:第一种用BigDecimal来做精度运算;第二种是将小数先转换为整数,运算结束后再转为小数。
public static void main(String[] args) { double i = 10.00D; double j = 9.60D; System.out.println(i-j); //结果:0.40000000000000036 System.out.println((i*100-j*100)/100); //结果:0.4 }
--不要让类型默默转换
看下面的代码,long j计算结果是负数,明显数据越界了,原因是在执行i*1000时还是以int整数类型进行运算的,所以超出int范围。
而long k = 1L*i*1000;在运算前显式申明了1L表示这是一个long字段,后面的数据就会以long进行运算,所以结果就会正确。
public static void main(String[] args) { int i = 3*1000*1000; System.out.println(i);//3000000 long j = i*1000; System.out.println(j);//-1294967296 long k = 1L*i*1000; System.out.println(k);//3000000000 }
--注意数字边界值测试
如下代码,(1000+1000)<=2000为true这是正确的,而(1000+2147483647)<=2000也是true就奇怪了!
原因也是类型转换问题,int的最大值是2147483647,如果再加一个值就会变成负数,所以“负数<=2000”就顺理成章了。
public static void main(String[] args) { int limit = 2000; int cur = 1000; int i = 1000; System.out.println((cur+i)<=limit);//true int j = 2147483647; System.out.println((cur+j)<=limit);//true }
注:这只是冰山一角,涉及到数字运算的都要考虑边界问题!
--不要让四舍五入亏了一方
传统的四舍五入规则是:尾数小于等于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的整数都缓存了,所以如果用户创建的是这个区间的数字则会直接从缓存空间中取值,极大地提升了数字创建性能。
public static Integer valueOf(int paramInt) { if ((paramInt >= -128) && (paramInt <= 127)) return IntegerCache.cache[(paramInt + 128)]; return new Integer(paramInt); } private static class IntegerCache { static final Integer[] cache = new Integer[256]; static { for (int i = 0; i < cache.length; ++i) cache[i] = new Integer(i - 128); } }
--接口不要存在任何实现代码
接口是一种契约,不要做任何代码实现。
如下代码是不好的编程习惯,接口是一种契约,一种规范,不应该在接口中提供可变的实现。
//不规范的接口 Interface A { public static final B b = new B(){ public void doSomeThing(){ System.out.print("这是接口"); } } } Interface B{ public void doSomeThing(); }
--避免在构造函数中实例化其它类
如下代码,如果这样写,会造成死循环实例化A、B对象,直到溢出。
public class A{ public A(){ new B(); } } public class B{ public B(){ new A(); } }
--构造代码块的功能
public class Demo { { System.out.println("这是代码块"); } public Demo(){ System.out.println("Demo();"); } public Demo(String demo){ System.out.println("Demo(String demo)"); } public static void main(String[] args) { new Demo(); new Demo(""); } }
以上代码的运行结果是:
这是代码块
Demo();
这是代码块
Demo(String demo)
代码块的作用是在提取所有构造函数的相同代码,让代码在构造函数执行前执行,而且不用担心构造函数内通过this()调用别的构造的函数,这样只会触发一次代码块。
--善用匿名类的代码块
接上面的例子,我们在实例化Demo对象时创建一个匿名类,并且定义一个代码块
public static void main(String[] args) { new Demo(){ {System.out.println("这也是代码块哦");} }; }
最后执行结果是这样的:
这是代码块
Demo();
这也是代码块哦
要注意的是new Demo()和new Demo(){}所生成的类是不一样的,通过getClass比较就会知道。
--让工具类不可实例化
java有很多utils工具类,开发人员也可以自己开发工具类,一般工具类下都是static类型的可直接访问的属性和方法,完全不需要实例化。
所以既然不能实例化,就做一个无法实例化的构造函数吧:
public class DateUtils {
private DateUtils(){
throw new RuntimeException("别实例化");
}
......
}
之所以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进行排序
List<String> words = new ArrayList<String>(Arrays.asList(new String[]{"赵","钱","孙","李"}));
Comparator paramComparator = Collator.getInstance(Locale.CHINA);
Collections.sort(words, paramComparator);
for (String string : words) {
System.out.println(string);
}
//返回结果 李 钱 孙 赵
--性能考虑,数组优先
使用基础数据类型的数组效率要于对象类型的List,因为前者直接从速度快的栈内存中取数据,而List还得装拆箱。在极限效率下,数组(基础数据类型的数组)非常好用。
--变长数组
java的数组是定长的,但是可以通过Arrays.copyOf扩充数组长度,其实List的自动扩充原理与之相似.
int[] i = {1,2,3,4,5};
System.out.println(i.length); //返回5
i = Arrays.copyOf(i, 20);
System.out.println(i.length); //返回20
值得注意的是copyOf是浅拷贝,所以不要指望通过这种方式copy一个全新数组出来。
--List初始化时最好定义一个长度
比如new ArrayList(100);因为如果不赋值,默认是10的长度,假设一个List第一次会有上百条数据添加,则它会调用多次数组扩容代码,明显浪费时间。
--通过Arrays.asList转换的集合无法增删数据
因为Arrays.asList创建的ArrayList是Arrays的内部类,而不是util.ArrayList,而该内部类没有重写add和remove方法。
Integer i = {1,2,3,4,5}; List list = Arrays.asList(i); 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哪种速度更快。
//arrayList 100000条 //linkedList 100000条 Integer ii = 0; begin = System.currentTimeMillis(); for (Integer integer : arrayList) { ii = ii+integer; } end = System.currentTimeMillis(); System.out.println("foreach arrayList:"+(end-begin)); //8 ii = 0; begin = System.currentTimeMillis(); for (Integer integer : linkedList) { ii = ii+integer; } end = System.currentTimeMillis(); System.out.println("foreach linkedList:"+(end-begin));//5 ii = 0; begin = System.currentTimeMillis(); for (int j=0;j<arrayList.size();j++) { ii = ii+arrayList.get(j); } end = System.currentTimeMillis(); System.out.println("for arrayList:"+(end-begin));//4 ii = 0; begin = System.currentTimeMillis(); for (int k=0;k<linkedList.size();k++) { ii = ii+linkedList.get(k); } end = System.currentTimeMillis(); System.out.println("for linkedList:"+(end-begin));//8725
从上面的结果来看,用foreach的时候linkedList确实要比arrayList快一点,但不明显,除非集合数量级上百万;而用for循环就要千万慎重了,linkedList.get(i)效率非常非常差!
综上所述,在遍历列表时,继承了RandomAccess的集合用普通for循环,其它用foreach循环。如果只能选择一种循环,那就选择foreach!
public static void forList(List<Integer> list){ int ii = 0; if(list instanceof RandomAccess){ for(int i=0;i<list.size();i++){ ii = ii + list.get(i); } }else{ for (Integer integer : list) { ii = ii + integer; } } }
--indexOf适用于小数据集合查找,Collections.binarySearch二分法适用于大数据集合(前提是集合进行过排序)
--List的并集、交集、差集
并集list1.addAll(list2);
无重复并集:list2.removeAll(list1);list1.addAll(list2);
交集list1.retainAll(list2);//此时list1包含了交集数据,其它数据会从list1中删除
差集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块中定义返回值
try{ if(x = 1){ return throw new RuntimeException(); }else{ return 1; } }catch(Exception e){ return 2; }finally{ return 3; }
如上代码,虽然有很多返回值,但如果真的执行,该方法的返回值永远是3,因为finally是最终执行代码,将其它返回值全部覆盖了!
--异常也慢,但还是支持多用异常
活用异常能让代码更面向对象,如下代码异常从userManager抛出,而不是通过返回值告诉你登录失败,这样的设计可读性更佳!
try{ userManager.login(account,passowrd) }catch(AccountException e){ //账号出错 }catch(PasswordException e){ //密码出错 }
创建IOException要比创建String慢5倍,因为它会记录StackTrace信息,但只是相对慢而已,如果不是极限性能代码,支持多用异常,这样能保证代码的健壮性。
--覆写Exception的fillInStackTrace方法
原生的fillInStackTrace方法会记录所有异常栈信息,如果不想记录异常栈就可以使用自定义异常并且覆写fillInStackTrace方法即可减小性能开支,据说覆写该方法能提高10倍性能。
public class MyException extends Exception { @Override public synchronized Throwable fillInStackTrace() { return this; } }
--线程优先级推荐只使用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/
推荐阅读
-
C#程序编写高质量代码改善的157个建议【10-12】[创建对象时需要考虑是否实现比较器、区别对待==和Equals]
-
C#程序编写高质量代码改善的157个建议【4-9】[TryParse比Parse、使用int?来确保值类型也可以为null、readonly和const、0值设为枚举的默认值、避免给枚举类型的元素提供显式的值、习惯重载运算符]
-
C#程序编写高质量代码改善的157个建议【13-15】[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]
-
C#程序编写高质量代码改善的157个建议【20-22】[泛型集合、选择集合、集合的安全]
-
C#程序编写高质量代码改善的157个建议【16-19】[动态数组、循环遍历、对象集合初始化]
-
编写高质量代码:改善Java程序的151个建议(第5章:数组和集合___建议60~66)
-
编写高质量代码:改善Java程序的151个建议(第4章:字符串___建议52~59)
-
编写高质量代码 改善Python程序的91个建议
-
摘抄笔记--《编写高质量代码:改善Java程序的151个建议》
-
摘抄笔记--《编写高质量代码:改善Java程序的151个建议》