String、StringBuilder、StringBuffer

本文深入剖析了Java中的String、StringBuilder及StringBuffer的区别与联系,详细解释了String常量池的工作原理,探讨了final关键字的使用,并对比了StringBuilder与StringBuffer的性能特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

String、StringBuilder、StringBuffer

String

String是只读字符串,String引用的字符串内容是不可更改的。当我们通过不同的方式创建一个String对象,这个对象就唯一确定了。

String str1 = “hello”;
String str2 = “hello”;
String str3 = new String(“hello”);
String str4 = new String(str1);

这里str1、str2是同一个对象,所以str1==str2的结果为true;
str1、str3、str4是不同的对象,但是他们指向String Pool中同一个字符串(一时之间也没有找到,通过怎样的标识来确定str1、str3、str4指向的是String Pool中的同一字符串,以后知道了,再来补充。ps:我看到网上有人说,通过hashCode()方法确定它们只想的是同一个对象,看了下String.hashCode()的源码,对这种说法持怀疑态度。)

str1在String Pool中,str2、str4在Java Heap中。

注:
1. Java运行时会维护一个String Pool, JavaDoc翻译为“字符串缓冲区”, String Pool用来存放运行中产生的各种字符串,并且Pool中的字符串内容不重复。创建的String对象放在Java堆中,对象指向String Pool中的某个字符串。
2. Java对象的创建:

  • String s = X, Java运行是会拿这个X在String Pool中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串s,否则,不在池中添加。
  • String s = new String(X), 对这种方式,Java一定会(在堆区)创建一个String对象。

String.hashCode()源码如下:

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

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

}

从上面源码可以看出,hashCode只是根据该String对象的值,对其每一位进行运算,最后得出的一个int值而已,并没有表示出它在内存中的唯一标识。

因为str1、str2、str3、str4它们的值(value)都是“hello”,因此它们的hashCode相同。

str1、str3、str4它们的值相同,所以用equals()方法比较时,都为true;

String str5 = new String(“hello”).intern();

简单的说,intern方法会先从String Pool中查找是否存在“hello”的字符串常量,如果有,则返回常量池中的这个字符串;如果没有,则把其添加到String Pool中,并返回此String对象的引用。ps:可能说的不是很清晰,直接贴上jdk里的注释(注:intern()方法是native method)

因此str1==str5为true,因为它们是同一个对象。
(好吧,水平、文笔有限,也不知道描述清楚了没有,若有误,欢迎指正)

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

测试代码:(还有很多case没有写进去)

    public static void testString() {
        String str1 = "hello";
        System.out.println("str1 hashCode: " + str1.hashCode());

        String str2 = "hello";
        System.out.println("str2 hashCode: " + str2.hashCode());

        String str3 = new String("hello");
        System.out.println("str3 hashCode: " + str3.hashCode());

        String str4 = new String(str1);
        System.out.println("str4 hashCode: " + str4.hashCode());

        String str5 = new String("hello").intern();

        System.out.println("str1 == str2 : " + (str1 == str2));
        System.out.println("str1 == str3 : " + (str1 == str3));
        System.out.println("str1 == str4 : " + (str1 == str4));
        System.out.println("str1 == str5 : " + (str1 == str5));

        System.out.println("str1.equals(str2) : " + str1.equals(str2));
        System.out.println("str1.equals(str3) : " + str1.equals(str3));
        System.out.println("str1.equals(str4) : " + str1.equals(str4));
        System.out.println("str1.equals(str5) : " + str1.equals(str5));

        System.out.println("\nstr1 = \"world\".....");
        str1 = "world";
        System.out.println("str1 hashCode: " + str1.hashCode());

        String str6 = "world";
        System.out.println("str1 == str5 : " + (str1 == str6));
        System.out.println("str1.equals(str5) : " + str1.equals(str6));
        System.out.println("str1 = " + str1);
        System.out.println("str6 = " + str6);
        System.out.println("str2 = " + str2);
        System.out.println("str3 = " + str3);
        System.out.println("str4 = " + str4);
        System.out.println("str5 = " + str5);
    }

运行结果:

str1 hashCode: 99162322
str2 hashCode: 99162322
str3 hashCode: 99162322
str4 hashCode: 99162322
str1 == str2 : true
str1 == str3 : false
str1 == str4 : false
str1 == str5 : true
str1.equals(str2) : true
str1.equals(str3) : true
str1.equals(str4) : true
str1.equals(str5) : true

str1 = "world".....
str1 hashCode: 113318802
str1 == str6 : true
str1.equals(str5) : true
str1 = world
str6 = world
str2 = hello
str3 = hello
str4 = hello
str5 = hello

intern方法是一个native方法,

