1、String
String的部分源码(jdk1.8.0_65)如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
从以上看出:
- String类是final类,所以方法默认都是final方法,且成员变量都被final修饰,所以String类不可更改(value不能指向其他字符数组)。final类意味着不可继承,在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率,但是,此外还有常量优化、多线程安全
- String类是通过字符数组来实现的,一个String对象包含一个字符数组
- 例如substring、replace等对字符串的操作会返回一个新的String对象,并不是原String对象,原String对象未改变
1.1 常量优化
JVM为了提高性能和减少内存开销,在实例化字符串常量(字面量)的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个全局性的字符串常量池(String Pool,与类的Runtime Constant Pool有别,字符串常量池中里面存放的是引用),当通过字面量来创建(发生在加载阶段,不是运行时)字符串时,JVM通过字符串常量池,看能否查找到具有该字面量的字符串对象,不能则创建对象(放在运行时常量池中)并将其引用放入字符串常量池中,能则返回字符串常量池中的引用。
常量优化的前提是String对象是不可改变的,这样确保多个引用指向同一个对象,不论对String对象怎么处理,都不会改变对象。这里只是说了下原理,具体底层的实现细节请移步R大的博文(113楼)
来看几个例子,注意==比较是引用,并不是对象本身。
package offer;
/**
* @author 小锅巴
*/
public class StringTest {
public static void main(String[] args){
String str1 = "hello world";
String str2 = new String("hello world");
String str3 = "hello world";
String str4 = new String("hello world");
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str4);
System.out.println();
String str6 = "ab";
String str7 = "b";
final String str8 = "b";
String str9 = "a"+"b";
String str10 = "a" + str7;
String str11 = "a" + str8;
System.out.println((str6 == str9));
System.out.println((str6 == str10));
System.out.println((str6 == str11));
System.out.println();
String str5="abab";
System.out.println(("abab" == str5));
}
}
/**
输出:
false
true
false
true
false
true
true
*/
“hello world”实例实际上只有一份,所以str1和str3指向了同一对象;而str2和str4是通过new运行时创建的,那么不论字符串常量池中有没有相同内容的字符串,都要在堆上创建对象,显然是不同的对象,尽管对象的内容相同,str1和str2也就显然是不同的了。
1.2 编译优化
还是上面的例子,str9和str6是同一个对象。编译器将”a”+”b”作为常量表达式,直接取结果为”ab”,后面就和常量优化一样了。
将这两条语句单独拿出来,通过javap反编译查看字节码,可以看出,str9直接指向了”ab”
package offer;
/**
* @author 小锅巴
*/
public class test {
public static void main(String[] args) {
String str6 = "ab";
String str9 = "a"+"b";
System.out.println((str6 == str9));
}
}
然后再看str10和str11,区别在于表达式中的变量是否是final变量,这里因为str8是final变量(引用),因为一旦赋值之后就不允许修改,所以在编译时编译器就已经可以确定str8所指向对象的内容了,直接视为”b”,而str7则不能确定,它所指向的对象可能会改变。还是单独拿出来,反编译验证一下,看不懂字节码也没关系,通过给出的注释也能了解个大概
package offer;
/**
* @author 小锅巴
*/
public class test {
public static void main(String[] args) {
String str6 = "ab";
String str7 = "b";
final String str8 = "b";
String str10 = "a" + str7;
String str11 = "a" + str8;
System.out.println((str6 == str10));
System.out.println((str6 == str11));
}
}
反编译后:
D:\Develop\Java\jdk1.8.0_65\bin>javap -c test
警告: 二进制文件test包含offer.test
Compiled from "test.java"
public class offer.test {
public offer.test();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":
()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #16 // String ab
2: astore_1
3: ldc #18 // String b
5: astore_2
6: ldc #18 // String b
8: astore_3
9: new #20 // class java/lang/StringBuilder
12: dup
13: ldc #22 // String a
15: invokespecial #24 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
18: aload_2
19: invokevirtual #27 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #31 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
25: astore 4
27: ldc #16 // String ab这里就是执行"a" + str8
29: astore 5
31: getstatic #35 // Field java/lang/System.out:Ljava/
io/PrintStream;
34: aload_1
35: aload 4
37: if_acmpne 44
40: iconst_1
41: goto 45
44: iconst_0
45: invokevirtual #41 // Method java/io/PrintStream.printl
n:(Z)V
48: getstatic #35 // Field java/lang/System.out:Ljava/
io/PrintStream;
51: aload_1
52: aload 5
54: if_acmpne 61
57: iconst_1
58: goto 62
61: iconst_0
62: invokevirtual #41 // Method java/io/PrintStream.printl
n:(Z)V
65: return
}
D:\Develop\Java\jdk1.8.0_65\bin>
最后一个,初看可能觉得有点奇怪,一个是引用,一个是字面量,能比么?由于String是比较特殊的类,这里编译器会直接把引用的地方替换为对象的内容,还是单独拿出来,通过反编译来查看,为了便于查看字节码指令,稍微改了一下
package offer;
/**
* @author 小锅巴
*/
public class test {
public static void main(String[] args) {
String str5="abab";
boolean b = ("abab" == str5);
System.out.println(b);
}
}
除此之外,String的编译优化的常量表达式不仅限于字符串类型,基本数据类型都可以,比如:
package offer;
/**
* @author 小锅巴
*/
public class test {
public static void main(String[] args) {
String a = "a1";
String b = "a" + 1;
System.out.println((a == b));
}
}
输出结果为true
对于String str2 = new String("hello world");
“创建了”几个对象,这么说吧,涉及两个String对象,一个其引用在字符串常量池中,另一个是new创建出来的,前者是在类加载时创建的,后者是在运行时创建的。想了解更多请移步R大的博文
在查找Sring的资料时,发现java8(hotspot)中已经没有“永久代”了,取而代之的是“元空间”,可参考这里。而java8之前是用“永久代”实现方法区,运行时常量池是方法区的一部分。
2、StringBuffer StringBuilder
因为String对象是不可变的,每次操作都是创建一个新的String对象,浪费资源,也导致性能过低,StringBuilder不是线程安全的,StringBuffer是线程安全的。
2.1 重载+和StringBuilder
+
是java里唯一的一个重载操作符,当String对象通过+
和基本类型或字符串类型“相加时”,编译器会创建StringBuilder对象并调用StringBuilder的append方法来连接。比如:
package offer;
/**
* @author 小锅巴
*/
public class test {
public static void main(String[] args) {
String str = "";
str = str + "hello";
System.out.println(str);
}
}
AbstractStringBuilder
StringBuffer和StringBuilder都继承自AbstractStringBuilder类,这里只看下append方法的实现,以此为例来说明StringBuilder、StringBuffer的实现原理
AbstractStringBuilder部分源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
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);
}
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
public AbstractStringBuilder append(StringBuffer sb) {
if (sb == null)
return appendNull();
int len = sb.length();
ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);
count += len;
return this;
}
}
这里用到了Arrays.copyOf()方法,关键源码如下,可以看出就是新建了一个字符数组:
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
System.arraycopy()的源码:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
StringBuilder部分源码:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
static final long serialVersionUID = 4383685877147921099L;
public StringBuilder() {
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
@Override
public StringBuilder append(StringBuffer sb) {
super.append(sb);
return this;
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
StringBuffer和StringBuilder类似,区别在于方法都被Synchronized修饰,多个了被transient修饰的字段
append的实现最终还是要看AbstractStringBuilder,可以发现是使用可变长数组实现的(和用数组实现可扩容的栈思想类似),利用了System.arraycopy()实现数组的复制,该方法是个本地方法。
参考资料:
- http://www.cnblogs.com/dolphin0520/p/3778589.html
- http://rednaxelafx.iteye.com/blog/774673/
- http://droidyue.com/blog/2014/12/21/string-literal-pool-in-java/index.html
- http://www.cnblogs.com/fancydeepin/archive/2013/04/23/min-snail-speak_String-StringBuffer-StringBuilder.html
- http://www.cnblogs.com/fancydeepin/archive/2013/04/22/min-snail-speak_String.html
- http://sabaao.blogspot.my/2013/10/java-stringstringbufferstringbuilder.html
- http://www.infoq.com/cn/articles/Java-PERMGEN-Removed