基于CSS3的3D立方体旋转动画

new String("test")到底创建了几个对象?

new String("test")到底创建了几个对象?

我相信不少学 Java 的小伙伴都曾被这个高频的 Java 面试题困扰过,但大家的回答都多多少少有些出入,但是总的来说,关于 new String("test") 创建对象个数的答案有 3 种:

有的人说创建了 1 个对象;有的人说创建了 2 个对象;还有的人说创建了 1 个或 2 个对象。

其实这几个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。

我们可以分几步来学习这个问题

第一步

那我们就先来了解一下字符串常量池。字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区。

对于字符串的2种创建方式,我们做如下了解: (下图通过ProcessOn网站完成,有兴趣的小伙伴可以注册一个使用,这是网站链接:https://www.processon.com/i/5fb4e8516376895bf971d05b)

方式1:
String str = "test"; 这种方法创建字符串对象的时候,jvm首先会检查 字符串常量池 中是否存在该字符串的对象,如果已经存在,那么就不会在字符串常量池中再创建了,直接 返回该字符串常量池内存中的内存地址,如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中先创建该字符串的对象,然后在返回其内存地址。

image-20210919150337174

public class Test01 {
    public static void main(String[] args) {
        String s1 = "Test";
        String s2 = "Test";       
        System.out.println(s1 == s2);
    }
}

以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。

方式2:
new String("test");这种方式创建字符串对象的时候,jvm首先会检查字符串常量池中是否存在 "test"的字符串,如果已经存在,则不会在字符串常量池中创建了,如果还没有存在,那么就会在字符串常量池中创建 "test"字符串对象,然后还会去堆内存中再创建一份字符串的对象(时刻谨记:凡是以new关键字创建的对象,jvm都会在堆内存中开辟一个新的空间,创建一个新的对象),把字符串常量池中的 "test"字符串内容拷贝至内存中的字符串对象,然后返回堆内存中字符串对象的内存地址。

image-20210919150418018

以上说法可以通过如下代码进行证明:

public class Test02 {
    public static void main(String[] args) {
        String s1 = new String("Test");
        String s2 = new String("Test");
        System.out.println(s1 == s2);
    }
}

以上程序的执行结果为:false,是因为堆上的 对象s1 和对象s2 的地址不同。

第二步

探究 new String("test") 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:

public class Test02 {
    public static void main(String[] args) {
        String s1 = new String("Test");
        String s2 = "Test";
    }
}

首先我们使用 javac Test02.java 编译代码,然后我们再使用 javap -v Test02 查看编译的结果,相关信息如下:我的环境为 "1.8.0_121".

警告: 二进制文件Test02包含com.cqyti.stx.str.Test02
Classfile /E:/IDEA project/Training/src/com/cqyti/stx/str/Test02.class
  Last modified 2021-9-19; size 362 bytes
  MD5 checksum 0e430f2c1b03d2597be3cf5d03f449d3
  Compiled from "Test02.java"
public class com.cqyti.stx.str.Test02
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:                            // Constant pool是常量池
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Class              #16            // java/lang/String
   #3 = String             #17            // Test
   #4 = Methodref          #2.#18         // java/lang/String."<init>":(Ljava/lang/String;)V
   #5 = Class              #19            // com/cqyti/stx/str/Test02
   #6 = Class              #20            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Test02.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Utf8               java/lang/String
  #17 = Utf8               Test
  #18 = NameAndType        #7:#21         // "<init>":(Ljava/lang/String;)V
  #19 = Utf8               com/cqyti/stx/str/Test02
  #20 = Utf8               java/lang/Object
  #21 = Utf8               (Ljava/lang/String;)V
{
  public com.cqyti.stx.str.Test02();
    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 7: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String Test
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: ldc           #3                  // String Test
        12: astore_2
        13: return
      LineNumberTable:
        line 42: 0
        line 43: 10
        line 45: 13
}
SourceFile: "Test02.java"

其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("Test"); 定义的"Test"字符,在信息 #17 = Utf8 Test 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。

再进一步

我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:

public static void main(String[] args) {
      
        String a = "test";
        String b = "test";
        String c = new String("test");
        String d = "te" + "st";
        String e = "te";
        String f = "st";
        String g = e + f;
        System.out.println(a == b);//        true
        System.out.println(a.equals(b));//   true
    
        System.out.println(c == b);//        false
        System.out.println(a.equals(c));//   true
    
        System.out.println(d == a);//        true
        System.out.println(d == b);//        true        
        System.out.println(a.equals(d));//   true
    
        System.out.println(g == a);//        false
        System.out.println(a.equals(g));//   true
}

按照 String 不能被修改的思想来看,d 应该会在字符串常量池创建两个字符串"te" 和 "st",d == a 与 d == b 的结果也应该是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。

同样我们使用 javac Test02.java 先编译代码,再使用 javap -c Test02 命令查看编译的代码如下:

警告: 二进制文件Test02包含com.cqyti.stx.str.Test02
Compiled from "Test02.java"
public class com.cqyti.stx.str.Test02 {
  public com.cqyti.stx.str.Test02();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String test
       2: astore_1
       3: ldc           #2                  // String test
       5: astore_2
       6: new           #3                  // class java/lang/String
       9: dup
      10: ldc           #2                  // String test
      12: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      15: astore_3
      16: ldc           #2                  // String test
      18: astore        4
      20: ldc           #5                  // String te
      22: astore        5
      24: ldc           #6                  // String st
      26: astore        6
      28: new           #7                  // class java/lang/StringBuilder
      31: dup
      32: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
      35: aload         5
      37: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      40: aload         6
      42: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      45: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      48: astore        7
      50: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
      53: aload_1
      54: aload_2
      55: if_acmpne     62
      58: iconst_1
      59: goto          63
      62: iconst_0
      63: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
      66: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
      69: aload_1
      70: aload_2
      71: invokevirtual #13                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      74: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
      77: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
      80: aload_3
      81: aload_2
      82: if_acmpne     89
      85: iconst_1
      86: goto          90
      89: iconst_0
      90: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
      93: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
      96: aload_1
      97: aload_3
      98: invokevirtual #13                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     101: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
     104: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
     107: aload         4
     109: aload_1
     110: if_acmpne     117
     113: iconst_1
     114: goto          118
     117: iconst_0
     118: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
     121: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
     124: aload_1
     125: aload         4
     127: invokevirtual #13                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     130: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
     133: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
     136: aload         7
     138: aload_1
     139: if_acmpne     146
     142: iconst_1
     143: goto          147
     146: iconst_0
     147: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
     150: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
     153: aload_1
     154: aload         7
     156: invokevirtual #13                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     159: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
     162: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
     165: aload         4
     167: aload_2
     168: if_acmpne     175
     171: iconst_1
     172: goto          176
     175: iconst_0
     176: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
     179: return
}

从 Code 16: ldc #2 行可以看出字符串都被编译器优化成了字符串"test"了。

总结

我们可以通过 javap -c XXX 查看文件字节码信息, javap -v XXX 的方式查看编译结果的过程中发现 new String("test") 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。所以在学习过程中我们需要具体问题,具体分析。

posted @ 2021-09-19 16:19  只会写error  阅读(262)  评论(0)    收藏  举报