使用 new 运算符创建了多少字符串

How many Strings are getting created with the new operator

正在使用 new 运算符创建多少个字符串。

假设我正在使用 new 运算符创建一个字符串。

String str = new String("Cat")

它会创建 2 个字符串,一个在堆中,另一个在字符串池中吗?

如果它也在 string poll 中创建字符串,那么字符串 intern 方法的目的是什么?

我们可以使用intern()方法将其放入池中,或者从字符串池中引用另一个具有相同值的String对象。

如上一行,将创建 1 个或 2 个字符串。

如果池中已经有一个字符串文字“Cat”,那么只会在池中创建一个字符串“str”。如果池中没有字符串文字“Cat”,那么它会先在池中创建,然后在堆中创建space,所以一共会创建2个字符串对象

有多少对象?

Will it create 2 strings one in heap and other one is in string pool?

当您编写 "Cat" 时,您最终会用 Cat 填充池,并且 "Cat" 调用会从池中加载该对象。这通常在编译时就已经发生了。然后 new String(...) 将创建一个新的字符串对象,完全忽略池。

所以这个片段导致了两个对象的创建。为了消除您的困惑,请考虑以下示例:

String first = "Cat";
String second = "Cat";
String third = "Cat";
String fourth = new String("Cat");

这里,还创建了两个对象。所有 "Cat" 调用都会从池中加载字符串,因此 first == second == thirdfourth 将是它自己的对象,因为它使用了 new,这总是导致创建一个新对象,绕过任何类型的缓存机制。

对象是在堆上创建还是在栈上创建并没有真正定义。内存管理完全取决于 JVM。


字符串池详细信息

对于大多数 Java 实现,字符串池已在编译期间创建并填充。当你写 "Cat" 时,编译器会将一个代表 Cat 的字符串对象放入这个池中,你代码中的 "Cat" 将被替换为从池中加载这个对象。反汇编已编译程序时,您可以很容易地看到这一点。例如源代码:

public class Test {
    public static void main(String[] args) {
        String foo = "Hello World";
    }
}

反汇编(javap -v):

Classfile /C:/Users/Zabuza/Desktop/Test.class
  Last modified 30.03.2021; size 277 bytes
  SHA-256 checksum 83de8a7326af14fc95fb499af090f9b3377c56f79f2e78b34e447d66b645a285
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 59
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #9                          // Test
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // Hello World
   #8 = Utf8               Hello World
   #9 = Class              #10            // Test
  #10 = Utf8               Test
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
{
  public Test();
    descriptor: ()V
    flags: (0x0001) 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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #7                  // String Hello World
         2: astore_1
         3: return
      LineNumberTable:
        line 3: 0
        line 4: 3
}
SourceFile: "Test.java"

如你所见,有

#7 = String             #8             // Hello World
#8 = Utf8               Hello World

并且方法中的部分被替换为

0: ldc           #7

从字符串池中加载 Hello World


字符串实习

what is the purpose of string intern method?

好吧,它使您可以将您的字符串与池中的版本进行交换。并用您的字符串填充池,以防它以前不存在。例如:

String first = "hello"; // from pool
String second = new String("hello"); // new object
String third = second.intern(); // from pool, same as first

System.out.println(first == second); // false
System.out.println(first == third); // true

用例

虽然我还没有看到这个功能在现实世界中的应用。

但是,我可以想到一个用例,您可以在应用程序中动态创建可能很长的字符串,并且您知道它们稍后会再次出现。然后,您可以将字符串放入池中,以便 trim 减少稍后再次出现时的内存占用。

假设您从 HTTP 响应中收到一些长字符串,并且您知道这些响应在大多数情况下是完全相同的,并且您还想将它们收集在 List:

private List<String> responses = new ArrayList<>();

...

public void receiveResponse(String response) {
    ...
    responses.add(response);
}

如果没有实习,你最终会在你的记忆中保留每个字符串实例,包括重复项。但是,如果你实习他们,你不会在内存中有重复的字符串对象:

public void receiveResponse(String response) {
    ...
    String responseFromPool = response.intern();
    responses.add(responseFromPool);
}

当然,这有点做作,因为您也可以在这里使用 Set

new 运算符正在创建一个 String 实例。

在某个时间点创建了第二个 String 实例,以表示传递给 String(String) 构造函数的字符串文字参数。但是我们不能肯定地说它是在我们执行该语句时创建的。 (它可能已经创建。)

我们可以肯定地说:

  1. 表示文字的对象在 调用 new 运算符之前创建。
  2. 它在应用程序的生命周期内创建一次1

有趣的是,我们不能绝对肯定地说涉及“字符串池”,或者在任何时候都使用了 String.intern。 Java 15 JLS 在 section 3.10.5:

中说明了这一点

"Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.29) - are "interned" so as to share unique instances, as if by execution of the method String.intern (§12.5)."

as if by”用语表明运行时系统必须出现以特定方式运行...但是它不需要 实际上 以这种方式实施。如果有一个可行的替代方法来调用 String.intern 字符串文字,使 String 对象具有正确的唯一性属性,那也是一个可以接受的实现。根据 JLS!

(实际上,所有 Hotspot JVM do 使用字符串池,do 使用 String.intern。但这是一个“实施细节”。)


1 - 除非多次加载包含该语句的 class。

当我们创建一个String对象String s1=new String("hello");时,字符串文字"hello"存储在String常量池中,引用存储在堆内存中,s1指向该引用。 String s2="hello"; 在这种情况下,字符串文字存储在字符串常量池中,并且 s2 直接引用该地址。String s3=s1.intern(); 如果你尝试这样做,你正在取得 s3 点到存储在字符串常量池中的字符串文字的相同地址。

String s1=new String("hello");  
String s2="hello";  
String s3=s1.intern();//returns string from pool, now it will be same as s2  
System.out.println(s1==s2);//false because reference variables are pointing to different instance  
System.out.println(s2==s3);//true because reference variables are pointing to same instance  

希望对您有所帮助,如有其他问题,请post。