《Effective Java》笔记

第2章 创建和销毁对象

第1条:考虑用静态工厂方法代替构造函数

优点

  • 与构造函数不同,静态工厂方法具有名字
  • 与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象
  • 与构造函数不同,它们可以返回一个原返回类型的子类型的对象

缺点

  • 类如果不含公有的或者受保护的构造函数,就不能被子类化
  • 它们与其他的静态方法没有任何区别
     

第2条:使用私有构造函数强化singleton属

通过使构造函数私有化实现单例,有两种方法
1. public static final Singleton INSTANCE = new Singleton();
public属性,直接通过类名访问
2. private static final Singleton INSTANCE = new Singleton();
private属性,提供getInstance公有方法返回实例对象

为了使一个singleton类变成可序列化,仅仅在声明中加上“implements Serializable”是不够的。为了维护singleton性,必须要提供一个readResolve方法。否则,一个序列化的实例在每次反序列化的时候,都会导致创建一个新的实例,因此在Singleton类中加入下面的readResolve方法:

private Object readResolve() throws ObjectStreamException {
    /**
     * return the one true Singleton and let the garbage collector
     * take care of the Singleton impersonator
     */
    return INSTANCE;
}

第3条:通过私有构造函数强化不可实例化的能力

企图通过将一个类做成抽象类来强制控制该类不可被实例化是行不通的。该类可以被子类化,并且该子类也可以被实例化。更进一步,这样做会误导用户,以为这种类是专门为了继承而设计的。
为了使某个类不可实例化,只要让这个类包含单个显示的私有构造函数,则它就不可被实例化了
 

第4条:避免创建重复的对象

重复使用同一个对象,而不是每次需要的时候就创建一个功能等价的新对象,通常前者更为合适。如果一个对象是非可变的(immutable),那么它总是可以被重用。
 

不要错误地认为“创建对象的代价是非常昂贵的,我们应该要尽可能地避免创建对象”。小对象的创建和回收动作是非常廉价的。反之,通过维护自己的对象池(object pool)来避免对象的创建工作并不是一个好的做法,除非池中的对象是非常重量级的,如,数据库连接池。

第5条:消除过期的对象引用

一般而言,只要一个类自己管理它的内存,程序员就应该警惕内存泄露问题。一旦一个元素被释放掉,则该元素中包含的任何对象引用应该要被清空。
内存泄露的另一个常见来源是缓存。容易被遗忘,可使用软引用解决。
 

第6条:避免使用终结函数

终结函数(finalizer)通常是不可预测的,常常也是很危险的,使用终结函数会导致不稳定的行为,当然,终结函数也有其可用之处。

如果一个类封装的资源确实需要回收,我们只需提供一个显式的终止方法,并要求该类的客户在每个实例不再有用的时候调用这个方法,显式终止方法的一个典型雷子是InputStream的close方法。
显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。如下:

Foo foo = new Foo(...);
try {
    // Do what must be done with foo
    ...
} finally {
    foo.terminate();    // Explicit termination method
}

终结函数有两个用途:
1. 当一个对象的所有者忘记了调用显式终止方法的情况下,终结函数可以充当安全网。(迟一点释放资源总比永远不释放好)
2. 与本地对等体(native peer)有关。

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结函数。如果使用了终结函数,那么就要记住调用super.finalize(终结函数链)。最后,如果要把一个终结函数与一个公有的非final类关联起来,那么请考虑使用终结函数守卫者,以确保及时子类的终结函数未能调用super.finalize,该终结函数也会被执行。

第3章 对于所有对象都通用的方法

尽管Object是一个具体类,但是设计它主要是为了扩展。它的所有非final方法(equalshashCodetoStringclonefinalize)都有明确的通用约定。在改写这些方法时,如果不遵守约定,则其他一些一栏与这些约定的类就无法与这些类结合在一起正常运作。

第7条:在改写equals的时候请遵守通用约定

如果满足以下任一条件,则不需要改写equals方法

  • 一个类的每个实例本质上都是唯一的。
  • 不关心一个类是否提供了“逻辑相等”。
  • 超类已经改写了equals,从超累继承过来的行为对于子类也是合适的。
  • 一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。
     

在改写equals方法的时候,必须遵守通用约定。以下约定是来自java.lang.Object的规范:

  • 自反性(reflexive)。对于任意的引用值x,x.equals(x)一定为true。
  • 对称性(symmetric)。对于任意引用值x和y,当且仅当y.equals(x)返回true时,x.quals(y)也一定返回true。
  • 传递性(transitive)。若x.equals(y)返回true, y.equals(z)返回true,则x.equals(z)也一定返回true。
  • 一致性(consistent)。对于任意的引用值x和y,如果用于equals比较的对象信息没有被修改的话,那么多次调用x.equals(y)要么一致地返回true,要么一致返回false。
  • 对于任意的非空引用值x,x.equals(null)一定要返回false。
     

