欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

摘抄笔记--《编写高质量代码:改善Java程序的151个建议》

程序员文章站 2022-06-08 11:22:46
...

        记得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/