java字符串相关的类有三个,String、StringBuilder、StringBuffer,下面浅谈一下三者的区别
String
我们先看源码中它的属性(也就是用什么保存数据)
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
这是最重要的两个属性,我们重点看一下value,它是一个字符串数组,用来保存string的内容,值得注意的是,它是final的,也就意味值一旦赋值,就不可改变。所以大家经常说的string类型是不可变的就是这个意思,但是如果不能变的话,它怎样执行toLowerCase()和replace等操作呢?它是通过创建新的字符数组,然后作为返回值来实现的。我们可以看下下面的代码就知道了。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
因为每次执行这些操作都要重新创建新的字符串,所以String的效率就会受影响。
但是java设计者这样设计肯定是有他的原因的,主要原因是一下三个方面:
1.字符串池的需求
java中string对象有一个大家都知道的创建方法:
String str = "abc";
通过这种方法创建的字符串对象保存在字符串池中,(字符串池既不在栈中,也不在堆中,而是保存在独特的方法区中),创建的时候,先在字符串池中寻找是否存在“abc”,如果存在,则直接把它的引用赋值给str,如果不存在,则创建后赋值。这里需要注意的是,字符串池中的字符串并不会因为失去引用而被垃圾回收机制回收。
如果字符串不是不可改变的,那么一个引用字符串的改变,将会导致另外一个引用出现脏数据。
2.允许字符串缓存哈希码
在java中经常会用到字符串的哈希码(可以看前面String中hash的定义),String的不变性保证了哈希码的始终如一,所以也就不用担心哈希码发生变化,这样的话,就不用在使用的时候每次都计算重新计算哈希码,效果就会高很多。
3.安全
String广泛的用于java 类中的参数,如:网络连接(Network connetion),打开文件(opening files )等等。如果String不是不可变的,网络连接、文件将会被改变——这将会导致一系列的安全威胁。操作的方法本以为连接上了一台机器,但实际上却不是。由于反射中的参数都是字符串,同样,也会引起一系列的安全问题。下面给一个代码例子:
boolean connect(string s){
if (!isSecure(s)) {
throw new SecurityException();
}
//here will cause problem, if s is changed before this by using other references.
causeProblem(s);
}
StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence
从上面定义可以看到,StringBuild是从AbstractStringBuilder继承而来,大家有兴趣的话可以去看一下它们的源代码。
StringBuilder与string不同的地方在于,他是初始使用长度为16的char[]来存储数据,如果字符串长度超过则自动扩展,所以他能够实现在原来字符串上添加字符而不需要重新创建一个新的字符串。
可以使用toString方法将它转换成string
StringBuffer
StringBuffer也是从AbstractStringBuilder继承而来,它与stringbuffer的区别在于:
- StringBuileder是一个单线程使用的类,不执行线程同步,所以StringBuilder速度比较快,效率比较高,但是是线程不安全的。
- StringBuffer是线程安全的,所以相对应的,就要牺牲一下速度和效率。
三者执行效率比较
一般来说,三者的执行效率为 StringBuilder > StringBuffer > String
一道经典的面试题
以下代码创建了多少个对象
String str = new String("a" + "b");
一般来说,我们可以这样理解:首先创建对象”a“和”b“,然后创建“ab”,这三个都是存在于字符串池中,最后再用“ab“当参数,在堆中new一个字符串,所以总共创建了4个对象。注意:str不是对象,只是对象的引用。
但是实际上并没有创建4个对象,只创建了两个对象,因为编译器进行了自动优化:编译器创建了一个StringBuilder对象,并为”a“和”b“分别调用一次append()方法,最后调用toString()方法时候生成结果。所以现在为止只生成了一个对象。然后new让他在堆上创建一个对象,所以,是两个对象。
那么大家就会这样想,既然编译器会自动进行优化,那我们就放心用string就好了,其实也不尽然,因为编译器优化还是有限的,我们来看下面的例子(thinking in java 中的例子):
//: strings/WhitherStringBuilder.java
//使用string
public class WhitherStringBuilder {
public String implicit(String[] fields) {
String result = "";
for(int i = 0; i < fields.length; i++)
result += fields[i];
return result;
}
//使用stringBuffer
public String explicit(String[] fields) {
StringBuilder result = new StringBuilder();
for(int i = 0; i < fields.length; i++)
result.append(fields[i]);
return result.toString();
}
}
使用string的话,在for循环中,每一次都会创建一个StringBuilder对象,但是一开始就使用StringBuilder的话,全程只会有一个StringBuilder对象。关于这一点的验证,要涉及到汇编语言,在这里就不展开讨论了。所以,我们可以看到,编译器的优化能力还是有限的。