String源码解析

定义

先看一下文档中的注释

1
2
3
4
5

* Strings are constant; their values cannot be changed after they
* are created. String buffers support mutable strings.
* Because String objects are immutable they can be shared.
*/

String对象是常量,创建之后就不能被修改,所以该对象可以被多线程共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class 
implements java.io.Serializable, Comparable<>, CharSequence {

private final char value[];

private int hash; // Default to 0

private static final long serialVersionUID = -6849794470754667710L;

private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
......
}

从源码中可以看出,String是被final修饰的,说明该类不能被继承。并且实现了CharSequence, Comparable, Serializable接口。Serializable接口用于实现String的序列化和反序列化操作;Comparable接口用于实现字符串的比较操作;CharSequence是字符串类的父接口,StringBuffer和StringBuilder都继承自该接口。

  1. value字段是实现String类的底层数组,用于存储字符串内容。final修饰基本数据类型,那么在运行期间其内容不可变,如果修饰的是引用类型,那么引用的对象(包括数组)运行期地址不可变,但是对象的内容是可以改变的。
  2. hash字段用于缓存String对象的hash值,防止多次计算hash造成的时间损耗。
  3. 因为String实现了Serializable接口,所以需要serialVersionUID字段用来在String反序列化时,通过对比字节流中的serialVersionUID和本地实体类中的serialVersionUID是否一致,如果相同就可以进行反序列化,否则就会抛出InvalidCastException异常。

构造方法

空参构造方法

1
2
3
public () {
this.value = "".value;
}

该构造方法会创建一个空的字符序列,因为字符串的不可变对象,之后对象的赋值会指向新的字符串,因此使用这种构造方法会多创建一个无用对象。

使用字符串类型的对象初始化

1
2
3
4
public (String original) {
this.value = original.value;
this.hash = original.hash;
}

直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后就不可改变,所以也就不用担心源String的值会影响到目标String的值。

使用字符数组初始化

1
2
3
4
5
6
7
8
9
10
// 参数为char数组,通过java.utils包中的Arrays.copyOf复制
public (char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// 使用字符数组的一部分初始化,通过Arrays.copyOfRange复制
public String(char value[], int offset, int count) {
// 异常检测
......
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

使用字节数组初始化

在Java中,String实例保存有一个char[]字符数组,char[]字符数组是以Unicode编码方式存储的,String和char为内存形式,byte是网络传输或存储的序列化形式,所以在很多传输和存储过程中需要将byte[]数组和String进行相互转化。字节和字符自检的转化需要指定编码,不然很可能会出现乱码。String提供了多种字节数组的重载构造函数:

1
2
3
4
5
6
public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], String charsetName)
public String(byte bytes[], Charset charset)
public String(byte bytes[], int offset, int length)
public String(byte bytes[])

如果我们在使用 byte[] 构造 String 的时候,如果指定了charsetName或者charset参数的话,那么就会使用 StringCoding.decode 方法进行解码,使用的解码的字符集就是我们指定的 charsetName 或者 charset。如果没有指定解码使用的字符集的话,那么StringCodingdecode方法首先会使用系统的默认编码格式(ISO-8859-1)。

使用StringBuffer和StringBuilder初始化

1
2
3
4
5
6
7
8
9
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}

public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

因为StringBuilder不是线程安全的,所以在初始化时不需要加锁;而StringBuilder则需要加锁。我们一般使用StringBuffer和StringBuilder的toString方法来获取String,而很少使用String的这两种构造方法。

特殊的构造方法

String除了提供了很多共有的构造方法,还提供了一个保护类型的构造方法:

1
2
3
4
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}

该方法和String(char[] value)有两点区别:

  • 该方法多了一个参数:boolean share,但该参数在函数中并没有使用。因此加入该参数的目的只是为了区分String(char[] value)方法,只有参数不同才能被重载。
  • 该方法直接修改了value数组的引用,也就是说共享char[] value数组。而String(char[] value)通过Arrays.copyOf将参数数组内容复制到String中。

使用这种方式的的优点很明显:

  • 性能好,直接修改指针,避免了逐一拷贝。

  • 节约内存,底层共享同一字符数组。

    当然这种方式也存在缺点,如果外部修改了传进来的字符数组的内容,由于他们引用的是同一个数组,因此外部对数组的修改相当于修改了字符串。为了保证字符串对象的不变性,将其访问权限设置成了default,其他类无法通过该构造方法初始化字符串对象。这样一来,无论源字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部无法访问,保证了String的安全性。该函数只能用在不能缩短String长度的函数中,如concat(str1, str2),如果用在缩短String长度的函数如subString中会造成内存泄漏。

经典方法技巧

equals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
  1. 先判断两个对象的地址是否相等
  2. 在判断是否是String类型
  3. 如果都是String类型,就先比较长度是否相等,然后再逐一比较值。值的比较采取了短路操作,发现不一样的就返回false

compareTo方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;

int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}

从0开始逐一判断字符是否相等,若不相等则做差运算,巧妙的避免了三种判断情况。若字符都相等,接直接返回长度差值。所以在判断两个字符串大小时,使用是否为正数/负数/0,而不是通过1//-1/0判断。

hashCode方法

