编写高质量代码:改善Java程序的151个建议(9)

  建议28:优先使用整型池

  上一建议我们解释了包装对象的比较问题,本建议将继续深入讨论相关问题,首先看如下代码:

  

    publicstaticvoidmain(String[]args){ Scannerinput=newScanner(System.in); while(input.hasNextInt()){ intii=input.nextInt(); System.out.println(“\n====”+ii+”的相等判断======”); //两个通过new产生的Integer对象 Integeri=newInteger(ii); Integerj=newInteger(ii); System.out.println(“new产生的对象:”+(i==j)); //基本类型转为包装类型后比较 i=ii; j=ii; System.out.println(“基本类型转换的对象:”+(i==j)); //通过静态方法生成一个实例 i=Integer.valueOf(ii); j=Integer.valueOf(ii); System.out.println(“valueOf产生的对象:”+(i==j)); } }

  输入多个数字,然后按照3种不同的方式产生Integer对象,判断其是否相等,注意这里使用了“==”,这说明判断的不是同一个对象。我们输入三个数字127、128、555,结果如下:

  

    ====127的相等判断====== new产生的对象:false 基本类型转换的对象:true valueOf产生的对象:true ====128的相等判断====== new产生的对象:false 基本类型转换的对象:false valueOf产生的对象:false ====555的相等判断====== new产生的对象:false 基本类型转换的对象:false valueOf产生的对象:false

  很不可思议呀,数字127的比较结果竟然与其他两个数字不同,它的装箱动作所产生的对象竟然是同一个对象,valueOf产生的也是同一个对象,但是大于127的数字128和555在比较过程中所产生的却不是同一个对象,这是为什么?我们一个一个来解释。

  (1)new产生的Integer对象

  new声明的就是要生成一个新的对象,没二话,这是两个对象,地址肯定不等,比较结果为false。

  (2)装箱生成的对象

  对于这一点,首先要说明的是装箱动作是通过valueOf方法实现的,也就是说后两个算法是相同的,那结果肯定也是一样的,现在的问题是:valueOf是如何生成对象的呢?我们来阅读一下Integer.valueOf的实现代码:

  

    publicstaticIntegervalueOf(inti){ finalintoffset=128; if(i>=-128&&i<=127){//mustcache returnIntegerCache.cache[i+offset]; } returnnewInteger(i); }

  这段代码的意思已经很明了了,如果是-128到127之间的int类型转换为Integer对象,则直接从cache数组中获得,那cache数组里是什么东西,代码如下:

  

    staticfinalIntegercache[]=newInteger[-(-128)+127+1]; static{ for(inti=0;i<cache.length;i++) cache[i]=newInteger(i-128); }

  cache是IntegerCache内部类的一个静态数组,容纳的是﹣128到127之间的Integer对象。通过valueOf产生包装对象时,如果int参数在﹣128和127之间,则直接从整型池中获得对象,不在该范围的int类型则通过new生成包装对象。

  明白了这一点,要理解上面的输出结果就迎刃而解了,127的包装对象是直接从整型池中获得的,不管你输入多少次127这个数字,获得的对象都是同一个,那地址当然都是相等的。而128、555超出了整型池范围,是通过new产生一个新的对象,地址不同,当然也就不相等了。

  以上的解释也是整型池的原理,整型池的存在不仅仅提高了系统性能,同时也节约了内存空间,这也是我们使用整型池的原因,也就是在声明包装对象的时候使用valueOf生成,而不是通过构造函数来生成的原因。顺便提醒大家,在判断对象是否相等的时候,最好是用equals方法,避免用“==”产生非预期结果。

  注意:通过包装类的valueOf生成包装实例可以显著提高空间和时间性能。

  建议29:优先选择基本类型

  包装类型是一个类,它提供了诸如构造方法、类型转换、比较等非常实用的功能,而且在Java 5之后又实现了与基本类型之间的自动转换,这使包装类型如虎添翼,更是应用广泛了,在开发中包装类型已经随处可见,但无论是从安全性、性能方面来说,还是从稳定性方面来说,基本类型都是首选方案。我们来看一段代码:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ Clientcilent=newClient(); inti=140; //分别传递int类型和Integer类型 cilent.f(i); cilent.f(Integer.valueOf(i)); } publicvoidf(longa){ System.out.println(“基本类型的方法被调用”); } publicvoidf(Longa){ System.out.println(“包装类型的方法被调用”); } }

  在上面的程序中首先声明了一个int变量i,然后加宽转变成long型,再调用f()方法,分别传递int和long的基本类型和包装类型,诸位想想该程序是否能够编译?如果能编译输出结果又是什么呢?

  首先,这段程序绝对是能够编译的。不过,说不能编译的同学还是很动了一番脑筋的,只是还欠缺点火候,你可能会猜测以下这些地方不能编译:

  f()方法重载有问题。定义的两个f()方法实现了重载,一个形参是基本类型,一个形参是包装类型,这类重载很正常。虽然基本类型和包装类型有自动装箱、自动拆箱的功能,但并不影响它们的重载,自动拆箱(装箱)只有在赋值时才会发生,和重载没有关系。

  cilent.f(i)报错。i是int类型,传递到fun(long l)是没有任何问题的,编译器会自动把i的类型加宽,并将其转变为long型,这是基本类型的转换规则,也没有任何问题。

  cilent.f(Integer.valueOf(i)) 报错。代码中没有f(Integer i)方法,不可能接收一个Integer类型的参数,而且Integer和Long两个包装类型是兄弟关系,不是继承关系,那就是说肯定编译失败了?不,编译是成功的,稍后再解释为什么这里编译成功。

  既然编译通过了,我们来看一下输出:

  

    基本类型的方法被调用 基本类型的方法被调用

  cilent.f(i)的输出是正常的,我们已经解释过了。那第二个输出就让人很困惑了,为什么会调用f(long a)方法呢?这是因为自动装箱有一个重要的原则:基本类型可以先加宽,再转变成宽类型的包装类型,但不能直接转变成宽类型的包装类型。这句话比较拗口,简单地说就是, int可以加宽转变成long,然后再转变成Long对象,但不能直接转变成包装类型,注意这里指的都是自动转换,不是通过构造函数生成。为了解释这个原则,我们再来看一个例子:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ inti=100; f(i); } publicstaticvoidf(Longl){ } }

  这段程序编译是通不过的,因为i是一个int类型,不能自动转变为Long型。但是修改成以下代码就可以编译通过了:

  

    publicstaticvoidmain(String[]args){ inti=100; longl=(long)i; f(l); }

  这就是int先加宽转变为long型,然后自动转换成Long型。规则说明白了,我们继续来看f(Integer.valueOf(i))是如何调用的,Integer.valueOf(i)返回的是一个Integer对象,这没错,但是Integer和int是可以互相转换的。没有f(Integer i)方法?没关系,编译器会尝试转换成int类型的实参调用,OK,这次成功了,与f(i)相同了,于是乎被加宽转变成long型—结果也很明显了。整个f(Integer.valueOf(i))的执行过程是这样的:

  i通过valueOf方法包装成一个Integer对象。

  由于没有f(Integer i)方法,编译器“聪明”地把Integer对象转换成int。

  int自动拓宽为long,编译结束。

  使用包装类型确实有方便的地方,但是也会引起一些不必要的困惑,比如我们这个例子,如果f()的两个重载方法使用的是基本类型,而且实参也是基本类型,就不会产生以上问题,而且程序的可读性更强。自动装箱(拆箱)虽然很方便,但引起的问题也非常严重—我们甚至都不知道执行的是哪个方法。

  注意:重申,基本类型优先考虑。

  建议30:不要随便设置随机种子

  随机数在太多的地方使用了,比如加密、混淆数据等,我们使用随机数是期望获得一个唯一的、不可仿造的数字,以避免产生相同的业务数据造成混乱。在Java项目中通常是通过Math.random方法和Random类来获得随机数的,我们来看一段代码:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ Randomr=newRandom(); for(inti=1;i<4;i++){ System.out.println(“第”+i+”次:”+r.nextInt()); } } }

  代码很简单,我们一般都是这样获得随机数的,运行此程序可知:三次打印的随机数都不相同,即使多次运行结果也不同,这也正是我们想要随机数的原因。我们再来看下面的程序:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ Randomr=newRandom(1000); for(inti=1;i<4;i++){ System.out.println(“第”+i+”次:”+r.nextInt()); } } }

  上面使用了Random的有参构造,运行结果如下:

  

    第1次:-498702880 第2次:-858606152 第3次:1942818232

  计算机不同输出的随机数也不同,但是有一点是相同的:在同一台机器上,甭管运行多少次,所打印的随机数都是相同的,也就是说第一次运行,会打印出这三个随机数,第二次运行还是打印出这三个随机数,只要是在同一台硬件机器上,就永远都会打印出相同的随机数,似乎随机数不随机了,问题何在?

  那是因为产生随机数的种子被固定了,在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个规则:

  种子不同,产生不同的随机数。

  种子相同,即使实例不同也产生相同的随机数。

  看完上面两个规则,我们再来看这个例子,会发现问题就出在有参构造上,Random类的默认种子(无参构造)是System.nanoTime()的返回值(JDK 1.5版本以前默认种子是System. currentTimeMillis()的返回值),注意这个值是距离某一个固定时间点的纳秒数,不同的操作系统和硬件有不同的固定时间点,也就是说不同的操作系统其纳秒值是不同的,而同一个操作系统纳秒值也会不同,随机数自然也就不同了。(顺便说下,System.nanoTime不能用于计算日期,那是因为“固定”的时间点是不确定的,纳秒值甚至可能是负值,这点与System. currentTimeMillis不同。)

  new Random(1000)显式地设置了随机种子为1000,运行多次,虽然实例不同,但都会获得相同的三个随机数。所以,除非必要,否则不要设置随机种子。

  顺便提一下,在Java中有两种方法可以获得不同的随机数:通过java.util.Random类获得随机数的原理和Math.random方法相同,Math.random()方法也是通过生成一个Random类的实例,然后委托nextDouble()方法的,两者是殊途同归,没有差别。

  注意:若非必要,不要设置随机数种子。

  相关链接:

  编写高质量代码:改善Java程序的151个建议(1)

  编写高质量代码:改善Java程序的151个建议(2)

  编写高质量代码:改善Java程序的151个建议(3)

  编写高质量代码:改善Java程序的151个建议(4)

  编写高质量代码:改善Java程序的151个建议(5)

  编写高质量代码:改善Java程序的151个建议(6)

  编写高质量代码:改善Java程序的151个建议(7)

  编写高质量代码:改善Java程序的151个建议(8)

当一个人真正觉悟的一刻,他放弃追寻外在世界的财富,而开始追寻他内心世界的真正财富

编写高质量代码:改善Java程序的151个建议(9)

相关文章:

你感兴趣的文章:

标签云: