Effective Java——通用程序设计(上)

来源:互联网 发布:php 地址转换经纬度 编辑:程序博客网 时间:2024/05/03 21:42

                        目录
四十五、将局部变量的作用域最小化
四十六、for-each循环优先于传统的for循环
四十七、了解和使用类库
四十八、如果需要精确的答案,请避免使用float和double
四十九、基本类型优先于装箱基本类型
五十、如果其他类型更适合,则尽量避免使用字符串


四十五、将局部变量的作用域最小化

        要使局部变量的作用域最小化,最有力的实践就是在第一次使用它的地方声明。如果过早的声明,开发者就有可能在真正使用该变量的时候忘记了它的类型或者初始值了,而且也会带来代码块内变量名的名字污染问题,由此引发Bug。
        几乎每个局部变量的声明都应该包含一个初始化表达式。如果你没有足够的信息来满足对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个异常,该变量就必须在try块内初始化,如果这个变量的值也必须在try块之外被访问,它就必须在try块之前被声明,但是遗憾的是在try块之前,它还不能被“有意义地初始化”。
        循环中提供了特殊的机会将变量的作用域最小化,它们的作用域正好被限定在需要的范围之内。因此,如果在循环终止之后不再需要变量的内容,for循环就优先于while循环,见如下代码片段:

