字符串实习谜语

时间:2017-09-16 20:00:07

标签: java string

我正在与下面的同事谜语挣扎:

public class App1 {
  public static void main(String[] args) {
    String s1 = "Ja".concat("va"); // seems to be interned?!
    String s2 = s1.intern();

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

输出 true 。我有点惊讶,因为看起来s1 实习。但这不是常数表达,不是吗?

但后来我更加惊讶为什么以下打印 false

public class App2 {
  public static void main(String[] args) {
    String s1 = "Ja".concat("va"); // seems not to be interned?!
    String s3 = new String("Java"); // this changes output
    String s2 = s1.intern();

    System.out.println(s1 == s2); // false
  }
}

为什么引入s3会改变输出?

6 个答案:

答案 0 :(得分:6)

以下是管理Java String对象的规则,以及字符串池

  1. 使用String literal 创建String对象时,JVM会检查池中是否已存在字符串文字。如果池中存在该对象,则返回相同的对象而不是新对象。
  2. 使用 new 运算符创建String对象时,即使字符串池中存在字符串,也会创建新对象。
  3. 当您在String对象上调用实习生方法时,会创建一个新的String对象并将其放在(如果它不存在)上。 intern方法从池中返回对象。
  4. 让我们回顾一下你的例子,

    String s1 = "Ja".concat("va");
    

    如果您查看字符串源代码中的concat操作,您会注意到它最后会调用new运算符。

     new String(buf, true)
    

    因此,s1未添加到字符串池中。

    现在,让我们看看调用intern的行

    String s2 = s1.intern();
    

    这里,intern上的s1方法返回String池中的对象(如果它不存在则创建)。因此,s2包含String池中的对象。

    同时,s1仍然包含旧对象,而不是池中的对象。因此,     (s1 == s2) 总是会返回false

    Java 1.8.0_92-b14中的修改行为

    Java 8中的行为已更改。 Java编译器正在执行优化。如果在intern之后立即调用concat方法,Java 8将优化并在字符串池中创建字符串对象,并忽略我们在早期版本中见过的new的早期行为。 Java的。请检查反编译代码的操作码中的优化(checkOne是App1,checkTwo是App2),

      public static void checkOne();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=2, args_size=0
             0: ldc           #2                  // String Ja
             2: ldc           #3                  // String va
             4: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
             7: astore_0
             8: aload_0
             9: invokevirtual #5                  // Method java/lang/String.intern:()Ljava/lang/String;
            12: astore_1
            13: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
            16: aload_0
            17: aload_1
            18: if_acmpne     25
            21: iconst_1
            22: goto          26
            25: iconst_0
            26: invokevirtual #7                  // Method java/io/PrintStream.println:(Z)V
            29: return
          LineNumberTable:
            line 6: 0
            line 7: 8
            line 9: 13
            line 10: 29
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                8      22     0    s1   Ljava/lang/String;
               13      17     1    s2   Ljava/lang/String;
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 25
              locals = [ class java/lang/String, class java/lang/String ]
              stack = [ class java/io/PrintStream ]
            frame_type = 255 /* full_frame */
              offset_delta = 0
              locals = [ class java/lang/String, class java/lang/String ]
              stack = [ class java/io/PrintStream, int ]
    
      public static void checkTwo();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=3, args_size=0
             0: ldc           #2                  // String Ja
             2: ldc           #3                  // String va
             4: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
             7: astore_0
             8: new           #8                  // class java/lang/String
            11: dup
            12: ldc           #9                  // String Java
            14: invokespecial #10                 // Method java/lang/String."":(Ljava/lang/String;)V
            17: astore_1
            18: aload_0
            19: invokevirtual #5                  // Method java/lang/String.intern:()Ljava/lang/String;
            22: astore_2
            23: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
            26: aload_0
            27: aload_2
            28: if_acmpne     35
            31: iconst_1
            32: goto          36
            35: iconst_0
            36: invokevirtual #7                  // Method java/io/PrintStream.println:(Z)V
            39: return
          LineNumberTable:
            line 13: 0
            line 14: 8
            line 15: 18
            line 17: 23
            line 18: 39
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                8      32     0    s1   Ljava/lang/String;
               18      22     1    s3   Ljava/lang/String;
               23      17     2    s2   Ljava/lang/String;
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 35
              locals = [ class java/lang/String, class java/lang/String, class java/lang/String ]
              stack = [ class java/io/PrintStream ]
            frame_type = 255 /* full_frame */
              offset_delta = 0
              locals = [ class java/lang/String, class java/lang/String, class java/lang/String ]
              stack = [ class java/io/PrintStream, int ]
    

答案 1 :(得分:2)

intern方法的文档说明保证从唯一字符串池中返回一个字符串。它并不能保证它与您实习的内容完全相同。

执行String s3 = new String("Java")时,实际上是在传递给传递给构造函数的字符串文字"Java"。这使s1.intern()返回该字符串而不是s1

答案 2 :(得分:2)

我在JDK 1.8.0_144上。我运行你的程序,它在你描述的两个场景中打印'false'。它完全有道理。这就是原因。

执行语句"Ja".concat("va");时,它返回new String object。以下是return方法的java.lang.String#concat语句:

return new String(buf, true);

由于返回的String是使用new关键字创建的,因此字符串不会添加到字符串池中。 (请记住,只有String literals和由常量表达式生成的String被添加到池中;使用new创建的字符串不是)。

当您将s2创建为s1的实习生时,它是第一次将字符串“Java”添加到池中。所以在那一点上,s1和s2是不同的对象。字符串s1位于perm gen区域,而s2位于堆的主要部分的String池中。因此,它们在记忆方面并不相同。所以它正确打印错误。

String s3 = new String("Java");的引入与此行为无关。

答案 3 :(得分:0)

在App1中,连接的s1是第一个" Java"要获得实习的字符串,在App2中它是显式文字" Java"得到实习。让我们详细了解一下:

App1的

让我们假设没有字符串" Java"在运行main()之前的池中,[对于每个JRE版本可能都不是这样]。

String s1 = "Ja".concat("va");

现在有一个String" Java",不在池中。

String s2 = s1.intern();

现在," Java"在您的情况下,只需注册您的s1实例作为池的一部分即可实现。因此,s1s2相同。

App2的

该行

String s3 = new String("Java");

更改初始状态。你在这里有一个字符串文字" Java"它成为您的类文件的一部分,因此在加载App2类时会被插入到池中。

String s1 = "Ja".concat("va");

这仍然提供了一个新的" Java"池外的字符串,如App1中那样。

String s2 = s1.intern();

这找到了一个" Java"字符串已经存在于池中(String literal" Java"来自加载s3赋值语句),这是一个不同的实例。因此s1和s2现在不同了,s1是连接字符串,s2是实习文字。

如果App2类中的任何地方存在字符串文字或常量表达式,那么同样的情况也会发生。" Java"占据了#Java; Java"在你实习你的连锁字符串之前放在池中。

字符串是" Java"使案例变得棘手,因为在App类可能已经包含文字" Java"之前加载一些JRE类有点可能,这解释了为什么不同JRE的行为不同。尝试类似" hmkjlfhvjkh"。

答案 4 :(得分:0)

让我们分解。

String s3 = new String("Java");

s3 总是新的Object;它永远不会从实习生池中获得 但是,从实习池(根据JLS)传递给其构造函数的String常量"Java"

String s2 = s1.intern();

根据javadoc,如果字符串已经不在池中,intern()会将其添加到池中并返回它,否则它将返回已存在的字符串。< / p>

String s1 = "Ja".concat("va");

这会创建一个新的对象"Java" ,而根本不涉及该池。

因此,如果在调用'"Java"之前遇到字符串常量s1.intern(),则将返回字符串常量。否则,将返回s1

答案 5 :(得分:-2)

引用intern(强调我的)的文档:

  

当调用intern方法时,如果池已经包含等于String方法确定的equals(Object)对象的字符串,则返回池中的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用