zoukankan      html  css  js  c++  java
  • 编写高质量代码:改善Java程序的151个建议(第2章:基本类型___建议26~30)

    建议26:提防包装类型的null值

      我们知道Java引入包装类型(Wrapper Types)是为了解决基本类型的实例化问题,以便让一个基本类型也能参与到面向对象的编程世界中。而在Java5中泛型更是对基本类型说了"不",如果把一个整型放入List中,就必须使用Integer包装类型。我们看一段代码:

     1 import java.util.ArrayList;
     2 import java.util.List;
     3 
     4 public class Client26 {
     5 
     6     public static int testMethod(List<Integer> list) {
     7         int count = 0;
     8         for (int i : list) {
     9             count += i;
    10         }
    11         return count;
    12     }
    13 
    14     public static void main(String[] args) {
    15         List<Integer> list = new ArrayList<Integer>();
    16         list.add(1);
    17         list.add(2);
    18         list.add(null);
    19         System.out.println(testMethod(list));
    20     }
    21 }
      testMethod接收一个元素是整型的List参数,计算所有元素之和,这在统计和项目中很常见,然后编写一个测试testMethod,在main方法中把1、2和空值都放到List中,然后调用方法计算,现在思考一下会不会报错。应该不会吧,基本类型和包装类型都是可以通过自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)自由转换的,null应该可以转换为0吧,真的是这样吗?运行之后的结果是:  Exception in thread "main" java.lang.NullPointerException  运行失败,报空指针异常,我们稍稍思考一下很快就知道原因了:在程序for循环中,隐含了一个拆箱过程,在此过程中包装类型转换为了基本类型。我们知道拆箱过程是通过调用包装对象的intValue方法来实现的,由于包装类型为null,访问其intValue方法报空指针异常就在所难免了。问题清楚了,修改也很简单,加入null值检查即可,代码如下:  
    public static int testMethod(List<Integer> list) {
            int count = 0;
            for (Integer i : list) {
                count += (i != null) ? i : 0;
            }
            return count;
        }

      上面以Integer和int为例说明了拆箱问题,其它7个包装对象的拆箱过程也存在着同样的问题。包装对象和拆箱对象可以自由转换,这不假,但是要剔除null值,null值并不能转换为基本类型。对于此问题,我们谨记一点:包装类型参与运算时,要做null值校验。

    建议27:谨慎包装类型的大小比较

      基本类型是可以比较大小的,其所对应的包装类型都实现了Comparable接口,也说明了此问题,那我们来比较一下两个包装类型的大小,代码如下:

     1 public class Client27 {
     2     public static void main(String[] args) {
     3         Integer i = new Integer(100);
     4         Integer j = new Integer(100);
     5         compare(i, j);
     6     }
     7 
     8     public static void compare(Integer i, Integer j) {
     9         System.out.println(i == j);
    10         System.out.println(i > j);
    11         System.out.println(i < j);
    12 
    13     }
    14 }

      代码很简单,产生了两个Integer对象,然后比较两个的大小关系,既然包装类型和基本类型是可以自由转换的,那上面的代码是不是就可以打印出两个相等的值呢?让事实说话,运行结果如下:

      false  false  false

      竟然是3个false,也就是说两个值之间不相等,也没大小关系,这个也太奇怪了吧。不奇怪,我们来一 一解释:

    1. i==j:在java中"=="是用来判断两个操作数是否有相等关系的,如果是基本类型则判断值是否相等,如果是对象则判断是否是一个对象的两个引用,也就是地址是否相等,这里很明显是两个对象,两个地址不可能相等。
    2. i>j 和 i<j:在Java中,">" 和 "<" 用来判断两个数字类型的大小关系,注意只能是数字类型的判断,对于Integer包装类型,是根据其intValue()方法的返回值(也就是其相应的基本类型)进行比较的(其它包装类型是根据相应的value值比较的,如doubleValue,floatValue等),那很显然,两者不肯能有大小关系的。

    问题清楚了,修改总是比较容易的,直接使用Integer的实例compareTo方法即可,但是这类问题的产生更应该说是习惯问题,只要是两个对象之间的比较就应该采用相应的方法,而不是通过Java的默认机制来处理,除非你确定对此非常了解。

    建议28:优先使用整型池

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

     1 import java.util.Scanner;
     2 
     3 public class Client28 {
     4     public static void main(String[] args) {
     5         Scanner input = new Scanner(System.in);
     6         while (input.hasNextInt()) {
     7             int tempInt = input.nextInt();
     8             System.out.println("
    =====" + tempInt + " 的相等判断=====");
     9             // 两个通过new产生的对象
    10             Integer i = new Integer(tempInt);
    11             Integer j = new Integer(tempInt);
    12             System.out.println(" new 产生的对象:" + (i == j));
    13             // 基本类型转换为包装类型后比较
    14             i = tempInt;
    15             j = tempInt;
    16             System.out.println(" 基本类型转换的对象:" + (i == j));
    17             // 通过静态方法生成一个实例
    18             i = Integer.valueOf(tempInt);
    19             j = Integer.valueOf(tempInt);
    20             System.out.println(" valueOf产生的对象:" + (i == j));
    21         }
    22     }
    23 }

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

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

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

    (1)、new产生的Integer对象

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

    (2)、装箱生成的对象

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

     1  /**
     2      * Returns an {@code Integer} instance representing the specified
     3      * {@code int} value.  If a new {@code Integer} instance is not
     4      * required, this method should generally be used in preference to
     5      * the constructor {@link #Integer(int)}, as this method is likely
     6      * to yield significantly better space and time performance by
     7      * caching frequently requested values.
     8      *
     9      * This method will always cache values in the range -128 to 127,
    10      * inclusive, and may cache other values outside of this range.
    11      *
    12      * @param  i an {@code int} value.
    13      * @return an {@code Integer} instance representing {@code i}.
    14      * @since  1.5
    15      */
    16     public static Integer valueOf(int i) {
    17         assert IntegerCache.high >= 127;
    18         if (i >= IntegerCache.low && i <= IntegerCache.high)
    19             return IntegerCache.cache[i + (-IntegerCache.low)];
    20         return new Integer(i);
    21     }

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

     1  /**
     2      * Cache to support the object identity semantics of autoboxing for values between
     3      * -128 and 127 (inclusive) as required by JLS.
     4      *
     5      * The cache is initialized on first usage.  The size of the cache
     6      * may be controlled by the -XX:AutoBoxCacheMax=<size> option.
     7      * During VM initialization, java.lang.Integer.IntegerCache.high property
     8      * may be set and saved in the private system properties in the
     9      * sun.misc.VM class.
    10      */
    11 
    12     private static class IntegerCache {
    13         static final int low = -128;
    14         static final int high;
    15         static final Integer cache[];
    16 
    17         static {
    18             // high value may be configured by property
    19             int h = 127;
    20             String integerCacheHighPropValue =
    21                 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    22             if (integerCacheHighPropValue != null) {
    23                 int i = parseInt(integerCacheHighPropValue);
    24                 i = Math.max(i, 127);
    25                 // Maximum array size is Integer.MAX_VALUE
    26                 h = Math.min(i, Integer.MAX_VALUE - (-low));
    27             }
    28             high = h;
    29 
    30             cache = new Integer[(high - low) + 1];
    31             int j = low;
    32             for(int k = 0; k < cache.length; k++)
    33                 cache[k] = new Integer(j++);
    34         }
    35 
    36         private IntegerCache() {}
    37     }

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

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

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

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

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

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

     1 public class Client29 {
     2     public static void main(String[] args) {
     3         Client29 c = new Client29();
     4         int i = 140;
     5         // 分别传递int类型和Integer类型
     6         c.testMethod(i);
     7         c.testMethod(new Integer(i));
     8     }
     9 
    10     public void testMethod(long a) {
    11         System.out.println(" 基本类型的方法被调用");
    12     }
    13 
    14     public void testMethod(Long a) {
    15         System.out.println(" 包装类型的方法被调用");
    16     }
    17 }

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

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

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

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

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

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

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

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

     1 public class Client29 {
     2     public static void main(String[] args) {
     3         Client29 c = new Client29();
     4         int i = 140;
     5         c.testMethod(i);
     6     }
     7 
     8     public void testMethod(Long a) {
     9         System.out.println(" 包装类型的方法被调用");
    10     }
    11 }

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

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

      (1)、i 通过valueOf方法包装成一个Integer对象

      (2)、由于没有testMethod(Integer i)方法,编译器会"聪明"的把Integer对象转换成int。

      (3)、int自动拓宽为long,编译结束

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

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

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

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

     1 import java.util.Random;
     2 
     3 public class Client30 {
     4     public static void main(String[] args) {
     5         Random r = new Random();
     6         for(int i=1; i<=4; i++){
     7             System.out.println("第"+i+"次:"+r.nextInt());
     8             
     9         }
    10     }
    11 }

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

    1 public class Client30 {
    2     public static void main(String[] args) {
    3         Random r = new Random(1000);
    4         for(int i=1; i<=4; i++){
    5             System.out.println("第"+i+"次:"+r.nextInt());
    6             
    7         }
    8     }
    9 }

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

    第1次:-1244746321
    第2次:1060493871
    第3次:-1826063944
    第4次:1976922248  

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

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

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

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

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

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

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

  • 相关阅读:
    Git 总结
    .net报错大全
    对于堆和栈的理解
    html 局部打印
    c#面试问题总结
    算法题总结
    h5-plus.webview
    堆和栈,引用类型,值类型,指令,指针
    .NET framework具体解释
    前端之间的url 传值
  • 原文地址:https://www.cnblogs.com/selene/p/5854300.html
Copyright © 2011-2022 走看看