String源代码解析

一.简介

1.关于string设计中的享元模式

2.string源码解析

3jdk8相对于jdk7的不同

4.补充

二.String中的享元模式

享元模式(Flyweight)可以粗略的理解为缓存(cache),是设计中的一种优化策列。

1.常量与常量池

在这里需要引入常量池这个简单的概念。

常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。

它包括了关于类、方法、接口等中的常量,也包括字符串常量。

看例1:

String源代码解析_第1张图片

首先,我们要知结果Java会确保一个字符串常量只有一个拷贝。

因为例子中的a0和a1都是字符串常量,它们在编译期就被确定了,所以a0==a1为true;

而”learning ”和”String”也都是字符串常量,当一个字符串由多个字符串常量连接而成时

,它自己肯定也是字符串常量,所以a2也同样在编译期就被解析为一个字符串常量,

所以as2也是常量池中”learning String”的一个引用。所以我们得出a0==a1==a2;

到这里我们就可以理解string通过这种常量池中相同常量共享对象的方式实现了类似于缓存的享元模式设计

2.String的构造方法

用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,

它们有自己的地址空间。

String源代码解析_第2张图片

a0还是常量池中”learning String”的应用,a1因为无法在编译期确定,所以是运行时创建的新对象”learning String”

的引用,a2因为有后半部分new String(“String”)所以也无法在编译期确定,所以也是一个新创建对象”learning String”

的应用;明白了这些也就知道为何得出此结果了。

 

3. String.intern()扩充常量池:

存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;

当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,

则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;

String源代码解析_第3张图片

这里需要说明另外一点:

在这里常量池中一开始是没有”learning String”的,当我们调用a0.intern()后就在常量池中新添加了

一个”learning String”常量,原来的a0常量”kvill”地址仍然存在,所以两个的地址相同是false,

也就不是“将自己的地址注册到常量池中”了。

 

三.string源码解析

1.类和成员变量

我们看到string是个使用final 声明的不可变类,这个会在补充中涉及到,以及实现了几个接口,

这个会在后面的方法中涉及到。这里需要先提及的是

private final char value[];是string的值,所以string字符串声明后值属性是不可变的;

2.string众多构造函数

String源代码解析_第4张图片

//不含参数的构造函数,一般没什么用,因为value是不可变量
public String() {
    this.value = new char[0];
}
//参数为String类型
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
//参数为char数组,使用java.utils包中的Arrays类复制
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
//从bytes数组中的offset位置开始,将长度为length的字节,以charsetName格式编码,拷贝到value
public String(byte bytes[], int offset, int length, String charsetName)
        throws UnsupportedEncodingException {
    if (charsetName == null)
        throw new NullPointerException("charsetName");
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
//调用public String(byte bytes[], int offset, int length, String charsetName)构造函数
public String(byte bytes[], String charsetName)
        throws UnsupportedEncodingException {
    this(bytes, 0, bytes.length, charsetName);
}

这里需要注意的一点是,String不属于8种基本数据类型,String是一个对象。

因为对象的默认值是null,所以String的默认值也是null;

但它又是一种特殊的对象,有其它对象没有的一些特性。

比如构造方法new String()和new String(“”)都会返回一个空字符串而不是null。

 

3.intern()

intern在上面讲到过,这里需要注意的一点是,它是通过native调用的,

native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。

Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,

SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。Invocation API(JNI的一部分)

可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。

 

4.trim()

作用是去掉字符串的前后空格,在字符串传值比较中比较常用

public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */
    //找到字符串前段没有空格的位置
    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    //找到字符串末尾没有空格的位置
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    //如果前后都没有出现空格,返回字符串本身
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
 

 

5.int compareTo(String anotherString)

public int compareTo(String anotherString) {
    //自身对象字符串长度len1
    int len1 = value.length;
    //被比较对象字符串长度len2
    int len2 = anotherString.value.length;
    //取两个字符串长度的最小值lim
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    //从value的第一个字符开始到最小长度lim处为止,如果字符不相等,返回自身(对象不相等处字符-被比较对象不相等字符)
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    //如果前面都相等,则返回(自身长度-被比较对象长度)
    return len1 - len2;
}

这个方法在比较字符串类型的时间时可以起到作用,通过两个值a0.compareTo(a1)比较的结果>0,<0,=0,分别可以知道a0>a1,a0

 

6.boolean equals(Object anObject)

public boolean equals(Object anObject) {
    //如果引用的是同一个对象,返回真
    if (this == anObject) {
        return true;
    }
    //如果不是String类型的数据,返回假
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        //如果char数组长度不相等,返回假
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            //从后往前单个字符判断,如果有不相等,返回假
            while (n-- != 0) {
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            //每个字符都相等,返回真
            return true;
        }
    }
    return false;
}

 

