在Java中,与C++一样,也是亲自封装了字符串,命名为String类,用来对字符串的常用操作,其中Java也仿照String类设计出了其它的几个字符串类,用来处理String类不能处理的场景,分别是StringBuffer类与StringBuilder类,下面将通过源码来详细的讲解它们之间的相同与不同处。
String类
String类是一个不可被继承的类,也是一个不可变类,当其内容改变的时候,都会在静态常量池中创建一个对象。它实现了Serializable、Comparable、CharSequence三个接口,用来分别实现序列化、字符串的比较以及字符序列的。在Java中,所有的像如下定义的字符串都被默认为是String对象:
String a="asdf";
StringBuffer b="asdf" 2式
在定义二式后,编译器会自动提醒如下错误:
Required: java.lang.StringBuffer Found: java.lang.String
这也从侧面证明了静态的字符串是String对象。而且String类分为两种情况,一种是在jvm中存贮在常量池中,实现对象的共享机制,也就是上面描述的;而另外一种是存贮在堆上的,就是新创建一个对象,下面来看看这个例子:
public class Test {
public static void main(String args[]) {
String a="hello";
String b="hello";
String c=new String("hello");
System.out.println(a==b); //true
System.out.println(b==c); //false
}
}
如上面的代码结果所示,静态定义的字符串如果首次出现会将其放入常量池中,后出现的静态字符串如果值相同,则不会重新新建一个对象,而是新建一个引用,来指向常量池中与其相同内容的地址,也就是说两者是指向同一个地址,同一个内容。而采用构造器来创建String对象时,也会检查是否常量池中有没有,如果有则直接创建堆空间,并将引用指向它,如果没有则会将其放入常量池,再创建堆空间。另外注意的是,’+'号在拼接String对象时也有着两种策略,如下代码所示:
public class Test {
public static void main(String args[]) {
String a="hello";
String b="he"+"llo";
String c="he"+new String();
System.out.println(a==b); //true 1式
System.out.println(b==c); //false 2式
}
}
jvm在处理1式的时候,会自动的把"he"+“llo"直接为"hello”,所以1式的结果是true,而针对"he"+new String(),jvm并不知道将其转化为什么类型的值,则就直接新创建一个String对象,用来存贮该值,所以为false。而String类也提供了两种方法用来实现StringBuffer与StringBuilder类转化为String类,其中转化StringBuffer是线程安全的,加了同步方法:
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());
}
另外一个String的equals方法则是先判断是否地址相同,然后在判断是否是String类的对象,在比较其中的每个字符。我们需要注意的是String类里面的唯一一个本地方法,intern方法:
public native String intern();
在jdk 1.6以前,该方法返回一个常量池中的String对象的引用,假如该常量池中没有与实例相同值的String对象,则将该String对象实例值复制一遍并新建String类对象放入常量池,如果有,则返回常量池中对应的String对象的引用。而在jdk 1.7以后,当常量池中没有对应类的String对象时,jvm则不会复制实例,而是在常量池中记录第一次实例出现的地址,并返回该地址的引用,如果有,则返回对应常量池中String类的引用。我们用深入理解Java虚拟器的例子来讲解(基于jdk 1.8):
public class Test {
public static void main(String args[]) {
String a = "123";
System.out.println(a.intern() == a); //true
String s = new String("ddd");
System.out.println(s.intern() == s);//false
String s1 = new String(new StringBuilder("计算机").append("软件"));//true
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern() == s2);//false
String s3 = new String(new StringBuilder("计算机软件"));//true
System.out.println(s1.intern() == s1);
}
}
对于静态字符串a来说,在定义的时候就会被放到静态的常量池中,所以它的地址与常量池中的地址是相同的,所以是true;对于s字符串而言,是一个考验仔细阅读的能力的题,很多人会用jdk 1.7以后的解释来解释这个情况,但得出的答案是true,但其结果却是false,那是因为没有注意到:
String s = new String("ddd");
中的"ddd","ddd"这本身就是一个静态的字符串,也就是上文说采用new创建String类会先检查需要被创建的值有无在常量池中出现过。第一次出现会将"ddd"的地址放入静态常量区,而String方法的构造器只是另外的复制了其值,并开辟了另外的一块空间把值放进去,这里是第二次出现,而不是第一次,静态常量区中保存的是"ddd"的地址,所以是false。对于s1来说,因为是append操作,所以返回的是一个全新的String类对象,所以是true,对比s3,可验证这个正确性。而对于s2来说,"java"这个字符串本身就是静态常量池中的对象,所以是false。