转载

《Effective Java》Second Edition 第8章 通用程序设计

第8章 通用程序设计

第45条:将局部变量的作用域最小化

  1. 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。如果变量在使用之前进行声明,这只会造成混乱——对于试图理解程序功能的读者来说,这又多了一种只会分散他们注意力的因素。
  2. 几乎每个局部变量的声明都应该包含一个初始化表达式。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个受检的异常(checked exception),该变量就必须在try块的内部被初始化。如果变量的值必须在try块的外部被使用到,它就必须在try块之前被声明,但是在try块之前,它还不能被“有意义地初始化”。
  3. 循环中提供了特殊的机会来将变量的作用域最小化。(无论是传统的还是for-each形式的)for循环,都允许声明循环变量(loop variable),它们的作用域被限定在正好需要的范围之内。(这个范围包括循环体,以及循环体之前的初始化、测试、更新部分。)因此,如果在循环终止之后不再需要循环变量的内容,for循环就优先于while循环。

第46条:for-each循环优先于传统的for循环

  1. 注意,利用for-each循环不会有性能损失,甚至用于数组也一样。实际上,在某些情况下,比起普通的for循环,它还稍有些性能优势,因为它对数组索引的边界值只计算一次。虽然可以手工完成这项工作,但程序员并不总会这么做。
  2. 为了修正这些示例中的Bug,必须在外部循环的作用域中添加一个变量来保存外部元素:
// Fixed, but ugly - you can do better!
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()));
}
  • 1

如果使用的是嵌套的for-each循环,这个问题就会完全消失。产生的代码就如你所希望得那样简洁。

// Preferred idiom for nested iteration on collections and arrays
for (Suit suit : suits)
    for (Rank rank : ranks)
        deck.add(new Card(suit, rank));
  • 1

实现Iterable接口并不难。如果你在编写的类型表示的是一组元素,即使你选择不让它实现Collection,也要让它实现Iterable。这样可以允许用户利用for-each循环遍历你的类型,会令用户永远感激不尽的。

遗憾的是,有三种常见的情况无法使用for-each循环:

过滤——如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。 
转换——如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。 
平行迭代——如果需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移(就如上述关于有问题的牌和头子的示例中无意中所示范的那样)。

第47条:了解和使用类库

  1. 在1.2发行版本中,Collections Framework(集合框架)被加入到了java.util包中。它应该成为每个程序员基本工具箱中的一部分。Collections Framework是一个统一的体系结构,用来表示和操作集合,允许它们对集合进行独立于表示细节的操作。
  2. 1.5发行版本中,在java.util.concurrent包中增加了一组并发实用工具。这个包既包含高级的并发工具来简化多线程的编程任务,还包含低级别的并发基本类型,允许专家们自己编写更高级的并发抽象。java.util.concurrent的高级部分,也应该是每个程序员基本工具箱中的一部分。
  3. 一般而言,类库的代码可能比你自己编写的代码更好一些,并且会随着时间的推移而不断改进。这并不是在影射你作为一个程序员的能力。从经济角度的分析表明:类库代码受到的关注远远超过大多数普通程序员在同样的功能上所能够给予的投入。

第48条:如果需要精确的答案,请避免使用float和double

  1. float和double类型尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1(或者10的任何其他负数次方值)是不可能的。
  2. 解决这个问题的正确办法是使用BigDecimal、int或者long进行货币计算。
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 = price.add(TEN_CENTS)) {
        itemsBought++;
        funds = funds.substract(price);
    }
    System.out.println(itemsBought + " items bought.");
    System.out.println("Money left over: $" + funds);
}
  • 1

如果你想让系统来记录十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal。使用BigDecimal还有一些额外的好处,它允许你完全控制舍入,每当一个操作涉及舍入的时候,它允许你从8种舍入模式中选择其一。如果正通过法定要求的舍入行为进行业务计算,使用BigDecimal是非常方便的。

第49条:基本类型优先于装箱基本类型

  1. 第一,基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性。换句话说,两个装箱基本类型可以具有相同的值和不同的同一性。第二,基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值:null。
  2. 它在计算表达式(i==42)的时候抛出NullPointerException异常。问题在于,i是个Integer,而不是int,就像所有的对象引用域一样,它的初始值为null。当程序计算表达式(i==42)时,它会将Integer与int进行比较。几乎在任何一种情况下,当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱,这种情况无一例外。如果null对象引用被自动拆箱,就会得到一个NullPointerException。
  3. 那么什么时候应该使用装箱基本类型呢?它们有几个合理的用处。第一个是作为集合中的元素、键和值。你不能将基本类型放在集合中,因此必须使用装箱基本类型。在参数化类型中,必须使用装箱基本类型作为类型参数,因为Java不允许使用基本类型。例如,你不能将变量声明为ThreadLocal<int>类型,因此必须使用ThreadLocal<Integer>代替。最后,在进行反射的方法调用时,必须使用装箱基本类型。

