Was hat es mit String-Objekten in Java auf sich und wie kann man sie vergleichen?

Strings können in Java auf zwei verschiedene Weisen erzeugt werden und werden zudem teilweise intern durch einen Literal Pool gespeichert.

String-Vergleiche mit '=='

Ein String kann durch Zuweisung des Literals zu einer Variablen direkt oder aber, wie bei anderen Objekten auch, durch Aufruf des Konstruktors mit new gebildet werden. Ein Unterschied zwischen beiden Versionen ist zunächst nicht zu erkennen:

String s1 = "Hallo";
String s2 = new String("Welt!");

Führt man jedoch Vergleiche mit lexikalisch identischen String-Objekten durch, so zeigen sich eigenartige Ergebnisse:

String sl1 = "Hallo";
String sl2 = "Hallo";
String sk1 = new String("Hallo");
String sk2 = new String("Hallo");
System.out.println(sl1 == sl2);			// true
System.out.println(sk1 == sk2);			// false
System.out.println(sl1 == sk1);			// false
System.out.println(sl1.equals(sk1));		// true

Der Vergleichsoperator ' == ' dient beim Objektvergleich der Überprüfung der Identität der Objekte selbst, also ihrem Speicherort, nicht einer lexikalischen Übereinstimmung. Es werden somit Referenzen überprüft, ob sie auf das selbe Objekt zeigen.
Möchte man dagegen die lexikalische Gleichheit zweier Strings prüfen, so kann dies u.a. mit der Methode equals() erfolgen. Sie ist in der Klasse Object deklariert und wird in der Klasse String überschrieben. Dort werden die beiden zu vergleichenden Strings als Arrays primitiver char -Typen behandelt. Diese werden durchlaufen und die einzelnen char dann auf Übereinstimmung ihrer numerischen Unicode-Werte hin überprüft. Im letzten Beispiel sind alle char -Werte identisch und die Methode gibt true zurück.

Der Literal Pool

Wie kommt es jedoch zur Übereinstimmung der beiden Variablen sl1 und sl2 wo es sich doch auf den ersten Blick um zwei verschiedene Objekte handelt? Die Lösung liefert der Literal Pool, ein in der Klasse String angelegter Speicher, in dem zur Laufzeit je ein Exemplar bereits erzeugter, lexikalisch identischer Strings vorgehalten wird, um Speicher und Performance zu sparen. Er ist zunächst leer. Wird ein String neu erzeugt, so wird zunächst in diesem Pool nachgesehen, ob ein identischer String dort bereits eingetragen ist. Ist dies der Fall, wird lediglich eine Referenz auf den dort registrierten erzeugt, ansonsten wird er dem Pool neu hinzugefügt. Im Beispiel zeigen somit sl1 und sl2 auf das selbe Objekt, von dem im Literal Pool eine Referenz gespeichert ist.

Werden jedoch, wie bei den beiden Variablen sk1 und sk2 String-Objekte mit new erzeugt, so geschieht dies unabhängig vom Pool. Beide Objekte sind somit eigenständige Instanzen mit individuellen Speicherorten, deren Vergleich mit dem Vergleichsoperator false ergeben muss. Man erkennt hier, dass, wenn möglich, neue Strings somit ohne Verwendung des new-Operators erzeugt werden sollten.

Es gibt jedoch eine Möglichkeit, auch ein mit new erzeugtes String-Objekt auf ein lexikalisch identisches, im Pool eingetragenes Objekt zugreifen zu lassen. Die String-Methode intern() bietet diese Möglichkeit. Das folgende Beispiel verdeutlicht dies:

String s1 = "Hallo";
String s2 = new String("Hallo");
String s3 = s2.intern();
System.out.println(s1 == s3);			// true
System.out.println(s2 == s3);			// false

Methoden zum String-Vergleich

Strings können auf mehrere Arten lexikalisch verglichen werden. Die Klasse String stellt dazu selbst eine Reihe an Methoden zur Verfügung.

boolean equals(Object anObject)

