6.6 Zeichenketten (Strings)

 Zeichenketten werden in Java mit der Klasse String verwaltet. Sie sind Objekte.

Sie wurden bisher wie die Basistypen Character, Integer oder Float behandelt. Zeichenketten werden jedoch ähnlich wie Referenzen behandelt da Zeichenketten unterschiedlich lang sein können. Man kann Zeichenketten daher nicht die Zahlentypen mit fester Größe einfach in den Speicherbereich eines Objekts einbetten.

Zeichenketten werden bei Bedarf im Hauptspeicher (Heap) angelegt und referenziert. Die Referenz auf die Zeichenkette selbst hat eine konstante Größe.

Zeichenketten können mit einer direkten Zuweisung erzeugt werden. Sie können jedoch auch gleichwertig mit dem new() Operator und dem Konstruktor der Klasse String angelegt werden. Beide Varianten sind nur in der Syntax unterschiedlich. Der einzige Unterschied besteht darin, dass das Objekt "John" schon beim Laden der Klasse angelegt wird. Das Objekt "Doe" wird erst beim Aufruf des Befehls angelegt.

String a = "John";
String b = new String("Doe");
Drei Referenzen auf zwei Zeichenketten

 a sowie b sind Objektreferenzen nicht aber der direkte Datenbehälter!

Zeichenketten(Strings) können einfach mit dem plus "+" Operator verkettet werden. Der plus Operator konvertiert automatisch andere Basistypen in Zeichenketten bevor die neue verkettete Zeichenkette zugewiesen wird:

String a = "Das Ergebnis ist ";
double pi = 3.1415;
String b = "***";
String c = a + pi + b;

Tipp: Die Klasse String verfügt über eine ganze Reihe von hilfreichen Methoden. Hier eine Auswahl:

Zeichenketten werden von der virtuellen Maschine als unveränderbare ("immutable") Objekte gehandhabt. Sie können daher nicht überschrieben oder verändert werden. Die Implikationen können am folgenden Beispiel gezeigt werden.

Schritt 1: Anlegen der Zeichenketten

String a = "John";
String b = a;
String c = " Doe";
Drei Referenzen auf zwei Zeichenketten

Schritt 2: Verkettung und Objektallokation

Eine Verkettung der Zeichenketten mit dem plus Operator ergibt das folgende Speicherabbild

c = a + c;

Die Variable c zeigt jetzt auf ein neues Objekt. Die Zeichenkette " Doe" ist eventuell nicht mehr referenziert und wird dann vielleicht irgendwann gelöscht. Bei der oben gezeigten Verkettung entstehen typischerweise sehr viele neue Objekte und es werden sehr viele Stringobjekte zum Löschen freigesetzt! 

Drei Referenzen auf zwei Zeichenketten

Vergleich

Da ein Objekt vom Typ String eine Referenz auf eine Zeichenkette ist, muss man beim Vergleich von Zeichenketten unterscheiden, ob man die Referenz oder den Inhalt der Zeichenkette vergleichen möchte um Fehler zu vermeiden.

Der Javaübersetzer und das Javalaufzeitsystem speichern Literale wenn möglich nur einmal im Hauptspeicher. Konstante Literale die man zur Übersetzungszeit erkennen kann werden in Referenzen auf ein einziges Objekt zusammengefasst. Dynamisch erzeugte Zeichenketten werden in eigenen Objekten verwaltet. Die genaue Semantik ist in der Java Sprachspezifikation (3.te Version von 2005, Paragraph 3.10.5 String Literals) beschrieben.

Wichtig: Identische Zeichenketten haben nicht unbedingt die gleiche Objektidentität. Ein Vergleich mit Hilfe der Objektidentität ist daher im Normalfall nicht ratsam.

String a = "John";
String b = a;
String c = "John";
String d = "Jo"+"hn"; // Der konstante Ausdruck 
      // wird schon bei der Übersetzung aufgelöst!
String e = new String("John");

das Speicherabbild zum Quellcode links:

