本文参考:
Object中的hashCode()终于搞懂了!!!_马走日mazouri的博客-CSDN博客_object hashcode
Java加密:常见哈希算法总结_m.j.y.的博客-CSDN博客
Java Object类中的hashCode方法_qq_18974899的博客-CSDN博客_object类中的hashcode方法
Java对象的hashcode()方法_刘艾可的博客-CSDN博客_对象的hashcode
Java中的Hash值的计算方式,java哈希算法简单数据类型的具体实现_了迹奇有没的博客-CSDN博客
摘要:
深入浅出说清楚Java Object的hashCode方法及其在Integer、String以及Objects类中的重写,以及在HashMap中的应用
文章目录
- Object中的hashCode方法
- 哈希碰撞(哈希冲突)
- Integer类型重写的hashCode()
- String类型重写的hashCode()方法
- Objects的hash(Object... values) 方法
- HashMap中的hashCode()和equals()
Object中的hashCode方法
hashCode方法用来返回对象的哈希值,提供该方法是为了支持哈希表,例如HashMap,HashTable等,在Object类中的代码如下:
@IntrinsicCandidate
public native int hashCode();
这是一个native声明的本地方法,返回一个int型的整数。由于在Object中,因此每个对象都有一个默认的哈希值。
在openjdk8根路径/hotspot/src/share/vm/runtime路径下的synchronizer.cpp文件中,有生成哈希值的代码,默认为Marsaglia XORshift随机数算法。
对于默认的Marsaglia XORshift随机数算法需要了解的是,不同对象的结果一般不同,即使对象的属性相同,生成的结果也不一定相同,事实上大部分文章都认为它是和生成的对象的堆内存地址相关的,即使是相同属性的对象,它们在堆中的内存也通常不同,确实可以这么理解,但是从源码中看出来是不太准确的:
/**
* 测试HashCode生成
* -XX:hashCode=
* 0 - Park-Miller RNG: seed = (16807*seed) % (2147483947)
* 1 - 跟内存地址有关
* 2 - 固定返回1
* 3 - 自增
* 4 - 跟内存地址有关
* 默认 - Marsaglia XORshift随机数算法
*/
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// 返回随机数
value = os::random() ;
} else
if (hashCode == 1) {
//用对象的内存地址根据某种算法进行计算
intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
// 始终返回1,用于测试
value = 1 ;
} else
if (hashCode == 3) {
//从0开始计算哈希值
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
//输出对象的内存地址
value = cast_from_oop<intptr_t>(obj) ;
} else {
// 默认的hashCode生成算法,利用xor-shift算法产生伪随机数
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value; //返回hash值
}
源码中的hashCode其实是JVM启动的一个参数,每一个分支对应一个生成策略,通过-XX:hashCode
可以切换hashCode的生成策略。
下面验证第2种生成策略,用软件idea
输入参数-XX:hashCode=2
,可以看到输出结果正是1,从而进一步验证了上面的源码。
测试和运行结果如下:
哈希碰撞(哈希冲突)
哈希碰撞是指,在计算哈希值时,两个不同对象的输入得到了相同的输出,比如:
@Test
public void test2(){
System.out.println("AaAaAa".hashCode()); // 1952508096
System.out.println("BBAaBB".hashCode());; // 1952508096
System.out.println("通话".hashCode());; // 1179395
System.out.println("重地".hashCode()); // 1179395
}
碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,hashcode()输出是4字节整数,最多只有4294967296种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
冲突解决策略:
-
1、分离链表法(拉链法)
在每一个相同的散列地址上构建链表。
-
2、闭散列方法(开放地址法)
Integer类型重写的hashCode()
直接返回原值
/**
* Returns a hash code for this {@code Integer}.
*
* @return a hash code value for this object, equal to the
* primitive {@code int} value represented by this
* {@code Integer} object.
*/
@Override
public int hashCode() {
return Integer.hashCode(value);
}
/**
* Returns a hash code for an {@code int} value; compatible with
* {@code Integer.hashCode()}.
*
* @param value the value to hash
* @since 1.8
*
* @return a hash code value for an {@code int} value.
*/
public static int hashCode(int value) {
return value; // 直接返回原值
}
包装类型全都重写了hashcode方法
1)、Byte、Short、Integer、Long对应的hashcode方法,均是返回原值
2)、Character对应的hashcode方法,返回的是字符对应的ASCII码值(包含0-9、a-z、A-Z以及一些字符)
public static int hashCode(char value) {
return (int)value;
}
3)、Double、Float对应的hashcode方法,返回的是经过计算的值,与地址无关,也和数据类型无关
@Override
public int hashCode() {
return Double.hashCode(value);
}
public static int hashCode(double value) {
long bits = doubleToLongBits(value);
return (int)(bits ^ (bits >>> 32));
}
public static long doubleToLongBits(double value) {
long result = doubleToRawLongBits(value);
// Check for NaN based on values of bit fields, maximum
// exponent and nonzero significand.
if ( ((result & DoubleConsts.EXP_BIT_MASK) ==
DoubleConsts.EXP_BIT_MASK) &&
(result & DoubleConsts.SIGNIF_BIT_MASK) != 0L)
result = 0x7ff8000000000000L;
return result;
}
4)、Boolean对应的hashcode方法,返回的是固定的两个值,true返回1231,false返回1237
@Override
public int hashCode() {
return Boolean.hashCode(value);
}
public static int hashCode(boolean value) {
return value ? 1231 : 1237;
}
String类型重写的hashCode()方法
java.lang.String类重写了hashCode和equals方法,重写后的hashCode、和equals方法和内部的值有关:
重写hashCode()方法:
public int hashCode() {
// The hash or hashIsZero fields are subject to a benign data race,
// making it crucial to ensure that any observable result of the
// calculation in this method stays correct under any possible read of
// these fields. Necessary restrictions to allow this to be correct
// without explicit memory fences or similar concurrency primitives is
// that we can ever only write to one of these two fields for a given
// String instance, and that the computation is idempotent and derived
// from immutable state
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
public static int hashCode(byte[] value) {
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff);
}
return h;
}
重写equals(Object anObject):
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
测试代码:
@Test
public void test3(){
String str1="abc";
String str2="abc ";
System.out.println(str1.hashCode());
System.out.println(str2.hashCode());
}
输出:
96354
2987006
"abc"的hashcode值为96354,那么即为 31*(31*(31*0+97)+98)+99=96354
"abc ",多了个空格,空格的ASCII码值为32,所以 "abc " hashcode 为 96354*31+32 = 2987006
Objects的hash(Object… values) 方法
这个方法常用来重写原来Object的hashCode()方法,可变参数作为输入,可以传入对象的属性,从而根据属性生成哈希值,调用Arrays.hashCode(values)。
// 可变参数作为输入
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
// 调用Arrays.hashCode(values)
public static int hashCode(Object[] a) {
if (a == null)
return 0;
int result = 1;
for (Object element : a) // 根据传入的属性生成哈希值
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
HashMap中的hashCode()和equals()
hashCode() 和 equals()本质上没有任何关系,关键是看你要用到的时候,是不是会创建“类对应的散列表”,像HashSet, Hashtable, HashMap这些容器都会创建「数组」这种散列表。有些equals()纯粹就是比较下对象属性的,不存在散列表,就不需要重写hashCode()函数。如果HashSet没有重写HashCode(),那它存放元素的时候先会用hash(hashCode())%n计算存放元素的桶的位置,再调用equals()比较,如果不重写,大概率属性相等的元素引用地址不同,就不会存在一个桶里,也就不能比较了。
小栗子测试:
package com.jxz.hashCodeTest;
import java.util.HashMap;
public class Person {
int id;
String name;
public Person(int id,String name){
this.id=id;
this.name=name;
}
public static void main(String[] args) {
// key:Person类型 value:Integer类型
HashMap<Person,Integer> map=new HashMap<>();
map.put(new Person(1,"jxz"),100);
System.out.println(map.get(new Person(1,"jxz")));
}
}
输出:
null
我们期望通过map.get(new Person(1,"jxz"))
获取该对象的value,但上面代码的输出结果为null
。本质是map的key不一样,不能取到对应的值。原因一就在于Person类中没有覆盖hashCode
方法,从而导致两个属性相同的实例具有不同的哈希值,原因二在于没有重写equals()
,现在默认比较的还是两个堆对象的地址,不是比较对象内容。HashMap
中get()的核心代码如下:
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return first;
// 上面hash变量的计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
判重的时候同时用上了hashCode()和equals(),if条件表达式中的e.hash == hash
是先决条件,只有相等才会执行&&后面的代码,这样&&短路操作一旦生效,会极大提高程序的效率。hashCode()不同的话对象一定不同;hashCode()相同的话,equals()比较内容相同的话,对象才相同。
因此重写上面的equals和hashCode()方法,让它们根据对象的属性进行比较,用到了Objects的equals和hash方法:
package com.jxz.hashCodeTest;
import java.util.HashMap;
import java.util.Objects;
public class Person {
int id;
String name;
public Person(int id,String name){
this.id=id;
this.name=name;
}
@Override
public boolean equals(Object obj) { // 重写equals
if(this==obj) return true;
if(!(obj instanceof Person)) return false;
Person person=(Person) obj;
return Objects.equals(id,person.id)&&Objects.equals(name,person.name);
}
@Override
public int hashCode() { // 重写hashCode()
return Objects.hash(id,name);
}
public static void main(String[] args) {
// key:Person类型 value:Integer类型
HashMap<Person,Integer> map=new HashMap<>();
map.put(new Person(1,"jxz"),100);
System.out.println(map.get(new Person(1,"jxz")));
}
}
输出:
100