一文说透String的hashCode

一、Java中String的hashCode方法变化

关于String类的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;
}

这个hashCode方法也是大多人都知道并且熟知的,而实际上在JDK9版本的hashCode方法(从JDK9开始改变,只是jdk9中没有hashIsZero这个变量,jdk11中也没有,在jdk15中看到这个变量,只是多了一次判断)并不是这样的代码,具体请看:

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }

这是在jdk9/11中的写法,下面是15中的写法。 下面的介绍基于jdk15,和9/11相比多了一个变量, 增加了一个判断,计算哈希值的时候不用去取字节数组的长度,只要一个变量即可。

    public int hashCode() {
        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;
    }

看是不是有所不一样呢?实际上这里增加了对字符编码的应用。在JDK8及以下的hashCode方法中,存储字符串的对象是char数组,即定义的成员变量是:

private final char[] value;

而在jdk9中,存储字符串的对象则变为了:

private final byte[] value;

是不是确实有所变化呢?实际上还增加了另外的成员变量:

private final byte coder;

与此对应的还有实现逻辑的变化,以及与此相关的构造方法的变化和新增的属性(这些会在下面的介绍中一一说明),而核心算法则并未做修改。

二、分析hashCode方法

2.1 简单代码分析

下面我们一步步地介绍这个hashCode方法。首先看方法的注释:

返回此字符串的哈希码。 字符串对象的哈希码计算为 s [0] * 31 ^(n-1)+ s [1] * 31 ^(n-2)+ ... + s [n-1] 使用int算术,其中s [i]是字符串的第i个字符,n是字符串的长度,^表示幂。 (空字符串的哈希值为零。) 返回值: 此对象的哈希码值。

首先看方法的第一行:

int h = hash;

这个如果不假思索的回答,那就是将hash的值赋值给h变量。而实际上hash是一个私有的不可变的成员变量,重点是它并非是静态的,也就是说它并不是类变量,本身并没有默认值。而网上都说它是类的成员变量,默认值是0,这个当然没有错,可是问题是,如果我们自己在一个类中定义了一个成员变量,然后在一个方法中赋值给方法的局部变量,这显然是无法编译通过的,因为你并没有给这个成员变量赋值。是不是感觉,因为它是成员变量默认值是0的说法有一些敷衍了事了呢?那我们就要看一下到底是在哪里给它赋了值呢?

如果我们在String类中搜索hash的话,会发现有20个,如果全匹配的话会有12个匹配的值。我们一一对这代码去查看的话会发现,给hash赋值的地方只有两个:

一是一个构造函数:

  @HotSpotIntrinsicCandidate
    public String(String original) {
        this.value = original.value;
        this.coder = original.coder;
        this.hash = original.hash;
    }

二是在计算hashCode的方法中:

            if (h == 0) {
                hashIsZero = true;
            } else {
                hash = h;
            }

在二中是将h的值赋值给hash,显然不是在这赋值的,那就很明显了,将hash的值置为默认值为0的地方就是第一个方法中,也就是String的构造函数中。我们查看改构造方法的注释:

初始化新创建的字符串对象,使其表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。除非需要original的显式副本,否则不需要使用此构造函数,因为字符串是不可变的。

而在jdk9之前的版本中该构造方法并不是这样的,而是:

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

关于这样的赋值方式,我们可以依照String中的这个构造方法去写一个类,测试结果变回发现hash已经赋值为0了。这才是真正赋值的地方,所谓默认值为0,而没有找到真正赋值默认的地方我想这有些含糊其辞的。

在jdk8中,java采用了char数组来存储字符串,而从jdk9开始变为了字节数组。java的char类型默认是UTF16编码的,UTF-16编码以16位无符号整数为单位,即无论什么样的字符,都是使用两个字节或四个字节等偶数位个字节来存储。但是LATIN-1也就是ISO8859-1,Latin1是ISO-8859-1的别名,有些环境下写作Latin-1,ISO-8859-1编码是单字节编码。也就是说LATIN-1编码的字符都是占用一个字节的,都用char数组来存储的话会浪费一倍的空间。实际上,String字符串在内存中的占比非常高,即使用LATIN-1编码的字符,这就成为了改善内存占用与性能优化的一个机会,更重要的是:由于 JVM 存储字符串的方式导致 JVM 堆空间通常很大一部分都被字符串所占据。

正是以上种种原因,在JDK9中Sring的存储方式改变成了字节数组,并通过判断编码格式来进行存储、计算哈希、计算长度等。