实现高效equals方法的措施:

  1. 使用==操作符检查“实参是否指向对象的一个引用”。
  2. 使用instanceof操作符检查“实参是否为正确的类型”。
  3. 把实参转换到正确的类型。(instanceof)
  4. 对于该类中每一个关键域,检查实参中的域与当前对象中对应的域值是否相等。
  5. 编写完成equals方法之后,检查其是否满足对称性、传递性、一致性。
     

最后的告诫:

  • 改写equals方法时,总是要改写hashCode。
  • 不要企图让equals方法过于聪明。(过度寻求各种等价关系)
  • 不要使equals方法依赖于不可靠的资源。例如,java.net.URL比较主机IP地址需要访问网络。
  • 不要将equals声明中的Object对象替换成其他的类型。
     

第8条:改写equals时总是要改写hashCode

java.lang.Object中关于hashCode有如下规范:

  • 在一个应用程序执行期间,如果一个对象的equals方法做笔记所用到的信息没有被修改的话,那么,对该对象调用多次hashCode方法,均返回同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同。
  • 如果两个对象equals相等,则其hashCode一定相等
  • 如果两个对象equals不相等,则hashCode可以不相等。(不相等有利于提高散列性能)
     

如何尽量为不相等的对象产生不相等的散列码(可以接受排除冗余域的计算):

  1. 把某个非零长数值,比如说17,保存在一个叫result的int类型变量中。
  2. 对于对象中每一个关键域f,完成以下步骤:
    a. 为该域计算int类型的散列码c:
      i. 如果该域是boolean类型,则计算(f ? 0 : 1)。
      ii. 如果该域是byte、char、short或int类型,则计算(int)f。
      iii. 如果该域是long类型,则计算(int)(f^(f >> 32))。
      iv. 如果该域是float类型,则计算Float.floatToIntBits(f)。
      v. 如果该域是double类型,则计算Double.doubleToIntBits(f)得到一个long类型值,然后按照2.a.iii,对该long值计算散列值。
      vi. 如果该域是一个对象引用,并且该类的equals方法通过递归调用equals的方式来比较这个域,则同样对这个域递归调用hashCode。
      vii. 如果该域是一个数组,则把每一个元素当做单独的域来处理。
    b. 按照下面的公式,把步骤a中计算得到的散列码c组合到result中
      result = 37 * result + c;
  3. 返回result。
  4. 写完了hashCode方法之后,检查是否相等的实例具有相等的散列码。
     

第9条:总是要改写toString

toString的通用约定指出,被返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”。在实际应用中,toString应该返回对象中包含的所有令人感兴趣的信息。在实现toString时,必须决定是否在文档中指定返回值格式。对于值类(value class)推荐这样做,电话号码XXX-XXXXXXX。指定返回值格式也有缺点,一旦指定了某种形式,必须始终如一地坚持这种格式。
不管你是否决定指定格式,都应该是在文档中明确地表明你的意图。
为toString返回值中包含的所有信息,提供一种编程访问途径,这总是一个好的做法
 

第10条:谨慎地改写clone

clone的通用约定(很弱):创建和返回该对象的一个拷贝。对于任何对象x,都有
x.clone() != x 将会是true,
x.clone().getClass() == x.getClass() 将会是true,
x.clone().equals(x) 将会是true。

Cloneable接口没有包含任何方法(即不含clone方法),它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,则Object的clone方法返回该对象的逐域拷贝,否则,它将抛出一个CloneNotSupportedException异常。

如果改写了一个非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象
如果对象中包含的域引用了可变的对象,使用super.clone可能导致灾难性后果(Stack类的对象数组引用)。实际上,clone方法是另一个构造函数;你必须确保它不会伤害到原始的对象,并且正确地建立起被克隆对象中的约束关系(invariant),对于Stack类中的对象数组递归地调用clone:

public Object clone() throws CloneNotSupportedException {
    Stack result = (Stack)super.clone();
    result.elements = (Object[]) elements.clone();
    return result;
}

clone结构与指向可变对象的final域的正常用法是不兼容的。
另一个实现对象拷贝的好办法是提供一个拷贝构造函数(copy constructor)
public A(A a);
public static A newInstance(A a);
其不依赖于某一种很有风险的、语言之外的对象创建机制;他们不要求遵守尚未良好文档化的规范;不会与final域的正常使用发生冲突;不会要求客户捕获不必要的被检查类型;为客户提供了一个静态实例化的对象。

可以安全地说,其他的接口不应该扩展Cloneable接口,并且,为了继承而设计的类也不应该实现这个接口。
 

第11条:考虑实现Comparable接口

Comparable方法的通用约定规范:将当前这个对象与指定的对象进行顺序比较。当该对象小于、等于或大于指定对象的时候分别返回一个负整数、零或者正整数。如果由于指定对象的类型而使得无法进行比较,则抛出ClassCastException异常。

