《Effective Java》读书笔记

来源:互联网 发布:centos 6.5 安装nfs 编辑:程序博客网 时间:2024/06/15 10:52

第2章 创建和销毁对象

 

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

所谓静态工厂方法,实际上只是一个简单的静态方法,它返回的是类的一个实例。

静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字。如果一个类看起来需要多个构造函数,并且它们的原型特征相同,那么你应该考虑用静态工厂方法来代替其中一个或者多个构造函数,并且慎重选择它们的名字以便明显地标示出它们的不同。

静态工厂方法的第二个好处是,与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象。这使得一些非可变类可以使用一个预先构造好的实例或者把已经构造好的实例缓存起来,以后再把这些实例分发给客户,从而避免创建不必要的重复对象。例如

   publicstatic final Boolean TRUE  =new Boolean(true);

    publicstatic final Boolean FALSE =new Boolean(false);

 

    public static Boolean valueOf(boolean b) {

        return (b ?TRUE : FALSE);

    }

静态工厂方法的第三个好处是,与构造函数不同,它们可以返回一个原返回类型的子类型的对象。这种灵活性的一个应用是,一个API可以返回一个对象,同时又不使该对象的类成为公有的。以这种方式把具体的实现类隐藏起来,可以得到一个非常简洁的API。这项技术非常适合于基于接口的框架结构,因为在这样的框架结构中,接口成为静态工厂方法的自然返回类型。例如,CollectionsFramework有20个使用的集合接口实现,分别提供了不可修改的集合、同步集合等等。这些实现绝大多数都是通过一个不可实例化的类(java.util.Collections)中的静态工厂方法而被导出的,所有返回对象的类都不是公有的。代码如下

List<String> list = Collections.synchronizedList(newLinkedList<String>());

通过静态工厂方法synchronizedList导出了非公有类SynchronizedList,好好理解其中的精髓!

 

静态工厂方法的主要缺点是,类如果不含有公有的或者受保护的构造函数,就不能被子类化。然而这也许因祸得福,因为它会鼓励程序员使用复合结构,而不是继承。

静态工厂方法的第二个缺点是,它们与其他的静态方法没有任何区别。

 

总的来说,静态工厂方法和公有的构造函数都有它们各自的用途,我们需要理解它们各自的长处。要避免一上来就提供构造函数,而不考虑静态工厂,因为静态工厂通常更加合适。如果你正在权衡这两种选择,又没有其他因素强烈地影响你的选择,那么你最好还是简单地使用构造函数,毕竟它是语言提供的规范。

 

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

简单,略

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

搞清楚究竟咋回事?

 

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

偶尔情况下,你可能会编写出只包含静态方法和静态域的类。此时实例化就没有任何意义了。

企图通过将一个类做成抽象类来强制该类不可被实例化是行不通的,该类可以被子类化,在子类被实例化的时候,它就被实例化了。通过将构造函数私有化,那它就不可被外部实例化了。当然,它也不能被子类化了。

例:

public class Collections {

    // Suppresses default constructor, ensuring non-instantiability.

    private Collections() {

    }

 

public class Arrays {

    // Suppresses default constructor, ensuring non-instantiability.

    private Arrays() {

    }

 

public final class Math {

 

    /**

     * Don't let anyone instantiate this class.

     */

