一个问题完整解决过程记录
有时候一些东西不经意听见了,并不当回事,回头就忘了。人总是这么健忘!
学习hebernate的时候,看到一句话,对于需 要映射到数据库中的实体类需要满足几个要求,其中有一个是说对于重写了equals重写后要重写hashcode。相信这是一句老生常谈的话了,大家都不 陌生。当时我也一扫而过。后来看到一个短片说怎样验证一个cookie有效性也提到了hash算法。于是就想验证一下之前的两个重写。
当时随手就写了一段代码(没有用快捷键),如下:
1 public class hash {
2 public static void main(String[] args) {
3
4 HashSet<Student> hs = new HashSet<Student>();
5 Student stu1 = new Student("zhang");
6 Student stu2 = new Student("zhang");
7
8 hs.add(stu1);
9 hs.add(stu2);
10 System.out.println(hs.size());
11 }
12 }
13
14 class Student {
15 String name;
16
17 Student( String name) {
18 this.name = name;
19 }
20
21 // @Override
22 // public int hashCode() {
23 // // TODO Auto-generated method stub
24 // final int INDEX = 12345;
25 // return name.hashCode()*INDEX;
26 // }假若不覆盖hashcode方法会怎样啦!将会发现两个对象stu1和stu2我们期望是同一个,不用存进来了,但是在hash值这一关就没有过,new对象时默认以对象的内存地址映射hash值,也就是new了两个对象的hash值是不同的,
27
28 //因此我们需要覆盖hashcode(),让它按我们的期望来返回hash值
29
30 public int hashcode() {
31
32 final int INDEX = 12345;
33 return name.hashCode()*INDEX;
34 }
35
36
37 public boolean equals(Student stu ) {
38
39 if(this.name.equals(stu.name)){
40 return true;
41 }
42 return false;
43 }
44 }
我期望是结果是set中只插入一个,但结果总是两个,搞得我都有点怀疑set插入的判断过程了,难道不是像记忆中的那样首先判断hash值,如果相等就 判断equals,若equals相等就是说明是同一个,就不插入吗!郁闷了半天搞得。后来我说打印一下hash值看一下啦,结果吓我一大跳,怎么有两 个.hashcode()方法,难道是我眼花吗,再仔细一看,打自己耳光的心都有,怎么可以将hashCode()写成hashcode(),赶紧改过 来,但还是不对,问题又在哪里啦,这不是坑吗,短短的几行代码,为什么就是不能按我的想法来啦,郁闷。一共就两个方法,那在看看是不是equals啦,好 吧,懒得自己写了,还是用快捷键了,一个alt+shift+s,覆盖equals看看,结果又让我不爽了,我的刚才的那个equals怎么就是 Student参数啦,好吧,对自己无语了(说到这里就顺带说一下overload和override,前者是在同一个类中根据参数的不同重载,后面的是 子类覆盖父类的方法,除了方法体里的东西不一样外,其余的都一样)。赶快修改过来,再运行,出现了自己想要的结果。
1 public class hash { 2 public static void main(String[] args) { 3 4 HashSet<Student> hs = new HashSet<Student>(); 5 Student stu1 = new Student("zhang"); 6 Student stu2 = new Student("zhang"); 7 8 hs.add(stu1); 9 hs.add(stu2); 10 System.out.println(hs.size()); 11 } 12 } 13 14 class Student { 15 String name; 16 17 Student( String name) { 18 this.name = name; 19 } 20 21 // @Override 22 // public int hashCode() { 23 // // TODO Auto-generated method stub 24 // final int INDEX = 12345; 25 // return name.hashCode()*INDEX; 26 // }假若不覆盖hashcode方法会怎样啦!将会发现两个对象stu1和stu2我们期望是同一个,不用存进来了,但是在hash值这一关就没有过,new对象时默认以对象的内存地址映射hash值,也就是new了两个对象的hash值是不同的, 27 28 //因此我们需要覆盖hashcode(),让它按我们的期望来返回hash值 29 @Override 30 public int hashCode() { 31 // TODO Auto-generated method stub 32 final int INDEX = 12345; 33 return name.hashCode()*INDEX; 34 } 35 36 @Override 37 public boolean equals(Object obj) { 38 // TODO Auto-generated method stub 39 Student stu =(Student)obj; 40 if(this.name.equals(stu.name)){ 41 return true; 42 } 43 return false; 44 } 45 }
下面就说说如果我们不覆盖hashcode会是什么后果啦。按照我们的想法,stu1和stu2有同样的名字是同一个人,我们希望不要同时都放到set 中,但是我们如果不覆盖hashcode,在new对象的时候就会将对象的内存地址映射成hash值,就会导致两个对象拥有不同的hash值,当然这不是 我们想要的结果,所以我们需要覆盖上诉的两个方法次啊是完整的。
写到这里或者事情已经完美的解决了,但是再来看看下面这种情况就会发现原来上面说的完全不是那么一回事。真是让人头大啊!
1 public class HashcodeTest {
2 public static void main(String[] args) {
3 HashSet<Person> set1 = new HashSet<Person>();
4 Person p1 = new Person();
5 Person p2 = p1;
6 p1.setName("mike");
7 set1.add(p1);
8 set1.add(p2);
9 System.out.println(set1.size());
10 }
11 }
12 class Person{
13 String name;
14 public String getName() {
15 return name;
16 }
17 public void setName(String name) {
18 this.name = name;
19 }
20
21 @Override
22 public int hashCode() {
23 // TODO Auto-generated method stub
24 final int INDEX = 12345;
25 return name.hashCode()*INDEX;
26 }
27
28 public boolean equals(Object obj) {
29 // TODO Auto-generated method stub
30 Person p = (Person)obj;
31
32 if(name.equals(p.getName())){
33 return false;
34 }else {
35 return true;
36 }
37 }
38 }
--------------------------------重点------------------------------------------------------------------------------
按照我们上面说的,这里应该会添加两个进去才对啊,但是结果出乎我们的预料,这里真的只加入了一个。好吧,明明我的hashCode和equals全都 是好好地写的,那问题出在哪里了。有人会说你不用红色的标出来了嘛,但那不是根本的问题,那只是产生这个问题的表象原因。为了解决这个问题,我在第7行处 加上断点,一步一步的调试跟踪代码(话说虽然平时总会用到断点调试,但都是F6的,很少去看过源码,因为觉得看源码是一件好高大上的事情,我等小辈还 是.....),但这次是真的需要去看源码了,当我调试到这个地方的时候,那个地方啦,请看如下图:蓝色的部分就是这次的重点。当代吗运行到这里的时候我 觉得问题好像出在这里了(当然不是源码错了,要不那还了得),当源码里的变量我看不了,怎么办啦,于是我把这句话拿出来了,写在我自己程序中。
1 public class hash { 2 public static void main(String[] args) { 3 4 HashSet<Student> hs = new HashSet<Student>(); 5 Student stu1 = new Student("zhang"); 6 Student stu2 = new Student("zhang"); 7 stu2 = stu1; 8 System.out.println(stu1.hashCode()); 9 System.out.println(stu2.hashCode()); 10 11 stu1.me(stu2); 12 13 if(hs.add(stu1)){ 14 System.out.println("ok"); 15 } 16 if(hs.add(stu2)){ 17 System.out.println("ok"); 18 } 19 System.out.println(hs.size()); 20 } 21 } 22 23 class Student { 24 String name; 25 26 Student( String name) { 27 this.name = name; 28 } 29 30 // @Override 31 // public int hashCode() { 32 // // TODO Auto-generated method stub 33 // final int INDEX = 12345; 34 // return name.hashCode()*INDEX; 35 // }假若不覆盖hashcode方法会怎样啦!将会发现两个对象stu1和stu2我们期望是同一个,不用存进来了,但是在hash值这一关就没有过,new对象时默认以对象的内存地址映射hash值,也就是new了两个对象的hash值是不同的, 36 37 //因此我们需要覆盖hashcode(),让它按我们的期望来返回hash值 38 @Override 39 public int hashCode() { 40 // TODO Auto-generated method stub 41 final int INDEX = 12345; 42 return name.hashCode()*INDEX; 43 } 44 public void me(Student stu){ 45 if (stu.hashCode() == hashCode() && ((this == stu)|| this.equals(stu))) { 46 System.out.println("yes"); 47 } 48 } 49 @Override 50 public boolean equals(Object obj) { 51 // TODO Auto-generated method stub 52 Student stu =(Student)obj; 53 if(this.name.equals(stu.name)){ 54 return false; 55 } 56 return true; 57 } 58 }
其他部分都是一样的,只是我添加了一个me()方法,方法中就是从源码里面拿出来的一句话。当我注释掉 stu2 = stu1;的时候不会输出yes,不注释的时候会输出yes。再来仔细看看这句话if (stu.hashCode() == hashCode() && ((this == stu)|| this.equals(stu))),里面有三个判断。好了,这里就是重点,我想要纠正一下自己上面说的一句话,这句话也是我在好多网上看到的“首先判断hash值,如果相等就判断equals,若equals相等就是说明是同一个,就不插入吗!”事实就摆在眼前,除了判断上面说的hash值和equals外,还另外有判断==(即引用是不是相等的)。这点是很重要的,上面添加 stu2 = stu1;不能成功加入进去的原因就是“=="在判断两者引用的时候返回true了,即使后面返回false也没用。
我 想到这里问题才算正真的解决了,或许有点小题大做,或许是自讨没趣,也许上面我说的在别人看来就没这么一回事,用了hsahCode和equals这么多 年了,还用你来说。但是我只觉得这是我从发现问题一直到最后解决了这个问题的完整过程,我觉得这个过程是有意义的。并且在期间我还发下了一个长期理解错误 的地方,hashset添加不重复的元素除了判断hashCode的返回值和equals外还会判断两者的引用。我想这点就足够了。