Iterator<Element> i = c.iterator();while (i.hasNext()) {    doSomething(i.next());}... ...Iterator<Element> i2 = c2.iterator();    while (i.hasNext()) {  //BUG!    doSomethingElse(i2.next());}

        可以看到在第二个循环的循环条件判断处有一个非常明显的BUG,这极有可能是copy-paste所致。然而该类错误如果出现在for循环里,将直接引发编译期错误。

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {    doSomething(i.next());}... ...for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {    doSomethingElse(i2.next());}

        如果使用for循环,犯这种copy-paste错误的可能性大大降低,因为通常没有必要在两个循环中使用不同的变量名。循环是完全独立的,所以重用元素(或者迭代器)变量的名称不会有任何危害。实际上,这也是很流行的做法。


四十六、for-each循环优先于传统的for循环

        for-each循环是在Java 1.5 发行版本之后才支持的,之前只能使用传统的for循环。相比于普通for循环,for-each大大提高了代码可读性,由此也减少了低级BUG出现的几率。见如下代码片段:

enum Suit { CLUB,DIAMOND,HEART,SPADE }enum Rank  { ACE,DEUCE,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN,JACK,QUEEN,KING }... ...Collection<Suit> suits = Arrays.asList(Suit.values());Collection<Rank> ranks = Arrays.asList(Rank.values());List<Card> deck = new ArrayList<Card>();for (Iterator<Suit> i = suits.iterator(); i.hasNext();)    for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )        deck.add(new Card(i.next(),j.next()); //BUG, j被多次迭代

        上面代码的BUG是比较隐匿的,很多专家级的程序员也会偶尔犯类似的错误。下面我们来一下修复后的代码片段:

for (Iterator<Suit> i = suits.iterator(); i.hasNext();) {    Suit suit = i.next();    for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )        deck.add(new Card(suit,j.next()); //BUG, j被多次迭代}

        如果使用嵌套的for-each循环,这个问题就会完全消失:

for (Suit suit : suits)    for (Rank rank : Ranks)        deck.add(new Card(suit,rank));

        for-each循环不仅让你遍历集合和数组,还让你遍历任何实现Iterable接口的对象。这个简单的接口由单个方法组成。如果你在编写的类型是一组元素,即使你不让它实现Collection,也要让它实现Iterable接口,这样可以允许用户利用for-each循环来遍历你的类型。

        总之,for-each循环的简洁性和预防Bug方面有着传统for循环无法比拟的优势,并且没有性能损失。应该尽可能地使用for-each循环。遗憾的是,有三种常见的情况无法使用for-each循环:
        1. 过滤——如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。
        2. 转换——如果需要遍历列表或数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
        3. 并行迭代——如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。


四十七、了解和使用类库

        假设你希望产生位于0和某个上届之间的随机数,许多程序员会编写出如下方法:

private static final Random rnd = new Random();static int random(int n) {    return Math.abs(rnd.nextInt()) % n;}

        这个方法有三个缺点。第一个缺点是,如果n是一个比较小的2的乘方,经过一段相当短的周期后,它产生的随机数序列将会重复。第二个缺点是,如果n不是2的乘方,那么平均起来,有些数会比其他的数出现得更为频繁。如果n比较大,这个缺点将会非常明显。第三个缺点是,在极少数情况下,它的失败是灾难性的,返回一个落在指定范围之外的数。因为如果rnd.nextInt()返回Integer.MIN_VALUE,那么如果n不是2的乘方,取模操作符就会返回一个负数,这使得程序失败,且这种失败很难重现。

        幸运的是,Random.nextInt(int)完美修正了所有的缺点。使用这样的标准类库可以充分利用这些编写标准类库专家的知识,而且不必浪费时间在与工作不太相关的问题上。另外,标准类库的性能往往会随着时间的推移不断提高,功能也会不断增强。

        总而言之,不要会重新发明轮子。如果你要做的事情看起来十分常见,有可能类库中已经有某个类完成了这样的工作。一般而言,类库的代码比你自己编写的代码更好一些,并会随着时间的推移不断改进。


四十八、如果需要精确的答案,请避免使用float和double

        float和double类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合,如货币计算等。

        该条目给出一个例子,如果你手里有1美元,超市货架上有一排糖果,它们的售价分别为10美分、20美分、30美分,以此类推直到1美元。你打算从标价10美分的开始买,每个糖果买1颗,直到不能支付货架上下一中价格的糖果为止,那么你可以买多少糖果?还会找回多少零头呢?见如下代码:

public static void main(String[] args) {    double funds = 1.00;    int itemsBought = 0;    for (double price = .10; funds >= price; price += .10) {        funds -= price;        itemsBought++;    }    System.out.println(itemsBought + " items bought.");    System.out.println("Change: $" + funds);    }// 3 items bought.// Change: $0.39999999999999

        造成这一结果的主要原因就是double类型的精度问题。解决该问题的正确办法是使用BigDecimal、int或者long进行货币计算。下面我们看一下该程序用BigDecimal实现的翻版。

public static void main(String[] args) {    final BigDecimal TEN_CENTS = new BigDecimal(".10");     int itemsBought = 0;    BigDecimal funds = new BigDecimal("1.00");    for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0;price.add(TEN_CENTS)) {        itemsBought++;        funds = funds.substract(price);    }    System.out.println(itemsBought + " items bought.");    System.out.println("Money left over: $" + funds);}// 4 items bought. // Money left over: $0.00

        现在我们得到了正确的结果。然而,使用BigDecimal有两个主要缺点:和使用基本运算类型相比,这样做很不方便,而且效率也低。除了该方法之外我们还可以使用int或者long,至于使用哪种具体类型,需要视所涉及的数值大小而定。现在我们需要将计算单位转换为分,而不再是以元为单位,下面是这个例子的又一次翻版:

public static void main(String[] args) {    int itemsBougth = 0;    int funds = 100;    for (int price = 0; funds >= price; price += 10) {        itemsBought++;        fund -= price;    }    System.out.println(itemsBought + " items bought.");    System.out.println("Money left over: $" + funds + " cents.");}// 4 items bought.// Money left over: $0.00 cents.
        总而言之,对于任何需要精确答案的计算任务,请不要float或者double。如果性能非常关键,而且所涉及的数值不是太大,就可以使用int或者long。如果数值范围没有超过9位十进制数字,就可以使用int;如果不超过18位数字,就可以使用long。如果数值可能超过18位数字,就必须使用BigDecimal。


四十九、基本类型优先于装箱基本类型

        Java的类型系统中主要包含两个部分,分别是基本类型,如int、double、long,还有就是引用类型,如String、List等。其中每个基本类型都对应着一种引用类型,被称为装箱基本类型,如分别和int、double、long对应的装箱类型Integer、Double和Long等。

        Java在1.5 中新增了自动装箱的和自动拆箱的功能。这些特性仅仅是模糊了基本类型和装箱类型之间的区别,但是并没有完全消除他们之间的差异,而这些差别往往会给我们的程序带来一些潜在的问题。我们先看一下他们之间的主要区别:
        1. 基本类型只有值,在进行比较时可以直接基于值进行比较,而装箱类型在进行同一性比较时和基本类型相比有着不同的逻辑,毕竟他们是对象,是Object的子类,它们需要遵守Java中类对象比较的默认规则。
        2. 基本类型只有功能完备的值,而每个装箱类型除了它对应基本类型的所有功能之外,还有一个非功能值:null。记住,它毕竟是对象。
        3. 基本类型通常比装箱类型更节省时间和空间。

        见如下代码示例:

public class MyTest {    private static int compare(Integer first,Integer second) {        return first < second ? -1 : (first == second ? 0 : 1);    }    public static void main(String[] args) {        Integer first = new Integer(42);        Integer second = new Integer(42);        System.out.println("Result of compare first and second is " +            compare(first,second));    }}

        这段代码看起来非常简单,它的运行结果也非常容易得出,然而当我们真正运行它的时候却发现,实际输出的结果和我们的期望是完全不同的,这是为什么呢?见如下分析:compare方法中的第一次比较(first < second)将能够正常工作并得到正确的结果,即first < second为false。在进行相等性比较的时候问题出现了,如前所述,Integer毕竟是对象,在进行对象之间的同一性比较时它将遵守对象的同一性比较规则,由于这两个参数对象的地址是不同的,因为我们是通过两次不同的new方法构建出的这两个参数对象。结果可想而知,first == second返回false。现在最后的输出结果已经很清楚了:Result of compare first and second is 1。修改这个问题最清楚的做法是添加两个局部变量,来保存first和second的基本类型int值,并比较这两个int型局部变量。

        现在让我们再看一段代码片段:

public class Unbelievable {    static Integer i;    public static void main(String[] args) {        if (i == 42)            System.out.println("Unbelievable");    }}

        程序的运行结果并没有打印出"Unbelievable",而是抛出了空指针异常。这是因为装箱类型的i变量并没有被初始化,即它本身为null,当程序计算表达式(i == 42)时,它会将Integer与int进行比较。几乎在任何一种情况下,当在一项操作中混合使用基本类型和装箱基本类型时,装箱类型就会自动拆箱,这种情况无一例外。如果null对象引用被自动拆箱,就会得到一个NullPointerException。修正这一问题也非常简单,只需将i的类型从Integer变为int即可。

        在看一下最后一个代码示例:

public static void main(String[] args) {    Long sum = 0L;    for (long i = 0; i < Integer.MAX_VALUE; ++i) {        sum += i;    }    System.out.println(sum);}

        这段代码虽然不像之前的两个示例那样有着明显的Bug,然而在运行时却存在着明显的性能问题。因为在执行for循环时,会有不断的自动装箱和自动拆箱的操作发生。修改该代码也是非常容易的,只需将sum的类型从Long变为long即可。

        那么什么时候应该使用装箱基本类型呢?它们有几个合理的用处。第一个是作为集合中的元素、键和值。你不能讲基本类型放在集合中,因此必须使用装箱基本类型。还有,在参数化类型中,必须使用装箱基本类型作为类型参数。最后,在进行反射的方法调用时必须使用装箱基本类型。

        总之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单快速。当程序用==操作符比较两个装箱基本类型时,这几乎不是你所希望的。当程序进行拆箱时要注意会抛出NullPointerException异常。最后,当程序装箱了基本类型值时,会导致高开销和不必要的对象创建。


五十、如果其他类型更适合,则尽量避免使用字符串

        本条目讨论一些不应该使用字符串的情形:
        1.字符串不适合代替其他的值类型。当一段数据从文件、网络,或者键盘设备,进入到程序中之后,它通常以字符串的形式存在。如果它是数值,就应该转换为合适的数值类型,比如int、float或者BigInteger类型。如果它是一个“是或否”这种类型的答案,就应该被转换为boolean类型。
        2.字符串不适合代替枚举类型。枚举类型比字符串更加适合用来表示枚举类型。
        3.字符串不适合代替聚集类型。如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的。解析字符串的过程也非常慢,也很繁琐。简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类。

        总而言之,如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。若使用不当,字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错。经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚合类型。


0 0
原创粉丝点击