    private Math() {}

 

 

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

如果一个对象是非可变的,那么它总是可以被重用。

最典型的一个例子是:

String s = new String("silly"); // DON'TDO THIS

传递给String构造函数的实参"silly"本身就是一个String实例。(和C++中差异最大的地方)

一个改进版本如下所示:

String s = "silly";

它可以保证对于所有在同一个虚拟机中运行的代码,只要它们包含相同的字符串字面常量,则该对象就会被重用。

对于同时提供了静态工厂方法和构造函数的非可变类,你通常可以利用静态工厂方法而不是构造函数,以避免创建重复对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造函数Boolean(String)。

public static Boolean valueOf(String s) {

   returntoBoolean(s) ? TRUE : FALSE;

    }

不要错误地认为创建对象是昂贵的,通过维护自己的对象池来避免对象的创建工作并不是一个好的方法,除非池中的对象是非常重量级的(比如数据库连接池)。

 

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

书上指的内存泄露就跟C++中的vector实现一样,只会变大,不会变小,C++中假如保存的是对象,影响还挺大,如果是指针,基本就没多大影响了。但在Java中,显然保存的都是引用,要是这些引用未设置null,所有的对象相当于还在,应该避免。清空引用的另一个好处是,如果它们在以后又被错误地使用,则程序立即抛出异常。

消除过期引用最好的方法是重用一个已经包含对象引用的变量,或者让这个变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,则这种情形就会自然而然地发生。应该注意到,在目前的JVM实现平台上,仅仅退出定义变量的代码块是不够的,要想使引用消失,必须退出包含该变量的方法。(和C++差异较大,容易犯错)

内存泄露最常见的情况就是你把一个对象的引用放到了一个C++中vector类似的容器中,因为你删除元素的时候,其实这个元素还在。

理解WeakHashMap及弱引用,强引用?

 

通过查看Java中Vector等容器的代码,我们可以发现,所有的容器在删除元素时都把对应的引用置空了的,以便垃圾回收期回收内存。如下

elementData[elementCount] = null;/* to letgc do its work */

我就说嘛,Java设计者不会那么笨,那么容易让我们搞出内存泄露,换句话说,我们可以放心使用Java中的容器,不会出现内存泄露的。只要我们自己实现容器或缓存的时候注意一下将引用置空就行了。

 

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

终结函数通常是不可预测的,常常也是很危险的,一般情况下是不必要的。

参考Object类中的finalize()的注释,这个函数被调用跟对象被垃圾回收器回收是类似的:

即它不能保证会被及时地执行,这意味着,时间关键任务不应该由终结函数来完成(比如关闭文件)。

不仅不保证终结函数会被及时地执行,而且根本就不保证它们会被执行,当一个程序终止的时候,这是完全有可能的。我们不应该依赖一个终止函数来更新关键性的永久状态(比如释放一个共享资源(比如数据库)上的永久锁)。

那么,如果一个类对资源(例如文件或线程)确实需要回收,我们该怎么办呢?只需要提供一个显式的终止方法,并要求该类的客户在每个实例不再有用时调用这个方法。一个值得提及的细节是,该实例必须记录下自己是否已经被终止了:显式的终止方法必须在一个私有域中记录下“该对象已经不再有效了”,其他的方法必须检查这个域,如果在对象已经被终止后,这些方法被调用的话,那么它们应该抛出IllegalStateException异常。常见的方法名有cancel,close,dipose等。

但try-finally我们还是可以用的,这样可以保证即使try中有异常抛出,该finally也会被执行。

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

 

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

尽管Object是一个具体类,但是设计它主要是为了扩展。它的所有非final方法都有明确的通用约定,因为它们都是为了要被改写而设计的。任何一个类,它在改写这些方法的时候,有责任遵守这些通用约定;如果做不到这一点,则其他一些依赖于这些约定的类就无法与这些类结合在一起正常运作。

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

改写equals方法看起来非常简单,但是有许多改写的方式会导致错误,并且后果非常严重。要避免问题做容易的办法是不改写equals方法,在这种情况下,每个实例只与它自己相比。从代码可以看出:

    public boolean equals(Object obj) {

    return (this == obj);        // 即默认只比是不是同一个对象实例

    }

但是,当我们想比较的是两个对象中的值是否相等时,就得重写了,比如Integer

