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);
}

java中的Void


对于上方代码,COMPACT_STRINGSfinal修饰的静态变量,类在初始化的时候就给其设置为true
1.当长度为0的时候,直接将 ""的属性赋值给类的属性;
2.长度不为0的时候,进入第二个ifStringUTF16.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类的valuecoder 赋值成刚刚得到的valLATIN1,结束方法;
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[]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345