一篇不一样的StringBuffer和StringBuilder

来源:互联网 发布:网络十大恐怖图片 编辑:程序博客网 时间:2024/05/22 08:07

StringBuffer和StringBuilder相信大家都不陌生,我与它们之间也有过一段不愉快的经历。在一次面试过程中,面试官先是问了我的兴趣爱好,虽然我平时喜欢看书,音乐,踢球,跑步等等。但是那段时间为了面试没日没夜的看书,刷资料。当他问到我兴趣爱好时,脑子一片空白、答曰:“看书”。然后迎来的却是面试官的一阵嘲笑和质疑,我当时就已经很不舒服了,喜欢看书很奇怪吗?我本来就是一个转行生,不看书不学习,我拿什么支撑这一份工作?更何况我有过一次考研失败的经历,结果虽然不好,但是过程却培养了我能够静坐下来看一整天书的耐心。

开始技术问题面试后,我更是一脸懵逼,在我明明正确回答了StringBuffer和StringBuilder的区别、HashMap和HashTable的区别几个问题后,面试官再次表示质疑,他认为我说反了。这让我脑子一片混乱,临走时面试官还让我自己回去查。我还用回家查吗?我出了公司门就掏出手机,打开印象笔记,确认了的确是面试官自己弄混淆了。所以,我当时就萌生了要写一篇关于这两个问题的博客的想法。查阅资料,跑Demo,从源码角度去分析,真正的掌握它们。当下一次面试再遇到诸如此类问题时,不再仅仅从表面去回答,不再背出面经中的答案,而是自信得讲出自己的理解。

下面我将从:浅析区别,性能、部分源码解读,历史原因及选用。三个方面试图完整的解读StringBuffer和StringBuilder。希望读者看后既能游刃有余于面试题中,又能对日常开发提供一些支撑。

1.浅析区别

在进入本文主题之前,先提一下String。因为很多面试官会连带一起问,而涉及该问的String部分又比较简单,便一并讲述清楚,也有助于整体理解字符串储存这部分知识。

  • String

String可以用来保存字符串,一旦生成一个String对象之后,对其的任何改变操作,都会造成新的字符串生成。因此,当频繁操作(改变)String对象的时候,就会产生大量的垃圾对象。不仅导致不必要的内存消耗,当达到一定内存占用之后,就会引起JVM的GC操作,GC属于耗时操作,必然带来系统性能上的急剧下滑。

  • StringBuffer

StringBuffer也可以用来保存字符串,并且它的目的就是为了解决String类对象保存字符串时的尴尬处境。当我们对StringBuffer对象操作并改变时,它不会像String那样产生新的对象,而是对StringBuffer对象本身进行操作甚至改变。
StringBuffer上的主要操作是append和insert方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而insert方法则在指定的点添加字符。
除此之外,由于StringBuffer类中又大量的synchronize修饰,它是一个线程安全的类。(牵涉到更多线程安全的问题将在第三部分详细讲述)

  • StringBuilder

在jdk1.5之后,Java又新增了一个StringBuilder类,它是作为StringBuffer的建议替换出现的,它们兼容同一套API。但是StringBuilder不再有大量的synchronize修饰,因此它是非线程安全的。亦因此在实际运行中,不再存在由同步引起的额外开销,从而带来性能上的又一些提升。

2.性能、源码剖析

先简单看下为什么String保存的对象被修改时会产生新的对象。

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

从源码的开始部分可以看出,String是一个被final修饰的类,它是不能被继承的。虽然被final修饰的类,其中的属性和方法可以选择性的由final修饰与否。而String中字符串的保存是由字符数组来完成的,这个字符数组value恰好也由final修饰。我们知道,数组本身是属于引用变量类型,当final修饰引用类型变量后,它的指针是不可以更改的。所以String对象一旦生成并且有了指针指向,那么它是不可更改的。
有下列部分源码可知,当我们对String对象进行操作,试图更改它,无论是sub、concat还是replace都不是在原有的字符串上进行的,而是重新生成一个新的字符串对象。也就是说,对String对象的任何改变都不影响到原对象。

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 concat(String str) {        int otherLen = str.length();        if (otherLen == 0) {            return this;        }        int len = value.length;        char buf[] = Arrays.copyOf(value, len + otherLen);        str.getChars(buf, len);        return new String(buf, true);    }
 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;    }

可以看到,这三个方法的返回值都是new了一个String对象。当然,他们调用的String构造方法各不相同。
例如,concat方法就是在新声明的字符数组NC拷贝了原String对象内的字符数组之后,再将(增添)目标String对象中的字符数组OC拼接在NC之后,最后调用如下构造方法,并传入NC。

String(char[] value, boolean share) {        // assert share : "unshared not supported";        this.value = value;    }

关于这部分,如果想全面了解请自行阅读源码。

…由于在写博客的时候又冒出几个问题,我都还没弄懂。稍后接着写。

原创粉丝点击