【Java基础】String、StringBuilder、StringBuffer

String、StringBuilder、StringBuffer是经常会被拿来比较的三个类,本文主要研究三者之间的区别。

本文所用jdk为jdk1.8.0_151。

一、String源码

String的源码和注释已经列举了很多用法及原因,所以开头先贴出部分源码及注释,一些常用方法如charAt等本文暂不讨论。

/**
 * The {@code String} class represents character strings. All
 * string literals in Java programs, such as {@code "abc"}, are
 * implemented as instances of this class.
 * <p>
 * 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. For example:
 * <blockquote><pre>
 *     String str = "abc";
 * </pre></blockquote><p>
 * is equivalent to:
 * <blockquote><pre>
 *     char data[] = {'a', 'b', 'c'};
 *     String str = new String(data);
 * </pre></blockquote><p>
 * Here are some more examples of how strings can be used:
 * <blockquote><pre>
 *     System.out.println("abc");
 *     String cde = "cde";
 *     System.out.println("abc" + cde);
 *     String c = "abc".substring(2,3);
 *     String d = cde.substring(1, 2);
 * </pre></blockquote>
 * <p>
 *
 *还有很多没有贴出来
*/




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


    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }


    /**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     *
     * @param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(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;
    }



    /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

    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的,该类不可继承

2、类前的注释指出,String str = "abc"等效于String str = new String(data);其中data为{'a', 'b', 'c'}。查看该构造方法,就是将char数组拷贝到类中的value数组中,value数组为final的。也就是说,String实例化一个对象后,对象中存储的char数组就不能再改变了。这就是为什么说String类对象是不可变的原因。

3、由于String实例化后不可变,String中一些返回String的方法,并不是修改value后再返回,而是重新new一个String再返回,见replace、concat等方法的源码,最后返回时都是调用的new String()

4、String重写了equals方法,比较的两个字符串对象中存储的value数组的每个字符是否相等,内容相等而不是地址相等。

5、String重写了hashcode方法,hash值和value数组中的每个字符交织在了一起。

二、字符串常量池

字符串常量池是讨论String绕不开的话题。日常开发中打交道最多的就是字符串,而很多时候前后操作的字符串是重复的,如果每次都为字符串重新分配空间,势必造成性能下降和浪费内存。JVM使用字符串常量池来解决这个问题。JDK8中字符串常量池在堆中。

同一个字符串常量在常量池中只会存在一份。下面看个例子:

	public String stringTest() {
		String s1 = "hello";
		String s2 = "hello";
		String s3 = new String("hello");

		System.out.println(s1 == s2);
		System.out.println(s1 == s3);

		return s2;
	}

output:
true
false

eclipse debug看下

编译的时候"hello"已经被存在了class文件中的常量池,运行时又被jvm加载到运行时常量区(只存在一个“hello”)。s1指向了常量区的"hello",s2发现已经存在一个常量"hello",所以并没有重新再分配一个"hello",而是指向了和s1相同的同一对象,上图可以看到,s1和s2的id都是19。而s3是调用new来生成对象,new出的对象存在于堆中,所以s3和s1肯定指向的不是同一个对象,如上图,s3的id为24。尽管s3指向是堆上的空间,但是前面讲过,String对象最终都是存储在char数组value中的,s1、s2、s3中的value其实都是指向了常量区"hello"中的value,如上图所示三者的value的id都是25。

经过前面的分析,相信大家也就理解了三者之间的关系,也不难理解为什么两个输出一个是true,一个是false了。

三、字符串连接

String实例化后是不可变的,那么字符串连接(+)背后是怎么实现的呢?看个例子

	public String stringTest() {
		String s1 = "HelloString";
		String s2 = "Hello" + "String";
		String s3 = "Hello" + getTestString();
		final String s4 = "String";
		String s5 = "Hello" + s4;
		String s6 = s1 + s2;
		String s7 = "He" + "llo" + s2;
		String s8 = s2 + "He" + "llo";
		s2 += "he" + "llo";

		return s2;
	}

反编译如下:
	
	public String stringTest() {
		String s1 = "HelloString";
		String s2 = "HelloString";
		(new StringBuilder("Hello")).append(this.getTestString()).toString();
		String s4 = "String";
		String s5 = "HelloString";
		(new StringBuilder(String.valueOf(s1))).append(s2).toString();
		(new StringBuilder("Hello")).append(s2).toString();
		String s8 = s2 + "He" + "llo";
		s2 = s2 + "hello";

		return s2;
	}

我们看s2,"Hello" + "String"会被直接优化为"HelloString",两个字符串常量相加,在编译时会被优化为拼接后的字符串。

s3为一个常量+一个函数返回值,在编译时是不知道返回值是多少的,所以无法优化,而是调用了StringBuilder的append来生成可变字符串,最终再通过toString转化成String类型。s6和s3类似。s5则不同于s3,是因为s4是final修饰的,编译期间可确定其值就是"String",所以被优化为拼接后的字符串。而s7会先将可以拼接的常量拼接起来之后,再调用StringBuilder的append,其实这也是最大程度上的优化。s8和s7相比就是位置变化了下,但是可以看到,效果完全不同,先碰到不是常量的s2,后面的字符串拼接就不优化了。下面的s2自加又不一样了,常量拼接还是会被优化。

总结起来,就是编译期间可以确定的常量会被拼接在一起,不能确定的则调用StringBuilder去生成

如果是多次拼接呢?

public String stringTest() {
		String s1 = "Hello";
		for (int i = 1; i < 10; i++) {
			s1 += "String";
		}
		
		return s1;
	}

javap反编译如下:

可以看到,每次循环都会new StringBuilder,append之后再调用toString,效率很低下,这种情况就可以采用StringBuilder,用append拼接完之后再调用toString,效率会有很大提升。

四、StringBuilder、StringBuffer

StringBuilder部分源码如下:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;

    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }
    
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

}

继承的AbstractStringBuilder部分源码如下:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
}

StringBuilder也是final修饰的,即不可继承。

字符串存储在AbstractStringBuilder中定义的可变数组value中,默认的初始容量为16

StringBuffer和StringBuilder基本相同,区别就是StringBuffer是线程安全的,方法前有synchronized修饰。源码在此就不贴出来了。

五、三者之间的区别

前面对三者分别进行了介绍,对三者的区别和联系做个总结:

1、三者都是不可继承的。

2、String对象存储的字符串不可变,而StringBuilder、StringBuffer可变,因此对String对象的拼接操作实际上是转换成StringBuilder实现的。

3、StringBuilder不是线程安全的,因此多线程情况下要用线程安全的StringBuffer。由于StringBuffer有锁的操作,所以单线程情况下StringBuffer的性能是不如StringBuilder的。

4、如果是字符串常量直接相加,编译期会进行优化,所以用String效率更高。如果有大量的非常量的字符串拼接,用String的话,会进行频繁的String和StringBuilder之间的转换,效率不如直接用StringBuilder进行append操作。所以不同场景下需考虑使用哪种类型效率更高。

以上,如有错误,还望指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值