面试回答
String 是不可变的,StringBuilder 和 StringBuffer 是可变的。而 StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。
知识扩展
String 的不可变性
String 在 Java 中特别常用,相信很多人都过他的源码,在 JDK 中,关于 String 的类声明是这样的:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
可以看到,String 类是 final 类型的,表示这个类不可以被继承。
其次,String 中存储的 char[] 也是被 final 修饰的,表示他也是不能修改的。
所以,String 是一个不可变对象。
不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。
可是有人会有疑惑,String 为什么不可变,我的代码中经常改变 String 的值啊,如下:
String str="abcd";
str=str.concat("ef");
这样操作,不就将原本的 “abcd”的字符串改变成“abcdef”了么?
我们看一下 concat 的源码:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
所以,虽然字符串内容看上去从“abcd”变成了“abcdef”,但是实际上,我们得到的已经是一个新的字符串了。
如上图,在堆中重新创建了一个“abcdef”字符串,和“abcd”并不是同一个对象。
所以,一旦一个 string 对象在内存(堆)中创建出来,他就无法被修改。而且,String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果我们想要一个可修改的字符串,可以选择 StringBuffer
或者 StringBuilder
这两个代替 String
。
String 的“+”是如何实现的
使用+拼接字符串,其实只是 Java 提供的一个语法糖,那么,我们来解一解这个语法糖,看看他的内部原理到底是如何实现的。
还是这样一段代码。我们把他生成的字符码进行反编译,看看结果。
String str1="hello";
String str2="world";
String text=str1+str2;
反编译后的内容如下,反编译工具为 jad。
String str1="hello";
String str2="world";
String text=(new StringBuilder()).append(str1).append(str2).toString();
通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将 String 转成了 StringBuilder 后,使用其 append 方法进行处理的。
那么也就是说,Java 中的堆+对字符串的拼接,其实现原理是使用 StringBuilder.append。
StringBuffer 和 StringBuilder
接下来我们看看 StringBuffer
和 StringBuilder
的实现原理
和 String
类类似,StringBuilder
类也封装了一个字符数组,定义如下:
char[] value;
其 append 源码如下:
public StringBuilder append(String str) {
super.append(str);
return this;
}
该类继承了 AbstractStringBuilde
类,看下其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;
}
append 会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。
StringBuffer
和 StringBuilder
类似,最大的区别就是 StringBuffer
是线程安全的,看一下 StringBuffer
的 append
方法。
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
该方法使用 synchronized
进行说明,说明是一个线程安全的方法。而 StringBuilder
则不是线程安全的。
不要在 for 循环中使用 + 拼接字符串
前面我们分析过,其实使用 +
拼接字符串的实现原理也是使用的 StringBuilder
,那为什么不建议大家在 for 循环中使用呢?
我们把以下代码反编译下:
long t1=System.currentTimeMillis();
String str="tango";
for (int i = 0; i <50000 ; i++) {
String s=String.valueOf(i);
str+=s;
}
long t2=System.currentTimeMillis();
System.out.println("+ cost:"+(t2-t1));
反编译后代码如下:
long t1 = System.currentTimeMillis();
String str = "tango";
for(int i = 0; i < 50000; i++)
{
String s = String.valueOf(i);
str = (new StringBuilder()).append(str).append(s).toString();
}
long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
我们可以看到,反编译后的代码,在 for
循环中,每次都是 new
了一个 StringBuilder
,然后把 String 转成 StringBuilder
,再进行 append
。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
所以,阿里巴巴 Java 开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder
的 append
方法进行扩展。而不是使用 +
。