String类
打开 String 类就会发现,它是被 final 修饰的:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
......
}
从上面可以看出几点:
- String 类是 final 类,也即意味着 String 类不能被继承,并且它的成员方法都默认为 final 方法。在 Java 中,被 final 修饰的类是不允许被继承的,并且该类中的成员方法都默认为 final 方法。对于用 final 来修饰方法,只有在确定不想让该方法被覆盖时,才将方法设置为final。
- 上面列举出了 String 类中所有的成员属性,从上面可以看出String类其实是通过char数组来保存字符串的,而且这个 char 数组不能被改变。
而且,对于 String 类来说,无论是 sub、concat 还是 replace 操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
在这里要永远记住一点:
“对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的对象”。
String 的不可变性
虽然 String、StringBuffer 和 StringBuilder 都是 final 类,它们生成的对象都是不可变的,而且它们内部也都是靠 char 数组实现的。
但是不同之处在于,String类中定义的char数组被 final 修饰,而StringBuffer和StringBuilder都是继承自AbstractStringBuilder类。
在AbstractStringBuilder中,char数组只是一个普通是私有变量:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
...
}
因此,对于StringBuffer 和 StringBuilder 对象,都可以通过调用它们的 append()
方法来不断修改value属性。
String类不可变性的好处
- 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么 String interning 将不能实现(String interning 是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串)。因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
- 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
- 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
String str=“hello world"和String str=new String(“hello world”)的区别
public class Main {
public static void main(String[] args) {
String str1 = "hello world";
String str2 = new String("hello world");
String str3 = "hello world";
String str4 = new String("hello world");
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str4);
//false
//true
//false
}
}
分析
在上述代码中,·String str1 = "hello world";
和 String str3 = "hello world";
都在编译期间生成了字面常量(literal)和符号引用(symbolic reference),运行期间字面常量 “hello world” 被存储在运行时常量池(当然只保存了一份)。
通过这种方式(String str = “hello world”;)来将String对象跟引用绑定的话,JVM 执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。
String 和 StringBuilder 的区别
对比 1
既然在Java中已经存在了 String 类,那为什么还需要 StringBuilder 呢?
那么看下面这段代码:
public class Main {
public static void main(String[] args) {
String string = "";
for(int i=0;i<10000;i++){
string += "hello";
}
}
}
这句 string += “hello”; 的过程,相当于将原有的 String 变量指向对象的内容取出,并与"hello"作字符串相加操作,再存进另一个新的 String 对象当中,再让 string 变量指向新生成的对象。
分析
public com.concretepage.lang.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: sipush 10000
9: if_icmpge 38
12: new #3 // class java/lang/StringBuilder
15: dup
16: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
19: aload_1
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: ldc #6 // String hello
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_1
32: iinc 2, 1
35: goto 5
38: return
从这段反编译出的字节码文件可以很清楚地看出:从第 12 行开始到第 35 行是整个循环的执行过程,并且每次循环会 new 出一个 StringBuilder 对象,然后进行append操作,最后通过toString方法返回String对象。也就是说这个循环执行完毕new出了10000个对象。
因此,当这些对象还没有及时被回收时,大量的内存资源就暂时无法被使用。而且,对于回收大量对象这个操作本身,也是非常消耗资源的。
我们基于上面的字节码的语义进行分析,其实,上面的代码就等价于下面这样:
public class Main {
public static void main(String[] args) {
String string = "";
for(int i=0;i<10000;i++){
StringBuilder str = new StringBuilder(string);
str.append("hello");
string = str.toString();
}
}
}
再看下面这段代码:
public class Main {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
for(int i=0;i<10000;i++){
stringBuilder.append("hello");
}
}
}
反编译字节码文件得到:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: sipush 10000
14: if_icmpge 30
17: aload_1
18: ldc #4 // String hello
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: iinc 2, 1
27: goto 10
30: return
从这里可以明显看出,这段代码的for循环只进行了一次 new 操作,也就是说只生成了一个对象,append 操作是在原有对象的基础上进行的。因此在循环了 10000 次之后,这段代码所占的资源要比上面小得多。
对比 2
在某些特殊情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。
其实这是 JVM 的一个把戏,在 JVM 眼里,这个String S1 = “This is only a” + “ simple” + “test”;
其实就是:String S1 = “This is only a simple test”;
所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 + S3 + S4;
这时候 JVM 会规规矩矩的按照原来的方式去做。
StringBuffer 和 StringBuilder 的区别
那么有人会问既然有了StringBuilder类,为什么还需要StringBuffer类?查看源代码便一目了然,事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。
下面摘了2段代码分别来自StringBuffer和StringBuilder,insert方法的具体实现:
StringBuilder的insert方法
public StringBuilder insert(int index, char str[], int offset, int len) {
super.insert(index, str, offset, len);
return this;
}
StringBuffer的insert方法:
public synchronized StringBuffer insert(int index, char str[], int offset, int len)
{
super.insert(index, str, offset, len);
return this;
}
结论
- 对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I”+“love”+“java”; 的字符串相加,在编译期间便被优化成了"Ilovejava"。这个可以用javap -c命令反编译生成的class文件进行验证。
- 对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
- String、StringBuilder、StringBuffer三者的执行效率:StringBuilder > StringBuffer > String
- 当字符串相加操作或者改动较少的情况下,建议使用 String str=“hello"这种形式;
- 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。
Reference
- 《Thinking in Java》
- String,StringBuffer, StringBuilder 的区别是什么?String为什么是不可变的? - https://juejin.im/post/5a5d5c66f265da3e261bf46c