String、StringBuffer、StringBuffer 分析

关于String 类容易错误的几点内容,首先我们来看String 类的几种方法

public class Test {
    public static void main(String[] args) {
        String a = "hello";
        String b = new String("hello");
        System.out.println(a == b);  //false 
        System.out.println(a.equals(b)); //true

        System.out.println("--------");
        String c = "hel";
        String d = "lo";
        String e = c + d;
        String f = "hel"+"lo";
        System.out.println(a == e); //false
        System.out.println(a == f); //true
    }
}

以上是关于String 类的 == 和 equals 方法在不同情况下的返回结果。我们来逐步分析,为什么会产生不同的结果。

String 当中 equals 方法

我们来看下String 类当中equals 方法的源码

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;   //①
    }
    if (anObject instanceof String) { //②
        String anotherString = (String)anObject;
        int n = value.length;
        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;
}

第一步:判断要比较的对象和当前对象地址是否相同,如果相同直接返回true;

第二步:如果需要判断的对象是String 类型,进行强制类型转换

第三步:如果两个对象的char[] 长度相同,那么对两个字符串中char[] 数组逐个对比,如果有不相同的,直接返回false

结论:String 类中equals 方法对比的就是两个字符串的字面值,只要字符串的内容相同,那么equals方法,就会返回true, 与生成该对象的方式无关。

接下来我们来主要看 String 类当中不同情况下 == 方法为什么返回的是不同的。

1、String s = "hello";

2、String s = new String("hello");

3、String a = "hel",b = "lo", c = a+b;

对于以上三种生成String 对象的方式我们从java编译出来的源码来看下。

public class Test {
    public static void main(String[] args) {
        String a = "hello";
        String b = "hello";
        String c = "hel"+"lo";
    }
}

使用javap -v 对Test.class文件进行编译,我们可以看到常量池:

对于String a = "hello"; 这段代码由两部分组成,0:ldc 2 :astore_1 , 可以理解为在常量池当中为字符串hello开辟了一个内存空间,接下来当我们在执行 String b = "hello" 时,也是同样的操作,以及字符串内容拼接时,可以看到后面都是 #2 两者返回的都是常量池当中的同一个地址。

接下来对于String s = new String("hello"); 这段代码编译出来的内容

public class Test {
    public static void main(String[] args) {
        String a = new String("hello");
        String b = "hello";
    }
}

 

 

0-3 行,java 堆上为String 类申请内存。

4 判断常量池中是否存在字符串“hello”,如果没有在常量池中创建一个“hello”对象。并返回

10 是String b = "hello" 这个字符串已经在常量池中存在了,所以不创建新的对象,直接返回常量池中的对象。

总结:使用new String 的方式创建对象时,首先会在java内存的堆上创建一个string 对象,然后去判断常量池中是否存在这个字符串对象,如果存在直接,不创建字符串池中的对象,如果不存在,需要创建一个常量池中的对象,但是此时返回的是堆中对象的地址。

String a = "hel",b = "lo", c = a+b;

public class Test {
    public static void main(String[] args) {
        String a = "hello ";
        String b = "world";
        String c = a + b;
        String d = "hello world";
    }
}

可以看到字符串变量 a+b 这段代码在底层的操作

6-9 在java堆上为StringBuilder申请内存

10 调用StringBuilder 的无参构造方法

14、18 调用append方法拼接字符串

21 调用StringBuilder toString 方法返回

所以我们可以看到字符串变量的拼接,其底层其实是StringBuilder 的append 方法,然后使用toString返回的。

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

而StringBuilder中的toString 方法实际上市创建了一个新的String对象,因此 == 判断时,地址是不相同的。

关于StringBuilder 和StringBuffer 

StringBuilder和StringBuffer 底层都是维护了一个char[] 数组,不同的是 StringBuffer 在部分方法上会加上synchronized

关键字来保证线程安全。

所以在高并发场景下,StringBuffer 的性能是不如StringBuilder 的,但是并不是说使用StringBuilder 就能达到性能最优

关于字符串的操作有以下的建议

1、在for 循环当中,不建议使用 String + 的方式

2、在使用StringBuilder 时,最好可以初始化一个合适的长度

关于第一点:

在上面String 中已经介绍了,如果使用 变量 + 的方式,每次都要创建一个StringBuilder对象,造成内存的极大浪费,同时也会降低执行时间。

第二点:

StringBuilder 在创建时,会出事化一个数组长度为16 char[] 数组,如果我们append 追加的长度超过16 ,我们来看下StringBuilder 会怎么做

/**
 * 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);
}

1、在其父类当中会对数据进行扩容操作,首先扩容为原长度的2倍+2

2、如果扩容后,还是小于新增量,按照给定的容量

3、将原来的value 数组拷贝到新的数组当中

所以,如果我们在初始化的时候,没有指定StringBuilder 的长度,加入一个长度为129的字符串,通过StringBuilder append方法追加进来,需要经过16,34,70,142,四次复制和丢弃,一共浪费掉了几百个字符的数组。

所以初始化一个相对来说大一点的内存,会比成倍的扩容要好~

但素, 还是会有内存上的浪费,因为在最后toString 时,会new String 的方式拷贝一份数组到String 对象当中。

我们可以使用unsafe 类来避免这次拷贝,这里推荐一个比较好的方法,参照BigDecimal中 提供的StringBuilderHelper 的方法

// Accessors.
StringBuilder getStringBuilder() {
    sb.setLength(0);
    return sb;
}

设置StringBuilder的length 为0,这样的话,并没有清空数组中的内容,只是重置了count指针,这样在toString 时,count 也是传递的参数,就不担心超过新内容的旧数据被拷贝过去。

关于并发时,使用StringBuilder 和StringBuffer 如果需要并发访问,最好是将StringBuiler 放在ThreadLocal 当中访问

而不是使用StringBuffer ,毕竟加锁会比较降低访问效率。

参考:浅谈StringBuilder  StringBuilder在高性能场景下的正确用法

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值