平时很难遇到需要覆盖equals的情况。

什么时候不需要覆盖equals?

  • 类的每个实例本质上是唯一的,我们不需要用特殊的逻辑值来表述,Object提供的equals方法正好是正确的。
  • 超类已经覆盖了equals,且从超类继承过来的行为对于子类也是合适的。
  • 当确定该类的equals方法不会被调用时,比如类是私有的。

如果要问什么时候需要覆盖equals?
答案正好和之前的问题相反。
即,类需要一个自己特有的逻辑相等概念,而且超类提供的equals不满足自己的行为。
(PS:对于枚举而言,逻辑相等和对象相等都是一回事。)

既然只好覆盖equals,我们就需要遵守一些规定:

  • 自反性 (reflexive):对于任何一个非null的引用值x,x.equals(x)为true。
  • 对称性 (symmetric):对于任何一个非null的引用值x和y,x.equals(y)为true时y.equals(x)为true。
  • 传递性 (transitive):对于任何一个非null的引用值x、y和z,当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true。
  • 一致性 (consistent):对于任何一个非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)的结果依然一致。
    (PS:对于任何非null的引用值x,x.equals(null)必须返回false。)

其实这些规定随便拿出一个都是很好理解的。
难点在于,当我遵守一个规定时有可能违反另一个规定

自反性就不用说了,很难想想会有人违反这一点。

关于对称性,下面提供一个反面例子:

class CaseInsensitiveString {

    private final String s;

    public CaseInsensitiveString(String s) {
if (s == null)
this.s = StringUtils.EMPTY;
else
this.s = s;
} @Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
if (obj instanceof String)
return s.equalsIgnoreCase((String) obj);
return false;
} }

这个例子显然违反对称性,即x.equals(y)为true 但 y.equals(x)为false。
不仅是在显示调用时,如果将这种类型作为泛型放到集合之类的地方,会发生难以预料的行为。

而对于上面这个例子,在equals方法中我就不牵扯其他类型,去掉String实例的判断就可以了。

关于传递性,即,当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true。
这个规定在对类进行扩展时尤其明显。

比如,我用x,y描述某个Point:

class Point {
private final int x;
private final int y; public Point(int x, int y) {
super();
this.x = x;
this.y = y;
} @Override
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
Point p = (Point) obj;
return p.x == x && p.y == y;
} }

现在我想给Point加点颜色:

class ColorPoint extends Point {
private final Color color; public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
} @Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint))
return false;
return super.equals(obj) && ((ColorPoint) obj).color == color;
} }

似乎很自然的提供了ColorPoint的equals方法,但他连对称性的没能满足。

于是我们加以修改,令其满足对称性:

@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
if (!(obj instanceof ColorPoint))
return obj.equals(this);
return super.equals(obj) && ((ColorPoint) obj).color == color;
}

好了,接下来我们就该考虑传递性了。
比如我们现在有三个实例,1个Point和2个ColorPoint....
然后很显然,不满足<当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true>。
事实上,我们无法在扩展可实例化类的同时,既增加新的值组件,又保留equals约定。

于是我索性不用instanceof,改用getClass()。
这个确实可以解决问题,但很难令人接受。
如果我有一个子类没有覆盖equals,此时equals的结果永远是false。

既然如此,我就放弃继承,改用复合(composition)。
以上面的ColorPoint作为例子,将Point变成ColorPoint的field,而不是去扩展。
代码如下:

public class ColorPoint {
private final Point point;
private final Color color; public ColorPoint(int x, int y, Color color) {
if (color == null)
throw new NullPointerException();
point = new Point(x, y);
this.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() {
return point.hashCode() * 33 + color.hashCode();
}
}

关于一致性,即如果两者相等则始终相等,除非有一方被修改。
这一点与其说equals方法,到不如思考写一个类的时候,这个类应该设计成可变还是不可变。
如果是不可变的,则需要保证一致性。

考虑到这些规定,以下是重写equals时的一些建议:

