定义
先看一下文档中的注释
1 |
|
String对象是常量,创建之后就不能被修改,所以该对象可以被多线程共享。
1 |
public final class |
从源码中可以看出,String是被final修饰的,说明该类不能被继承。并且实现了CharSequence
, Comparable
, Serializable
接口。Serializable
接口用于实现String的序列化和反序列化操作;Comparable
接口用于实现字符串的比较操作;CharSequence
是字符串类的父接口,StringBuffer和StringBuilder都继承自该接口。
- value字段是实现String类的底层数组,用于存储字符串内容。final修饰基本数据类型,那么在运行期间其内容不可变,如果修饰的是引用类型,那么引用的对象(包括数组)运行期地址不可变,但是对象的内容是可以改变的。
- hash字段用于缓存String对象的hash值,防止多次计算hash造成的时间损耗。
- 因为String实现了
Serializable
接口,所以需要serialVersionUID字段用来在String反序列化时,通过对比字节流中的serialVersionUID和本地实体类中的serialVersionUID是否一致,如果相同就可以进行反序列化,否则就会抛出InvalidCastException
异常。
构造方法
空参构造方法
1 |
public () { |
该构造方法会创建一个空的字符序列,因为字符串的不可变对象,之后对象的赋值会指向新的字符串,因此使用这种构造方法会多创建一个无用对象。
使用字符串类型的对象初始化
1 |
public (String original) { |
直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后就不可改变,所以也就不用担心源String的值会影响到目标String的值。
使用字符数组初始化
1 |
// 参数为char数组,通过java.utils包中的Arrays.copyOf复制 |
使用字节数组初始化
在Java中,String实例保存有一个char[]字符数组,char[]字符数组是以Unicode编码方式存储的,String和char为内存形式,byte是网络传输或存储的序列化形式,所以在很多传输和存储过程中需要将byte[]数组和String进行相互转化。字节和字符自检的转化需要指定编码,不然很可能会出现乱码。String提供了多种字节数组的重载构造函数:
1 |
public String(byte bytes[], int offset, int length, String charsetName) |
如果我们在使用 byte[] 构造 String 的时候,如果指定了charsetName
或者charset
参数的话,那么就会使用 StringCoding.decode
方法进行解码,使用的解码的字符集就是我们指定的 charsetName
或者 charset
。如果没有指定解码使用的字符集的话,那么StringCoding
的decode
方法首先会使用系统的默认编码格式(ISO-8859-1)。
使用StringBuffer和StringBuilder初始化
1 |
public String(StringBuffer buffer) { |
因为StringBuilder不是线程安全的,所以在初始化时不需要加锁;而StringBuilder则需要加锁。我们一般使用StringBuffer和StringBuilder的toString
方法来获取String,而很少使用String的这两种构造方法。
特殊的构造方法
String除了提供了很多共有的构造方法,还提供了一个保护类型的构造方法:
1 |
String(char[] value, boolean share) { |
该方法和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 |
public boolean equals(Object anObject) { |
- 先判断两个对象的地址是否相等
- 在判断是否是String类型
- 如果都是String类型,就先比较长度是否相等,然后再逐一比较值。值的比较采取了短路操作,发现不一样的就返回false
compareTo方法
1 |
public int compareTo(String anotherString) { |
从0开始逐一判断字符是否相等,若不相等则做差运算,巧妙的避免了三种判断情况。若字符都相等,接直接返回长度差值。所以在判断两个字符串大小时,使用是否为正数/负数/0,而不是通过1//-1/0判断。
hashCode方法
1 |
public int hashCode() { |
- 若第一次调用
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值就相同。 - 返回缓存的hash值。
replaceFirst、replaceAll,replace区别
1 |
String replaceFirst(String regex, String replacement) |
replace
的参数是char和CharSequence,既可以支持字符的替换,也支持字符串的替换replaceFirst
和replaceAll
的参数是regex,基于正则表达式替换replace
和replaceAll
方法会替换字符串中的全部字符或字符串,replaceFirst
只替换第一次出现的字符或字符串
copyValueOf和valueOf
String的底层是通过char[]实现的,早期的String构造器的实现并不会拷贝数组。为了防止char[]数组被外部修改,提供了copyValueOf
方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String在构造器中就通过拷贝新数组实现,所以这两个方法在本质上已经没区别了。
valueOf()
有很多种重载形式:
1 |
public static String valueOf(boolean b) { |
底层调用了基本数据类型的toString()
方法。
intern方法
1 |
public native String intern(); |
intern
方法是Native调用,它的作用是每当定义一个字符字面量,字面量进行字符串连接或final的String字面量初始化的变量的连接,都会检查常量池中是否有对应的字符串,如果有就不创建新的字符串,而是返回指向常量池对应字符串的引用。所有通过new String(str)
方式创建的对象都会保存在堆中,而不是常量区。普通变量的连接,由于不能在编译期确定下来,所以不会储存在常量区。
其他方法
1 |
int length() //返回字符串长度 |
String对“+”的重载
Java不支持运算符重载,但是String可以通过+
来连接两个字符串。那么java是如何实现对+
的重载的呢?
1 |
public class Main{ |
反编译Main.java,执行命令javap -c Main
,输出结果:
我们看到了StringBuilder,还有windy和lee,以及调用了StringBuilder的append
和toString
方法。既然编译器已经在底层为我们进行了优化,那么为什么还要提倡我们用StringBuilder呢?
我们注意到在第3行代码,new了一个StringBuilder对象,如果实在一个循环里面,我们使用”+”号就会创建多个StringBuilder的对象。但是编译器事先不知道我们StringBuilder的长度,并不能事先分配好缓冲区,会加大内存的开销,而且使用重载的时候根据java的内存分配也会创建多个对象。
switch对字符串支持的实现
1 |
public class Main { |
反编译之后得到
1 |
public static void main(String args[]) { |
- 首先调用String的
hashCode
方法,拿到相应的Code,通过这个code然后给每个case唯一的标识 - 判断时先获取对象的hashCode,进入对应的case分支
- 通过
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源码解析