[Java] Object方法浅析(一): equals与hashCode
来源:互联网 发布:linux signal定义 编辑:程序博客网 时间:2024/05/19 10:39
摘要
- equals描述的是一种等价关系,不仅仅是引用相等
- equals重载需要满足自反性、对称性与传递性
- 任何实例equals(null)需返回false,且多次调用equals返回值不变
- hashCode返回对象的哈希值,多次调用hashCode返回值不变
- hashCode不一定与内存地址相关
- hashCode会发生碰撞
- hashCode与equals密切相关
- equals返回true的两个实例,hashCode值必须一样
- hashCode值一样的两个实例,其equals方法返回不一定为true(可能有哈希碰撞等因素)
equals
equals是Object类的一个方法,常用来比较两个对象是否“相等”。当然,equals方法描述的相等是广义的,实际上应该是离散数学中的等价关系。
等价关系(equivalence relation)
等价关系等同于集合的划分,简单来说就是物品分类,比如定义一个车辆集合Cars,定义分类规则(二元关系)为同种颜色的车,那么所有红色的车划分为一堆(等价类),所有绿色的车为另一堆。每辆车都存在且只存在于一堆中,所有堆的合起来等同于Cars。
划分的好处在于每个等价类中的个体都是等价的,均能代表整个类(如红色的普桑可以代表红色的车)。且等价类之间互斥,任何一个元素均只包含在一个等价类中。
equals重载
等价关系不同于相等,它在不同的类中可以有不同的规则,equals在不同类中也被不同重载。
在Object类中,只有同一个对象才被认定为等价。
Object.class public boolean equals(Object obj) { return (this == obj);}
而在Integer类中,同为Integer类且值相等则便认为等价,而不仅仅同一个引用。
Integer.classpublic boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false;}
但无论规则是什么,等价关系必须满足定义的三大原则:
1. 自反性:对于任何非空的x,x.equals(x)都应该返回true。2. 对称性:对于任何非空x和y,当且仅当x.equals(y)返回true时, y.equals(x)也应该返回true。3. 传递性:对于任何非空x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。
此外,对于Java来说,还需满足另外两个原则:
4. 一致性:如果x和y的引用没有发生变化,多次调用x.equals(y)的结果应该相同。5. 关于null:对于任何非空的x,x.equals(null)都应该返回true。
例如自定义Person类,当身份证identity号相等时候便可以认为两个实例等价,可以这样写:
static class Person { public String name; public long identity; public Person(String name, long identity) { this.name = name; this.identity = identity; } @Override public boolean equals(Object obj) { if (obj instanceof Person) { if (((Person) obj).identity == this.identity) { return true; } } return false; }} 测试类 @Test public void testEqual() throws Exception { Person x = new Person("aa", 1234); Person y = new Person("aa", 12345); Person z = new Person("bb", 1234); Assert.assertTrue(x.equals(x)); Assert.assertTrue(x.equals(z)); Assert.assertTrue(z.equals(x)); Assert.assertFalse(x.equals(y)); Assert.assertFalse(x.equals(null)); }
hashCode
hashCode方法返回的是对象的哈希值。
Object类的hashcode方法取决于JVM的实现,比较典型的一种实现是基于内存地址进行哈希运算,此外也有基于伪随机数的实现。
需要注意的是hashCode与equals一样,多次调用不改变返回值。所以每个对象一旦计算出其identity hash code之后,在该对象死之前都必须保持同一个identity hash code值不可以改变,而不是每次基于内存地址运算(JVM GC会影响内存地址)。
hashCode 与 equals联系
hashCode与equals方法密切相关:两个equals为true的实例必须返回相同的hashCode。这在HashMap等类的使用中是非常有用的。
套用之前的Person类,我们知道Set具有去重功能,但是如果不重写hashCode,纵然两个Person实例是等价的,也是不能达到去重的效果
public void testHash() throws Exception { HashSet<Person> set = new HashSet<>(); Person a = new Person("123", 123); Person b = new Person("123", 123); set.add(a); set.add(b); System.out.print("set size : " + set.size());} 输出 set size : 2
下面重写hashCode方法使其与equals方法相关
@Overridepublic int hashCode() { return (int) identity;}重新run后输出set size : 1
显然这样比较符合人的习惯,这种影响广泛存在于基于hashCode的容器类中,例如HashMap的contains方法等。
所以,一般来说重写了equals方法就需要重写hashCode方法。比如Object中equals判断的是否为相同的引用,因此hashCode基于引用内存地址返回。在Integer中判断的是Integer的value值,因此hashCode值则直接返回的是value值。
哈希碰撞
但需要注意的是,hashCode相同的两个实例,equals方法不一定会返回true。因为hashCode是一种空间映射函数,空间大的数据映射至小空间则势必会产生哈希碰撞。
举个简单的例子
String.classpublic int hashCode() { int h = hash; if (h == 0 && count > 0) { for (int i = 0; i < count; i++) { h = 31 * h + charAt(i); } hash = h; } return h;}
上述代码为String源码,String的hashCode方法上转换成函数等同于:
hashCode = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
简单来说就是讲其字符转换成31进制。
我们这样构造两个String实例
public void testHash() throws Exception { char A = 1; char B = 2; char C = 33; String aString = String.valueOf(A) + String.valueOf(B); String bString = String.valueOf(C); System.out.println("aString length: " + aString.length()); System.out.println("bString length: " + bString.length()); System.out.println("aString equals bString: " + aString.equals(bString)); System.out.println("aString hashCode: " + aString.hashCode()); System.out.println("String hashCode: " + bString.hashCode());}
输出为:
aString length: 2bString length: 1aString equals bString: falseaString hashCode: 33String hashCode: 33
显然,两个完全不一样的String对象产生可哈希碰撞。
关于数字31的引申
String equals方法中选31也是为了减少哈希碰撞,引用Effective Java中的原话来说
《Effective Java》之所以选择31,是因为它是个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不是很明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性,就是用移位和减法来代替乘法,可以得到更好的性能:31*i==(i<<5)-i。现在的VM可以自动完成这种优化。
关于奇数,在计算机中,一个数乘偶数表现为该数字左移n位,余位补0,因此可能造成信息丢失。比方说,一个二进制数字X4就可能导致两位数字丢失。
1010 0110 -> 10011000
而奇数则没有这个问题,因为任何一个奇数都可以转换为2的n次方+1,表现在计算机中则是左移n位再加上自己。
所以我们同样可以选择31进制作为hashCode的一种计算方式。
- [Java] Object方法浅析(一): equals与hashCode
- Java中的hashCode()方法与equals(Object)方法
- 浅析java中的hashcode()方法与equals()方法
- java equals与hashCode方法
- Java equals()与hashCode()方法
- Java Object equals() & hashCode()
- Java object equals hashcode
- Java equals() 、hashCode()浅析
- Object与equals与HashCode
- java中hashCode方法与equals方法
- 浅析Java中equals()方法和hashCode方法
- Java面试中hashCode()与equals(Object obj)方法关系的准确回答
- java中hashcode与equals方法
- Java中equals()与hashCode()方法详解
- java 集合中hashcode与equals方法
- java基础入门-hashcode与equals方法
- Java中equals()与hashCode()方法详解
- Java的equals方法与hashcode
- BOM—浏览器对象模型
- 将摄像头数据显示在窗口中并具有录像、截屏功能
- PAT(Basic Level)_1044_火星数字
- 下载maven依赖的方法步骤
- 前端开发工具之chrome
- [Java] Object方法浅析(一): equals与hashCode
- signal ()函数详细介绍
- 面向对象2
- Android data Binding
- JDK源码分析之主要阻塞队列实现类ArrayBlockingQueue -- java消息队列/java并发编程/阻塞队列
- 背包问题
- java sqlite util 工具类,测试类
- HDU5965(64/600)
- lua中的json.decode和json.encode解析