Comparing Strings: equals vs ==
Know why == on strings sometimes works and sometimes does not, and compare text safely with equals, Objects.equals and compareTo.
a.equals(b) // same text
"admin".equals(role) // false when role is null
Objects.equals(a, b) // null-safe on both sides
a.compareTo(b) < 0 // a sorts before bFor objects, == asks whether two references point at the same object. equals asks whether two objects hold the same value. For strings you almost always want the second question: is this the same text?
Why == seems to work
String literals and compile-time constant expressions are interned: every "admin" in your code refers to one pooled object. So role == "admin" passes in a quick test where role was set from a literal. In production the value comes from a request body, a database row or substring, which create new String objects, and the same check returns false for identical text. This is a classic bug that only appears once real input arrives. Use equals.
You can force a pooled copy with intern(), but there is no reason to do that to compare values; equals is already fast and checks the length first.
null-safe comparison
role.equals("admin") throws NullPointerException when role is null, which is common for optional JSON fields. Two safe forms:
"admin".equals(role): the literal is never null, so this returnsfalsefor a null role.Objects.equals(a, b): true when both are null or both are equal, the right choice when either side might be missing.
Ignoring case and ordering
equalsIgnoreCase compares character by character ignoring case, which suits header names, URL schemes and file extensions. HTTP method names are case-sensitive, so compare those with equals. compareTo returns a negative number, zero or a positive number for sorting. It compares UTF-16 values, so every uppercase ASCII letter sorts before every lowercase one: "Zebra" comes before "apple". Use String.CASE_INSENSITIVE_ORDER for simple case-insensitive sorting, and java.text.Collator when users expect sorting by the rules of their language.
switch on strings
A switch over a String compares with hashCode and equals, not ==, so it is safe with runtime values. It throws NullPointerException for a null selector unless you add a case null label.
Secrets need a different comparison
equals stops at the first differing character, so the time it takes leaks how much of a guess was right. For API keys, webhook signatures and reset tokens, compare the bytes with MessageDigest.isEqual, which does not stop early on a mismatch.
Example
When to use it
- Check a role claim from a decoded token with "admin".equals(role) so a missing claim is a 403, not a 500.
- Match an incoming header name or file extension with equalsIgnoreCase regardless of how the client cased it.
- Verify a webhook signature header against the expected value with MessageDigest.isEqual.
More examples
null-safe comparisons
Putting the literal first, or using Objects.equals, turns a missing value into a plain false instead of a crash. Use these wherever a field is optional in the request.
void main() {
String role = null; // e.g. a missing JSON field
IO.println("admin".equals(role));
IO.println(Objects.equals(role, "admin"));
IO.println(Objects.equals(null, null));
try {
IO.println(role.equals("admin"));
} catch (NullPointerException e) {
IO.println("NullPointerException");
}
}
// Output:
// false
// false
// true
// NullPointerExceptionCase-insensitive checks and sort order
Natural order puts all capitalised names first because uppercase letters have smaller char values. CASE_INSENSITIVE_ORDER gives the order a user expects for simple English lists.
void main() {
IO.println("Content-Type".equalsIgnoreCase("content-type"));
IO.println("apple".compareTo("banana") < 0);
IO.println("Zebra".compareTo("apple") < 0);
List<String> names = new ArrayList<>(List.of("bob", "Alice", "carol", "Dave"));
names.sort(null); // natural order
IO.println(names);
names.sort(String.CASE_INSENSITIVE_ORDER);
IO.println(names);
}
// Output:
// true
// true
// true
// [Alice, Dave, bob, carol]
// [Alice, bob, carol, Dave]Comparing secrets without an early exit
MessageDigest.isEqual examines every byte rather than returning at the first difference, so response timing does not reveal how many leading characters an attacker guessed. Use it for API keys and webhook signatures.
boolean sameToken(String expected, String supplied) {
byte[] a = expected.getBytes(StandardCharsets.UTF_8);
byte[] b = supplied.getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(a, b);
}
void main() {
String expected = "fake-token-123";
IO.println(sameToken(expected, "fake-token-123"));
IO.println(sameToken(expected, "fake-token-124"));
IO.println(sameToken(expected, "x"));
}
// Output:
// true
// false
// false
Discussion