【转】effective java 笔记2

from:http://blog.csdn.net/ilibaba/archive/2009/01/13/3769578.aspx

NO.7 在改写equals方法时请遵守通用约定
下列情况是不需要改写equals方法的:

1。同一个类的不同实例本质上是唯一的
2。不关心该类是否提供了逻辑相等的功能。

3。父类已经改写过equals方法,对于子类来说,继承过来的equals方法已经是最合适的了。

4。一个类是私有的或者是包可见的,且确定它的equals方法不会被调用。

对于需要改写equals方法的时候,应该遵守如下约定:

1。自反性,即x.equals(x)为true.

2。对称性,即当且仅当x.equals(y)为true时y.equals(x)也一定为true。

3。传递性,即对任意的x,y,z,如果x.equals(y)为true,并且y.equals(z)也为true,那么x.equals(z)也必须为true.

4。一致性,即对于任意的x,y,如果x,y都没有被修改的话,那么多次调用x.equals(y)要么一致地返回ture,要么一致地返回false.

5。对于非空的引用x,x.equals(null)一定要返回false.

改写equals方法时的建议:

1。用==操作符检查实参是否为指向对象的同一个引用。

2。使用instanceof检查实参是否是正确的类型。

3。在2的基础上,把实参转换成正确的类型。

4。检查实参的域与当前对象的域值是否相等。

