0

I have the following code. I understand the concept of java string immutability and string constant pool. I don't understand why 'name1 == name2' results false and 'name2 == name3' results true in the following program. How are the string variables name1, name2, and name3 placed in the string constant pool?

public class Test {
    public static void main(String[] args) {
        final String firstName = "John";
        String lastName = "Smith";
        String name1 = firstName + lastName;
        String name2 = firstName + "Smith";
        String name3 = "John" + "Smith";
        System.out.println(name1 == name2);
        System.out.println(name2 == name3);
   }
}

Output:
false
true
  • 1
    final does not appear to play a role here – DontKnowMuchBut Getting Better Dec 13 '20 at 14:12
  • You shouldn't use `==` to compare strings anyway, so in the bigger picture it really doesn't matter why the results are what they are. – Kevin Anderson Dec 13 '20 at 14:19
  • Strings are always immutable. Final just means that you can't assign a new string to firstName. But that is not the problem. Try using `equals` to compare Strings not `==` – WJS Dec 13 '20 at 14:20
  • 1
    I had to reopen this question. __NOTE TO QUESTION CLOSERS__: Read more carefully! OP is not confused about how to compare strings at all, they are using `str1 == str2` explicitly because they are asking questions about java's interning mechanisms. "Same reference" __is the point__. – rzwitserloot Dec 13 '20 at 15:19
  • This definitely *has* something to do with `final`, as `final` causes the compiler to recognize the value as a *constant expression*. – MC Emperor Dec 13 '20 at 16:07

3 Answers3

1

The answer is fairly simple: As a shortcut, java treats certain concepts as a so-called 'compile time constant' (CTC). The idea is to entirely inline a variable, at the compilation level (which is extraordinary; normally javac basically just bashes your java source file into a class file using very simple and easily understood transformations, and the fancypants optimizations occur at runtime during hotspot).

For example, if you do this:

Save to UserOfBatch.java:

class BatchOConstants {
    public static final int HELLO = 5;
}

public class UserOfBatch {
    public static void main(String[] args) {
        System.out.println(BatchOConstants.HELLO);
    }
}

Run on the command line:

> javac UserOfBatch.java
> java UserOfBatch
5
> javap -c UserOfBatch # javap prints bytecode
 public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: iconst_5
       4: invokevirtual #15                 // Method java/io/PrintStream.println:(I)V
       7: return

Check out line 3 up there. iconst_5. That 5? It was hardcoded!!

No reference to BatchOConstants remains. Let's test that:

on command line:

> rm BatchOConstants.class
> java UserOfBatch
5

Wow. The code ran even though it's missing the very class file that should be providing that 5, thus proving, it was 'hardcoded' by the compiler itself and not the runtime.

Another way to 'observe' CTC-ness of any value is annoparams. An annotation parameter must be hardcoded into the class file by javac, so you can't pass expressions. Given:

public @interface Foo {
    long value();

I can't write: @Foo(System.currentTimeMillis()), because System.cTM obviously isn't a compile time constant. But I can write @Foo(SomeClass.SOME_STATIC_FINAL_LONG_FIELD) assuming that the value assigned to S_S_F_L_F is a compile time constant. If it's not, that @Foo(...) code would not compile. If it is, it will compile: CTC-ness now determines whether your code compiles or not.

There are specific rules about when the compiler is allowed to construe something as a 'compile time constant' and go on an inlining spree. For example, null is not an inline constant, ever. Oversimplifying, but:

  • For fields, the field must be static and final and have a primitive or String type, initialized on the spot (in the same breath, not later in a static block), with a constant expression, that isn't null.
  • For local variables, the rules are very similar, except, the need for them to be static is obviously waived as they cannot be. Other than that, all the fixins apply: final, primitive-or-String, non-null, initialized on the spot, and with a constant expression.

Unfortunately, CTC-ness of a local is harder to directly observe. The code you wrote is a fine way to indirectly observe it though. You've proven with your prints that firstName is CTC and lastName is not.

We can observe a select few things though. So let's take your code, compile it, and toss it at javap to witness the results. Via Jonas Konrad's online javap tool, let's analyse:

