《Effective Java》读书笔记 - 3.对于所有对象都通用的方法

时间:2023-08-30 22:22:40
《Effective Java》读书笔记 - 3.对于所有对象都通用的方法

Chapter 3 Methods Common to All Objects

Item 8: Obey the general contract when overriding equals

以下几种情况不需要你override这个类的equals方法:这个类的每一个实例天生就是unique的,比如Thread;这个类的父类已经override了equals,并已经足够;这是个private或者package的class,你确定equals永远不会被调用,保险起见这时候你其实可以override一下equals然后throw new AssertionError();。再比如Enum types也不需要override equals,因为每一种enum值都只有一个实例。

而当你决定override equals时,你需要遵守以下几点general contracts:

一.Reflexive(自反性)对任何非null的x,x.equals(x)必须返回true。

二.Symmetric(对称性)对任何非null的x,y,x.equals(y)当且仅当y.equals(x)。

举个反例吧:

public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {...}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
@Override public int hashcode(){...}
}

那么如果:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);

cis.equals(s)和 s.equals(cis)的结果是不一样的,因为String的equals并不是Case Insensitive的。list.contains(s)的返回结果也是不确定的,可能true可能false,取决于其具体的实现。问题就在于这个equals太贪心了,不应该把String类型的对象也包括在可以跟自己比较的东西的范围里,正确做法是:

return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);

三.Transitive(传递性)x,y,z均非null,如果x.equals(y)和y.equals(z)返回true,那么x.equals(z)必返回true。

举个反例:现在有个Point类,它包括x和y两个信息,equals方法是上述标准写法,然后有个ColorPoint类继承了Point,它包括三个信息,x,y和Color。由于Point类中的equals方法有这么一句:if (!(o instanceof Point)) return false;,因为instanceof会考虑多态,所以如果point.equals(colorPoint)是可以的,而且会忽略Color信息(这里的小写字母开头的变量都是对象实例)。那么我们为了满足对称性,必须让colorPoint.equals(point)也返回相同结果,并且还要考虑Color信息在内,那么我们只能想出以下逻辑:

在ColorPoint里面重写equals方法,写成这样的:如果传过来的是个Point那就只比较x,y;如果传过来的是ColorPoint,那就x,y和Color都要比较。这时候虽然解决了对称性问题,但是会造成其他问题:

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

p1和p2相等,p2和p3也相等,但p1和p3不相等。首先说一下违背了这个contract有什么不好?书上没说,但是我个人认为不好就在于背了这个contract就等于违背了基本的数学认知。

事实上,如果你想继承一个instantiable的类(就是可实例化的类,至于为什么后面会说),并加一个value component(上述例子里面就是加了个Color),那么无论你怎么重写equals方法,都必然会违背某个contract,除非你用 o.getClass() != getClass()来代替instanceof,也就是说传进来的o必须和当前对象是完全相同的类型,子类也不行,比如你写了个SonPoint extends Point{},注意除了继承啥都没有,那么sonPoint和point依然会被认为是不相等的,但是用instanceof就没这个问题。那么到底咋办?看下面:

public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {...}
/**
* Returns the point-view of this color point.
*/
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override public int hashcode(){...}
}

没错,这就是所谓的“Favor composition over inheritance.”。

在Java类库里面也有刚才那种继承了一个instantiable的类还增加了一个value component的反面教材:java.sql.Timestamp继承了java.util.Date并加了个nanoseconds,反正别在同一个集合里面混着用这两个就行了,否则会出现诡异的事情。

现在讲一下所谓的instantiable的类:你可以继承一个abstract class兵并加一个value component,因为你根本无法实例化得到一个abstract class的对象,所以abstract class里面根本没必要override equals。

四.Consistent(一致性)只要用来比较的信息没被修改,那么x.equals(y)总是返回相同的结果。举个反例:java.net.URL的equals会把host名翻译成IP地址,这需要访问网络,所以随着时间的迁移,不保证每次结果都一样。

五.对于非null的x,x.equals(null)必须返回false。

感觉只是个规定而已。注意如果o是null的话,o instanceof MyType会返回false,所以没必要再if (o == null)return false;这样检查一遍了。

下面总结一下如果你要override equals,该怎么做:

一.if(argument == this) return true;只是个优化而已。

二.用instanceof检查argument是不是正确的类型,一般来说,正确类型是指当前这个方法所在的class,有时候也可以是当前这个class实现的某个接口。

三.Cast成刚才那个“正确的类型”。

四.比较所有“重要的”field,这里的“重要的”就相当于一个表中的两行,你只需要比较他们的主键就行。对于primitive type的field,用==,float和double例外,要用Float.compare和Double.compare,因为Float.NaN, -0.0f的存在。对于数组field,如果其中每个元素都重要,可以用Arrays.equals。对于允许null的field,如果两个null可以看作相等的,记得(field == null ? o.field == null :field.equals(o.field)),

对于一些需要复杂计算的field,比如CaseInsensitiveString中对s的比较,也许你可以定义一个对应s的小写版本的field来提高性能,当然这一般适用于inmmutable class。另外,你应该先比较那些计算简单的,并且更容易不同的field,也是为了提高性能。

五.最后一定要检查是不是满足了上述的contracts,写一些单元测试进行测试。

小提醒:equals中的逻辑别太复杂,简简单单地比较field就好;记得用 @Override。

Item 9: Always override hashCode when you override equals