Wie oben bereits erwähnt, kann equals() zum lexikalischen Vergleich herangezogen werden. Intern arbeitet die Methode so, dass der aufrufende String zunächst auf Identität mit dem als Parameter übergebenen Object geprüft wird. Bei Gleichheit wird true zurückgegeben. Ist dies nicht der Fall, so werden beide zu vergleichende Strings in char-Arrays gewandelt, diese durchlaufen und char für char gegeneinander geprüft. Bei Ungleichheit eines char-Paares wird false zurückgegeben und die Methode terminiert.

System.out.println("Foo".equals("Foo"));    // true
System.out.println("Foo".equals("foo"));    // false

boolean equalsIgnoreCase(String str)

Diese Methode ignoriert die Groß-/Kleinschreibung und bedient sich der Überprüfung mittels regionMatches(), die Stringvergleiche von Teilstrings vornehmen kann. Ihr werden in diesem Fall die vollständigen Strings übergeben und diese von Anfang bis Ende verglichen. Auch hier werden intern wieder char[] verwendet, die durchlaufen und Index für Index verglichen werden. Bei Ungleichheit oder auch bei unterschiedlicher Stringlänge wird false zurückgegeben.

System.out.println("Foo".equalsIgnoreCase("Foo"));    // true
System.out.println("Foo".equalsIgnoreCase("foo"));    // true

int compareTo(String str)

Wie die vorhergehenden basiert auch diese Methode auf dem Vergleich der char-Werte beider Strings. Zurückgegeben wird allerdings ein int. Dieser wird folgendermaßen ermittelt: Nach dem Konvertieren der zu vergleichenden Strings in char[] wird die Länge des kürzeren Arrays ermittelt. Sie wird als Abbruchbedingung einer Schleife verwendet, in der beide Arrays gleichzeitig durchlaufen und die char an gleichen Arraypositionen verglichen werden. Sind sie nicht gleich, so wird die Differenz der Unicode-Werte zurückgegeben. Läuft die Schleife bis zum Ende, wird die Längendifferenz zwischen beiden Strings zurückgegeben.

System.out.println("FooBar".compareTo("Foobar"));    // -32
System.out.println('B' - 'b');    // -32

int compareToIgnoreCase(String str)

Die Methode ignoriert Unterschiede in der Groß-/Kleinschreibung und bedient sich eines etwas anderen Vorgehens: Sie ruft das statische Feld CASE_INSENSITIVE_ORDER auf, das ein Comparator-Objekt der privaten Klasse CaseInsensitiveComparator speichert. Die Klasse deklariert die Methode compare(String s1, String s2). Hier geschieht nun allerdings ähnliches wie in compareTo(): Die Strings werden in char-Arrays gewandelt und diese durchlaufen und verglichen. Die char-Werte werden jedoch vorher durch die Wrapper-Klasse Character gekapselt und vor dem Vergleich dort hinsichtlich der Groß-/Kleinschreibung normalisiert. Die Differenz wird dann zurückgegeben.

System.out.println("FooBar".compareToIgnoreCase("Foobar"));    // 0

boolean regionMatches()

Die Methode ist überladen und mit zwei unterschiedlichen Parameterlisten deklariert.
regionMatches(int toffset, String other, int ooffset, int len)
regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)
Bei der zweiten Variante wird als erstes noch ein Parameter für die Berücksichtigung der Groß-/Kleinschreibung angegeben. Er muss true sein, wenn die Groß-/Kleinschreibung ignoriert werden soll.
Intern arbeiten beide Methoden wieder mit char-Vergleichen, wobei der Vergleich zwischen groß und klein geschriebenen char-Literalen wiederum durch Character.toUpperCase() und Character.toLowerCase() normalisiert wird.

String s1 = "DampfSchiffFahrt", s2 = "schiff";
System.out.println(s1.regionMatches(true, 5, s2, 0, 6));    // true
System.out.println(s1.regionMatches(true, 5, s2, 0, 3));    // true
System.out.println(s1.regionMatches(true, 7, s2, 2, 2));    // true