一、对String类的了解
我们先看一下这个类的实现源代码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte[] value;
private final byte coder;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
······
}
从上面可以看出看几点:
1)String类是final类,意味着String类不能被继承,且它的成员方法都默认为final方法。
2)String类是通过char数组来保存字符串的。
下面继续看String类的一些方法实现:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = length() - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
if (beginIndex == 0) {
return this;
}
return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
: StringUTF16.newString(value, beginIndex, subLen);
}
public String concat(String str) {
int olen = str.length();
if (olen == 0) {
return this;
}
if (coder() == str.coder()) {
byte[] val = this.value;
byte[] oval = str.value;
int len = val.length + oval.length;
byte[] buf = Arrays.copyOf(val, len);
System.arraycopy(oval, 0, buf, val.length, oval.length);
return new String(buf, coder);
}
int len = length();
byte[] buf = StringUTF16.newBytesFor(len + olen);
getBytes(buf, 0, UTF16);
str.getBytes(buf, len, UTF16);
return new String(buf, UTF16);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar)
: StringUTF16.replace(value, oldChar, newChar);
if (ret != null) {
return ret;
}
}
return this;
}
从上面三个方法中可以看出:对String对象的任何改变都不影响到原对象,相关的任何Change操作都会生成新的对象。
二、深入理解String、StringBuilder、StringBuffer
1)我们先来看一个例子:
public static void main(String[] args) {
String str1="hello";
String str2=new String("hello");
String str3="hello";
String str4=new String("hello");
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str3);
}
运行结果:
解释:
在上述代码代码中,str1和str3都在编译期间生成了字面常量和符号引用。运行期间,字面常量"hello"被存储在运行常量池。JVM执行引擎会先在运行常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则就在运行常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。
通过new关键字来生成对象的操作是在堆中进行的。在堆区进行对象生成的过程是不会去检测该对象是否已经存在,即时字符串内容是相同的。
2)String、StringBuffer、StringBuilder的区别
为什么需要StringBuffer类和StringBuilder类?我们来看一段代码:
public static void main(String[] args) {
String string="";
for(int i=0;i<5;i++) {
string+="hello";
}
}
运行结果:
说明:
"string+="hello";"的过程相当于将原有的String变量指向的对象内容取出与“hello”作为字符串相加操作,再存进另一个新的String对象当中,再让String变量指向新生成的对象。如果我们再反编译一下机会发现,整个执行过程中,每次循环会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象。也就是说,这个循环执行完毕new出了5个对象,如果这些对象没有被回收的话会造成很大的内存资源浪费。
从上面我们可以看出:"string+="hello";的操作事实上会被JVM优化成:
StringBuilder str=new StringBuilder(string);
str.append("hello");
str.toString();
再看下面这段代码:
public static void main(String[] args) {
StringBuilder builder=new StringBuilder();
for(int i=0;i<5;i++) {
builder.append("hello");
}
}
反编译字节码文件得到:
我们明显可以看出,for循环从开始到结束,new操作只执行了一次,也就是说只生成了一个对象,append操作是在原有对象基础上进行的。相比于上面的,这段代码所占的资源要小的多。
那有了StringBuilder为什么还要StringBuffer呢?继续看代码:
//StringBuilder的append方法的实现
public StringBuilder append(String str) {
super.append(str);
return this;
}
//StringBuffer的append方法实现
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
从上面两段代码可以看出:StringBuilder和StringBuffer类所拥有的成员属性和成员方法基本相同,区别是StringBuffer类的成员方法前面多了synchronized关键字,使得StringBuffer是线程安全的。
三、不同场景下三个类的性能测试
通过代码来测试三个类的性能区别:
public class B {
private static int time=50000;
public static void main(String[] args) {
testString();
testOptimalString();
testStringBuilder();
testStringBuffer();
test1String();
test2String();
}
public static void testString() {
String s="";
long begin=System.currentTimeMillis();
for(int i=0;i<time;i++) {
s+="java";
}
long over=System.currentTimeMillis();
System.out.println("操作"+s.getClass().getName()+"类型使用的时间为:"+(over-begin)+"毫秒");
}
public static void testOptimalString() {
String s="";
long begin=System.currentTimeMillis();
for(int i=0;i<time;i++) {
StringBuilder sb=new StringBuilder();
sb.append("java");
s=sb.toString();
}
long over=System.currentTimeMillis();
System.out.println("模拟JVM优化操作的时间为:"+(over-begin)+"毫秒");
}
public static void testStringBuilder() {
StringBuilder sb=new StringBuilder();
long begin=System.currentTimeMillis();
for(int i=0;i<time;i++) {
sb.append("java");
}
long over=System.currentTimeMillis();
System.out.println("操作"+sb.getClass().getName()+"类型使用的时间为:"+(over-begin)+"毫秒");
}
public static void testStringBuffer() {
StringBuffer sb=new StringBuffer();
long begin=System.currentTimeMillis();
for(int i=0;i<time;i++) {
sb.append("java");
}
long over=System.currentTimeMillis();
System.out.println("操作"+sb.getClass().getName()+"类型使用的时间为:"+(over-begin)+"毫秒");
}
public static void test1String() {
long begin=System.currentTimeMillis();
for(int i=0;i<time;i++) {
String s="I"+"love"+"java";
}
long over=System.currentTimeMillis();
System.out.println("字符串直接相加操作:"+(over-begin)+"毫秒");
}
public static void test2String() {
String s1="I";
String s2="love";
String s3="java";
long begin=System.currentTimeMillis();
for(int i=0;i<time;i++) {
String s=s1+s2+s3;
}
long over=System.currentTimeMillis();
System.out.println("字符串间接相加操作:"+(over-begin)+"毫秒");
}
}
测试结果:
从上面的执行结果得到:
1.对于直接相加字符串,效率很高。
2.三者执行效率:StringBuilder>StringBuffer>String;当然这是相对的,不一定在所有情况下都是这样。比如:String str="hello"+"java"的效率就比StringBuilder st=new StringBuilder().append("hello").append("java");要高。
3.不同的情况选择不同的类:
当字符串相加操作或者改动较小的情况下,建议使用String str="hello";这种形式;
当字符串相加操作较多的情况下,建议使用StringBuilder;
如果使用了多线程,则使用StringBuffer。
四、常见面试题
1)代码的输出结果:
String s1="hello2";
String s2="hello"+2;
System.out.println(s1==s2);
输出结果为:true。“hello”+2在编译期间就已经优化成了“hello2“,因此在运行期间,变量s1和变量s2指向的是同一个对象。
2)代码的输出结果:
String s1="hello";
String s2="hello2";
String s3=s1+2;
System.out.println(s2==s3);
输出结果为:false。因为有符号引用,s3不会再编译期间被优化,不会把s1+2当作字面常量来处理,因此这种方式生成的对象事实上保存在堆上。
3)代码的输出结果:
String s1="hello2";
final String s2="hello";
String s3=s2+2;
System.out.println(s1==s3);
输出结果为:true。对于被final修饰的变量,会在字节码文件常量池中保存一个副本,不会通过连接而进行访问。对final变量的访问在编译期间就会代替为真实的值。s3在编译期间就会被优化为:string s3="hello"+2;下面是反编译内容:
4)代码的输出结果:
public class C {
public static void main(String[] args) {
String s1="hello2";
final String s2=getHello();
String s3=s2+2;
System.out.println(s1==s3);
}
public static String getHello() {
return "hello";
}
}
输出结果为:false。因为s2的赋值是通过方法调用返回的,他的值只能在运行期间确定,所以s1和s3指向的不是同一个对象。
5)代码的输出结果:
String s1="hello";
String s2=new String("hello");
String s3=new String("hello");
String s4=s2.intern();
System.out.println(s1==s2);
System.out.println(s2==s3);
System.out.println(s2==s4);
System.out.println(s1==s4);
输出结果为:false false false true。在String类中,intern方法是一个本地方法,该方法会在运行常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用;否则会将该字符串入池,并返回一个指向该字符串的引用。
6)String str=new String("abc");创建了多少个对象?
创建了一个,涉及到了两个对象。
7)1和2的区别:
String s1="i";
s1+="love"+"java";//1
s1=s1+"love"+"java";//2
1的效率比2的效率要高。1中的"love"+"java"在编译期间会被优化成"lovejava",而2不会被优化。1只进行了一次append操作,而2中进行了两次。