equals返回true的话,那么这俩objects的hashcode也必须返回一样的值;不equals的objects允许返回相同的hashcode,但是作为一个合格的coder,要返回分布均匀的才对。后面写了计算Hashcode的具体方法,貌似和Thinking in Java的差不多,需要的时候可以都参考一下,总之就是你在equals里面用到的field,在hashcode里面也要用。如果一个class是immutable的或者计算hashcode比较麻烦,你可以考虑把hashcode cache起来。书上提供的这个方法也并不是“最新式的;使用了最先进技术的;顶尖水准的(state-of-the-art)”,这问题最好留给数学家什么的。

Item 10: Always override toString

由toString()返回的字符串应该满足“concise but informative representation that is easy for a person to read”。当对象被pass到println, printf,字符串连接操作符,assert,或者被debugger显示的时候,toString都会自动被调用,client在记录一些诊断信息的时候就很有用,前提是你override了toString。

当override toString时,你要决定是否把return string的具体的Format写入文档,好处在于这样就是一种标准,可以用于各种输入输出,比如XML,同时最好提供static factory或者constructor来从某个string representation转换为对应的object,大多包装类都实现了。坏处就在于,一旦release后,就不能改了,否则会破坏client代码。但不管怎样,你都应该在文档中清楚地写明自己的意图,并且为所有toString方法中用到的字段提供getter。

Item 11: Override clone judiciously

首先看一下Object中的clone方法:

protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class doesn't implement Cloneable");
}
return internalClone((Cloneable) this);
}

Cloneable是个接口,然而它里面什么方法都没有,这个接口的唯一用处就是:如果实现了这个接口,就表示这个类可以用clone方法。这种用法并不是接口应有的用法,不应该被模仿。internalClone是个native的方法(不会调用constructor),它会创建一个新的实例并把所有field都拷贝过去(shallow copy)。当我们override clone时,正确的做法是要调用super.clone(),所以最终肯定能调用到Object的clone,让它来创建一个当前类(this)的实例(所以在任何clone方法里都不能调用constructor,以及其他任何非final的方法,因为你不知道“当前这个类有没有子类”),然后再初始化一些子类上的field。所以,如果实现了Cloneable,那么就应该写一个well-behaved的clone方法,前提是如果这个类所有的基类都提供了well-behaved的clone方法。但因为Cloneable里面啥都没有(按理说应该把clone方法放进去的),所以这些“正确的做法”并不是强制性的。如果所有的field都是primitive value或者指向immutable对象,那么Object的clone就足够了:

@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
throw new AssertionError(); //Can't happen,因为已经实现了Cloneable接口,所以不需要像Object中的clone那样,
//还要在方法中声明throws CloneNotSupportedException
}
}

上面的cast成PhoneNumber是利用了方法override中的covariant return types,这样就省得client再去cast了。如果用浅复制clone出来的对象会影响到原有对象,就需要用深复制。

比如有个数组的field叫elements,可以直接result.elements = elements.clone(),这时候(我猜的噢)elements.clone()会返回一个新的并且内容相同的数组对象:

@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}

但注意如果是包含reference元素的数组,其中的references就都还指着同样的对象,如果对于你的类来说这样不行的话,那就必须“把每个数组元素再都new一个”。但注意如果elements是final的,那么就不行了,clone方法和指向mutable对象的final field是冲突的。如果一个类是被设计为要被别人继承的,那么它就应该像Object中的clone方法那样(方法中声明throws,并不实现Cloneable接口),否则就像上面写得这样就行(不需要throws,并且实现Cloneable接口)。BTW,Object.clone不是线程安全的。

上面讲了很多的clone()的缺点,所以最好别用clone(),除非万不得已。比如,immutable的对象就不需要被clone。更好的办法是copy constructor或者copy factory,比如:

public Yum(Yum yum);
public static Yum newInstance(Yum yum);

这种方法不仅没有clone()的缺陷,而且你还可以把输入参数的类型定义成某个接口,如

HashSet s;
new TreeSet(s);

这里的TreeSet构造函数就接受一个Set类型的参数。

Item 12: Consider implementing Comparable

首先是接口定义:

public interface Comparable<T> {
int compareTo(T t);
}

有点像equals方法,但是它可以用来比较谁大谁小,而equals只能比较两个是不是相等。实现Comparable就表明这个类的实例有内在的排序关系(算法看了那么多了,应该很熟了)。compareTo的contract是:如果this object小于传进来的object,就返回一个小于零的数....。这里的泛型参数T,至少在Java类库里面,都是跟当前的类是同一个类,所以你也别弄太复杂,就把T写成当前类就行。还有compareTo的规范和equals的差不多,也是自反性、对称性、传递性什么的,而且和equals一样,就是“there is no way to extend an instantiable class with a new value component while preserving the compareTo contract,unless you are willing to forgo the benefits of object-oriented abstraction(用getclass)”。强烈建议让equals和compareTo一致,当然也有例外,比如BigDecimal("1.0")和BigDecimal("1.00"),,equals就是不相等,而compareTo就是相等,所以如果你把他俩传给一个HashSet,就会装着两个元素;传给TreeSet就会只装着一个元素(TreeSet好像是基于红黑树的,所以是有顺序的,所以肯定基于compareTo)对于double和float用Double.compare和Float.compare,其他primitive用大于小于号。如果有多个field,你应该先比较最重要的field,然后比较第二重要的,以此类推。