String源码详解
前言
String是我们平时写代码最常用的类型,所以了解String的源码也是我们必要的功课。
下面是String源码解读,JDK版本是1.9。
//String实现了序列化接口,比较接口和字符序列接口
//说明String类可以序列化和反序列化,实现了自己的比较逻辑,和定义不可变字符
//CharSequence 是可读可写字符序列接口
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
//用于存储字符串即String的值,final修饰,说明值不会改变
@Stable //该注解表示字符数组value第一个非null值永远不会改变
private final byte[] value;
//字符串所用的编码器,LATIN1或UTF16
//Latin1是ISO-8859-1的别名,向下兼容ASCII码
private final byte coder;
//String的hash值,默认是0
private int hash;
//序列号:ID
private static final long serialVersionUID = -6849794470754667710L;
//字符串编码是否紧凑,为true则为紧凑以LATIN1编码优先,为false则总是使用UTF16编码
static final boolean COMPACT_STRINGS;
//字符串编码是否紧凑默认为true
static {
COMPACT_STRINGS = true;
}
//用于声明String类的序列化字段
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
//初始化新创建的String对象,使其表示空字符串。 但是,由于字符串是不可变的,因此不建议使用该无参构造函数,因为没有意义。
public String() {
this.value = "".value;
this.coder = "".coder;
}
//构造函数,传入一个字符串
// HotSpotIntrinsicCandidate标注的方法,在HotSpot中都有一套高效的实现,该高效实现基于CPU指令,运行时,HotSpot维护的高效实现会替代JDK的源码实现,从而获得更高的效率。
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
//构造函数,将chars数组转换为String
public String(char value[]) {
//具体实现看这个方法
this(value, 0, value.length, null);
}
//指定char数组以及开头和长度,即截取char数组的某一长度为String
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) {
//如果开始值小于0,或者截取的长度小于0,或者开始值加长度大于char数组的总长度,那么就会直接跑出异常,如果正常则直接返回null,下面是方法实现
checkBoundsOffCount(offset, count, value.length);
/*
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);
}
}
*/
return null;
}
//构造函数。
String(char[] value, int off, int len, Void sig) {
if (len == 0) { //如果需要截取char长度为0,那么构造一个空字符串
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) { //判断字符串编码是否紧凑,默认为true
//这里判断要取的字符数组里面的字符的长度是否小于255,如果都小于255,
//那么编码就可以用LATIN1,如果有一个字符不小于255,那么返回val字节数组为null,编码就要用UTF16了。
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16; //这里就是编码用UTF16
this.value = StringUTF16.toBytes(value, off, len);
}
// StringUTF16.compress方法
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;
}
@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;
}
}
}
String(int[],int,int) 这个构造函数
public String(int[] codePoints, int offset, int count) {
/*
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);
}
}
*/
//这里还是校验一下开始值和长度如果小于0,或者开始值加长度要大于int数组长度就抛出异常。
checkBoundsOffCount(offset, count, codePoints.length);
if (count == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
//上面说过,判断字符是否紧凑编码
if (COMPACT_STRINGS) {
//这里依然是判断int数组中如果有值大于255那么就val字节数组就是null,
//编码格式用UTF16,否则就用LATIN1编码
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);
}
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)) { //这里来判断是否数组中是否有值大于255,如果有就返回null
return null;
}
ret[i] = (byte)cp;
}
return ret;
}
//位运算,int类型32位,低八位代表256,右移8位为0是否为0就能判断该int值是否大于255
public static boolean canEncode(int cp) {
return cp >>> 8 == 0;
}
下面是以字节数组作为参数的构造函数,方法都大同小异,就不挨个的说了
两个int依然是字节数组的开始位置和长度,String和Charset是代表编码的字符集
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
//和之前的一样检查参数有没有越界
checkBoundsOffCount(offset, length, bytes.length);
//按照指定编码格式来对字节数组进行编码,如果为空,则默认使用ISO-8859-1来编码
StringCoding.Result ret =
StringCoding.decode(charsetName, bytes, offset, length);
this.value = ret.value;
this.coder = ret.coder;
}
构造函数的参数还可以是StringBuffer和StringBuilder但是基本没见人用过,因为这两个类都有自己的toString方法,没必要这么写,吃力不讨好。
public String(StringBuffer buffer) {
this(buffer.toString());
}
//StringBuffer的比较简单,直接构造就行
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
public String(StringBuilder builder) {
this(builder, null);
}
//stringBuilder的比较复杂
String(AbstractStringBuilder asb, Void sig) {
byte[] val = asb.getValue();
int length = asb.length();
if (asb.isLatin1()) {
this.coder = LATIN1;
this.value = Arrays.copyOfRange(val, 0, length);
} else {
if (COMPACT_STRINGS) {
byte[] buf = StringUTF16.compress(val, 0, length);
if (buf != null) {
this.coder = LATIN1;
this.value = buf;
return;
}
}
this.coder = UTF16;
this.value = Arrays.copyOfRange(val, 0, length << 1);
}
}
length()
获取String的长度
public int length() {
//返回String字符串的长度
//判断是LATIN1说明是单字节编码,直接返回字节的长度的就行了
//判断是UTF16说明是双字节编码,要右移一位,即字节长度除以2才是String字符的长度
return value.length >> coder();
}
byte coder() {
return COMPACT_STRINGS ? coder : UTF16;
}
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
empty()
判断String是否为空
public boolean isEmpty() {
//简答明了,判断字节数组是否为空
return value.length == 0;
}
charAt(int index)
返回String指定下标的字符,依然根据不同的编码格式返回不同的字符
public char charAt(int index) {
//依然判断编码,
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
codePointAt
返回对应下标字符的unicode编码值
比如"abcd".codePointAt(1)值为98,就是返回这个字符串下标为1,即b字符的unicode编码值。
public int codePointAt(int index) {
if (isLatin1()) {
//如果是Latin1编码,直接返回字节数组的下标值
checkIndex(index, value.length);
return value[index] & 0xff;
}
//如果是utf16,那么就返回utf16编码的字符的unicode值
int length = value.length >> 1;
checkIndex(index, length);
return StringUTF16.codePointAt(value, index, length);
}
codePointBefore
返回对应下标字符的前一个字符的unicode编码值
比如"abcd".codePointBefore(2)值为98,就是返回这个字符串下标为2的前一个字符,即b字符的unicode编码值。
public int codePointBefore(int index) {
//这里就是把下标减1,其他的和上面的就一样了
int i = index - 1;
if (i < 0 || i >= length()) {
throw new StringIndexOutOfBoundsException(index);
}
if (isLatin1()) {
return (value[i] & 0xff);
}
return StringUTF16.codePointBefore(value, index);
}
codePointCount
//返回开始开始下标和结束下标之间的代码点数,从代码可以得知,如果是Latin1编码,那么直接返回他们之间的差值,如果是utf16编码,那么就返回utf16编码的代码点数。代码点数也就是他们的下标之间的差值,和length()方法一样,区别就在于length()方法返回的是两个char字符编码的结果,而这个方法返回的是unicode编码的结果,unicode编码的范围要大于两个char编码的范围,所以一般情况下两者是相等的,但是unicode编码的范围超过的时候就不一样了。比如四个字符编码的内容。
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || beginIndex > endIndex ||
endIndex > length()) {
throw new IndexOutOfBoundsException();
}
if (isLatin1()) {
return endIndex - beginIndex;
}
return StringUTF16.codePointCount(value, beginIndex, endIndex);
}
offsetByCodePoints
第一个参数是开始索引,第二个参数是偏移数,返回的结果就是从开始索引处的偏移数的索引值,即比如参数为(1,3)意思就是从索引处为1,偏移三个代码点的索引,即返回的是4,这个方法和上面的一样,也会出现unicode编码大于两个char编码的情况。具体的可以查阅相关资料,这里不做详细讨论。
public int offsetByCodePoints(int index, int codePointOffset) {
if (index < 0 || index > length()) {
throw new IndexOutOfBoundsException();
}
return Character.offsetByCodePoints(this, index, codePointOffset);
}
getChars
srcBegin:此字符串的开始下标
srcEnd:此字符串的结束下标
dst:字符数组
dstBegin:字符数组的开始长度
将该字符串的开始索引到结束索引之间的值复制到dst数组中并以dstBegin索引开始复制
比如有个字符串str:“hello” 有个字符数组dst:‘a’‘b’‘c’‘d’
str.getChars(0,1,dst,1) 结果就是将hello的从0开始截取到下标1,就是h,然后替换数组的从1开始,结果dst数组就变成了‘a’‘h’‘c’‘d’
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);
}
}