String comparison in Java
-
Last Updated: January 14, 2025
-
By: javahandson
-
Series
Learn Java in a easy way
String comparison in Java looks easy until it bites you. This guide walks through equals, equalsIgnoreCase, ==, compareTo, and compareToIgnoreCase with plain examples, so you always pick the right one.
String comparison in Java trips up more beginners than almost any other topic. You write an if statement, it works on your machine, and then it quietly fails in production. The code looks fine. The logic looks fine. Yet the branch never runs.
Nine times out of ten, the culprit is a single character: the second = in ==.
Here is the thing. A String in Java is an object, not a primitive. So the rules you learned for comparing two int values do not carry over. With numbers, == asks “same value?”. With objects, it asks a completely different question: “same box?”
Java gives you five main tools for this job. Two check content. One checks identity. Two give you an ordering for sorting. Each has a job, and picking the wrong one leads to bugs that hide well.
Let us walk through all five, one at a time, with runnable examples. By the end you will know exactly which method to call and why.
We start with why strings behave differently from numbers. Then we take each comparison method in turn. Here is the plan:
You only need to know what a String is and how to write a main method. Everything else we build up slowly.
Before we touch a single method, we need to understand what a String variable actually holds. That one idea explains almost every surprise in this article.
Think of a primitive like a sticky note. When you write int a = 5, the value 5 sits right there in the variable. Comparing two sticky notes means reading both numbers. Simple.
A String works more like a house address. The variable holds an address, not the house. The characters live somewhere else in memory, and your variable just points at them.
So what happens when you write a == b for two strings? Java compares the two addresses. It never looks inside the houses. Two identical houses on different streets give you false, even though the contents match perfectly.
That single fact drives everything else. If you care about the characters, you must call a method that reads them.
Java stores string literals in a special area called the string constant pool. Whenever the compiler sees “JavaHandsOn” in your source, it puts one copy in the pool. Every other literal with the same characters reuses that copy.
Why bother? Strings show up everywhere in real code. Sharing one copy saves a lot of memory. Since Java 7, this pool lives on the regular heap, so it grows and shrinks with the rest of your objects.
Now compare that with new String(“JavaHandsOn”). The new keyword forces a fresh object every single time. Same characters, different address.
String pooled1 = "JavaHandsOn"; // pool
String pooled2 = "JavaHandsOn"; // same pool entry
String fresh = new String("JavaHandsOn"); // brand new object
System.out.println(pooled1 == pooled2); // Output: true
System.out.println(pooled1 == fresh); // Output: falseRead that output again. Same characters in all three variables, yet == answers differently. The pool is exactly why beginners think == “usually works”.
Strings in Java never change. Call toUpperCase() and you do not modify the original. You get a brand new String back, and the old one stays exactly as it was.
This matters for comparison in two ways. First, sharing pool entries stays safe. Nobody can edit a shared literal and break every other user of it.
Second, a String caches its hash code after the first calculation. That makes strings excellent HashMap keys, and it makes repeated lookups fast.
String name = "java"; name.toUpperCase(); // result thrown away! System.out.println(name); // Output: java String upper = name.toUpperCase(); // keep the result System.out.println(upper); // Output: JAVA
Beginners hit this all the time. They call trim() or toLowerCase(), forget to assign the result, then wonder why the comparison still fails.
For a deeper look at this behaviour, see our article on String immutability in Java.
The equals method answers the question you almost always mean to ask: do these two strings hold the same characters?
String overrides equals from Object. Inside, it does three quick things. It checks whether both references point to the same object. Then it confirms the other value really is a String. Finally it walks the characters, position by position.
If every character matches and the lengths match, you get true. Otherwise you get false.
package com.javahandson.string;
public class EqualsDemo {
public static void main(String[] args) {
String str1 = "JavaHandsOn";
String str2 = "JavaHandsOn";
String str3 = new String("JavaHandsOn");
System.out.println("str1 vs str2 : " + str1.equals(str2));
System.out.println("str1 vs str3 : " + str1.equals(str3));
System.out.println("str2 vs str3 : " + str2.equals(str3));
}
}
// Output:
// str1 vs str2 : true
// str1 vs str3 : true
// str2 vs str3 : trueNotice how str3 lives on the heap while str1 and str2 share a pool entry. The equals method does not care. It reads characters, so all three match.
This is what you want in ninety-nine percent of your code.
An uppercase J and a lowercase j are different characters. So equals treats them as different, and one mismatched letter is enough to return false.
String str1 = "JavaHandsOn";
String str2 = "JAVAHandsOn";
String str3 = new String("JavaHandsOn");
System.out.println("str1 vs str2 : " + str1.equals(str2)); // Output: false
System.out.println("str1 vs str3 : " + str1.equals(str3)); // Output: true
System.out.println("str2 vs str3 : " + str2.equals(str3)); // Output: falseHere str2 shouts its first four letters. That makes it a different string, so both comparisons against it fail.
Most of the time, strict matching is exactly right. Passwords, API keys, and file checksums all need it. For usernames and email addresses you probably want the case-insensitive version instead.
The signature takes an Object, not a String. So the compiler happily lets you pass anything at all. At runtime, equals simply returns false for any non-String argument.
String text = "42";
Integer number = 42;
StringBuilder builder = new StringBuilder("42");
System.out.println(text.equals(number)); // Output: false
System.out.println(text.equals(builder)); // Output: false
System.out.println(text.equals("42")); // Output: trueThat second line catches people out. A StringBuilder holding the very same characters still fails, because it is not a String. We fix that case in section 8.2.
Passing null into equals is perfectly safe. You get false back, no exception.
Calling equals on a null reference is a different story. That throws a NullPointerException, and it is one of the most common crashes in Java code.
String known = "Java";
String maybeNull = null;
System.out.println(known.equals(maybeNull)); // Output: false
// System.out.println(maybeNull.equals(known)); // throws NullPointerException
System.out.println("Java".equals(maybeNull)); // Output: false (safe order)See the trick on the last line? Put the literal first. A literal is never null, so the call can never blow up. Developers call this the Yoda comparison, and many teams treat it as a house rule.
Sometimes case simply does not matter. A user who types ADMIN means the same thing as one who types admin. That is where equalsIgnoreCase earns its keep.
This method compares character by character, just like equals. The difference is how it decides two characters match. It folds both to uppercase and checks, then folds both to lowercase and checks again.
package com.javahandson.string;
public class IgnoreCaseDemo {
public static void main(String[] args) {
String str1 = "JavaHandsOn";
String str2 = "JAVAHANDSON";
String str3 = new String("javahandson");
System.out.println("str1 vs str2 : " + str1.equalsIgnoreCase(str2));
System.out.println("str1 vs str3 : " + str1.equalsIgnoreCase(str3));
System.out.println("str2 vs str3 : " + str2.equalsIgnoreCase(str3));
}
}
// Output:
// str1 vs str2 : true
// str1 vs str3 : true
// str2 vs str3 : trueAll three strings carry the same letters in the same order. Only the shouting differs, so every check comes back true.
Null behaves the same way as before. Pass null in and you get false, with no exception.
Use it wherever a human types the value and case carries no meaning:
Skip it for passwords. Case is real information there, and throwing it away weakens every password in your system.
Many developers write a.toLowerCase().equals(b.toLowerCase()) instead. It looks equivalent. It is not.
The toLowerCase() method with no argument uses the default locale of whatever machine runs your code. In Turkish, the capital I lowercases to a dotless ı, not to i. So the same code passes in London and fails in Istanbul.
import java.util.Locale;
Locale turkish = new Locale("tr", "TR");
System.out.println("TITLE".toLowerCase(turkish)); // Output: tıtle
System.out.println("TITLE".toLowerCase(Locale.ROOT)); // Output: title
// equalsIgnoreCase never consults the locale
System.out.println("TITLE".equalsIgnoreCase("title")); // Output: trueThe javadoc states plainly that equalsIgnoreCase ignores locale. That predictability is the whole point. Two extra allocations also disappear, since no lowercase copies get built.
Now we reach the operator behind most string bugs. It is not broken. It simply answers a question you rarely meant to ask.
The == operator asks whether two variables point to the exact same object in memory. Same address, true. Different address, false. Content plays no part.
package com.javahandson.string;
public class ReferenceDemo {
public static void main(String[] args) {
String str1 = "JavaHandsOn";
String str2 = "JavaHandsOn";
String str3 = new String("JavaHandsOn");
System.out.println("str1 == str2 : " + (str1 == str2));
System.out.println("str1 == str3 : " + (str1 == str3));
System.out.println("str1.equals(str3) : " + str1.equals(str3));
}
}
// Output:
// str1 == str2 : true
// str1 == str3 : false
// str1.equals(str3) : trueBoth literals share one pool entry, so the first check passes. The new keyword built a separate object, so the second check fails even though the letters match.
Here lies the danger. Beginners test == with two literals, watch it print true, and conclude it works. Then real input arrives from a file, a database, or a web request. Those strings never touch the pool, and the comparison starts failing.
String literal = "yes";
String fromUser = new Scanner(System.in).nextLine(); // user types: yes
if (fromUser == literal) {
System.out.println("never prints");
}
if (fromUser.equals(literal)) {
System.out.println("this one prints"); // Output: this one prints
}Nothing about the failing branch looks wrong. That is what makes the bug so nasty. It hides in code that passed every test you wrote with hard-coded values.
The rule is short. Never use == to compare string content. Not once, not as a shortcut.
Concatenation adds another wrinkle. When the compiler can work out the whole value at compile time, the result goes into the pool. When it cannot, the JVM builds a new object at runtime.
String a = "JavaHandsOn"; String b = "Java" + "HandsOn"; // folded at compile time final String finalPrefix = "Java"; String c = finalPrefix + "HandsOn"; // still a constant expression String prefix = "Java"; // not final String d = prefix + "HandsOn"; // built at runtime System.out.println(a == b); // Output: true System.out.println(a == c); // Output: true System.out.println(a == d); // Output: false System.out.println(a.equals(d)); // Output: true
Adding one final keyword flips the answer. Removing it flips it back. Would you want your login check to depend on that? Neither would we.
The intern() method asks the pool for the canonical copy of a string. If the pool already holds those characters, you get that reference back. Otherwise your string joins the pool.
String pooled = "JavaHandsOn";
String heap = new String("JavaHandsOn");
String interned = heap.intern();
System.out.println(pooled == heap); // Output: false
System.out.println(pooled == interned); // Output: true
System.out.println(heap == interned); // Output: falseInterning has a narrow, legitimate use. If you load millions of records with a small set of repeated values, interning collapses them into shared copies and saves memory.
Do not use it to make == work. That trades a clear one-line fix for a subtle rule every future reader must remember.
So far we have asked yes-or-no questions. Sorting needs more. It needs to know which string comes first, and compareTo supplies that answer.
Lexicographic ordering is dictionary ordering, with one important twist. Java compares characters by their Unicode value, not by how a human alphabet works.
Here are the values that explain most surprises:
That last pair of rules produces the classic complaint: “why is Zebra before apple?” Because Z is 90 and a is 97. Java is doing exactly what you asked.
The method scans both strings from the left until it finds a position where they differ. At that point it subtracts the second character from the first and hands you the result.
What if one string simply runs out? Then no mismatch exists, so the method subtracts the lengths instead.
package com.javahandson.string;
public class CompareToDemo {
public static void main(String[] args) {
String str1 = "Java";
String str2 = "HandsOn";
String str3 = new String("java");
System.out.println("str1 vs str2 : " + str1.compareTo(str2));
System.out.println("str1 vs str3 : " + str1.compareTo(str3));
System.out.println("str2 vs str3 : " + str2.compareTo(str3));
System.out.println("prefix case : " + str1.compareTo("JavaHandsOn"));
}
}
// Output:
// str1 vs str2 : 2
// str1 vs str3 : -32
// str2 vs str3 : -34
// prefix case : -7Let us decode each number. Java versus HandsOn differs at position zero: ‘J’ is 74, ‘H’ is 72, so you get 2.
Next, Java versus java also differs at position zero: ‘J’ is 74 and ‘j’ is 106, giving -32. Every uppercase letter sits exactly 32 below its lowercase twin.
Then HandsOn versus java gives 72 minus 106, which is -34. Finally, Java is a prefix of JavaHandsOn, so the method returns 4 minus 11, or -7.
String implements Comparable, and compareTo is the method that interface demands. That single fact makes strings sortable everywhere in Java, with no extra work from you.
import java.util.Arrays;
String[] names = {"banana", "apple", "Cherry", "Apple"};
Arrays.sort(names);
System.out.println(Arrays.toString(names));
// Output: [Apple, Cherry, apple, banana]Look at that result. Every capitalised word came first, and the alphabet got shuffled. Nobody wants a contact list ordered like that.
Sorting a TreeMap, a TreeSet, or a stream with sorted() all follow the same rule, because they all lean on compareTo by default.
A frequent bug looks like this: if (a.compareTo(b) == 1). The author assumed the method returns 1, 0, or -1. It does not, as our -32 example proved.
String a = "Java";
String b = "HandsOn";
if (a.compareTo(b) == 1) {
System.out.println("wrong test"); // never runs, value is 2
}
if (a.compareTo(b) > 0) {
System.out.println("right test"); // Output: right test
}Always test the sign with > 0, < 0, or == 0. The Comparable contract only promises a sign, so any other comparator you plug in later stays compatible.
This method does the same job as compareTo, minus the case sensitivity. It gives you the ordering humans actually expect.
Internally it folds each character before comparing, so J and j count as the same letter. Everything else behaves exactly as before.
package com.javahandson.string;
public class CompareIgnoreCaseDemo {
public static void main(String[] args) {
String str1 = "Java";
String str2 = "HandsOn";
String str3 = new String("java");
System.out.println("str1 vs str2 : " + str1.compareToIgnoreCase(str2));
System.out.println("str1 vs str3 : " + str1.compareToIgnoreCase(str3));
System.out.println("str2 vs str3 : " + str2.compareToIgnoreCase(str3));
}
}
// Output:
// str1 vs str2 : 2
// str1 vs str3 : 0
// str2 vs str3 : -2The middle line is the interesting one. Plain compareTo gave -32 for that same pair. Folding the case makes both strings identical, so you get 0.
The last line shrank from -34 to -2. After folding, ‘h’ is 104 and ‘j’ is 106, and the gap closes to two.
You rarely call compareToIgnoreCase by hand. Java ships a ready-made comparator that wraps it: String.CASE_INSENSITIVE_ORDER.
import java.util.Arrays;
String[] names = {"banana", "apple", "Cherry", "Apple"};
Arrays.sort(names, String.CASE_INSENSITIVE_ORDER);
System.out.println(Arrays.toString(names));
// Output: [apple, Apple, banana, Cherry]Compare that with the earlier output. This ordering reads like a real index, which is what a user expects to see on screen.
Notice that apple kept its place ahead of Apple. The sort treats them as equal, and Java’s object sort is stable, so their original order survives.
The big five cover most cases. A handful of extras cover the rest, and knowing them saves you from writing clumsy workarounds.
The Objects.equals helper handles nulls on both sides. Two nulls count as equal. One null gives false. Otherwise it delegates to equals.
import java.util.Objects; String a = null; String b = "Java"; System.out.println(Objects.equals(a, b)); // Output: false System.out.println(Objects.equals(a, null)); // Output: true System.out.println(Objects.equals(b, "Java")); // Output: true
Reach for this whenever both sides might be null, such as comparing two fields loaded from a database row.
Remember the StringBuilder problem from section 3.3? The contentEquals method solves it. It accepts any CharSequence and compares the characters directly.
StringBuilder builder = new StringBuilder("JavaHandsOn");
String text = "JavaHandsOn";
System.out.println(text.equals(builder)); // Output: false
System.out.println(text.contentEquals(builder)); // Output: trueYou can also flip the problem around and call builder.toString() first. That allocates a new String, though, so contentEquals stays the leaner choice. Our guides on StringBuilder in Java and StringBuffer in Java go deeper on those classes.
Often you only care about part of a string. Three methods cover that neatly:
String file = "notes.TXT";
System.out.println(file.endsWith(".txt")); // Output: false
System.out.println(file.toLowerCase().endsWith(".txt")); // Output: true
String path = "report_2026_final.txt";
// ignoreCase, start in path, other string, start in other, length
System.out.println(path.regionMatches(true, 7, "2026", 0, 4)); // Output: trueThat regionMatches overload takes an ignore-case flag as its first argument. It also avoids building substrings, which keeps tight loops fast.
When the shape of the text matters more than its exact letters, use matches. It takes a regular expression and tests the whole string against it.
String code1 = "IN-2026";
String code2 = "in-2026";
System.out.println(code1.matches("[A-Z]{2}-\\d{4}")); // Output: true
System.out.println(code2.matches("[A-Z]{2}-\\d{4}")); // Output: false
System.out.println(code2.matches("(?i)[A-Z]{2}-\\d{4}")); // Output: trueThe (?i) flag at the front switches the pattern to case-insensitive mode. Keep in mind that matches compiles the pattern on every call, so cache a Pattern object inside hot loops.
Since Java 7 you can switch on a String. Under the hood, the compiler uses hashCode() to pick a candidate branch, then confirms with equals().
String command = "start";
switch (command) {
case "start" -> System.out.println("Starting up");
case "stop" -> System.out.println("Shutting down");
default -> System.out.println("Unknown command");
}
// Output: Starting upTwo things follow from that. Matching stays case-sensitive, so normalise your input before the switch. And a null value throws a NullPointerException in a classic switch, so check for null first.
Five methods, plus a few extras, can feel like a lot. One table and one short decision path settle it.
| Method | Compares | Case? | Null argument |
|---|---|---|---|
| equals | Content | Yes | Returns false |
| equalsIgnoreCase | Content | No | Returns false |
| == | Reference | n/a | No exception |
| compareTo | Order | Yes | Throws NPE |
| compareToIgnoreCase | Order | No | Throws NPE |
| Objects.equals | Content | Yes | Safe both sides |
| contentEquals | Content | Yes | Throws NPE |
The Case? column says whether the method treats upper and lower case as different. The last column says what happens when you pass null as the argument.
Pin that table somewhere. The null column alone prevents a whole family of production crashes. Note that the first two methods return a boolean, while compareTo and compareToIgnoreCase return an int you read by sign.
Work through these questions in order and you will land on the right method every time:
Notice that == never appears. That is deliberate. Comparing string content with == has no place in application code.
Everything so far assumed plain English text. Real applications handle names, cities, and product titles from everywhere, and that raises two extra issues.
Unicode often offers two ways to spell an accented letter. You can use one composed character, or a plain letter followed by a combining accent. Both look identical on screen. Neither matches the other under equals.
import java.text.Normalizer; String composed = "caf\u00e9"; // cafe, with e-acute as ONE character String decomposed = "cafe\u0301"; // plain e plus a combining accent System.out.println(composed.length()); // Output: 4 System.out.println(decomposed.length()); // Output: 5 System.out.println(composed.equals(decomposed));// Output: false String n1 = Normalizer.normalize(composed, Normalizer.Form.NFC); String n2 = Normalizer.normalize(decomposed, Normalizer.Form.NFC); System.out.println(n1.equals(n2)); // Output: true
Normalising both sides to the same form fixes it. Do this once at the edge of your system, right where text arrives, rather than at every comparison.
Ordering has a similar problem. Because ä sits at Unicode 228, compareTo pushes it past z. A German reader expects it right next to a.
import java.text.Collator;
import java.util.Locale;
System.out.println("äpfel".compareTo("zebra")); // Output: 106 (positive!)
Collator german = Collator.getInstance(Locale.GERMAN);
System.out.println(german.compare("äpfel", "zebra") < 0); // Output: trueA Collator knows the rules of a language. Use one whenever a human reads the sorted list. Stick with compareTo for internal keys, where speed matters and nobody sees the order.
These six mistakes account for most string comparison bugs we have seen in real code reviews.
This one tops the list and always will. The code passes your quick test with two literals, then fails the moment real data arrives.
// Wrong
if (role == "admin") { ... }
// Right
if ("admin".equals(role)) { ... }Turn on your IDE’s warning for reference comparison of strings. IntelliJ and Eclipse both flag it, and that alone catches the mistake before it ships.
A value from a map, a database column, or a JSON field can arrive as null. Calling any method on it crashes immediately.
String role = map.get("role"); // may be null
// Risky
if (role.equals("admin")) { ... } // NullPointerException
// Safe
if ("admin".equals(role)) { ... } // false when role is null
// Also safe
if (Objects.equals(role, "admin")) { ... }Users paste text with stray spaces. A trailing space makes “admin “ a completely different string from “admin”, and equals reports false.
String typed = " Admin ";
System.out.println(typed.equalsIgnoreCase("admin")); // Output: false
System.out.println(typed.trim().equalsIgnoreCase("admin")); // Output: true
System.out.println(typed.strip().equalsIgnoreCase("admin"));// Output: trueSince Java 11, strip() beats trim(). The older method only removes characters up to code 32, while strip() understands the full Unicode definition of whitespace.
We covered this in section 6.4, and it deserves a second mention. Testing for exactly 1 or -1 fails for most inputs, because the method returns a character difference.
Write > 0 and < 0 instead. Your future self will thank you when someone swaps in a custom comparator.
The compiler accepts text.equals(builder) without a murmur, since equals takes an Object. At runtime the answer is always false, whatever the characters say.
Call contentEquals instead, or convert the builder with toString() before you compare.
A tempting shortcut looks like this: (x, y) -> x.toLowerCase().compareTo(y.toLowerCase()). It allocates two throwaway strings on every single comparison, and it drags the default locale into your sort order.
// Wasteful and locale-dependent list.sort((x, y) -> x.toLowerCase().compareTo(y.toLowerCase())); // Clean, allocation-free, locale-independent list.sort(String.CASE_INSENSITIVE_ORDER);
Time to put the pieces together in one small program that you could drop into a real project.
We want a tiny user directory. It must answer two questions. Is this name registered? And what does the full list look like when we print it?
Real input is messy, so the checker has to handle four things:
package com.javahandson.string;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class UserDirectory {
private static final List<String> USERS =
Arrays.asList("Suraj", "amit", "Neha", "Ravi");
static boolean isRegistered(String input) {
if (input == null) {
return false; // never call a method on null
}
String name = input.strip(); // drop stray spaces first
for (String user : USERS) {
if (user.equalsIgnoreCase(name)) {
return true; // content check, case ignored
}
}
return false;
}
static List<String> sortedForDisplay() {
List<String> copy = new ArrayList<>(USERS);
copy.sort(String.CASE_INSENSITIVE_ORDER);
return copy;
}
public static void main(String[] args) {
System.out.println(isRegistered("suraj")); // Output: true
System.out.println(isRegistered(" AMIT ")); // Output: true
System.out.println(isRegistered("Kiran")); // Output: false
System.out.println(isRegistered(null)); // Output: false
System.out.println(sortedForDisplay());
// Output: [amit, Neha, Ravi, Suraj]
}
}Four lines of defence do all the work here. The null guard comes first, so nothing downstream can crash. Then strip() cleans the input before any comparison touches it.
Next, equalsIgnoreCase matches suraj against the stored Suraj. A plain equals would have rejected both of the first two lookups.
Finally, String.CASE_INSENSITIVE_ORDER gives a display list that reads alphabetically. Plain compareTo would have printed [Neha, Ravi, Suraj, amit], pushing the lowercase entry to the end.
Swap any one of those choices and the program breaks in a way a user would notice. That is the whole lesson of this article in a single class.
A: The == operator compares references, so it asks whether both variables point to the same object in memory. The equals() method compares the actual characters. For string content, always use equals().
A: Java keeps string literals in the constant pool and reuses one copy for identical literals. So two literals with the same characters share an address, and == gives true. Build a string with new, or read it from input, and the same test gives false.
A: Call equalsIgnoreCase() for a yes-or-no check, and compareToIgnoreCase() when you need an ordering. Avoid a.toLowerCase().equals(b.toLowerCase()), because toLowerCase() uses the default locale and gives different answers on a Turkish machine.
A: It scans both strings and, at the first differing position, returns the first character minus the second. If one string is a prefix of the other, it returns the difference in lengths. Zero means the strings match. Test the sign with > 0 or < 0, never against 1.
A: A literal can never be null, so the first form never throws a NullPointerException. If role is null, it simply returns false. The second form crashes. Developers call this the Yoda comparison.
A: Use Objects.equals(a, b) from java.util. It treats two nulls as equal, returns false when only one side is null, and otherwise delegates to String.equals(). No null check of your own is needed.
A: Both give an ordering, but compareTo uses raw Unicode values, so every uppercase letter sorts before every lowercase one. compareToIgnoreCase folds the case first, which produces the alphabetical order a reader expects. For sorting, prefer String.CASE_INSENSITIVE_ORDER.
A: No. String.equals() returns false for anything that is not a String, even when the characters match. Call text.contentEquals(builder) instead, or convert the builder with toString() before comparing.
A: It uses equals(). The compiler first switches on hashCode() to pick a candidate branch, then confirms the match with equals(). Matching stays case-sensitive, and a null value throws a NullPointerException in a classic switch.
A: Three causes explain almost every case. One side has leading or trailing whitespace, so call strip() first. The case differs, so use equalsIgnoreCase(). Or an accented letter uses a different Unicode form, which Normalizer.normalize() fixes.
A: No. The intern() method exists to save memory when you hold millions of repeated values. Using it so that == compares content makes your code depend on a hidden rule that every future reader must remember. Call equals() instead.
Let us wrap up what we covered. A String variable holds an address, not the characters, so == compares addresses and answers a question you almost never mean to ask.
For content, equals is your default. Switch to equalsIgnoreCase whenever a human typed the value and the shouting carries no meaning.
For ordering, compareTo gives raw Unicode order, which puts every capital letter ahead of every lowercase one. When a person reads the list, sort with String.CASE_INSENSITIVE_ORDER instead.
Keep the small helpers close at hand too. Objects.equals survives nulls on both sides, contentEquals handles a StringBuilder, and Normalizer rescues accented text.
Clean up your input before you compare it. Strip the whitespace, settle on a case, and normalise the Unicode. Do that once at the edge, and every comparison after it becomes simple.