美文网首页
String,StringBuilder,StringBuffe

String,StringBuilder,StringBuffe

作者: Snipers_onk | 来源:发表于2019-10-23 15:51 被阅读0次

    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即可。

    相关文章

      网友评论

          本文标题:String,StringBuilder,StringBuffe

          本文链接:https://www.haomeiwen.com/subject/jfskvctx.html