4 Referenzen auf zwei Zeichenketten

Ein Vergleich mit dem == Operator vergleicht nur die Adressen der referenzierten Objekte nicht aber deren Inhalt. Zum Vergleich der Inhalte wird die .equals() Methode benötigt. Siehe

if (a == b) System.out.println(" a und b zeigen auf das gleiche Objekt");
    else System.out.println(" a und b zeigen nicht auf das gleiche Objekt");
if (a == c) System.out.println(" a und c zeigen auf das gleiche Objekt");
    else System.out.println(" a und c zeigen nicht auf das gleiche Objekt");
if (a == d) System.out.println(" a und d zeigen auf das gleiche Objekt");
    else System.out.println(" a und d zeigen nicht auf das gleiche Objekt");
if (a == e) System.out.println(" a und e zeigen auf das gleiche Objekt");
    else System.out.println(" a und e zeigen nicht auf das gleiche Objekt");
if (a.equals(b)) System.out.println(" a und b sind gleiche Zeichenketten");
    else System.out.println(" a und b sind nicht gleiche Zeichenketten");
if (a.equals(c)) System.out.println(" a und c sind gleiche Zeichenketten");
    else System.out.println(" a und c sind nicht gleiche Zeichenketten");
if (a.equals(d)) System.out.println(" a und d sind gleiche Zeichenketten");
   else System.out.println(" a und d sind nicht gleiche Zeichenketten");
if (a.equals(e)) System.out.println(" a und e sind gleiche Zeichenketten");
   else System.out.println(" a und e sind nicht gleiche Zeichenketten");

Führt zum Ergebnis:

 a und b zeigen auf das gleiche Objekt
 a und c zeigen auf das gleiche Objekt
 a und d zeigen auf das gleiche Objekt
 a und e zeigen nicht auf das gleiche Objekt
 a und b sind gleiche Zeichenketten
 a und c sind gleiche Zeichenketten
 a und d sind gleiche Zeichenketten
 a und e sind gleiche Zeichenketten

Zeichenkettenverwaltung im "Stringpool"

Der javac Übersetzer erkennt Literale schon beim Übersetzen einer Klasse und speichert sie nur einmal in der .class Datei mit dem Binärcode. Zur Laufzeit eines Javaprogramms wird die entsprechende Klasse bei der ersten Benutzung dynamisch geladen und die konstanten Zeichenketten (Literale) zu einem "Stringpool" hinzugefügt, wenn sie vorher noch nicht im Stringpool existierten. Dieses Verfahren minimiert den Speicherverbrauch und steigert den Durchsatz da Zeichenketten, in der Regel, kleine Objekte sind, die sonst den Freispeicher (Heap) dauerhaft belegen und fragmentieren.

Der Stringpool ist ein spezialisierter Bereich des Freispeichers. Objekte im allgemeinen Freispeicher und im Stringpool verhalten sich gleich. Der einzige Unterschied besteht darin, dass alle Literale im Stringpool nur genau einmal vorkommen.

Wird eine Zeichenkette dynamisch mit new String() angelegt so wird sie wie ein normales Objekt auf dem Freispeicher (Heap) angelegt und nicht im Stringpool. Die Klasse String hat jedoch eine Methode String.intern() die immer auf das kanonisierte Literal im Stringpool zeigt.

Der Zusammenhang zwischen Stringobjekten im Freispeicher wird durch das folgende Beispielprogramm deutlich:

String a = new String("Test");
String b = a.intern();
String c = new String("Test");
String d = c.intern();
String e = "Test";
if (a == c) System.out.println
   ("a,c sind die gleichen Objekte");
if (b == d) System.out.println
   ("b,d sind die gleichen Objekte"+
    " im Stringpool");
if (e == d) System.out.println
   ("e,d sind die gleichen Objekte"+
     " im Stringpool");

Ausgabe:

b,d sind die gleichen Objekte im Stringpool
d,e sind die gleichen Objekte im Stringpool
Stringobjekte und Stringpoolbeispiel

 

 