1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
  1. 若第一次调用hashCode方法且value数组长度大于0,则通过算法s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]计算hash值。hash值很多时候用来判断两个对象的值是否相等,所以需要尽可能的避免冲突。选择31是因为31是一个素数,且i * 31可以通过(i << 5) - 1来提高运算速度,现在很多虚拟机都有做相关优化。 hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。
  2. 返回缓存的hash值。

replaceFirst、replaceAll,replace区别

1
2
3
4
String replaceFirst(String regex, String replacement)
String replaceAll(String regex, String replacement)
String replace(Char Sequencetarget, Char Sequencereplacement)
String replace(CharSequence target, CharSequence replacement)
  1. replace的参数是char和CharSequence,既可以支持字符的替换,也支持字符串的替换
  2. replaceFirstreplaceAll的参数是regex,基于正则表达式替换
  3. replacereplaceAll方法会替换字符串中的全部字符或字符串,replaceFirst只替换第一次出现的字符或字符串

copyValueOf和valueOf

String的底层是通过char[]实现的,早期的String构造器的实现并不会拷贝数组。为了防止char[]数组被外部修改,提供了copyValueOf方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String在构造器中就通过拷贝新数组实现,所以这两个方法在本质上已经没区别了。

valueOf()有很多种重载形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}

底层调用了基本数据类型的toString()方法。

intern方法

1
public native String intern();

intern方法是Native调用,它的作用是每当定义一个字符字面量,字面量进行字符串连接或final的String字面量初始化的变量的连接,都会检查常量池中是否有对应的字符串,如果有就不创建新的字符串,而是返回指向常量池对应字符串的引用。所有通过new String(str)方式创建的对象都会保存在堆中,而不是常量区。普通变量的连接,由于不能在编译期确定下来,所以不会储存在常量区。

其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int length() //返回字符串长度

boolean isEmpty() //返回字符串是否为空

char charAt(int index) //返回字符串中第(index+1)个字符

char[] toCharArray() //转化成字符数组

void trim() //去掉两端空格

String toUpperCase() //转化为大写

String toLowerCase() //转化为小写

String concat(String str) //拼接字符串

String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符

//以上两个方法都使用了String(char[] value, boolean share);

boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式

boolean contains(CharSequence s) //判断字符串是否包含字符序列s

String[] split(String regex, int limit) //按照字符regex将字符串分成limit份。

String[] split(String regex) //按照regex表达式切分字符串

boolean equals(Object anObject) //比较对象

boolean contentEquals(String Buffersb) //与字符串比较内容

boolean contentEquals(Char Sequencecs) //与字符比较内容

boolean equalsIgnoreCase(String anotherString) //忽略大小写比较字符串对象

int compareTo(String anotherString) //比较字符串

int compareToIgnoreCase(String str) //忽略大小写比较字符串

boolean regionMatches(int toffset, String other, int ooffset, int len) //局部匹配

boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) //可忽略大小写局部匹配

String对“+”的重载

Java不支持运算符重载,但是String可以通过+来连接两个字符串。那么java是如何实现对+的重载的呢?

1
2
3
4
5
6
public class Main{
public static void main(String[] args){
String str1 = "windy";
string str2 = str1 + "lee";
}
}

反编译Main.java,执行命令javap -c Main,输出结果:

我们看到了StringBuilder,还有windy和lee,以及调用了StringBuilder的appendtoString方法。既然编译器已经在底层为我们进行了优化,那么为什么还要提倡我们用StringBuilder呢?

我们注意到在第3行代码,new了一个StringBuilder对象,如果实在一个循环里面,我们使用”+”号就会创建多个StringBuilder的对象。但是编译器事先不知道我们StringBuilder的长度,并不能事先分配好缓冲区,会加大内存的开销,而且使用重载的时候根据java的内存分配也会创建多个对象。

switch对字符串支持的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default: break;
}
}
}

反编译之后得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String args[]) {
String str = "world";
String s;
switch((s = str).hashCode()) {
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
default: break;
}
}
  1. 首先调用String的hashCode方法,拿到相应的Code,通过这个code然后给每个case唯一的标识
  2. 判断时先获取对象的hashCode,进入对应的case分支
  3. 通过equals方法进行安全检查,这个检查是必要的,因为哈希可能会发生冲突

switch只支持整型,其他数据类型都是转换成整型之后在使用switch的

总结

  • String被final修饰,一旦被创建,无法修改

    • final保证value不会指向其他的数组,但不保证数组内容不可修改
    • private属性保证不可在类外访问数组,也就不能改变其内容
    • String内部没有改变value内容的函数,保证String不可变
    • String声明为final杜绝了通过集成的方法添加新的函数
    • 基于数组的构造方法,会拷贝数组元素,避免了通过外部引用修改value的情况
    • 用String构造其他可变对象时,返回的数组的拷贝

    final只在编译期有效,在运行期间无效,因此可以通过反射改变value引用的对象。反射虽然改变了s的内容,并没有创建新的对象。而且由于String缓存了hash值,所以通过反射改变字符数组内容,hashCode返回值不会自动更新。

  • String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。

  • 如果你需要一个可修改的字符串,应该使用StringBuilder或者 StringBuffer。

  • 如果你只需要创建一个字符串,你可以使用双引号的方式,如果你需要在堆中创建一个新的对象,你可以选择构造函数的方式。

原文:大专栏  String源码解析


猜你喜欢

转载自www.cnblogs.com/wangziqiang123/p/11643619.html