【源码】String源码

236 阅读15分钟

1、String 类的定义

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {}

一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的, 包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串),接着实现了 Serializable接口,这是一个序列化标志接口,还实现了 Comparable 接口,用于比较两个字符串的大小(按顺序比较单个字符的ASCII码),后面会有具体方法实现;最后实现了 CharSequence 接口,表示是一个有序字符的集合。 String str = "abc";

相当于:

 char data[] = {'a', 'b', 'c'};
 String str = new String(data);

值的注意的是 String str="abc"只是代表了 str指向了abc的引用,abc存在常量池中, String str=new String("abc") 首先判断abc在常量池中是否存在,不存在则创建,并在堆中创建一份。

image.png

image.png

(1)字面量赋值创建:

String str1 = "hello";String str2 = "hello";String str3 = "world"; 这样创建字符串,首先会去常量池里找有没有这个字符串,有就直接指向常量池的该字符串,没有就先往常量池中添加一个,再指向它。图解:

image.png

(2)用new创建:

String str1 = new String("hello");String str2 = new String("hello");String str3 = new String("world"); new一个字符串时,做了两件事。首先在堆中生成了该字符串对象,然后去看常量池中有没有该字符串,如果有就不管了,没有就往常量池中添加一个。图解:

image.png

2.String 的属性

   private int hash; // Default to 0
   private static final long serialVersionUID = -6849794470754667710L;
   private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

其中包括hash值,序列化值和字符串值 其本质上是字符数组

3.方法

image.png