    public Main() {
        final String a = "hello";
        String b = "world";
        String c = a + "!";
        String d = b + "!";
        System.out.println(c == "hello!");
        System.out.println(d == "world!");
    }

The relevant parts:

         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: ldc           #7                  // String hello
         6: astore_1
        start local 1 // java.lang.String a
         7: ldc           #9                  // String world
         9: astore_2
        start local 2 // java.lang.String b
        10: ldc           #11                 // String hello!
        12: astore_3
        start local 3 // java.lang.String c
        13: aload_2
        14: invokedynamic #13,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
        19: astore        4
        start local 4 // java.lang.String d

Note how 'start local 2' (which is c; javap starts counting at 0) shows just loading hello! as a complete constant, but 'start local 3' (which is d) shows loading 2 constants and invoking makeConcat to tie em together.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
1

run javap -c Test after you compiled the code,

you will see that

Compiled from "Test.java"                                                                                                     
public class Test {                                                                                                           
  public Test();                                                                                                              
    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 Smith                                                                   
       2: astore_2                                                                                                            
       3: aload_2                                                                                                             
       4: invokedynamic #3,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
       9: astore_3                                                                                                            
      10: ldc           #4                  // String JohnSmith                                                               
      12: astore        4                                                                                                     
      14: ldc           #4                  // String JohnSmith                                                               
      16: astore        5                                                                                                     
      18: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;                               
      21: aload_3                                                                                                             
      22: aload         4                                                                                                     
      24: if_acmpne     31                                                                                                    
      27: iconst_1                                                                                                            
      28: goto          32                                                                                                    
      31: iconst_0                                                                                                            
      32: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V                                        
      35: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;                               
      38: aload         4                                                                                                     
      40: aload         5                                                                                                     
      42: if_acmpne     49                                                                                                    
      45: iconst_1                                                                                                            
      46: goto          50                                                                                                    
      49: iconst_0                                                                                                            
      50: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V                                        
      53: return                                                                                                              
}                                                                                                                             

As you can see

 4: invokedynamic #3,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;

in public static void main(java.lang.String[]);, this is the actual compiled byte code when running firstname + lastname. So generated String will not have the same hashcode as "JohnSmith" in constant pool.

where as on

10: ldc           #4                  // String JohnSmith

and

14: ldc           #4                  // String JohnSmith 

this is the byte code generated by the compiler when doing both firstname + "Smith" and "John" + "Smith" which means both actually reading from the constant pool.

This is the reason why when you comparing name1 with name2 using == it will return false. since name2 and name3 reference the same string from the constant pool. Hench it return true when compare with ==. This is the reason why it is not a good idea to compare 2 string with ==. Please use String.equals() when doing String comparison.

since both

MK Tan
  • 586
  • 1
  • 4
  • 12
0

Let's look at the bytecode with final:

public class Test {
  public Test();
    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           #7                  // String Smith
       2: astore_1
       3: aload_1
       4: invokedynamic #9,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
       9: astore_2
      10: ldc           #13                 // String JohnSmith
      12: astore_3
      13: ldc           #13                 // String JohnSmith
      15: astore        4
      17: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
      20: aload_2
      21: aload_3
      22: if_acmpne     29
      25: iconst_1
      26: goto          30
      29: iconst_0
      30: invokevirtual #21                 // Method java/io/PrintStream.println:(Z)V
      33: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
      36: aload_3
      37: aload         4
      39: if_acmpne     46
      42: iconst_1
      43: goto          47
      46: iconst_0
      47: invokevirtual #21                 // Method java/io/PrintStream.println:(Z)V
      50: return
}

And the bytecode without final:

public class Test {
  public Test();
    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           #7                  // String John
       2: astore_1
       3: ldc           #9                  // String Smith
       5: astore_2
       6: aload_1
       7: aload_2
       8: invokedynamic #11,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
      13: astore_3
      14: aload_1
      15: invokedynamic #15,  0             // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
      20: astore        4
      22: ldc           #18                 // String JohnSmith
      24: astore        5
      26: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
      29: aload_3
      30: aload         4
      32: if_acmpne     39
      35: iconst_1
      36: goto          40
      39: iconst_0
      40: invokevirtual #26                 // Method java/io/PrintStream.println:(Z)V
      43: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
      46: aload         4
      48: aload         5
      50: if_acmpne     57
      53: iconst_1
      54: goto          58
      57: iconst_0
      58: invokevirtual #26                 // Method java/io/PrintStream.println:(Z)V
      61: return
}

As you can see, with final, Java recognizes both the left and right hand sides of + to be constant, so it replaces the concatenation with the constant string "JohnSmith" at compile time. The only call to makeConcatWithConstants (to concatenate strings) is made for firstName + lastName, since lastName isn't final.

In the second example, there are two calls to makeConcatWithConstants, one for firstName + lastName and another for firstName + "Smith", since Java doesn't recognize firstName as a constant.

That's why name1 == name2 is false in your example: name2 is a constant "JohnSmith" in the string pool, whereas name1 is dynamically computed at runtime. However, name2 and name3 are both constants in the string pool, which is why name2 == name3 is true.

Aplet123
  • 33,825
  • 1
  • 29
  • 55