下面看第二行代码:

if (h == 0 && !hashIsZero) {

这一行代码的意思很明了:如果h=0且hashIsZero为false,即hash不为0,则进行hashcode值的计算,赋值给h。如果h不为0,或者虽然h=0,但是hashIsZero不为false,即为true,那么会直接返回h。

我们来分析一下h和hashIsZero的具体含义。

h是一个局部变量,默认值为0,当初次调用hashCode方法时,如果最终计算的哈希值为0,会将hashIsZero置为true。当再次调用hashCode方法时,h=0,而hashIsZero=true,if条件就不能满足,直接返回h。此时的哈希值同样是0,这是没问题的。如果第一次计算后的哈希值不为0,当再次调用hashCode方法时,直接返回h,即返回第一次计算的哈希值。在第一次调用该方法时,hashIsZero为false,if条件满足,如果最终计算的哈希值为0,会将hashIsZero置为true;当再次调用的时候,if条件中的第二个就不满足了。

总结下来就是:h=0,可能代表初次计算哈希值,h=hash,hash的默认值为0;也可能代表最终计算的哈希值为0,再次调用该方法时,h同样等于0。所以如果只有一个h是否等于0的条件的话这个方法并不完善,因为虽然可以再次满足if条件计算,得到最后的哈希值为0,但是再次调用该方法时,虽然用hash缓存了哈希值,但是需要再次计算得到这个0,显然这是多此一举的。所以第二个判断条件登场了。hashIsZero的默认值为false,意思就是初次计算时,这个标识位不认为哈希值为0,那就要进行计算了。如果最终计算结束,确实不为0,那就真的不为0了,如果计算结束发现哈希值是0,那就把这个标识位置为true,此时才是哈希值真的为0了。

简而言之,h=0,可能代表初次计算的默认值,也可能是最终的哈希值确实为0;hashIsZero=false,同理,可能代表初次计算的默认值,也可能是最终计算的哈希值确实不为false。

2.2 核心代码分析

下面我们看看第三行代码:

h = isLatin1() ? StringLatin1.hashCode(value)
                           : StringUTF16.hashCode(value);

首先是isLatin1()方法,它的返回值是一个布尔类型,它的意思是,是LATIN1编码,如果是,则调用StringLatin1的hashCode方法,否则调用StringUTF16的hashCode方法。这两个类分别是LATIN1编码的类和UTF16的类,至于这两个类的方法不在本文的讨论范围,我们稍后会来分析一下它们各自的hashCode方法。

我们先看看isLatin1()方法:

    boolean isLatin1() {
        return COMPACT_STRINGS && coder == LATIN1;
    }

这个方法很简单,只有一行代码,但是它背后涉及到的知识有一些多,我们简单分析。

2.3 字符串压缩

首先是COMPACT_STRINGS:

    static final boolean COMPACT_STRINGS;

    static {
        COMPACT_STRINGS = true;
    }

它的意思是压缩字符串。在String类中定义了一个静态常量,并用静态代码块初始化为true。在整个String类中并没有其他地方给它赋值,也就是说,这是一个默认值为true的静态常量。这个方法上的注释,大意是:

如果禁用了字符串压缩,则value中的字节是总是用UTF16编码。

对于具有多个可能实现路径的方法,当禁用字符串压缩时,只采用一个代码路径。

实例字段值对于优化JIT编译器来说通常是不透明的。因此,在对性能敏感的地方,首先要对静态布尔值{@code COMPACT_STRINGS}进行显式检查,然后再检查{@code coder}字段,因为静态布尔值{@code COMPACT_STRINGS}将被优化的JIT编译器折叠成常量。

比如下面的代码:

if (coder == LATIN1) { ... }

可以被写成更好的代码

f (coder() == LATIN1) { ... }

或者

 if (COMPACT_STRINGS && coder == LATIN1) { ... }

优化编译器可以将上述代码折叠为:

COMPACT_STRINGS == true => if (coder == LATIN1) { ... }

COMPACT_STRINGS == false => if (false) { ... }

该字段的实际值由JVM注入。静态初始化块用于在此处设置值,以告知此静态final字段不可静态可折叠的,并避免在vm初始化期间出现任何可能的循环依赖性。

首先我们看第一行,这一行说的是字符串压缩的事情,如果禁用的话那字节数组的编码格式就总是UTF16编码了。下面我们说一下字符串压缩。

字符串压缩就是将一个很长的字符串通过算法压缩成短的字符串,并且这种压缩必须是可逆的,也就是说可以解压缩。这其实就是我们平时将多个文件进行压缩发送给别人,也可以将接收到的别人的压缩文件解压,道理是一样的。

其他的注释说的是JIT优化等问题,不在我们讨论范围。

下面的部分源自:Java 9 新特性 - Compact Strings

这篇文章原文翻译自:Compact Strings in Java 9

Compressed String - Java 6

在java中的字符串压缩要从jdk6开始说起。在JDK 6 引入了可选的 Compressed String 功能,为的就是优化字符串在 JVM 中的内存占用。JDK 6 update 21 版本中引入了一个新的虚拟机参数选项:

-XX:+UseCompressedStrings

当此选项启用时,字符串将以 byte[] 的形式存储,代替原来的 char[],因此可以节省一些内存。然而,此功能最终在 JDK 7 中被移除,主要原因在于它将带来一些无法预料的性能问题。

Compact String - Java 9

Java 9 重新采纳字符串压缩这一概念。这意味着无论何时我们创建一个所有字符都能用一个字节的 LATIN-1 编码来描述的字符串,都将在内部使用字节数组的形式存储,且每个字符都只占用一个字节。另一方面,如果字符串中任一字符需要多于 8 比特位来表示时,该字符串的所有字符都统统使用两个字节的 UTF-16 编码来描述。因此基本上能如果可能,都将使用单字节来表示一个字符。所有 JVM 需要的信息都准备就绪,虚拟机参数 CompactString 默认是被启用的,如果想要关闭它,可以使用如下启动参数:

+XX:-CompactStrings

至此,我们知道了COMPACT_STRINGS默认是为true的,那么isLatin1方法的取值就是coder==LATIN1,我们先看一下LATIN1:

@Native static final byte LATIN1 = 0;
@Native static final byte UTF16  = 1;

也就是说,看coder是否等于0,也就是LATIN1编码。下面我们来分析一下coder,也就是编码器,先上代码:

 private final byte coder;

我们看到它被定义成了一个byte类型的常量,下面我们看一下这个字段的注释:

用于对value中的字节进行编码的编码标识符。这个实现中支持的值是LATIN1和UTF16。

实现注意:

该字段受虚拟机信任,如果String的实例是一个常量,则该字段会被常量折叠。在构建之后重写这个字段会导致问题。

也就是说这个编码器的编码支持的是LATIN1和UTF16。

2.4 核心算法

如果是LATIN1编码的,就使用StringLatin1的hashCode方法,否则就使用StringUTF16的hashCode方法。下面我们先看StringLatin1的hashCode方法:

2.4.1 StringLatin1的hashCode方法

    public static int hashCode(byte[] value) {
        int h = 0;
        for (byte v : value) {
            h = 31 * h + (v & 0xff);
        }
        return h;
    }

这个就很简单了就是对字节数组进行遍历,以31作为乘数去乘以每次遍历后的h值(初始值尾0),然后将字节的ASCII码与0xff——十六进制数字,即十进制的255,二进制的8个1——11111111取与,然后将两者相加作为本次遍历的结果赋值给h,供下次遍历作为乘数使用。这是在JDK9以后的hashCode计算方式,在JDK9以前的就很简单,没有关于编码的问题:

    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;
    }

