Java String 源码分析 一(基于 JDK 14)


前置知识

Unicode

  在 Unicode 未出现之前,不同的国家(地区)对字符编码有着不同的标准,例如:美国的 ASCII、中国大陆的 GB 18030、俄罗斯的 KOI-8 等等。这些不兼容的字符编码对跨国家(地区)的网络交流非常不友好,随着互联网的普及,更加凸显了这一问题。在这样的大环境下,Unicode 应运而生。

  在设计 Unicode 之初,人们认为用 2 个字节表示 1 个字符绰绰有余,因此初版的 Unicode 采用了这种方案,即使这样,依然还有一半以上的空位。但是随着版本的迭代,越来越多的字符被收录了进来,这导致已经无法用 2 个字节表示 1 个字符了。所以现在的 Unicode 用 3 个字节表示 1 个字符,最多可以表示 1, 114, 112 个字符(没有占满 3 个字节)。

  在继续阅读之前,有必要了解一下专业术语。Unicode 将字符的编号称为代码点Code Point),将所有代码点的集合称为代码空间Code Space)。此外,还将代码空间平均分为了 17 个部分,1 个部分就是 1 个平面Plane)。第 1 个平面被称为基本多语言平面Basic Multilingual Plane, BMP),它包含了大部分的常用字符,例如:ASCII 字符、ISO-8859-1 字符、中日韩统一表意文字等。其他平面被称为辅助平面Supplementary Multilingual Plane, SMP)。

UTF-16

  Unicode 用 3 个字节表示 1 个字符,但是常用字符大多数在基本多语言平面中,这无疑会造成额外的开销,所以人们设计出了 UTF-16 这种基于 Unicode 的可变长度的字符编码。

  UTF-16 的设计思路是用 1 个或 2 个 代码单元Code Unit)表示 1 个代码点(1 个代码单元占 2 个字节)。如果代码点在 BMP 中,则用 1 个代码单元表示;如果在 SMP 中,则用 2 个代码单元表示。具体做法是:在 BMP 中预留两个空白区域作为 代理项Surrogate),然后将 SMP 的代码点映射至这两个区域中,即:用 代理项对 表示 SMP 的代码点;对于 BMP 中的其它代码点,不做任何改变。如下图所示:

将辅助平面的代码点映射至代理区间内
  [0xD800, 0xDBFF] 和 [0xDC00, 0xDFFF] 就是 Unicode 为 UTF-16 保留的空白区域,前者被称为高位代理High Surrogates),后者被称为低位代理Low Surrogates)。而且这样做还有另一个好处,就是计算机读取数据时不用从 0 位置开始,因为 UTF-16 的三种代码单元:高位代理、低位代理和 BMP 中其它的代码点已经能够说明位置和顺序关系了。


Java.lang.String

重要的成员变量

 1 byte[ ] value
@Stable 
private final byte[] value;

  value 表示存储的字符串。

  final 表示 value 指向的数组对象不能改变,但是不能保证数组的元素也不会改变,所以用 @Stable 注解标记,表示数组的元素也不会改变。

在 JDK 9 之前,String 用 char[ ] 存储字符串,一个 char 类型数据表示一个 UTF-16 代码单元。但这对于代码点在 [0, 0x100) 的字符而言会造成额外的开销,所以之后的版本用 byte[ ] 存储字符串,并用 coder 表示其字符编码。


 2 byte coder
private final byte coder; 

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

  coder 表示字符编码,可选值有 2 个, 分别是:

可选值说明
0 / LATIN1表示 Latin1,1 个字符用 1 个字节表示
1 / UTF16表示 UTF-16,1 个字符用 2 个或 4 个字节表示

  在实例化 String 时,构造方法会自动地为 coder 赋值。而且由于 value 是不可变的,所以 code 也被 final 修饰。

  @Native 表示一个成员变量引用的值来自于本地代码。(暂时不知道是什么意思)

 3 boolean COMPACT_STRINGS
static final boolean COMPACT_STRINGS;

static {
    COMPACT_STRINGS = true;
}

  类常量 COMPACT_STRINGS 表示是否使用紧凑布局,默认值为 true。紧凑布局的意思是尽量使用 Latin1 字符编码。

构造 String 对象

  由于 String 底层用 byte[ ] 作为存储容器,所以讨论如何构造 String 对象就是在讨论如何 “花式” 的填充一个 byte[ ]。

 1 将 char[ ] 转换为 byte[ ]

  一个 char 类型数据表示一个 UTF-16 代码单元,占 2 个字节,而一个 byte 类型数据只占 1 个字节。所以将 char[ ] 转换为 byte[ ] 会有以下两种情况:

  1. char[ ] 中所有元素小于 256,1 个 char 类型数据转换为 1 个 byte 类型数据
  2. char[ ] 中某个元素大于 255,1 个 char 类型数据转换为 2 个 byte 类型数据

   1.1 char[ ] 中所有元素小于 256

  这是最简单的情况,只需将 char[ ] 中的每个元素强制类型转换成 byte 类型即可。下面是相关代码:

