coder
coder

Reputation: 1445

String interning riddle

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?

Upvotes: 6

Views: 206

Answers (6)

Indra Basak
Indra Basak

Reputation: 7394

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 ]

Upvotes: 6

Bohemian
Bohemian

Reputation: 424983

Let's break it down.


String s3 = new String("Java");

s3 is always a new Object; it is never obtained from the intern pool.
However, the String constant "Java" that is passed to its constructor is from the intern pool (as per the JLS).


String s2 = s1.intern();

As per the javadoc, if the String isn't already in the pool, intern() adds it to the pool and returns it, otherwise it returns the String that was already there.


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

This creates a new Object "Java" without involving the pool at all.


Thus, if the string constant '"Java" is encountered before calling s1.intern(), then the string constant will be returned. Otherwise, s1 will be returned.

Upvotes: 0

Ralf Kleberhoff
Ralf Kleberhoff

Reputation: 7290

In App1, the concatenated s1 is the first "Java" String to get interned, in App2 it's the explicit literal "Java" that gets interned. Let's have a detailed look:

App1

Let's assume there's no String "Java" in the pool before running main(), [which might not be true for every JRE version].

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

Now there's a String "Java", not in the pool.

String s2 = s1.intern();

Now, "Java" is interned, in your case simply by registering your s1 instance as part of the pool. So, s1 and s2 are identical.

App2

The line

String s3 = new String("Java");

changes the initial state. You have a String literal here "Java" that becomes part of your class file and thus gets interned into the pool when your App2 class is loaded.

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

This still gives a fresh "Java" String outside of the pool, as in App1.

String s2 = s1.intern();

This finds a "Java" String already present in the pool (the String literal "Java" from loading the s3 assignment statement), which is a different instance. Hence s1 and s2 are different now, s1 the concatenated string, s2 the interned literal.

The same will happen if anywhere in your App2 class there is a String literal or constant expression "Java" that occupies the "Java" place in the pool before you intern your concatenated string.

The String being "Java" makes the case tricky, as it's somewhat likely that some JRE classes loaded before your App class might already contain a literal "Java", explaining why the behaviour is different with different JREs. Try it with something like "hmkjlfhvjkh".

Upvotes: 0

VHS
VHS

Reputation: 10184

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.

Upvotes: 2

marstran
marstran

Reputation: 27971

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.

Upvotes: 2

Joe C
Joe C

Reputation: 15684

To quote the documentation for intern (emphasis mine):

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

Upvotes: -2

Related Questions