这个是JDK8中的hashCode计算方法,同样是以31作为乘数,相加的部分为被遍历的char的ASCII码,并没有移位操作。关于最终的取值公式,网络上文章很多,我们就不再推导了。而在JDK9中的LATIN1编码的计算公式,是否就不能直接推导为原来的公式了呢?即下面的公式:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

实际上,原来的s[i]为char数组中第i个字符的ASCII码,现在只是将字节进行了移位操作,舍弃了高八位而已。我们可以这么说,是每个字节的低八位的ASCII码。为什么这么说呢?下面我们看一下v & 0xff。

我们上面说道,0xff其实就是二进制的8个1,即11111111,2的8次方-1。取与的两个二进制的对应的数字都为1,结果才为1,否则为0,那么一个数字与0xff取与的话就等于说高八位的数字全部被舍弃了——因为0xff的高八位全部为0,取与后依然为0。低八位和0xff取与后就是它自身——如果某一位为1,则取与后为1,如果为0,则取与后为0。所以说一个数字与0xff取与,等于舍弃自身高八位的值,只留下低八位就是取与后的结果。

现在我们明白了,StringLatin1类的hashCode计算方法就是针对字节数组进行遍历,取字节的低八位然后与31和上次计算的值相乘的结果相加,一样可以推导出那个经典的公式,只不过其中的s[i]是字节数组中的第i个字节的低八位的ASCII码。

