AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

03 Strings / Lesson 2 of 7

Comparing strings — equals, compareTo and hashCode

12 min · +10 XP · 2 exercises · 5 questions

Read the text12 min
Exercises0/2
Quiz5 q

Quick answer

What is the difference between == and equals() for strings in Java?

For strings in Java, == checks whether two references point to the same object, while equals() compares the characters. Strings read from input or built at run time are new objects, so input == "yes" is false even when the user typed yes. Always compare content with equals, or with equalsIgnoreCase to ignore case.

Comparing strings correctly is a rite of passage: every Java programmer has shipped a == that worked in tests and failed with real input. This lesson makes the rules precise, and then goes further into ordering (compareTo), case-insensitive comparison, and the hashCode contract that lets strings work as map keys.

== compares references

String input = new Scanner(System.in).next();   // user typed "yes"
if (input == "yes") { … }                        // FALSE even when they typed yes
if (input.equals("yes")) { … }                   // correct

== on two references asks "are these the same object?" — and a string read from input, built by concatenation, or returned by most methods is a fresh object, never the pooled literal. The comparison compiles, runs, and is silently wrong.

equals: content, case-sensitive

"Java".equals("Java")        // true
"Java".equals("java")        // false
"Java".equals(null)          // false — never throws
"Java".equalsIgnoreCase("JAVA")   // true

String.equals checks: same object → true; not a String → false; different length → false; then compares character by character. It is O(n) in the worst case and short-circuits on the first difference. Its null handling is why the idiom is "constant".equals(variable), or Objects.equals(a, b) when both may be null.

equalsIgnoreCase compares after folding case per character (using both Character.toUpperCase and toLowerCase, which handles most alphabets). For locale-sensitive folding — Turkish dotless i, German ß — use a Collator; for programming identifiers and protocol keywords, equalsIgnoreCase is right.

compareTo: lexicographic ordering

String implements Comparable<String>, so strings sort naturally:

"apple".compareTo("banana")   // negative (a < b)
"banana".compareTo("apple")   // positive
"apple".compareTo("apple")    // 0
"apple".compareTo("applesauce")   // negative: shorter prefix sorts first (returns -5, the length difference)
"Zebra".compareTo("apple")    // negative! 'Z' (90) < 'a' (97) — uppercase sorts before lowercase
"10".compareTo("9")           // negative: '1' < '9' — strings compare character by character, not numerically

The rule: compare characters at each index by their UTF-16 code unit values; at the first difference return this.charAt(i) - other.charAt(i); if one is a prefix of the other return the length difference. The sign is all you should rely on — the magnitude is an implementation detail.

Consequences you must know:

  • Uppercase before lowercase (ASCII order). compareToIgnoreCase fixes that for sorting names.
  • Digits compare as characters: "10" < "9". To sort numerically, parse, or use a comparator on Integer.parseInt.
  • Accented characters sort by code point, which is rarely what a user expects; java.text.Collator gives locale-correct ordering.

Collections.sort(list) and Arrays.sort(strings) use compareTo; list.sort(String.CASE_INSENSITIVE_ORDER) uses the case-insensitive comparator built into String.

hashCode: the map-key contract

"hello".hashCode()      // 99162322

String.hashCode() is s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1], computed with int overflow — specified in the Javadoc, so it is the same on every JVM. It is cached in the object after the first call.

The contract that every equals override must obey (Module 8) is visible here: two strings that are equals always have the same hashCode, which is what lets HashMap<String, V> find a key that was inserted as a different object with the same characters. "hello".hashCode() == new String("hello").hashCode() — always.

Because 31 is small and the arithmetic wraps, collisions exist ("Aa" and "BB" both hash to 2112); HashMap handles collisions, so this is trivia, not a problem — until someone sends a million colliding keys as a denial-of-service, which is why Java 8 turned overfull buckets into trees.

contentEquals, regionMatches, matches

  • s.contentEquals(sb) — compare a String with a StringBuilder or any CharSequence without converting.
  • s.regionMatches(ignoreCase, offset, other, otherOffset, len) — compare a substring without allocating one.
  • s.matches(regex) — full-match against a regular expression (lesson 7). "abc".matches("a.*") is true; note it matches the whole string.
  • s.startsWith(prefix), s.endsWith(suffix), s.contains(sub) — the everyday tests; contains is indexOf(sub) >= 0.

switch on strings

switch (command) {
    case "start" -> start();
    case "stop"  -> stop();
    default      -> System.out.println("unknown: " + command);
}

Since Java 7, switch accepts a String. It is compiled into a hashCode lookup followed by an equals check, so it is content-based and case-sensitive, and a null selector throws NullPointerException.

A comparison checklist

WantUse
Same charactersa.equals(b)
Same characters, any casea.equalsIgnoreCase(b)
Either may be nullObjects.equals(a, b)
Orderinga.compareTo(b), or String.CASE_INSENSITIVE_ORDER
Locale-correct orderingCollator.getInstance(locale).compare(a, b)
Same objecta == b — and ask yourself why

