在Java编程中,除Object类外,最常用的类就是String类了。本文将从String类源码出发,对String类进行一个全面的分析,以帮忙我们更好的理解和使用String类。
String类概述
Java 使用 String 类代表字符串。Java 中的所有字符串字面值(如 “abc” )都使用此类实现。字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。
String的不可变性(immutable)
在Java中,String是不可变的,这主要体现在三个方面:(1)String类使用final关键字修饰,表示其不可继承;(2)String类使用字节数组存储数据,且使用final关键字修饰,表示该字段创建后引用地址不可变。另外该字符数组的访问权限为 private,表示外部无法访问,且 String 没有对外提供可以修改该属性的方法。关键源码如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
// ...
}
注意,字符串的底层实现使用字节数组存储。使用字节数字而非字节数组的好处是,字节数组与数据的底层存储保持一致,无需额外转换。使用字节数组,保证使用指定的编码、解码方式,屏蔽了底层设备的差异。对于网络传输场景,无需进行额外的转换(网络数据传输使用字节流)。
String不可变性的好处
将String设计成不可变,主要有以下方面的考虑:(1) 出于性能方面的考虑。可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,就共享此对象,而不是创建一个新对象,注意,这种方式仅适用于"="操作符创建的String对象。(2) 出于安全方面的考虑。由于String的不可变性,可以确保创建后不会被篡改(当然,这也不是绝对,使用反射仍可修改对象的值)。(3) 防止内存泄露。HashMap 的 key 为String类型,如果String对象可变,则会造成该key无法手动删除,从而造成内存泄露。(4)出于并发安全的考虑。由于String的不可变性,多个线程可以安全的共享String对象,则不用担心被修改。
字符串的创建
使用字符串时,遇到的第一个问题就是字符串的创建。根据是否使用运算符创建,可以将其分为两类:(1) 直接赋值创建字符串;(2)使用构造方法创建字符串。
直接赋值创建字符串
直接赋值创建字符串就是使用运算符直接赋值,可以使用的运算符有等号和加号。示例代码如下:
String strWithEqualOperator = "foo";
String strWithAddOperator = strWithEqualOperator + "test";
直接赋值创建字符串时,会优先从字符串常量池中获取已存在的字符串,如果不存在,则会将新生成的字符串添加到常量池,方便下次使用。字符串常量池是享元模式的具体应用,后面会进一步介绍。
注意,这里使用"+"运算符的语法,在Java编译阶段,会将变量替换成真实的字符串并完成拼接。
使用构造方法创建字符串
除了直接赋值创建字符串外,还可以使用构造方法创建字符串。String类支持多种场景的创建。如字节数组、字符数组、StringBuilder实例等,这里不再一一列举。关键源码如下(为避免方法过长,影响阅读,仅展示方法声明,具体实现可以参考源码):
// 创建空字符串
public String();
// 基于字符串对象创建字符串对象
@HotSpotIntrinsicCandidate
public String(String original);
// 基于字符数组创建字符串对象
public String(char value[]);
// 基于字符数组指定长度创建字符串对象
public String(char value[], int offset, int count);
// 基于整数数组指定长度创建字符串对象
public String(int[] codePoints, int offset, int count);
// 基于字节数组指定长度创建字符串对象
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException;
// 基于字节数组指定长度创建字符串对象
public String(byte bytes[], int offset, int length, Charset charset);
// 基于字节数组创建字符串对象
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException;
// 基于字节数组创建字符串对象
public String(byte bytes[], Charset charset);
// 基于字节数组指定长度创建字符串对象
public String(byte bytes[], int offset, int length);
// 基于字节数组创建字符串对象
public String(byte[] bytes);
// 基于StringBuffer实例创建字符串对象
public String(StringBuffer buffer);
// 基于StringBuilder实例创建字符串对象
public String(StringBuilder builder);
需要说明的是,使用构造方法创建字符串对象时,不会复用字符串常量池。如果两个字符串对象分别使用相同的字符串字面量由构造函数创建,且使用等号运算进行比较,因为是两个不同的对象,所以尽管其值相同,但不相等。示例如下:
String str1 = new String("foo");
String str2 = new String("foo");
// 打印false
System.out.println(str1 == str2);
所以,在进行字符串比较时,尽量使用equals方法,而不要使用相等运算符。
字符串常量池与享元模式
为了提高对象的复用率,减少重复对象的内存占用,Java语言引入了字符串常量池。针对使用赋值运算符创建的字符串对象,Java会优先从字符串常量池中尝试获取该对象,如果对象存在,则直接复用。如果对象不存在,则新生成一个字符串并将其放到常量池中。熟悉缓存使用的同学可能会发现,字符串常量池的使用与缓存的使用一致,这里缓存的对象是字符串对象。从设计模式角度来说,字符串常量池是享元设计模式的具体应用。
所以,在以后的字符串创建操作中,为了提高内存的复用率,尽量使用赋值运算符创建对象。
字符串的比较
针对字符串,最常用的功能就是字符串的相等比较。针对字符串的相等比较有两种选择:使用==运算符和使用equals方法。对于Java语言来说,==运算符,对于“值类型”和“空类型”是比较他们的值;对于“引用类型”是是比较对象在内存中的存放地址,即是否指向指向同一个对象。对Object类的equals方法来说,其功能与 == 运算符一致,但是String类重写了该方法,使其可以进行值相等比较。关键源码如下:
public class Object {
//...
public boolean equals(Object obj) {
return (this == obj);
}
}
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// ...
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
// 优先比较hashCode是否相等
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
}
所以,在对字符串进行相等比较时,为了减少字符串底层存储的优化带来的可能影响,尽量使用equals方法。更多equals方法和==运算符使用细节上的差异,可以参考笔者之前的文章。
除了相等比较,String类还实现了Comparable接口,支持自定义比较。关键代码如下:
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
其他常用方法
除了构造方法、比较方法,String类还提供了不少方便的功能,如:字符串分割、字符串转换、字符串格式化。这里不再进一步介绍,有兴趣的同学可以阅读相关源代码。
总结
String类是Java语言中使用频率极高的类,针对String类的源码分析有利于更好的理解和使用String类。String对象的最大特点是其不可变性。String的不可变性,使其在性能、不可篡改、并发安全等方面展现优越性。在使用String类时,要善于利用该特性。如HashMap使用String类型作为key。在查询key时,优先对其hash-code进行比较。在创建字符串时,为了提高内存的复用率,尽量使用赋值运算符创建对象。这种方式会优先从字符串常量池中获取重复对象,减少不必要的内存。不同的创建方式(直接赋值创建字符串、使用构造方法创建字符串)会带来字符串比较上的不同。为减少字符串底层存储的策略差异带来的影响,推荐使用equals方法来进行字符串的相等比较。为了更好的使用String类,还应熟悉其提供的常用方法,如:字符串比较、字符串分割、字符串转换、字符串格式化等。
参考
https://www.cnblogs.com/zhangyinhua/p/7689974.html Java常用类(二)String类详解
https://zhuanlan.zhihu.com/p/94228628 String 的不可变性
https://www.alpharithms.com/byte-array-sequences-of-8-bit-groups-183908/ 字节数组