Effiziente Zeichenverwaltung mit der Klasse Stringbuffer

Die Klasse StringBuffer dient dem effizienten Handhaben von Zeichenketten. Ihre wesentlichen Eigenschaften sind:

  • "Threadsave" beim parallelen Ausführen von Threads in einem Javaprogramm werden Zeichenketten nicht versehentlich zerstört. Die Klasse ist hiermit sicher jedoch langsamer als eine nicht threadsichere Klasse (aber immer noch schneller als die Klasse String!)
  • Die Zeichenketten sind veränderbar

Wichtige Methoden Methoden der Klasse Stringbuffer sind:

Die Klasse StringBuffer verfügt über viele weitere Methoden zur Manipulation von Zeichenketten die in der API Dokumentation beschrieben werden.

Tipp: Seit JDK 5.0 steht die Klasse StringBuilder für Anwendungen die extreme Leistung benötigen zur Verfügung. Sie bietet jedoch keine Synchronisation zwischen Threads.

Anonymous (not verified)

Wed, 12/03/2014 - 23:53

Bitte erläutern Sie die Stelle mit "[...]dynamisch geladen[...]" sowie die Stelle mit [...]auf das kanonisierte Literal [...].

Was bedeutet dynamisch geladen? Was hat es mit dem kanonisierten Literal auf sich, also was heißt kanonisiert?

Können Sie die Methode String.intern() genauer erklären bitte?

Danke im Voraus und lG

Motivation für diese Optimierung

  • Zeichenketten wind wahrscheinlich die häufigsten Objekte auf dem Heap.
  • Sie werden wahrscheinlich oft dereferenziert.
  • Sie sind in der Regel kleine Objekte
  • Sie müssen vom Garbagekollektor identifiziert und geköscht werden

Die Kosten zum Löschen sind also in Relation zum gewonnenen Speicherplatz hoch. Man versucht ausserdem viele dieser konstanten Objekte in einem gemeinsamen Speicherbereich eng zu packen. Dadurch wird das cachen von Hauptspeicherseiten im Prozessor effizienter.

Erste Überlegung:

  • Strings sind immer nicht veränderliche Javaobjekte. Das bedeutet: Es kann mehrere Objekte geben, die den gleichen Inhalt besitzen. Java arbeitet immer mit Zeigern auf diese Objekte. Da die Objekte alle unveränderbar sind, könnten auch alle Referenzen auf das gleiche Objekt zeigen, das würde Platz sparen und Verwaltungsaufwand (gc!) sparen.

Die zweite Überlegung: Wann werden dem Laufzeitsystem Zeichenketten bekannt gemacht:

  1. Beim Programmieren im Quellcode: Hier werden viele Zeichenketten "als Konstanten" (das war eine konstantes Javaliteral) deklariert. Alle diese Zeichenketten können schon vom Übersetzer (javac) so zusammengefasst werden, dass in jeder *.class Datei jede Zeichenkette die in der Klasse verwendet wird nur noch einmal vorkommen muß. Alle Benutzer in der Klasse können auf die gleiche Zeichenkette zeigen. Beim Starten eines Javaprogramms werden alle *.class Dateien gelesen. Jetzt werden nochmals alle statischen und schon bekannten Zeichenketten in einem Stringpool konsolidert. Alle Zeichenketten die schon im Quellcode bekannt waren werden jetzt genau einmal (kanonisch) gespeichert und als Objekte angelegt.
  2. Es gibt aber auch Zeichenketten die zur Laufzeit des Programms dynamisch angelegt werden müssen. Ein Webserver muss zum Beispiel die URL "http://www.scalingbits.com/about" als Zeichenkette dynamisch anlegen können um eine entrsprechende Seite auszuliefern. Diese Zeichenketten werden (nicht unbedingt) im Stringpool verwaltet. Sie werden wie normale Objekte verwaltet. Man kann eine Kopie eines solchen Objekts aber mit der Methode .intern() in den Stringpool befördern und sich die Referenz zurückgeben lassen.