Interview angle

  • "String s = "hi"; String t = new String("hi"); s == t?" False. "s.equals(t)?" True. "s == t.intern()?" True.
  • "What does "apple".compareTo("banana") return?" A negative number (−1: 'a' − 'b'). Only the sign is contractual.
  • "Why must equals and hashCode agree?" Hash-based collections find the bucket by hashCode and then the entry by equals; disagreement makes keys unfindable.
  • "Is switch on a string by reference?" No — by hashCode then equals.

Key takeaways

  • == compares references and is wrong for strings; equals compares content.
  • "literal".equals(var) and Objects.equals are null-safe.
  • compareTo is character-by-character by code unit: uppercase first, digits as text.
  • hashCode is specified, cached, and agrees with equals — the basis of HashMap keys.
  • switch on strings is content-based and throws on null.

Common questions

How does compareTo work for strings in Java?

It compares two strings character by character by UTF-16 code unit and returns a negative number, zero or a positive number. At the first difference it returns the difference of the two characters; if one string is a prefix of the other, the difference in length. Only the sign is guaranteed.

Why does "Zebra" sort before "apple" in Java?

compareTo orders by character code, and every uppercase ASCII letter comes before every lowercase one: 'Z' is 90 and 'a' is 97. Use compareToIgnoreCase or String.CASE_INSENSITIVE_ORDER to sort names, and a Collator for locale-correct order.

How do I compare strings that may be null in Java?

Use Objects.equals(a, b), which returns true when both are null and never throws. When one side is a constant, put it first, "yes".equals(input), since equals returns false for a null argument instead of throwing.

Why must equals and hashCode agree for strings?

Hash-based collections such as HashMap find a key's bucket by hashCode and then the entry by equals. If two equal strings had different hash codes, a lookup would search the wrong bucket and never find the key; String guarantees they agree.

Is a switch on a String case-sensitive in Java?

Yes. Since Java 7 a switch on a String compares contents, compiled into a hashCode lookup followed by an equals check, so it is case-sensitive, and a null selector throws NullPointerException.

Mark as readSign in to keep your place.

How to Compare Strings in Java: equals vs == and compareTo

Compare Java strings with equals, never ==, which checks identity. How compareTo orders strings, ignoring case, and why hashCode agrees with equals.

  • Course: Java study plan
  • Module: Strings
  • Kind: Lesson
  • Reading time: 12 min
  • Runtime: OpenJDK 18

What is the difference between == and equals() for strings in Java?

For strings in Java, == checks whether two references point to the same object, while equals() compares the characters. Strings read from input or built at run time are new objects, so input == "yes" is false even when the user typed yes. Always compare content with equals, or with equalsIgnoreCase to ignore case.

Lesson

Comparing strings correctly is a rite of passage: every Java programmer has shipped a == that worked in tests and failed with real input. This lesson makes the rules precise, and then goes further into ordering (compareTo), case-insensitive comparison, and the hashCode contract that lets strings work as map keys.

== compares references

String input = new Scanner(System.in).next();   // user typed "yes"
if (input == "yes") { … }                        // FALSE even when they typed yes
if (input.equals("yes")) { … }                   // correct

== on two references asks "are these the same object?" — and a string read from input, built by concatenation, or returned by most methods is a fresh object, never the pooled literal. The comparison compiles, runs, and is silently wrong.

equals: content, case-sensitive

"Java".equals("Java")        // true
"Java".equals("java")        // false
"Java".equals(null)          // false — never throws
"Java".equalsIgnoreCase("JAVA")   // true

String.equals checks: same object → true; not a String → false; different length → false; then compares character by character. It is O(n) in the worst case and short-circuits on the first difference. Its null handling is why the idiom is "constant".equals(variable), or Objects.equals(a, b) when both may be null.

equalsIgnoreCase compares after folding case per character (using both Character.toUpperCase and toLowerCase, which handles most alphabets). For locale-sensitive folding — Turkish dotless i, German ß — use a Collator; for programming identifiers and protocol keywords, equalsIgnoreCase is right.

compareTo: lexicographic ordering

String implements Comparable<String>, so strings sort naturally:

"apple".compareTo("banana")   // negative (a < b)
"banana".compareTo("apple")   // positive
"apple".compareTo("apple")    // 0
"apple".compareTo("applesauce")   // negative: shorter prefix sorts first (returns -5, the length difference)
"Zebra".compareTo("apple")    // negative! 'Z' (90) < 'a' (97) — uppercase sorts before lowercase
"10".compareTo("9")           // negative: '1' < '9' — strings compare character by character, not numerically

The rule: compare characters at each index by their UTF-16 code unit values; at the first difference return this.charAt(i) - other.charAt(i); if one is a prefix of the other return the length difference. The sign is all you should rely on — the magnitude is an implementation detail.

Consequences you must know:

  • Uppercase before lowercase (ASCII order). compareToIgnoreCase fixes that for sorting names.
  • Digits compare as characters: "10" < "9". To sort numerically, parse, or use a comparator on Integer.parseInt.
  • Accented characters sort by code point, which is rarely what a user expects; java.text.Collator gives locale-correct ordering.

