广西网站建设开发团队,怎么在百度上能搜到自己的网站,成都广告公司排名,渭南网站建设远景重写equal()时为什么也得重写hashCode()之深度解读以及equal方法与hashCode方法渊源转载自#xff1a;http://blog.csdn.net/javazejian/article/details/51348320
今天这篇文章我们打算来深度解读一下equal方法以及其关联方法hashCode()#xff0c;我们准备从以下几点入手分…
重写equal()时为什么也得重写hashCode()之深度解读以及equal方法与hashCode方法渊源转载自http://blog.csdn.net/javazejian/article/details/51348320
今天这篇文章我们打算来深度解读一下equal方法以及其关联方法hashCode()我们准备从以下几点入手分析1.equals()的所属以及内部原理即Object中equals方法的实现原理
说起equals方法我们都知道是超类Object中的一个基本方法用于检测一个对象是否与另外一个对象相等。而在Object类中这个方法实际上是判断两个对象是否具有相同的引用如果有它们就一定相等。其源码如下[java] view plaincopypublic boolean equals(Object obj) { return (this obj); } 实际上我们知道所有的对象都拥有标识(内存地址)和状态(数据)同时“”比较两个对象的的内存地址所以说 Object 的 equals() 方法是比较两个对象的内存地址是否相等即若 object1.equals(object2) 为 true则表示 equals1 和 equals2 实际上是引用同一个对象。
2.equals()与‘’的区别
或许这是我们面试时更容易碰到的问题”equals方法与‘’运算符有什么区别“并且常常我们都会胸有成竹地回答“equals比较的是对象的内容而‘’比较的是对象的地址。”。但是从前面我们可以知道equals方法在Object中的实现也是间接使用了‘’运算符进行比较的所以从严格意义上来说我们前面的回答并不完全正确。我们先来看一段代码并运行再来讨论这个问题。
[java] view plaincopypackage com.zejian.test; public class Car { private int batch; public Car(int batch) { this.batch batch; } public static void main(String[] args) { Car c1 new Car(1); Car c2 new Car(1); System.out.println(c1.equals(c2)); System.out.println(c1 c2); } } 运行结果
falsefalse
分析对于‘’运算符比较两个Car对象返回了false这点我们很容易明白毕竟它们比较的是内存地址而c1与c2是两个不同的对象所以c1与c2的内存地址自然也不一样。现在的问题是我们希望生产的两辆的批次batch相同的情况下就认为这两辆车相等但是运行的结果是尽管c1与c2的批次相同但equals的结果却反回了false。当然对于equals返回了false我们也是心知肚明的因为equal来自Object超类访问修饰符为public而我们并没有重写equal方法故调用的必然是Object超类的原始方equals方法根据前面分析我们也知道该原始equal方法内部实现使用的是运算符所以返回了false。因此为了达到我们的期望值我们必须重写Car的equal方法让其比较的是对象的批次即对象的内容而不是比较内存地址于是修改如下[java] view plaincopyOverride public boolean equals(Object obj) { if (obj instanceof Car) { Car c (Car) obj; return batch c.batch; } return false; } 使用instanceof来判断引用obj所指向的对象的类型如果obj是Car类对象就可以将其强制转为Car对象然后比较两辆Car的批次相等返回true否则返回false。当然如果obj不是 Car对象自然也得返回false。我们再次运行
truefalse
嗯达到我们预期的结果了。因为前面的面试题我们应该这样回答更佳总结默认情况下也就是从超类Object继承而来的equals方法与‘’是完全等价的比较的都是对象的内存地址但我们可以重写equals方法使其按照我们的需求的方式进行比较如String类重写了equals方法使其比较的是字符的序列而不再是内存地址。
3.equals()的重写规则
前面我们已经知道如何去重写equals方法来实现我们自己的需求了但是我们在重写equals方法时还是需要注意如下几点规则的。
自反性。对于任何非null的引用值xx.equals(x)应返回true。对称性。对于任何非null的引用值x与y当且仅当y.equals(x)返回true时x.equals(y)才返回true。传递性。对于任何非null的引用值x、y与z如果y.equals(x)返回truey.equals(z)返回true那么x.equals(z)也应返回true。一致性。对于任何非null的引用值x与y假设对象上equals比较中的信息没有被修改则多次调用x.equals(y)始终返回true或者始终返回false。对于任何非空引用值xx.equal(null)应返回false。
当然在通常情况下如果只是进行同一个类两个对象的相等比较一般都可以满足以上5点要求下面我们来看前面写的一个例子。
[java] view plaincopypackage com.zejian.test; public class Car { private int batch; public Car(int batch) { this.batch batch; } public static void main(String[] args) { Car c1 new Car(1); Car c2 new Car(1); Car c3 new Car(1); System.out.println(自反性-c1.equals(c1) c1.equals(c1)); System.out.println(对称性); System.out.println(c1.equals(c2)); System.out.println(c2.equals(c1)); System.out.println(传递性); System.out.println(c1.equals(c2)); System.out.println(c2.equals(c3)); System.out.println(c1.equals(c3)); System.out.println(一致性); for (int i 0; i 50; i) { if (c1.equals(c2) ! c1.equals(c2)) { System.out.println(equals方法没有遵守一致性); break; } } System.out.println(equals方法遵守一致性); System.out.println(与null比较); System.out.println(c1.equals(null)); } Override public boolean equals(Object obj) { if (obj instanceof Car) { Car c (Car) obj; return batch c.batch; } return false; } } 运行结果
自反性-c1.equals(c1)true对称性truetrue传递性truetruetrue一致性equals方法遵守一致性与null比较false
由运行结果我们可以看出equals方法在同一个类的两个对象间的比较还是相当容易理解的。但是如果是子类与父类混合比较那么情况就不太简单了。下面我们来看看另一个例子首先我们先创建一个新类BigCar继承于Car,然后进行子类与父类间的比较。
[java] view plaincopypackage com.zejian.test; public class BigCar extends Car { int count; public BigCar(int batch, int count) { super(batch); this.count count; } Override public boolean equals(Object obj) { if (obj instanceof BigCar) { BigCar bc (BigCar) obj; return super.equals(bc) count bc.count; } return false; } public static void main(String[] args) { Car c new Car(1); BigCar bc new BigCar(1, 20); System.out.println(c.equals(bc)); System.out.println(bc.equals(c)); } } 运行结果
truefalse
对于这样的结果自然是我们意料之中的啦。因为BigCar类型肯定是属于Car类型所以c.equals(bc)肯定为true对于bc.equals(c)返回false是因为Car类型并不一定是BigCar类型Car类还可以有其他子类。嗯确实是这样。但如果有这样一个需求只要BigCar和Car的生产批次一样我们就认为它们两个是相当的在这样一种需求的情况下父类Car与子类BigCar的混合比较就不符合equals方法对称性特性了。很明显一个返回true一个返回了false根据对称性的特性此时两次比较都应该返回true才对。那么该如何修改才能符合对称性呢其实造成不符合对称性特性的原因很明显那就是因为Car类型并不一定是BigCar类型Car类还可以有其他子类在这样的情况下(Car instanceof BigCar)永远返回false因此我们不应该直接返回false而应该继续使用父类的equals方法进行比较才行因为我们的需求是批次相同两个对象就相等父类equals方法比较的就是batch是否相同。因此BigCar的equals方法应该做如下修改
[java] view plaincopyOverride public boolean equals(Object obj) { if (obj instanceof BigCar) { BigCar bc (BigCar) obj; return super.equals(bc) count bc.count; } return super.equals(obj); } 这样运行的结果就都为true了。但是到这里问题并没有结束虽然符合了
对称性却还没符合传递性
实例如下[java] view plaincopypackage com.zejian.test; public class BigCar extends Car { int count; public BigCar(int batch, int count) { super(batch); this.count count; } Override public boolean equals(Object obj) { if (obj instanceof BigCar) { BigCar bc (BigCar) obj; return super.equals(bc) count bc.count; } return super.equals(obj); } public static void main(String[] args) { Car c new Car(1); BigCar bc new BigCar(1, 20); BigCar bc2 new BigCar(1, 22); System.out.println(bc.equals(c)); System.out.println(c.equals(bc2)); System.out.println(bc.equals(bc2)); } } 运行结果
truetruefalse
bcbc2c的批次都是相同的按我们之前的需求应该是相等而且也应该符合equals的传递性才对。但是事实上运行结果却不是这样违背了传递性。出现这种情况根本原因在于
父类与子类进行混合比较。子类中声明了新变量并且在子类equals方法使用了新增的成员变量作为判断对象是否相等的条件。
只要满足上面两个条件equals方法的传递性便失效了。而且目前并没有直接的方法可以解决这个问题。因此我们在重写equals方法时这一点需要特别注意。虽然没有直接的解决方法但是间接的解决方案还说有滴那就是通过组合的方式来代替继承,还有一点要注意的是组合的方式并非真正意义上的解决问题只是让它们间的比较都返回了false从而不违背传递性然而并没有实现我们上面batch相同对象就相等的需求而是让equals方法满足各种特性的前提下让代码看起来更加合情合理代码如下
[java] view plaincopypackage com.zejian.test; public class Combination4BigCar { private Car c; private int count; public Combination4BigCar(int batch, int count) { c new Car(batch); this.count count; } Override public boolean equals(Object obj) { if (obj instanceof Combination4BigCar) { Combination4BigCar bc (Combination4BigCar) obj; return c.equals(bc.c) count bc.count; } return false; } } 从代码来看即使batch相同Combination4BigCar类的对象与Car类的对象间的比较也永远都是false但是这样看起来也就合情合理了毕竟Combination4BigCar也不是Car的子类因此equals方法也就没必要提供任何对Car的比较支持同时也不会违背了equals方法的传递性。
4.equals()的重写规则之必要性深入解读
前面我们一再强调了equals方法重写必须遵守的规则接下来我们就是分析一个反面的例子看看不遵守这些规则到底会造成什么样的后果。
[java] view plaincopypackage com.zejian.test; import java.util.ArrayList; import java.util.List; /** * 反面例子 * author zejian */ public class AbnormalResult { public static void main(String[] args) { ListA list new ArrayListA(); A a new A(); B b new B(); list.add(a); System.out.println(list.contains(a)- list.contains(a)); System.out.println(list.contains(b)- list.contains(b)); list.clear(); list.add(b); System.out.println(list.contains(a)- list.contains(a)); System.out.println(list.contains(b)- list.contains(b)); } static class A { Override public boolean equals(Object obj) { return obj instanceof A; } } static class B extends A { Override public boolean equals(Object obj) { return obj instanceof B; } } } 上面的代码我们声明了 A,B两个类注意必须是static否则无法被main调用。B类继承A两个类都重写了equals方法但是根据我们前面的分析这样重写是没有遵守对称性原则的我们先来看看运行结果
list.contains(a)-truelist.contains(b)-falselist.contains(a)-truelist.contains(b)-true
19行和24行的输出没什么好说的将ab分别加入list中list中自然会含有ab。但是为什么20行和23行结果会不一样呢我们先来看看contains方法内部实现
[java] view plaincopyOverride public boolean contains(Object o) { return indexOf(o) ! -1; } 进入indexof方法[java] view plaincopy Override ublic int indexOf(Object o) { E[] a this.a; if (o null) { for (int i 0; i a.length; i) if (a[i] null) return i; } else { for (int i 0; i a.length; i) if (o.equals(a[i])) return i; } return -1; 可以看出最终调用的是对象的equals方法所以当调用20行代码list.contains(b)时实际上调用了
b.equals(a[i]),a[i]是集合中的元素集合中的类型而且为A类型(只添加了a对象)虽然B继承了A,但此时
[java] view plaincopya[i] instanceof B 结果为falseequals方法也就会返回false而当调用23行代码list.contains(a)时实际上调用了a.equal(a[i]),其中a[i]是集合中的元素而且为B类型(只添加了b对象)由于B类型肯定是A类型B继承了A所以[java] view plaincopya[i] instanceof A 结果为trueequals方法也就会返回true这就是整个过程。但很明显结果是有问题的因为我们的 list的泛型是A,而B又继承了A此时无论加入了a还是b都属于同种类型所以无论是contains(a),还是contains(b)都应该返回true才算正常。而最终却出现上面的结果这就是因为重写equals方法时没遵守对称性原则导致的结果如果没遵守传递性也同样会造成上述的结果。当然这里的解决方法也比较简单我们只要将B类的equals方法修改一下就可以了。[java] view plaincopystatic class B extends A{ Override public boolean equals(Object obj) { if(obj instanceof B){ return true; } return super.equals(obj); } } 到此我们也应该明白了重写equals必须遵守几点原则的重要性了。当然这里不止是list只要是java集合类或者java类库中的其他方法重写equals不遵守5点原则的话都可能出现意想不到的结果。
5.为什么重写equals()的同时还得重写hashCode()
这个问题之前我也很好奇不过最后还是在书上得到了比较明朗的解释当然这个问题主要是针对映射相关的操作Map接口。学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码当我们调用put方法或者get方法对Map容器进行操作时都是根据键对象的哈希码来计算存储位置的因此如果我们对哈希码的获取没有相关保证就可能会得不到预期的结果。在java中我们可以使用hashCode()来获取对象的哈希码其值就是对象的存储地址这个方法在Object类中声明因此所有的子类都含有该方法。那我们先来认识一下hashCode()这个方法吧。hashCode的意思就是散列码也就是哈希码是由对象导出的一个整型值散列码是没有规律的如果x与y是两个不同的对象那么x.hashCode()与y.hashCode()基本是不会相同的下面通过String类的hashCode()计算一组散列码
[java] view plaincopypackage com.zejian.test; public class HashCodeTest { public static void main(String[] args) { int hash0; String sok; StringBuilder sb new StringBuilder(s); System.out.println(s.hashCode() sb.hashCode()); String t new String(ok); StringBuilder tb new StringBuilder(s); System.out.println(t.hashCode() tb.hashCode()); } } 运行结果
3548 18291647003548 2018699554
我们可以看出字符串s与t拥有相同的散列码这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码这是因为StringBuilder没有重写hashCode方法它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址所以散列码自然也就不同了。那么我们该如何重写出一个较好的hashCode方法呢其实并不难我们只要合理地组织对象的散列码就能够让不同的对象产生比较均匀的散列码。例如下面的例子
[java] view plaincopypackage com.zejian.test; public class Model { private String name; private double salary; private int sex; Override public int hashCode() { return name.hashCode()new Double(salary).hashCode() new Integer(sex).hashCode(); } } 上面的代码我们通过合理的利用各个属性对象的散列码进行组合最终便能产生一个相对比较好的或者说更加均匀的散列码当然上面仅仅是个参考例子而已我们也可以通过其他方式去实现只要能使散列码更加均匀所谓的均匀就是每个对象产生的散列码最好都不冲突就行了。不过这里有点要注意的就是java 7中对hashCode方法做了两个改进首先java发布者希望我们使用更加安全的调用方式来返回散列码也就是使用null安全的方法
Objects.hashCode注意不是Object而是java.util.Objects
方法这个方法的优点是如果参数为null就只返回0否则返回对象参数调用的hashCode的结果。
Objects.hashCode 源码如下[java] view plaincopypublic static int hashCode(Object o) { return o ! null ? o.hashCode() : 0; } 因此我们修改后的代码如下[java] view plaincopypackage com.zejian.test; import java.util.Objects; public class Model { private String name; private double salary; private int sex; Override public int hashCode() { return Objects.hashCode(name)new Double(salary).hashCode() new Integer(sex).hashCode(); } } java 7还提供了另外一个方法java.util.Objects.hash(Object... objects),当我们需要组合多个散列值时可以调用该方法。进一步简化上述的代码
[java] view plaincopypackage com.zejian.test; import java.util.Objects; public class Model { private String name; private double salary; private int sex; // Override // public int hashCode() { // return Objects.hashCode(name)new Double(salary).hashCode() // new Integer(sex).hashCode(); // } Override public int hashCode() { return Objects.hash(name,salary,sex); } } 好了到此hashCode()该介绍的我们都说了还有一点要说的如果我们提供的是一个数值类型的变量的话那么我们可以调用Arrays.hashCode()来计算它的散列码这个散列码是由数组元素的散列码组成的。接下来我们回归到我们之前的问题重写equals方法时也必须重写hashCode方法。在Java API文档中关于hashCode方法有以下几点规定原文来自java深入解析一书。
在java应用程序执行期间如果在equals方法比较中所用的信息没有被修改那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时不要求该整数必须相同。如果两个对象通过调用equals方法是相等的那么这两个对象调用hashCode方法必须返回相同的整数。如果两个对象通过调用equals方法是不相等的不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。
通过前面的分析我们知道在Object类中hashCode方法是通过Object对象的地址计算出来的因为Object对象只与自身相等所以同一个对象的地址总是相等的计算取得的哈希码也必然相等对于不同的对象由于地址不同所获取的哈希码自然也不会相等。因此到这里我们就明白了如果一个类重写了equals方法但没有重写hashCode方法将会直接违法了第2条规定这样的话如果我们通过映射表(Map接口)操作相关对象时就无法达到我们预期想要的效果。如果大家不相信, 可以看看下面的例子来自java深入解析一书
[java] view plaincopypackage com.zejian.test; import java.util.HashMap; import java.util.Map; public class MapTest { public static void main(String[] args) { MapString,Value map1 new HashMapString,Value(); String s1 new String(key); String s2 new String(key); Value value new Value(2); map1.put(s1, value); System.out.println(s1.equals(s2):s1.equals(s2)); System.out.println(map1.get(s1):map1.get(s1)); System.out.println(map1.get(s2):map1.get(s2)); MapKey,Value map2 new HashMapKey,Value(); Key k1 new Key(A); Key k2 new Key(A); map2.put(k1, value); System.out.println(k1.equals(k2):s1.equals(s2)); System.out.println(map2.get(k1):map2.get(k1)); System.out.println(map2.get(k2):map2.get(k2)); } /** * 键 * author zejian * */ static class Key{ private String k; public Key(String key){ this.kkey; } Override public boolean equals(Object obj) { if(obj instanceof Key){ Key key(Key)obj; return k.equals(key.k); } return false; } } /** * 值 * author zejian * */ static class Value{ private int v; public Value(int v){ this.vv; } Override public String toString() { return 类Value的值v; } } } 代码比较简单我们就不过多解释了注意Key类并没有重写hashCode方法直接运行看结果
[java] view plaincopys1.equals(s2):true map1.get(s1):类Value的值2 map1.get(s2):类Value的值2 k1.equals(k2):true map2.get(k1):类Value的值2 map2.get(k2):null 对于s1和s2的结果我们并不惊讶因为相同的内容的s1和s2获取相同内的value这个很正常因为String类重写了equals方法和hashCode方法使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了k1获取到的值是2
k2获取到的是null这是为什么呢想必大家已经发现了Key只重写了equals方法并没有重写hashCode方法这样的话equals比较的确实是内容而hashCode方法呢没重写那就肯定调用超类Object的hashCode方法这样返回的不就是地址了吗k1与k2属于两个不同的对象返回的地址肯定不一样所以现在我们知道调用map2.get(k2)为什么返回null了吧那么该如何修改呢很简单我们要做也重写一下hashCode方法即可如果参与equals方法比较的成员变量是引用类型的则可以递归调用hashCode方法来实现
[java] view plaincopyOverride public int hashCode() { return k.hashCode(); } 再次运行
[java] view plaincopys1.equals(s2):true map1.get(s1):类Value的值2 map1.get(s2):类Value的值2 k1.equals(k2):true map2.get(k1):类Value的值2 map2.get(k2):类Value的值2 6.重写equals()中getClass与instanceof的区别
虽然前面我们都在使用instanceof当然前面我们是根据需求批次相同即相等而使用instanceof的但是在重写equals() 方法时一般都是推荐使用 getClass 来进行类型判断除非所有的子类有统一的语义才使用instanceof不是使用 instanceof。我们都知道 instanceof 的作用是判断其左边对象是否为其右边类的实例返回 boolean 类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。下来我们来看一个例子父类Person
[java] view plaincopypublic class Person { protected String name; public String getName() { return name; } public void setName(String name) { this.name name; } public Person(String name){ this.name name; } public boolean equals(Object object){ if(object instanceof Person){ Person p (Person) object; if(p.getName() null || name null){ return false; } else{ return name.equalsIgnoreCase(p.getName ()); } } return false; } } 子类 Employee
[java] view plaincopypublic class Employee extends Person{ private int id; public int getId() { return id; } public void setId(int id) { this.id id; } public Employee(String name,int id){ super(name); this.id id; } /** * 重写equals()方法 */ public boolean equals(Object object){ if(object instanceof Employee){ Employee e (Employee) object; return super.equals(object) e.getId() id; } return false; } } 上面父类 Person 和子类 Employee 都重写了 equals(),不过 Employee 比父类多了一个id属性,而且这里我们并没有统一语义。测试代码如下[java] view plaincopypublic class Test { public static void main(String[] args) { Employee e1 new Employee(chenssy, 23); Employee e2 new Employee(chenssy, 24); Person p1 new Person(chenssy); System.out.println(p1.equals(e1)); System.out.println(p1.equals(e2)); System.out.println(e1.equals(e2)); } } 上面代码我们定义了两个员工和一个普通人虽然他们同名但是他们肯定不是同一人所以按理来说结果应该全部是 false但是事与愿违结果是true、true、false。对于那 e1!e2 我们非常容易理解因为他们不仅需要比较 name,还需要比较 ID。但是 p1 即等于 e1 也等于 e2这是非常奇怪的因为 e1、e2 明明是两个不同的类但为什么会出现这个情况首先 p1.equals(e1)是调用 p1 的 equals 方法该方法使用 instanceof 关键字来检查 e1 是否为 Person 类这里我们再看看 instanceof判断其左边对象是否为其右边类的实例也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系肯定会返回 true 了而两者 name 又相同所以结果肯定是 true。所以出现上面的情况就是使用了关键字 instanceof这是非常容易导致我们“钻牛角尖”。故在覆写 equals 时推荐使用 getClass 进行类型判断。而不是使用 instanceof除非子类拥有统一的语义。7.编写一个完美equals()的几点建议下面给出编写一个完美的equals方法的建议出自Java核心技术 第一卷基础知识1显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量参数名命名强制转换请参考建议52检测this与otherObject是否引用同一个对象 if(this otherObject) return true;存储地址相同肯定是同个对象直接返回true3) 检测otherObject是否为null 如果为null,返回false.if(otherObject null) return false;4) 比较this与otherObject是否属于同一个类 视需求而选择如果equals的语义在每个子类中有所改变就使用getClass检测 if(getClass()!otherObject.getClass()) return false; (参考前面分析的第6点)如果所有的子类都拥有统一的语义就使用instanceof检测 if(!(otherObject instanceof ClassName)) return false;即前面我们所分析的父类car与子类bigCar混合比我们统一了批次相同即相等5) 将otherObject转换为相应的类类型变量ClassName other (ClassName) otherObject;6) 现在开始对所有需要比较的域进行比较 。使用比较基本类型域使用equals比较对象域。如果所有的域都匹配就返回true否则就返回flase。如果在子类中重新定义equals就要在其中包含调用super.equals(other)当此方法被重写时通常有必要重写 hashCode 方法以维护 hashCode 方法的常规协定该协定声明 相等对象必须具有相等的哈希码 。