    public boolean equals(Object obj) {

    if (obj instanceof Integer) {

        return value == ((Integer)obj).intValue();     // 如果先判断一下是不是同一个实例有可能能提高性能 

    }

    return false;

    }

比较的方法大概就像上面那样。

下面是最后的一些告诫:

当你改写equals的时候,总是要改写hashCode。

不要企图让equals方法过于聪明。如果只是简单地测试域中的值是否相等,则不难做到遵守equals约定。

不要使equals方法依赖于不可靠的资源。比如访问网络

不要将equals声明中的Object对象替换为其他的类型。这样实际上没有override,而是overload。通过@override可以解决。

 

equals默认测试是不是同一个实例,但有可能是比对象的值,而==无论怎么都是测试是不是同一个实例。所以,只要我们想比的是值,那么就用equals,比如str.equals("abc");这跟C++中的CString有很大区别。在JavaAPI中,一般我们想要比值的类型,它们应该都是实现了equals的,比如Integer,String,Vector,我们大可放心使用。

 

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

首先要搞清楚哈希码用来干嘛?

答:因为HashMap、HashSet、Hashtable这些集合类的正常运作是依赖于对象的哈希码的。

 

Object中的hashCode是怎么产生的?

答:Thisis typically implemented by converting the internal address of the object intoan integer

 

因没有改写hashCode而违反的关键约定是第二条:相等的对象必须具有相等的散列码。

 

假如两个对象equals相等,但有不同的hashCode会有什么问题?

答:假如我有两个电话号码实例,实际上是同一个号码,equals已经改写为相等了。我想将电话存入HashSet中,但我可不想保存多个重复号码的实例,此时要是哈希码不同的话,就都保存进去了,显然违背了我的期望。

 

对于如何计算哈希码,可以参考一下Java中的那些类,比如Integer,Float,String等

 

第9条: 总是要改写toString

注意一下Objectzhong的toString是如何实现的?

    public String toString() {

    return getClass().getName()+"@" + Integer.toHexString(hashCode());

    }

提供一个好的toString实现,可以使一个类用起来更加愉快。(println,字符串连接+,assert等)

在实际应用中,toString应该返回对象中包含的所有令人感兴趣的信息。(注意加上格式的注释)

 

到目前为止,我开始越来越喜欢Java了,从架构、设计上来说,它确实比C++要好,并且Java弱化了文档的作用,更加强化了代码中的注释的作用,也就是说,只要你看代码,就什么都明白了。这本书上的几乎所有的知识点,在Java的代码中都有,就看你能否发现,好多实现你都可以参考代码解决,所以,如果要想提升Java水平,看代码是最快捷的方式。

 

第10条: 谨慎地改写clone

尽管clone存在这样那样的缺陷,这项设施仍然被广泛地使用着。为什么呢?

答:

......由此得到一种语言本身之外的机制:无须调用构造函数就可以创建一个对象。

简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法改写clone,并依次调用super.clone,直到Object的clone方法,而Object的clone方法实现了对象的按位拷贝(注意是整个子类对象而不仅仅是Object,已验证),然后修正任何需要修正的域。

如果你扩展了一个实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。否则的话,最好的做法是,提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的能力。

另一个实现对象拷贝的好办法是提供一个拷贝构造函数。

另一种方法是它的一个微小变形:提供一个静态工厂来代替构造函数。

拷贝构造函数的做法,以及它的静态工厂方法变形,比Cloneable/clone方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未良好文档化的规范;它们不会与final域的正常使用发生冲突;它们不会要求客户捕获不必要的被检查异常。

......由于它具有这么多的缺点,有些专家级的程序员从来不去改写clone方法,也从来不去调用它,除非是为低开销地拷贝一个数组。

看看Vector是如何实现clone的?是不是和我们上面讲的一致,呵呵

    public synchronized Object clone() {

    try {

       Vector<E> v = (Vector<E>) super.clone();   // 仅仅复制了成员变量

        v.elementData = Arrays.copyOf(elementData,elementCount);     // 复制成员变量指向的数据

        v.modCount = 0;

        return v;

    } catch (CloneNotSupportedException e) {

        // this shouldn't happen, since we are Cloneable

        throw new InternalError();

    }

    }

看起来确实和上面的一致。

为什么一定得实现Cloneable接口才允许调用clone,虽然Cloneable中什么都没有?难道Object中的本地方法clone还具有检查当前类是否实现了Cloneable接口的功能?要是有,这部分检查代码大致是如何实现呢?

答:通过将一个类中的   publicnative Object add() throwsCloneNotSupportedException;生成对应的C++头文件(注意,需要add被调用后才能通过javah正常生成头文件,否则会报错),

JNIEXPORT jobject JNICALL Java_MyInteger_add