final class StringUTF16 {
	@HotSpotIntrinsicCandidate
	public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) {
	    for (int i = 0; i < len; i++) {
	        char c = src[srcOff];
	        if (c > 0xFF) {
	            len = 0;
	            break;
	        }
	        dst[dstOff] = (byte) c;
	        srcOff++;
	        dstOff++;
	    }
	    return len;
	}
	
    public static byte[] compress(char[] val, int off, int len) {
        byte[] ret = new byte[len];
        if (compress(val, off, ret, 0, len) == len) {
            return ret;
        }
        return null;
    }
}

  第一个方法会判断 char[ ] 中的所有元素是否都小于 256,如果成立,则会执行强制类型转换的操作;如果不成立,则返回 0。第二个方法则是对前者的简单封装,而且还避免了前者 len = 0 与条件不成立时返回值相同的尴尬情况。

  @HotSpotIntrinsicCandidate 注解表示该方法在 HotSpot 虚拟机中有另外更加高效的实现。

   1.2 char[ ] 中某个元素大于 255

  将 1 个 char 类型数据转换为 2 个 byte 类型数据,就需要考虑计算机在存储和传输多字节数据时的顺序问题,即:大端模式(Big Endian)和小端模式(Little Endian)。

  大端模式会将高字节放在低地址上,小端模式则相反。例如,“中” 的代码点为 U+4E2D,按照低地址在左的顺序,大端模式为:4E2D,小端模式为:2D4E。下面是相关代码:

final class StringUTF16 {
	static final int HI_BYTE_SHIFT;
	static final int LO_BYTE_SHIFT;
	
	private static native boolean isBigEndian();
	
	static {
	    if (isBigEndian()) {
	        HI_BYTE_SHIFT = 8;
	        LO_BYTE_SHIFT = 0;  
	    } else {
	        HI_BYTE_SHIFT = 0;
	        LO_BYTE_SHIFT = 8;
	    }
	}
}

  静态代码块会根据 isBigEnding 的返回值修改 高/低 字节的向右偏移量,这两个偏移量最终会与 char [ ] 中的每个元素分别做右移运算,得到 低/高 地址的 byte 类型数据。下面是相关代码:

final class StringUTF16 {
	static final int MAX_LENGTH = Integer.MAX_VALUE >> 1;	
	
	public static byte[] newBytesFor(int len) {
        if (len < 0) {
            throw new NegativeArraySizeException();
        }
        if (len > MAX_LENGTH) {
            throw new OutOfMemoryError("UTF16 String size is " + len + ", should be less than " + MAX_LENGTH);
        }
        return new byte[len << 1];
    }
    
	public static int length(byte[] value) {
        return value.length >> 1;
    }
    
	@HotSpotIntrinsicCandidate
	static void putChar(byte[] val, int index, int c) {
	    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
	    index <<= 1;
	    val[index++] = (byte)(c >> HI_BYTE_SHIFT);
	    val[index]   = (byte)(c >> LO_BYTE_SHIFT);
	}
	
	@HotSpotIntrinsicCandidate
    public static byte[] toBytes(char[] value, int off, int len) {
        byte[] val = newBytesFor(len);
        for (int i = 0; i < len; i++) {
            putChar(val, i, value[off]);
            off++;
        }
        return val;
    }
}

   第一个方法与第二个方法的 “输入、输出” 正好是相反的,第一个方法是根据代码单元的数量获取存储的 byte[ ];第二个方法是根据byte[ ] 反推其能存储的代码单元的数量。

  第三个方法是将一个 char 类型的数据写入到 byte[ ] 中。不要被 index++index 的表面蒙骗了,前者才是低地址。

  第四个方法是对前者的封装,它将原本 1 次只能写入 1 个 char 类型的数据变为 1 次可以写入 1 个 char[ ],并返回写入后的 byte[ ] 。

   1.3 整合的构造方法
String(char[] value, int off, int len, Void sig) {
    if (len == 0) {
        this.value = "".value;
        this.coder = "".coder;
        return;
    }
    if (COMPACT_STRINGS) {
        byte[] val = StringUTF16.compress(value, off, len);
        if (val != null) {
            this.value = val;
            this.coder = LATIN1;
            return;
        }
    }
    this.coder = UTF16;
    this.value = StringUTF16.toBytes(value, off, len);
}

  该构造方法会先尝试 1.1 中的操作,如果成功,则直接返回;如果失败,则再进行 1.2 中的操作。由于存在相同签名的构造方法,所以用 sig 加以区分。流程图如下:
在这里插入图片描述
  需要注意的是:由于其内部调用的方法没有对数组下标进行检查,所以会产生数组下标越界的问题,因此依赖该方法的其它构造方法都对下标进行了检查。

  依赖该构造方法的其它构造方法有:

public String(char value[]) {
    this(value, 0, value.length, null);
}

public String(char value[], int offset, int count) {
       this(value, offset, count, rangeCheck(value, offset, count));
   }

private static Void rangeCheck(char[] value, int offset, int count) {
    checkBoundsOffCount(offset, count, value.length);
    return null;
}