  • 第一步使用"=="操作验证是否为同一个引用,以免不必要的比较操作。
  • 使用instanceof检查参数的类型。
  • 检查所有关键的field,对float和double以外的基本类型field直接使用"=="比较。
  • 回过头来重新检查一遍:是否满足自反性、对称性、传递性和一致性。

任何覆盖了equals方法的类都需要覆盖hashCode方法。
忽视这一条将导致类无法与基于散列的数据结构一起正常工作,比如和HashMap、HashSet和Hashtable。

下面是hashCode相关规范:

  • 在程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这个对象调用多少次hashCode,起结果必须始终如一地返回同一个证书。
    如果是同一个程序执行多次,每次调用的结果可以不一致。

  • 如果两个对象根据equals方法比较是相等的,那么两个对象的hashCode结果必须相同。

  • 如果两个对象根据equals方法比较是不相等的,那么这两个对象的hashCode不一定返回不同的结果。
    但是,如果不同的对象返回不同的hashCode,则能提高散列表的性能。

下面的代码是一个反面例子:

import java.util.HashMap;
import java.util.Map; public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
} private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name + ": " + arg);
} @Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNumber == lineNumber && pn.prefix == prefix
&& pn.areaCode == areaCode;
} // Broken - no hashCode method! // A decent hashCode method - Page 48
// @Override public int hashCode() {
// int result = 17;
// result = 31 * result + areaCode;
// result = 31 * result + prefix;
// result = 31 * result + lineNumber;
// return result;
// } public static void main(String[] args) {
Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
}
}

通过equals方法比较,两个实例在逻辑上是相等的。
但由于没有覆盖hashCode方法,两个实例返回的hashCode是不同的。
在散列表中,如果散列码不匹配,就不必检查两个实例是否相等。

如果随便提供这样的一个hashCode方法:

public int hashCode(){
return 42;
}

这样会让散列表失去优势,退化为链表。

最好的hashCode应该是<不同的对象产生不同的散列码>。
即,散列函数把集合中不同的实例均匀地分布到所有可能的散列值上。

下面是一种简单的思路(也就是上面例子中注释的部分):

  • 把一个非零常数值放在result变量中。
  • 针对每一个关键的field(假设变量名为f)计算int类型的散列码,不同类型有不同的计算方式。

    • boolean:f?1:0
    • byte,short,char:(int)f
    • long:(int)(f^(f>>>32))
    • float:Float.floatToIntBits(f)
    • double:Double.doubleToIntBits(f)
    • 引用:递归调用hashCode
    • 数组:每个元素作为一个field遵循上述规则
  • 对计算出的散列码值c进行:result = result*31+c;
  • 重复测试。

注意,这里仅限关键field。
对于那些用其他field值计算出来的field,我们可以将其排除在外。

如果一个类是不可变的,而且计算散列值的开销比较大,我们可以试着将散列值缓存。

或者我们也可以试试延迟初始化,在hashCode第一次被调用时进行初始化:

private volatile int hashCode; // (See Item 71)

@Override public int hashCode() {
int result = hashCode;
if (result == 0) {
result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result;
}
return result;
}

另外,Josh Bloch在最后加了一段话:

Many classes in the Java platform libraries, such as String, Integer, and Date, include in their specifications the exact value returned by their hashCode method as a function of the instance value. This is generally not a good idea, as it severely limits your ability to improve the hash function in future releases.

<可以把它们的hashCode方法返回的确切值规定为该实例的一个函数。> 看了翻译后一头雾水...

后来在爆栈中看到这么一个回复,记下来作为参考:

The API docs specify that String.hashCode() is computed by a specific formula. Client code is free to independently compute the hash code using that exact formula and assume it will be the same as that returned by String.hashCode(). This might seem perverse for pure Java code, but does make some sense with JNI. There are probably other cases where it would make sense to take advantage of the extra knowledge that the API specifies.

