定义
先看一下文档中的注释
12345 | * 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对象是常量,创建之后就不能被修改,所以该对象可以被多线程共享。
12345678910111213 | 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都继承自该接口。
- value字段是实现String类的底层数组,用于存储字符串内容。final修饰基本数据类型,那么在运行期间其内容不可变,如果修饰的是引用类型,那么引用的对象(包括数组)运行期地址不可变,但是对象的内容是可以改变的。
- hash字段用于缓存String对象的hash值,防止多次计算hash造成的时间损耗。
- 因为String实现了
Serializable
接口,所以需要serialVersionUID字段用来在String反序列化时,通过对比字节流中的serialVersionUID和本地实体类中的serialVersionUID是否一致,如果相同就可以进行反序列化,否则就会抛出InvalidCastException
异常。
构造方法
空参构造方法
123 | public () { this.value = "".value;} |
该构造方法会创建一个空的字符序列,因为字符串的不可变对象,之后对象的赋值会指向新的字符串,因此使用这种构造方法会多创建一个无用对象。
使用字符串类型的对象初始化
1234 | public (String original) { this.value = original.value; this.hash = original.hash;} |
直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后就不可改变,所以也就不用担心源String的值会影响到目标String的值。
使用字符数组初始化
12345678910 | // 参数为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提供了多种字节数组的重载构造函数:
123456 | 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
。如果没有指定解码使用的字符集的话,那么StringCoding
的decode
方法首先会使用系统的默认编码格式(ISO-8859-1)。
使用StringBuffer和StringBuilder初始化
123456789 | 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除了提供了很多共有的构造方法,还提供了一个保护类型的构造方法:
1234 | 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方法
123456789101112131415161718192021 | 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;} |
- 先判断两个对象的地址是否相等
- 在判断是否是String类型
- 如果都是String类型,就先比较长度是否相等,然后再逐一比较值。值的比较采取了短路操作,发现不一样的就返回false
compareTo方法
123456789101112131415161718 | 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方法
123456789101112 | 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;} |
- 若第一次调用
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区别
1234 | String replaceFirst(String regex, String replacement)String replaceAll(String regex, String replacement)String replace(Char Sequencetarget, Char Sequencereplacement)String replace(CharSequence target, CharSequence replacement) |
replace
的参数是char和CharSequence,既可以支持字符的替换,也支持字符串的替换replaceFirst
和replaceAll
的参数是regex,基于正则表达式替换replace
和replaceAll
方法会替换字符串中的全部字符或字符串,replaceFirst
只替换第一次出现的字符或字符串
copyValueOf和valueOf
String的底层是通过char[]实现的,早期的String构造器的实现并不会拷贝数组。为了防止char[]数组被外部修改,提供了copyValueOf
方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String在构造器中就通过拷贝新数组实现,所以这两个方法在本质上已经没区别了。
valueOf()
有很多种重载形式:
12345678910111213141516171819 | 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)
方式创建的对象都会保存在堆中,而不是常量区。普通变量的连接,由于不能在编译期确定下来,所以不会储存在常量区。
其他方法
12345678910111213141516171819202122232425262728293031323334353637383940414243 | 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) //判断字符串是否包含字符序列sString[] 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是如何实现对+
的重载的呢?
123456 | 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的append
和toString
方法。既然编译器已经在底层为我们进行了优化,那么为什么还要提倡我们用StringBuilder呢?
我们注意到在第3行代码,new了一个StringBuilder对象,如果实在一个循环里面,我们使用”+”号就会创建多个StringBuilder的对象。但是编译器事先不知道我们StringBuilder的长度,并不能事先分配好缓冲区,会加大内存的开销,而且使用重载的时候根据java的内存分配也会创建多个对象。
switch对字符串支持的实现
1234567891011121314 | 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; } }} |
反编译之后得到
123456789101112131415 | 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; } } |
- 首先调用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源码解析