有如下约定:

  • x.compareTo(y) == -y.compareTo(x)(这暗示着,若左边抛出异常,则右边也必须抛出异常)。
  • 传递性:若x.compareTo(y) > 0, y.compareTo(z) > 0, 则x.compareTo(z) > 0。
  • 若x.compareTo(y) == 0,则x.compareTo(z) == y.compareTo(z)。
  • 强烈建议 ( x.compareTo(y) == 0 ) == x.equals(y),但不是严格要求。若违反该规定,应该注明。(与equals一致)

对于最后一条,举个例子说明。考虑BigDecimal类,它的compareTo与equals不一致,如果往一个HashSet中加入 new BigDecimal(“1.0) 与 new BigDecimal(“1.00”),最终HashSet中将包含这两个对象(通过equals比较)。而往TreeSet中添加这两个对象,则只含1个对象(通过CompareTo比较)。
 

第4章 类和接口

第12条:使类和成员的可访问能力最小化

信息隐藏封装是软件设计的基本原则之一。好处:解耦,加快开发速度;可重用;有利于分析局部模块性能优化等等。

如果一个包级私有的顶层类或者接口只是在某一个类的内部被用到,那么你应该考虑使它成为后者的一个私有嵌套类(或者接口)。

如果一个方法改写了超类中的一个方法,那么子类中该方法的访问级别低于超类中的访问级别是不允许的。

公有类应该尽可能少地包含公有的域(通过公有的静态final域暴露类的常量是允许的)。

具有公有的静态final数组域几乎总是错误的。(非零长度的数组总是可变的)
 

第13条:支持非可变性

Java平台库中包含许多非可变类,如String、原始类型的包装类等,非可变类的存在有许多理由:非可变类比可变类更加易于设计、实现和使用。非可变对象本质上是线程安全的,它们不要求同步。

为了使一个类成为非可变类,遵循以下五条规则:

  1. 不要提供任何会修改对象的方法。
  2. 保证没有可被子类改写的方法。(一般将类设为final)
  3. 使所有的域都是final的。
  4. 使所有的域都成为private。
  5. 保证对于任何可变组件的互斥访问。

非可变类真正唯一的缺点是,对于每一个不同的值都要求一个单独的对象。

构建一个非可变类,除了可以通过将该类设为final之外,还可以:

  1. 将这个类的每一个方法都成为final。它使得程序员可以扩展这个类,在原来的基础上增加新的方法。
  2. 使这个类的所有构造函数成为私有,或者包级私有,并且增加公有的静态工厂,来代替公有的构造函数。(缺少公有或保护的构造函数而无法被扩展)

如果一个类不能被做成非可变类,那么你仍然应该尽可能地显示它的可变性。

构造函数应该创建完全初始化的对象,所有的约束关系应该在这时候建立起来。
 

第14条:复合优先于继承

与方法调用不同的是,继承打破了封装性。超类的实现有可能会随着发行版本的不同而有所变化,如果发生了变化,则子类可能会被打破,即使它的代码完全没有改变。只有当子类真正是超类的“子类型”的时候,继承才是合适的。