  (JNIEnv *,jobject);

并且发现,不管声明中是否抛异常,生成的C++头文件都是一样的。也就是说,在add和Java_MyInteger_add之间,JVM还应该做了一部分事情,比如抛异常工作,显然不可能C++中的异常直接抛到Java中,还有就是参数之间的转换和函数调用等。对于检查是否实现了某个接口的工作,完全可以在这部分中搞定,如果发现不匹配,直接抛出异常。假如我是JVM的设计者的话,我应该不会把Java中的类以及类继承了那个类,实现了哪些接口带进C++中去,C++中做的事情应该都是简单的、低耦合的工作才对。要是检查是否实现了某个接口是在C++中实现的话,那么这些信息一定是通过参数JNIEnv*, jobject带进去,要是后者不成立的话,那么前者肯定就是对的,从功能集中性来说,即clone当然要包括检查是否实现了Cloneable接口,其实把检查放到C++中也未尝不可。如何证明呢?

 

看来要了解JVM的底层,对JNI得非常熟才行,JNI还是很有用的,说白了,不管怎样,要使用操作系统的功能,你就得通过JNI调用动态链接库。

 

第11条:考虑实现Comparable接口

就像STL中存入map,set的元素都得重载<或>运算符一样,Java中为了实现排序(TreeMap,TreeSet默认就是排序的,所以它们也依赖于compareTo),也得实现对应的比较函数compareTo。

由此推理,如果一个类未实现compareTo,那么就不能放入TreeSet中。(经验证,编译时可以通过,但运行时就会抛异常)

为啥Integer,String可以自由地加入TreeSet中,那是因为它们都实现了Comparable接口的。

 

第4章 类和接口

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

如果没有指定任何访问修饰符,那么就是包级私有的。

 

第13条:支持非可变性

一个非可变类是一个简单的类,它的实例不能被修改。每个实例中包含的所有信息都必须在该实例被创建的时候就提供出来,并且在对象的整个生命期内固定不变。Java平台库包含许多非可变类,其中有String、原始类型的包装类、BigInteger和BigDecimal。非可变类的存在有许多理由:非可变类比可变类更加易于设计、实现和使用。它们不容易出错,更加安全。

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

1.不要提供任何会修改对象的方法

2.保证没有可被子类改写的方法

3.使所有的域都是final的

4.使所有的域都成为私有的

5.保证对于任何可变组件的互斥访问

总而言之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让一个类成为可变类,否则就应该是非可变的。

看看String,Integer等类是否符合上面的规则?

 

第14条:复合优先于继承

总而言之,继承机制的功能非常强大,但是它存在诸多问题,因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类在不同的包中,并且超类并不是为了扩展而设计的,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在一个适当的接口来实现一个包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

 

脆弱性是指如果超类修改则会引起子类的变动,而复合则不一定。

 

 

 

第5章 C语言结构的替代

第19条:用类代替结构

 

第20条:用类层次来代替联合

 

第21条:用类代替enum结构

// The typesafe enum pattern

public class Suit{

   privatefinal String name;

   privateSuit(String name){this.name = name;}

   publicString toString(){return name;}

   publicstatic final Suit CLUBS   = newSuit("clubs");

   publicstatic final Suit DIAMOND = new Suit("diamonds");

   publicstatic final Suit HEARTS  = newSuit("hearts");

