W3docs

Java-String-Vergleich

Java-Strings korrekt mit equals, equalsIgnoreCase, compareTo vergleichen und verstehen, warum == nur Referenzen vergleicht.

Zwei Strings zu vergleichen wirkt wie die harmloseste Operation der Sprache – und doch ist es die Stelle, an der neue Java-Programmierer am häufigsten stolpern. Der Grund: == sieht offensichtlich aus wie „sind das dieselben?" – aber bei Strings beantwortet es eine Frage, die man fast nie stellen möchte. Dieses Kapitel führt durch die Vergleichs-API in der Reihenfolge, in der man sie einsetzen sollte, und schließt mit den Regeln für die Sortierung – Groß-/Kleinschreibung, Locale, numerische Strings und die Falle, sich auf die Standard-Sortierung der JVM zu verlassen.

Warum == für Strings falsch ist

== vergleicht bei Objekten Referenzen. Es fragt: „Zeigen diese zwei Variablen auf buchstäblich dasselbe Objekt auf dem Heap?". Bei Strings, die über den String-Pool dieselbe Identität teilen, gibt == zufällig true zurück. Bei allem anderen – zur Laufzeit erzeugten Strings, aus Eingaben geparsten Strings, Strings aus new String(...) – liefert es false, selbst wenn der Inhalt identisch ist.

String a = "hello";
String b = "hello";
String c = new String("hello");
String d = "hel" + new String("lo");

a == b;             // true  — both pooled literals
a == c;             // false — c is a fresh object
a == d;             // false — d is built at runtime
a.equals(c);        // true  — contents match
a.equals(d);        // true  — contents match

Die Regel ist kurz und absolut: Für String-Gleichheit immer equals verwenden. Egal, woher die Strings stammen. == „funktioniert zufällig" bei frühen Tests mit Literalen und versagt dann stillschweigend, sobald ein String durch I/O wandert.

equals und equalsIgnoreCase

equals gibt true zurück, genau dann wenn die beiden Strings dieselben Zeichen in derselben Reihenfolge haben:

"hello".equals("hello");           // true
"hello".equals("Hello");           // false — case-sensitive
"hello".equals(null);              // false — never throws

equalsIgnoreCase führt einen zeichenweisen Vergleich nach Unicode-Groß-/Kleinschreibungsfaltung durch:

"hello".equalsIgnoreCase("HELLO");      // true
"hello".equalsIgnoreCase("Hello");      // true
"straße".equalsIgnoreCase("STRASSE");   // false — ß and SS differ in length

Das deutsche ß-Beispiel ist eine nützliche Warnung: Groß-/Kleinschreibungsfaltung in Unicode ist keine Bijektion (ß wird zu sich selbst kleingeschrieben, passt aber in manchen Kontexten auch auf ss). Wenn Benutzereingaben Nicht-ASCII-Text enthalten können und ein Groß-/Kleinschreibungs-unabhängiger Vergleich benötigt wird, ist es besser, beide Seiten mit String#toLowerCase(Locale) zu normalisieren, bevor man vergleicht, oder java.text.Collator für locale-bewussten Vergleich zu verwenden.

null-sicherer Vergleich

equals auf einem null-Empfänger wirft NullPointerException. Das folgende ist also ein Fehler:

if (input.equals("admin")) { ... }     // NPE when input is null

Drei idiomatische Lösungen:

"admin".equals(input)                  // literal on the left — handles null safely
Objects.equals(input, "admin")         // null-safe symmetric comparison (java.util.Objects)
Objects.requireNonNull(input).equals("admin")  // fail-fast if null is a bug

"literal".equals(variable) – manchmal als Yoda-Vergleich bezeichnet – ist in etablierten Codebasen am verbreitetsten. Objects.equals ist etwas eleganter, wenn keiner der beiden Werte statisch bekannt ist.

compareTo und compareToIgnoreCase

Wenn eine Ordnung benötigt wird (Sortieren, Bereichsprüfung, binäre Suche), gibt compareTo ein int zurück:

  • < 0, wenn der Empfänger vor dem Argument sortiert
  • 0, wenn beide gleich sind
  • > 0, wenn der Empfänger nach dem Argument sortiert
"apple".compareTo("banana");        // negative
"banana".compareTo("apple");        // positive
"apple".compareTo("apple");         // 0

Der Vergleich erfolgt nach Unicode-Codeeinheit (UTF-16), Zeichen für Zeichen, wobei kürzere Strings vor längeren sortiert werden, wenn einer ein Präfix des anderen ist. Das ergibt eine definierte Totalordnung – aber nicht die Ordnung, die ein Mensch als „alphabetisch" bezeichnen würde:

