浅析hashcode和equals
1. jdk源代码 ——不同类中的hashcode方法
1:Object类的hashCode().返回对象的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。
public native int hashCode();
这就是Object.java中的hashcode源代码
分析:1.使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。
这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的,这也是java的底层机制。
实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。
2.native的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。所以native关键字的函数都是操作系统实现的, java只能调用。
2:String类的hashCode.根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串所在的堆空间相同,返回的哈希码也相同。
String.java 类中的hashcode源代码
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
分析:
String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。
哈希计算公式可以计为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
关于为什么取31为权?主要是因为31是一个奇质数,所以31*i=32*i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。
String s1 = new String("2"); String s2 = "2"; String s3 = "A"; String s4 = "a"; System.out.println(s1.hashCode()); //50 System.out.println(s2.hashCode()); //50 System.out.println(s3.hashCode()+" "+s4.hashCode()); //65 97
3:Integer类,返回的哈希码就是Integer对象里所包含的那个整数的数值。
@Override public int hashCode() { return Integer.hashCode(value); }
Integer i2 = new Integer(2); Integer i3 = new Integer(2); System.out.println(i2.hashCode()+" "+i3.hashCode()); // 2 2
2. jdk源代码 ——不同类中的equals方法
Java语言对equals()的要求如下,这些要求是必须遵循的:
A 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
B 反射性:x.equals(x)必须返回是“true”。
C 类推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。
D 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”
1 . Object.java 类中的equals方法 , 默认比较的是对象的地址,因为只有是相同的地址才会相等.
1 //lang包-- Object.java 中的equals(); 2 public boolean equals(Object obj) { 3 return (this == obj); 4 }
2 . String.java类中的equals方法 , 比较的是对象的值。
判断条件分为四步:
1. 若当前对象和比较的对象是同一个对象,即return true。也就是Object中的equals方法。
2. 若当前传入的对象是String类型,则比较两个字符串的长度,即value.length的长度。 若长度不相同,则return false
3. 若长度相同,则按照数组value中的每一位进行比较,不同,则返回false。若每一位都相同,则返回true。
4. 若当前传入的对象不是String类型,则直接返回false
public boolean equals(Object anObject) { //如果是同一个对象 if (this == anObject) { return true; } //如果传递进来的参数是String类的实例 if (anObject instanceof String) { String anotherString = (String)anObject; int n = count;//字符串长度 if (n == anotherString.count) //如果长度相等就进行比较 { char v1[] = value;//取每一个位置的字符 char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) //对于每一位置逐一比较 { if (v1[i++] != v2[j++]) return false; } return true; } } return false; }
3. Integer类中的equals方法 , 首先判断比较的对象是不是Integer类型的,若是,则转为int基本类型,再用 == 比较,比较的就是数值了
public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
Integer类中的intValue方法
public int intValue() {
return value;
}
3. 先提出个问题: 当向 HashSet 集合 (无须,不重复) 中插入对象时,如何判别在集合中是否已经存在该对象了?
分析: 1. 如果先用 equals方法来逐个进行比较,倘若集合中已经存在一万条数据或者更多的数据,效率必然是一个问题。
2. 此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不插入,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了。
说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
下面这段代码是java.util.HashMap的中put方法的具体实现:
public V put(K key, V value) { //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因 if (key == null) return putForNullKey(value); //计算key的hash值 int hash = hash(key.hashCode()); ------(1) //计算key hash 值在 table 数组中的位置 int i = indexFor(hash, table.length); ------(2) //从i出开始迭代 e,找到 key 保存的位置 for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; //判断该条链上是否有hash值相同的(key相同) //若存在相同,则直接覆盖value,返回旧value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; //旧值 = 新值 e.value = value; e.recordAccess(this); return oldValue; //返回旧值 } } //修改次数增加1 modCount++; //将key、value添加至i位置处 addEntry(hash, key, value, i); return null; }
put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。
总结:
HashSet判断两个对象是否相等 必须同时满足两个条件:
- hashcode是否相等
- equals是否相等
4. 几种情况比较
1. 只看java类库中的各个方法,没有自定义重写的情况下。
两个对象==相等,则其hashcode一定相等,反之不一定成立。
两个对象equals相等,则其hashcode一定相等,反之不一定成立。?【 因为Object的equals实现用的就是 对象的==相等来判断】
String s1 = new String("2"); String s2 = new String("2"); String s3 = "4"; String s4 = "4"; System.out.println(s1.hashCode()==s2.hashCode()); //true System.out.println(s1.equals(s2)); //true //Stirng类中重写了equals方法 System.out.println(s1==s2); //false //两个对象的hashcode 相同,== 不一定相同。 System.out.println(s3.hashCode()==s4.hashCode()); //true System.out.println(s3.equals(s4)); //true System.out.println(s3==s4); //true
5. 为什么重写equals时 总是要改写hashcode ?
java.lnag.Object中对hashCode的约定:
1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。
2. 如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。
3. 如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。
根据上一个问题,实际上我们已经能很简单的解释这一点了,比如改写String中的equals为基于内容上的比较而不是内存地址的话,那么虽然equals相等,但并不代表内存地址相等,由hashcode方法的定义可知内存地址不同,没改写的hashcode值也可能不同。所以违背了第二条约定。
又如new一个对象,再new一个内容相等的对象,调用equals方法返回的true,但他们的hashcode值不同,将两个对象存入HashSet中,会使得其中包含两个相等的对象,因为是先检索hashcode值,不等的情况下才会去比较equals方法的。
6.根据实际需求重写equals 方法和 hashcode 方法(常见的写法如下)
@Override public boolean equals(Object obj){ if(obj == null){ return false; } if(this == obj){ return false; } if(obj instanceof Stduent){ Stduent ss = (Stduent)obj; if(this.id==ss.id&&this.name.equals(ss.name)){ return true; } } return false; } @Override public int hashCode() { //return this.id+this.name.hashCode();//返回的是跟塑所有属性值 相关 的int值 //上面这种写法产生的问题是不同的属性值相加之后 可能会存在相同的结果 //解决办法: 将每个属性值用字符串进行拼接,不会去运算,也就不会产生上述问题 String s = "" + this.id+this.name.hashCode(); return Integer.parseInt(s); }