String、StringBuilder、 StringBuffer 深入分析 源码解析

java学习有一段时间了,但学习的东西都是框架等东西,java基础知识有点遗忘,所以重温一下java基础知识,写写文章里面有错的希望大家指正共同进步~~

一、String

大家经常会说使用“+”号连接String 字符串比StringBuffer慢,String类对象为不可变对象,一旦你修改了String对象的值,隐性重新创建了一个新的对象,那接下来我们详细分析一下为什么使用“+”号速度会慢,为什么String
对象是不可变对象:

1、final修饰类、引用变量、基本变量

(1)、如果一个类被final修饰则这个类是不能被继承的,没有子类。String类是一个final类,只能说明这个类不能被继承也就没有子类。

(2)、如果一个引用变量被final修饰,则引用变量的值是不能修改,而不是说被引用对象

(3)、2中说的引用变量的值的问题,如果大家看了深入理解jvm这本书的话可能对这句话有印象:由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有规定这个引用应该通过何种方式去定位,访问堆中的对象的具体位置,具体实现因不同的虚拟机而不同,有句柄地址和对象地址两种

(4)、句柄方式实现也就是在javaheap(堆内存)中分配了一个句柄池这个句柄池中存放着具体对象的地址,而引用变量中存放的是句柄池中某个句柄的地址,这种方式要查找两次地址所以速度有点慢。

(5)、对象地址方式实现,引用变量中存放的就是javaheap(堆内存)中对象的地址。

(6)、如果final修饰的是基本类型的变量则这个变量的值不能能修改,如果final修饰的是引用变量则这个引用变量的值不能改变也就是4,5 中所说的句柄的地址或对象的地址不能改变而他们所引用的对象的内容是可以改变的。

2、String 类使用"+" 来连接字符的整个过程描述

(1)、大家经常会说不要使用"+" 来连接字符串这样效率不高(相对于 StringBuilder、StringBuffer)那为什么那,看看下面:

String  str= "a";  str=str+"b";  str=str+"c";

实现过程:

String  str= "a";创建一个String对象,str 引用到这个对象。

在创建一个长度为str.length() 的StringBuffer 对象

StringBuffer  strb=new  StringBuffer(str);

调用StringBuffer的append()方法将”b“添加进去,strb.append("b");

调用strb 的toString()方法创建String对象,之前对象失去引用而str重新引用到这个新对 
象。

同样在创建StringBuffer对象 调用append()方法将”c“添加进去,调用toString() 方法 创建String对象

再将strb引用到 新创建的String对象。之前对象失去引用之后存放在常量池中,等待垃圾回收。

看到上面使用“+”连接字符串的过程,就明白了为什么要使用StringBuffer 来连接字符而不是使用String 的“+”来连接。

(2)、知道了使用”+“连接的过程,我们再来看看上面提到的使用”+“号为什么会创建新的对象,也就是说String对象是不可变对象。这里有个概念就是对象不可变,而String 的对象就是一个不可变对象。那什么叫对象不可变那: 当一个对象创建完成之后,不能再修改他的状态,不能改变状态是指不能改变对象内的成员变量,包括基本数据类型的值不能改变。引用类型的变量不能指向其他对象,引用类型指向的对象的状态也不能改变。对象一旦创建就没办法修改期所有属性,所以要修改不可变对象的值就只能重新创建对象。

3、String 对象不可变  源码分析

(1)、上面说了String 对象为不可变对象,为什么String 对象不可变,String对象的状态不能改变,接下来我们看看String 类的源码:

jdk 1.7 的源码

<span style="font-family:SimSun;">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
</span>

我们会发现String 的底层是使用字符数组来实现的。

String 类中只有两个成员变量一个是value 一个是hash,这个hash和我们讨论的问题没关系,通过注解我们知道他是缓存String对象的hash值

value 是一个被final修饰的数组对象,所以只能说他不能再引用到其他对象而不能说明他所引用的对象的内容不能改变。但我们在往下看源码就会发现String 类没有给这两个成员变量提供任何的方法所以我们也没办法修改所引用对象的内容,所以String 对象一旦被创建,这个变量被初始化后就不能再修改了,所以说String 对象是不可变对象。

(2)、String 对象不是提供了像replace()等方法可以修改内容的吗,其实这个方法内部创建了一个新String 对象 在把这个新对象重新赋值给了引用变量,看看源码你就相信了他是在内部重现创建了String 对象

<span style="font-family:SimSun;"> 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++;
                }
              <span style="color:#CC0000;">  return new String(buf, true);</span>
            }
        }
        return this;
    }</span>

总结:

1、String 类是一个final 修饰的类所以这个类是不能继承的,也就没有子类。

2、String 类的成员变量都是final类型的并且没有提供任何方法可以来修改引用变量所引用的对象的内容,所以一旦这个对象被创建并且成员变量初始化后这个对象就不能再改变了,所以说String 对象是一个不可变对象。

3、使用“+”连接字符串的过程产生了很多String 对象和StringBuffer 对象所以效率相比直接使用StringBuffer 对象的append() 方法来连接字符效率低很多。

