大牛的博客http:blog.csdn.netlifetragedy,里面有一系列面试题.本篇就写关于equals和hashcode的.首先,我们提出问题.1.equals和hashcode
大牛的博客http://blog.csdn.net/lifetragedy, 里面有一系列面试题. 本篇就写关于equals和hashcode的.
首先,我们提出问题.
1.equals和hashcode有没有什么样的规范呢?
2.Object类的equals和hashcode方法是什么样的?
3.什么情况下需要重写equals和hashcode方法?重写equals的话必须重写hashcode吗?
4.如果两个对象equals,那么hashcode是否一定相等? 反之呢?
5.怎么重写equals方法和hashcode方法?
6.重写hashcode时,为什么要使用31这个数字,别的数字可以吗?
OK. 有了上面的问题,下面我们就来解决这些问题.
1.equals和hashcode有没有什么样的规范呢?
关于这2个方法的规范,我网上看到了很多,后来才发现,打开Object类的源码,找到这2个方法,方法注释上面就有说明.
可能主要因为注释是英文,所以一直跳过了. OK,如果害怕英文,那么找一个中文的API文档.上面也一样有.
简单的记下(equals的reflexive(自反性),symmetric(对称性),transitive(传递性),consistent(一致性)
hashcode的同一对象多次调用返回相同值...equals不等,hashcode不一定不等..)
关于这个规范的详细介绍,这里就不引用了,自己去看文档吧,最准确.(网上有些地方还写错了....)
(知道这个规范很重要,牵扯到第3,4问)
2.Object类的equals和hashcode方法是什么样的?
上面第一点,主要是看的Object源码的注释,这次我们看Object源码的实现.
equals方法的源码很简单,直接用==比较. ==的比较,是直接比较栈上的值,即引用本身.所以除非是同一个对象引用,在没有重写equals的情况下,
默认的equals都是false.
hashcode方法的源码更简单,只有一句 public native int hashCode();
这里面native关键字我的理解就是这个方法用另外的语言实现了...
hashcode的源码没有看头,但是注释最后一短话确说明了它是怎么运行的,直接上汉字注释.
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
这里我们可以得到结论,Object的hashcode方法,是根据内存地址来的,不同对象hashcode肯定不同.
3.什么情况下需要重写equals和hashcode方法?重写equals的话必须重写hashcode吗?
首先:我们说equals方法, 当一个类要实现一个"逻辑相等" 的概念时,就需要重写equals方法.
逻辑相等是什么意思? 例如有一个user类,有多个对象,当某些对象的身份证这个属性相等时,我们就认为这2个对象相等,
很明显,这就是逻辑上的相等(也就是业务上的相等),在内存中是不等的.
其次:我们说hashcode方法. 我们重写hashcode方法,主要是因为第1点中的规范性.
例如根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode方法,它们仅仅是两个对象。因此,违反了“相等的对象必须具有相等的散列码”.
再次:我们讨论,重写equals方法,必须重写hashcode吗?
上面我们讨论,重写hashcode主要是为了规范性.如果我们破坏这个规范,会怎么样? 答案是不怎么样.编译器也不会报错,程序依然运行.
但是这里有一个问题,就是当把对象放到set的时候,set是不允许有重复的对象,那怎么判断是否重复呢?
就需要调用对象的equals方法.当set值比较多的时候,这样比较效率就比较低.于是,有人就发明了一种哈希算法来提高从集合中查找元素的效率,
这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,
根据一个对象的哈希码就可以确定该对象应该存储的那个区域.
这样,判断一个对象是否存在的时候,就可以先通过hashcode来判断,如果hashcode不等.那么对象就不等.
如果hashcode相等,再调用equals方法判断是否相等. 这样会提高效率.
最后:我们再来钻一次牛角尖,我们破坏规范,我们也不图提高效率,看看代码会什么样.
import java.util.HashSet;
import java.util.Set;
public class TestMain {
public static void main(String[] args) {
Person1 p1= new Person1();
Person1 p2= new Person1();
p1.setName("张三");
p2.setName("张三");
System.out.println(p1.equals(p2));
Set set = new HashSet();
set.add(p1);
set.add(p2);
System.out.println(set.size());
}
}
class Person1{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person1 other = (Person1) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
运行结果:
true
2
...............set里面竟然放了2个equals相等的值. 通过这个例子,你会发现,规范还是遵守的好,
4.如果两个对象equals,那么hashcode是否一定相等? 反之呢?
1).两个对象equals,那么hashcode一定相等.(规范的情况一定等,不规范的情况不一定等)
2).hashcode相等,不一定equals.(可以想set集合add对象的时候,需要先判断hashcode,再判断equals,所以这一点很容易得到.)
5.怎么重写equals方法和hashcode方法?
怎么重写,so easy, eclipse里面右键-source-genreate hashcode and equals 搞定.
等等,面试的时候没有eclipse怎么办?总要说出个大概吧.
这里把大牛的方法抄过来.
覆写equals方法
0 使用==符号检查“参数是否为这个对象的引用”
1 使用instanceof操作符检查“实参是否为正确的类型”。
2 对于类中的每一个“关键域”,检查实参中的域与当前对象中对应的域值。
3. 对于非float和double类型的原语类型域,使用==比较;
4 对于对象引用域,递归调用equals方法;
5 对于float域,使用Float.floatToIntBits(afloat)转换为int,再使用==比较;
6 对于double域,使用Double.doubleToLongBits(adouble)转换为int,再使用==比较;
7 对于数组域,调用Arrays.equals方法。
覆写hashcode
1. 把某个非零常数值,例如17,保存在int变量result中;
2. 对于对象中每一个关键域f(指equals方法中考虑的每一个域):
3, boolean型,计算(f? 0 : 1);
4. byte,char,short型,计算(int);
5. long型,计算(int)(f ^ (f>>>32));
6. float型,计算Float.floatToIntBits(afloat);
7. double型,计算Double.doubleToLongBits(adouble)得到一个long,再执行[2.3];
8. 对象引用,递归调用它的hashCode方法;
9. 数组域,对其中每个元素调用它的hashCode方法。
10. 将上面计算得到的散列码保存到int变量c,然后执行result=37*result+c;
11. 返回result。
我们看一个eclipse自动生成的代码
import java.util.Arrays;
public class TestMain {
private int i;
private boolean b;
private float f;
private double d;
private long l;
private char c;
private byte by;
private short sh;
private String s;
private String[] str;
private Person p;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (b ? 1231 : 1237);
result = prime * result + by;
result = prime * result + c;
long temp;
temp = Double.doubleToLongBits(d);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + Float.floatToIntBits(f);
result = prime * result + i;
result = prime * result + (int) (l ^ (l >>> 32));
result = prime * result + ((p == null) ? 0 : p.hashCode());
result = prime * result + ((s == null) ? 0 : s.hashCode());
result = prime * result + sh;
result = prime * result + Arrays.hashCode(str);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TestMain other = (TestMain) obj;
if (b != other.b)
return false;
if (by != other.by)
return false;
if (c != other.c)
return false;
if (Double.doubleToLongBits(d) != Double.doubleToLongBits(other.d))
return false;
if (Float.floatToIntBits(f) != Float.floatToIntBits(other.f))
return false;
if (i != other.i)
return false;
if (l != other.l)
return false;
if (p == null) {
if (other.p != null)
return false;
} else if (!p.equals(other.p))
return false;
if (s == null) {
if (other.s != null)
return false;
} else if (!s.equals(other.s))
return false;
if (sh != other.sh)
return false;
if (!Arrays.equals(str, other.str))
return false;
return true;
}
public static void main(String[] args) {
}
}
可以看到,虽然有些不一样,但是道理和前面文字描述的都是一样的.
6.重写hashcode时,为什么要使用31这个数字,别的数字可以吗?
我们看到第5点,eclipse自动生成的重写hashcode方法时,用到了final int prime = 31;
为什么是31?这个31能不能替换成别的?
这主要是因为我们第3点来的. 我们知道,在把对象放到set集合的时候,会先比较hashcode,如果hashcode相同,再比较equals.
试想一下,如果我们的对象,很多hashcode都相同,那么,这个用来优化的操作,可能变成退化了.
所以,hashcode应该尽量的为不同对象生成不同的hashcode,在Object.hashcode中也有注释,
(但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能)
为了避免hashcode过多的冲突,使用了31,为什么是31呢?这里引用大牛的解释.
大家都知道,计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!
所谓素数:质数又称素数
素数在使用的时候有一个作用就是如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除!如:我们选择素数3来做系数,那么3*n只能被3和n或者1来整除,我们可以很容易的通过3n来计算出这个n来。这应该也是一个原因!
在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。
31是个神奇的数字,因为任何数n * 31就可以被JVM优化为 (n <<5) -n,移位和减法的操作效率要比乘法的操作效率高的多,对左移现在很多虚拟机里面都有做相关优化,并且31只占用5bits!
OK.关于equals和hashcode的问题写完了,以后遇到相关问题,再补充进来.