第51条:当心字符串连接的性能

  1. 为了获得可以接受的性能,请使用StringBuilder替代String,来存储建造中的对账单。(Java 1.5发行版本中增加了非同步StringBuilder类,代替了现在已经过时的StringBuffer类。):
public String statement() {
    StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
    for (int i=0; i<numItems(); i++)
        b.append(lineForItem(i));
    return b.toString();
}
  • 1

第52条:通过接口引用对象

  1. 更一般地讲,应该优先使用接口而不是类来引用对象。如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。
  2. 有一点值得注意:如果原来的实现提供了某种特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么很关键的一点是,新的实现也要提供同样的功能。例如,如果第一个声明周围的代码依赖于Vector的同步策略,在声明中用ArrayList代替Vector就是不正确的。如果依赖于实现的任何特殊属性,就要在声明变量的地方给这些需求建立相应的文档说明。
  3. 那么,为什么要改变实现呢?因为新的实现提供了更好的性能,或者因为它提供了期望得到的额外功能。有个真实的例子与ThreadLocal类有关。在内部,这个类在Thread中使用了一个包级私有的Map域,将每个线程的值(per-thread values)与ThreadLocal实例关联起来。在1.3发行版本中,这个域被初始化为HashMap实例。在1.4发行版本中,Java平台增加了一个新的、被称为IdentityHashMap的专用Map实现。只需将初始化域的那一行代码改变为IdentityHashMap,代替原来的HashMap,ThreadLocal机制就会变快许多。
  4. 如果没有合适的接口存在,完全可以用类而不是接口来引用对象。例如,考虑值类(value class),比如String和BigInteger。记住,值类很少会用多个实现编写。它们通常是final的,并且很少有对应的接口。使用这种值类作为参数、变量、域或者返回类型是再合适不过的了。更一般地讲,如果具体类没有相关联的接口,不管它是否表示一个值,你都没有别的选择,只有通过它的类来引用它的对象。Random类就属于这种情形。

第53条:接口优先于反射机制

  1. 执行反射访问所需要的代码非常笨拙和冗长。编写这样的代码非常乏味,阅读起来也很困难。
  2. 核心反射机制最初是为了基于组件的应用创建工具而设计的。这类工具通常要根据需要装载类,并且用反射功能找出它们支持哪些方法和构造器。这些工具允许用户交互式地构建出访问这些类的应用程序,但是所产生出来的这些应用程序能够以正常的方式访问这些类,而不是以反射的方式。反射功能只是在设计时(design time)被用到。通常,普通应用程序在运行时不应该以反射方式访问对象。
  3. 有一些复杂的应用程序需要使用反射机制。这些示例包括类浏览器、对象监视器、代码分析工具、解释型的内嵌式系统。在RPC(远程过程调用)系统中使用反射机制也是非常合适的,这样可以不再需要存根编译器(stub compiler)。如果你对自己的应用程序是否也属于这一类应用程序而感到怀疑,它很有可能就不属于这一类。
  4. 第一,这个例子会产生3个运行时错误,如果不使用反射方式的实例化,这3个错误都会成为编译时错误。第二,根据类名生成它的实例需要20行冗长的代码,而调用一个构造器可以非常简洁地只使用一行代码。
// Translate the class name into a Class object
Class<?> cl = null;
try {
    cl = Class.forName(args[0]);
} catch(ClassNotFoundException e) {
    System.err.println("Class not found.");
    System.exit(1);
}

// Instantiate the class
Set<String> s = null;
try {
    s = (Set<String>) cl.newInstance();
} catch(IllegalAccessException e) {
    System.err.println("Class not accessible.");
    System.exit(1);
} catch(InstantiationException e) {
    System.err.println("Class not instantiable.");
    System.exit(1);
}

如果要编写一个包,并且它运行的时候必须依赖其他某个包的多个版本,这种做法可能就非常有用。这种做法就是,在支持包所需要的最小环境下对它进行编译,通常是最老的版本,然后以反射方式访问任何更加新的类或者方法。

如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。

