In this post, I will try to demonstrate the rationale behind overriding java.lang.Object’s equals() method in java.lang.String class and why it’s recommended to call String’s equals() instead of == operator to check String equality. By the end of this post, it should be clear when to use == and equals() to do equality checks. Also, it should be clear why you have to write your own implementation logic in overridden equals() method in custom objects like Employee, Student, Person etc.

Referential Equality in Strings

In Java, we have equality == operator to check if two references of Primitive/Object types are pointing/referring to the same object in memory, and that’s Referential Equality

  • Referential Equality check for int primitive type:

      int x = 10;
      int y = 10;
      System.out.println(x==y);
    

    The above code verifies that x and y are pointing to the same memory address. That’s referential equality.

  • Referential Equality check for java.lang.String

    String x = "hello"; // String literal declaration
    String y = "hello"
    System.out.println(x==y);
    

    The above code returns true as they are string literals and variables x and y are referring to the same string “hello” in the string constant pool, which is a special area in JVM memory.

  • After a slight alteration to the above code, let’s examine the output.

    String x = "hello"; // String literal declaration
    String y = new String("hello"); // String object declaration
    /* Note: According to sonar rules the above string initialization
    is a code smell 
    Refer: https://rules.sonarsource.com/java/RSPEC-2129 */
    System.out.println(x==y);
    /*Note: According to sonar rules the above statement is a code smell
    Refer: https://rules.sonarsource.com/java/RSPEC-1698
    */
    
    • The above snippet prints false. There are 2 objects created in the above use case, one in constant pool and one in heap area. Because in the first declaration x is a representation of string literal, meaning the value "hello" is placed in String constant pool a special area in your JVM memory. And, in the second declaration it is a String object wherein it is placed in java heap memory. So x==y obviously returns false as x and y are pointing to different memory addresses one in string constant pool and other in heap memory
  • Note:

    • Myth: x==y does hashCode() comparison.
    • Fact: Referential equality checks if 2 variables/reference types are pointing to the same memory address.

Overview of String Constant Pool

It’s a special area in JVM memory in which string literals are placed that are referred by one or more variables/reference types.

  • Example:

      String x = "hello";
      String y = "hello";
      System.out.println(x==y);
    
  • There is only one object created in string constant pool. The variable x and y are referring to the same memory address in String constant pool and that memory address contains value "hello".
  • Assume that the constant pool is empty initially. Now, when String x is initialized with value "hello" using literal declaration, it will create a new String in constant pool at some memory address.
  • Subsequently, after y is initialized with String literal representation, then it checks if there is already a string "hello" in constant pool, if yes it points to it, otherwise creates one in constant pool. In this scenario as "hello" already exists in constant pool y refers/points to existing "hello"
  • Hence, x==y resolves to true as they both are referring or pointing to the same memory address in string constant pool.

Interning a String object

  • intern() ensures that your String reference points to a memory address in string constant pool. Create new ones in constant pool if not already present otherwise reuses the existing string from constant pool. But the point is that it changes the String reference address from heap memory to string constant pool.
  • Example:

      String x = new String("hello");
      x = x.intern(); // Ensures that the reference points to string in constant pool
      String y = "hello"; // Resuses string in constant pool otherwise creates one
      System.out.println(x==y);
    
  • There are 2 objects created in the above use case, one in heap area, then a string literal in constant pool. In the above example, x==y prints true because initially "hello" string object is created in heap memory and after intern() invocation it checks if there is already a literal "hello" in constant pool if yes it will point to that memory address thereby reusing the existing one, otherwise it creates a new literal in constant pool and variable x now points to new memory address in constant pool. So now when you initialize y through literal representation, since "hello" is already present in constant pool it just reuses the literal, points to that memory location/address and hence x==y prints true.

So far, we are clear on how == works, and we have got a brief understanding of String constant pool.

But, the way we have checked string equality using == operator in the above snippets is a bad practice and results in inconsistent and functionally incorrect results. Why? Because, ideally String x = "srk" and String y = new String("srk"); should be equal as long as they have the same value srk, and it shouldn’t matter if the string is stored in Constant Pool or Heap Memory.


Why java.lang.String class has overriden java.lang.Object’s equals() method?

Let’s assume for a moment that String class has not overridden equals() method. And consider the following snippet for string equality check

// Assumption: String class has not overridden java.lang.Object's equal() method
String x = new String("srk");
String y = "srk";
System.out.println(x.equals(y)); 

The above snippet prints false as it calls java.lang.Object’s equals() method, which does referential equality check.

  • Here is the java.lang.Object class’s default equals() implementation

    • public boolean equals(Object obj) {
        return (this == obj);
      }
      

The string equality example above is functionally incorrect as it says that both strings are not equal though they have the same value “srk”

To correct such scenarios, java.lang.String class in java has overridden java.lang.Object class’s equals() method.

Now, the reality is that the java.lang.String class has overridden the java.lang.Object's equals() method. So, if we rerun the below snippet, it will print true.

String x = new String("srk");
String y = "srk";
System.out.println(x.equals(y));

Hence, the sonar rule: ”==” and “!=” should not be used when “equals” is overridden

In summary

Never do String equality check with == operator

Always do String equality check with java.lang.String’s overridden equals() method

Also, do override equals() method in your custom objects like an Employee, Person, Student, and call overridden equals() method. Not doing so, will just call Object’s equals() method which does referential equality check.


References:

  1. java.lang.Object equals() Javadoc
  2. java.lang.String equals() Javadoc
  3. Referential Equality
  4. String intern() Javadoc
  5. SonarRule: Strings and Boxed types should be compared using “equals()”
  6. SonarRule: Constructors should not be used to instantiate “String”, “BigInteger”, “BigDecimal” and primitive-wrapper classes