5。编写完equals方法后,检查是否满足等价关系。
例如:

  1. public boolean equals(Object o)
  2. {
  3. if(o== this) return true;
  4. if(!(o instanceof xxxx) return false;
  5.        xxx in = (xxx)o;
  6. return ……..
  7. }

改写equals方法的告诫:

1、不要企图让equals方法做太多事。

2、不要使equals依赖不可靠的资源,否则会违背一致性。

3、不要将equals中的对象装换为其他的类型。要注意的是,不要提供这样的方法public boolean equals(MyClass o)这样是overload并不是override Object的equals方法。实参必须为Object类型,只是overload了equals方法,如果两者返回同样的结果是可以接受的,但是这种做法不值得。

总结:不用重写equals方法就尽量不要去找麻烦,确实需要改写equals方法时,遵守通用约定,因为对象会在程序中不停的传递,所以可能会导致程序运行不正常,甚至崩溃而很难找到程序崩溃的原因。总之,还是遵守约定吧!

 

NO.8 改写equals方法时必须覆盖hashCode方法
       这点必须切忌,不然在你和hash-based集合打交道的时候,错误就会出现了。关键问题在于一定要满足相等的对象必须要有相等的hashCode。如果你在PhoneNumber类中覆盖了equals方法,但是没有覆盖hashCode方法,那么当你做如下操作的时候就会出现问题了。

  1. Map m = new HashMap();
  2. m.put(new PhoneNumber(408,863,3334),”ming”)

当 你调用m.get(new PhoneNumber(408,863,3334))的时候你希望得到ming但是你却得到了null,为什么呢因为在整个过程中有两个 PhoneNumber的实例,一个是put一个是get,但是他们两个逻辑相等的实例却得到不同的hashCode那么怎么可以取得以前存入的ming 呢。

 

NO.9 总是要改写toString()方法
        在Object的toString方法返回的形式是Class的类型加上“@”加上16进制的hashcode,非常难以理解。最好在自己的类中提供toString方法更好的表述实例的信息,不然别人怎么看得明白呢。
        在实际应用中,toString方法应该返回对象中包含的所有令人感兴趣的信息。同时,最好在程序中提供一个相匹配的构造函数或者静态工厂方法,便于程序员在对象和它的字符串表示之间进行来回转换。
       在实现toString方法的时候,必须要做出是否在文档中指定返回值的格式的决定。指定格式可以被用来做为一种标准的,无二意性的表达形式,但这样也会使字符串的表示嵌入到永久数据中,如果以后改变了表达形式,则会影响到系统的代码和数据。不管你是否决定指定格式,都应该在文档中清晰的表明自己的意图。为toString返回值中所包含的信息,提供一种编程访问途径,用来获取toString方法返回字符串中的信息,可以避免程序员自己去解析字符串而导致的错误。

 

NO.10 谨慎地改写clone(clone方法详解请参见java clone方法使用详解

      一个对象要想被Clone,那么要implements Cloneable接口,实现clone()方法,如果你不实现这个接口的话,调用clone方法的 时候会出现CloneNotSupportedException,这就是作者叫做mixin的接口类型。通常clone()方法可以这样覆盖

  1. public Object clone() 
  2. try
  3. return super.clone(); 
  4. catch(CloneNotSupportedException e) 
  5. {} 

但是当你要clone的类里面含有可修改的引用字段的时候,那么你一定要把整个类的蓝图进行复制,如果对你clone得到的对象进行修改的时候还会影响到原来的实例,那么这是不可取的。所以应该这样clone():

  1. public Object clone() throws CloneNotSupportedException 
  2.        Stack Result  = (Stack)super.clone(); 
  3.        Result.elements = (Object[])elements.clone(); 
  4.        Return result; 

其中elements是stack类中可修改的引用字段,注意如果elements是final的话我们就无能为力了,因为不能给他重新赋值了.其实如果不是必须的话,根本就不用它最好。

      注意深复制和浅复制的问题。为了实现深复制,实现Cloneable接口的类应该首先调用super.clone,然后修正任何需要修正的域,拷贝任何“深层结构”的可变对象。

      clone方法如果实现得不当会给系统带来隐藏的bug,如果非要使用类似的功能最好的办法是提供某些其他的途径(拷贝构造函数或者提供一个静态工厂方法来替代构造函数)来替代对象的拷贝,或者干脆不提供这样的能力。

      Cloneable有很多问题,所以安全的说,其他的接口不应该扩展(extend)这个接口,并且为了继承而设计的类也不应该实现(implement)这个接口。

 

NO.11 考虑实现Comparable接口

      compareTo方法是java.lang.Comparable接口中的唯一方法,它允许进行简单的相等比较,也允许执行顺序比较,一个类实现了comparable接口就表明他的实例具有内置的排序关系。Java平台库中所有的值类都实现了Comparable。将当前对象与指定对象进行顺序比较的时,返回负整数,0或者正整数(<、=、>)。

     compareTo方法应遵守如下限制条件:自反性、对称性、传递性和非空性的限制条件。在实现数值比较的compareTo方法时还要防止值域溢出的情况(例如i为很大的正数,j为很大的负数,使用i-j判断大小就可能溢出返回一个负数)。

     违反compareTo约定的类也会破坏其他依赖于比较关系的类,包括TreeSet,TreeMap,以及工具类Collections和Arrays。这些内部包含搜索和排序算法的类。

    编写compareTo和equals方法类似,但存在几个不同点。compareTo不需要检查实参的类型,如果类型不合适应该抛出ClssCastException,如果为null应该抛出NullPointerException;域的本身是顺序比较而不是相等比较。

 

NO.12 使类和成员的可访问能力最小化

好的模块设计应该尽最大可能封装好自己的内部信息,隐藏信息可以把模块之间的耦合程度降到最低。开发得以并行,无疑这将加快开发的速度,便于系统地维护。Java中通过访问控制符来解决这个问题。

  • public表示这个类在任何范围都可用。
  • protected表示只有子类和包内的类可以使用
  • default(private-package,即不写)表示在包内可用
  • private表示只有类内才可以用

在设计的时候应该尽可能的使每一个类或者成员不被外界所访问。如果一个类只是被另一个类使用,那么应该考虑把它设计成这个类的内部类。子类在override超类的函数时访问级别不能低于超类的,一个具体例子就是实现接口的方法时,都必须是public的,因为接口中的方法都是public的。

公有类应该尽可能少地包含共有的域,包含公有可变域的类不是线程安全的。这条规则有个例外,就是通过共有静态final域来暴露类的常量是允许的。这样的域的名字由大写字母组成,单词之间用下划线隔开。不过必须保证这些字段要么是基本数据类型,要么引用指向的对象是不可修改的。不然他们将可能被修改。例如下面的定义中data就是不合理的,其他人可以改变数组中的内容,有安全漏洞,后面两个没有问题:

  1. public class Con 
  2. public static final int[] data = {1,2,3};// it is bad
  3. public static final String hello = "world"; 
  4. public static final int i = 1; 
  5. }  

注意:非零长度的数组总是可变的,所以具有公有静态final数组域几乎总是错误的。

解决这个安全隐患的方法有两种:

①将公有方法替换为一个私有数组,以及一个公有的非可变列表

  1. private static final Type[] PRIVATE_VALUES = {...}; 
  2. public static final List VALUES = Collections.unmodifiableLis(Arrays.asList(PRIVATE_VALUES)); 

②把公有的数组替换为一个公有的方法,它返回私有数组的一份拷贝。这个方法会牺牲一点性能。

  1. private static final Type[] PRIVATE_VALUES = {...}; 
  2. public static final Type[] values(){ 
  3. return (Type[])PRIVATE_VALUES.clone(); 

总之,应该防止把任何杂散的累接口和成员变成API的一部分,除了公有静态final域的特殊情形之外,公有类不应该包含公有域,并且确保有静态final域所引用的对性是不可变的。

 

NO.13 支持非可变性

一个非可变类的实例不能被修改,每个实例中包含的所有信息都必须在该实例被创建的时候就提供出来,并且在对象的整个生存期内固定不变。例如String类,BigInteger、BigDecimal都是非可变类。存在理由:比可变类更加易于设计、实现、使用,不容易出错,所以安全。

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

①不要提供任何会修改对象的方法;

②保证没有可被子类改写的方法;

③使所有的域都是final的;

④使所有的域都成为私有的;

⑤保证对于任何可变组件的互斥访问。(如果一个类指向可变对象的域,则必须确保该类的客户无法活得指向这些对象的引用,并且永远不要用客户提供的对象引用来初始化这样的域,也不要在任何一个访问方法中返回该对象的引用);

以上规则比真正的要求强了一点,为了提高性能可以有所方式,如:保证没有一个方法能够对对象的状态产生外部可见的改变,许多非可变的类拥有一个或者多个非final的冗余域,把一个开销昂贵的计算结果缓存在这些域中。
非可变对象本质上是线程安全的,它们不要求同步。非可变对象可以被自由地共享。你不仅可以共享非可变对象,甚至也可以共享它们的内部信息。非可变对象为其他对象--无论是可变的还是不可变的--提供了大量的构件。

非可变类真正唯一的缺点是,对于每一个不同的值都要求一个单独的对象。String就是这样的。通常有个解决的办法就是提供一个帮助类来弥补,例如StringBuffer类。

使一个类成为非可变类有如下三种方法:

①将一个类声明为final类型的;

②让该类中的每一个方法都成为final,而不是让整个类是final的,这种方法的好处在于其子类不可以继承,但是可以扩展新的方法;

③把类的构造函数声明为私有的或者包级私有的,增加静态工厂方法,来代替公有的构造函数;(该方法虽然不常用,但却是最值得推荐的)
如果选择让自己的非可变类实现Serializable接口,并且包含一个或多个指向可变对象的域,那么,你必须提供一个显示的readObject或者readResolve方法。默认的readObject方法使攻击者可以从非可变类创建可变的实例。

总之,除非有好的理由,否则类应该设计成非可变的。如果一个类不能被做成非可变类,那么你仍然应该尽可能地限制它的可变性。构造函数应该创建完全初始化的对象,所有的约束关系应该在这时候建立起来,不应该把“只构造了一部分的实例”传递给其他的方法,在构造函数之外提供公有的初始方法。

 

NO.14 复合优先于继承
       实现代码重用最重要的办法就是继承,但是继承破坏了封装,导致软件的键壮性不足。如果子类继承了父类,那么它从父类继承的方法就依赖父类的实现,一旦他改变了会导致不可预测的结果。如果子类和超类在不同的包中,并且超类并不是为了扩展而设计的,那么继承会导致脆弱性。作者介绍了InstrumentedHashSet作为反例进行说明,原因就是没有明白父类的方法实现。作者给出的解决办法是通过复合来代替继承,尤其是当存在一个适当的接口来实现一个包装类的时候,用包装类和转发方法来解决问题。把想扩展的类作为本类的一个private final成员变量。把方法参数传递给这个成员变量并得到返回值。这样做的缺点是这样的类不适合回调框架。继承虽然好,我们却不应该滥用,只有我们能确定它们之间是is-a得关系的时候才使用。

 

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

对并没有文档说明的类进行继承是非常危险的,它的公有方法有可能被改变。在设计一个专门用来继承的类时必须注意以下几点(不适用于final类):

①必须精确地描述改写每个方法带来的影响,虽然这样的描述违法了文档格言“好的API文档应该描述一个方法做了什么工作,而不是描述它如何做”,但这也是继承破坏了程序的封装性而导致的。

②允许继承的类的构造函数一定不能调用可被改写的方法,无论是直接进行还是间接进行。因为超类的构造函数会在子类的构造函数之前运行,所以子类中override版本的方法将会在子类的构造函数运行之前就被调用。如下边出现的错误:

  1. public class SuperClass{ 
  2. private final Date date; 
  3. public SuperClass() { 
  4.      m();
  5.      } 
  6. public void m() { } 
  7. final class Sub extends SuperClass {
  8.     private final Date date;
  9.     public Sub () { 
  10.         date=new Date();
  11.     } 
  12.     public void m() {  //overrides SuperClass.m,invoked by constructor SuperClass()
  13.         System.out.println(date); 
  14.     } 
  15. public static void main(String[] args) {  
  16.         Sub s = new Sub();  
  17.         s.m();  
  18.     } 

在这个程序中,第一次打印会出现null,因为m被构造函数SuperClass调用的时候,Sub还没有机会初始化date域。

同样的问题会出现在clone和readObject这两个类似构造函数的方法上,所以clone和readObject都不可以调用一个可改写的方法,无论直接还是间接。

“构造函数一定不能调用可被改写的方法”的实现方式:把每个可改写的方法的代码体移到一个私有的“辅助方法”中,并且让每个可改写的方法调用他的私有辅助方法,然后用“直接调用可改写方法的私有辅助方法”来代替“可改写方法的每个自用调用”。

在为了继承而设计类的时候,不推荐实现Cloneable和Serializable接口。clone和readObject方法在行为上和构造函数相似——“一定不能调用可被改写的方法,无论是直接进行还是间接进行”,对于readObject方法,子类中改写版本的方法将子类的状态被反序列化之前运行,而对于clone方法,改写版本的方法将在子类的clone方法有机会修正被克隆对象的状态之前被运行,无论哪种情形,它们都会不可避免的导致程序失败。

实现Serializable,并且类中有readResolve或者writeReplace方法的时候,必须使它们成为protected而不是private,因为private的函数不能被子类调用。

对于既不是final类,也不是为了子类化而设计和编写文档的普通类而言,防止出现问题的最好方法是禁止子类化,方法有两种:

①把这个类声明为final的。

②把所有的构造函数变成私有的,或者包级私有的,并增加一个公有的静态工厂方法来代替构造函数。

继承的另一个替代方案就是利用包装类模式,具体参见NO.14 复合优先于继承

 

NO.16 接口优于抽象类

接口和抽象类的区别:

①抽象类允许包含某些方法的实现,而接口是不允许的;

②一个类要实现抽象类,它必须成为抽象类的一个子类,而实现接口的类只要定义了所要求的方法,并遵守通用的约定,不管这个类位于类层次的哪个地方;

接口可以构造出非层次结果的类型框架,比如一个接口可以继承多个其他的接口。还可以安全地增加一个类的功能。

当然,也可以把接口和抽象类的有点结合起来,对于你期望导出的每一个重要的接口,先提供一个抽象的骨架实现类,这样,接口的作用仍然是定义类型,骨架实现类负责所有与接口实现相关的工作。

抽象类也有明显的优势,它可以在一个类的后续的版本中方便的增加一个新的方法,但不影响到其他相关的类,而接口则做不到这一点。

总之,接口通常是定义具有多个实现类型的最佳途径(例外的情况:当演化的容易性比灵活性和功能更为重要的时候,应该使用抽象类来定义类型,但也必须理解抽象类的局限性,并确保可以接受这些局限性)。如果已经导出了一个重要的接口,那么,也应该考虑同时提供一个骨架实现类。最后,应该尽可能的谨慎设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。

 

NO.17 接口只是被用于定义类型

接口只是用来定义一个类型,不要把接口用来做其他的事情(如在接口中定义常量,这种常量接口模式是对接口的不良使用)。

如果要导出常量,可以有以下几种方式:

①如果这些常量与某个已有的类或者接口有着紧密的联系,则可以把常量添加到这个类或者接口中。

②定义一个类型安全的枚举类,把这些常量看做枚举类型的成员。

③使用一个可以实例化的工具类(构造函数设为private)来导出这些常量。

posted @ 2010-10-19 18:45  irischan  阅读(264)  评论(0编辑  收藏  举报