static void checkBoundsOffCount(int offset, int count, int length) {
    if (offset < 0 || count < 0 || offset > length - count) {
        throw new StringIndexOutOfBoundsException(
            "offset " + offset + ", count " + count + ", length " + length);
    }
}

 2 将 int[ ] 转换为 byte[ ]

  一个 int 类型数据表示一个代码点,占 4 个字节,而一个 byte 类型数据只占 1 个字节,所以将 int[ ] 转换为 byte[ ] 会有以下两种情况:

  1. int[ ] 中所有元素小于 256,1 个 int 类型数据转换为 1 个 byte 类型数据
  2. int[ ] 中某个元素大于 255,1 个 int 类型数据转换为 2 个或 4 个 byte 类型数据

   2.1 int[ ] 中所有元素小于 256
final class StringLatin1 {
    public static byte[] toBytes(int[] val, int off, int len) {
        byte[] ret = new byte[len];
        for (int i = 0; i < len; i++) {
            int cp = val[off++];    
            if (!canEncode(cp)) {
                return null;
            }
            ret[i] = (byte)cp;
        }
        return ret;
    }
    
     public static boolean canEncode(int cp) {
        return cp >>> 8 == 0;
    }
}

  换汤不换药,仍然是判断 int[ ] 中每个元素是否小于 256,如果条件成立,则强制类型转换;如果不成立,则返回 null。不过这里的判断条件是右移 8 位是否为 0 而已。(我也不知道为什么要用无符号右移)

   2.2 int[ ] 中某个元素大于 255

  由于 int 类型的范围和代码空间的范围不一致,所以需要检查 int[ ] 中的元素是否符合 Unicode 规范。同时设置一个粒度更细的 isBmpCodePoint 方法还可以测量其代码单元的个数,从而确定 byte[ ] 的长度。相代码如下:

public final class Character implements java.io.Serializable, Comparable<Character> {
    public static final int MAX_CODE_POINT = 0X10FFFF;
    
    public static boolean isValidCodePoint(int codePoint) {
        int plane = codePoint >>> 16;
        return plane < ((MAX_CODE_POINT + 1) >>> 16);
    }
    
    public static boolean isBmpCodePoint(int codePoint) {
        return codePoint >>> 16 == 0;
    }
}

  在确定了 byte[ ] 的长度后,就需要将 SMP 中的代码点转换成 2 个代码单元,然后写入其中。相关代码如下:

public final class Character implements java.io.Serializable, Comparable<Character> {
    public static final char MIN_HIGH_SURROGATE = '\uD800';
    public static final char MIN_LOW_SURROGATE  = '\uDC00';
    public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;
    
    public static char highSurrogate(int codePoint) {
        return (char) ((codePoint >>> 10) + (MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10)));
    }
    
    public static char lowSurrogate(int codePoint) {
        return (char) ((codePoint & 0x3ff) + MIN_LOW_SURROGATE);
    }
}

  下面的方法就是对前二者的整合,就不再赘述。

final class StringUTF16 {    
    public static byte[] toBytes(int[] val, int index, int len) {
        final int end = index + len;
        int n = len;    // 计算代码单元的个数,以确定 byte[] 的长度
        for (int i = index; i < end; i++) {
            int cp = val[i];
            if (Character.isBmpCodePoint(cp))
                continue;
            else if (Character.isValidCodePoint(cp))
                n++;
            else throw new IllegalArgumentException(Integer.toString(cp));
        }
        byte[] buf = newBytesFor(n);    // 该方法在 1.2 中出现过,功能是获得存储代码单元的 byte[]
        for (int i = index, j = 0; i < end; i++, j++) {    // 遍历,然后转换成代码单元,最后写入 byte[] 中
            int cp = val[i];
            if (Character.isBmpCodePoint(cp)) {
                putChar(buf, j, cp);	// 同样地,1.2 中出现过,功能是将代码单元写入 j * 2 和 j * 2 + 1 的下标位置
            } else {					// 而且还考虑到了大小端的问题
                putChar(buf, j++, Character.highSurrogate(cp));
                putChar(buf, j, Character.lowSurrogate(cp));
            }
        }
        return buf;
    }
}

   2.3 整合的构造方法
public String(int[] codePoints, int offset, int count) {
        checkBoundsOffCount(offset, count, codePoints.length);    // 1.3中出现过,功能是边界检查
        if (count == 0) {
            this.value = "".value;
            this.coder = "".coder;
            return;
        }
        if (COMPACT_STRINGS) {
            byte[] val = StringLatin1.toBytes(codePoints, offset, count);
            if (val != null) {
                this.coder = LATIN1;
                this.value = val;
                return;
            }
        }
        this.coder = UTF16;
        this.value = StringUTF16.toBytes(codePoints, offset, count);
    }

  该构造方法整合了 2.1 与 2.2 中的操作,由于与 1.3 中的构造方法步骤类似,因此不再赘述。

 3 将 byte[ ] 转换为 byte[ ]

  

 4 其它的构造方式
   4.1 使用 " " 构造一个 String 对象
   4.2 使用 String 对象构造一个 String 对象

未完待续。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值