摘抄笔记--《编写高质量代码:改善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/

你可能感兴趣的:(高质量代码)