Java - 谨慎覆盖equals
平时很难遇到需要覆盖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的更多相关文章
- Java - 谨慎覆盖clone
覆盖clone时需要实现Cloneable接口,Cloneable并没有定义任何方法. 那Cloneable的意义是什么? 如果一个类实现了Clonable,Object的clone方法就可以返回该对 ...
- Effective Java —— 谨慎覆盖clone
本文参考 本篇文章参考自<Effective Java>第三版第十三条"Always override toString",在<阿里巴巴Java开发手册>中 ...
- Java hashCode() 和 equals()的若干问题
原文:http://www.cnblogs.com/skywang12345/p/3324958.html 本章的内容主要解决下面几个问题: 1 equals() 的作用是什么? 2 equals() ...
- Java hashCode() 和 equals()的若干问题解答
本章的内容主要解决下面几个问题: 1 equals() 的作用是什么? 2 equals() 与 == 的区别是什么? 3 hashCode() 的作用是什么? 4 hashCode() 和 equa ...
- Java hashCode() 和 equals()的若干问题解答<转载自skywang12345>
第1部分 equals() 的作用equals()的作用是用来判断两个对象是否相等.equals()定义在JDK的Object类中.通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否 ...
- 【Java实战】源码解析为什么覆盖equals方法时总要覆盖hashCode方法
1.背景知识 本文代码基于jdk1.8分析,<Java编程思想>中有如下描述: 另外再看下Object.java对hashCode()方法的说明: /** * Returns a hash ...
- Effective Java —— 覆盖equals时遵守通用约定
本文参考 本篇文章参考自<Effective Java>第三版第十条"Obey the general contract when overriding equals" ...
- Java提高篇——equals()与hashCode()方法详解
java.lang.Object类中有两个非常重要的方法: 1 2 public boolean equals(Object obj) public int hashCode() Object类是类继 ...
- 第9条:覆盖equals时总要覆盖hashCode
在每个覆盖equals方法的类中,也必须覆盖hashCode方法.否则,会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,包括HashMap,Hash ...
随机推荐
- 二十一、Node.js-EJS 模块引擎
初识 EJS 模块引擎 我们学的 EJS 是后台模板,可以把我们数据库和文件读取的数据显示到 Html 页面上面.它是一个第三方模块,需要通过 npm 安装https://www.npmjs.com/ ...
- checkbox attr 和 prop ,onclick 事件,
给checkbox 添加默认的勾选,尽量使用 checked=“checked”,不要使用checked=“true”. <td><input id="checkboxid ...
- 6.BOM
BOM的介绍 浏览器对象模型. 操作浏览器部分功能的API.比如让浏览器自动滚动. BOM的结构图 window对象是BOM的顶层(核心)对象,所有对象都是通过它延伸出来的,也可以称为window的子 ...
- day02.4-字典内置方法
字典——dict的定义:test = {"k1":18,2:True,"k3":[11,22,("zi","zai")] ...
- jQuery动态数字翻滚计数到指定数字的文字特效代码
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- LINUX主机通过域名访问网络失败
故障现象: 用telnet IP+端口的方式测试正常,能够访问: 通过telnet域名+端口方式,提示:未知的名称或服务(Name or service not known). 测试百度的网络地址,p ...
- vue Vue-cli 笔记
之前用vue写项目都是用的es5,这几天看到小右弄了个命令行工具打算试水一下es6.结果老是报错,但是可以正常编译,虽然我不是处女座但是看到报错很不舒服.结果发现源代码最后一行要空一行,我心想这什么狗 ...
- Flume启动运行时报错org.apache.flume.ChannelFullException: Space for commit to queue couldn't be acquired. Sinks are likely not keeping up with sources, or the buffer size is too tight解决办法(图文详解)
前期博客 Flume自定义拦截器(Interceptors)或自带拦截器时的一些经验技巧总结(图文详解) 问题详情 启动agent服务 [hadoop@master flume-1.7.0]$ ...
- JAVA数据结构--快速排序
快排概念 快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),一种排序算法,最早由东尼·霍尔提出.在平均状况下,排序个项目要(大O符号)次比较.在 ...
- 高阶篇:4.1)QFD质量功能展开-总章
本章目的:了解QFD概念和作用,为FMEA打下基础. 1.QFD定义 质量功能展开QFD(Quality Function Deployment),是把顾客或市场的要求转化为设计要求.零部件特性.工艺 ...