Collections.sort(list) and Arrays.sort(strings) use compareTo; list.sort(String.CASE_INSENSITIVE_ORDER) uses the case-insensitive comparator built into String.

hashCode: the map-key contract

"hello".hashCode()      // 99162322

String.hashCode() is s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1], computed with int overflow — specified in the Javadoc, so it is the same on every JVM. It is cached in the object after the first call.

The contract that every equals override must obey (Module 8) is visible here: two strings that are equals always have the same hashCode, which is what lets HashMap<String, V> find a key that was inserted as a different object with the same characters. "hello".hashCode() == new String("hello").hashCode() — always.

Because 31 is small and the arithmetic wraps, collisions exist ("Aa" and "BB" both hash to 2112); HashMap handles collisions, so this is trivia, not a problem — until someone sends a million colliding keys as a denial-of-service, which is why Java 8 turned overfull buckets into trees.

contentEquals, regionMatches, matches

  • s.contentEquals(sb) — compare a String with a StringBuilder or any CharSequence without converting.
  • s.regionMatches(ignoreCase, offset, other, otherOffset, len) — compare a substring without allocating one.
  • s.matches(regex) — full-match against a regular expression (lesson 7). "abc".matches("a.*") is true; note it matches the whole string.
  • s.startsWith(prefix), s.endsWith(suffix), s.contains(sub) — the everyday tests; contains is indexOf(sub) >= 0.

switch on strings

switch (command) {
    case "start" -> start();
    case "stop"  -> stop();
    default      -> System.out.println("unknown: " + command);
}

Since Java 7, switch accepts a String. It is compiled into a hashCode lookup followed by an equals check, so it is content-based and case-sensitive, and a null selector throws NullPointerException.

A comparison checklist

WantUse
Same charactersa.equals(b)
Same characters, any casea.equalsIgnoreCase(b)
Either may be nullObjects.equals(a, b)
Orderinga.compareTo(b), or String.CASE_INSENSITIVE_ORDER
Locale-correct orderingCollator.getInstance(locale).compare(a, b)
Same objecta == b — and ask yourself why

Interview angle

  • "String s = "hi"; String t = new String("hi"); s == t?" False. "s.equals(t)?" True. "s == t.intern()?" True.
  • "What does "apple".compareTo("banana") return?" A negative number (−1: 'a' − 'b'). Only the sign is contractual.
  • "Why must equals and hashCode agree?" Hash-based collections find the bucket by hashCode and then the entry by equals; disagreement makes keys unfindable.
  • "Is switch on a string by reference?" No — by hashCode then equals.

Key takeaways

  • == compares references and is wrong for strings; equals compares content.
  • "literal".equals(var) and Objects.equals are null-safe.
  • compareTo is character-by-character by code unit: uppercase first, digits as text.
  • hashCode is specified, cached, and agrees with equals — the basis of HashMap keys.
  • switch on strings is content-based and throws on null.

Common questions

How does compareTo work for strings in Java?

It compares two strings character by character by UTF-16 code unit and returns a negative number, zero or a positive number. At the first difference it returns the difference of the two characters; if one string is a prefix of the other, the difference in length. Only the sign is guaranteed.

Why does "Zebra" sort before "apple" in Java?

compareTo orders by character code, and every uppercase ASCII letter comes before every lowercase one: 'Z' is 90 and 'a' is 97. Use compareToIgnoreCase or String.CASE_INSENSITIVE_ORDER to sort names, and a Collator for locale-correct order.

How do I compare strings that may be null in Java?

Use Objects.equals(a, b), which returns true when both are null and never throws. When one side is a constant, put it first, "yes".equals(input), since equals returns false for a null argument instead of throwing.

Why must equals and hashCode agree for strings?

Hash-based collections such as HashMap find a key's bucket by hashCode and then the entry by equals. If two equal strings had different hash codes, a lookup would search the wrong bucket and never find the key; String guarantees they agree.

Is a switch on a String case-sensitive in Java?

Yes. Since Java 7 a switch on a String compares contents, compiled into a hashCode lookup followed by an equals check, so it is case-sensitive, and a null selector throws NullPointerException.

Exercises

First and last in order

Read three words (whitespace-separated). Print two lines:

  1. strict: <first> <last> — the smallest and largest by compareTo.
  2. ignoreCase: <first> <last> — the smallest and largest by compareToIgnoreCase.

Print the words as they were given (do not change their case).

Example: banana Apple cherry →

strict: Apple cherry
ignoreCase: Apple cherry

and zebra apple Mango →

strict: Mango zebra
ignoreCase: apple zebra

Two orders, one list

Read an integer n and n words. Print natural: <words sorted with the natural String order>, then ignoreCase: <words sorted with String.CASE_INSENSITIVE_ORDER> (the sort is stable, so words equal ignoring case keep their input order), then distinctIgnoreCase: <how many distinct words there are when case is ignored> using a TreeSet built with that comparator. Join words with spaces.

Example: 4 then banana Apple apple Cherry →

natural: Apple Cherry apple banana
ignoreCase: Apple apple banana Cherry
distinctIgnoreCase: 3

In this module: Strings

← Strings are immutable objects · The String API you will actually use →