附 String类的equal()方法源码:(顺便带几个方法的源码吧)

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    ......
    ......

    /**
     * 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 the length of this string.
     * The length is equal to the number of <a href="Character.html#unicode">Unicode
     * code units</a> in the string.
     *
     * @return  the length of the sequence of characters represented by this
     *          object.
     */
    public int length() {
        return value.length;
    }

    /**
     * Returns {@code true} if, and only if, {@link #length()} is {@code 0}.
     *
     * @return {@code true} if {@link #length()} is {@code 0}, otherwise
     * {@code false}
     *
     * @since 1.6
     */
    public boolean isEmpty() {
        return value.length == 0;
    }

    /**
     * Returns the {@code char} value at the
     * specified index. An index ranges from {@code 0} to
     * {@code length() - 1}. The first {@code char} value of the sequence
     * is at index {@code 0}, the next at index {@code 1},
     * and so on, as for array indexing.
     *
     * <p>If the {@code char} value specified by the index is a
     * <a href="Character.html#unicode">surrogate</a>, the surrogate
     * value is returned.
     *
     * @param      index   the index of the {@code char} value.
     * @return     the {@code char} value at the specified index of this string.
     *             The first {@code char} value is at index {@code 0}.
     * @exception  IndexOutOfBoundsException  if the {@code index}
     *             argument is negative or not less than the length of this
     *             string.
     */
    public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }

从源码可以看出,其实String字符串本质上是一个char[]数组,将char[] value 封装成了一个对象,并给它一些操作method,方便我们直接使用。

补充一个纠结了半个多小时的问题

先上一段“诡异”的代码

public class Test {
    public static void main(String[] args)  {
        String a = "hello2"; 
        final String b = "hello";
        String d = "hello";
        String c = b + 2; 
        String e = d + 2;
        String f = "hello" + 2;
        System.out.println((a == c));
        System.out.println((a == e));
        System.out.println((a == f));
    }
}

这段“诡异”的代码的运行结果:

true
false
true

想必很多人都不能够理解吧(如果你知道原因,那么可以不必往下看了,下面的你肯定都知道。)

首先,fianl关键字修饰变量(修饰类和方法就不说了)。当final作用于类的成员变量(局部变量只要保证在使用之前被初始化赋值即可)时,必须在定义时或者构造器中进行初始化赋值,而final变量一旦被初始化赋值之后,就不能再次被赋值了。OK,这是解决final修饰类成员变量的。
对于上面 a==c 和 a==e,它们的区别就是 c=b+2, b是final变量,e=d+2, d是普通变量。当final变量是基本数据类型以及String类型是,如果编译期间就能知道它的确切值,则编译期会把它当做编译期常量使用。也就是说在用到该final的地方,相当于直接访问常量,就如同:String f = “hello” + 2 一样,不需要在运行时确定。而对于e=d+2,对变量d的访问需要在运行时通过链接来完成、进行。(注:只有在编译期能确切知道final变量值得情况下,编译器才做进行这样的优化)。
这是其中的一个点。

其次,对于String c = d + 2, 这段代码 <=====> String c = “hello” + 2; 这句话在编译期就能够确切的得出 c的值。(注:使用只包含常量的字符串连接符,如:String str = “aa” + “bb”, 创建的也是常量,编译期就能够确定,并且放在String Pool中)。

总结:就是上面那段代码,在编译结束后,String Pool中会创建三个字符串常量,分别是: “hello”, “2”, “hello2”,能够确定 a, b, c, d, f的值。 e只有在运行时,才会知道,String e = d + 2, 在运行时,会被编译器,优化为:String e = new StringBuilder(d).append(2).toString(); StringBuilder的toString()方法里面做的 是 return new String(), 所以该句最终是通过new String()生成,通过前面的分析,得知,通过new一定会在堆中生成一个对象。

附:

String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true

总结:

  • 单独使用”“引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
  • 使用new String(“”)创建的对象会存储到Java堆中,是运行期新创建的;
  • 使用包含常量的字符串连接符,如“aa” + “bb”创建的也是常量,编译期就能确定,已经确定存储到String Pool中;
  • 使用包含变量的字符串连接符,如:“aa” + s1 创建的对象是运行期才创建的, 存储在Java堆中

参考:

浅析Java中的final关键字

Java中的String类常量池详解

StringBuilder & StringBuffer

StringBuilder 和 StringBuffer都是可变长度的字符串,字符串的值可以直接修改,并且还是原来的对象。
StringBuilder是Java 5引入的(StringBuffer从jdk1.0里就存在了),和StringBuffer的方法完全相同,区别仅仅是:StringBuilder是线程不安全的,因为StringBuilder的所有操作方法都没有被synchronized修饰,这样就: 非线程安全,但是StringBuiler的效率比StringBuffer要高。

下面就简单的比较一下两者;
StringBuffer

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

StringBuilder:

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

StringBuilder、StringBuffer都实现了接口:java.io.Serializable, CharSequence, 同样的继承于 AbstractStringBuilder,
并且它们的构造方法基本类似。它们都是final类(String也是final类),所以它们是不可以被继承的。

它们的不同就是在一些操作method上(比如append()方法,StringBuffer有synchronize修饰。)

StringBuffer:

@Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > value.length) {
            expandCapacity(minimumCapacity);
        }
    }

StringBuilder没有重写这些方法,而是直接用的父类的,父类的这些方法是没有synchronize修饰的。

StringBuffer:

@Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

    /**
     * Appends the specified {@code StringBuffer} to this sequence.
     * <p>
     * The characters of the {@code StringBuffer} argument are appended,
     * in order, to the contents of this {@code StringBuffer}, increasing the
     * length of this {@code StringBuffer} by the length of the argument.
     * If {@code sb} is {@code null}, then the four characters
     * {@code "null"} are appended to this {@code StringBuffer}.
     * <p>
     * Let <i>n</i> be the length of the old character sequence, the one
     * contained in the {@code StringBuffer} just prior to execution of the
     * {@code append} method. Then the character at index <i>k</i> in
     * the new character sequence is equal to the character at index <i>k</i>
     * in the old character sequence, if <i>k</i> is less than <i>n</i>;
     * otherwise, it is equal to the character at index <i>k-n</i> in the
     * argument {@code sb}.
     * <p>
     * This method synchronizes on {@code this}, the destination
     * object, but does not synchronize on the source ({@code sb}).
     *
     * @param   sb   the {@code StringBuffer} to append.
     * @return  a reference to this object.
     * @since 1.4
     */
    public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }

    /**
     * @since 1.8
     */
    @Override
    synchronized StringBuffer append(AbstractStringBuilder asb) {
        toStringCache = null;
        super.append(asb);
        return this;
    }

    /**
     * Appends the specified {@code CharSequence} to this
     * sequence.
     * <p>
     * The characters of the {@code CharSequence} argument are appended,
     * in order, increasing the length of this sequence by the length of the
     * argument.
     *
     * <p>The result of this method is exactly the same as if it were an
     * invocation of this.append(s, 0, s.length());
     *
     * <p>This method synchronizes on {@code this}, the destination
     * object, but does not synchronize on the source ({@code s}).
     *
     * <p>If {@code s} is {@code null}, then the four characters
     * {@code "null"} are appended.
     *
     * @param   s the {@code CharSequence} to append.
     * @return  a reference to this object.
     * @since 1.5
     */
    @Override
    public synchronized StringBuffer append(CharSequence s) {
        toStringCache = null;
        super.append(s);
        return this;
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @since      1.5
     */
    @Override
    public synchronized StringBuffer append(CharSequence s, int start, int end)
    {
        toStringCache = null;
        super.append(s, start, end);
        return this;
    }

    @Override
    public synchronized StringBuffer append(char[] str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public synchronized StringBuffer append(char[] str, int offset, int len) {
        toStringCache = null;
        super.append(str, offset, len);
        return this;
    }

    @Override
    public synchronized StringBuffer append(boolean b) {
        toStringCache = null;
        super.append(b);
        return this;
    }

    @Override
    public synchronized StringBuffer append(char c) {
        toStringCache = null;
        super.append(c);
        return this;
    }

    @Override
    public synchronized StringBuffer append(int i) {
        toStringCache = null;
        super.append(i);
        return this;
    }
    .......

StringBuilder:

@Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }

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

    /**
     * Appends the specified {@code StringBuffer} to this sequence.
     * <p>
     * The characters of the {@code StringBuffer} argument are appended,
     * in order, to this sequence, increasing the
     * length of this sequence by the length of the argument.
     * If {@code sb} is {@code null}, then the four characters
     * {@code "null"} are appended to this sequence.
     * <p>
     * Let <i>n</i> be the length of this character sequence just prior to
     * execution of the {@code append} method. Then the character at index
     * <i>k</i> in the new character sequence is equal to the character at
     * index <i>k</i> in the old character sequence, if <i>k</i> is less than
     * <i>n</i>; otherwise, it is equal to the character at index <i>k-n</i>
     * in the argument {@code sb}.
     *
     * @param   sb   the {@code StringBuffer} to append.
     * @return  a reference to this object.
     */
    public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
    }

    @Override
    public StringBuilder append(CharSequence s) {
        super.append(s);
        return this;
    }

    /**
     * @throws     IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder append(CharSequence s, int start, int end) {
        super.append(s, start, end);
        return this;
    }

    @Override
    public StringBuilder append(char[] str) {
        super.append(str);
        return this;
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder append(char[] str, int offset, int len) {
        super.append(str, offset, len);
        return this;
    }

    @Override
    public StringBuilder append(boolean b) {
        super.append(b);
        return this;
    }

    @Override
    public StringBuilder append(char c) {
        super.append(c);
        return this;
    }

    @Override
    public StringBuilder append(int i) {
        super.append(i);
        return this;
    }

    @Override
    public StringBuilder append(long lng) {
        super.append(lng);
        return this;
    }

    @Override
    public StringBuilder append(float f) {
        super.append(f);
        return this;
    }

    @Override
    public StringBuilder append(double d) {
        super.append(d);
        return this;
    }

    .......

总而言之,StringBuilder和StringBuffer的区别就是,StringBuilder是线程不安全的,StringBuffer是线程安全的。而,具体的实现就是,StringBuffer所有的method都没有synchronize修饰。虽然这样做,线程不安全,但是它的效率比StringBuilder高。在单线程的场景下,果断使用StringBuilder。

注:让我想起了 HashMap 和 HashTable ,这两者也是一个线程安全,一个线程不安全。(待会儿去看看源码。看看这两者是如何实现的。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值