String、StringBuffer、StringBuilder

1、String

String的部分源码(jdk1.8.0_65)如下:

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;



public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

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;
}

从以上看出:

  1. String类是final类,所以方法默认都是final方法,且成员变量都被final修饰,所以String类不可更改(value不能指向其他字符数组)。final类意味着不可继承,在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率,但是,此外还有常量优化、多线程安全
  2. String类是通过字符数组来实现的,一个String对象包含一个字符数组
  3. 例如substring、replace等对字符串的操作会返回一个新的String对象,并不是原String对象,原String对象未改变

1.1 常量优化

JVM为了提高性能和减少内存开销,在实例化字符串常量(字面量)的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个全局性的字符串常量池(String Pool,与类的Runtime Constant Pool有别,字符串常量池中里面存放的是引用),当通过字面量来创建(发生在加载阶段,不是运行时)字符串时,JVM通过字符串常量池,看能否查找到具有该字面量的字符串对象,不能则创建对象(放在运行时常量池中)并将其引用放入字符串常量池中,能则返回字符串常量池中的引用。

常量优化的前提是String对象是不可改变的,这样确保多个引用指向同一个对象,不论对String对象怎么处理,都不会改变对象。这里只是说了下原理,具体底层的实现细节请移步R大的博文(113楼)

来看几个例子,注意==比较是引用,并不是对象本身。

package offer;
/**
* @author 小锅巴
*/
public class StringTest {

    public static void main(String[] args){
        String str1 = "hello world";
        String str2 = new String("hello world");
        String str3 = "hello world";
        String str4 = new String("hello world");         
        System.out.println(str1==str2);
        System.out.println(str1==str3);
        System.out.println(str2==str4);
        System.out.println();

        String str6 = "ab";
        String str7 = "b";
        final String str8 = "b"; 
        String str9 = "a"+"b";
        String str10 = "a" + str7; 
        String str11 = "a" + str8;
        System.out.println((str6 == str9)); 
        System.out.println((str6 == str10));
        System.out.println((str6 == str11));
        System.out.println();

        String str5="abab"; 
        System.out.println(("abab" == str5)); 
        }
}
/**
输出:
false
true
false

true
false
true

true
*/

“hello world”实例实际上只有一份,所以str1和str3指向了同一对象;而str2和str4是通过new运行时创建的,那么不论字符串常量池中有没有相同内容的字符串,都要在堆上创建对象,显然是不同的对象,尽管对象的内容相同,str1和str2也就显然是不同的了。

1.2 编译优化

还是上面的例子,str9和str6是同一个对象。编译器将”a”+”b”作为常量表达式,直接取结果为”ab”,后面就和常量优化一样了。

将这两条语句单独拿出来,通过javap反编译查看字节码,可以看出,str9直接指向了”ab”

package offer;
/**
* @author 小锅巴
*/
public class test {

    public static void main(String[] args) {
        String str6 = "ab";
        String str9 = "a"+"b";
        System.out.println((str6 == str9));
    }
}

然后再看str10和str11,区别在于表达式中的变量是否是final变量,这里因为str8是final变量(引用),因为一旦赋值之后就不允许修改,所以在编译时编译器就已经可以确定str8所指向对象的内容了,直接视为”b”,而str7则不能确定,它所指向的对象可能会改变。还是单独拿出来,反编译验证一下,看不懂字节码也没关系,通过给出的注释也能了解个大概

package offer;
/**
 * @author 小锅巴
 */
public class test {

    public static void main(String[] args) {
        String str6 = "ab";
        String str7 = "b";
        final String str8 = "b"; 
        String str10 = "a" + str7; 
        String str11 = "a" + str8;
        System.out.println((str6 == str10));
        System.out.println((str6 == str11));
    }
}

反编译后:

D:\Develop\Java\jdk1.8.0_65\bin>javap -c test
警告: 二进制文件test包含offer.test
Compiled from "test.java"
public class offer.test {
public offer.test();
    Code:
       0: aload_0
   1: invokespecial #8                  // Method java/lang/Object."<init>":
()V
       4: return

public static void main(java.lang.String[]);
    Code:
       0: ldc           #16                 // String ab
       2: astore_1
       3: ldc           #18                 // String b
       5: astore_2
       6: ldc           #18                 // String b
       8: astore_3
       9: new           #20                 // class java/lang/StringBuilder
      12: dup
      13: ldc           #22                 // String a
      15: invokespecial #24                 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
      18: aload_2
      19: invokevirtual #27                 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #31                 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      25: astore        4
      27: ldc           #16                 // String ab这里就是执行"a" + str8
      29: astore        5
      31: getstatic     #35                 // Field java/lang/System.out:Ljava/
io/PrintStream;
      34: aload_1
      35: aload         4
      37: if_acmpne     44
      40: iconst_1
      41: goto          45
      44: iconst_0
      45: invokevirtual #41                 // Method java/io/PrintStream.printl
n:(Z)V
      48: getstatic     #35                 // Field java/lang/System.out:Ljava/
io/PrintStream;
      51: aload_1
      52: aload         5
      54: if_acmpne     61
      57: iconst_1
      58: goto          62
      61: iconst_0
      62: invokevirtual #41                 // Method java/io/PrintStream.printl
n:(Z)V
      65: return
}

