概述
目录
char类型
一、定义
二、属性
String类型的性能优化
三、构造方法
四、其他方法
length方法
isEmpty方法
charAt方法
codePointAt方法
codePointBefore方法
getChars方法
equals方法
在进行String源码分析之前,我们先来介绍一下char数据类型。
char类型
char类型用于表示单个字符。通常用来表示字符常量。是一个无符号十六进制的值,从u 000到uffff。
char数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。,关于UTF-16编码具体可参考 字符编码笔记:ASCII,Unicode , UTF-8,UTF-16和ISO8859-1(Latin-1)。
下面有两个个概念。
代码点:代码点(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,代码点采用十六进制书写,并加上前缀U+,例如U+0041就是字母A的代码点。
Unicode的代码点可以分成17个代码级别(code plane)。第一个代码级别称为基本的多语言级别(简称BMP),代码点从U+0000到U+FFFF,其中包括了经典的Unicode代码;其余的16个附加级别,代码点从U+10000到U+FFFFF,其中包括了一些辅助字符。
代码单元:在BMP中,每个字符用16位表示,称为代码单元(code unit)。辅助字符采用一对连续的代码单元进行编码。
因为一个char类型字符代码的是一个代码单元,而UTF-16编码辅助平面字符需要用两个代码单元来表示一个代码点。这可能会带来一些误解,因此,尽量避免用char类型。
一、定义
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable、 Comparable<String>、 CharSequence
二、属性
@Stable
private final byte[] value;
private final byte coder;
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;
-
value表示用来存储字符串对象的值,这里用byte[],@Stable表示该数组不会为null,final表示字符串一旦被初始化后是不会被改变的。
-
coder表示编码方式,Latin1(1个字节)或者UTF16(2个字节或4个字节)。
-
COMPACT_STRINGS 默认将 COMPACT_STRINGS 设置为 true。而如果要取消紧凑的布局可以通过配置 VM 参数-XX:-CompactStrings实现。
-
hash:hash值,默认为0。
String类型的性能优化
从jdk9之后,value的类型由char[]数组变成了byte[]数组。那么为什么要做这样的优化呢?为了节省空间,提高String的性能。
char占用两个字节(16bit),比如字符A,则为0x00 0x41,前面8个bit就是浪费的。也就是说刚好ISO-8859-1(0~255)编码范围的字符都会浪费8个bit,之外的字符则不会浪费。
jdk9之后,用byte[]数组存储字符串,一个byte占一个字节,即8bit。
在不指定字符编码的情况下,会有两种编码方式,Latin1(ISO-8859-1)和UTF-16,当字符都在Latin1范围内的时候就采用Latin1编码方式(紧凑布局),否则使用UTF-16。使用Latin1编码方式的时候,用了byte[]数组之后相对于使用char[]数组,空间节省了一半。
因为改变了String的实现,使用了Latin1和UTF16编码方式,需要增加一个属性coder来表明使用的是哪种编码方式。LATIN1=0;UTF16=1。
字符串对象是 Java 中大量使用的对象,而且我们会轻易大量使用它而从不考虑它的代价,所以对其的空间优化是有必要的,Java9 开始对String类进行优化, 这能帮助我们减少字符串在堆中占用的空间,而且还能减轻GC压力。同时也能看到该空间优化对中文来说意义不大。
三、构造方法
String有很多构造方法,这里介绍几个常见的构造方法。
无参数的构造函数
public String() {
this.value = "".value;
this.coder = "".coder;
}
传入String的构造函数
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
传入char[]数组的构造方法
public String(char value[]) {
this(value, 0, value.length, null);
}
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);
}
//如果COMPACT_STRINGS=true,则采用LATIN1编码方式,如果其中有一个字符不在LATIN1编码范围内,则采用UTF-16编码。
//LATIN1:StringUTF16.compress(value, off, 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;
}
//LATIN1:char[]-->byte[]
@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) {//判断是否在LATIN1编码0~255范围内
len = 0;
break;//如果不在则返回,用UTF16编码方式
}
dst[dstOff] = (byte)c;
srcOff++;
dstOff++;
}
return len;
}
//UTF16:StringUTF16.toBytes(value, off, len)
//UTF16:char[]--->byte[]
@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;
}
//newBytesFor() 因为utf16编码方式,一个char占用16bit,一个byte占用8bit,一个char会占用2个byte的长度,这个函数计算出char[]数组转换为bit[]之后,bit[]数组的长度。
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];//左移一位,相当于乘以2.
}
//将char字符写入到byte数组中。一个char字符,写入到byte[]中,占用两个位置。
@HotSpotIntrinsicCandidate
// intrinsic performs no bounds checks
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);
}
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 = 8;LO_BYTE_SHIFT = 0;
对于char字符存储进byte[]数组
一个char字符占用两个byte[]数组的位置,先存高8位的bit,再存低8位的bit。
val[index++] = (byte)(c >> HI_BYTE_SHIFT); 存高8位,将字符c右移8位,保存高8位。
val[index] = (byte)(c >> LO_BYTE_SHIFT); 存低8位,将字符c右移0位,c是16位,byte类型是8位,相当于将c强转为byte类型,直接截取低8位。
例如:c=289;(所有的字符,都可以用数字来表示)
存储的二进制为:0000 0001 0010 0001
高8位:0000 0001 0010 0001>>8 = 0000 0000 0000 0001--->转为byte得到 0000 0001
低8位:0000 0001 0010 0001>>0 = 0000 0001 0010 0001--->转为byte得到 0010 0001
因此,byte[]数组中存储的值分别为:0000 0001 和 0010 0001
在有些函数中,我们可能需要将byte[]数组转化为char[]数组,在之后的源码中也会看到如下源码
@HotSpotIntrinsicCandidate
// intrinsic performs no bounds checks
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));
}
上面的函数则是将byte[]数组转化为char字符,我们来看下具体是如何实现的?
还是刚刚的c=289 存在byte[]数组中是 0000 0001 和 0010 0001
c=(char)(((0000 0001 &0xff )<<8)|((0010 0001 & 0xff)<<0)) 【为什么要&0xff?是为了保持低8位不变,高位补0,如果不&0xff,那么高位会自动补足符号位。而这里我们只想保持原来的二进制不变。】
高8位:
0000 0001 & 0xff =
0000 0000 0000 0000 0000 0000 0000 0001
&
0000 0000 0000 0000 0000 0000 1111 1111
=0000 0000 0000 0000 0000 0000 0000 0001
再 0000 0000 0000 0000 0000 0000 0000 0001 <<8 = 0000 0000 0000 0000 0000 0001 0000 0000
低8位
0000 0000 0000 0000 0000 0000 0010 0001
&
0000 0000 0000 0000 0000 0000 1111 1111
=0000 0000 0000 0000 0000 0000 0010 0001<<0= 0000 0000 0000 0000 0000 0000 0010 0001
然后作或|运算
0000 0000 0000 0000 0000 0001 0000 0000
|
0000 0000 0000 0000 0000 0000 0010 0001
=0000 0000 0000 0000 0000 0001 0010 0001
最后 强转为char类型,截取低16位= 0000 0001 0010 0001=1+32+256=289
感觉char->byte[] 和 byte[]-->char 这个过程很有意思。
四、其他方法
length方法
public int length() {
return value.length >> coder();
}
返回字符串的长度,指的是返回该字符串的字符(代码单元)的个数【辅助平面的Unicode代码点,有两个代码单元,即两个字符】,而不是value[]数组的长度,所以,这里做了右移运算。LATIN1编码时:coder()=0,字符串的长度=value数组长度的本身;UTF16编码时,code()=1,字符串的长度=value数组长度的一半。
isEmpty方法
public boolean isEmpty() {
return value.length == 0;
}
charAt方法
返回给定位置的代码单元。辅助平面的Unicode代码点,有两个代码单元,即两个字符。
public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
//latin:byte[]-->char StringLatin1.charAt(value, index)
public static char charAt(byte[] value, int index) {
if (index < 0 || index >= value.length) {
throw new StringIndexOutOfBoundsException(index);
}
return (char)(value[index] & 0xff);
}
//utf16:byte[]-->char StringUTF16.charAt(value, index)
public static char charAt(byte[] value, int index) {
checkIndex(index, value);
return getChar(value, index);
}
//byte[]-->char 上面有讲过
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));
}
codePointAt方法
返回从给定位置开始或结束的代码点,如果是辅助平面的代码点,则这个函数相当于返回两个字符(两个代码单元)。
LATIN1编码:直接将 byte 数组对应索引的元素与0xff做&操作并转成 int 类型。
UTF16编码: 会返回该位置开始或结束的代码点,会去判断该位置的字符是否处于辅助平面,如果是则返回两个字节(代码单元)-->返回的是对应的int的数字。
public int codePointAt(int index) {
if (isLatin1()) {
checkIndex(index, value.length);
return value[index] & 0xff;
}
int length = value.length >> 1;
checkIndex(index, length);
return StringUTF16.codePointAt(value, index, length);
}
//checked=false
private static int codePointAt(byte[] value, int index, int end, boolean checked) {
assert index < end;
if (checked) {
checkIndex(index, value);
}
char c1 = getChar(value, index);
if (Character.isHighSurrogate(c1) && ++index < end) {
if (checked) {
checkIndex(index, value);
}
char c2 = getChar(value, index);
if (Character.isLowSurrogate(c2)) {
return Character.toCodePoint(c1, c2);
}
}
return c1;
}
codePointBefore方法
用于返回指定索引值前一个字符的代码点,实现与codePointAt方法类似,只是索引值要减1。
getChars方法
用于获取字符串对象指定范围内的字符到目标 char 数组中,主要是根据两种编码做不同处理,如果是 LATIN1 编码则直接将 byte 数组对应索引的元素与0xff做&操作并转成 char 类型。而如果是 UTF16 编码则需要两个字节一起转为 char 类型。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
checkBoundsBeginEnd(srcBegin, srcEnd, length());
checkBoundsOffCount(dstBegin, srcEnd - srcBegin, dst.length);
if (isLatin1()) {
StringLatin1.getChars(value, srcBegin, srcEnd, dst, dstBegin);
} else {
StringUTF16.getChars(value, srcBegin, srcEnd, dst, dstBegin);
}
}
equals方法
比较两个字符串对象是否相等
1.比较两个对象的引用是否一样;
2.判断是否为字符串对象;
3.根据编码方式比较;
3.1 latin1:直接比较value的每一个值是否相等;
3.2 utf16:将byte[]转化为char类型之后,一一比较每个char是否相等;
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
....还有好多方法懒得写,反正几乎所有的方法都会分两种编码方式来写。
因为已经有其他人写过这个源码分析啦,可以直接参考:
从JDK源码看String(上)
从JDK源码看String(下)
字符串连接你用+还是用StringBuilder
最后
以上就是大气面包为你收集整理的jdk11-String源码分析的全部内容,希望文章能够帮你解决jdk11-String源码分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复