前面我们介绍过字符串拼接的时候效率比较低,建议使用Stringbuilder。那么有没有一种情况,字符串拼接的时候,不会降低效率呢,这里我们介绍字符串常量。先看一个demo:
public class Constant {
public static void f1() {
final String x="hello";
final String y=x+"world";
String z=x+y;
System.out.println(z);
}
public static void f2() {
final String x="hello";
String y=x+"world";
String z=x+y;
System.out.println(z);
}
}
然后这里是他们的字节码:
Classfile /F:/code/java/test/out/production/test/Constant.class
Last modified Nov 19, 2018; size 851 bytes
MD5 checksum 5b213b215e6c949bcb4fcb341381599b
Compiled from "Constant.java"
public class Constant
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#28 // java/lang/Object."<init>":()V
#2 = String #29 // hello
#3 = String #30 // helloworld
#4 = String #31 // hellohelloworld
#5 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #34.#35 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #36 // java/lang/StringBuilder
#8 = Methodref #7.#28 // java/lang/StringBuilder."<init>":()V
#9 = Methodref #7.#37 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Methodref #7.#38 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#11 = Class #39 // Constant
#12 = Class #40 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 LConstant;
#20 = Utf8 f1
#21 = Utf8 x
#22 = Utf8 Ljava/lang/String;
#23 = Utf8 y
#24 = Utf8 z
#25 = Utf8 f2
#26 = Utf8 SourceFile
#27 = Utf8 Constant.java
#28 = NameAndType #13:#14 // "<init>":()V
#29 = Utf8 hello
#30 = Utf8 helloworld
#31 = Utf8 hellohelloworld
#32 = Class #41 // java/lang/System
#33 = NameAndType #42:#43 // out:Ljava/io/PrintStream;
#34 = Class #44 // java/io/PrintStream
#35 = NameAndType #45:#46 // println:(Ljava/lang/String;)V
#36 = Utf8 java/lang/StringBuilder
#37 = NameAndType #47:#48 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#38 = NameAndType #49:#50 // toString:()Ljava/lang/String;
#39 = Utf8 Constant
#40 = Utf8 java/lang/Object
#41 = Utf8 java/lang/System
#42 = Utf8 out
#43 = Utf8 Ljava/io/PrintStream;
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
#46 = Utf8 (Ljava/lang/String;)V
#47 = Utf8 append
#48 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#49 = Utf8 toString
#50 = Utf8 ()Ljava/lang/String;
{
public Constant();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LConstant;
public static void f1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: ldc #2 // String hello
2: astore_0
3: ldc #3 // String helloworld
5: astore_1
6: ldc #4 // String hellohelloworld
8: astore_2
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_2
13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 9
line 7: 16
LocalVariableTable:
Start Length Slot Name Signature
3 14 0 x Ljava/lang/String;
6 11 1 y Ljava/lang/String;
9 8 2 z Ljava/lang/String;
public static void f2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: ldc #2 // String hello
2: astore_0
3: ldc #3 // String helloworld
5: astore_1
6: new #7 // class java/lang/StringBuilder
9: dup
10: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
13: ldc #2 // String hello
15: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_1
19: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_2
26: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_2
30: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 6
line 12: 26
line 13: 33
LocalVariableTable:
Start Length Slot Name Signature
3 31 0 x Ljava/lang/String;
6 28 1 y Ljava/lang/String;
26 8 2 z Ljava/lang/String;
}
SourceFile: "Constant.java"
我们可以看到f1()函数的字节码里面不会新建string builder,直接使用了字符串常量。而f2在String=x+y的做字符串拼接的时候,使用了new String builder。f1是因为y用了final修饰,所以直接变成了字符串常量。
被final修饰的String 叫做constant variable ,是一种编译时替换。也就是final String y=x+"world"在编译的时候y就被替换成“helloworld”了。官网解释constantvariable:https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4
A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28)
也就是被final修饰的基本类型或者String,并且用constant expression初始化的。
那么constant expression又是什么呢?常量表达式,官网的解释:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.28。我们直接看里面的Literals of primitive type and literals of type String ,也就是String literals。
String literals即String 字面常量,用双引号括起来的,0或者多个的字母。String 字面常量实际都是存到String 常量池里面的。官网解释:https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.10.5。往官网文档下看他举了一个很有意思的例子:
package testPackage;
class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((hello == "Hello") + " ");
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
在这个代码里面,分别输出true true true true false true
- 第一个,由于他们两个都是在String 常量池里面的,指向同一个,所以是相等的。
- 第二个,相等
- 第三个,相等
- 第四个,由于"hel"和"lo"都是字面常量,在常量池里面,会做编译时替换,所以变成常量了之后比较起来也相等
- 第五个就不同了,lo是变量,变量加常量不是在常量池里面的,是new 了之后放到堆里面,所以等号两边,右边是堆里面的,左边是常量池里面的,他们指向的不是同一个对象,所以不相等。
- 最后一个,intern值的是取出常量池里面的内容,如果常量池里面没有,就创建一个再返回,所以 (“Hel”+lo).intern()最终会指向常量池里面的内容,和hello是一样的。
说到intern,我们引入下一个话题。
intern官方的解释是:“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。这里又一篇写得很好的文章:https://blog.csdn.net/goldenfish1919/article/details/80410349。我截取里面一些内容分析下。
intern的返回在jdk1.6包括之前,返回的和jdk1.7包括之后的版本不太一样。因为从jdk1.7开始,字符串常量的存放位置从Perm区变化到堆区了。也就是:在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。
关于这个产生的影响,首先看一段代码:
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
在jdk1.6,是falsefalse
在jdk1.7,是falsetrue
我们首先解释jdk1.6的。
String s = new String(“1”)首先产生了两个对象,第一个是字面常量“1”,放在常量池里面,也就是Perm区里;产生的第二个对象是new String()出来的对象,存放在堆里面。
然后s.intern()之后,其实没什么变化,因为“1”在第一步的时候就在常量池里面了。
String s2=“1”自然取得是常量池里面的,所以ss2比较的时候,s取得是常量池里面的对象,s2取得是堆里面的对象,所以不相等,返回false
String s3=new String(“1”)+new String(“1”)会在堆中产生一个对象,并且值为“11”,而常量池里面没有“11”。
所以s3.intern()的时候,常量池里面就多了一个“11”。
String s4=“11”的时候,直接取常量池里面的对象。
但是s3和s4比较的时候,终究是堆里面的对象和Perm里面的常量比较,所以永远都是false的。
我们解析下jdk1.7的。
String s = new String(“1”)首先产生了两个对象,第一个是字面常量“1”,放在常量池里面,但是此时常量池在堆里面;产生的第二个对象是new String()出来的对象,存放在堆里面。
s.intern()之后,也没有什么变化,因为“1”在第一步的时候就在常量池里面了。
String s2=“1”自然取得是常量池里面的,所以ss2比较的时候,s取得是常量池里面的对象,s2取得是堆里面的对象,所以不相等,返回false
String s3=new String(“1”)+new String(“1”)会在堆中产生一个对象,并且值为“11”,而常量池里面没有“11”。
s3.intern()这一步比较关键:常量池里面没有“11”,但是堆里面有“11”,并且此时的常量池在堆里面,所以常量池里面产生一个指向堆“11”的引用,常量池实际上没有创建“11”,只是创建了引用指向堆里面的“11”
String s4=“11”的时候,取常量池里面的引用指向的内容,也就是堆里面的数据。
所以最后s3和s4比较的时候,他们是相等的。
再看一段代码更加深入一点理解:
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
这段代码只是intern凡在后面执行,jdk1.6是falsefalse,jdk1.7是falsefalse。
jdk1.6的解析还是和之前一样,他们比较的一个是堆里面的对象,一个是常量池里面的对象,所以不相等。
jdk1.7中s3==s4不在相等了,是因为在s4=“11”的时候,常量池里面是有创建“11”对象的,并不是直接指向了堆里面的对象,所以最终他们不相等。
最后对我们的String做优化的一个知识点:测试表明,在这些类型的应用里面,java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:string1.equals(string2)=true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。
去重有一些选项:
UseStringDeduplication (bool):开启String去重
PrintStringDeduplicationStatistics (bool) :打印详细的去重统计信息
StringDeduplicationAgeThreshold (uintx):达到这个年龄的String对象被认为是去重的候选对象
这里又是一篇优秀的对String去重讲解的文章:https://blog.csdn.net/goldenfish1919/article/details/20233263