字符串常量

前面我们介绍过字符串拼接的时候效率比较低,建议使用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”自然取得是常量池里面的,所以s
s2比较的时候,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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值