关于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在高性能场景下的正确用法