平时很难遇到需要覆盖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. nowcoder(牛客网)提高组模拟赛第一场 解题报告

    T1 中位数(二分) 这个题是一个二分(听说是上周atcoder beginner contest的D题???) 我们可以开一个数组b存a,sort然后二分b进行check(从后往前直接遍历check ...

  2. WEB新手之sql注入

    继续写题. 这题看上去是一道sql注入题.F12查看后台代码. 可以看到后台有两个变量,分别是uname以及passwd.然后接下来读一下后台的代码,这里的意思是,如果用户输入的密码经过md5加密后, ...

  3. 实用的bash别名和函数

    本文来自于:程序师 作为一个命令行探索者,你或许发现你自己一遍又一遍重复同样的命令.如果你总是用ssh进入到同一台电脑,如果你总是将一连串命令连接起来,如果你总是用同样的参数运行一个程序,你也许希望在 ...

  4. 新建MAVEN项目--pom.xml报错

    使用集成了maven的Eclipse版本新建maven项目后,配置文件pom.xml会在project以及引用的xsd文件处出现错误(第一.二行报错) 其中一个报错例子: Multiple annot ...

  5. struts中如何查看配置文件中是否存在某个返回值

    ActionConfig config = ActionContext.getContext() .getActionInvocation().getProxy().getConfig(); Resu ...

  6. python 学习(pip工具的安装)

    mac 电脑上使用终端命令 curl https://bootstrap.pypa.io/get-pip.py | python3 基于Python 3 pip --version pip3 list ...

  7. webpack2的一些使用入门

    首先创建一个webpack文件夹我取名叫webpackVue(为了后续把vue集成进来) 1.首先用npm初始化一下,在这个目录下,执行npm init 2.npm install webpack - ...

  8. 2016级算法第四次上机-A.Bamboo 和人工zz

    Bamboo和人工ZZ 题意: 非常直白,经典的动态规划矩阵链乘问题 分析: 矩阵链A1A2..An满足结合律,可以使用加括号的方式,降低运算代价. 一个pq的矩阵和一个qr的矩阵相乘,计算代价为pq ...

  9. Navicat设定mysql定时任务

    有个需求:每天将一张表的前一天的数据抽取到另一张表中,使用Mysql数据库的客户端Navicat配置 第一步,创建过程cust_report,直接在查询窗口中执行,保存后函数列表中就会出现. 第二步, ...

  10. 高阶篇:4.2.5)DFMEA建议措施及后续完备

    本章目的:填写建议措施及DFMEA后续完备. 1.建议措施(k) 定义 总的来说,预防措施(降低发生率)比探测措施更好.举例来说,比起设计定稿后的产品验证/确认,使用已证实的设计标准或最佳实践更加可取 ...