关于String及StringBuilder的几点区别

直接看示例1:

public class StringTest{
	
	void stringReplace(String strTemp){
		strTemp=strTemp.replace('l','i');
	}

	void stringBufferAppend(StringBuffer sbTemp){
		sbTemp=sbTemp.append('c');
	}

	public static void main(String[] args){
		StringTest st=new StringTest();
		String str=new String("hello");
		StringBuffer sb=new StringBuffer("hello");

		// String str1=new String("hello");
		// StringBuffer sb1=new StringBuffer("hello");

		// System.out.println("str.equals(sb)= "+str.equals(sb));
		// System.out.println("str.equals(str1	)= "+str.equals(str1));
		// System.out.println("sb.equals(str)= "+sb.equals(str));
		// System.out.println("sb.equals(sb1)= "+sb.equals(sb1));

		st.stringReplace(str);
		st.stringBufferAppend(sb);
		System.out.println("str= "+str+"\tsb= "+sb);
	}
}
输出结果:

str= hello	sb= helloc
上面的示例无非就是方法传值与传址的问题,因为形参为引用类型,因此实参传递过来的是对象的地址值,问题来了:改变该地址所对应的内容,实参应该也会跟着发生变化,如sb最终结果那样,但str却没有,为何?

这就是String与StringBuilder的区别:

字符串的可变与不可变

JDK_1.8中这样解释:

Strings are constant; their values cannot be changed after they are created. 
String buffers support mutable strings. Because String objects are immutable 
they can be shared.

String创建的字符串是不可变的,而StringBuilder(或StringBuffer)通过字符缓冲区创建字符串,可变;

先来解析下String的不可变:

String s="hello";
s="world";
对象s在创建的时候先查看常量池中是否有"hello",有则指向它,没有就创建一个"hello",再指向它,但当重新对s赋值为"world"时,常量池中的"hello"并没有改变,java会重新开辟一个内存存储新值"world",并令s指向"world";

因此在示例1中,当调用stringReplace()方法时,实参str所存储的地址值传给了形参strTemp后,strTemp指向了常量池中"hello",当对其进行字符替换时,java会重新创建"heiio",并让形参strTemp指向它,因此形参和实参指向了不同的区域,结果自然显而易见;

另外,String的不可变是由于:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];//final的作用,使之初始化后不可改变
    ……
}

(P.S.虽然value是用final来修饰的,但仍有办法可以直接改变其值,具体可以参见 反射 )

再来对比下StringBuilder的可变:

StringBuilder sb=new StringBuilder("hello");
sb.append("world");
在创建StringBuilder对象时,java实际上在堆中创建了字符类型数组char[],存储完"hello"后通过append方法在增加"world",并未开辟新的内存区,形参和实参指向同一区域,因此形参对对象内容进行变化,实参也会跟着变化;

来看下StringBuilder构造器的底层代码:

public StringBuilder() {
        super(16);
    }
public StringBuilder(int capacity) {
        super(capacity);
    }
public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
}
public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
}
其基类AbstractStringBuilder的构造方法:
char[] value;
AbstractStringBuilder(int capacity) {
        value = new char[capacity];
}
……

可以发现StringBuilder实际创建了一个默认16字符长的char型数组(亦可指定长度):char[] value;

若为对象赋值时,所存储的字符串长度未超过所定义的char数组长度,则按顺序存储相应字符,若超过所定义的数组长度,则自动扩充其长度,因此说StringBuffer是可变的;

我们还是继续看append()底层代码比较形象:

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
}
当为StringBuilder对象增加内容时,会先计算所需最小空间:minimumCapacity=count+len;并与原数组长度的2倍+2即newCapacity进行比较,取较大值并为数组value重新赋值,赋值采用的是数组复制的方式进行;

常见的数组复制有两种方法:一为:Arrays.copyOf(),另一为:System.arraycopy(),差别在于后者可能存在下标越界的问题,其实前者也是通过调用后者的方法来实现的:

public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
}

由于重新为临时数组copy定义了长度newLength,因此复制的时候不会出现越界问题;在数组复制结束后,将临时数组copy赋值给value,使之指向新的区域,完成append()操作 (这里注意:引用变量value的指向发生了变化,但StirngBuilder的指向没有变,只不过其成员变量(恰巧是个引用变量value)的值发生了改变,因此形参sbTemp和sb仍指向同一区域);

equals方法

示例1中注释掉的几个equals()语句,结果只有String与String比较时才返回true,这是String与StringBuilder的另一个区别;

我们要直到equals()方法的返回结果取决于你如何重写的,如直接继承Object的方法,则比较的是变量的值,对于StringBuilder,它并未重写该方法,因此对两个引用变量进行比较自然返回false;而String类重写了该方法,具体如下:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
可知,若两个引用变量指向同一区域,则自然返回true,若非,则判断anObject是否是String的实例,若是则进行值比较,否则直接返回false;

效率的差别

public class StringBuilderTest2{
	public static void main(String[] args){
            
            StringBuilder sb=new StringBuilder();
            StringBuilder sb1=new StringBuilder(Integer.MAX_VALUE/7);
            String s=null;
            System.out.println(Integer.MAX_VALUE+"\n"+sb1.capacity());
            long start=System.currentTimeMillis(),end;
            for(int i=0;i<1000000;i++){
                sb.append(1);
            }
            end=System.currentTimeMillis();
            System.out.println("默认初始化SB时循环100w次耗时:"+(end-start));
            for(int i=0;i<1000000;i++){
                sb1.append(1);
            }
            start=System.currentTimeMillis();
            System.out.println("给定值初始化SB1时循环100w次耗时:"+(start-end));
            
            for(int i=0;i<100000;i++){
                s+=1;
            }
            end=System.currentTimeMillis();
            System.out.println("String“+”运算10W次耗时:"+(end-start));
        }
}

输出:

2147483647
306783378//初始化值
默认初始化SB时循环100w次耗时:35
给定值初始化SB1时循环100w次耗时:16
String“+”运算10W次耗时:3211
可以发现在循环“+”/append()运算时,String类的效率明显低于StringBuilder,为何?

这里继续使用javap来分析:

String s=null;
s+=1;
0: aconst_null   
1: astore_1      
2: new           #2                  // class java/lang/StringBuilder
5: dup           
6: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
9: aload_1       
10: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
13: iconst_1      
14: invokevirtual #5                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
17: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
20: astore_1      
21: return 

可以看到编译器在解析String"+"运算时,会转换成StringBuilder,调用append()方法,再通过toString()赋值,因此循环运算时,就不断发生创建对象和产生垃圾,这过程消耗了资源,造成了效率的下降;

另外,我们也可以发现调用StringBuilder构造方法时若给它传入较大整数作为参数,则运算效率也会有明显的提升,这跟前面所提扩容时数组复制有关,给定较大初始缓存区,自然不需要频繁扩容;
当然,这里顺带提一下,如果直接把String的"+"运算写成形如:s=""+1+1+1……,而不是通过循环来不断调用变量,则编译器会直接求出其字面量,运行时不会再转换为StringBuilder,效率当然就比后者高了;

posted @ 2014-05-05 23:59  冰川孤辰  阅读(113)  评论(0编辑  收藏  举报