关于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,效率当然就比后者高了;