美文网首页
String、StringBuilder、StringBuffe

String、StringBuilder、StringBuffe

作者: jxiang112 | 来源:发表于2022-01-18 15:01 被阅读0次

String是字符串对象,它是不可变的,StringBuilder是可变字符串序列且是非同步的,StringBuffer是可变字符串序列且是同步的。

String

String是不可变字符串对象,重点是不可变,即每次对字符串的更改得到的都是新的字符串对象,看如下示例:

String s = "ab";
String ss1 = s;
s += "c";
String ss2 = s.replace("c", "");

System.out.println("s: " + s + "; ss1: " + (ss1) + "; ss2: " + ss2 + "; ss1 == ss2: " + (ss1 == ss2));
//结果:s: abc; ss1: ab; ss2: ab; ss1 == ss2: false

上面这段代码,对s不论是加还是替换操作,得到的都是新的字符串对象,所以字符串对象是不可变的。
String除了不可变特性值得关注之外,还有一个很重要的点需要关注:字符串常量和intent方法,虚拟机会将字符串常量放入常量池中,可以减少内存和创建对象的开销,我们来看看下面的示例:

        String s = "a" + "b";
        String s1 = "ab";
        String s2 = new String("ab");
        String i = "a";
        String ss = i + "b";

        String s3 = s.intern();
        String s4 = s2.intern();



        System.out.println("s == s1: " + (s == s1));
        System.out.println("s == s2: " + (s == s2));
        System.out.println("s == s3: " + (s == s3));
        System.out.println("s == s4: " + (s == s4));
        System.out.println("s2 == s4: " + (s2 == s4));
        System.out.println("s == ss: " + (s == ss));
        System.out.println("s2 == ss: " + (s2 == ss));

可以猜猜结果是什么?下面是执行结果:

s == s1: true
s == s2: false
s == s3: true
s == s4: true
s2 == s4: false
s == ss: false
s2 == ss: false

我们知道对象用==进行判断,是判断是否同一个对象,即否指向同一地址;equals判断是判断内容值是否相等。

  • s == s1为什么为true?按我们的正常理解,它们都是不同的对象,结果为什么是同一对象呢?这是因为编译时,将字符串常量放入本地常量池中,然后将常量池中的字符串地址赋值给字符串变量。s = "a" + "b" 字符串a和b都是字符串常量,编译时会把a+b得到的结果作为字符串常量放入常量池中,再把常量池中的ab地址赋值给s,s1 = "ab"这里因为ab已经在常量池中,所以直接把常量池中ab的地址赋值给s1,所以最终s和s1指向的是同一个地址。
  • s == s2 为什么是false? s我们已经知道指向的是常量池中的字符串ab的地址,而s2则是创建了一个String对象,并且值是字符串"ab",此时s 和 s2指向的不是同一地址
  • s == s3 为什么是true? 首先说明下intern方法是如果字符串String的值如果已经在常量池中,则返回常量池中字符串的地址;如果不在常量池中,则将String的字符串值存到常量池中并返回指向该字符串的地址。
  • s == s4 为什么是true? 原因与s == s3 相同
  • s2 == s4 为什么是false? 因为s4指向的是s2.intent()放入常量池的字符串,而s2本身并没有变化还是指向new String,所以这两个对象是不相等的
  • s == ss 为什么是false呢?因为ss = i + "b"经过编译之后,变成了 ss = new StringBuilder().append(i).append("b").toString(), StringBuilder的toString是new String创建一个新的字符串对象,而s 指向的是常量池中的“ab”字符串,所以这两个并不是同一对象
  • s2 == ss 为什么是false呢?因为ss = i + "b"经过编译之后,变成了 ss = new StringBuilder(i).append("b").toString(), StringBuilder的toString是new String创建一个新的字符串对象,而s2本事是一个new String的一个字符串对象,两种并不是同一对象

读者可以执行执行看下结果,我们借助ASM ByteCode Viewer插件(基于Android Studio开发工具)来看看编译之后的字节码分析是不是如上面我分析的一样:

