“What is the difference between == and equals for strings?” is often the first Java question in a screening interview. Most candidates know to use equals. Interviewers then probe deeper: “Why does "hello" == "hello" print true?”, “What does intern() do?”, or “Predict the output of these six comparisons.” Those follow-ups test whether you understand object identity, the string pool, and compile-time constant folding.
Getting this wrong in production causes intermittent bugs: a comparison that works in a unit test with literals and fails with strings read from a request or a database.
Before you start
You should know that Java variables of reference types hold references to objects, and that new always creates a new object. Recall that String is immutable, which is what makes sharing string instances safe in the first place.
The short answer
== checks whether two references point to the same object; equals checks whether two strings contain the same sequence of characters. String literals and compile-time constant expressions are interned in a shared pool, so identical literals refer to the same object and == happens to return true, but strings created at runtime by concatenation, new String, substring or I/O are separate objects. Always compare content with equals, or Objects.equals(a, b) when either may be null.
How it works
The JVM keeps a string pool (also called the intern table), which since Java 7 lives on the regular heap. When a class is loaded, every string literal in it resolves to a pooled instance, so equal literals across your whole application share one object.
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // true: both refer to the pooled literal
System.out.println(a == c); // false: new always creates a new object
System.out.println(a.equals(c)); // true: same charactersThe compiler also folds constant expressions. If every part of a concatenation is a literal or a final variable initialised with a constant, the result is computed at compile time and pooled:
final String prefix = "hel"; // compile-time constant
String nonFinal = "hel"; // not a constant variable
System.out.println(("hel" + "lo") == a); // true: folded to "hello" at compile time
System.out.println((prefix + "lo") == a); // true: prefix is a constant variable
System.out.println((nonFinal + "lo") == a); // false: concatenated at runtimeRuntime concatenation, which since Java 9 is implemented through StringConcatFactory and invokedynamic, always produces a new String object.
String.intern() returns the pooled instance for a string’s content, adding it to the pool if absent:
String runtime = nonFinal + "lo";
System.out.println(runtime.intern() == a); // trueInterning trades CPU and pool memory for the ability to compare by reference. It rarely pays off in application code; modern G1 can instead deduplicate the backing arrays of equal strings with -XX:+UseStringDeduplication without changing identity.
String.equals is fast: it returns true immediately on identical references, returns false when lengths or encodings differ, and only then compares bytes. Since Java 9, compact strings store Latin-1 content in one byte per character, which makes these comparisons cheaper still.
Step-by-step walkthrough
Step 1: Ask what the comparison means
Are you checking that two variables refer to the same object (rarely needed for strings) or that two pieces of text match (almost always)? For text, use equals.
Step 2: Make the comparison null-safe
String role = request.getParameter("role"); // may be null
if (role.equals("admin")) { } // NullPointerException when role is null
if ("admin".equals(role)) { } // safe: literal first
if (Objects.equals(role, expectedRole)) { } // safe for two variablesThe literal-first style works because a literal is never null. Objects.equals handles the case where both sides are variables.
Step 3: Choose case and locale rules explicitly
"Title".equalsIgnoreCase("TITLE"); // true
"FILE".toLowerCase(Locale.ROOT).equals("file"); // true in every localetoLowerCase() without an argument uses the default locale. In Turkish, "TITLE".toLowerCase() produces a dotless i, so comparisons against "title" fail. Use Locale.ROOT for protocol keywords and identifiers. For ordering, compareTo compares UTF-16 code units, and a Collator handles language-aware sorting.
Step 4: Compare other text types correctly
StringBuilder sb = new StringBuilder("abc");
"abc".equals(sb); // false: different types
"abc".contentEquals(sb); // true: compares characters with any CharSequenceStringBuilder does not override equals, so two builders with the same content are not equal either.
Worked scenario
A feature-flag check passed every unit test but failed in production:
static final String ENABLED = "on";
boolean isEnabled(Properties props) {
return props.getProperty("feature.export") == ENABLED;
}In tests, the properties were built with props.setProperty("feature.export", "on"). The literal "on" in the test and the constant ENABLED are the same pooled object, so == returned true. In production, the value was read from a file, and Properties.load creates new String objects from the parsed characters. Different object, same content, so == returned false and the export feature never turned on.
The fix compares content and normalises input:
boolean isEnabled(Properties props) {
String value = props.getProperty("feature.export");
return value != null && ENABLED.equals(value.trim().toLowerCase(Locale.ROOT));
}The lesson: tests built from literals can hide identity bugs, because the pool makes == behave like equals. Static analysis tools such as SpotBugs and IntelliJ inspections flag == on strings for exactly this reason.
Common mistake
- “== works for strings because of the pool.” Only for literals and constants; runtime strings are new objects.
- Calling equals on a possibly null variable. Put the literal first or use
Objects.equals. - Using toLowerCase without a locale. Results differ by default locale.
- Interning everything for speed. The pool lookup costs more than most
equalscalls saves, and unbounded interning of user input fills memory. - Comparing StringBuilder with equals. It uses identity; use
contentEqualsortoString().
Verify the behavior
Save this as StringCompare.java and run java StringCompare.java. Each comment shows the output the language rules guarantee:
public class StringCompare {
public static void main(String[] args) {
String a = "hello";
final String constant = "hel";
String variable = "hel";
System.out.println(a == "hello"); // true
System.out.println(a == new String("hello")); // false
System.out.println(a == ("hel" + "lo")); // true
System.out.println(a == (constant + "lo")); // true
System.out.println(a == (variable + "lo")); // false
System.out.println(a == (variable + "lo").intern()); // true
System.out.println(a.equals(variable + "lo")); // true
System.out.println("abc".contentEquals(new StringBuilder("abc"))); // true
}
}Follow-up questions
How many objects does new String("hello") create? One new String on the heap at runtime. The literal "hello" is a pooled instance created when the literal is first resolved, if it does not already exist, so the answer is “one or two” depending on whether the literal was already pooled.
Does switch on strings use equals? Yes. The compiler switches on hashCode() first, then confirms with equals, so it is content-based and null throws NullPointerException.
Why is String immutable? Pooling, hash code caching, thread safety and security all depend on it: a pooled literal shared across the JVM must never change.
Is equals or compareTo better for equality? equals. compareTo returning zero means equal content too, but it computes ordering and reads less clearly.
Interview exercise
Predict the output and explain each line:
String s1 = "Java";
String s2 = "Ja" + "va";
String s3 = "Ja";
String s4 = s3 + "va";
String s5 = s4.intern();
System.out.println((s1 == s2) + " " + (s1 == s4) + " " + (s1 == s5) + " " + s1.equals(s4));Answer and reasoning
It prints true false true true. s2 is a constant expression of two literals, folded at compile time to "Java" and pooled, so it is the same object as s1. s4 concatenates a non-final variable at runtime, producing a new object, so s1 == s4 is false. s4.intern() finds the pooled "Java" that s1 already refers to and returns it, so s1 == s5 is true. equals compares characters, so it is true regardless of identity. The interviewer is checking that you can tell compile-time constants from runtime values, and that you still recommend equals in real code.
Continue learning
Practise further with the Java interview questions and the Java MCQs. Related notes: equals and hashCode contracts and immutable objects and defensive copies. Method details for equals, intern and contentEquals are in the String API documentation.