0%

深入了解String,StringBuilder,StringBuffer

String 字符串常量

po出String的源码:

1
2
3
4
5
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}

这里可以看得出 String 内部实现其实就是一个不可变的char类型的数组

再来看看 String 的几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 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);
}

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;
}

从上面三个方法可以看的出 , 无论是subString还是replace亦或是concat都是重新创建了一个String字符串 , 并没有对原字符串进行修改.

在这永远要记住一点:对String对象的任何改变都不会影响到原对象,相关的任何更改操作都会生成新对象。

StringBuilder & StringBuffer

  • StringBuilder : 字符串变量 // 线程不安全
  • StringBuffer : 字符串变量 // 线程安全

那么 , 既然已经有了String作为字符串类 , 为什么还会派生出 StringBuilderStringBuffer两个类呢

先来看看下面这段代码:

1
2
3
4
5
6
7
8
9
public class StringDemo{

public static void main(String[] args){
String s = "";
for(int i = 0; i < 6666; i++){
s += "weison";
}
}
}

这里的s += "weison" , 相当于每次循环都重新声明一个字符串变量来拼接原始字符串

来看看反编译之后的代码 :
反编译的String代码

这段反编译后的class文件可以很明显的看出整个循环的执行过程 :

  1. 每次循环先new出一个StringBuilder对象
  2. 然后对StringBuilder对象进行append操作
  3. 最后将这个StringBuilder对象进行toString操作

就是最后这个toString的操作 , 这让这个循环完毕后, 整整new了6666个对象 , 可想而知 , 这些对象没有被回收的话 , 是多大的一笔开销.

从反编译后的代码可以看出 s += "weison"; 这句操作 实际上被JVM自动优化成了下面这串代码:

1
2
3
StringBuilder str = new StringBuilder(s);
str.append("weison");
s = str.toString();

再来看看下面这段代码 :

1
2
3
4
5
6
7
8
class StringDemo{
public static void main(String[] args){
StringBuilder sb = new StringBuilder();
for(int i = 0; i < 6666; i++){
sb.append("weison");
}
}
}

看看反编译后的代码 :
反编译的StringBuilder代码
这一段代码可以看得出来 , StringBuilder对象是在循环外面就已经声明了的 , 这里的for循环一直是对这个StringBuilder对象进行append操作 , 因此 这段代码的资源占用明显要比上面的小很多.

但是 , 这里有衍生出了一个新的问题 :

为什么有了StringBuilder类 , 为什么还会有一个StringBuffer类 , 看过源码的同学都知道 , StringBufferStringBuilder的成员方法和属性基本一致.

但是 ! 在StringBuffer的成员方法上 , 多了一个关键字synchronized , 这个关键字可以在多线程中起到安全保护作用 , 也就是说StringBuffer是线程安全的.

再来看看下面这份代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.weison.stringtest;

public class Test1 {

public static void main(String[] args) {
testString1();
testStringBuffer();
testStringBuilder();
testString2();
testString3();
}

public static void testString1() {
String s = "";
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
s += "weison";
}
long end = System.currentTimeMillis();
System.out.println("testString1使用时间为:" + (end - start) + "毫秒");
}

public static void testStringBuffer() {
StringBuffer sb = new StringBuffer();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
sb.append("weison");
}
long end = System.currentTimeMillis();
System.out.println("testStringBuffer使用时间为:" + (end - start) + "毫秒");
}

public static void testStringBuilder() {
StringBuilder sb = new StringBuilder();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
sb.append("weison");
}
long end = System.currentTimeMillis();
System.out.println("testStringBuilder使用时间为:" + (end - start) + "毫秒");
}

public static void testString2() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
String s = "I"+"am"+"weison";
}
long end = System.currentTimeMillis();
System.out.println("字符串直接相加操作使用时间为:" + (end - start) + "毫秒");
}
public static void testString3() {
String s1 = "I";
String s2 = "am";
String s3 = "weison";
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
String s = s1 + s2 + s3;
}
long end = System.currentTimeMillis();
System.out.println("字符串间接相加操作使用时间为:" + (end - start) + "毫秒");
}

}

测试结果 :

Stringtest result

在最开始的反编译解析中 , 有说过 s += "weison"; , JVM会自动优化 , 我们来看看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.weison.stringtest;

public class Test1 {

public static void main(String[] args) {
testString1();
testString();
}
public static void testString1() {
String s = "";
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
s += "weison";
}
long end = System.currentTimeMillis();
System.out.println("testString1使用时间为:" + (end - start) + "毫秒");
}

public static void testString(){
String s = "";
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
StringBuilder sb = new StringBuilder(s);
sb.append("weison");
s=sb.toString();
}
long end = System.currentTimeMillis();
System.out.println("模拟jvm优化操作使用时间为:" + (end - start) + "毫秒");
}
}

测试结果 :
模拟JVM的代码

两个的执行时间都相当长 , 也可以看得出 , JVM在转换中也耗费了一部分的时间.

在这里对上面那几个方法进行推测和解析 :

  • 对于直接相加的字符串 , 可以看得出来效率非常高 , 这里可以推测String字符串在编译的时候对于直接拼接的字符串会直接进行拼接, 而不是先声明出几个字符串再进行拼接 . 即 : "i" + "am" + "weison"; , 这样的字符串相加 , 会在编译的时候直接优化成 iamweison , 这里看看反编译的结果 :

  • 反编译结果 :
    Stirng拼接

  • 但是对于间接相加, 比如引用变量进行相加的 , 这种的效率要比字符串直接相加要低 , 因为编译器不会对引用变量进行优化.

  • 反编译结果 :
    Stirng引用变量拼接

  • String,StringBuilder,StringBuffer三者的执行效率:

    1. StringBuilder > StringBuffer > String
    2. 当然这个也不是一定的 : 比如像上面讲的直接拼接字符串 , 效率就肯定要比StringBuilder这种要高
    3. 在对字符串需要进行相当多的操作时候 , 可以使用StringBuilder
  • 下面在来踩几个坑:

    1. 下面这段代码输出结果?
      1
      2
      3
      String a = “hello2″;   
      String b = “hello” + 2;   
      System.out.println((a == b));
      1
      2
      输出结果为: true;
      原因很简单的 , a为hello2 , b在编译器就自动优化成了hello2 ,而hello2在之前已经被创建并加入到常量池中 , 所以b这里指向的也是常量池中的hello2 , 所以返回为 true
    2. 下面这段代码输出结果?
      1
      2
      3
      4
      String a = “hello2″;    
      String b = “hello”;
      String c = b + 2;
      System.out.println((a == c));
      1
      2
      输出结果为 : false;
      由于这里c是由引用变量b和整型2相加的 , 所以 JVM并不会自动优化 , 这里就需要重新创建对象 , 所以c创建出来的对象是保存在堆内存上的 , 两个的内存地址指向不一样 , 所以这里结果为 false
    3. 下面这段代码输出结果?
      1
      2
      3
      4
      String a = “hello2″;    
      final String b = “hello”;
      String c = b + 2;
      System.out.println((a == c));
      1
      2
      输出结果为 : true;
      由于被final修饰的变量 , 会在class文件的常量池中存在 , 也就是编译后会存在于常量池中 , 所以c = b + 2; 这里 , 在编译阶段会被JVM解释成 -> c = "hell0" + 2; 这也就回到第一个问题 , 所以这里的结果还是 true

      个人见解 , 若有不对的地方 , 可以一起讨论.