public static testString()V
   L0  //函数内行号
    LINENUMBER 45 L0 //源码中的行号
    LDC "ab" //将"a" + "b"合并为“ab”之后,放入本地常量池中(LDC 是local constant的缩写),将常量池“ab”的引用入栈顶
    ASTORE 0  //将栈顶即“ab”所在常量池的引用赋值给第1个局部变量即s
   L1 //函数内行号
    LINENUMBER 46 L1  //源码中的行号
    LDC "ab" //将常量池“ab”的引用入栈顶
    ASTORE 1 //将栈顶即“ab”所在常量池的引用赋值给第2个局部变量即s1
   L2 //函数内行号
    LINENUMBER 47 L2 //源码中的行号
    NEW java/lang/String //创建String对象,并将其引用值压栈顶
    DUP //复制栈顶值并将复制值压入栈顶
    LDC "ab" //将常量池“ab”的引用入栈顶
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V //出栈顶常量池“ab”的引用作为init参数,再出栈顶即new String的引用调用init方法
    ASTORE 2 //出栈顶即new String的引用,并将其引用赋值给第3个局部变量即s2,这就是new String之后为什么要DUP复制引用并压入栈顶,因为要使用两次引用,一次使用引用调用init方法,一次赋值给变量
   L3 //函数内行号
    LINENUMBER 48 L3 //源码中的行号
    LDC "a"  //将常量池“a”的引用入栈顶
    ASTORE 3 //将栈顶即“a”所在常量池的引用赋值给第4个局部变量即i
   L4 //函数内行号
    LINENUMBER 49 L4 //源码中的行号
    NEW java/lang/StringBuilder //创建String对象,并将其引用值压栈顶
    DUP  //复制栈顶值并将复制值压入栈顶
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V //出栈顶即StringBuilder的引用,调用其init方法
    ALOAD 3 //加载第4个局部变量即i进入栈
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; //调用StringBuilder的append方法添加i的值
    LDC "b" //加载常量池“b”入栈
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; //调用StringBuilder的append方法添加常量池中的字符串“b”
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; //调用StringBuilder的toString方法,将StringBuilder转为String对象
    ASTORE 4 //将StringBuuilder的toString的值赋值给第5个局部变量即ss
   L5 //函数内行号
    LINENUMBER 51 L5 //源码中的行号
    ALOAD 0 //加载第1个局部变量即s入栈
    INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String; //调用s的intent方法,返回s的字符串所在常量池中的引用并入栈
    ASTORE 5 //将栈顶的引用复制给第6个局部变量即s3
   L6 //函数内行号 
    LINENUMBER 52 L6  //源码中的行号
    ALOAD 2 //加载第3个局部变量即s2入栈
    INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String; //调用s2的intent方法,返回s2的字符串所在常量池中的引用并入栈
    ASTORE 6 //将栈顶的引用复制给第7个局部变量即s4

StringBuilder 与 StringBuffer

StringBuilder 与 StringBuffer均是AbstractStringBuilder的子类,区别在于StringBuilder 是非线程安全的,而StringBuffer是线程安全的,因为StringBuffer的操作相关的方法均加了synchronized同步锁,而StringBuilder没有:
下面是StringBuffer的一些常见方法:

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

@Override
    public synchronized char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }

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

下面是StringBuilder常见的方法:

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

可见StringBuilder没有加同步锁,而StringBuffer加了同步锁,它们的方法实现实际均有父类AbstractStringBuilder实现,而AbstractStringBuilder内部有一个char[] value;表示字符串的值,
int count;是字符串的实际长度,它们默认的容量都是16:

public StringBuilder() {
        super(16);
    }

public StringBuffer() {
        super(16);
    }

常用的就是append方法,及时就是将字符串值加入char[] value中,加入的同时会进行容量的扩容,每次扩容规则:默认扩容是原容量的2倍+2,如果默认扩容小于实际字符串长度大小,则使用实际字符串长度大小:

public AbstractStringBuilder append(String str) {
        if (str == null)
            //如果字符串为空对象,那么将添加的是"null"字符串
            return appendNull();
        int len = str.length(); // 要添加的字符串长度
        ensureCapacityInternal(count + len); //count+len代表新的字符串长度,如有value的容量小于新的字符串长度则进行扩容
        str.getChars(0, len, value, count);//将str字符串值拷贝到value中
        count += len; //新的字符串长度
        return this;
    }

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            //如果新的字符串长度大于value的容量
            //按规则计算新的容量:默认扩容是原容量的2倍+2,如果默认扩容小于实际字符串长度大小,则使用实际字符串长度大小
            //接着使用新新的容量创建一个新的char数组,并将原数组拷贝到新数组中,并把新数组赋值给value
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
private int newCapacity(int minCapacity) {
        // overflow-conscious code
        //默认扩容:原来的一倍 + 2
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            //默认扩容比新的容量消息,则使用新的容量
            newCapacity = minCapacity;
        }
        //新的容量如果超过允许的的最大值(Integer.MAX_VALUE - 8),则使用运行的最大值,否则是有新的容量
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            //超出int的最大值,抛出异常
            throw new OutOfMemoryError();
        }
        //新的容量如果超过允许的的最大值(Integer.MAX_VALUE - 8),则使用运行的最大值,否则是有新的容量
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//Array
public static long[] copyOf(long[] original, int newLength) {
        //用新的容量创建新的数组
        long[] copy = new long[newLength];
        //将原数组original赋值到新的数组中
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        //返回新数组
        return copy;
    }

StringBuilder的toString

@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

相关文章

网友评论

      本文标题:String、StringBuilder、StringBuffe

      本文链接:https://www.haomeiwen.com/subject/ltlacrtx.html