String,String Builder,String Buffer-源码

String

String是一个很普通的类

源码分析

//该值用于字符存储
private final char value[];

//缓存字符串的哈希码
private int hash;// Default to 0

//这个是一个构造函数
//把传递进来的字符串对象value这个数组的值,
//赋值给构造的当前对象,hash的处理方式也一样。

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
}



//String的初始化有很多种
//空参数初始化
//String初始化
//字符数组初始化
//字节数组初始化
//通过StringBuffer,StringBuilder构造


问题: 我现正在准备构造一个String的对象,那original这个对象又是从何而来?是什么时候构造的呢?

测试一下:

public static void main(String[] args) {
        String str = new String("zwt");
        String str1 =  new String("zwt");
}

在Java中,当值被双引号引起来(如本示例中的"abc"),JVM会去先检查看一看常量池里有没有abc这个对象,

如果没有,把abc初始化为对象放入常量池,如果有,直接返回常量池内容。

Java字符串两种声明方式在堆内存中不同的体现

为了避免重复的创建对象,尽量使用String s1 ="123" 而不是String s1 = new String("123"),因为JVM对前者给做了优化。

常用的API

System.out.println(str.isEmpty());//判断是不是空字符串
System.out.println(str.length());//获取字符串长度
System.out.println(str.charAt(1));//获取指定位置的字符
System.out.println(str.substring(2, 3));//截取指定区间字符串
System.out.println(str.equals(str1));//比较字符串

isEmpty()

    public boolean isEmpty() {
        return value.length == 0;
    }

length()

    public int length() {
        return value.length;
    }

charAt()

    public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }

substring()

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //如果截取的开始范围刚好是0并且结束范围等于数组的长度,直接返回当前对象,
        //否则用该数组和传入的开始范围和结束范围重新构建String对象并返回。
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

equals()

    public boolean equals(Object anObject) {
        //如果是同一个引用,直接返回true
        if (this == anObject) {
            return true;
        }
        //判断是否是String
        if (anObject instanceof String) {
            //判断长度是否一致
            String anotherString = (String)anObject;
            int n = value.length;
            //判断char[]里面的每一个值是否相等
            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;
    }

equals()与“==”

这两者之间没有必然的联系,

在引用类型中,"=="是比较两个引用是否指向堆内存里的同一个地址(同一个对象),

equals是一个普通的方法,该方法返回的结果依赖于自身的实现

intern()

public native String intern();

//如果常量池中有当前String的值,就返回这个值,如果没有就加进去,返回这个值的引用,

一些基础

Java基本数据类型和引用类型

Java中一共有四类八种基本数据类型, 除掉这四类八种基本类型,其它的都是对象,也就是引用类型。

基本数据类型
浮点类型 float double
字符型 char
逻辑型 boolean
整型 byte short int long

Java自动装箱/拆箱

Integer 里面我们曾经说过得 valueOf (), 这个加上valueOf方法的过程,就是Java中经常说的装箱过程。

在JDK1.5中,给这四类八种基本类型加入了包装类 。

第一类:整型
byte Byte
short Short
int Integer
long Long

第二类:浮点型
float Float
double Double

第三类:逻辑型
boolean Boolean

第四类:字符型
char Character

将int的变量转换成Integer对象,这个过程叫做装箱,

反之将Integer对象转换成int类型值,这个过程叫做拆箱。

以上这些装箱拆箱的方法是在编译成class文件时自动加上的,不需要程序员手工介入,因此又叫自动装箱/拆箱。

用处:

1、对象是对现实世界的模拟 。

2、为泛型提供了支持。

3、提供了丰富的属性和API

public static void main(String[] args) {
        int int1 = 180;
        Integer int2 = new Integer(180);
}

表现如下图:

StringBuilder

StringBuilder类被 final 所修饰,因此不能被继承。

StringBuilder类继承于 AbstractStringBuilder类。

实际上,AbstractStringBuilder类具体实现了可变字符序列的一系列操作,

比如:append()、insert()、delete()、replace()、charAt()方法等。

值得一提的是,StringBuffer也是继承于AbstractStringBuilder类。

StringBuilder类实现了2个接口:

Serializable 序列化接口,表示对象可以被序列化。

CharSequence 字符序列接口,提供了几个对字符序列进行只读访问的方法,

比如:length()、charAt()、subSequence()、toString()方法等。

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

定义的常量

//toString 返回的最后一个值的缓存。每当修改 StringBuffer 时清除。 
private transient char[] toStringCache;

AbstractStringBuilder

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    //value 用来存储字符序列中的字符。value是一个动态的数组,当存储容量不足时,会对它进行扩容。
    char[] value;

    /**
     * The count is the number of characters used.
     */
    //count 表示value数组中已存储的字符数。
    int count;
    
    

构造方法

public StringBuilder() {
    super(16);
}

public StringBuilder(int capacity) {
    super(capacity);
}

public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

public StringBuilder(CharSequence seq) {
    this(seq.length() + 16);
    append(seq);
}

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

StringBuilder类提供了4个构造方法。构造方法主要完成了对value数组的初始化。

其中:

  1. 默认构造方法设置了value数组的初始容量为16。
  2. 第2个构造方法设置了value数组的初始容量为指定的大小。
  3. 第3个构造方法接受一个String对象作为参数,设置了value数组的初始容量为String对象的长度+16,并把String对象中的字符添加到value数组中。
  4. 第4个构造方法接受一个CharSequence对象作为参数,设置了value数组的初始容量为CharSequence对象的长度+16,并把CharSequence对象中的字符添加到value数组中。

append()方法

有多种实现,一般的顺序为:

append() ----> ensureCapacityInternal() 确保value数组有足够的容量 ----> newCapacity()新的容量

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }



    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        
        //扩容参数
        
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

