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 matchDie 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 throwsequalsIgnoreCase 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 lengthDas 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 nullDrei 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 sortiert0, wenn beide gleich sind> 0, wenn der Empfänger nach dem Argument sortiert
"apple".compareTo("banana"); // negative
"banana".compareTo("apple"); // positive
"apple".compareTo("apple"); // 0Der 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 ASCIIFür eine menschenlesbare Sortierung gibt es zwei echte Antworten:
compareToIgnoreCasefür die günstige, ASCII-orientierte Lösung, die für Englisch gut funktioniert.java.text.Collatorfü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 sortWenn 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 CharSequence – String, 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 allocationequals 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 sameDie 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.
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.