public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; //char数组,存储内容
private int hash; //存放哈希值,默认为0
……
}
String类是被final所修饰的,因此String类对象不可变,也不可继承。String变量存储的是对String对象的引用,即地址,String对象里存储的才是字符串的具体值。
不变的优势:
(1)高效性。Java中经常会用到字符串的哈希码(hashcode)。字符串的不可变能保证其hashcode永远保持一致,创建String对象时,同时存储其hash值,这样每次使用一个字符串的hashcode的时候不用重新计算一次,更加高效;
(2)便捷性。在其它类中对String对象的引用更加方便;
(3)安全性。String被广泛的使用在其他Java类中充当参数。比如网络连接、打开文件等操作。如果字符串可变,那么类似操作可能导致安全问题。比如,引用同一String对象的修改会导致该连接中的字符串内容被修改,可变的字符串也可能导致反射的安全问题。
(4)线程安全。在多个线程之间共享而无需同步。注:String对象不是绝对不变的,也可以通过反射的方式进行修改。
(1)字面量赋值 eg:String str = “Hello”;
直接赋值:JVM会首先去字符串常量池中寻找是否有equals(“Hello”)的String对象,如果有,就把该“Hello”对象在字符串常量池中的引用复制给字符串变量str;如若没有,就在堆中新建一个对象,同时把引用驻留在字符串常量池中,再把引用赋给字符串变量str。
用该方法创建字符串时,无论创建多少次,只要字符串的值(内容)相同,那么它们所指向的都是堆中的同一个对象。
(2)new关键字创建新对象 eg:String str = new String(“Hello”);
利用new来创建字符串时,在编译期,若常量池中已经存在字面量“hello”,则直接引用,若不存在,则“Hello”会被加入到Class文件的常量池中;在运行期,无论字符串常量池中是否有与当前值相同的对象引用,都会在堆中新开辟一块内存,创建一个新的字符串对象,这个对象所对应的字符串字面量是保存在字符串常量池中的,new出来的对象str保存的是堆中刚刚创建出来的的字符串对象的引用。
总结:常量池是方法区的一部分,线程共享,且它是线程安全的,具有相同值的引用指向常量池中同一个位置,如果常量池中没有新的值,那么就会新建一个常量来交给新的引用。
对于同一个对象,new出来的字符串存放在堆中,而直接赋值给变量的字符串存放在常量池里。
intern()
intern方法可以在运行期向运行时常量池中增加常量。intern()有两个作用,一是将字符串字面量放入常量池(如果池中没有的话),第二个是返回这个常量的引用。
使用:很多时候,程序中得到的字符串在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。比如字符串拼接时,非字面量的拼接s3 = s1+s2,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的,只有在运行期才能确定。此时使用intern对那些大量使用的字符串进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样可以减少大量字符串对象的创建。
“==”比较的是在堆中创建的对象的地址, equals比较的就是字面量的内容。
效率(用时短到长):StringBuilder < StringBuffer < concat < + < StringUtils.join
(1)“+”
(语法糖,Java唯一的运算重载)
底层原理:将String转成StringBuilder后,使用其append方法以及toString方法进行处理。
- 在for循环中,采用“+”进行拼接,每次都会new一个StringBuilder,频繁的新建对象不仅仅会耗费时间,还会造成内存资源的浪费。所以在循环中,推荐使用StringBuilder拼接;
- 表达式右边是纯字符串常量,那么存放在常量池里面。eg:String str = “Hello” + “World”;
- 表达式右边如果存在字符串引用,那么就存放在堆里面。eg:String str = str1 + str2。
(2)contact
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
底层原理:首先创建一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,最后使用这个字符数组创建一个新的String对象并返回。
(3)StringBuilder
其append方法继承AbstractStringBuilder类
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩容。
(4)StringBuffer
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的,该方法使用synchronized进行声明。
(5)StringUtils.join
StringUtils.join也是使用了StringBuilder,并且其中还有很多其他操作,耗时较长,擅长处理字符串数组或者列表的拼接。
参考
Java详解【String】+【StringBuilder vs StringBuffer】+【字符串拼接】
为什么阿里巴巴不建议在for循环中使用"+"进行字符串拼接
我终于搞清楚了和String有关的那点事儿。