2.4.2 StringUTF16的hashCode方法

说完了StringLatin1类的hashCode方法后,我们来看看StringUTF16.hashCode(value):

    public static int hashCode(byte[] value) {
        int h = 0;
        int length = value.length >> 1;
        for (int i = 0; i < length; i++) {
            h = 31 * h + getChar(value, i);
        }
        return h;
    }

我们看到这里多了个length,length的取值为字节数组长度右移一位,即除以2。其中涉及到一个getChar方法,下面我们分写getChar方法。

2.4.3 getChar方法

    static char getChar(byte[] val, int index) {
        assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
        index <<= 1;
        return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
                      ((val[index]   & 0xff) << LO_BYTE_SHIFT));
    }

这里的val就是字节数组,index是字节数组长度除以2后的索引。然后是assert,这是断言,java中的保留字,不是本文讨论的范围,有兴趣的可以自行百度。然后是对index左移1位,然后赋值给自身。看到这是不是感觉有些奇怪了呢?哪里奇怪呢?在计算hashCode方法内,开始遍历之前,将字节数组的长度右移1位,现在通过字节数组去获取char字符的时候又左移一位,怎么想的呢?当然这不是多此一举,下面我们来分析一下。

因为这是UTF16编码的类,也就是在字符串是UTF16编码时的计算方法。上面我们说过,UTF16编码会占用2个字节,现在把字符串以字节数组的形式存储,那么是不是一个UTF16编码的字符会占用字节数组的两位呢?既然如此,那进行遍历的时候要获取该字符的ASCII码,也就是char类型的值,是不是要一次性拿到2个字节,然后针对这两个字节同时进行计算后得到最终的char类型的值,也就是该字符的ASCII码呢?也就是说,存储的时候一个UTF16编码的字符占用字节数组的两位,取的时候同样要一次性取两位,也就是只能拿偶数位的。其实这也是getChar方法的作用,或者说是用意,就是为了拿到对应字符串中该字符的ASCII码。

下面我们分析getChar方法。

index对应字节数组除以2(虽然说右移不一定就是除以2,但是这里的UTF16编码的类的该字节数组的长度必定是偶数)后的值,index取值为0、1、2、3...然后左移后,取值就成了0、2、4、6...我们接着分析最后的return中的方法。

val[index] & 0xff就是每次从字节数组中拿两个字节的第一个,也就是整个字节数组的第一个、第三个、第五个等奇数位的值和0xff取与,关于与0xff取与,我们前面介绍过,就是舍弃高八位,保留低八位。同理,val[index++] & 0xff是取两个字节中的第二个,整个字节数组的偶数位,最终只保留低八位。

接着我们看左移操作,左移的位数是两个常量:

    static final int HI_BYTE_SHIFT;
    static final int LO_BYTE_SHIFT;
    static {
        if (isBigEndian()) {
            HI_BYTE_SHIFT = 8;
            LO_BYTE_SHIFT = 0;
        } else {
            HI_BYTE_SHIFT = 0;
            LO_BYTE_SHIFT = 8;
        }
    }

这里的HI_BYTE_SHIFT和LO_BYTE_SHIFT的意思是高字节移位和低字节移位。它们是静态常量,在静态代码块中进行了赋值。赋值时进行了条件判断,我们看一下isBigEndian方法:

    private static native boolean isBigEndian();

这是一个私有静态本地方法,我们不能打开源码查看它的实现逻辑,但是我们可以试着猜测它是做什么的。从名字上看,其实就是大端字节序。关于字节序我们简单介绍一下,详情请参考:

字节序

Endianness(字节序)

三、什么是字节序?

字节序就是字节存储的顺序。更准确地说,应该是多字节数据的存储顺序。

具体什么意思呢?如果一个字符用一个字节就可以存储,那么放到计算机上就是二进制的数字,不管怎么放都一样。假设现在一个多字节字符,当然同样可以放到计算机中,但是计算机怎么知道它是表示一个字符还是多个字符呢?如果内存地址的增长顺序是从左往右的,那这个多字节字符怎么在内存中存储呢?是低位内存地址存储低位字节、高位内存地址存储高位字节呢?还是说低位内存地址存储高位字节、高位内存地址存储低位字节呢?也就是说,内存地址从左到右,那字节存储是从左往右,还是从右往左呢?如果不同的平台机器都自行其是,那岂不是乱套了吗?所以就有了字节序。