4、引用变量是存在java虚拟机栈内存中的,它里面存放的不是对象,而是对象的地址或句柄地址。

5、对象是存在java heap(堆内存)中的值

6、引用变量的值改变指的是栈内存中的这个引用变量的值的改变是,对象地址的改变或句柄地址的改变,而对象的改变指的是存放在Java heap(堆内存)中的对象内容的改变和引用变量的地址和句柄没有关系。

二、StringBuffer

我们在上面说String对象是不可变的,而StringBuffer 对象是可变的,大家都说在能大体了解字符串的长度的情况下创建StringBuffer对象时 指定其容量,在上面的string中我们也知道使用“+”号的时候我们也是调用了append方法。

1、 为什么StringBuffer 对象可变, 为什么要尽量指定初始大小,append方法是怎么实现的 下面我们来看看这几个为什么

2、String 对象不可变是因为成员变量都被final修饰并且没有提供任何访问被引用对象的方法所以不能改变,而StringBuffer是怎么样的那我们可以去看看源码:

<span style="font-family:SimSun;"> public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    /**
     * Constructs a string buffer with no characters in it and an
     * initial capacity of 16 characters.
     *
     * 构造函数,默认容量为16个字符
    */
    public StringBuffer() {
        super(16);
    }

    /**
     * Constructs a string buffer with no characters in it and
     * the specified initial capacity.
     *
     * @param      capacity  the initial capacity.
     * @exception  NegativeArraySizeException  if the <code>capacity</code>
     *               argument is less than <code>0</code>.
     */
    public StringBuffer(int capacity) {
        super(capacity);
    }

    /**
     * Constructs a string buffer initialized to the contents of the
     * specified string. The initial capacity of the string buffer is
     * <code>16</code> plus the length of the string argument.
     *
     * @param   str   the initial contents of the buffer.
     * @exception NullPointerException if <code>str</code> is <code>null</code>
     * 容量为str的长度加上16个字符
     */
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }</span>

StringBuffer 类继承自AbstractStringBuilder 那在看看AbstractStringBuilder的源码

<span style="font-family:SimSun;">abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
    */
    char[] value;

    /**
     * The count is the number of characters used.
     * count 为value数组中存放的字符的个数而不是value的长度
     */
    int count;

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

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     * 调用构造函数来创建容量为capacity 的数组
    */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

    /**
     * Returns the length (character count).
     *
     * @return  the length of the sequence of characters currently
     *          represented by this object
     */
    public int length() {
        return count;
    }</span>

从这些源码我们看到 他的数组和String 的不一样,因为成员变量value数组没有被final修饰所以可以修改他的引用变量的值,即可以引用到新的数组对象。所以StringBuffer对象是可变的

3、如果知道字符串的长度则创建对象的时候尽量指定大小

(1)、在上面的源代码中我们看到StringBuffer 的构造函数默认创建的大小为16个字符。

(2)、如果我们在创建对象的时候指定了大小则创建指定容量大小的数组对象

<span style="font-family:SimSun;">/**
     * Constructs a string buffer with no characters in it and
     * the specified initial capacity.
     *
     * @param      capacity  the initial capacity.
     * @exception  NegativeArraySizeException  if the <code>capacity</code>
     *               argument is less than <code>0</code>.
     */
    public StringBuffer(int capacity) {
      // 调用父类的构造方法
    super(capacity);
    }
</span>
<span style="font-family:SimSun;"> /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }</span>

按照指定容量创建字符数组

(3)、如果在创建对象时构造函数的参数为字符串则 创建的数组的长度为字符长度+16字符

这样的长度,然后再将这个字符串添加到字符数组中,添加的时候会判断原来字符数组中的个数加上这个字符串 的长度是否大于这个字符数组的大小如果大于则进行扩容如果没有则添加,源码如下:

<span style="font-family:SimSun;"> /**
     * Constructs a string buffer initialized to the contents of the
     * specified string. The initial capacity of the string buffer is
     * <code>16</code> plus the length of the string argument.
     *容量为16加上string 的长度
     * @param   str   the initial contents of the buffer.
     * @exception NullPointerException if <code>str</code> is <code>null</code>
     */
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }</span>

调用父类的构造函数创建字符数组对象

<span style="font-family:SimSun;"> /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
</span>

append 出现在了这里刚好一起来看看 append方法的实现

其实append方法就做两件事,如果 count (字符数组中已有字符的个数)加添加的字符串的长度小于 value.length 也就是小于字符数组的容量则直接将要添加的字符拷贝到数组在修改count就可以了。

如果cout和添加的字符串的长度的和大于value.length  则会创建一个新字符数组 再将原有的字符拷贝到新字符数组,再将要添加的字符添加到字符数组中,再改变conut(字符数组中字符的个数)

整个添加过程的源码如下:

<span style="font-family:SimSun;"> public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }</span>

调用父类的方法

<span style="font-family:SimSun;">public AbstractStringBuilder append(String str) {
        if (str == null) str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }</span>

这个方法中调用了ensureCapacityInternal ()方法判断count(字符数组原有的字符个数)+str.length() 的长度是否大于value容量