在上面代码中可以看到具体的扩容规则是 * 2 + 2

StringBuffer

基本就是加了一个synchronized的StringBuilder。

StringBuilder 和 StringBuffer 适用的场景是什么?

stringbuffer固然是线程安全的,stringbuffer固然是比stringbuilder更慢,固然,在多线程的情况下,理论上是应该使用线程安全的stringbuffer的。

实际上基本没有什么地方显示你需要一个线程安全的string拼接器 。

stringbuffer基本没有适用场景,你应该在所有的情况下选择使用stringbuiler,除非你真的遇到了一个需要线程安全的场景 。

如果你遇见了,,,

stringbuffer的线程安全,仅仅是保证jvm不抛出异常顺利的往下执行而已,它可不保证逻辑正确和调用顺序正确。大多数时候,我们需要的不仅仅是线程安全,而是锁。

最后,为什么会有stringbuffer的存在,如果真的没有价值,为什么jdk会提供这个类?

答案太简单了,因为最早是没有stringbuilder的,sun的人不知处于何种考虑,决定让stringbuffer是线程安全的,

于是,在jdk1.5的时候,终于决定提供一个非线程安全的stringbuffer实现,并命名为stringbuilder。

顺便,javac好像大概也是从这个版本开始,把所有用加号连接的string运算都隐式的改写成stringbuilder,

也就是说,从jdk1.5开始,用加号拼接字符串已经几乎没有什么性能损失了。

扩展小知识

Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。

在Java9以前字符串采用char[]数组来保存字符,因此字符串的每个字符占2字节,

而Java9的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,因此字符串的每个字符只占1字节。

所以Java9的字符串更加节省空间,字符串的功能方法也没有受到影响。

参考链接

https://zhuanlan.zhihu.com/p/28216267

https://blog.csdn.net/u012317510/article/details/83721250

https://www.zhihu.com/question/20101840

posted @ 2021-08-03 21:20  Ricardo_ML  阅读(204)  评论(0编辑  收藏  举报