   publicstatic final Suit SPADES  = newSuit("spades");

}

顾名思义,类型安全枚举模式提供了编译时的类型安全性。如果你声明了一个方法,它的一个参数为Suit类型,则可以保证,任何传入的非null的对象引用一定表示了这四种纸牌花色(suit)之一。

缺点:

1.无法按位或(|)枚举值

2.无法switch,只能用if-else代替

3.性能差一些

在要求使用一个枚举类型的环境下,我们首先应该考虑类型安全枚举模式。与int枚举类型相比,使用类型安全枚举类型的API对于程序员来说要友好得多,在Java平台API中,类型安全枚举类型没有被大力使用的唯一原因在于,在编写大多数这些API的时候,这种类型安全枚举模式尚未被人知晓。

 

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

简而言之,C语言中函数指针的主要用途是实现Strategy(策略)模式。为了在Java程序设计语言中实现这样模式,声明一个接口来实现该策略,并且为每个具体策略声明一个实现了该接口的类。如果一个具体策略只被使用一次,那么通常使用匿名类来声明和实例化这个具体策略类。如果一个具体策略需要被导出去以便重复使用,那么它的类通常是一个私有的静态成员类,并且通过一个公有静态final域被导出,其类型为该策略接口。

 

第6章 方法

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

 

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

因为类中维护的成员变量都是对象的引用,所有在构造对象或返回成员变量时,需要利用拷贝构造函数或clone才能完全和客户断绝关系。

例如

Date start = new Date();

Date end   =new Date();

Period p = new Period(start, end);

end.setYear(78); // 结果把p都给改了

 

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

谨慎选择方法的名字

不要过于追求便利的方法:只有当一个操作被使用得非常频繁的时候,才考虑为它提供一个快捷方法。如果不能确定的话,还是不考虑的为好。

避免长长的参数列表

对于参数类型,优先使用接口而不是类

谨慎地使用函数对象

 

第26条:谨慎地使用重载

 

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

简而言之,没有理由从一个取数组值的方法中返回null,而不是返回一个零长度数组。这种习惯做法(指返回null)很有可能是从C程序设计语言中沿袭过来的,在C语言中,数组长度在被返回的时候,是与实际的数组分离的。并且,在C语言中,如果返回数组长度为零的话,在分配一个数组是没有任何好处的。

 

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

为了正确地编写API文档,你必须在每一个被导出的类,接口,构造函数,方法和域声明之前增加一个文档注释。

每一个方法的文档注释应该简洁地描述出它和客户之间的约定。

总而言之,要为API编写文档,文档注释是最好的,最有效的途径。对于所有可导出的API元素来说,使用文档注释应该是强制性的。

 

参考JDK

 

第7章 通用程序设计

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

 

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

 

第30章:了解和使用库

总而言之,不要从头发明轮子。

 

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

lue

 

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

总而言之,如果可以使用更加合适的数据类型,或者可以编写更加适合的数据类型。那么应该避免使用字符串来表示对象,若使用不当,则字符串比其他类型更加笨拙、缺乏灵活性、速度缓慢,更加容易出错。通常被错误地用字符串来代替的类型包括原语类型、枚举类型和聚集类型。

 

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

原则很简单:不要使用字符串连接操作符合并多个字符串,除非性能无关紧要。相反,应该使用StringBuffer的append方法,或者采用其他的方法,比如使用字符串数组,或者每次只处理一个字符串,而不是将它们组合起来。

 

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

// Good-uses interface as type

List subscribers = new Vector();

 

// Bad uses class as type

Vector subscribers = new Vector();

 

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

如果没有合适的接口存在的话,那么,用类而不是接口来引用一个对象,是完全合适的。比如String,记住,值类很少有多个实现。

不存在合适的接口类类型的第二种情况是,对象属于一个框架,而框架的基本类型是类,不是接口。比如java.util.TimerTask。

实际上,一个给定的对象是否具有合适的接口应该是很显然的。如果是的话,那么,使用接口来引用对象会使程序更加灵活;如果不是,则使用类层次结构中提供了所需功能的最高层的类。

 

第35条:接口优先于映像机制

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

 

你损失了编译时类型检查的好处。

 

要求执行映像访问的代码非常笨拙和冗长。

 

性能损失。

 

简而言之,映像机制是一项功能强大的设施。对于一些特定的复杂程序设计任务它是非常必要的,但它也有一些缺点。如果你编写的程序必须要与编译时刻未知的类一起工作,那么,有可能的话,仅仅使用映像机制实例化对象,而访问对象时使用编译时刻已知的某个接口或者超类。

 

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

简而言之,在使用本地方法之前仔细考虑。很少情况下需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层的资源,或者遗留代码库,那么尽可能少用本地代码,并且全面进行测试。本地代码中的一个错误可以破坏整个应用程序。

 

第37条:谨慎地进行优化

 

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

包的名字应该是层次状的(反Internet域名),标准库和一些可选的库,其名字以java和javax作为开头,它们是这条guize的例外。

 

第8章 异常

充分发挥异常的优点,可以提高一个程序的可读性、可靠性和可维护性。如果使用不当的话,它们也会带来负面影响。

 

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

创建,抛出和捕获异常的开销是很昂贵的,把代码放在try-catch块中反而阻止了现代JVM实现本来要执行的某些特定优化。

 

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

通过抛出一个被检查的异常,你强迫调用者在一个catch子句中处理该异常,或者将它传播到外面。对于一个方法声明要抛出的每一个被检查的异常,它是对API用户的一种潜在指示:与异常相关联的条件是调用这个方法的一种可能结果。

有两种未被检查的可抛出结构:运行时异常和错误。在行为上两者是等同的:它们都是不需要,也不应该被捕获的抛出物。如果一个程序抛出一个未被检查的异常,或者一个错误,则往往是不可恢复的情形,继续执行下去有害无益。如果一个程序没有捕捉这样的可抛出结构,则将会导致当前线程停止,并伴以一个适当的错误消息。

 

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

被检查的异常是Java程序设计语言的一个很好的特性。与返回代码不同,它们强迫程序员处理例外的条件,大大提高了可靠性。然而,过分使用被检查的异常会使API用起来非常不方便。如果一个方法会抛出一个或者多个被检查的异常,那么调用该方法的代码必须在一个或者多个catch块中处理这些异常,或者它必须声明这些异常,以便让它们传播出去。无论哪种方法,都给程序员增添了不可忽视的负担。

如果一个方法只抛出一个被检查的异常,那么仅仅为了这个异常,该方法必须放置于try块中。在这样的情形下,你应该问自己,是否可以有别的途径来避免使用被检查的异常。

 

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

 

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

如果无法阻止来自底层的异常,那么,其次的做法是,让高层来处理这些异常,从而将高层方法的调用者与底层的问题隔离开,在这种情况下,用某种适当的记录设置(比如1.4发行版本中引入的java.util.logging)将底层的异常记录下来可能是很合适的。这使得管理员可以调查问题,同时将客户代码和最终用户与问题隔离开。

 

如果既不能阻止来自底层的异常,也无法将它们与高层隔离开,那么,一般的做法是使用异常转译。只有在底层方法的规范碰巧可以保证“它所抛出的异常对于高层也是合适的”情况下,才可以将异常从底层传播到高层。

 

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

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

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

 

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

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

异常的字符串表示不应该与“针对用户层次的错误消息”混为一谈,后者对于最终用户而言必须是可理解的。与用户层次的错误消息不同,异常的字符串表示主要是针对程序员或者域服务人员的,用于分析失败的原因。因此,对它来说,内容比可理解性重要得多。

 

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

一般而言,一个失败的方法调用应该使对象保持“它在被调用之前的状态”。具有这种属性的方法被成为具有失败原子性。

办法:

最简单的办法莫过于设计一个非可变的对象,在执行操作之前检查参数的有效性。

编写一般性恢复代码

在对象的一份临时拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。

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

 

第47条:不要忽略异常

即不要搞

// Empty catch block ignores exception-Highlysuspect!

try{

   ...

} catch(SomeException e){

}

至少catch块也应该包含一条说明,用来解释为什么忽略掉这个异常是合适的。

正确地处理异常能够避免无可挽回的失败。简单地将一个未被检查的异常传播给外界至少会使程序迅速地失败,从而保留了有助于调试该失败条件的信息。

 

我有时忽略异常发生了但不知道该如何处理,比如关闭文件失败?关闭socket失败?

 

第9章 线程

 

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

Java语言保证读或者写一个变量是原子的(atomic),除非这个变量的类型是long或double。

注意++不是原子操作,它和C++中的一样,实际上是先将变量读到寄存器,然后给寄存器加1,最后再将寄存器中的值写到变量。

对于《JavaThreadProgramming》中讲到的volatile问题,这儿说除非是在一台多处理器的机器上运行,否则在实践中不可能观察到对于的错误行为。

究竟怎样才能看到?

答:

 

......

在这样的情况下,按需初始化容器类(initialize-on-demand holder class)模式是非常合适的。下面的代码演示了这种模式:

// The initialize-on-demand holder class idiom

private static class FooHolder{