使用包装类来实现复合,这正是Decorator模式。此时不需要扩展一个已有的类,而是在新类中增加一个私有域,它引用了这个已有的类的一个实例,原来已有的类变成了新类的一个组成部分。新类中的每个实例方法都可以调用被包含的已有类实例中对应的方法,并返回它的结果。(这被称为转发

需要注意,包装类不适合用在回调框架(callback framework)中。
 

第15条:要么专门为继承而设计,并给出文档说明,要么禁止继承

该类的文档必须精确地描述了改写每一个方法所带来的影响。该类必须有文档说明其可改写(非final的公有或受保护方法)的方法的自用性:对于每一个共有的或受保护的方法或构造函数,它的文档必须指明它调用了哪些可改写的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。

为了允许继承,一个类还必须遵守其他一些约束。构造函数一定不能调用可被改写的方法。举例

class Super {
    public Super() {
        m();    // 错误的调用
    }

    public void m() {}
}

final class Sub extends Super {
    private final Date date;

    public Sub() {
        date = new Date();
    }

    public void m() {
        System.out.println(date);
    }

    public static void main(String[] args) {
        Sub s = new Sub();
        s.m();
    }
}

运行结果输出为:
null
Wed Nov 18 16:31:36 CST 2015
第一次打印出null,因为方法m被构造函数super()调用的时候,构造函数Sub()还没有机会初始化data域。

如果决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,要注意:无论是clone还是readObject,都不能调用一个可改写的方法,不管是以直接还是间接方式。
 

第16:条:接口优于抽象类

接口与抽象类的比较:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。

接口的好处都有啥:

  • 已有的类可以很容易被更新,以实现新的接口。
  • 接口是定义mixin(混合类型)的理想选择。有关mixin的学习,可参考诊断 Java 代码: “杀手组合”― mixin、Jam 和单元测试
  • 接口使得我们可以构造出非层次结构的类型框架。
  • 接口使得安全第一增强一个类的成为可能。可以把接口和抽象类的优点结合起来,对于期望导出的每一个重要接口,都提供一个抽象的骨架实现类,参考Collection Framework中的AbstractSet等。

然而,要注意:抽象类的演化比接口要容易得多。考虑往抽象类中增加一个新的方法和往接口中增加一个新方法的区别。

第17条:接口只是被用于定义类型

当一个类实现了一个接口时,这个接口被用作一个类型,通过此类型可以引用这个类的实例。

常量接口没有包含任何方法,它只包含静态的final域,每个域都导出一个常量。一个类只要实现了这个接口,就可以避免用类名来修饰常量名。但常量接口是对接口的不良使用。

接口应该只是被用来定义类型的,它们不应该被用来导出常量。
 

第18条:优先考虑静态成员类

嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了第一种之外,其他三种都被称为内部类。

静态成员类是外围类的一个静态成员,可访问外围类的所有成员。

非静态成员类的每一个实例都隐含着与外围类的一个外围实例紧密关联在一起。如果声明的成员类不要求访问外围实例,要将该类设为静态成员(加static)。因为非静态成员类的每个实例都将包含一个额外的指向外围对象的引用,维护这份引用要消耗时间和空间。

匿名类的实用性有限制,因为它们同时被声明和实例化,只能被用在代码中它将被实例化的那个点上,如Thread。匿名类通常只实现了其接口中或者超类中的方法,不会声明任何新的方法,因为不存在可命名的类型可以访问新增加的方法。

在任何可声明局部变量的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。

总之,如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在一个方法内部,那么应该使用成员类。如果成员类的每一个实例都需要一个指向其外围实例的引用,则把成员类做成非静态的;否则,就做成静态的。假设一个嵌套类属于一个方法的内部,如果你只需要在一个地方创建它的实例,并且已经有了一个预先存在的类型可以说明这个类的特征,则把它做成匿名类;否则就做成局部类。
 

第5章 C语言结构的替代

书的作者:你可以选择跳过本章的内容,即使这样,读一读第21条的内容仍然是很有价值的。(因此第5章只摘录第21条,其他条目仅给出名称)

第19条:用类代替结构struct
第20条:用类层次代替联合union

第21条:用类来代替enum结构

C程序中,若有如下语句

typedef enum {A, B, C} fst;
typedef enum {A, D, E} sec;

则sec将会与fst发生冲突,用enum结构定义的类型非常脆弱。在enum类型中增加新的常量但又不重新编译客户代码,会引起不可预知的行为。
不幸地,Java中针对枚举类型的模式也具有同样的缺点

public class A {
    public static final int PARAM_1 = 1;
    public static final int PARAM_2 = 2;
    ...
}

幸运的是,Java提供了类型安全枚举模式,可以避免常见的int和String模式的所有缺点。基本思想:定义一个类来代表枚举类型的单个元素,并且不提供任何公有的构造函数。相反,提供公有的静态final域,使枚举类型中的每一个常量都对应一个域。代码如下

public class Fruit {
    private final String name;

    private Fruit(String name) { this.name = name; }

    public String toString() { return name; }

    public static final Fruit APPLE = new Fruit("apple");
    public static final Fruit ORANGE = new Fruit("orange");
}

类型安全枚举模式提供了编译时的类型安全性。如果你声明了一个方法,它的一个参数为Fruit类型,则可以保证任何传入的非null的对象引用一定表示了APPLE和ORANGE之一,企图传递一个类型不正确的对象将会在编译的时候被捕捉到。

类型安全枚举模式的唯一严重缺点是难以把类型安全枚举常量聚集在一起。对于int枚举类型,可以选择枚举常量值,使得每一个常量都是2的不同幂次方,因而相关的常量通过按位或就可以表示一个常量集合:

public static final int A = 1;
public static final int B = 2;
public static final int C = 4;

public static final int A_B_C = A | B | C;

此外,类型安全枚举常量还有一个小的缺点是不能用在switch语句中。(可以用if代替)

在要求使用一个枚举类型的环境下,我们首先应该考虑类型安全枚举模式。
 

第22条:用类和接口来代替函数指针
 

第6章 方法

第23条:检查参数的有效性

方法的编写者应该在文档中清楚地指明参数的限制,并且在方法体的起始处对参数进行检查,以强迫施加这些限制。

对于公有的方法,使用Javadoc @throws标签记录下“一旦针对参数值的限制被违反之后将会被抛出的异常”。

对于一个未被导出的方法,作为包的编写者,可以控制这个方法将在哪些情形下被调用,而且也应该确保只有有效的参数值才会被传递进来,此时应使用assert结构而不使用正常的检查语句。

“在一个方法执行它的计算任务之前,应该检查它的参数”,这条规则也有例外。比如Collections.sort(List),在比较的过程中如果两个对象类型不同,会抛出异常,因此提前检查列表中的元素是否是可互相比较的并没有多大意义。
 

第24条:需要时使用保护性拷贝

对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是必要的。

// 非保护性拷贝
public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throws new IllegalArgumentException("...");

        this.start = start;
        this.end = end;
    }

    public Date getStart() { return start; }
    public Date getEnd() { return end; }
}

