String类型的本质
先看一下String的头部源码
public final class String implements Serializable, Comparable<String>, CharSequence {
private static final long serialVersionUID = -6849794470754667710L;
private static final char REPLACEMENT_CHAR = (char) 0xfffd;
打开String的源码,类注释中有这么一段话“Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.”。
这句话总结归纳了String的一个最重要的特点:
String是值不可变(immutable)的常量,是线程安全的(can be shared)。
接下来,String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。
String类表示字符串。java程序中的所有字符串,如“ABC”,是实现这个类的实例
字符串是常量,它们的值不能被创建后改变。支持可变字符串字符串缓冲区。因为字符串对象是不可改变的,所以它们可以被共享。例如:
String str = "abc";
这里实际上创建了两个String对象,一个是”abc”对象,存储在常量空间中,一个是使用new关键字为对象s申请的空间,存储引用地址。
在执行到双引号包含字符串的语句时,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里,如上面所示,str 和 s 指向同一个引用.
String的定义方法归纳起来总共为以下四种方式:
- 直接使用”“引号创建;
- 使用new String()创建;
- 使用new String(“abcd”)创建以及其他的一些重载构造函数创建;
- 使用重载的字符串连接操作符+创建。
Java String对象和字符串常量的关系?
JAVA中所有的对象都存放在堆里面,包括String对象。字符串常量保存在JAVA的.class文件的常量池中,在编译期就确定好了。
比如我们通过以下代码块:
String s = new String( "myString" );
其中字符串常量是”myString”,在编译时被存储在常量池的某个位置。在运行阶段,虚拟机发现字符串常量”myString”,它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对( [myString], s1 )保存到内部字符串常量列表中。
如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。
例如,String s2 = “myString”,运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。但是String对象s在堆里的一个不同位置,所以和s1不相同。
JAVA中的字符串常量可以作为String对象使用,字符串常量的字符序列本身是存放在常量池中,在字符串内部列表中每个字符串常量的字符序列对应一个String对象,实际使用的就是这个对象。
tring 一些提高性能方法
string的contact()方法
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
char buf[] = new char[count + otherLen];
getChars(0, count, buf, 0);
str.getChars(0, otherLen, buf, count);
return new String(0, count + otherLen, buf);
}
这是concat()的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的:return new String(0, count + otherLen, buf);这同样也创建了10W个字符串对象,这是它变慢的根本原因。
String的intern()方法
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
“abc”.intern()方法的返回值还是字符串”abc”,表面上看起来好像这个方法没什么用处。但实际上,它做了个小动作:检查字符串池里是否存在”abc”这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把”abc”添加到字符串池中,然后再返回它的引用。
String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true
过多得使用 intern()将导致 PermGen 过度增长而最后返回 OutOfMemoryError,因为垃圾收集器不会对被缓存的 String 做垃圾回收,所以如果使用不当会造成内存泄露。
使用StringBuilder 提高性能
在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象。但是如何正确的使用StringBuilder呢?
初始合适的长度
StringBuilder继承AbstractStringBuilder,打开AbstractStringBuilder的源码
abstract class AbstractStringBuilder {
static final int INITIAL_CAPACITY = 16;
private char[] value;
private int count;
private boolean shared;
我们可以看到
StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。
new StringBuilder(),并且 时char[]的默认长度是16,
private void enlargeBuffer(int min) {
int newCount = ((value.length >> 1) + value.length) + 2;
char[] newData = new char[min > newCount ? min : newCount];
System.arraycopy(value, 0, newData, 0, count);
value = newData;
shared = false;
}
然后如果StringBuilder的剩余容量,无法添加全部内容,如果要append第17个字符,怎么办?可以看到enlargeBuffer函数,用System.arraycopy成倍复制扩容!导致内存的消耗,增加GC的压力。
这要是在高频率的回调或循环下,对内存和性能影响非常大,或者引发OOM。
同时StringBuilder的toString方法,也会造成char数组的浪费。
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
我们的优化方法是StringBuilder在append()的时候,不是直接往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。
public class StringBuilderHolder {
private final StringBuilder sb;
public StringBuilderHolder(int capacity) {
sb = new StringBuilder(capacity);
}
/**
* 重置StringBuilder内部的writerIndex, 而char[]保留不动.
*/
public StringBuilder resetAndGetStringBuilder() {
sb.setLength(0);
return sb;
}
}