类型方法
charcharAt(int index)返回 char指定索引处的值。
intcodePointAt(int index) 返回指定索引处的字符(Unicode代码点)。
intcodePointBefore(int index)返回指定索引之前的字符(Unicode代码点)。
intcodePointCount(int beginIndex, int endIndex)返回此 String指定文本范围内的Unicode代码点数。
intcompareTo(String anotherString)按字典顺序比较两个字符串。
intcompareToIgnoreCase(String str)按字典顺序比较两个字符串,忽略病例差异。
Stringconcat(String str)将指定的字符串连接到该字符串的末尾。
booleancontains(CharSequence s)当且仅当此字符串包含指定的char值序列时才返回true。
booleancontentEquals(CharSequence cs)将此字符串与指定的CharSequence进行 CharSequence 。
booleancontentEquals(StringBuffer sb)将此字符串与指定的StringBuffer进行 StringBuffer 。
static StringcopyValueOf(char[] data)相当于 valueOf(char[]) 。
static StringcopyValueOf(char[] data, int offset, int count)相当于 valueOf(char[], int, int) 。
booleanendsWith(String suffix)测试此字符串是否以指定的后缀结尾。
booleanequals(Object anObject)将此字符串与指定对象进行比较。
booleanequalsIgnoreCase(String anotherString)将此 String与其他 String比较,忽略案例注意事项。
static Stringformat(Locale l, String format, Object... args)使用指定的区域设置,格式字符串和参数返回格式化的字符串。
static Stringformat(String format, Object... args)使用指定的格式字符串和参数返回格式化的字符串。
byte[]getBytes()使用平台的默认字符集将此 String编码为字节序列,将结果存储到新的字节数组中。
byte[]getBytes(Charset charset)使用给定的charset将该String编码为字节序列,将结果存储到新的字节数组中。
voidgetBytes(int srcBegin, int srcEnd, byte[] dst, int dstBegin)已弃用此方法无法将字符正确转换为字节。
byte[]getBytes(String charsetName)使用命名的字符集将此 String编码为字节序列,将结果存储到新的字节数组中。
voidgetChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)将此字符串中的字符复制到目标字符数组中。
inthashCode()返回此字符串的哈希码。
intindexOf(int ch)返回指定字符第一次出现的字符串内的索引。
intindexOf(int ch, int fromIndex)返回指定字符第一次出现的字符串内的索引,以指定的索引开始搜索。
intindexOf(String str)返回指定子字符串第一次出现的字符串内的索引。
intindexOf(String str, int fromIndex)返回指定子串的第一次出现的字符串中的索引,从指定的索引开始。
Stringintern()返回字符串对象的规范表示。
booleanisEmpty()返回 true如果,且仅当 length()为 0 。
static Stringjoin(CharSequence delimiter, CharSequence... elements返回一个新的字符串,由 CharSequence elements的副本组成,并附有指定的delimiter的 delimiter 。
static Stringjoin(CharSequence delimiter, Iterable<? extends CharSequence> elements)返回一个新 String的副本组成 CharSequence elements与指定的副本一起加入delimiter 。
intlastIndexOf(int ch)返回指定字符的最后一次出现的字符串中的索引。
intlastIndexOf(int ch, int fromIndex)返回指定字符的最后一次出现的字符串中的索引,从指定的索引开始向后搜索。
intlastIndexOf(String str)返回指定子字符串最后一次出现的字符串中的索引。
intlastIndexOf(String str, int fromIndex)返回指定子字符串的最后一次出现的字符串中的索引,从指定索引开始向后搜索。
intlength()返回此字符串的长度。
booleanmatches(String regex)告诉这个字符串是否匹配给定的 regular expression 。
intoffsetByCodePoints(int index, int codePointOffset)返回此 String内的指数,与 index codePointOffset代码点。
booleanregionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)测试两个字符串区域是否相等。
booleanregionMatches(int toffset, String other, int ooffset, int len)测试两个字符串区域是否相等。
Stringreplace(char oldChar, char newChar)返回从替换所有出现的导致一个字符串 oldChar在此字符串 newChar 。
Stringreplace(CharSequence target, CharSequence replacement)将与字面目标序列匹配的字符串的每个子字符串替换为指定的字面替换序列。
StringreplaceAll(String regex, String replacement)用给定的替换替换与给定的 regular expression匹配的此字符串的每个子字符串。
StringreplaceFirst(String regex, String replacement)用给定的替换替换与给定的 regular expression匹配的此字符串的第一个子字符串。
String[]split(String regex)将此字符串分割为给定的 regular expression的匹配。
String[]split(String regex, int limit)将这个字符串拆分为给定的 regular expression的匹配。
booleanstartsWith(String prefix)测试此字符串是否以指定的前缀开头。
booleanstartsWith(String prefix, int toffset)测试在指定索引处开始的此字符串的子字符串是否以指定的前缀开头。
CharSequencesubSequence(int beginIndex, int endIndex)返回一个字符序列,该序列是该序列的子序列。
Stringsubstring(int beginIndex)返回一个字符串,该字符串是此字符串的子字符串。
Stringsubstring(int beginIndex, int endIndex)返回一个字符串,该字符串是此字符串的子字符串。
char[]toCharArray()将此字符串转换为新的字符数组。
StringtoLowerCase()将所有在此字符 String使用默认语言环境的规则,以小写。
StringtoLowerCase(Locale locale)将所有在此字符 String ,以降低使用给定的规则情况下 Locale 。
StringtoString()此对象(已经是字符串!)本身已被返回。
StringtoUpperCase()将所有在此字符 String使用默认语言环境的规则大写。
StringtoUpperCase(Locale locale)将所有在此字符 String使用给定的规则,大写 Locale 。
Stringtrim()返回一个字符串,其值为此字符串,并删除任何前导和尾随空格。
static StringvalueOf(boolean b)返回 boolean参数的字符串 boolean形式。
static StringvalueOf(char c)返回 char参数的字符串 char形式。
static StringvalueOf(char[] data)返回 char数组参数的字符串 char形式。
static StringvalueOf(char[] data, int offset, int count)返回 char数组参数的特定子阵列的字符串 char形式。
static StringvalueOf(double d)返回 double参数的字符串 double形式。
static StringvalueOf(float f)返回 float参数的字符串 float形式。
static StringvalueOf(int i)返回 int参数的字符串 int形式。
static StringvalueOf(long l)返回 long参数的字符串 long形式。
static StringvalueOf(Object obj)返回 Object参数的字符串 Object形式。

4、equals(Object anObject) 方法

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            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;
    }

equals其本质上是Object类的方法,比较的是引用的地址是否相等,这里对equals方法进行了重写,用来比较内容是否相等。 首先判断引用地址是否相等,通常来说判断是否为同一对象,如果是同一对象则相等,在比较是否为String类型,如果是在比较长度是否相等,最后比较值是否相等。

5、hashCode() 方法

image.png

String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]   s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:

  ①、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。

  ②、31可以被 JVM 优化,31 * i = (i << 5) - i。因为移位运算比乘法运行更快更省性能。

6、String 真的不可变吗?

  String 类是用 final 关键字修饰的,所以我们认为其是不可变对象。但是真的不可变吗?

  每个字符串都是由许多单个字符组成的,我们知道其源码是由 char[] value 字符数组构成。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

  value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。

