StringBuilder,String与StringBuffer 的异同

引言

根据我在网上查到的资料显示,这三者的区别主要是:
String:字符串常量
StringBuffer:字符创变量(多线程)
StringBuilder:字符创变量(单线程)
对String的操作表面上看是对同一个变量的操作,但实际上是新建了一个常量,然后修改对象的引用。基于这样的机制,需要不停的GC旧的对象,其效率也很低下。
而StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,当然速度快。

源码分析

上述是一般博客上都能查到的东西,但是我们还是要看一下源码的具体实现,比如,为啥一个是常量一个是变量。

成员变量

StringBuffer与StringBuilder类似,故只介绍string与StringBuilder的对比。

String:

/** 用来存放字符串的内容 */
    private final char value[];
/** 缓存本字符串的hash值 */
    private int hash; // 默认为0
/** 因为实现了Serializable接口,所以需要定义一个序列化ID */
    private static final long serialVersionUID = -6849794470754667710L;

StringBuilder:

/** 同上,序列化ID */
    static final long serialVersionUID = 4383685877147921099L;

因为StringBuilder继承的是AbstractStringBuilder,很多方法都是直接调用父类的方法,变量也是使用父类的变量。
AbstractStringBuilder:

/**用来存放字符串的内容.*/
    char[] value;
/** 这个变量用来统计已经使用的字符数,字符数组不一定所有的空间都被使用.*/
    int count;

对比很明显,String对象的字符数组直接被声明为final,一旦被赋值就不能再修改。所以String被称为常量。

构造函数

String:(过时的不介绍)

 /**
     创建一个空串,但是这个构造函数没有必要,因为数组不可变,一旦被定义,那就是一个空串,但这毫无意义。
     */
    public String() {
        this.value = "".value;
    }

    /**
    以字符串为参数构造,把该字符串的hash值及内容赋值给新的字符串。
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    /**
     把字符数组的内容赋值给新的字符串
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

    /**
     把字符数组的一部分赋给新的字符串
     */
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

    /**
    将存放unicode编码的数组赋值到String中去
     */
    public String(int[] codePoints, int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= codePoints.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > codePoints.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }

        final int end = offset + count;

        // Pass 1: Compute precise size of char[]
        int n = count;
        for (int i = offset; i < end; i++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                continue;
            else if (Character.isValidCodePoint(c))
                n++;
            else throw new IllegalArgumentException(Integer.toString(c));
        }

        // Pass 2: Allocate and fill in char[]
        final char[] v = new char[n];

        for (int i = offset, j = 0; i < end; i++, j++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                v[j] = (char)c;
            else
                Character.toSurrogates(c, v, j++);
        }

        this.value = v;
    }

值得一提的是,几乎所有的构造函数注释都提到一句,使用String的构造函数毫无意义。其实是因为如果使用String s = “ABC”,那么s是存放在字符串常量池中的,如果池中已经有字符串“ABC”,就不会新建一个字符串对象,而是会使用原来的对象。
但是,如果你使用String s1 = new String(s),那么,就会再java堆中重新开辟一篇内存区域用来存放这个s对象。最明显的区别莫过于s1 == s 的结果是false,毕竟起始地址不同,而且后者会浪费资源。

StringBuilder:

/**
     传入一个整数作为容量。
     */
    public StringBuilder(int capacity) {
        super(capacity);
    }

    /**
     传入一个字符串作为参数,把字符串的长度+16作为容量
     */
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

    /**
     传入字符序列作为参数,把字符序列的长度+16作为容量,然后调用第一个构造函数
     */
    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

以上都是调用父类的构造函数,而父类的构造函数只做了一件事情:

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

初始化一个长度为capacity的字符数组。

从构造函数来看,二者都是给字符数组赋值,只不过String一般是调用Arraycopy方法一步到位,而String Builder一般是先新建一个一定长度的数组,再调用append方法。

内容的修改

String

最常用的莫过于以下的情况:

String s1 = "PangYuQing";
String s2 = "Pang";
String s3 = "YuQing";
String s4 = "Pang" + "YuQing";
String s5 = s2 + s3;
String s5s = s5.intern();
String s6 = "Pang" + s3;
String s6s = s6.intern();
String s7 = new String("PangYuQing");
String s7s = s7.intern();

其实以上就两种情况,s4是一种,s5,s6是一种。s4经过jvm的优化,其实质就是String s4 = “PangYuQing”。后者,便转化为new StringBuilder.append(s2).append(s3)。然后再调用toString方法。因为需要额外生成一个String对象,速度自然变慢。
而s5s,s6s,s7s,官方API的解释是“当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串 (用equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。”

StringBuilder

一般使用append方法,以下选几个常用的介绍:

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

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

可以看到,其都是调用父类的append方法。
而父类的方法是:

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        //把str的内容拷贝到value里去
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

其中

    /**
     如果需要的容量大于当前数组的长度,则进行扩容
     */
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    /**
     扩容,如果栈溢出则把容积置为Integer.MAX_VALUE
     */
    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);
    }

值得注意的一点是,char[] value是父类私有的成员,所以StringBuilder中所有对内容的修改都是调用父类的方法类完成。

可以看到,在修改内容方面,String新建一个String对象或者往字符串常量池中添加常量,而StringBuiler是直接对原有的字符串数组扩容后添加字符。

小结

各位还是用StringBuilder吧。。。。

tips

发现一个有趣的事情

private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

原来这个方法是真往字符串后面加个null啊。。。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kevinjqy/article/details/72210972
文章标签: string stringbuilder
个人分类: Java学习笔记
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