   staticfinal Foo foo = new Foo();

}

public static Foo getFoo{

   returnFooHolder.foo;

}

该模式充分利用了Java语言中“只有当一个类被用到的时候才被初始化”。

 

在某些特定的条件下,使用volatile修饰符可以提供另一种不同于普通同步机制的选择,但这是一项高级的技术,而且,由于当前正在进行之中的内存模型尚未完成,所以,这项技术的使用范围还不得而知。

真的是这样吗?

 

第49条:避免过多的同步

过多的同步可能会导致性能降低,死锁,甚至不确定的行为。

略,所有多线程程序考虑的东西都差不多。

 

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

略,和《JavaThreadProgramming》中描述的一致。

简而言之,总是在一个while循环中调用wait,并且使用标准的模式。你没有理由不这样做。一般情况下,你应该使用notifyAll优先于notify。然而,在有些情况下这样做会导致实质性的性能负担。如果使用notify,请一定小心,以确保程序的活性(liveness)。

 

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

任何依赖于线程调度器而达到正确性或性能要求的程序,很有可能是不可移植的。

不要企图通过调用Thread.yield来“修正”该程序。

线程优先级是Java平台上最不可移植的特征了。

对于大多数程序员来说,Thread.yield的唯一用途是在测试期间人为地增加一个程序的并发性。

 

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

.....

而且,“出现了synchronized关键字就足以将线程安全性文档化了”这种说法隐含了一个错误的观念,即认为线程安全性是一种“要么全有要么全无”的属性,实际上,一个类支持的线程安全性有很多级别。一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全级别。

下面的列表概括了一个类可能支持的线程安全性级别。这份列表并没有涵盖所有的可能,而只是常见的情形。列表中使用的名字不是标准的,因为在这个领域中还没有被广泛接受的惯例:

非可变的(immutable) 这个类的实例对于其客户而言是不变的。所以,不需要外部的同步。这样的例子包括String、Integer和BigInteger。

线程安全的(thread-safe) 这个类的实例是可变的,但是所有的方法都包含足够的同步手段,所以,这些实例可以被并发使用,无需外部同步。包括Random和java.util.Timer。

有条件的线程安全(conditionallythread-safe)这个类(或者关联的类)包含有某些方法,它们必须被顺序调用,而不能受到其他线程的干扰,除此之外,这种线程安全级别与上一种情形相同。为了消除被其他线程干扰的可能性,客户在执行此方法序列期间,必须获得一把适当的锁。这样的例子包括Hashtable和Vector,它们的迭代器(iterator)要求外部同步。

线程兼容的(thread-compatible) 在每个方法调用(有些情况下,在每个方法调用序列)的外围使用外部同步,此时这个类的实例可以被安全地并发使用。其例子包括通用的集合实现,比如Arraylist和HashMap。

线程对立的(thread-hostile) 这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。幸运的是,在Java平台库中,线程对立的类或方法非常少。System.runFinalizersOnExit方法是线程对立的,但已经被废弃了。

简而言之,每一个类都应该清楚地在文档中说明它的线程安全属性。要做到这一点,唯一的办法是提供字句确凿的描述。synchronized修饰符并不能成为一个类的线程安全性文档。然而,对于有条件的线程安全类,在文档中指明“为了允许方法调用序列以原子方式执行,哪一个对象应被锁住”,这是非常重要的。一个类的线程安全性描述通常属于这个类的文档注释,但是,对于具有特殊线程安全属性的方法来说,它们应该在自己的文档注释中描述这些线程安全属性。

 

第53条:避免使用线程组

线程组的初衷是作为一种隔离applet的机制,当然是出于安全的考虑,它们并没有真正实现这个承诺,它们的安全重要性已经差到在Java2平台安全模型的核心工作中不被提及的地步。

......

获取线程组子组列表的API也有类似的缺陷。虽然通过增加新的方法,这些问题都可能被修正,但是,它们目前还没有被修正,因为没有实际的需要。线程组基本上已经过时了

总之,线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的,我们最好把线程组看做一个不成功的试验,你可以忽略掉它们,就当它们根本不存在一样。

上面提到的建议“你应该忽略线程组”有一个小小的例外。有一种小功能只有ThreadGroupAPI才有。当线程组中的一个线程抛出一个未被捕获的异常时,ThreadGroup.uncaughtException方法会自动被调用,“执行环境”使用这个方法,以便用适当的方式来响应未被捕获的异常。该方法的默认实现将栈轨迹打印到标准错误流中,你有时候可能会希望改写这个实现,以便完成特定的功能,比如将栈轨迹定位到一个由应用指定的日志中。

 

第10章 序列化

 

第54条:谨慎地实现Serializable

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

 

如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示,那么结果会导致序列化形式的不兼容。客户企图用这个类的老版本来序列化一个类,然后用新的版本来做反序列化,则结果将导致程序失败。

 

实现Serializable的第二个代价是,它增加了错误和安全漏洞的可能性。通常情况下,对象是由构造函数来创建的;序列化机制是一种语言之外的对象创建机制。

 

实现Serializable的第三个代价是,随着一个类的新版本的发行,相关的测试负担增加了。当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以“在新版本中序列化一个实例,然后在老版本中反序列化”,或者相反的过程。

 

实现Serializable接口不是一个很轻松就可以做出的决定。

从Java 1.4发行版本开始,有一种基于XML的JavaBeans永久机制,所以,对于Beans来说,实现Serializable不再是必需的了。

 

为了继承而设计的类应该很少实现Serializable,接口也应该很少会扩展它。如果违反了这条规则,则扩展这个类或者实现这个接口的程序员会背上沉重的负担。

 

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

当你在时间紧迫的情况下设计一个类时,一般合理的做法是把工作重心集中在API的设计上,努力设计出最好的API。有时候,这意味着要发行一个“用完后即丢弃”的实现,因为你知道很快你将会用一个新的版本来替换它。正常情况下,这不是一个问题,但是,如果这个类实现了Serializable,并且使用了默认的序列化形式,那么你永远也摆脱不了那个要被丢弃的实现。它将永远牵制住这个类的序列化形式。这不是一个纯理论的问题,在Java平台库中已经有几个类出现了这样的问题,比如BigInteger。

若没有认真考虑默认序列化形式是否合适,则不要接受这种形式。

 

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

 

无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化。

 

不管你选择了哪种序列化形式,你都要为自己编写的每个可序列化的类声明一个显式的序列化版本UID(serialversion UID)。这样可以消除序列版本UID成为潜在不兼容根源。而且,这样做也会带来小小的性能好处。如果没有提供显式的序列化版本UID,则需要在运行时刻通过一个开开销的计算过程产生一个序列版本UID。

 

你为UID选择什么值并不重要。实践中常用的做法是,通过在该类上运行serialver工具,你就可以得到一个这样的值,但是,如果你随意地编造一个数值,那也是可以的。如果你想为一个类生成一个新的版本,并且希望它与现有的类不兼容,那么你只需修改声明中的序列化版本UID即可。结果是,以前版本的实例经序列化之后,再做反序列化时会引发InvalidClassException而失败。

 

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

当一个对象被反序列化的时候,对于客户不应该拥有的对象引用,如果哪个域包含了这样的对象引用,则必须要做保护性拷贝,这是非常重要的。

 

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

在1.2发行版本中,序列化设施中新增加了readResolve特性。对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法就会被调用。然后,该方法返回的对象引用将被返回,它取代了新创建的对象。在这个特新挺贵的绝大多数用法中,指向新创建对象的引用不需要再被保留;实际上这个对象将不再有用,立即成为垃圾回收qi3的回收对象。

总而言之,无论是singleton,或是其他实例受控的类,你必须使用readResolve方法来保护“实例-控制的约束”。从本质上来讲,readResolve方法把readObject方法从一个事实上的公有构造函数变成一个事实上的公有静态工厂。对于那些禁止包外继承的类而言,readResolve方法作为保护性的readObject方法的一种替代选择,也是非常有用的。