Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

String interning riddle

Tags:

java

string

I am struggling with the following riddle of my coworker:

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
  }
}

This outputs true. I am little bit surprised because it looks like s1 is interned. But this is no constant expression, isn't it?

But then I am even more surprised why the following prints 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
  }
}

Why does the introduction of s3 change the output?

like image 244
coder Avatar asked Sep 16 '17 20:09

coder


3 Answers

Here are the rules governing Java String objects wrt to String pool:

  1. When a String object is created using a String literal, JVM checks if the String literal is already present in the pool. If the object exists in the pool, the same object is returned instead of a new object.
  2. When a String object is created using a new operator, a new object is created even if the string exists in the string pool.
  3. When you call the intern method on a String object, a new String object is created and put on the pool if it doesn't exist. The intern method returns the object from the pool.

Let's go over your example,

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

If you look at the concat operation in String source, you will notice it calls new operator at the end.

 new String(buf, true)

Therefore, s1 is not added to the string pool.

Now, let's look at the line where intern is called,

String s2 = s1.intern();

Here, the intern method on s1 returns the object from the String pool (created if it didn't exist). So, s2 contains the object from the String pool.

Meanwhile, s1 still contains the old object and not the one in the pool. Therefore, (s1 == s2) is always going to return false.

Modified Behavior in Java 1.8.0_92-b14

The behavior in Java 8 has changed. The Java compiler is performing optimization. If the intern method is called immediately after concat, Java 8 optimizes and creates the string object in the String Pool and ignores the earlier behavior of new which we have witnessed in the earlier versions of Java. Please check the optimization in the opcodes of the decompiled code (checkOne is App1 and checkTwo is 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 ]
like image 74
Indra Basak Avatar answered Nov 03 '22 02:11

Indra Basak


The documentation for the intern method says that it is guaranteed to return a string from the pool of unique strings. It doesn't guarantee that it is the exact same string that you call intern on.

When you do String s3 = new String("Java"), you are actually interning the string literal "Java" that you pass to the constructor. This makes s1.intern() return that string instead of s1.

like image 41
marstran Avatar answered Nov 03 '22 04:11

marstran


I am on JDK 1.8.0_144. I ran your program and it prints 'false' in both the scenarios you have described. It makes complete sense. Here's why.

When you execute the statement "Ja".concat("va");, it returns a new String object. Here's the return statement from the java.lang.String#concat method:

return new String(buf, true);

Since the String returned is created using new keyword, the string is not added to the String pool. (Remember, only String literals and String resulting from constant expressions are added to the pool; the strings created using new are not).

When you create s2 as an intern of s1, it is the first time the string "Java" is added to the pool. So at that point s1 and s2 are different objects. The string s1 sits on perm gen area while s2 is in the String pool in the main part of the heap. Hence, they are not equal memory-wise. So it rightly prints false.

The introduction of the line String s3 = new String("Java"); has nothing to do with this behavior.

like image 43
VHS Avatar answered Nov 03 '22 04:11

VHS