Java中的字符串
Java中没有原生的字符串类型,但是提供了String
、StringBuffer
和StringBuilder
来表示字符串,在它们的代码实现中都是通过char[]
来存储字符串中的字符的。下图是它们的继承关系:
它们的区别如下:
- 可变性:
String
是不可变的,StringBuilder
和StringBuffer
是可变的。 - 安全性:
String
和StringBuffer
是线程安全的,StringBuilder
是线程不安全的。
可变性
字符串的可变性体现在是否可以修改char[]
数组。首先,char[]
数组的访问权限要么是私有的,要么是包访问权限,这意味着不可能直接修改char[]
数组;其次,String
、StringBuffer
和StringBuilder
都被final
修饰,这意味着不可能通过继承的方式修改char[]
数组;最后,String
类没有提供任何修改char[]
数组的API,而StringBuffer
和StringBuilder
提供了大量的修改API。因此,String
是不可变的,而StringBuilder
和StringBuffer
是可变的。
字符串的特殊性
- 字符串字面量:在Java中实例化对象一般有三种方式(
new
关键字、调用clone()
方法和反序列化),但是,字符串对象可以直接使用String str="字符序列"
的形式实例化。 - 操作符的重载:当一个字符串对象通过
+
或+=
与其它对象相加时,这两种操作符会被重载为对字符串拼接的操作,并且这个操作会由StringBuilder
实现:
int a=1;
String str="a="+a;
//字节码为:
iconst_1
istore_1
new #2 <java/lang/StringBuilder>
dup
invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
ldc #4 <a=>
invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
iload_1
invokevirtual #6 <java/lang/StringBuilder.append : (I)Ljava/lang/StringBuilder;>
invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
astore_2
字符串常量池
由于字符串的不可变性以及字符串使用的频繁性,JVM在堆中通过哈希表实现了一个字符串常量池,用于避免字符串的重复创建。在使用字符串字面量实例化字符串对象时,如果字符串常量池中没有该字符串,就会将该字符串实例化并将该字符串的引用加入字符串常量池;如果字符串常量池中有该字符串引用,那么就会直接返回该引用。
public static void main(String[] args) {
String str1 = "helloWorld";
String str2 = "helloWorld";
System.out.println(str1 == str2);//true
}
new关键字实例化字符串时创建了几个字符串对象
如果已经了解了字符串常量池,那么再看这个问题就是一个笑话。
String str1=new String("helloWorld");
String str2new String(new char[]{'h','e','l','l','o','W','o','r','l','d'});
先看str1
,很明显是两个:一个字面量对象和一个new
出来的对象:
再看str2
,就只有一个new
出来的对象:
字符串的intern方法
intern()
方法是一个本地方法,该方法会判断字符串常量池是否存在指向当前字符串对象的引用,如果存在,则返回该引用;如果不存在,则先在常量池中添加指向该字符串对象的引用,然后再返回该引用。请看下面两段程序的分析图加深理解:
String str=new String("helloWorld");
System.out.println(str.intern()==str);//false
String str=new String("hello")+new String("World");
System.out.println(str.intern()==str);//true
字符串拼接中的优化
当字符串字面量进行拼接时,拼接后的字符串也将直接放在字符串常量池中,原因是编译器在编译前可以直接确认它的值,因此在编译时进行了常量折叠优化:
String str1 = "hello";
String str2 = "h"+"e"+"l"+"l"+"o";
System.out.println(s1==s2);//true
如果拼接字符串中有一个是变量或是通过new
关键字实例化的对象,那么拼接后的字符串将放在堆中,原因是编译器在编译前不能确认它们的值,只能当作操作符重载处理:
String s1 = "helloWorld";
String s2 = "hello";
String s3 = "World";
String s4 = new String("hello") + new String("World");
String s5 = new String("hello") + "World";
String s6 = s2 + "World";
String s7 = "hello" + "World";
System.out.println(s1==s2+s3);//false
System.out.println(s1==s4);//false
System.out.println(s1==s5);//false
System.out.println(s1==s6);//false
System.out.println(s1==s7);//true
final
修饰的字面量实例化的字符串编译器在编译前也可以直接确认它的值:
public static void main(String[] args) {
String s1 = "helloWorld";
final String s2 = "World";
String s3 = new String("hello") + s2;
String s4 = "hello" + s2;
System.out.println(s1==s3);//false
System.out.println(s1==s4);//true
}