"Z".compareTo("a");                 // negative — 'Z' is 0x5A, 'a' is 0x61
"apple".compareTo("Banana");        // positive — uppercase letters come first in ASCII

Für eine menschenlesbare Sortierung gibt es zwei echte Antworten:

  • compareToIgnoreCase für die günstige, ASCII-orientierte Lösung, die für Englisch gut funktioniert.
  • java.text.Collator für eine korrekt locale-bewusste Sortierung, die Akzente, deutsches ß, den richtigen Platz für ñ im Spanischen und die Konvention berücksichtigt, dass manche Schriftsysteme Ziffern nach Buchstaben einordnen.
Collator c = Collator.getInstance(Locale.FRENCH);
List<String> names = new ArrayList<>(List.of("éclair", "Étoile", "anvil"));
names.sort(c);                     // ["anvil", "éclair", "Étoile"]  — proper French sort

Wenn ein Benutzer die Liste sehen wird, sollte man Collator verwenden. Handelt es sich um einen Sortierschlüssel in einem internen Index, ist compareTo schneller und stabil.

contentEquals und CharSequence

String#contentEquals vergleicht mit jeder CharSequenceString, StringBuilder, StringBuffer oder einem CharBuffer. Es ist die richtige Methode, wenn man wissen möchte, ob der aktuelle Inhalt eines Builders einem bekannten String entspricht, ohne ein zwischengeschaltetes toString():

StringBuilder sb = new StringBuilder("hello");
"hello".contentEquals(sb);         // true — no toString allocation

equals hilft hier nicht, weil String#equals per Vertrag für jedes Nicht-String-Argument false zurückgibt.

equals ignoriert den Pool vollständig

Der Pool ist eine interne Speicheroptimierung; equals konsultiert ihn nicht. Zwei Strings mit denselben Zeichen gelten als gleich, unabhängig davon, ob sie an derselben Adresse liegen:

String pooled = "x";
String fresh  = new String("x");
pooled == fresh;             // false — different objects
pooled.equals(fresh);        // true  — same contents
fresh.hashCode() == pooled.hashCode();  // true — equal strings always hash the same

Die letzte Zeile ist der Grund, warum String sicher als HashMap-Schlüssel verwendet werden kann: Gleiche Strings haben immer gleiche Hash-Codes, und der Cache macht Lookups günstig.

Ein praktisches Beispiel

Das Programm führt durch die Vergleichsentscheidungen, die man im echten Code trifft: die richtige Methode für Gleichheit wählen, null behandeln, mit dem richtigen Comparator sortieren. Die Ausgabe macht die Unterschiede zwischen compareTo und einem Collator nebeneinander sichtbar.

java— editable, runs on the server

Die drei Sortierungen machen die Unterschiede sichtbar. compareTo ergibt [Banana, anvil, apple, Étoile, éclair]: das großgeschriebene Banana springt an den Anfang, weil B (0x42) vor allen Kleinbuchstaben sortiert, und Étoile landet am Ende, weil É (0xC9) oberhalb von z liegt. CASE_INSENSITIVE_ORDER erzeugt [anvil, apple, Banana, éclair, Étoile], indem die Groß-/Kleinschreibung gefaltet wird, sodass die Liste alphabetisch lesbar ist. Bei dieser Liste erzeugt der französische Collator zufällig dieselbe Reihenfolge – aber er ist der einzige, dessen Ergebnis konstruktionsbedingt korrekt ist: Er faltet die Groß-/Kleinschreibung und behandelt Akzente als sekundäre Unterscheidung, sodass er auch bei Eingaben korrekt bleibt, bei denen der Codepoint-Trick versagt (zum Beispiel beim Sortieren von côté relativ zu cote oder bei der korrekten Einordnung von ñ im Spanischen).

Was als Nächstes kommt

Einen String in Teile aufzuteilen ist das nächste naheliegende Thema. Java bietet dafür zwei Werkzeuge, und eines davon ist älter als das Sprachdesign es heute wünscht. Weiter zu Java StringTokenizer.

Übungen

Übung
Welcher Aufruf prüft, ob ein (möglicherweise `null`) `input` gleich dem String 'admin' ist, und ist **sowohl** korrekt als auch `null`-sicher?
Welcher Aufruf prüft, ob ein (möglicherweise `null`) `input` gleich dem String 'admin' ist, und ist **sowohl** korrekt als auch `null`-sicher?
Was this page helpful?