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。