这个类其实是可变的,如下

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(2000);  // 修改了p的内部成员数据

此时需要进行保护性拷贝

public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());

    if (start.start.compareTo(this.end) > 0)
        throws new IllegalArgumentException("...");
}

注意,保护性拷贝动作实在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象而不是原始对象。
此外,对于“参数类型可以被不可信方子类化”的情形,请不要使用clone方法进行参数的保护性拷贝。
此时,我们发现,通过Period的get方法是可以改变其实例的

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.getEnd().setYear(2000);  // 修改了p的内部成员数据

为了防御这种攻击,需修改两个访问方法,使它返回可变内部域的保护性拷贝即可:

public Date getStart() {
    // return start;
    return (Date) start.clone();
}

第25条:谨慎设计方法的原型

本条目是若干API设计技巧的总结:

  • 谨慎选择方法的名字。方法的名字应该总是遵循标准的命名习惯。
  • 不要过于追求提供便利的方法。方法太多会使一个类难以学习、使用、文档化、测试和维护。
  • 避免长长的参数列表,类型相同的长参数序列尤其有害。有两项技术可以缩短太长的参数列表。第一项技术是把一个方法分解成多个方法;第二项技术是创建辅助类(往往是静态成员类),用来保存参数的聚集。
  • 对于参数类型,优先使用接口而不是类。例如,没有理由在编写一个方法时使用HashTable作为输入,相反,应该使用Map,这使得你可以传一个HashTable、HashMap、TreeMap等Map的实现类。
  • 谨慎地使用函数对象。

第26条:谨慎地使用重载

有如下集合分类程序

public class CollectionClassifier {
    public static String classfy(Set s) {
        return "Set";
    }

    public static String classfy(Collection c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection[] tests = new Collection[] {
            new HashSet(),
            new ArrayList()
        };

        for (int i = 0; i < tests.length; i++)
            System.out.println(classfy(tests[i]));
    }
}

该程序将会打印“Unknown Collection”两次,尽管其运行时的类型不同。因为参数编译时都是Collection类型,都会调用同一个重载方法。对于重载方法的选择是静态的,而对于被改的方法的选择是动态的。

避免方法重载机制的混淆用法,一个安全而保守的策略是,永远不要到处两个具有相同参数数目的重载方法。

不得已而进行重载时,要保证:当传递同样的参数时,所有的重载方法行为一致。

第27条:返回零长度的数组而不是null

观察程序

public class Test {
    public static void printNullArrLen(int[] arr) {
        if (arr != null) {
            System.out.println("空数组测试:" + arr.length);
        }
    }

    public static void printZeroArrLen(int[] arr) {
        System.out.println("零长度测试:" + arr.length);
    }

    public static void main(String[] args) {
        int arr1[] = null;
        int arr2[] = new int[0];
        printNullArrLen(arr1);
        printZeroArrLen(arr2);
    }
}

最终输出:零长度测试:0
编写客户程序的程序员可能会忘记写出专门的代码来处理null返回值而导致出错,显然返回零长度的数组能使程序变得简单。

不必担心零长度数组带来分配数组所需要的开销。

第28条:为所有导出的API元素编写文档注释

为了正确地编写API文档,你必须在每一个被导出的类、接口、构造函数、方法和域声明之前增加一个文档注释,每一个方法的文档注释应该简洁地描述出它和客户之间的约定。在文档注释内部出现任意HTML标签都是允许的,但是HTML元字符必须要经过转义。

第7章 通用程序设计

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

使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明。

几乎每一个局部变量的声明都应该包含一个初始化表达式。(try-catch例外)

如果在循环终止之后循环变量的内容不再被需要的话,则for循环优先于while循环。(while循环体内的循环变量没有被撤销)

最后一项“最小化局部变量的作用域”的技术是使方法小而集中。如果你把两个操作组合到同一个方法中,与一个操作相关的局部变量有可能会出现在执行另一个操作的代码范围之中。为了防止这种情况发生,只需简单地把这个方法分成两个:每个操作一个方法。

第30条:了解和使用库

使用标准库好处多多,了解无碍。

第31条:如果要求精确答案,请避免使用float和double

对于有些要求精确答案的计算任务,请不要使用float或者double,此时可考虑使用BigDecimal类。使用BigDecimal允许完全控制舍入。如果性能非常关键,并且不介意自己处理十进制小数点,而且所涉及的数值又不太大,此时可使用int或者long。如果数值范围不超过9位十进制数字,可使用int;不超过18位,可使用long;超过18位,必须要使用BigDecimal。

第32条:如果其他类型更适合,则尽量避免使用字符串

本条目讨论一些不应该使用字符串的场合。

  • 字符串不适合代替其他的值类型。
  • 字符串不适合代替枚举类型。
  • 字符串不适合代替聚集类型。
  • 字符串也不适合代替能力表。

第33条:了解字符串连接的性能

