Effective Java 读书笔记 - 第三章
这一章主要介绍了如何覆盖Object的非final方法和Comparable.compareTo方法。
如何重写equals方法
当一个类具有特定的逻辑相等的概念而不是对象等同(即obj1 == obj2)时,类的编写者就需要考虑重写equals方法,但是不恰当的重写equals方法容易导致一些问题,所以在重写equals方法时,应该遵守一些通用的约定。
- 自反性,即
x.equals(x)为true。 - 对称性,即
x.equals(y)为true的充要条件是y.equals(x)为true。 - 传递性,即
x.equals(y)为true,y.equals(z)为true,那么x.equals(z)也为true。 - 一致性,即在没有对 x 和 y,做任何修改的情况下,任何时候调用
x.equals(y)的值应该是一致的。 - 对于一个非
null的引用,x.equals(null)必须是false。
自反性要求一个对象比如等价于自身。对称性涉及两个对象之间的比较,假如实现一个CaseInsensitiveString类,并重写equals方法。
public class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = s;
}
@Override
public boolean equals(Object o) {
if ((o instanceof CaseInsensitiveString)) {
return ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}
if(o instanceof String) {
return s.equalsIgnoreCase((String)o);
}
return false;
}
}
在重写的equals方法中,判断了比较对象是否是String类型,显然这会违反对称性,因为String.equals(CaseInsensitiveString)一定会返回false,而CaseInsensitiveString.equals(String)可能会返回true。
传递性涉及 3 个对象之间的比较,考虑这样的情形,Point类有两个整型变量,表示二维平面的坐标,而colorPoint类继承自Point类,它增加了一个color属性。
为了不违反对称性,如果Point.equals(ColorPoint)为true,ColorPoint.equals(Point)必须为true,所以ColorPoint的equals方法可以是这样的。
上代码创建的 3 个对象可以看出,这样就违反了传递性,所以没法继承一个对象并且添加一个值后,仍然能遵守equals方法的约定。
针对这种情况,可以使用_组合优于继承_的原则,将ColorPoint改写成这样。
// Composition
一致性要求在两个对象没有变更过的情况下,任何一次调用equals方法的结果必须是一样的。无论是可变对象还是不可变对象,equals方法不能依赖不可靠的资源。
下面这些诀窍有助于实现高质量的equals方法。
- 首先使用
==操作符检查参数的引用是否就是当前的对象。 - 使用
instanceof操作符检查参数是否是正确的类型,因为equals方法的参数类型是Object。 - 在比较前将参数强制转换成正确的类型。
- 在两个对象检查每个待比较的成员变量是否匹配。
在比较时,对于除
float和double的基本类型,使用==操作符,对于引用,调用它们的equals进行比较,对于数组类型则需要比较每个元素(可以调用Arrays.equals()的重载方法),对于float和double类型,使用其包装类型的compare进行比较。 - 在完成
equals方法之后检查它是否遵守了equals方法的约定。
同时重写equals方法和hashCode方法
计算对象的哈希值的变量必须与equals一致。在重写equals方法后,必须以相同的方式重写hashCode方法,否则会导致依赖对象hashCode方法的行为与预期不符,例如HashMap,HashSet等。
重写hashCode方法的原则如下:
- 在不修改
equals相关的成员变量的情况下,每次调用hashCode方法的返回值应该一致。 - 如果
x.equals(y)为true,那么x.hashCode()与y.hashCode()必须相等。 - 如果
x.equals(y)为false,那么x.hashCode()与y.hashCode()可以不同,也可以相同,如果要产生更好的性能,尽量不通。
理想情况下,hashCode方法应该为不同对象产生不同的哈希值,下面这种方法可以给出相近的效果。
- 选择一个非 0 值作为初始值,比如 17,赋值给
result; 2.a 对于每一个待计算哈希值的变量,按照如下规则计算:- 1)如果类型是
boolean,计算f ? 1 : 0; - 2)如果类型是
byte,char
- 1)如果类型是
第一步选择一个非 0 的初始值,是为了让 2.a 为 0 的初始域影响到哈希值,进而增加减少哈希值冲突(考虑一个集合中有多种类型,这多种类型是都集继承自一个父类,计算哈希值的初始域为 0,每个子类的非零初始值选择不一样)。在 2.b 中选择使用 31 来计算哈希值,有两个原因,其中一个原因是必须选择一个非 0 值来计算,减少冲突(考虑相同字母异序字符串,如果不使用非 0 值,将会计算出相同的哈希值),另一个原因是31 * i == (i << 5) - i,计算性能更高。
始终重写toString方法
按照具体实现类的需求重写toString方法,有助于该类更易使用,所以toString方法应该返回所有该类重要的字段信息。比如对于PhoneNumber,toString方法的返回值
谨慎重写clone方法
Java 的Object类提供了clone方法,但是一个类如果不实现Cloneable接口,则无法成功调用clone方法,可见Cloneable接口只是标识了clone方法能否被正确调用。Object.clone的描述部分如下:
创建一个对象的拷贝,这个拷贝的精确含义取决于具体clone方法的实现。对于一个对象来说
x.clone() != x
为true,
x.clone().getClass() == x.getClass()
为true,但是者不是必须的。
x.clone().equals(x)
为true,但是这也不是必须的。拷贝一个对象将会创建一个新的对象,并且复制内部数据结构,但是这些操作不会调用构造方法。
在重写clone方法时,必须调用super.clone(),来克隆一个当前类的对象,对于PhoneNumer类,其clone方法如下。
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber)super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
该重写方法返回的是PhoneNumber类型,而Object的clone方法返回的类型是Object,对于 JDK 1.5 发布的_协变返回类型_,这是可行的,所以对于成员变量为基本类型以及不可变对象的类,这样克隆一个对象,是合适的,但是如果成员变量有可变对象,那么继续使用类似PhoneNumber中的方式就会产生问题。
public class Stack {
private Object[] elements;
private int size;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object o) {
ensureCapacity();
elements[size++] = o;
}
public Object pop() {
if(size == 0) {
throw new RuntimeException("empty stack");
}
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
在Stack类中,有一个数组类型的成员变量,如果在clone方法中直接调用super.clone(),则新对象的elements属性与原对象都指向同一个引用,修改了新对象的elements,原对象也会跟着改变。所以,如果要正确的定义clone方法,必须递归的调用可变对象的clone方法,那么这就要求这个可变对象的类也集成了Cloneable接口,并且其clone方法能够正确返回克隆对象。Stack.clone方法定义如下。
@Override
public Stack clone() {
try {
Stack stack = (Stack)super.clone();
stack.elements = this.elements.clone();
return stack;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
需要注意的是,现在这个版本的Stack.clone方法针对基本类型,或者String类型,是可以正常运行的,但是如果elements数组存的是其他类型,又需要单独针对每个对象调用clone方法进行拷贝。所以在clone方法中,可以采取一些其他手段对可变对象进行拷贝(克隆),比如自定义一种拷贝方法,但是需要注意的是,clone方法相当于另一种构造方法,在待克隆的对象创建完之前,调用对象的方法会产生一些问题。
另外,Object.clone()是非线程安全的,在多线程的场景下,重写clone方法需要保证线程安全。
实现Cloneable接口从而重写clone方法来做对象拷贝(克隆)存在一系列问题,所以应该尽量避免使用该方式来拷贝对象。
实现Comparable接口
Comparable接口的compareTo方法与Object的equals方法类似,也必须遵守类似equals方法的约束:自反性,对成性,传递性,所以无法通过继承来扩展类的同时保持这三种约束。
此外,实现compareTo方法时,建议x.compareTo(y) == 0的同时,x.equals(y)为true,但是这只是建议,不是必须的。比如BigDecimal类,对象BigDecimal b1 = new BigDecimal("1.0")与BigDecimal b2 = new BigDecimal("1.00"),b1.equals(b2)为false,但是b1.compareTo(b2)为 0。