2 所有对象共有的方法
Item 10:重写 equals 方法时遵守通用协同
不需要重写 equals() 方法的情况:
- 类的每一个实例都认为是不同。比如 Thread 这种代表活跃的实体而不是值
- 不需要“逻辑相等”的判断。比如 Pattern 不需要检查内嵌的正则表达式是否相等
- 父类已经重写了合适的 equals() 方法。比如,AbstractList 等的子类
- private 或 package 访问权限的类。包外无法访问,不需要
需要重写 equals() 方法的情况(大多数 value class):
- 只比较是否是堆中的同一个对象不够,还需要判断“逻辑相等”
- 父类没有合适的 equals() 方法
有一些 value class 不需要重写,因为它们能确保每一个值最多只存在一个对象,比如 Enum 类型。
Object 类的 API 文档注释中,说明了 equals() 方法的通用协同。
继承一个可实例化的类并增加新属性的时候,必然会违反 equals 协同:
public class Point {
private final int x, y;
// ...
@Override public boolean equals(Object o) {
if (!(o instance of Point)) return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
public class ColorPoint extends Point{
private final Color color;
// ...
@Override public boolean equals(Object o) { // 违反了对称性
if (!(o instance of ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
Point 对象可能等于某个 ColorPoint 对象,但是反之不成立,所以违反了对称性。比如 java.util.Date 和它的子类 java.sql.Timestamp 之间就存在这个问题,它们不能混合比较,否则会产生混乱,而这个问题只能靠程序员在使用时自己注意。
解决上述问题的一个妥协方法是,用组合代替继承:
public class ColorPoint {
private final Point point;
private final Color color;
// ...
public Point asPoint() { return point; }
@Override public boolean equals(Object o) {
if (!(o instance of ColorPoint)) return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
重写 equals() 方法的建议:
- 使用
==
检查是否是堆中的同一个对象 - 使用
instanceOf
检查对象类型,并考虑了 null 的情况 - 将参数对象强制类型转换为正确类型
- 对于对象中的每一个“有意义”的字段属性,检查是否和测试对象相等,元素数据类型可以用
==
,float 和 double 用包装类的compare()
静态方法,数组使用Arrays.equals()
方法,引用类型使用考虑了 null 的Objects.equals()
方法。 - 最后,不要忘了,重写 equals 方法时也要重写 hashCode 方法(Item 11)
例如,String 的例子:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Item 11:重写 equals 方法时也要重写 hashCode 方法
如果不做到这一条的话,在使用 HashMap 和 HashSet 等会使用哈希值的集合类时,会出现异常。
Object 的 API 文档中同样规定了 hashCode 的通用协同:
- 一个进程中,同一个对象多次调用 hashCode 方法,如果 equal 方法中比较的字段属性没有修改的话,它应该返回相同的 int 值
- equals 方法返回 true 的两个对象,hashCode 方法返回相等的 int 值
- equals 方法返回 false 的两个对象,不要求但是墙裂建议 hashCode 方法返回不相等的 int 值
重写 equals 方法时没有重写 hashCode 方法的话,很可能会违反上述协同的第二条。
为了尽可能满足第三条规定,这个有一个生成哈希值的建议:
-
声明 int 变量 result,初始化为你的对象的第一个重要字段的哈希值 c ,计算方法如 2.a.(“重要字段”是指会影响 equals 比较的字段)
-
对象中剩下的每个重要字段,都按如下处理:
a. 计算得到该字段的 int 类型哈希值 c :
- ⅰ. 字段是原始数据类型,使用包装类的 hashCode 静态函数来计算
- ⅱ. 字段是对象引用,如果 equals 方法中在比较此字段时会递归调用它的 equals 方法,那么这里也要递归调用它的 hashCode 方法。如果字段为 null,使用 0
- ⅲ. 字段是数组,把数组中的每个重要元素当作字段,使用 2.a. 中的规则计算每个字段的哈希值,使用 2.b. 中的规则组合这些值。如果数组没有重要元素,使用一个常量(最好不要是 0);如果数组中每个元素都重要,使用
Arrays.hashCode
b. 将 2.a. 中计算得到的哈希值 c 按照如下规则组合进 result 中:
result = 31 * result + c
-
返回 result
之所以会用乘数因子 31 ,是因为它是素数而且 31 * i == (i << 5) - i
,JVM 会自动完成此优化。
// java.lang.String 的 hashCode
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) { // 懒加载
char val[] = value;
// 重要字段为数组
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
Item 12:总是重写 toString 方法
设计良好的 toString 方法使得对象使用起来很惬意,也方便 debug 过程。尽可能使 toString 方法返回对象中的所有重要信息。如果你决定在 toString 的 API 文档中指明返回格式的话,其他程序员可能为此量身定做一些持久化的对象存储和解析的工具,这可能会限制未来的类的更新。
比如,AbstractMap 的 toString 方法,规定了返回格式为 {k1=v1, k2=v2}
,或 {}
。
/**
* Returns a string representation of this map. The string representation
* consists of a list of key-value mappings in the order returned by the
* map's <tt>entrySet</tt> view's iterator, enclosed in braces
* (<tt>"{}"</tt>). Adjacent mappings are separated by the characters
* <tt>", "</tt> (comma and space). Each key-value mapping is rendered as
* the key followed by an equals sign (<tt>"="</tt>) followed by the
* associated value. Keys and values are converted to strings as by
* {@link String#valueOf(Object)}.
*
* @return a string representation of this map
*/
Item 13:重写 clone 方法要慎重
immutable 类不需要提供 clone 方法。
ArrayList 的 clone 方法:
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
使用父类的 super.clone()
,只是简单的字段赋值,原始数据类型和 immutable 字段不会出问题,但是对于 mutable 的对象引用类型的字段,拷贝前后,引用的是同一个堆中的对象,因此需要额外的拷贝工作。
clone 方法最大的亮点是对数组拷贝的实现,它会新建一个新数组,并进行元素的赋值,和 Arrays.copyOf
的效果一样。
另一个大问题是,clone 方法中不要调用子类可能会重写的方法,否则会导致子类还为拷贝完成,就执行了自己的方法,有安全隐患,因此调用的方法最好有 final 修饰,比如 HashMap 的 clone 方法中调用了 final 类型的 putMapEntries 方法。
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
考虑到 clone 方法的安全隐患,当你需要拷贝的功能时,最好考虑提供拷贝构造器或拷贝工厂方法,它们有如下优点:
- 没有 clone 的种种注意事项
- 不会和 final 字段冲突
- 不需要多余的 try-catch 语句
- 不需要强制类型转换
- 支持基于接口的拷贝
如下是 HashMap 中基于 Map 接口的拷贝构造器:
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
Item 14:考虑实现 Comparable 接口
Comparable 的 API 文档中指明了 compareTo 方法的通用协同(sgn 函数将负数、0、正数转换为 -1、0、1):
sgn(x.compareTo(y)) == - sgn(y.compareTo(x))
,前者抛出异常的充要条件是后者也抛出异常- transitive:
x.compareTo(y) > 0 && y.compateTo(z) > 0
,可推导出x.compareTo(z) > 0
- x.compareTo(y) == 0,可推导出
sgn(x.compareTo(z)) == sgn(y.compareTo(z))
- 需要补充的是,不要求但是墙裂推荐,
(x.compareTo(y) == 0) == (x.equals(y))
,不满足这条要求的 compareTo 方法必须在 API 文档中声明:“Note: This class has a natural ordering that is inconsistent with equals.”
和 equals 方法类似,继承一个可实例化的类并增加新属性的时候,必然会违反 compareTo 协同 。解决方法也是一样,使用组合代替继承 。
协同的第四项需要特别注意,比如 BigDecimal 类就不满足此建议,有如下效果:
Set<BigDecimal> treeSet = new TreeSet<BigDecimal>();
Set<BigDecimal> hashSet = new HashSet<BigDecimal>();
treeSet.add(new BigDecimal("1.0"));
hashSet.add(new BigDecimal("1.0"));
treeSet.add(new BigDecimal("1.00"));
hashSet.add(new BigDecimal("1.00"));
System.out.println(treeSet.size() == 1); // true
System.out.println(hashSet.size() == 2); // true
这是因为排序集合类(如 TreeSet,TreeMap)使用 compareTo 来判断元素是否相等,而一般集合类使用 equals 来判断元素是否相等。
原始数据类型的比较,避免使用 <
>
符号,或者减法(可能溢出),建议使用包装类的 compare 静态方法,或者用 Comparator 接口的链式构造方法(会有少量性能损耗,但可读性强且简洁)。
private static final Comparator<PhoneNumber> COMPARATOR =
Comparator.compaingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPATATOR.compare(this, pn);
}