前提知识:
String、StringBuffer以及StringBuilder的区别
先看一个简单程序:
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
为什么会出现这样的结果?下面解释一下原因:
在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。
- 因此在上述代码中,String str1 = "hello world";和String str3 = "hello world"; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。
- 通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;
- 否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。
- 总所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。
后来我修改str2的值,str之前是new出来的,我添加 Str2=“world” ;看他是如何操作的?
public static void main(String[] args) {
String str1 = "hello world";
String str2 = new String("hello world");
String str3 = "world";
String str4 = new String("hello world");
String str5="hello world";
System.out.println(str2==str3);
str2= "world";
System.out.println(str2==str3);
System.out.println(str1==str2);
System.out.println(str1==str5);
System.out.println(str2==str4);
}
输出结果:
false
true
false
true
false
可以看出第一个为false,而第二个为true。
分析:没有使用new,而是先判断常量池是否有,若有的话, str2指向该引用。若没有的话,会在常量池里保留一份下来。但原先new出来的在堆上的一块空间还存在。
那么问题来了,如果这些对象有很多,且没有被回收,会造成多大的内存资源浪费。如下程序:
public class Lianxi {
public static void main(String[] args) {
String string = "";
for(int i=0;i<10000;i++){
string += "hello";
}
}
}
反汇编可知:
string+="hello"的操作事实上会自动被JVM优化成: (每次都要new,然后改变指向,GC还要回收,速度较慢)
StringBuilder str = new StringBuilder(string);
str.append("hello");
str.toString();
那么通过以下程序来比较以下:
public static void main(String[] args) {
//花费时间://new StringBuider.append,
// 每次都new
//String
long begin=System.currentTimeMillis();
String str=" ";
for(int i=0;i<50000;i++) {
str +=" ";
}
long end=System.currentTimeMillis();
System.out.println(end-begin);
//StringBuilder 所花费的时间
long begin1 = System.currentTimeMillis();
StringBuilder str1 = new StringBuilder(); //只用new一次,
str1.append(" ");
for(int i=0;i<50000;i++) {
str1.append(" ");
}
long end1 = System.currentTimeMillis();
System.out.println(end1-begin1);
//StringBuffer 所花费的时间
long begin2 = System.currentTimeMillis();
StringBuffer str2 = new StringBuffer(); //只new一次
str2.append(" ");
for(int i=0;i<50000;i++) {
str2.append(" ");
}
long end2 = System.currentTimeMillis();
System.out.println(end1-begin1);
}
输出结果:(每次结果可能不太一样,取决于那时电脑的执行速度)
但结果很明显,Str速度最慢,StringBuilder和StringBuffer相差不大。
2300
1
1
但通过查询java的源代码:
StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。
总结:
String:适用于少量的字符串操作的情况。
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。
注意:(面试题的考点)
String a = "hello2";
String b = "hello";
String c = b + 2;
System.out.println((a == c));
输出: flase
原因:
有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。
String a = "hello2";
final String b = "hello";
String c = b + 2;
System.out.println((a == c));
输出结果:true
原因;对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2;
public class Main {
public static void main(String[] args) {
String a = "hello2";
final String b = getHello();
String c = b + 2;
System.out.println((a == c));
}
public static String getHello() {
return "hello";
}
}
输出结果:false
这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。
字符串驻留:
String s0= “kvill”;
String s1=new String(”kvill”);
String s2=new String(“kvill”);
System.out.println( s0==s1 );
System.out.println( “**********” );
s1.intern();
s2=s2.intern(); //把常量池中“kvill”的引用赋给s2
System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2 );
输出结果:
false
**********
false //虽然执行了s1.intern(),但它的返回值没有赋给s1
true //说明s1.intern()返回的是常量池中”kvill”的引用
true
还有一点要注意:
String s1=new String("kvill");
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
输出结果:
false //s1==s1.intern()为false说明原来的“kvill”仍然存在;
kvill kvill
true //s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。
在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加 了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。
总结:
.intern()方法的时候,会将共享池中的字符串与外部的字符串(s)进行比较,如果共享池中有与之相等 的字符串,则不会将外部的字符串放到共享池中的,返回的只是共享池中的字符串,如果不同则将外部字符串放入共享池中,并返回其字符串的句柄(引用)-- 这样做的好处就是能够节约空间。