String 源码学习笔记
一下是源码中 String 类的声明
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {可以看到的是:
- String 类是 final 的,意味着不能被子类继承。
- String 实现了 Serializable 接口,意味着它能被序列化
- String 实现了 Comparable 接口,这表明 String 类实现了此接口的 compareTo 方法,在比较时最好使用此方法进行比较,而不是直接使用
==。在 Stirng 中直接使用==进行比较的话,比较的是两个对象的地址。(compareTo 方法和 equals 方法的区别在于,equals 主要是比较两个字符串内容 “是否相等”,返回值为boolean。compareTo 关心的是 “顺序大小”, - String 和 StringBuffer、StringBuilder 一样,都实现了 CharSequence 接口。由于 String 是不可变的,所以遇到字符串拼接的时候就可以考虑一下 String 的另外两个好兄弟,StringBuffer 和 StringBuilder,它俩是可变的。
String 底层从 char 数组优化为 byte 数组
在 Java9 以前,String 底层是使用 char 数组所实现的,之后就改成了使用 byte 数组实现,然后增加了 coder 来表示编码。 由于 char 占用 2 个字节,byte 占用 1 个字节,这个改动可以使得 String 对象如果存储的都是以单字节字符为主的内容,可以节省一半的内存占用(还能减少 GC 次数)。 但是由于并不是所有字符都是占用一个字节的,改动也导致底层需要再加入编码检测。
查看源码可以得知
/** * The identifier of the encoding used to encode the bytes in * {@code value}. The supported values in this implementation are * * LATIN1 * UTF16 * * @implNote This field is trusted by the VM, and is a subject to * constant folding if String instance is constant. Overwriting this * field after construction will cause problems. */
private final byte coder;coder 为的是区分是 Latin-1 编码还是 UTF-16 编码,也就是说 Java 会根据字符串的内容自动设置为相应的编码,要么 Latin-1 要么 UTF-16。
那么为什么是 UTF-16 而不能是 UTF-8 呢?
UTF-8 和 UTF-16 的编码区别
- UTF-8:
- 是一种可变长度的编码。它使用 1 到 4 个字节来表示一个字符。
- 规则:
- ASCII 字符(U+0000 ~ U+007F):占用1 个字节。编码方式与 ASCII 完全一致,这是其巨大优势。
- 大部分拉丁文、希腊文、阿拉伯文等(U+0080 ~ U+07FF):占用2 个字节。
- 大部分常用汉字(U+0800 ~ U+FFFF):占用3 个字节。
- 非常用汉字、emoji、历史字符等(U+10000 ~ U+10FFFF):占用4 个字节。
- UTF-16:
- 也是一种可变长度的编码。它使用 2 个或 4 个字节(即 1 个或 2 个 16 位码元 )来表示一个字符。
- 规则:
- 基本多文种平面(BMP, U+0000 ~ U+FFFF)的字符:占用2 个字节(1 个码元)。这包含了几乎所有现代常用字符,包括汉字。
- 辅助平面(U+10000 ~ U+10FFFF)的字符:占用4 个字节(2 个码元)。例如,一些罕见的汉字和 emoji(如
😊)。
从上面可以得知,UTF-8 是变长的,并且可能是 1 到 4 个字节中的任意一个,这对于有随机访问方法的类来说,就很不方便。这会导致随机访问需要从头数一遍每个字符的长度才能找到你要的位置。
UTF-16 虽然也是变长的编码,但是它只使用 2 个或 4 个字节来表示一个字符,在 Java 中,一个字符(char)就是 2 个字节,占 4 个字节的字符,在 Java 里也是用两个 char 来存储的,而 String 的各种操作,都是以 Java 的字符(char)为单位的,charAt 是取得第几个 char,subString 取的也是第几个到第几个 char 组成的子串,甚至 length 返回的都是 char 的个数。因此在 Java 中可以将它视为定长的编码。
String 类的 hashCode 方法
[!INFO] 以下的源码来自于 jdk24 版本
首先找到 hashCode 方法对应位置,可以看到根据 Latin-1 和 UTF-16 编码,分成了两种情况。
public int hashCode() { if (h == 0 && !hashIsZero) { h = isLatin1() ? StringLatin1.hashCode(value) : StringUTF16.hashCode(value); if (h == 0) { hashIsZero = true; } else { hash = h; } } return h;}首先看一下 Latin1 的情况:
Latin1 编码的 hashCode 源码
StringLatin1.hashCode(value) :
public static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUnsigned(value, 0, value.length, 0);}继续跳转
ArraysSupport.hashCodeOfUnsigned(value, 0, value.length, 0):
public static int hashCodeOfUnsigned(byte[] a, int fromIndex, int length, int initialValue) { return switch (length) { case 0 -> initialValue; //若字符串长度为0 则直接返回initialValue作为哈希值,这里会直接返回0 case 1 -> 31 * initialValue + Byte.toUnsignedInt(a[fromIndex]); // 如果长度为1,则直接进行一次31-Hash计算并返回 default -> vectorizedHashCode(a, fromIndex, length, initialValue, T_BOOLEAN); // 长度大于1时调用此方法获取哈希值 };}vectorizedHashCode(a, fromIndex, length, initialValue, T_BOOLEAN):
/** * 计算数组的向量化哈希码(使用SIMD等向量指令优化性能) * 此方法由HotSpot VM内部调用,用于高效计算基本类型数组的哈希码 * * @param array 目标数组,必须是基本类型数组(boolean/char/byte/short/int) * @param fromIndex 起始索引(包含) * @param length 要处理的元素数量 * @param initialValue 初始哈希值,通常为1 * @param basicType 数组元素的基本类型标识符,使用JVM的T_*常量 * @return 计算出的哈希码值 * @throws IllegalArgumentException 如果basicType不被支持 * * @implNote 此方法使用@IntrinsicCandidate注解,表示HotSpot VM可能会用特定于CPU架构的 * 内置实现(如使用AVX2指令集)替换该方法调用,以提升计算性能 */@IntrinsicCandidate // 指示HotSpot VM可以为此方法生成内置实现private static int vectorizedHashCode(Object array, int fromIndex, int length, int initialValue, int basicType) { return switch (basicType) { // boolean类型数组:在JVM内部表示为byte数组,使用无符号哈希计算 case T_BOOLEAN -> unsignedHashCode(initialValue, (byte[]) array, fromIndex, length);
// char类型数组:可能是UTF-16编码的byte数组或真正的char数组 case T_CHAR -> array instanceof byte[] ? utf16hashCode(initialValue, (byte[]) array, fromIndex, length) // 处理UTF-16字节序列 : hashCode(initialValue, (char[]) array, fromIndex, length); // 处理普通字符数组
// byte类型数组:直接处理字节数据 case T_BYTE -> hashCode(initialValue, (byte[]) array, fromIndex, length);
// short类型数组:处理16位有符号整数 case T_SHORT -> hashCode(initialValue, (short[]) array, fromIndex, length);
// int类型数组:处理32位有符号整数 case T_INT -> hashCode(initialValue, (int[]) array, fromIndex, length);
// 不支持的类型:抛出异常 default -> throw new IllegalArgumentException("unrecognized basic type: " + basicType); };}这里虽然根据情况调用了不同的方法,但是并不复杂,而查看 hashCode 源码也可以看得出来,String 类使用的哈希计算方法是31 倍哈希法,这是一种简单有效的字符串哈希算法,常用于对字符串进行哈希处理。该算法的基本思想是将字符串中的每个字符乘以一个固定的质数 31 的幂次方,并将它们相加得到哈希值。
private static int unsignedHashCode(int result, byte[] a, int fromIndex, int length) { int end = fromIndex + length; for (int i = fromIndex; i < end; i++) { result = 31 * result + Byte.toUnsignedInt(a[i]); } return result;}
private static int utf16hashCode(int result, byte[] value, int fromIndex, int length) { int end = fromIndex + length; for (int i = fromIndex; i < end; i++) { result = 31 * result + JLA.getUTF16Char(value, i); } return result;}
private static int hashCode(int result, byte[] a, int fromIndex, int length) { int end = fromIndex + length; for (int i = fromIndex; i < end; i++) { result = 31 * result + a[i]; } return result;}假设字符串为 s,长度为 n,则 31 倍哈希值计算公式如下:
//s[i]表示字符串 s 中第 i 个字符的 ASCII 码值,^表示幂运算。H(s) = (s[0] * 31^(n-1)) + (s[1] * 31^(n-2)) + ... + (s[n-1] * 31^0)同理,下面是UTF-16编码时的情况:
UTF-16 编码的 hashCode 源码
StringUTF16.hashCode(value):
public static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUTF16(value, 0, value.length >> 1, 0);}ArraysSupport.hashCodeOfUTF16(value, 0, value.length >> 1, 0):
public static int hashCodeOfUTF16(byte[] a, int fromIndex, int length, int initialValue) { return switch (length) { case 0 -> initialValue; case 1 -> 31 * initialValue + JLA.getUTF16Char(a, fromIndex); default -> vectorizedHashCode(a, fromIndex, length, initialValue, T_CHAR); //private static final int T_CHAR = 5; };}vectorizedHashCode(a, fromIndex, length, initialValue, T_CHAR):
@IntrinsicCandidateprivate static int vectorizedHashCode(Object array, int fromIndex, int length, int initialValue, int basicType) { return switch (basicType) { case T_BOOLEAN -> unsignedHashCode(initialValue, (byte[]) array, fromIndex, length); case T_CHAR -> array instanceof byte[] ? utf16hashCode(initialValue, (byte[]) array, fromIndex, length) : hashCode(initialValue, (char[]) array, fromIndex, length); case T_BYTE -> hashCode(initialValue, (byte[]) array, fromIndex, length); case T_SHORT -> hashCode(initialValue, (short[]) array, fromIndex, length); case T_INT -> hashCode(initialValue, (int[]) array, fromIndex, length); default -> throw new IllegalArgumentException("unrecognized basic type: " + basicType); };}可以看出来,两种编码都是用的“31 倍哈希法” 差别并不大。