<span style="font-family:SimSun;">/**
     * This method has the same contract as ensureCapacity, but is
     * never synchronized.
     */
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }</span>

如果count+str.length() 长度大于value的容量 则调用方法进行扩容

<span style="font-family:SimSun;"> /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    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);
    }</span>

Arrays.copyOf(value,newCapacity) 复制指定的数组,截取或用 null 字符填充(如有必要),以使副本具有指定的长度。

上面的getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)   将字符从此字符串复制到目标字符数组dst中,第一个参数 第二个参数截取要添加字符串的长度,第三个为目标字符数组第四个为字符串要添加到数组的开始位置

到这里数组的赋值都结束了,修改count的值,整个append也就结束了。

总结:

1、StringBuffer 类被final 修饰所以不能继承没有子类

2、StringBuffer 对象是可变对象,因为父类的 value [] char 没有被final修饰所以可以进行引用的改变,而且还提供了方法可以修改被引用对象的内容即修改了数组内容。

3、在使用StringBuffer对象的时候尽量指定大小这样会减少扩容的次数,也就是会减少创建字符数组对象的次数和数据复制的次数,当然效率也会提升。

StringBuilder 和StringBuffer 很像只是不是线程安全的其他的很像所以不罗嗦了。

时间: 2024-10-11 12:02:00

String、StringBuilder、 StringBuffer 深入分析 源码解析的相关文章

2017.4.16 StringBuilder &amp; StringBuffer关键源码解析

String.StringBuilder.StringBuffer的异同点 结合之前写的博文,我们对这三个常用的类的异同点进行分析: 异: 1>String的对象是不可变的:而StringBuilder和StringBuffer是可变的. 2>StringBuilder不是线程安全的:而StringBuffer是线程安全的 3>String中的offset,value,count都是被final修饰的不可修改的:而StringBuffer和StringBuilder中的value,cou

String源码解析(一)

本篇文章内的方法介绍,在方法的上面的注释讲解的很清楚,这里只阐述一些要点. Java中的String类的定义如下: 1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence { ...} 可以看到,String是final的,而且继承了Serializable.Comparable和CharSequence接口. 正是因为这个特性,字符串对象可以被共享,例如下面

Java String源码解析

String类概要 所有的字符串字面量都属于String类,String对象创建后不可改变,因此可以缓存共享,StringBuilder,StringBuffer是可变的实现 String类提供了操作字符序列中单个字符的方法,比如有比较字符串,搜索字符串等 Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串. 字符串的连接实际上是通过StringBuffer或者StringBuilder的append()方法来实现的 一般情况下,传递一个空参数在这类构造函

String源码解析

定义 先看一下文档中的注释 12345 * 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. */ String对象是常量,创建之后就不能被修改,所以该对象可以被多线程共享. 123456789

死磕 Java 系列(一)&mdash;&mdash; 常用类(1) String 源码解析

写在前面 这是博主新开的一个 java 学习系列,听名字就可以看出来,在这一些系列中,我们学习的知识点不再是蜻蜓点水,而是深入底层,深入源码.由此,学习过程中我们要带着一股钻劲儿,对我们不懂的知识充满质疑,力求把我们学过的知识点都搞清楚,想明白. 一.引言 在 java 的世界里,存在一种特殊的类,它们的创建方式极为特别,不需要用到 new XXX(当然也可以用这种方式创建), 但是却大量出现在我们的代码中,那就是 String 类.作为日常中使用频率最高的类,它是那么普通,普通到我们从来都不会

StringBuffer源码解析

在重写ArrayList的toString方法时,查看了StringBuffer的源码. //new StringBuffer("[");构造方法public StringBuffer(String str) { super(str.length() + 16); append(str);}//super(str.length() + 16);调用父类构造AbstractStringBuilder(int capacity) { //初始化value数组 char[] value;le

Android研发中对String的思考(源码分析)

1.常用创建方式思考: String text = "this is a test text "; 上面这一句话实际上是执行了三件事 1.声明变量 String text; 2.在内存中开辟空间 (内存空间一) 3.将变量的text的引用指向开辟的内存空间 当有 text = "this is a change text"; 这一句话执行了两件事 1.在内存中开辟空间 2.将变量text 的引用指向 新开辟的内存空间 3.内存空间一此时依然存在,这就是说明了Stri

JDK源码及其他框架源码解析随笔地址导航

置顶一篇文章,主要是整理一下写过的JDK中各个类的源码解析以及其他框架源码解析的文章,方便自己随时阅读也方便网友朋友们阅读及指正 基础篇 从为什么String=String谈到StringBuilder和StringBuffer Java语法糖1:可变长度参数以及foreach循环原理 Java语法糖2:自动装箱和自动拆箱 集合篇 图解集合1:ArrayList 图解集合2:LinkedList 图解集合3:CopyOnWriteArrayList 图解集合4:HashMap 图解集合5:不正确

Android EventBus源码解析, 带你深入理解EventBus

上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢? 1.概述 一般使用EventBus的组件类,类似下面这种方式: [java] view plain copy public class SampleComponent extends Fragment { @Override public vo