String str = "vae";
//打印原字符串
System.out.println(str);//vae
//获取String类中的value字段
Field fieldStr = String.class.getDeclaredField("value");
//因为value是private声明的,这里修改其访问权限
fieldStr.setAccessible(true);
//获取str对象上的value属性的值
char[] value = (char[]) fieldStr.get(str);
//将第一个字符修改为 V(小写改大写)
value[0] = 'V';
//打印修改之后的字符串
System.out.println(str);//Vae

  通过前后两次打印的结果,我们可以看到 String 被改变了,但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的。

  那么,String 类为什么要这样设计成不可变呢?我们可以从性能以及安全方面来考虑:

安全

引发安全问题,譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

保证线程安全,在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程。

HashCode,当 String 被创建出来的时候,hashcode也会随之被缓存,hashcode的计算与value有关,若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。

性能

当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的 String 将在堆内开辟出新的空间,占据更多的内存。

7、intern() 方法

public native String intern();

  当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。

  这句话什么意思呢?就是说调用一个String对象的intern()方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中),如果没有,则将该对象添加到池中,并返回池中的引用。

8、String有长度限制吗

字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac执行过程中控制了最大值为65534。

在运行期,长度不能超过Int的范围,否则会抛异常。

9、常量池

在前面讲解构造函数的时候,我们知道最常见的两种声明一个字符串对象的形式有两种:

  ①、通过“字面量”的形式直接赋值

String str = "hello";

  ②、通过 new 关键字调用构造函数创建对象

String str = new String("hello");

  那么这两种声明方式有什么区别呢?在讲解之前,我们先介绍 JDK1.7(不包括1.7) 以前的 JVM 的内存分布:

  

  ①、程序计数器:也称为 PC 寄存器,保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。线程私有

  ②、虚拟机栈:基本数据类型、对象的引用都存放在这。线程私有

  ③、本地方法栈:虚拟机栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和虚拟机栈合二为一。

  ④、方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。注意:在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

  ⑤、堆:用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。

  在 JDK1.7 以后,方法区的常量池被移除放到堆中了,如下:

  

  常量池:Java运行时会维护一个String Pool(String池), 也叫“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。

①、字面量创建字符串或者纯字符串(常量)拼接字符串会先在字符串池中找,看是否有相等的对象,没有的话就在字符串池创建该对象;有的话则直接用池中的引用,避免重复创建对象。

②、new关键字创建时,直接在堆中创建一个新对象,变量所引用的都是这个新对象的地址,但是如果通过new关键字创建的字符串内容在常量池中存在了,那么会由堆在指向常量池的对应字符;但是反过来,如果通过new关键字创建的字符串对象在常量池中没有,那么通过new关键词创建的字符串对象是不会额外在常量池中维护的。

③、使用包含变量表达式来创建String对象,则不仅会检查维护字符串池,还会在堆区创建这个对象,最后是指向堆内存的对象。

String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1==str2);//true
System.out.println(str1==str3);//fasle
System.out.println(str2==str3);//fasle
System.out.println(str1.equals(str2));//true
System.out.println(str1.equals(str3));//true
System.out.println(str2.equals(str3));//true

  对于上面的情况,首先 String str1 = "hello",会先到常量池中检查是否有“hello”的存在,发现是没有的,于是在常量池中创建“hello”对象,并将常量池中的引用赋值给str1;第二个字面量 String str2 = "hello",在常量池中检测到该对象了,直接将引用赋值给str2;第三个是通过new关键字创建的对象,常量池中有了该对象了,不用在常量池中创建,然后在堆中创建该对象后,将堆中对象的引用赋值给str3,再将该对象指向常量池。如下图所示:

  

  注意:看上图红色的箭头,通过 new 关键字创建的字符串对象,如果常量池中存在了,会将堆中创建的对象指向常量池的引用。我们可以通过文章末尾介绍的intern()方法来验证。

  使用包含变量表达式创建对象:

String str1 = "hello";
String str2 = "helloworld";
String str3 = str1+"world";//编译器不能确定为常量(会在堆区创建一个String对象)
String str4 = "hello"+"world";//编译器确定为常量,直接到常量池中引用

System.out.println(str2==str3);//fasle
System.out.println(str2==str4);//true
System.out.println(str3==str4);//fasle

  str3 由于含有变量str1,编译器不能确定是常量,会在堆区中创建一个String对象。而str4是两个常量相加,直接引用常量池中的对象即可。