Comparing Strings: equals vs ==

Know why == on strings sometimes works and sometimes does not, and compare text safely with equals, Objects.equals and compareTo.

Syntaxa.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 b

For 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 returns false for 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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
// NullPointerException

Case-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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.