对Java中String、StringBuilder、StringBuffer三者的理解

一、对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中进行了两次。

猜你喜欢

转载自blog.csdn.net/Marmara01/article/details/80305074