D:\Develop\Java\jdk1.8.0_65\bin>

最后一个,初看可能觉得有点奇怪,一个是引用,一个是字面量,能比么?由于String是比较特殊的类,这里编译器会直接把引用的地方替换为对象的内容,还是单独拿出来,通过反编译来查看,为了便于查看字节码指令,稍微改了一下

package offer;
/**
 * @author 小锅巴
 */
public class test {

    public static void main(String[] args) {
        String str5="abab"; 
        boolean b = ("abab" == str5);
        System.out.println(b);
    }
}

除此之外,String的编译优化的常量表达式不仅限于字符串类型,基本数据类型都可以,比如:

package offer;
/**
 * @author 小锅巴
 */
public class test {

    public static void main(String[] args) {
        String a = "a1"; 
        String b = "a" + 1; 
        System.out.println((a == b));
    }
}

输出结果为true

对于String str2 = new String("hello world");“创建了”几个对象,这么说吧,涉及两个String对象,一个其引用在字符串常量池中,另一个是new创建出来的,前者是在类加载时创建的,后者是在运行时创建的。想了解更多请移步R大的博文

在查找Sring的资料时,发现java8(hotspot)中已经没有“永久代”了,取而代之的是“元空间”,可参考这里。而java8之前是用“永久代”实现方法区,运行时常量池是方法区的一部分。

2、StringBuffer StringBuilder

因为String对象是不可变的,每次操作都是创建一个新的String对象,浪费资源,也导致性能过低,StringBuilder不是线程安全的,StringBuffer是线程安全的。

2.1 重载+和StringBuilder

+是java里唯一的一个重载操作符,当String对象通过+和基本类型或字符串类型“相加时”,编译器会创建StringBuilder对象并调用StringBuilder的append方法来连接。比如:

package offer;
/**
* @author 小锅巴
*/
public class test {

    public static void main(String[] args) {
        String  str = "";
        str = str + "hello";
        System.out.println(str);
    }
}

AbstractStringBuilder

StringBuffer和StringBuilder都继承自AbstractStringBuilder类,这里只看下append方法的实现,以此为例来说明StringBuilder、StringBuffer的实现原理

AbstractStringBuilder部分源码:

abstract class AbstractStringBuilder implements Appendable, CharSequence    {
    char[] value;
    int count;

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

    public void getChars(int srcBegin, int srcEnd, char[] dst, int  dstBegin)
    {
        if (srcBegin < 0)
            throw new StringIndexOutOfBoundsException(srcBegin);
        if ((srcEnd < 0) || (srcEnd > count))
            throw new StringIndexOutOfBoundsException(srcEnd);
        if (srcBegin > srcEnd)
            throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

    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);
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return appendNull();
        int len = sb.length();
        ensureCapacityInternal(count + len);
        sb.getChars(0, len, value, count);
        count += len;
        return this;
    }
}

这里用到了Arrays.copyOf()方法,关键源码如下,可以看出就是新建了一个字符数组:

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;
}

System.arraycopy()的源码:

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

StringBuilder部分源码:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    static final long serialVersionUID = 4383685877147921099L;

    public StringBuilder() {
        super(16);
    }

    public StringBuilder(int capacity) {
        super(capacity);
    }

    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

    @Override
    public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
    }

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}   

StringBuffer和StringBuilder类似,区别在于方法都被Synchronized修饰,多个了被transient修饰的字段

append的实现最终还是要看AbstractStringBuilder,可以发现是使用可变长数组实现的(和用数组实现可扩容的栈思想类似),利用了System.arraycopy()实现数组的复制,该方法是个本地方法。

参考资料:

  1. http://www.cnblogs.com/dolphin0520/p/3778589.html
  2. http://rednaxelafx.iteye.com/blog/774673/
  3. http://droidyue.com/blog/2014/12/21/string-literal-pool-in-java/index.html
  4. http://www.cnblogs.com/fancydeepin/archive/2013/04/23/min-snail-speak_String-StringBuffer-StringBuilder.html
  5. http://www.cnblogs.com/fancydeepin/archive/2013/04/22/min-snail-speak_String.html
  6. http://sabaao.blogspot.my/2013/10/java-stringstringbufferstringbuilder.html
  7. http://www.infoq.com/cn/articles/Java-PERMGEN-Removed
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值