Java String常见问题

首先看一段代码块:

1
2
3
4
5
6
7
8
9
public String gets1() {
return "" + System.currentTimeMillis();
}
public String gets2() {
return String.valueOf(System.currentTimeMillis());
}
public String gets3() {
return new String("") + System.currentTimeMillis();
}

这三个方法都是输出当前时间戳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
public java.lang.String gets1();
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: ldc #4 // String
9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
15: invokevirtual #7 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
18: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: areturn'

public java.lang.String gets2();
Code:
0: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
3: invokestatic #9 // Method java/lang/String.valueOf:(J)Ljava/lang/String;
6: areturn
public java.lang.String gets3();
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #10 // class java/lang/String
10: dup
11: ldc #4 // String
13: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
22: invokevirtual #7 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
25: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
28: areturn
  • 方法1:””+System.currentTimeMillis()是通过StringBuilder的append方法添加了一个空字符串及当前时间戳,然后通过toString方法生成一个全新的String对象。

该方法中使用了一个StringBuilder和一个String对象。

  • 方法2:String.valueOf(System.currentTimeMillis())直接获取当前时间戳,然后创建了一个String对象。

该方法中使用了一个String对象。

  • 方法3:new String(“”) + System.currentTimeMillis()则最麻烦,先创建了StringBuilder对象,然后新建了一个空字符串的String对象,通过append方法添加进去,最后toString创建了一个新的String对象。

该方法中使用了一个StringBuilder对象和两个String对象。

从上面的分析中就可以看出,很明显方法2是最优的,为什么呢?在深入String之前,先了解下JVM的组成。

  • 堆:JVM运行中申请的对象存放的位置。也就是所说的新生代+老年代。
  • 虚拟机栈:每个方法在被调用执行时都会创建一个虚拟机栈,用于存储临时的遍历、方法等信息。调用相当于进栈,返回结果则相当于出栈,异常输出的栈信息就是从这里来的。
  • 本地方法栈:也是方法调用,只是调用的方法是本地native方法。
  • 方法区:存储的类的结构信息,静态变量等信息。也就是永久代,发生FULL GC的地方(Java8中以元空间代替了永久代这个概念)。
  • 常量池:方法区的一部分,用来存放各种生成的字面量,例如定义的一个String类型的数据(Java7中把常量池移到了堆中)

0. String的特性

1、String类是final的,不可被继承。
2、String类是的本质是字符数组char[], 并且其值不可改变。
3、Java运行时会维护一个String Pool(String池),JavaDoc翻译很模糊“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象仅仅存在于方法的堆栈区。

1. 创建字符串
创建字符串一般就两种方法:

1
2
3
String s1="hello";

String s2=new String("hello");

第一种方法是在栈中创建一个String类型的引用s1,然后在常量池中寻找,如果常量池中存在hello的字符串数据,则直接把s1指向常量池中hello的地址;
否则就会在常量池中创建hello这个字符串数据,然后把s1指向新创建的hello地址。

第二种方法是在栈中创建一个String类型的引用s2,然后在常量池中寻找,如果常量池中存在该数据,则在堆中复制拷贝该数据,然后把s2指向堆中新建的地址;
否则会创建一个字符串存放在常量池中,然后进行拷贝一份到堆中,s2指向堆中的地址。

通过new创建的string一定会在堆中创建一份数据,同时常量池肯定有一个值的备份操作;而单独的字符串则是直接指向常量池,所以一般还是使用字符串更好些,具体可看如下的图解:
img

2. 字符串 + 操作

1
2
3
4
5
6
7
8
9
10
11
String s1="helloworld";

String s2="hello" + "world";

String s3=new String("hello") + "world";

System.out.println(s1 == s2); // true

System.out.println(s2 == s3); // false

System.out.println(s1 == s3); // false

代码中”hello” + “world”在编译期已经知道了数据情况(使用javac编译查看class文件会发现s1和s2是一致的),JVM会自动优化使得s1和s2是一样的,s3由于有new操作,所以需要StringBuilder的append完成,具体可看如下图解:
img

3. 字符串变量 + 操作

1
2
3
4
5
6
7
8
9
String s1="hello";

String s2="world";

String s3="helloworld";

String s4=s1 + s2;

System.out.println(s3 == s4); // false

这里的样例和上一个样例存在一些差别,这里的s4是由s1 + s2获得的,在编译期无法感知到其实际值,在运行期时会利用StringBuilder的append剩下一个新的String对象,所以s3指向的是常量池,而s4指向的堆,两者自然是不一样的。

1
s4 = new StringBuilder()).append(s1).append(s2).toString();

5. 带final的字符串变量 + 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
final String s1="hello";

String s2="world";

String s3="helloworld";

String s4=s1 + "world";

String s5=s1 + s2;

System.out.println(s3 == s4); // true

System.out.println(s3 == s5); // false

使用javac反编译之后的class文件如下:

1
2
3
4
5
6
7
8
9
10
11
String var1 = "world";

String var2 = "helloworld";

String var3 = "helloworld";

String var4 = "hello" + var1;

System.out.println(var2 == var3);

System.out.println(var2 == var4);

添加了final关键字修饰的变量在编译期会被对应的字符串直接替换掉,相当于字符串数据,而包含了字符串变量的+操作则依旧是使用了StringBuilder。

5. intern()方法
String.intern方法是一个native方法,获取的是当前字符串在常量池的数据,如果常量池存在该数据则直接返回,如果不存在则把该数据添加到常量池中后返回,所以有String s1 = “abc” 和 String s2 = s1.intern() 中的s1 == s2和String s1=”world” 和 String s2=new String(“world”) 中 s1.intern() == s2.intern()

6. 总结

String 本身是final类型的类,在日常使用中需要频繁的做字符串合并操作时,尽可能的使用StringBuilder(如需要考虑线程安全则使用StringBuffer),降低无谓的字符串创建操作,在保证安全的情况下,提高效率!

本文标题:Java String常见问题

文章作者:王洪博

发布时间:2018年03月15日 - 10:03

最后更新:2020年02月10日 - 03:02

原始链接:http://whb1990.github.io/posts/d0a49d51.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%