百木园-与人分享,
就是让自己快乐。

String、StringBuilder和StringBuffer

JVM(Java虚拟机)

学习String类前,先了解一下JVM,也称为Java虚拟机。

JVM内存分有几大区域,其中,常见有堆、桟、方法区、常量池。

堆是运行时数据区,类通过new指令创建的对象会在堆内存里分配空间。堆内存的数据是由java垃圾回收器自动回收。堆的优势是可以动态地分配内存大小缺点是,由于要在运行时动态分配内存,存取速度较慢。

桟是存放一些基本类型的变量数据和对象的引用。优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。


String

好的,大概了解了JVM后来学习String。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {}

String是java中代表字符串的类。java中所有的字符串面量值都由此类实现,它被声明为 final,因此它不可被继承

private final char value[];

查阅底层代码,String内部使用了数组来存储数据,这个数组由final修饰,当数组初始化后不能再引用其它数组,也确保了String不可变

创建字符串对象

两种方式

  1. 直接赋值,在字符串常量池创建了一个对象

    String str = \"che\";
    
  2. 通过构造方法,创建字符串对象

    String str = new String(\"che\");
    

先来看一下程序

String chen1 = \"chen\";
String chen2 = \"chen\";
String chen3 = \"chen!!\";
String newChen1 = new String(\"chen\");
String newChen2 = new String(\"chen\");
System.out.println(newChen2 == chen1);
System.out.println(newChen1 == newChen2);
System.out.println(chen1 == chen2);
System.out.println(chen1.equals(newChen2));
/*
true
false
true
true

Process finished with exit code 0
*/

两种创建分式的区別

  • 从内存上分析。
    1. 直接赋值的方式。先查找字符串常量池中有没有s1要创建的对象,没有则在常量池中创建对象“chen”,然后到s2定义同样的字符串对象时,还会去常量池中找着是否已经有该对象存在,有则把对象的引用实例共享给s2。以上s1、s2在字符串常量池中只创建了一个对象,因为代码中还没有出现new所以没有在堆里创建对象
    2. 通过new创建字符串对象。首先会先去字符串常量池中查找有没有“chen”的实例引用,有则把该引用共享给堆中的new String(),并把堆中的引用返回到栈中对应的数据,然后压栈。以上str1、str2在字符串常量池有对应的对象时,只在堆中创建了两个对象,并没有在常量池中创建对象

字符串常量池不会存在两个相同的字符串


Q&A

Q1:Hash table Entry

哈希表条目,是字符串常量池底层实现的一种,用于记录字符串常量池中的数据,我们从常量池中获取字符串,实际是从哈希表条目中获取对应的条目值

Q2:字节码文件指令

此处参考的文献: Java字符串字面量是何时进入到字符串常量池中的、Java 中new String(\"字面量\") 中 \"字面量\" 是何时进入字符串常量池的?

以下是上列程序编译后的部分字节码指令,通过执行javap -c FileClass对class文件反编译。或者javap -v FileClass可以更清楚知道常量池的编号对应的数据

public class string_base.TestBase {
  public string_base.TestBase();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object.\"<init>\":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String chen
       2: astore_1
       3: ldc           #2                  // String chen
       5: astore_2
       6: ldc           #3                  // String chen!!
       8: astore_3
       9: new           #4                  // class java/lang/String
      12: dup
      13: ldc           #2                  // String chen
      15: invokespecial #5                  // Method java/lang/String.\"<init>\":(Ljava/lang/String;)V
      18: astore        4
      20: new           #4                  // class java/lang/String
      23: dup
      24: ldc           #2                  // String chen
      26: invokespecial #5                  // Method java/lang/String.\"<init>\":(Ljava/lang/String;)V
      29: astore        5

解析:

main方法:

0-8行,ldc将常量池中的常量值加载到操作数栈;astore_indexbyte将栈顶引用类型值保存到局部变量indexbyte中。

9-29行,new创建一个String对象;dup复制栈顶一个字长的数据,将复制后的数据压栈;ldc将常量池中的常量值加载到操作数栈;invokespecial用于调用特殊的方法,如实例初始化方法、私有方法和父类方法;astore_indexbyte将栈顶引用类型值保存到局部变量indexbyte中。

注:ldc 在常量池中没有对应的常量值时JVM会在常量池中创建该常量值对象。ldc后面的#index是指在常量池中的编号

所以,建议在日常开发中,尽量使用直接赋值的方式去创建String对象,这样可以节省一部分空间。

虽然堆内存的垃圾会有垃圾回收器去回收,但垃圾回收器是随机去回收的,我们不能让回收器立即回收某个不再使用的对象,但可以显示的表明那个对象不再使用了建议垃圾回收器去回收,但它还是不会立即回收。

Q3:==与equals比较的区别

==在对字符串比较的时候,对比的是内存地址,而equals比较的是字符串内容,在开发的过程中, equals()通过接受参数,可以避免空指向。对空指针的对象调用方法也是一件错误的事,因为他没有指向具体的实例,所以其中包含的方法无从得知


方法

返回字符串的长度

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

底层是返回字符数组的长度

返回某个字符在此字符串上出现的索引

//返回变量ch在此字符串中第一次出现的索引
public int indexOf(int ch)
//返回变量ch在此字符串中最后一次出现的索引
public int lastIndexOf(int ch)

将指定的类型值转换为字符串

public static String valueOf(Object obj)

可以是任何类型,底层是在调用toString方法

将字符串转换为大小写

//转小写
public String toLowerCase()
//转大写
public String toUpperCase()

根据JVM的默认语言环境转换

用指定的字符替换掉字符串中的某个字符

public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)

用指定的字面替换序列替换此字符串中与目标字面序列相匹配的每个子串。CharSequence接口被String等类实现。

根据参数分割字符串

public String[] split(String regex)
public String[] split(String regex, int limit)

根据regex分割字符串,也可以根据limit分割成多少个子串,返回一个字符串数值

将字符串从指定索引截取返回索引后面的字符串

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)

判断字符串是否以指定的子字符串开始或结束

public boolean startsWith(String prefix)
public boolean endsWith(String suffix)

判断字符串是否包含指定子字符串

public boolean contains(CharSequence s)

判断字符串长度是否为0

public boolean isEmpty()

判断字符串与指字符串内容是否相同

public boolean equals(Object anObject)

String重写了equals方法,还有比较字符串内容但忽略大小写的,equalsIgnoreCase

拼接字符串

java允许使用+号连接两个字符串,如果与非字符串的值进行拼接时,非字符串的值会被转换成字符串

public static String join(CharSequence delimiter, CharSequence... elements)

多字符串拼接时,可以用join静态方法,参数delimiter是用指定的定界符分隔这些字符串

String不可变

参考文献:

Why String is immutable in Java?

《Effective Java》中对于不可变对象的定义是:对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化

当我们尝试对一个String对象再次赋值,String会新创建一个对象,旧对象还存在,但没有被引用。此时,内存中就会存在两个对象。

String str = \"s1\";
str = \"s2\";

不可变

String的不可变不仅仅是因为底层数组被final修饰,从而无法被修改。

  • 底层char数组被private修饰,且内部没有对外提供修改数组的方法,所以外界没有有效的手段去改变它

  • String被final修饰,避免被继承破坏

  • 在String的中,避免了去修改char数组的代码,涉及对char数组的操作都会重新去创建一个对象

为什么要不可变

  1. 如果代码中出现了大量频繁的创建字符串,可以提高性能和减少内存开销。创建字符串时,首先检查字符串常量池中是否存在该字符串。存在,则返回该引用实例;不存在,则实例化该字符串放入池中,返回引用实例
  2. 为了安全。不可变可以保证线程安全。当多个线程同时调用同一个字符串时,如果有一个线程改变了字符串内容,那将是个很危险的操作
  3. 字符串池的要求,字符串常量池是一个特殊的存储区。当字符串符被创建,并且该字符串已经存在池中,返回已有字符串的引用,而不是创建新对象。如果String是可变的,通过一个字符串引用改变字符串,那么会导致其他字符串引用的值错误

字符串共享

字符串常量池String Pool是JVM实例全局共享的,JAVA会确保池中每个不同的字符串只存在一个拷贝,不会存在相同字符串出现两份拷贝在池中。这样的设计模式称为“享元模式”,采用一个共享来避免大量拥有相同内容对象的开销

String str1 = \"hello\";
String str2 = \"hello\";
System.out.println(str1 == str2);

因为相同的字符串都是引用字符串常量池中的一个字符串常量,所以以上输出的是true

JVM怎么判断新创建的字符串需不需要在Java Heap(堆)中创建 新对象呢?

先根据比较与String Pool中某一个是否相等,如果有,则使用其引用。反之则根据的字面量创建一个字符串对象,再将这个字面量与字符串对象引用关联起来


AbstractStringBuilder

AbstractStringBuilder是对可变字符序列的概念描述,其提供了可变字符序列的基本协议约定。其内部也有用于存储字符串的字符数组,与String不同,其不被final修饰,也就是AbstractStringBuilder的内部成员数组是可变的。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

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

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

成员变量count用于记录实际的字符个数

通过有参构造可以为value引用一个指定大小的数组,当然这是给子类调用的

既然底层是个数组,数组又是顺序存储的,那对数组做操作必然会出现大量的元素移动

方法

获取长度

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

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

length()用于获取数组实际数据的个数

capacity()获取数组的容量。

如果实际数据的个数超过数组的容量,则容量自动增大

设置长度

public void setLength(int newLength) {
    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);
    ensureCapacityInternal(newLength);

    if (count < newLength) {
        Arrays.fill(value, count, newLength, \'\\0\');
    }

    count = newLength;
}

设置数组的容量。

自动扩容

查阅源码,在每次对value数组做操作时都会调用ensureCapacityInternal()方法用于确保空间大小足够。

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

底层是拷贝数组,重新分配大小,大小由newCapacity()方法来决定

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;
}

private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

扩容是原数组长度*2再+2。

如果*2再+2之后的容量够大,那数组容量就使用这个数值

如果*2再+2之后的容量还不够大,先检查数值是否比Integer.MAX_VALUE还大,成立则OutOfMemoryError();再和MAX_ARRAY_SIZE比较,如数值较大,则使用数值,反之使用MAX_ARRAY_SIZE

去除未使用的空间

数组中除count-1外,其他的索引都由\'\\0\'来占用,这就产生资源浪费

public void trimToSize() {
    if (count < value.length) {
        value = Arrays.copyOf(value, count);
    }
}

trimToSize()方法重新拷贝一个以count为目标容量的数组

获取和设定指定索引的值

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

先检查索引是否越界,再返回指定值

public void setCharAt(int index, char ch) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    value[index] = ch;
}

先检查索引是否越界,再给索引处指定值

append方法

很多重载的append方法都会去调用getChars()方法实现从尾部插入数值

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
    if (srcBegin < 0)
        throw new StringIndexOutOfBoundsException(srcBegin);
    if ((srcEnd < 0) || (srcEnd > count))
        throw new StringIndexOutOfBoundsException(srcEnd);
    if (srcBegin > srcEnd)
        throw new StringIndexOutOfBoundsException(\"srcBegin > srcEnd\");
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

StringBuileder、StringBuffer

和String不同的是,StringBuileder、StringBuffer是一个可变的字符序列。StringBuilder、StringBuffer也是实现CharSequence接口,但它俩还继承了AbstractStringBuilder类。

它俩的内部方法基本都是从AbstractStringBuilder类继承下来。构造函数也是调用自父类。

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

三者的区別

1.String不可改变的,线程安全的;StringBuileder是可变的,非线程安全的;StringBuffer也是可变的,线程安全的,推荐在多线程里使用。

String str = \"hello\";
str = str + \" word\";

上面代码在第一行我们创建了String对象,并把“hello”字符串在常量池中的引用和str关联。在执行第二行,先把“hello”和“word”做拼接,再把拼接后新的String对象存储到常量池中,再把新的String对象在常量池中的引用和str关联。而之前的对象并没有发生变化,且之前的对象会被垃圾回收器CG回收掉。

而StringBuffer和StringBuilder则不会,因为底层没有对数组和类做final修饰,所以可以对这个数组“重定义”,当对他们的字符串做操作也就是对这个对象做操作,不会再去创建额外的对象

2.字符串对象使用“+”与字符串或字符串对象做拼接时,编译器碰到每个“+”时,会去new一个StringBuilder并调用append做拼接,最后再调用toString返回字符串

String str = \"hello\";
str += \" word\";
str += \"!!!\";
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #2                  // String hello
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder.\"<init>\":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #6                  // String  word
        16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_1
        23: new           #3                  // class java/lang/StringBuilder
        26: dup
        27: invokespecial #4                  // Method java/lang/StringBuilder.\"<init>\":()V
        30: aload_1
        31: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        34: ldc           #8                  // String !!!
        36: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        39: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        42: astore_1
        43: return

从字节码可以看到,第3和23行新建了两次StringBuilder,而这样的拼接方式无疑是对内存的一种浪费,因为要额外创建对象,所以效率也不是很好。

如果在程序中要对字符串对象做拼接,建议使用StringBuilder或StringBuiffer

2.在大多数情况下,执行速度上比较,StringBuilder > StringBuffer > String

但是,下面的代码就会是String执行的比较快

String str = \"hello\" + \"word\" + \"!!!\";
StringBuilder sb = new StringBuilder(\"hello\").append(\"word\").append(\"!!!\");
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String helloword!!!
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: ldc           #4                  // String hello
         9: invokespecial #5                  // Method java/lang/StringBuilder.\"<init>\":(Ljava/lang/String;)V
        12: ldc           #6                  // String word
        14: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: ldc           #8                  // String !!!
        19: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: astore_2

从字节码可以看出,程序中的第一行代码,JVM会自动解析成String str = “helloword!!!“,因为这些字符串都是编译期间即可知的常量。这种情况,String会比StringBuffer执行的更快些,但是如果拼接的是对象而不是字符串则不会这样。

总结:如果只是简单的的声明字符串,没有过多的操作,那么使用String或StringBuilder都可,但后续要对这个字符串有过多频繁的操作则建议使用StringBuilder。



来源:https://www.cnblogs.com/hello12153-java/p/16174053.html
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » String、StringBuilder和StringBuffer

相关推荐

  • 暂无文章