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即可。
网友评论