为链接n个字符串而重复使用字符串连接操作符,要求n的平方级的时间。

为了获得可接受的性能,使用StringBuffer代替String。

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

如果有合适的接口存在,那么对参数、返回值、变量和域的声明都应该使用接口类型。只有当你创建某个对象的时候,你才真正需要引用这个对象的类。在声明变量的时候应该养成这样的习惯:
List subscribers = new Vector();
而不是这样的声明:
Vector subscribers = new Vector();

如果你养成了使用接口作为类型的习惯,那么你的程序将会更加灵活。

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

反射机制允许一个类使用另一个类,及时当前者被编译的时候后者还根本不存在,然而,这种能力需要付出代价:

  • 你损失了编译时类型检查的好处,也包括异常检查。
  • 要求执行反射访问的代码非常笨拙和冗长。
  • 性能损失。反射方法调用比普通方法调用慢了不少。

对于有些程序,它们用到的类在编译时刻是不可用的,但是在编译时刻存在适合的接口或超类,通过它们可以引用到这些类。如果是这种情况,那么你可以以反射方式创建实例,然后通过它们的接口或者超类,以正常方式访问这些实例。

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

从历史上看,本地方法主要有三种用途:

  1. 提供了“访问与平台相关的设施”的能力,比如访问注册表和文件锁。(Java 1.4发行版本中新增加的java.util.prefs包提供了注册表功能)
  2. 提供了访问老式代码库的能力,通过这些老式代码库进一步可以访问老式数据。(JDBC API提供了访问老式数据库的能力)
  3. 通过本地语言,实现性能关键的部分,以提高系统的性能。(随着1.3发行版本的退出,使用本地方法来提高性能的做法已经不值得提倡)

使用本地方法有一些严重的缺点。

  • 因为本地语言不是安全的,使用本地方法的应用程序有可能会受到内存毁坏错误的影响。
  • 因为本地语言是平台相关性的,使用本地方法的应用程序也不再是可自由移植的。
  • 在进入和退出本地代码时,也需要较高的固定开销。
  • 本地方法编写起来单调乏味,并且难以阅读。

第37条:谨慎地进行优化

三条与优化有关的格言:
很多计算上的过失都被归咎于效率原因(没有获得必要的效率),而不是其他的原因——甚至包括盲目地做傻事。 —— William A. Wulf

不要去计较一些小的效率上的得失,在97%的情况下,不成熟的优化是一切问题的根源。 —— Donald E. Knuth

在优化方面,我们应遵守两条规则:一、不要做优化;二(仅针对专家)、还是不要做优化,在你还没有绝对清晰的未优化方案之前,请不要做优化。 —— M. A. Jackson

努力编写好的程序而不是快的程序;努力避免那些限制性能的设计决定;考虑你的API设计决定的性能后果。

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

命名惯例分为字面惯例与语法惯例。
下表示字面惯例的例子

标识符类型例子
com.sun.medialib, com.sun.jdi.event
类或接口Timer,TimerTask,HttpServlet
方法或域remove,ensureCapacity,getCrc
常量域VALUES,NEGATIVE_INFINITY
局部变量i,xref,houseNumber

第8章 异常

第39条:只针对不正常的条件才使用异常

遍历一个数组,有如下两种方法:

// 标准模式
for (int i = 0; i < a.length; i++)
    a[i].f();
// 滥用异常的模式
try {
    int i = 0;
    while (true) {
        a[i++].f();
    }
} catch (ArrayIndexOutOfBoundsException e) {
}

使用基于异常的模式的人企图利用Java的错误判断机制来提高性能,因为VM对每次数组访问都要检查越界情况,所以要避免越界检查操作 i < a.length。这种想法有3个错误:

  • 因为异常机制的设计初衷是用于不正常的情形,所以很少会有JVM实现试图对它们的性能做优化。异常的开销是很昂贵的。
  • 把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特定的优化。
  • 对数组进行遍历的标准模式并不会导致冗余的检查;有些现代的JVM实现会将它们优化掉。
     

异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。一个设计良好的API不应该强迫它的客户为了正常的控制流而使用异常。

第40条:对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

如果期望调用者能够恢复,对于这样的条件应该使用被检查的异常。

用运行时异常来指明程序错误。大多数的运行时异常都是表明前提为例,所谓前提为例是指API的客户没有遵守API规范建立的约定(数组越界)。

错误(Error)往往被JVM保留用于只是资源不足、约束失败,或者其他使程序无法继续执行的条件。按照惯例,你所实现的所有的未被检查的抛出结构都应该是RuntimeException的子类(直接或间接)

第41条:避免不必要地使用被检查的异常

何时使用被检查的异常:如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生了异常,使用API的程序员可以采取有用的动作。

如果一个方法抛出的被检查异常是唯一的,那么它给程序员带来的额外负担会非常高。(嵌套try-catch块)

第42条:尽量使用标准的异常

