String str = “”;
for (int i = 0; i < 1000000; i++) {
str += i;
}
System.out.println(“String 耗时:” + (System.currentTimeMillis() - startTime) + “毫秒”);
long startTime = System.currentTimeMillis();
StringBuilder str = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
str.append(i);
}
System.out.println(“StringBuilder 耗时” + (System.currentTimeMillis() - startTime) + “毫秒”);
long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for (int i = 0; i < 1000000; i++) {
str.append(i);
}
System.out.println(“StringBuffer 耗时” + (System.currentTimeMillis() - startTime) + “毫秒”);
综上,分别使用了 String、StringBuilder、StringBuffer,做字符串链接操作(100个、1000个、1万个、10万个、100万个),记录每种方式的耗时。最终汇总图表如下;
从上图可以得出以下结论;
- String 字符串链接是耗时的,尤其数据量大的时候,简直没法使用了。这是做实验,基本也不会有人这么干!
- StringBuilder、StringBuffer,因为没有发生多线程竞争也就没有锁升级,所以两个类耗时几乎相同,当然在单线程下更推荐使用 StringBuilder 。
String str = “”;
for (int i = 0; i < 10000; i++) {
str += i;
}
这段代码就是三种字符串拼接方式,最慢的一种。不是说这种+加的符号,会被优化成 StringBuilder 吗,那怎么还慢?
确实会被JVM编译期优化,但优化成什么样子了呢,先看下字节码指令;javap -c ApiTest.class
一看指令码,这不是在循环里(if_icmpgt)给我 new 了 StringBuilder 了吗,怎么还这么慢呢?再仔细看,其实你会发现,这new是在循环里吗呀,我们把这段代码写出来再看看;
String str = “”;
for (int i = 0; i < 10000; i++) {
str = new StringBuilder().append(str).append(i).toString();
}
现在再看这段代码就很清晰了,所有的字符串链接操作,都需要实例化一次StringBuilder,所以非常耗时。**并且你可以验证,这样写代码耗时与字符串直接链接是一样的。**所以把StringBuilder 提到上一层 for 循环外更快。
四、String 源码分析public final class String
implements java.io.Serializable, Comparable, CharSequence {
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
…
}
在与 谢飞机 的面试题中,我们聊到了 String 初始化的问题,按照一般我们应用的频次上,能想到的只有直接赋值,String str = "abc";,但因为 String 的底层数据结构是数组char value[],所以它的初始化方式也会有很多跟数组相关的,如下;
String str_01 = “abc”;
System.out.println(“默认方式:” + str_01);
String str_02 = new String(new char[]{‘a’, ‘b’, ‘c’});
System.out.println(“char方式:” + str_02);
String str_03 = new String(new int[]{0x61, 0x62, 0x63}, 0, 3);
System.out.println(“int方式:” + str_03);
String str_04 = new String(new byte[]{0x61, 0x62, 0x63});
System.out.println(“byte方式:” + str_04);
以上这些方式都可以初始化,并且最终的结果是一致的,abc。如果说初始化的方式没用让你感受到它是数据结构,那么str_01.charAt(0);呢,只要你往源码里一点,就会发现它是 O(1) 的时间复杂度从数组中获取元素,所以效率也是非常高,源码如下;
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
字符串创建后是不可变的,你看到的+加号连接操作,都是创建了新的对象把数据存放过去,通过源码就可以看到;
从源码中可以看到,String 的类和用于存放字符串的方法都用了 final 修饰,也就是创建了以后,这些都是不可变的。
举个例子
String str_01 = “abc”;
String str_02 = “abc” + “def”;
String str_03 = str_01 + “def”;
不考虑其他情况,对于程序初始化。以上这些代码 str_01、str_02、str_03,都会初始化几个对象呢?其实这个初始化几个对象从侧面就是反应对象是否可变性。
接下来我们把上面代码反编译,通过指令码看到底创建了几个对象。
反编译下
public void test_00();
Code:
0: ldc #2 // String abc
2: astore_1
3: ldc #3 // String abcdef
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."")V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #7 // String def
19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: return
- str_01 = "abc",指令码:0: ldc,创建了一个对象。
- str_02 = "abc" + "def",指令码:3: ldc // String abcdef,得益于JVM编译期的优化,两个字符串会进行相连,创建一个对象存储。
- str_03 = str_01 + "def",指令码:invokevirtual,这个就不一样了,它需要把两个字符串相连,会创建StringBuilder对象,直至最后toString:()操作,共创建了三个对象。
所以,我们看到,字符串的创建是不能被修改的,相连操作会创建出新对象。
3. intern() 3.1 经典题目String str_1 = new String(“ab”);
String str_2 = new String(“ab”);
String str_3 = “ab”;
System.out.println(str_1 == str_2);
System.out.println(str_1 == str_2.intern());
System.out.println(str_1.intern() == str_2.intern());
System.out.println(str_1 == str_3);
System.out.println(str_1.intern() == str_3);
这是一道经典的 String 字符串面试题,乍一看可能还会有点晕。答案如下;
false
false
true
false
true
看了答案有点感觉了吗,其实可能你了解方法 intern(),这里先看下它的源码;
public native String intern();
这段代码和注释什么意思呢?
native,说明 intern() 是一个本地方法,底层通过JNI调用C++语言编写的功能。
openjdk8jdksrcsharenativejavalangString.c
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
oop result = StringTable::intern(string, CHECK_NULL);
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
unsigned int hashValue = java_lang_String::hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop string = the_table()->lookup(index, name, len, hashValue);
if (string != NULL) return string;
return the_table()->basic_add(index, string_or_null, name, len,
hashValue, CHECK_NULL);
}
- 代码块有点长这里只截取了部分内容,源码可以学习开源jdk代码
- C++这段代码有点像HashMap的哈希桶+链表的数据结构,用来存放字符串,所以如果哈希值冲突严重,就会导致链表过长。
- StringTable 是一个固定长度的数组 1009 个大小,jdk1.6不可调、jdk1.7可以设置-XX:StringTableSize,按需调整。
看图说话,如下;
- 先说 ==,基础类型比对的是值,引用类型比对的是地址。另外,equal 比对的是哈希值。
- 两个new出来的对象,地址肯定不同,所以是false。
- intern(),直接把值推进了常量池,所以两个对象都做了 intern() 操作后,比对是常量池里的值。
- str_3 = "ab",赋值,JVM编译器做了优化,不会重新创建对象,直接引用常量池里的值。所以str_1.intern() == str_3,比对结果是true。
理解了这个结构,根本不需要死记硬背应对面试,让懂了就是真的懂,大脑也会跟着愉悦。
五、StringBuilder 源码分析 1. 初始化new StringBuilder();
new StringBuilder(16);
new StringBuilder(“abc”);
这几种方式都可以初始化,你可以传一个初始化容量,也可以初始化一个默认的字符串。它的源码如下;
public StringBuilder() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
定睛一看,这就是在初始化数组呀!那是不操作起来跟使用 ArrayList 似的呀!
2. 添加元素stringBuilder.append(“a”);
stringBuilder.append(“b”);
stringBuilder.append(“c”);
添加元素的操作很简单,使用 append 即可,那么它是怎么往数组中存放的呢,需要扩容吗?
2.1 入口方法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;
}
- 这个是 public final class StringBuilder extends AbstractStringBuilder,的父类与 StringBuffer 共用这个方法。
- 这里包括了容量检测、元素拷贝、记录 count 数量。
ensureCapacityInternal(count + len);
al(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
- 这个是 public final class StringBuilder extends AbstractStringBuilder,的父类与 StringBuffer 共用这个方法。
- 这里包括了容量检测、元素拷贝、记录 count 数量。
ensureCapacityInternal(count + len);
[外链图片转存中…(img-2l8ICS4y-1635180186679)]