第54条:谨慎地使用本地方法

  1. 从历史上看,本地方法主要有三种用途。它们提供了“访问特定于平台的机制”的能力,比如访问注册表(registry)和文件锁(file lock)。它们还提供了访问遗留代码库的能力,从而可以访问遗留数据(legacy data)。最后,本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
  2. 使用本地方法来访问特定于平台的机制是合法的,但是随着Java平台的不断成熟,它提供了越来越多以前只有在宿主平台上才拥有的特性。例如,1.4发行版本中新增加的java.util.prefs包,提供了注册表的功能,1.6发行版本中增加了java.awt.SystemTray,提供了访问桌面系统托盘区的能力。
  3. 使用本地方法来提供性能的做法不值得提倡。在早期的发行版本中(1.3发行版本之前),这样做往往是很有必要的,但是JVM实现变得越来越快了。对于大多数任务,现在即使不使用本地方法也可以获得与之相当的性能。举例来说,当Java 1.1发行版本中增加了java.math时,BigInteger是在一个用C编写的快速多精度运算库的基础上实现的。在当时,为了获得足够的性能这样做是必要的。在1.3发行版本中,BigInteger则完全用Java重写了,并且进行了精心的性能调优。即便如此,新的版本还是比原来的版本更快,在这些年间,VM也已经变得更快了。
  4. 因为本地语言不是安全的,所以,使用本地方法的应用程序也不再能免受内存毁坏错误的影响。

第55条:谨慎地进行优化

  1. 它们讲述了一个关于优化的深刻真理:优化的弊大于利,特别是不成熟的优化。
  2. 不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。如果好的程序不够快,它的结构将使它可以得到优化。好的程序体现了信息隐藏(information hiding)的原则:只要有可能,它们就会把设计决策集中在单个模块中,因此,可以改变单个决策,而不会影响到系统的其他部分。
  3. 实现上的问题可以通过后期的优化而得到修正,但是,遍布全局并且限制性能的结构缺陷几乎是不可能被改正的,除非重新编写系统。
  4. 当一个系统设计完成之后,其中最难以更改的组件是那些指定了模块之间交互关系以及模块与外界交互关系的组件。在这些设计组件之中,最主要的是API、线路层(wire-level)协议以及永久数据格式。这些设计组件不仅在事后难以甚至不可能改变,而且它们都有可能对系统本该达到的性能产生严重的限制。
  5. 要考虑API设计决策的性能后果。使公有的类型成为可变的(mutable),这可能会导致大量不必要的保护性拷贝。同样地,在适合使用复合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能。最后一个例子,在API中使用实现类型而不是接口,会把你束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用。
  6. API设计对于性能的影响是非常实际的。考虑java.awt.Component类中的getSize方法。这个决定就是,这个注重性能的方法返回Dimension实例,与此密切相关的决定是,Dimension实例是可变的,迫使这个方法的任何实现都必须为每个调用分配一个新的Dimension实例。尽管在现代VM上分配小对象的开销并不大,但是分配数百万个不必要的对象仍然会严重地损害性能。
  7. 在Java平台上对优化的结果进行测量,比在其他的传统平台上更有必要,因为Java程序设计语言没有很强的性能模型(performance model)。各种基本操作的相对开销也没有明确定义。程序员所编写的代码与CPU执行的代码之间存在“语义沟(semantic gap)”,而且这条语义沟比传统编译语言中的更大,这使得要想可靠地预测出任何优化的性能结果都非常困难。大量流传的关于性能的说法最终都被证明为半真半假,或者根本就不正确。
  8. 第一个步骤是检查所选择的算法:再多的低层优化也无法弥补算法的选择不当。

第56条:遵守普遍接受的命名惯例

  1. 常量域是个静态final域,它的值是不可变的。如果静态final域有基本类型,或者有不可变的引用类型,它就是个常量域。例如,枚举常量是常量域。如果静态final域有个可变的引用类型,若被引用的对象是不可变的,它也仍然可以是个常量域。注意,常量域是唯一推荐使用下划线的情形。
  2. 类型参数名称通常由单个字母组成。这个字母通常是以下五种类型之一:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型,X表示异常。任何类型的序列可以是T、U、V或者T1、T2、T3。
  3. 转换对象类型的方法、返回不同类型的独立对象的方法,通常被称为toType,例如toString和toArray。返回视图(view,视图的类型不同于接收对象的类型)的方法通常被称为asType,例如asList。返回一个与被调用对象同值得基本类型的方法,通常被称为typeValue,例如intValue。静态工厂的常用名称为valueOf、of、getInstance、newInstance、getType和NewType。

原文地址:http://www.jsdblog.com/article/63   

正文到此结束
Loading...