重用现有的异常有许多好处。

  • 使得你的API更加易于学习和使用,它与程序员原来已经熟悉的习惯用法是一致的。
  • 对于用到这些API的程序而言,它们的可读性更好,因为它们不会充斥着程序员不熟悉的异常。
  • 异常类越少,意味着内存占用越小,并且装载这些类的时间开销也越小。

常用异常如下

异常使用场合
IllegalArgumentException参数的值不合适
IllegalStateException对于这个方法调用而言,对象状态不合适
NullPointerException在null被禁止的情况下参数值为null
IndexOutOfBoundsException下标越界
CouncurrentModificationException在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException对象不支持客户请求的方法

第43条:抛出的异常要适合于相应的抽象

高层的实现应该捕获低层的异常,同时抛出一个可以按照高层抽象进行解释的异常。这种做法成为异常转译(exception Translation),如下所示

try {
    // do sth.
} catch(LowerLevelException e) {
    throw new HigherLevelException(...);
}

一种特殊形式的异常转译被称为异常链接(exception chaining),如果低层的异常对于调试该异常被抛出的情形非常有帮助,那么使用异常链接时很合适的。

try {
    // do sth.
} catch(LowerLevelException e) {
    throw new HigherLevelException(e);
}

自JDK 1.4起,异常链接可通过Throwable来获得支持,主要让高层异常的构造函数链接到Throwable(Throwable)即可:

HigherLevelException(Throwable t) {
    super(t);
}

尽管异常转译比不加选择地传递低层异常的做法有所改进,但是它也不能被滥用。

第44条:每个方法抛出的异常都要有文档

总是要单独地声明被检查的异常,并且利用Javadoc的@throws标记,准确地记录下每个异常被抛出的条件。

使用Javadoc的@throws标签记录一个方法可能会抛出的每个未被检查的异常,但是不要使用throws关键字将未被检查的异常包含在方法的声明中。

如果一个类中许多方法处于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的。

第45条:在细节消息中包含失败 - 捕获信息

为了捕获失败,一个异常的字符串表示应该包含所有“对该异常有贡献”的参数和域的值。例如,IndexOutOfBoundsException异常的细节消息应该包含下界、上界,以及没有落在其中的实际下标值。

为了确保在异常的字符串表示中包含足够的失败 - 捕获信息,一种办法是在异常的构造函数中以参数形式引入这些信息。例如,IndexOutOfBoundsException异常有一个这样的构造函数:

public IndexOutOfBoundsException(int lowerBound, int upperBound,
        int index ) {
    // Generate a detail message that captures the failure
    super( "Lower bound: " + lowerBound +
         ", Upper bound: " + upperBound +
               ", Index: " + index );
}

第46条:努力使失败保持原子性

一般而言,一个失败的方法调用应该使对象保持“它在被调用之前的状态”。具有这种属性的方法被称为具有失败原子性(failure atomic)。有几种途径可以获得这种效果:

  1. 最简单的办法是设计一个非可变的对象。
  2. 对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改之前。
  3. 编写一段恢复代码,由它来解释操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。
  4. 在对象的一份拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。

错误(相对于异常)通常是不可恢复的,当一个方法抛出错误时,它不需要保持失败原子性。即使在可以实现失败原子性的场合,它也并不总是所期望的。

规则:作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态。如果这条规则被违反,则API文档应该清楚地指明对象将会处于什么样的状态。

第47条:不要忽略异常

要忽略一个异常很简单:

try {
    // do sth.
} catch (Exception e) {
}

空catch块会使异常达不到应有的目的。只要catch块也应该包含一条说明,用来解释为什么忽略掉这个异常是合适的。

一个可以忽略异常的例子是动画中的多帧图像连续播放。

第9章 线程

第48条:对共享可变数据的同步访问

Java语言保证读或者写一个变量是原子的,除非这个变量的类型为long或double。也就是说,读入一个非long或double类型的变量,可以保证返回的值一定是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改这个变量,也是如此。

你可能听说过,为了提高性能,在读或写原子数据的时候,你应该避免使用同步。这个建议是非常危险而错误的。原子性不保证一个线程写入的值对另一个线程将是可见的:为了在线程之间可靠地通信,同步是必须的。

第49条:避免过多的同步

为了避免死锁,在一个被同步的方法或者代码块中,永远不要放弃对客户的控制。换句话说,在一个被同步的区域,不要调用一个可被改写的公有或受保护的方法。

通常,在同步区域内你应该做尽可能少的工作。

第50条:永远不要在循环的外面调用wait

使用wait方法的标准模式:

synchronized (obj) {
    while (<condition does not hold>)
        obj.wait();

    ... // perform action appropriate to condition
}

总是在一个while循环中调用wait,并且使用标准的模式,以避免在公有可访问的对象中的意外或恶意的通知。一般情况下,你应该使用notifyAll优先于notify,以避免来自不相关的意外或恶意的等待,但是会影响性能。如果使用notify,必须谨慎,以确保程序的活性。

第51条:不要依赖于线程调度器