3.1 大端字节序(Big Endian)和小端字节序(Little Endian)

3.1.1 端(endian)的起源

endian”一词来源于乔纳森·斯威夫特的小说格列佛游记。小说中,小人国为水煮蛋该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为“大端派”和“小端派”。以下是1726年关于大小端之争历史的描述:

“我下面要告诉你的是,Lilliput和Blefuscu这两大强国在过去36个月里一直在苦战。战争开始是由于以下的原因:我们大家都认为, 吃鸡蛋前,原始的方法是打破鸡蛋较大的一端,可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了。因此他的父亲,当时的皇帝,就下 了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。老百姓们对这项命令极其反感。历史告诉我们,由此曾经发生过6次叛乱,其中一个皇帝送 了命,另一个丢了王位。这些叛乱大多都是由Blefuscu的国王大臣们煽动起来的。叛乱平息后,流亡的人总是逃到那个帝国去寻求避难。据估计,先后几次 有11000人情愿受死也不肯去打破鸡蛋较小的一端。关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派任何人不得做官。”

1980年,Danny Cohen,一位网络协议的早期开发者,在其著名的论文"On Holy Wars and a Plea for Peace"中,为平息一场关于字节该以什么样的顺序传送的争论,而第一次引用了该词。

3.1.2 大小端介绍

上面提到,高位字节对应高位地址、低位字节对应低位地址,这是小端字节序;高位字节对应低位地址、低位字节对应高危地址,这是大端字节序。有说大头与小头的,有说大尾与小尾的,个人感觉大段与小端更优雅,使用的人更多。

对于数据0x12345678,假设内存地址从0x1000开始,在大端和小端情况下的存放位置为:

内存地址小端模式大端模式
0x10000x780x12
0x10010x560x34
0x10020x340x56
0x10030x120x78

其实无论怎么存储都不会有什么问题,我看到有一些文章说,大端字节序更符合人类的思维习惯,小端字节序更直观,其实这些只是最后的结果而已,不是决定使用大段与小端的原因。准确来说,都是社会、政治、组织相互博弈,或者说是自行决定、商定的结果,就像小说中的说的吃鸡蛋从哪吃的问题。我记得我小时候吃香蕉都是从头开始剥皮,后来听说美国人都是从尾巴剥皮,自从那试了试之后发现,从尾巴开始剥皮感觉用手一撕就好了,比从头剥皮更容易一些,可是中国大多数人依然是从头开始剥皮。其实就是吃个香蕉而已,从哪剥皮都是习惯而已。至于大段与小端的争执也没有太多必要。

不止多字节数据会有字节序,单字节内部的存储一样有字节序,毕竟是8位的数字,这个不作深入讨论。

了解了大端字节序与小端字节序后,我们回来继续分析我们的getChar方法。

四、getChar的核心算法

我们讲到了通过判断是否是大端字节序拉决定高字节移位与低字节移位的值,如果是大端字节序则高字节移位就是8,低字节移位就是0,否则则反之。那getchar的结果就是,双数字节的第一位只去低八位字节并左移0位或8位,然后与双数字节的下一位的左移后的结果取或。

我们假设在java中是大端字节序的存储方式,那么index++位的字节取低八位的值然后左移8位——变成了高八位,低八位补0;index位的字节取低八位的值左移0位,依然是低八位;最后将两者取或,充分利用了高八位与低八位的值。

这种计算方式,将双字节字符偶数位通过舍弃高八位的值,并左移至高八位,奇数位同样舍弃高八位,最后高八位与低八位取或,充分使用了各位字节的低八位的值,而非直接使用16位字节进行取或。降低了产生哈希冲突的概率,也减少了计算成本。我们距离说明。

一个双字节,我们假设它的第一位的二进制表示为:

0  |  0  |  1  |  0  |  0  | 0  | 0  | 1                | 0  |  0  |  1  |  0  |  0  |  1  |  1  |  1

第二位的二进制表示为:

0  |  0  |  1  |  0  |  0 1| 0  | 0  | 1                | 0  |  0  |  1  |  0  |  1 |  1  |  0  |  1

第一个取低八位,第二个取低八位并左移八位变为高八位,最终:

