对象相等性——如何给自定义对象添加equals和hashCode方法
2016-06-29 13:26
495 查看
译自 http://www.javaworld.com/article/2072762/java-app-dev/object-equality.html
每个Java对象都从
这些方法都提供了默认实现,其中除了
由于计算对象是否相等是一件很耗时的事,Java提供了一种快速判断两个对象是否相等的方法,即
计算哈希码的目的是哈希要比计算整个对象的相等性快。
切记,一个类只重写
自反性(Reflexive),一个对象必须和自身相等,即`a.equals(a);
对称性(Symmetric),即如果
传递性(Transitive),即如果
非null,一个对象任何时候都不能等于
根据上面规则,很容易写出一个
如果参数是
如果参数是
如果参数类型和当前对象类型不同,返回
所有非
为何不需要比较
以二维平面坐标中的点
}
`
[/code]
注意:这里使用
如果
如果
如下面的
}
`
[/code]
注意:
二维平面上的点
}
`
[/code]
上面的
当然,我们期望不同对象的hash code越分散越好。上面的实现中,所有具有相同
使用多个变量的乘积;
使用位运算异或
给整数变量乘上一个质数。`public class Point {
private int x, y;
}
`
然而,为了使hash code的计算速度快,一般不使用乘法。
}
`
[/code]
因为
}
`
[/code]
注意,上面代码中的变量x和y都是
下面是Bloch推荐的做法:
}
`
[/code]
由于这个代码更短,而且是《Effective Java》一书所推荐,所以已经根深蒂固于很多Java程序员的编码中。这也成为本书最具争议的内容之一。
使用
}
`
[/code]
当一个
}
`
[/code]
Point3D
}
[/code]
更深的学习可以参看 如何在Java中避免equals方法的隐藏陷阱 | 酷 壳 - CoolShell.cn
说明:本文译自javaword.com,如果侵权,请联系站长删除。同时,如果转载本文,请注明本文链接。
每个Java对象都从
java.lang.Object继承了一些方法:
Creational methods | |
---|---|
Object() | Default no-argument constructor |
clone() | Returns a new instance of the class |
**Equality methods** | |
equals(Object) | Returns true if this instance is equals to the argument |
hashCode() | Returns a hash code based on the instance data |
**Synchronizing methods** | |
notify() | Sends a signal to a waiting thread (on the current instance) |
notifyAll() | Sends a signal to all waiting threads (on the current instance) |
wait() | Forces the current thread to wait for a signal (on the current instance) |
**Other methods** | |
toString() | Returns a string representation of the object |
finalize() | Perform garbage-collection duties |
getClass() | Returns the Class object associated with the instance |
notify(), notifyAll(), wait()三个方法是
final的,无法被子类重写,其他方法都可以被重写。这篇文章将讨论如何重写
equals()和
hashCode()方法。
equals()和hashCode()方法做什么?
equals()方法的目的是判断参数对象和当前实例是否相等。实际上,
java.util包中的所有集合类都使用了该方法,还有其他很多较为底层的库(如RMI,JDBC,等等)都隐式地依赖于该方法的正确性。如果两个对象被认为是相等的,那么该方法返回
tree,否则返回
false。哪些内容相等才被认为是两个对象相等由每个类自己定义。
由于计算对象是否相等是一件很耗时的事,Java提供了一种快速判断两个对象是否相等的方法,即
hashCode()。该方法根据对象的内部数据结构生成一个小的数值,被称为哈希码(hash code),如果两个对象具有不同的哈希码,那么他们不可能相等。(比如字典里的两个英文单词,如果它们都以A开头,那么它们有可能相等,如果一个以A开头,一个以B开头,那么它们不可能相等。)
计算哈希码的目的是哈希要比计算整个对象的相等性快。
HashMap就使用了哈希码来尽可能地避免计算对象的相等性,
HashMap比
List快的一个原因就是,
List需要搜索整个数据结构判断对象是否存在,而
HashMap只需搜索那些具有相同哈希值的对象。
切记,一个类只重写
equals()方法而不重写
hashCode()方法是错误的。在继承体系中,只需父类提供一个
hashCode()方法即可,后面会详细讨论。
实现equals()方法
方法签名必须为public boolean equals(Object obj) `</pre> 注意:任何类的`equals()`方法的参数都必须是`Object`类型,否则该方法就不是重写,而是重载了,当判断两个对象是否相等时就会调用`java.lang.Object`类的默认的`equals()`方法,而非你定义的。 Javadoc中描述`equals()`方法必须满足:
自反性(Reflexive),一个对象必须和自身相等,即`a.equals(a);
对称性(Symmetric),即如果
a.equals(b),那么
b.equals(a);
传递性(Transitive),即如果
a.equals(b),并且
b.equals(c),那么
a.equals(c);
非null,一个对象任何时候都不能等于
null,即
a.equals(null)永远返回false。
根据上面规则,很容易写出一个
equals()方法的实现,只需要比较以下内容:
如果参数是
this,返回
true;(自反性)
如果参数是
null,返回
false;(非null)
如果参数类型和当前对象类型不同,返回
false;(对称性)
所有非
static和非
transient域都是相等的。(对称性,传递性)
为何不需要比较
static域和
transient域?因为
static数据是属于类的而非对象实例,所有对象实例共享
static数据。
transient关键字的目的是对象在序列化时不让某些域写入(如为安全起见,用户的银行卡号和密码等信息,不希望在网络操作中被传输,那么对这些变量加上transient关键字后就不会被持久化 [详见]),如果这些域用于测试对象是否相等,那么同一个对象在序列化之前和之后就会不等。
以二维平面坐标中的点
Point为例,我们实现一个简单的
equals()方法:
`public class Point { private static double version = 1.0; private transient double distance; private int x, y; [code]public boolean equals(Object other) { if (other == this) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; Point point = (Point)other; return (x == point.x && y = point.y); }
}
`
[/code]
注意:这里使用
getClass()来比较两个对象是否属于同一个类型,而非
instancof,后面我们会讨论为何不用
instanceof。
比较引用类型
如果一个对象里含有引用类型,那么如何比较两个对象的引用是否相等?答案是根据以下规则:如果
this的引用变量为
null,那么
other相应的引用变量也必须为
null;
如果
this的引用变量不为
null,那么它必须和
other相应的引用变量
equals()。
如下面的
Person类,含有两个引用类型的变量name和birth:
`public class Person { private String name; private Date birth; [code]public boolean equals(Object other) { if (other == this) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; Person person = (Person)other; return (name == person.name || (name != null && name.equals(person.name))) && (birth == person.birth || (birth != null && birth.equals(person.birth))); }
}
`
[/code]
注意:
name == person.name检查了两个引用都为
null的情况和两个引用指向同一个对象的情况。在调用
name.equals(person.name)方法前一定要先检查
name != null,否则将会抛出’NullPointerException’。
实现hashCode()方法
哈希码(hash code)就是根据实例数据计算出来的一个int值,如果两个实例被认为是
equals,那么它们必须具有相同的hash code。因此,hash code**只能**根据
equals()方法中所比较的变量域来计算,但并不一定需要使用所有的域。
二维平面上的点
Point的
hashCode()实现如下:
`public class Point { private int x, y; [code]public boolean equals(Object other) { // see above } publice int hashCode() { return x; }
}
`
[/code]
上面的
hashCode()实现时正确的(尽管不是最优的),因为它依赖于
equals()方法的比较域
x。
当然,我们期望不同对象的hash code越分散越好。上面的实现中,所有具有相同
x坐标的点都具有相同的hash code,我们可以通过以下方法改进:
使用多个变量的乘积;
使用位运算异或
^;
给整数变量乘上一个质数。`public class Point {
private int x, y;
public boolean equals(Object other) { // see above } publice int hashCode() { return 31*x ^ 37*y; }
}
`
然而,为了使hash code的计算速度快,一般不使用乘法。
带引用类型的hashCode()方法
如果一个类包含了引用变量,那么就可以使用引用变量的hashCode()方法。但是,跟
equals()方法一样,一定要注意引用是否为
null,否则可能抛出
NullPointerException异常。对于
null的情况,可以返回一个固定值(这个固定值应当是正整数,因为在一些hash map的实现中这些值可能有特殊的意义)。
`public class Person { private String name; private Date birht; [code]public boolean equals(Object other) { // see above } public int hashCode() { return (name == null ? 17 : name.hashCode()) ^ (birht == null ? 31 : birth.hashCode()); }
}
`
[/code]
equals()和hashCode()方法的默认实现
这两个方法的默认实现只适用于简单的情况。equals方法只有在与其自身比较时才返回
true。
hashCode()方法依赖于单一对象的哈希(unique instance hash,如对象在内存中的地址或者不同虚拟机有不同的实现)。
因为
hashCode()依赖于对象的唯一性(identity),所以只重写
equals()方法而不重写
hashCode()方法是错误的。否则,两个对象可能是相等的,却有不同的哈希值。如果实在没有合适的哈希值计算方法,那么可以返回一个常数(比如7),这样也比使用默认的实现好,尽管这样会导致
Map退化成
List。
高级策略
equals()和
hashCode()必须都尽可能地快,因为它们经常被重复地调用。
equals()方法的某些部分要比其他部分快,所以可以先比较快的部分。比如,基本数据类型的比较要快于引用类型的
equals(),那么可以先比较基本数据类型。同样地,如果两个对象类型不同,那么就没必要比较任何数据域了。
只读对象
如果一个对象是只读的(immutable),那么可以提前计算出它的哈希值。当这样的对象被创建时,所有的值都会通过构造函数传入,这时就可以计算出它的哈希值。`public class Point { private final int x, y; private final int hashCode; [code]public Point(int x, int y) { this.x = x; this.y = y; this.hashCode = 31*x ^ 37*y; } public boolean equals(Object other) { // see above } public int hashCode() { return hashCode; }
}
`
[/code]
注意,上面代码中的变量x和y都是
final的,保证以后不会被改变,所以可以在创建对象时就计算出哈希值。
为何不用instanceof
在Joshua Bloch非常有名的《Effective Java》一书中,他推荐使用instanceof来测试以决定对象的类型,表面上看这是一个很好的想法,实际上这有一个重大缺陷,因为
instanceof不是对称的。
下面是Bloch推荐的做法:
`public class BadPoint { private int x, y; [code]public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof BadPoint)) return false; // Bad!!! BadPoint point = (BadPoint)other; return (x == point.x && y == point.y); } public int hashCode() { return x + y; }
}
`
[/code]
由于这个代码更短,而且是《Effective Java》一书所推荐,所以已经根深蒂固于很多Java程序员的编码中。这也成为本书最具争议的内容之一。
使用
instanceof的最大问题是它不具备对称性,当使用继承时这个问题将会体现出来:
`public class BadPoint3D extends BadPoint { private int z; [code]public boolean equals(Object other) { if (!super.equals(other)) return false; if (!(other instanceof BadPoint3D)) return false; // Bad!!! BadPoint3D point = (BadPoint3D)other; return (z == point.z); }
}
`
[/code]
当一个
BadPoint对象和一个
BadPoint3D对象比较时就会出现问题,
badPoint instanceof badPoint3D == false,而
point3D instanceof badPoint == true。
完整的写法
Point`public class Point { private static double version = 1.0; private transient double distance; [code]private String name; private int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public Point(String name, int x, int y) { this(x, y); this.name = name; } public boolean equals(Object other) { if (other == this) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; Point point = (Point)other; return (x == point.x && y == point.y && (name = point.name || (name != null && name.equals(point.name)))); } public int hashCode() { return x ^ y; }
}
`
[/code]
Point3D
`public class Point3D { private int z; [code]public Point3D(int x, int y, int z) { super(x, y); this.z = z; } public Point3D(String name, int x, int y, int z) { super(name, x, y); this.z = z; } public boolean equals(Object other) { if (!super.equals(other)) return false; Point3D point = (Point3D)other; return (z == point.z); } public int hashCode() { return super.hashCode() ^ z; }
}
[/code]
更深的学习可以参看 如何在Java中避免equals方法的隐藏陷阱 | 酷 壳 - CoolShell.cn
说明:本文译自javaword.com,如果侵权,请联系站长删除。同时,如果转载本文,请注明本文链接。
相关文章推荐
- spiral-matrix-ii
- 浅谈大型网站动态应用系统架构
- 自定义ViewPager的兼容性问题及解决办法
- 在多项目工程中统计子工程的覆盖率
- sizeof与strlen的区别
- 视觉slam学习资料
- 视觉slam学习资料
- cookie 和session 的区别详解
- log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析
- Qt下 QString转char*
- 自定义View实现手指在屏幕上绘制线条(贝塞尔曲线处理)(一)
- JVM中的栈(Stack)和堆(Heap)
- 可伸缩系统的架构经验
- 使用自定义签名的https的ssl安全问题解决和metro的webservice调用
- HDU1018
- Nginx基本使用方法及各模块基本功能
- ElasticSearch 教材收藏
- MongoDB Sharded cluster架构原理
- mybatis 中的SqlSessionTemplate 理解
- MD5 base64 sha1 加密,解密