关于String及StringBuilder的几点区别

来源:互联网 发布:kindle阅读器mac版 编辑:程序博客网 时间:2024/06/11 01:51

直接看示例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= hellosb= 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));        }}

输出:

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

这里继续使用javap来分析:

String s=null;s+=1;
0: aconst_null   1: astore_1      2: new           #2                  // class java/lang/StringBuilder5: dup           6: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V9: 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,效率当然就比后者高了;

0 0
原创粉丝点击