String,StringBuilder,StringBuffe

2019-10-23  本文已影响0人  Snipers_onk

String

字符串类中值的存储其实是在内部的字节数组 char[] value中,并且以final修饰,所以只会被初始化一次,且不可改变。这就是常说的String不可变。

String内部结构如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final char value[];
    ...
}

String 常量池

在Java内存分配中,存在字符串常量池。原因是字符串的分配和其他对象一样,是需要消耗高昂的空间和时间的,并且字符串的使用非常多。所以在实例化字符串时,会优先在常量池中查找是否存在字符串,存在则返回该字符串的引用,如果不存在,则进行实例化并将该实例放入常量池中。因为String不可变,所以常量池中不会存在两个相同的字符串。

String str = "abc";

char[] data = {'a','b','c'};
String str2 = new String(data);
System.out.println(str.equals(str2));       //true
System.out.println(str == str2);    //false

String str3 = "abc";
System.out.println(str == str3);    //true
记录一个面试常见的问题:

下面两句代码生成了几个对象:

String s1 = new String("abc");
String s2 = new String("abc");

1.“abc”是一个字符串,它是一个字符串常量,首先要建立它,建立好后把它加入常量池。

2.以“abc”为参数,new String(“abc”)建立了一个对象

3.此时“abc”已经加入常量池,从常量池获取引用即可,new String(“abc”)建立了一个对象。

所以,答案是3个。

intern()

直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。

在JDK1.6中,String常量池保存在永久代中,如果常量池中不存在,会在常量池中复制该对象,并返回引用。

image

在JDK1.7以后,String常量池从永久代(PermGen)移动到了堆内存(Java Heap区),如果在堆内存中存在该对象,会在常量池中保存该对象的引用并返回。

image

在JDK1.8中移除了永久代的概念。

+

在日常开发中,使用 + 链接两个字符串是非常常用的。在编译过程中,会转换为创建StringBuilder对象进行append操作,最后调用StringBuilder.toString()方法生成String。可以看到,toString方法中,新建了一个String对象。

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

还有一种情况,当+两端是编译器确定的字符串,则编译器会进行相应的优化,直接将两个字符串拼接好。

        String str3 = "abc";
        String str4 = "ab"+"c";
        System.out.println("str4 == str3:"+ (str4 == str3));    //true

StringBuilder

StringBuilder继承了AbstractStringBuilder,大部分方法都是在抽象方法中实现的。在AbstractStringBuilder内部维护了一个char[] value,初始化StringBuilder时,实际为新建了一个长度为16的char[]。StringBuilder.append() 时不断向value填充内容。

和String类不同的是,它并没有被final修饰,是一个可变char数组。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    
    char[] value;
    
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
    
}

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
    
    public StringBuilder() {
        super(16);
    }
    
}

当使用append方式添加字符串时,会先获取字符串的长度,然后判断是否需要扩容。如果需要扩容,则先进行扩容,容量为之前的2倍+2。

StringBuilder 性能优化

指定初始长度

从上面代码中,初始化StringBuilder时,初始char[]数组的长度为16,当空间不够时,需要成倍的扩容,如果数组长度很长,则需要多次扩容,每次扩容都需要消耗系统的性能;另外一方面,扩容前的char[]数组也会被浪费掉,等待GC回收。

所以,指定初始长度是一个非常重要的操作。

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

上面说,扩容前的char[]会被浪费掉,等待GC回收。所以让StringBuilder被StringBuilderHolder管理,不被GC回收。

public class StringBuilderHolder {
    private final StringBuilder sb;
    public StringBuilderHolder(int capacity) {
        sb = new StringBuilder(capacity);
    }

    public StringBuilder resetAndGet() {
        sb.setLength(0);
        return sb;
    }
}

//设置长度操作,只改变count值,并且将value数组填充为'\0',并没有改变char[]长度
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;
}

通过sb.setLength(0) 方法可以把char数组的内存区域设置为0,这样char数组重复使用。

为了避免并发访问,可以在ThreadLocal中使用StringBuilderHolder,使用方式如下:

private static final ThreadLocal<StringBuilderHolder> stringBuilder= new ThreadLocal<StringBuilderHolder>() {
    @Override
    protected StringBuilderHolder initialValue() {
        return new StringBuilderHolder(256);
    }
};
 
StringBuilder sb = stringBuilder.get().resetAndGet();

这种方式下,StringBuilder实例的内存空间一直不会被回收,如果char[]扩容到占用内存很大,且其他操作不会用到这么大的空间,就造成了内存浪费。

+ 和 StringBuilder
String s = “hello ” + ”world“;

等价于

String s = new StringBuilder().append(“hello”).append(”world“);

但是,如果是以下情况,

for(;;){
    s = s + ”hello world“
}

每一条语句,都会生成一个新的StringBuilder,性能就完全不一样了。

StringBuilder 和 StringBuffer

这两个都继承了AbstractStringBuilder,不同的是,StringBuffer的函数都有sycronized关键字。这里就不贴代码了。

一般情况下,不会出现几个线程同时操作StringBuffer的情况,所以多数情况下正常使用StringBuilder即可。

上一篇下一篇

猜你喜欢

热点阅读