String源码中的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 (310+99)31+111
2 100 ((310+99)31+111)*31+100
3 101 (((310+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编码表


    在这里插入图片描述

你可能感兴趣的:(String源码中的hashCode算法)