0  |  0  |  0  |  0  |  0  | 0  | 0  |  0                |  0  |  0  |  1  |  0  |  0  |  1  |  1  |  1 ------------------->第一位和0xff取与后只保留低八位

0  |  0  |  1  |  0  |  1 |  1  |  0  |  1               |  0  |  0  |  0  |  0  |  0  |  0  |  0  |  0 ------------------->第二位和0xff取与后只保留低八位并左移八位变为高八位

然后将两者取或:

0  |  0  |  1  |  0  |  1 |  1  |  0  |  1                |  0  |  0  |  1  |  0  |  0  |  1  |  1  |  1------------------->只要有一个为1则为1,否则为0

其实就是取第二位的低八位左移八位后的高八位与第一位的低八位。

当然如果java是小端字节序,只是奇数位与偶数位计算方式的差别,其实都无所谓。那么java是大端还是小端呢?

4.1 字节序在不同平台的实现

谈到字节序的问题,必然牵涉到两大CPU派系,那就是Motorola的PowerPC系列CPU和Intel的x86系列CPU。PowerPC系列采用big endian方式存储数据,而x86系列则采用little endian方式存储数据。

其实,除了计算机内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存,而且所有网络协议也都是采用big endian的方式来传输数据的。所以有时我们也会把big endian方式称之为网络字节序。

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。同样 在网络程序开发时 或是跨平台开发时 也应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug。

主机字节序 就是 小端字节序,现代PC大多采用小端字节序。

BIG-ENDIAN、LITTLE-ENDIAN跟CPU有关,每一种CPU不是BIG-ENDIAN就是LITTLE-ENDIAN。IA架构(Intel、AMD)的CPU中是Little-Endian,而PowerPC 、SPARC和Motorola处理器是Big-Endian。这其实就是所谓的主机字节序。而网络字节序是指数据在网络上传输时是大头还是小头的,在Internet的网络字节序是BIG-ENDIAN。所谓的JAVA字节序指的是在JAVA虚拟机中多字节类型数据的存放顺序,JAVA字节序也是BIG-ENDIAN。

在用C/C++写通信程序时,在发送数据前务必用htonl和htons去把整型和短整型的数据进行从主机字节序到网络字节序的转换,而接收数据后对于整型和短整型数据则必须调用ntohl和ntohs实现从网络字节序到主机字节序的转换。如果通信的一方是JAVA程序、一方是C/C++程序时,则需要在C/C++一侧使用以上几个方法进行字节序的转换,而JAVA一侧,则不需要做任何处理,因为JAVA字节序与网络字节序都是BIG-ENDIAN,只要C/C++一侧能正确进行转换即可(发送前从主机序到网络序,接收时反变换)。如果通信的双方都是JAVA,则根本不用考虑字节序的问题了。

现在我们知道了,java就是大端字节序,那计算方式就是我上面说的了。

最终getChar返回该双字节的字符值,也就是它的ASCII码。同样的,以31作为乘数,以该双字节字符的ASCII码作为想加的条件,依次遍历,最终同样得到了那个公式。只是此时的s[i]变成了,这个双字节字符的第一位的低八位是s[i]的低八位,第二位的低八位左移8位后的高八位是s[i]的高八位。高八位与低八位取或得到一个随机性更高,更不容易冲突的值。

现在我们回到String中的hashCode方法。

如果是Latin1编码格式就使用Latin1的方法计算哈希值,否则使用UTF16的方式计算,最终返回计算的哈希值。至此,整个String的hashCode方法分析完毕。

五、总结

(一)Java的String中的hashCode方法,在jdk9开始变更为字节数组来存储字符串,增加了编码器等变量,到jdk15又增加了hashIsZero的布尔类型的变量,而且在在hashCode方法中增加了一个逻辑判断,将原来的判断字节数组长度的逻辑该为hashIsZero是否为false。

(二)原来是char数组中char的ASCII码作为相加的值,从jdk9更改为了字节的低八位(Latin1编码格式——占用一个字节),或双位字节的第一位的低八位作为char的取值的低八位,第二个的低八位左移八位后的高八位作为高八位(UTF16编码)。

(三)选择31作为计算哈希值的乘数。

(四)不同的字节学存储字节的方式不一样,pc上是小端字节序,网络以及Java中都是大端字节序。

(五)更改过后的方法,其算法逻辑不变,只是实现方式有所变化,哈希冲突的概率更低,更改为字节数组后存储字符串所需的内存会进一步变小了。

 

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北冥牧之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值