任何依赖于线程调度器而达到正确性或性能要求的程序,很有可能是不可移植的。编写健壮的、相应良好的、可移植的多线程应用程序的最好办法是,尽可能确保在任何给定的时刻只有少量的可运行程序。

如果一个程序因为某些线程无法像其他的线程那样获得足够的CPU时间,而不能工作,不要企图通过调用Thread.yield来“修正”该程序。(不可移植)线程优先级是Java平台最不可移植的特征了。对大多数程序员来说,Thread.yield的唯一用途是在测试期间人为地增加一个程序的并发性。

第52条:线程安全性的文档化

在一个方法的声明中出现synchronize修饰符,这是一个实现细节,并不是导出的API的一部分。一个类为了可被多个线程安全地使用,必须在文档中清除地说明它所支持的线程安全性级别。一个类可能支持的线程安全性级别如下:

  • 非可变的(immutable)——这个类的实例对于其客户而言是不变的。所以不需要外部的同步。
  • 线程安全的(thread-safe)——这个类的实例是可变的,但是所有的方法都包含足够的同步手段,所以这些实例可以被并发使用,无需外部同步。
  • 有条件的线程安全(conditionally thread-safe)——这个类包含某些方法,它们必须被顺序调用,而不能受到其他线程的干扰,除此之外,这种线程安全级别与上一种(线程安全)情形相同。为了消除被其他线程干扰的可能性,客户在执行此方法序列期间,必须获得一把适当的锁。
  • 线程兼容(thread-compatible)——在每个方法调用的外围使用外部同步,此时这个类的实例可以被安全地并发使用。
  • 线程对立(thread-hostile)——这个类不能安全地被多个线程并发使用,即使所有方法调用都被外部同步包围。
     

第53条:避免使用线程组

线程组基本已经过时了。

第10章 序列化

第54条:谨慎地实现Serializable

实现Serializable而付出的最大代价是,一旦一个类被发布,则“改变这个类实现”的灵活性将大大降低。

实现Serializable的第二个代价是,它增加了错误(bug)和安全漏洞的可能性。

实现Serializable的第三个代价是,随着一个类的新版本的发行,相关的测试负担增加了。

为了继承而设计的类应该很少实现Serializable,接口也应该很少会扩展它。如果一个类专门为了继承而设计的类不是可序列化的,那么要编写出可序列化的子类几乎不可能。特别地,如果超类没有提供一个可访问的、无参数的构造函数的话,那么子类要做到可序列化是不可能的。因此,对于为继承而设计的不可序列化的类,你应该考虑提供一个无参数的构造函数。

第55条:考虑使用自定义的序列化形式

若没有认真考虑默认序列化形式是否合适,则不要接受这种形式。接受默认的序列化形式是一个非常重要的决定,你需要从灵活性、性能和正确性多个角度对这种编码格式进行考察。

如果当一个对象的物理表示等同于它的逻辑内容,则默认的序列化形式可能是合适的。如下表示人名的类:

public class Name implements Serializable {
    private String lastName;

    private String firstName;

    private char middleInitial;

    ...// remainder omitted
}

即使你确定了默认序列化是合适的,通常你仍然要提供一个readObject方法以保证约束关系和安全性。

当一个对象的物理表示与它的逻辑数据内容有实质性的区别时,使用默认序列化形式有4个缺点:

  • 它使这个类的导出API永久地束缚在该类的内部表示上。
  • 它要消耗过多的空间。
  • 它要消耗过多的时间。
  • 它会引起栈溢出。

第56条:保护性地编写readObject方法

readObject方法实际上相当于另一个公有的构造函数!一个构造函数必须检查它的实参的有效性,并且必要的时候对参数进行保护性拷贝,因此readObject也需要这样做。

下面以摘要的形式给出一些为编写出更加健壮的readObject方法而应该遵循的指导原则:

  • 对于对象引用域必须保持为私有的类,对“将被保存到这些域中的对象”进行保护性拷贝。非可变类的可变组件就属于这一类别。
  • 对于具有约束条件的类,一定要检查约束条件是否满足,如果不满足,则抛出异常。这些检查动作应该跟在所有保护性拷贝之后。
  • 如果在对象图被反序列化之后,整个对象图必须都是有效的,则应该使用ObjectInputValidation接口。
  • 无论直接方式还是间接方式,都不要调用类中可被改写的方法。

第57条:必要时提供一个readResolve方法

如果单实例类要实现Serializable接口,则readResolve方法可以保证其单例属性:

private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
}

readResolve方法不仅仅对于singleton对象是必要的,而且对于所有其他的实例受控的类也是必需的。凭经验,如果你正在编写的可序列化的类没有包含公有的或者受保护的构造函数,那么请考虑它是否需要一个readResolve方法。

readResolve方法的第二个用法是作为保护性的readObject方法的一种保守的替代选择。
 
  
   

注:所有内容摘录与修改自《Effective Java》,Joshua Bloch著,潘爱民译。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值