数据结构与算法-HashCode
1.hashCode的作用
hashCode官方文档的定义
hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。
hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
总结:
1.hashCode()会通过算法返回该对象的hash码值
2.在不修改同一对象计算所用信息的情况下,返回的hashCode()相同,反之不同
3.重写equals()方法时,hashCode()方法也需要一起重写,且hashCode()中参与计算的属性最好和equals()相同(保证结果具有更多的散列值)
2.数据在计算机中的存储形式
信息如何储存
在计算机内部,信息都是釆用二进制的形式进行存储、运算、处理和传输的。信息存储单位有位、字节和字等几种。各种存储设备存储容量单位有KB、MB、GB和TB等几种
基本的存储单元
-
位(bit):二进制数中的一个数位,可以是0或者1,是计算机中数据的最小单位。
-
字节(Byte,B):计算机中数据的基本单位,每8位组成一个字节。各种信息在计算机中存储、处理至少需要一个字节。例如,一个ASCII码用一个字节表示,一个汉字用两个字节表示。
-
字(Word):两个字节称为一个字。汉字的存储单位都是一个字。
存储方式
计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同 [1] 。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理
数据不能直接储存在计算机里,需要转化为二进制,经过反码和补码才能被计算机所识别。
这里有三个概念,既原码、反码、补码
-
原码
原码就是符号位加上真值的绝对值, 即用第一位表示符号(正数为0负数为1),其余位表示值例1:1的绝对值是1,转换为8位的二进制为 0000 0001
因为1是正数,所以符号位为0,最终结果为 0000 0001
[+1] 原码 = 0000 0001例2:-1的绝对值是1,转换为8位的二进制为 0000 0001
因为-1是负数,所以符号位为1,最终结果为 1000 0001
[-1] 原码 = 1000 0001 -
反码
正数的反码是其本身,负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.例3:由例1可知1的源码为 0000 0001
因为1是正数,正数的反码是其本身,最终结果为 0000 0001
[+1] 原码 = 0000 0001
[+1] 反码 = 0000 0001例4:由例2可知-1的源码为 1000 0001
因为-1是负数,负数的反码是除符号位外按位取反,最终结果为 1111 1110
[-1] 原码 = 1000 0001
[-1] 反码 = 1111 1110 -
补码
正数的补码就是其本身,负数的补码是在其反码的基础上+1例5:由例3可知1的反码为 0000 0001
因为1是正数,正数的补码是其本身,最终结果为 0000 0001
[+1] 原码 = 0000 0001
[+1] 反码 = 0000 0001
[+1] 补码 = 0000 0001例6:由例4可知-1的反码为 1111 1110
因为-1是负数,负数的补码是在其反码的基础上+1,最终结果为 1111 1111
[-1] 原码 = 1000 0001
[-1] 反码 = 1111 1110
[-1] 补码 = 1111 1111
3.二进制运算符
java中有以下几种位运算符
<< : 左移运算符,num << 1,相当于num乘以2 低位补0
>> : 右移运算符,num >> 1,相当于num除以2 高位补0
>>> : 无符号右移,忽略符号位,空位都以0补齐
% : 模运算 取余
^ : 位异或 第一个操作数的的第n位于第二个操作数的第n位相反,那么结果的第n为也为1,否则为0
& : 与运算 第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0
| : 或运算 第一个操作数的的第n位于第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0
~ : 非运算 操作数的第n位为1,那么结果的第n位为0,反之,也就是取反运算(一元操作符:只操作一个数)
- 左移运算符( << )
int num = 1;
int result = num << 4;
运算结果为16
图解:
//运算过程
[+1] 补码 = 0000 0001
左移四位 = 0001 0000 = 16(十进制)
- 右移运算符( >> )
int num = 16;
int result = num >> 4;
运算结果为1
//运算过程
[+16] 补码 = 0001 0000
右移四位 = 0000 0001 = 1(十进制)
- 无符号右移( >>> )
int num = -3;
int result = num >>> 3;
运算结果为536870911
因为无符号右移会带着符号位一起移动,负数的最高位是1,1向左移,高位补0,此时变成了正数。
//运算过程
[-3] 补码 = 1111 1111 1111 1111 1111 1111 1111 1101
无符号右移3位 = 0001 1111 1111 1111 1111 1111 1111 1111
由于无符号右移之后为正数,正数的补码和原码是一致的
即 -3>>>3 = 0*2^31+0*2^30+0*2^29+1*2^28+1*2^27+.....+1*2^1+1*2^0 = 536870911
- 位异或( ^ )
int a= 7;
int b= 5;
int result = a ^ b;
运算结果为2
//运算过程 不相等为1,相等为0
[+7] 补码 = 0000 0111
[+5] 补码 = 0000 0101
异或运算 = 0000 0010 = 2(十进制)
- 与运算( & )
int a= 7;
int b= 5;
int result = a & b;
运算结果为5
//运算过程 有两个1则1,否则为0
[+7] 补码 = 0000 0111
[+5] 补码 = 0000 0101
与运算 = 0000 0101 = 5(十进制)
- 或运算( | )
int a= 7;
int b= 5;
int result = a | b;
运算结果为7
//运算过程 只要有1个1则1,两个0则0
[+7] 补码 = 0000 0111
[+5] 补码 = 0000 0101
或运算 = 0000 0111 = 7(十进制)
- 非运算( ~ )
int a= 7;
int result = ~a;
运算结果为-8
//运算过程 按位取反
[+7] 补码 = 0000 0111
非运算 = 1111 1000
反码 = 1111 0111
原码 = 1000 1000 = -8
4.hashCode算法的实现
贴一下源码
public int hashCode() {
int h = hash;//把当前对象的hash值赋予h
if (h == 0 && value.length > 0) {//判断当前hashCode是否计算过,不重复计算
char val[] = value;//把字符串装进char数组
//以公式h=s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]计算hash值
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
h=s[0]*31^(n-1) + s[1]31^(n-2) + … + s[n-1]
这个公式怎么来的呢?
来看一个例子
String code = "code";
int h=code.hashCode();
运行h=3059181
我们来一步步看
char val[] = value;
把字符串"code"转为char数组,即获取该字符串每个字符的ASCII编码存入char数组
查ASCII编码表可得
字符 | ASCII码 |
---|---|
c | 99 |
o | 111 |
d | 100 |
e | 101 |
char val[] ={99,111,100,101}
代入循环
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
i | val[i] | h |
---|---|---|
0 | 99 | 31*0+99 |
1 | 111 | (31*0+99)*31+111 |
2 | 100 | ((31*0+99)*31+111)*31+100 |
3 | 101 | (((31*0+99)*31+111)*31+100)*31+101 |
分解因式可得
h = (((31*0+99)*31+111)*31+100)*31+101
= ((99*31+111)*31+100)*31+101
= (99*31^2+111*31+100)*31+101
= 99*31^3+111*31^2+100*31+101
= 3059181
正好是我们测试的结果值,进一步推导
假设数组长度为n,char数组为s,则有
h = 99*31^3+111*31^2+100*31+101
= s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
5.补充说明
-
为什么hashcode算法使用31
在名著 《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:
之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。
原因:
1.31是一个质数,质数的特性是只有1和本身是因子,选择质数可以让结果尽可能的散列
2.hashCode是用int来储存,如果选择的数过大,则结果溢出的可能性也增大,会导致hash值精度丢失,散列结果不理想
3.31接近2的5次幂, VM 可以自动完成移位优化,获得更好的性能
6.附录
- ASCII编码表