Java String类源码解析(jdk1.9)
注:源码内容实在太多,又想详细分析,尽量督促自己在年前写完~
第一部分:String类的基本介绍。
- 在
java.lang
包下; - 引用了
java.io,java.util
等包下面的类; - 类上方的介绍,如下:
String类代表的就是字符串,比如 "abc"等 所有的字符串都是被此类所实现。
String类的值是不可改变的,他们的值再被创建之后就不可改变!字符串缓冲区(也就是StringBuffer)支持可变字符串,因为String对象虽然不可变,但是可以被共享。比如,String str = "abc";
与char data[] = {'a', 'b', 'c'}; String str = new String(data);
他们共享了同一个"abc"。
再举一些String怎么用的栗子:
System.out.println("abc");//直接输出
String cde = "cde";//赋值给一个String对象
System.out.println("abc" + cde);//拼接输出
String c = "abc".substring(2,3);//直接调用substring()方法
String d = cde.substring(1, 2);//赋值给一个String对象后调用substring()方法
String类的包含实现各种功能的方法,如检测某个字符是否存在,比较字符串,搜索字符串,截取字符串,把某个字符串全部转换成大写或小写返回并拷贝一个新的字符串返回。java.lang.Character的Character类基于Unicode标准版本指明了转换规则。
Java 语言对于字符串拼接操作与其他类转换成字符串提供了特殊的支持。详情请见 The Java™ Language Specification.
除非有特殊的说明,否则 把null传入String类的构造函数或者方法中,会抛出一个空指针异常!
String类定义的字符串是UTF-16格式的。索引值是指字符代码单元,所以在字符串中新增字符使用两个位置。
String类可以用来处理 Unicode code points(也就是characters)与Unicode code units (也就是values)
第二部分:代码解读
1. 类与属性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
//String类被final修饰了,表明其不可被继承。
//实现了 Serializable接口--->使其可以序列化,方便数据的传输
//实现了Comparable接口,可以调用Collections.sort() 和 Arrays.sort() 方法排序,并且String类实现 compareTo() 方法。
//实现了CharSequence接口,该接口能表示char值的一个可读序列。几个String兄弟类都实现了此接口。
@Stable //表示下方属性 最多被赋值1次!
private final byte[] value;
//String类的第一个属性,用于字符储存。
//这个属性被java虚拟机信任,如果String实例恒定,此属性也不可改变,重写会造成问题。
private final byte coder;
//此属性为用于编码字节的编码的标识符,分为 LATIN1 与 UTF16,同样也是被虚拟机信任,不可变,不重写。
private int hash; // 默认值是 0
//此属性是缓存的hash码
private static final long serialVersionUID = -6849794470754667710L;
//从jdk1.0.2就开始使用序列化的版本号。
static final boolean COMPACT_STRINGS;
//此属性与coder属性 都是jdk9新增属性。
//表明String对象是否为紧凑的/简洁的,如果不是 value 数组中的code都是UTF16编码的,反之为UTF16。对于那些有几个实现方式的方法 。当对象不是紧凑属性的时候,只能使用其中的一种实现方式了。实例属性值通常对JIT编译器没有什么优化。
//因此,在性能敏感的地方,在 coder 属性校验之前会有一个对于 静态boolean类型变量COMPACT_STRINGS的校验。因为COMPACT_STRINGS可以被一个优化的JIT编译器不断折叠。
所以有了如下结论:
if (coder == LATIN1) { ... } 应该写成 if (coder() == LATIN1) { ... }
或者 if (COMPACT_STRINGS && coder == LATIN1) { ... }
一个优化的JIT编译器可以将上面的条件折叠起来
COMPACT_STRINGS == true => if (coder == LATIN1) { ... }
COMPACT_STRINGS == false => if (false) { ... }
这个属性的实际值是由java虚拟机注入的。下面的静态初始代码块就是用来设置此属性并声明这个static final字段不是静态可折叠的并且在虚拟机初始化期间避免任何可能的循环依赖。
2.构造方法
static {
COMPACT_STRINGS = true;//类加载设置值为 ture
}
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
//String类是序列化流协议的特例(实在不理解,没用过,略过)
注:序列化流协议
以下开始,源码在上,解析在下,并以横线分隔不同代码
public String() {
this.value = "".value;
this.coder = "".coder;
}
这是String类的第一个构造方法,类加载的时候会调用。作者也注释说明了是用来实例化代表空字符序列的String对象,但是String具有不可变性,只要实例化一个就一直储存在堆中的常量池中。
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
初始化一个与参数字符串一样的String对象,也是参数字符串的一个拷贝。除非明确地需要一个参数的拷贝,否则这个构造函数是没有必要的,因为String不可变。
public String(char value[]) {
this(value, 0, value.length, null);
}
入参的value是一个 char类型的数组,此方法可以生成一个代表该数组的String对象,相当于复制了这个入参的数组,之后对于入参数组的修改不会影响新建的String对象。
public String(char value[], int offset, int count) {
this(value, offset, count, rangeCheck(value, offset, count));
}
此方法三个参数,同上,也是生成一个String对象,但是根据
参数1,value---->char类型的数组
参数2,offset---->开始的位置(0.1.2....)
参数3,count---->长度(生成String对象的长度)
,调用rangeCheck()
如下
private static Void rangeCheck(char[] value, int offset, int count) {
checkBoundsOffCount(offset, count, value.length);
return null;
}
进入checkBoundsOffCount()
如下
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);
}
}
//此方法在 offset < 0(开始位置小于0) 或者 count < 0(生成String长度小于0) 或者 offset > length - count(从开始位置+长度 超过了数组的长度)时,抛出异常。
这时回到最上面的
this(value, offset, count, rangeCheck(value, offset, count));
此构造方法的最后一个参数类型为Void类型。
校验完毕,来到主要介绍的构造方法。
String(char[] value, int off, int len, Void sig) {//可以看到 Void类型的参数 sig 并没有使用,但是根据java的语法次参数必传。
//所以作用为,要求调用此方法的时候 必须进行校验等操作,返回一个 Void类型的参数,作为次方法的入参。
//在Void类中,也说明此类与关键字 void 类似。
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
是final
修饰的静态变量,类在初始化的时候就给其设置为true
。
1.当长度为0的时候,直接将 ""
的属性赋值给类的属性;
2.长度不为0的时候,进入第二个if
,StringUTF16.compress(value, off, len)
里面发生了如下:
public static byte[] compress(char[] val, int off, int len) {
byte[] ret = new byte[len];
//很明显,此处构造了的新的byte型数组是为了本方法的返回
if (compress(val, off, ret, 0, len) == len) {
return ret;
}
return null;
}
对于compress(char[] val, int off, int len)
到底是返回 ret
还是null
需要调用如下:
public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) {
//如下循环判断了每一个 src 中的元素
for (int i = 0; i < len; i++) {
char c = src[srcOff];
if (c > 0xFF) {//不是Latin1编码,直接跳出循环,返回len(值为0)
len = 0;
break;
}
dst[dstOff] = (byte)c;
srcOff++;
dstOff++;
}
return len;
}
通过上方代码可知,对于public static byte[] compress(char[] val, int off, int len){...}
只要数组里面的某个字符不是Latin1
编码,即返回null
,否则返回 ret
。
再次回到构造方法:
1.如果数组value
里面的所有元素都是Latin1
编码,那么直接将String类的value
与coder
赋值成刚刚得到的val
与LATIN1
,结束方法;
2.如果不是coder
赋值为UTF16
,然后进入最后一个方法:StringUTF16.toBytes(value, off, len);
public static byte[] toBytes(char[] value, int off, int len) {
byte[] val = newBytesFor(len);//此方法会校验len是否小于0或者大于Integer.MAX_VALUE >> 1(抛出异常),
//通过后返回new byte[len << 1] --->2倍的len数组
for (int i = 0; i < len; i++) {
putChar(val, i, value[off]);
off++;
}
return val;
}
本想就此结束此构造方法,但putChar()
里面还是由内容的,如下:
static void putChar(byte[] val, int index, int c) {
assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
index <<= 1;//判断之后,直接 * 2,然后将val[0],val[2],val[4]...等赋值,再将val[1],val[3],val[5]...赋值
//这里也表明了之前返回2被的`len`数组是有意义的。
val[index++] = (byte)(c >> HI_BYTE_SHIFT);
val[index] = (byte)(c >> LO_BYTE_SHIFT);
}
至此,构造方法的最后一步完结,即value
已经被赋值成byte[]
。