Java - 谨慎覆盖equals的更多相关文章

  1. Java - 谨慎覆盖clone

    覆盖clone时需要实现Cloneable接口,Cloneable并没有定义任何方法. 那Cloneable的意义是什么? 如果一个类实现了Clonable,Object的clone方法就可以返回该对 ...

  2. Effective Java —— 谨慎覆盖clone

    本文参考 本篇文章参考自<Effective Java>第三版第十三条"Always override toString",在<阿里巴巴Java开发手册>中 ...

  3. Java hashCode() 和 equals()的若干问题

    原文:http://www.cnblogs.com/skywang12345/p/3324958.html 本章的内容主要解决下面几个问题: 1 equals() 的作用是什么? 2 equals() ...

  4. Java hashCode() 和 equals()的若干问题解答

    本章的内容主要解决下面几个问题: 1 equals() 的作用是什么? 2 equals() 与 == 的区别是什么? 3 hashCode() 的作用是什么? 4 hashCode() 和 equa ...

  5. Java hashCode() 和 equals()的若干问题解答<转载自skywang12345>

    第1部分 equals() 的作用equals()的作用是用来判断两个对象是否相等.equals()定义在JDK的Object类中.通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否 ...

  6. 【Java实战】源码解析为什么覆盖equals方法时总要覆盖hashCode方法

    1.背景知识 本文代码基于jdk1.8分析,<Java编程思想>中有如下描述: 另外再看下Object.java对hashCode()方法的说明: /** * Returns a hash ...

  7. Effective Java —— 覆盖equals时遵守通用约定

    本文参考 本篇文章参考自<Effective Java>第三版第十条"Obey the general contract when overriding equals" ...

  8. Java提高篇——equals()与hashCode()方法详解

    java.lang.Object类中有两个非常重要的方法: 1 2 public boolean equals(Object obj) public int hashCode() Object类是类继 ...

  9. 第9条:覆盖equals时总要覆盖hashCode

    在每个覆盖equals方法的类中,也必须覆盖hashCode方法.否则,会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,包括HashMap,Hash ...

随机推荐

  1. 【QTP专题】05_参数化之Excel

    QTP使用外部Excel实现参数化主要有以下两种方式 导入到DataTable中 Syntax:DataTable.ImportSheet(FileName, SheetSource, SheetDe ...

  2. [转]解读Unity中的CG编写Shader系列5——理论知识

    经过前面的系列文章中的三个例子,尽管代码简单,但是我想应该还有些地方没有100%弄明白,我们现在得回过头来补充一些必备的数学.图形学知识 1.图形管道第一个例子中我有提到顶点着色和片段着色在整个图形绘 ...

  3. Kalibr installation tutorial

    How to install Kalibr I was confused about installing Kalibr, but there is no even one hint in READM ...

  4. “全栈2019”Java第八十一章:外部类能否访问嵌套接口里的成员?

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  5. jqury属性操作,特殊效果

    一. 常用属性操作 1.html() 取出或设置html内容 // 取出html内容 var $htm = $('#div1').html(); // 设置html内容 $('#div1').html ...

  6. ios处理键盘的大小

    iOS的键盘有几个通知 UIKeyboardWillShowNotification UIKeyboardDidShowNotification UIKeyboardWillHideNotificat ...

  7. jenkins定时

    分别的定义为:分 时 天 月 星期 Minutes within the hour (0–59) HOUR       Thehour of the day (0–23) DOM         Th ...

  8. Q481 神奇字符串

    神奇的字符串 S 只包含 '1' 和 '2',并遵守以下规则: 字符串 S 是神奇的,因为串联字符 '1' 和 '2' 的连续出现次数会生成字符串 S 本身. 字符串 S 的前几个元素如下:S = & ...

  9. C 和 C++ 字符串函数操作

    1)字符串操作  strcpy(p, p1) 复制字符串 strncpy(p, p1, n) 复制指定长度字符串 strcat(p, p1) 附加字符串 strncat(p, p1, n) 附加指定长 ...

  10. PIE SDK矢量点生成等值线、面

    1.算法功能简介 等值线图能直观地展示数据的变化趋势,是众多领域展示成果的重要图建之一,被广泛应用于石油勘探.矿物开采.气象预报等众多领域.等值线的绘制是指从大量采样数据中提取出具有相同值的点的信息, ...