本文将从源码和原理的角度全面分析String、StringBuild和StringBuffer,不会仅仅分析三者的区别。
API中这么描述:String类代表字符串。 Java程序中的所有字符串文字(例如"abc" )都被实现为此类的实例。
字符串是常量,值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。
String字符串有两种创建方式:
String str="hello";//直接赋值的方式
String str=new String("hello");//实例化的方式
我们先看以下代码:
public static void main(String[] args) {
String a = "aaa";
String a1 = "aaa";
String a2 = a;
System.out.println(a == a1); //true
System.out.println(a.equals(a1)); //true
System.out.println(a1 == a2); //true
System.out.println(a1.equals(a2)); //true
String aStr = new String("aaa");
String aStr1 = new String("aaa");
String aStr2 = aStr;
System.out.println(aStr == aStr1); //false
System.out.println(aStr.equals(aStr1)); //true
System.out.println(aStr1 == aStr2); //false
System.out.println(aStr1.equals(aStr2)); //true
String ab = "aaabbb";
String ac = "aaaccc";
String abStr = new String("aaabbb");
String acStr = new String("aaaccc");
System.out.println(ab == abStr); //false
System.out.println(ab.equals(abStr)); //true
System.out.println(ac == acStr); //false
System.out.println(ac.equals(acStr)); //true
}
分析以上代码之前,首先,你得先了解:java中==和equals的区别(hashCode)。
而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;
}
String重写的equals方法的本质是比较其中value值是否相同,==比较常量池的值或者两个对象的内存地址是否相等。以此我们分析第一段代码。
讲到这里,你还得了解jvm的一些基础知识:JVM系列(1)——java内存区域
我们知道,jdk1.8之后,开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配,元空间的大小取决于本地内存的大小。
也就是说,字符串常量池是在堆中分配的内存。
我们借助下图来方便理解:
以下是string的重点,也是面试的重点!!!
我们看图中的第1步和第3步,对于
String a = "aaa";
String ab = "aaabbb";
String ac = "aaaccc";
会将匿名对象“aaa”“aaabbb”“aaaccc”放入对象池,每当下一次对不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象。
(1)jvm首先在常量池内看找不找到字符串 “aaa”,找到,返回他的引用给a,否则,创建新的string对象,放到常量池里。
(2)注意“aaabbb”对象是一个整体,不是简单的“aaa”+“bbb”两个字符串对象进行组合,是独立的一个字符串对象。
我们看图中的第2步和第4步,对于
String aStr = new String("aaa");
String aStr1 = new String("aaa");
String abStr = new String("aaabbb");
String acStr = new String("aaaccc");
创建了几个对象,有两种情况
(1)如果常量池中有字符串aaa,那么只会字内存中创建一个对象;
(2)如果常量池中没有字符串aaa,那么在常量池中创建一个内容为aaa的字符串, 但是遇到了new关键字,则还是会在堆内存(不是常量池)中创建一个对象,然后将对象返回给引用aStr 。
所以用==判断,返回的是false。
(3)当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。所以aStr .intern()==a;返回值为ture。
String 是由final修饰的,所以String是不可变的。参考:详解final、finally和finalize
这里说的不可变,是一旦创建了一个字符串常量,那这个常量就是固定的,比如小a变成了大A。并不代表其引用不可变,比如以下代码,是将a这个引用指向了bbb,并没有改变aaa的值;
String a = "aaa";
a = "bbb";
为什么String要设计成不可变的呢?
(1)享元模式:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。String每当生成一个新内容的字符串时,他们都被添加到一个字符串常量池池中,当第二次再次生成同样内容的字符串实例时,就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化。所以,不可变情况就尤为重要,其他引用(用户)不会因为任一用户把字符串内容改变了而受到影响。安全性,节省空间。
(2)HashMap中key为String类型(在使用 String 类型的对象做 key 时我们可以只根据传入的字符串内容就能获得对应存在 map 中的 value 值,因为内容相同的字符串散列码相同),如果可变,hashmap将变得混乱。逻辑性
(3)在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。安全性,性能。
参考:菜鸟教程
废话不多说,先上核心代码:
StringBuffer:
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
StringBuilder
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
(1)StringBuffer中的所有方法都被synchronized 修饰,所以StringBuffer是线程安全的,StringBuilder不是。
(2) StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder的性能要远大于 StringBuffer。
(3)StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。缓冲区