三千多行的代码,String中还有很多方法,这里就不一一列举了。

 

四.jdk8相对于jdk7的不同

 

1.jdk8去掉的方法

int hash32()

 

private transient int hash32 = 0;
int hash32() {
    int h = hash32;
    if (0 == h) {
       // harmless data race on hash32 here.
       h = sun.misc.Hashing.murmur3_32(HASHING_SEED, value, 0, value.length);

       // ensure result is not zero to avoid recalcing
       h = (0 != h) ? h : 1;

       hash32 = h;
    }

    return h;
}

在JDK1.7中,Hash相关集合类在String类作key的情况下,不再使用hashCode方式离散数据,而是采用hash32方法。

这个方法默认使用系统当前时间,String类地址,System类地址等作为因子计算得到hash种子,

通过hash种子在经过hash得到32位的int型数值。

2.jdk8新加的方法

public static String join(CharSequence delimiter, CharSequence... elements) {

Objects.requireNonNull(delimiter);

Objects.requireNonNull(elements);

// Number of elements not likely worth Arrays.stream overhead.

StringJoiner joiner = new StringJoiner(delimiter);

for (CharSequence cs: elements) {

joiner.add(cs);

}

return joiner.toString();

}

使用方法

String源代码解析_第5张图片

public static String join(CharSequence delimiter,

Iterable elements) {

Objects.requireNonNull(delimiter);

Objects.requireNonNull(elements);

StringJoiner joiner = new StringJoiner(delimiter);

for (CharSequence cs: elements) {

joiner.add(cs);

}

return joiner.toString();

}

String源代码解析_第6张图片

四.补充

1.关于equals()和==:

这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同

,也就是是否是同一个字符串的引用。

 

2. 关于String是不可变的

String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”;

就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,

最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,

这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。

 

3.String类被设计成不可变的原因

1.字符串常量池的需要

字符串常量池(String pool, String intern pool, String保留池) 是Java方法区中一个特殊的存储区域,

当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。

如下面的代码所示,将会在堆内存中只创建一个实际String对象.
代码如下:
String s1 = "abcd"; String s2 = "abcd";

假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,

这种常量池的思想,是一种优化手段.

String s1= "ab" + "cd"; String s2= "abc" + "d";

也许这个问题违反新手的直觉, 但是考虑到现代编译器会进行常规的优化, 所以他们都会指向常量池中的同一个对象.

或者,你可以用 jd-gui 之类的工具查看一下编译后的class文件.

2. 允许String对象缓存HashCode

Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。

字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码.

3. 安全性

String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等,

假若String不是固定不变的,将会引起各种安全隐患。

假如有如下的代码:boolean connect(string s){ if (!isSecure(s)) {throw new SecurityException();}

// 如果在其他地方可以修改String,那么此处就会引起各种预料不到的问题/错误 causeProblem(s);}

4. 线程安全

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。

字符串自己便是线程安全的。

总体来说, String不可变的原因包括 设计考虑,效率优化问题,以及安全性这三大方面.

 

4. 如何实现一个不可变类

既然不可变类有这么多优势,那么我们借鉴String类的设计,自己实现一个不可变类。

不可变类的设计通常要遵循以下几个原则:

  1. 将类声明为final,所以它不能被继承。
  2. 将所有的成员声明为私有的,这样就不允许直接访问这些成员。
  3. 对变量不要提供setter方法。
  4. 将所有可变的成员声明为final,这样只能对它们赋值一次。
  5. 通过构造器初始化所有成员,进行深拷贝(deep copy)。
  6. 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。

 

写在最后,我在学习的过程中,喜欢结合自己的理解记录在我的有道云笔记中,在这里我重新整理了关于String的学习理解,写篇博客,如果有一些地方存在歧义,希望能够有人指出,并在后期改正,同时希望对Java学习者有所帮助。

你可能感兴趣的:(jdk源代码阅读)