Zum Inhalt
wannacode

Strings

Kurz gesagt: Ein String in Java ändert sich nicht: Jedes + macht einen neuen Text, deshalb setzt man viele Stücke mit StringBuilder zusammen, und compareTo vergleicht in alphabetischer Reihenfolge.

Problem

Der Bericht, der sich hinzieht

Du weißt schon, dass title.toUpperCase(); allein nichts bewirkt: Ein String ist unveränderlich, und die Methode gibt einen neuen String zurück. Das hat einen Preis, und in einer Schleife sieht man ihn.

Das Programm des Onlineshops baut den Tagesbericht Zeile für Zeile zusammen:

String report = "";
for (int i = 1; i <= 3; i++) {
report += "Auftrag " + i + ": 12.50 €\n";
}
System.out.print(report);
Ausgabe
Auftrag 1: 12.50 €
Auftrag 2: 12.50 €
Auftrag 3: 12.50 €

Mit drei Bestellungen geht es sofort. Am Samstag sind es 50.000 Bestellungen, und dieselbe Schleife braucht etwa 2,5 Sekunden. Wir haben es auf dem Rechner gemessen, auf dem wir den Code der Lektionen prüfen.

Warum? Jedes report += ... hängt nicht an den alten String an. Es macht einen neuen String, kopiert den ganzen bisherigen Bericht hinein und klebt erst dann die neue Zeile an. Gegen Ende kopiert jeder Durchlauf über eine Million Zeichen.

Die Idee

StringBuilder: Text, der sich ändert

Um Text aus vielen Stücken zu bauen, gibt es StringBuilder. Das ist ein Objekt, das den Text in seinem eigenen Puffer hält und direkt an Ort und Stelle anhängt, ohne alles neu zu kopieren:

StringBuilder report = new StringBuilder();
for (int i = 1; i <= 3; i++) {
report.append("Auftrag ").append(i).append(": 12.50 €\n");
}
System.out.print(report.toString());
Ausgabe
Auftrag 1: 12.50 €
Auftrag 2: 12.50 €
Auftrag 3: 12.50 €
  • new StringBuilder() erzeugt einen leeren „Builder“.
  • append(...) hängt am Ende an. Es nimmt Text, eine Zahl oder ein Zeichen und gibt dasselbe Objekt zurück, deshalb kannst du die Aufrufe hintereinander verketten.
  • toString() macht daraus einen gewöhnlichen String, einmal, am Ende.

Mit 50.000 Bestellungen braucht diese Variante etwa 7 Millisekunden statt 2,5 Sekunden.

So sieht es bei beiden Wegen im Speicher aus. Damit man sieht, dass der Puffer nicht kopiert wird, ist er hier mit Platz für 64 Zeichen erzeugt.

String report = "";
for (int i = 1; i <= 3; i++) {
report += "Auftrag " + i + ": 12.50 €\n";
}
StringBuilder builder = new StringBuilder(64);
for (int i = 1; i <= 3; i++) {
builder.append("Auftrag ").append(i).append(": 12.50 €\n");
}
System.out.println(report.length() + " " + builder.length());
System.out.println(report.equals(builder.toString()));
Schritt für Schritt, in Worten
  1. report zeigt auf einen leeren String.
  2. Das erste += macht einen neuen String mit einer Zeile: 19 Zeichen.
  3. Das zweite += macht noch einen String. Es kopiert die alte Zeile hinein (19 Zeichen) und hängt erst dann die neue an. Den alten String braucht niemand mehr.
  4. Das dritte kopiert 38 Zeichen. Mit jeder Zeile wird mehr kopiert: bei 50.000 Zeilen sind es über eine Million Zeichen pro Durchlauf.
  5. StringBuilder ist ein einziges Objekt mit einem Puffer. Hier hat der Puffer Platz für 64 Zeichen.
  6. append schreibt direkt in den Puffer. Nichts wird kopiert, das Objekt bleibt dasselbe.
  7. Der Text ist derselbe, 57 Zeichen. Mit += wurden zusätzlich 57 kopiert, mit StringBuilder kein einziges.
Genauer, für Neugierige
  • Der Puffer wächst von selbst: Wenn er voll ist, legt StringBuilder einen etwa doppelt so großen an und kopiert einmal. Deshalb wird selten kopiert.
  • Einen einzelnen Ausdruck wie "Auftrag " + i + ": " + price macht der Compiler selbst zu effizientem Code. Das Problem ist nur += in einer Schleife, wo jeder Durchlauf ein eigener neuer String ist.
  • Es gibt auch StringBuffer mit denselben Methoden. Er ist sicher, wenn mehrere Threads gleichzeitig in ein Objekt schreiben, und deshalb etwas langsamer. Threads kommen im Modul „Programme in der echten Welt“. Bis dahin nimm StringBuilder.

Sieh es dir an

An Ort und Stelle ändern

StringBuilder kann mehr als anhängen. Hier ist eine Kartennummer für den Druck auf dem Kassenbon:

StringBuilder card = new StringBuilder("4532015112830366");
card.insert(12, ' ').insert(8, ' ').insert(4, ' ');
System.out.println(card);
card.replace(0, 14, "**** **** ****");
System.out.println(card);
StringBuilder phone = new StringBuilder("0888-123-456");
phone.deleteCharAt(8).deleteCharAt(4);
System.out.println(phone);
System.out.println(new StringBuilder("regal").reverse());
Ausgabe
4532 0151 1283 0366
**** **** **** 0366
0888123456
lager
Methode Was sie macht
insert(i, x) fügt x vor dem Zeichen mit dem Index i ein
replace(a, b, x) ersetzt die Zeichen von a bis b (ohne b) durch x
deleteCharAt(i) entfernt das Zeichen mit dem Index i
delete(a, b) entfernt die Zeichen von a bis b (ohne b)
reverse() dreht den Text um, von hinten nach vorne
length(), charAt(i) wie bei String

Die Leerzeichen in der Kartennummer kommen von hinten hinein: zuerst bei 12, dann bei 8, dann bei 4. Wenn du bei 4 anfängst, verschiebt das eingefügte Leerzeichen alles dahinter, und 8 ist nicht mehr die Stelle, die du willst. Die Bindestriche in der Telefonnummer werden aus demselben Grund zuerst von hinten entfernt.

Probier es aus

Zwei Namen, ein Objekt

Was gibt das Programm aus?

StringBuilder note = new StringBuilder("Tisch 4");
StringBuilder copy = note;
copy.append(", 2 Kaffee");
System.out.println(note);
String text = "Tisch 4";
String other = text;
other += ", 2 Kaffee";
System.out.println(text);
Antwort

Die Idee

In alphabetischer Reihenfolge: compareTo

equals sagt dir nur „sind sie gleich“. Zum Sortieren brauchst du mehr: welcher String vor dem anderen kommt. Das sagt dir compareTo:

System.out.println("Albrecht".compareTo("Bauer"));
System.out.println("Bauer".compareTo("Bauer"));
System.out.println("neumann".compareTo("Neumann"));
System.out.println("neumann".compareToIgnoreCase("Neumann"));
Ausgabe
-1
0
32
0
  • eine negative Zahl: Der erste kommt vor dem zweiten;
  • 0: Sie sind gleich;
  • eine positive Zahl: Der erste kommt nach dem zweiten.

Wichtig ist das Vorzeichen, nicht die Zahl selbst. Damit sortierst du eine Liste von Namen, zum Beispiel mit dem Bubblesort aus der Lektion „Arrays“:

String[] names = { "Weber", "Albrecht", "Neumann", "Bauer" };
for (int i = 0; i < names.length - 1; i++) {
for (int j = 0; j < names.length - 1 - i; j++) {
if (names[j].compareTo(names[j + 1]) > 0) {
String tmp = names[j];
names[j] = names[j + 1];
names[j + 1] = tmp;
}
}
}
System.out.println(String.join(", ", names));
Ausgabe
Albrecht, Bauer, Neumann, Weber

String.join(", ", names) klebt die Elemente des Arrays mit einem Trennzeichen dazwischen zusammen.

Genauer, für Neugierige
  • compareTo vergleicht Zeichen für Zeichen, nach ihren Codes in der Zeichentabelle. Großbuchstaben kommen vor Kleinbuchstaben, deshalb ist "neumann".compareTo("Neumann") positiv. compareToIgnoreCase achtet nicht auf Groß- und Kleinschreibung.
  • Die Zahl ist die Differenz zwischen den Codes der ersten Zeichen, die sich unterscheiden (hier unterscheiden sich „n“ und „N“ um 32), oder zwischen den Längen, wenn ein String der Anfang des anderen ist.
  • Zum Sortieren nach den Regeln einer Sprache, zum Beispiel mit Umlauten wie „ä“ oder mit „ß“, gibt es java.text.Collator.getInstance(Locale.of("de")).
  • java.util.Arrays.sort(names) sortiert ein Array von Strings nach derselben Regel, ohne dass du den Bubblesort schreibst.

Probier es aus

Rechnung 10 oder 9?

Die Rechnungsnummern sind Text. Welche ist früher?

Was gibt das Programm aus?

String a = "INV-10";
String b = "INV-9";
System.out.println(a.compareTo(b) < 0);
System.out.println(Integer.parseInt(a.substring(4)) < Integer.parseInt(b.substring(4)));
Antwort

Die Idee

Suchen, zerschneiden, zusammensetzen

Hier ist eine Zeile aus einer Kontaktdatei: Name, E-Mail und Stadt, getrennt durch „ ; “, mit überflüssigen Leerzeichen an den Enden:

String line = " Maria Becker ; maria.becker@firma.de ; Köln ";
String[] parts = line.strip().split(" ; ");
String email = parts[1];
int at = email.indexOf('@');
System.out.println(email.substring(0, at));
System.out.println(email.substring(at + 1));
System.out.println(email.indexOf('.', at));
System.out.println(String.join(" | ", parts));
System.out.println("=".repeat(20));
Ausgabe
maria.becker
firma.de
18
Maria Becker | maria.becker@firma.de | Köln
====================
  • strip() entfernt die Leerzeichen vorne und hinten, wie trim(), kennt aber auch die „breiten“ Leerzeichen aus anderen Sprachen. Nimm es in neuem Code.
  • indexOf('@') gibt den Index des ersten Vorkommens, und indexOf('.', at) sucht erst ab dem Index at. lastIndexOf sucht von hinten.
  • substring und split kennst du schon. String.join ist das Gegenteil von split.
  • "=".repeat(20) wiederholt den Text 20-mal.

Leer und „leer aussehend“ sind nicht dasselbe:

String empty = "";
String spaces = " ";
System.out.println(empty.isEmpty() + " " + spaces.isEmpty());
System.out.println(empty.isBlank() + " " + spaces.isBlank());
Ausgabe
true false
true true

isEmpty() ist nur für Text der Länge 0 wahr. isBlank() ist auch wahr, wenn nur Leerzeichen darin stehen. Um ein Feld zu prüfen, das jemand ausgefüllt hat, willst du meistens isBlank().

Die Idee

Zahlen und Text

Zahlen kommen in Text hinein und aus Text heraus:

int code = Integer.parseInt("0042");
double price = Double.parseDouble("2.40");
String label = String.valueOf(code) + "-" + String.valueOf(price);
System.out.println(label);
Ausgabe
42-2.4

Integer.parseInt und Double.parseDouble kennst du schon. String.valueOf ist das Gegenteil: Es macht Text aus einer Zahl, einem Zeichen oder einem boolean.

Für eine Tabelle mit Spalten brauchst du String.format aus der Lektion „Eingabe und Ausgabe“, mit einer Breite für jede Spalte. Hier ist ein Kassenbon aus einem Café:

String[] names = { "Kaffee", "Croissant", "Mineralwasser" };
double[] prices = { 2.40, 3.10, 1.50 };
int[] qty = { 2, 1, 3 };
double total = 0;
for (int i = 0; i < names.length; i++) {
double sum = prices[i] * qty[i];
total += sum;
System.out.println(String.format("%-16s%2d x %5.2f = %6.2f", names[i], qty[i], prices[i], sum));
}
System.out.println("-".repeat(35));
System.out.println("%-26s%9.2f €".formatted("Gesamt", total));
Ausgabe
Kaffee 2 x 2.40 = 4.80
Croissant 1 x 3.10 = 3.10
Mineralwasser 3 x 1.50 = 4.50
-----------------------------------
Gesamt 12.40 €
  • %-16s ist Text in 16 Zeichen, linksbündig. %5.2f ist eine Zahl in 5 Zeichen mit 2 Stellen nach dem Punkt, rechtsbündig. %2d ist eine ganze Zahl in 2 Zeichen.
  • "vorlage".formatted(...) macht dasselbe wie String.format("vorlage", ...). Du rufst es direkt auf der Vorlage auf.
Genauer, für Neugierige
  • a.concat(b) ist dasselbe wie a + b. Du siehst es in älterem Code.
  • Die Breite ist mindestens so viele Zeichen. Längerer Text wird nicht abgeschnitten. Er schiebt die Spalte nach rechts.
  • Double.parseDouble("2,40") wirft NumberFormatException: Hier ist das Dezimalzeichen immer ein Punkt. Aber String.format("%.2f", 2.4) ergibt auf einem Computer mit deutschen Einstellungen 2,40. Für Text, den ein anderes Programm lesen wird, nimm String.format(Locale.ROOT, ...).

Probier es aus

Finde den Fehler

„neben“ liest sich rückwärts genauso, aber das Programm sagt „neben ist kein Palindrom“. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Übung

Text, den du zusammensetzt

Löse die Aufgaben in deinem Editor. Gib keine Eingabeaufforderungen aus.

1. Palindrom. Lies eine Zeile und sag, ob sie ein Palindrom ist, ohne auf Großbuchstaben, Leerzeichen, Kommas und Bindestriche zu achten. Dreh den Text diesmal mit der Methode StringBuilder.reverse() um statt mit einer Schleife.

Eingabe
Erika feuert nur untreue Fakire
Ausgabe
Palindrom
Eingabe
Erika feuert nur treue Fakire
Ausgabe
Kein Palindrom

2. Ein Kassenbon mit Spalten. In der ersten Zeile steht die Anzahl der Artikel. In jeder Zeile danach stehen ein Name, ein Preis und eine Menge, getrennt durch ;. Gib einen Kassenbon mit ausgerichteten Spalten und der Summe am Ende aus.

Eingabe
3
Brot;1.20;2
Käse;9.80;1
Eier;0.35;10
Ausgabe
Brot 2 x 1.20 = 2.40
Käse 1 x 9.80 = 9.80
Eier 10 x 0.35 = 3.50
------------------------------------
Gesamt 15.70

3. Wörter rückwärts. Lies einen Satz und gib seine Wörter in umgekehrter Reihenfolge aus. Zwischen den Wörtern kann mehr als ein Leerzeichen stehen, und im Ergebnis muss genau eines stehen. Setz es mit einem StringBuilder zusammen.

Eingabe
Morgen um 9 ist eine Besprechung
Ausgabe
Besprechung eine ist 9 um Morgen

Weitere Aufgaben

  • Initialen. Lies einen vollen Namen („Maria Luise Becker“) und gib die Initialen mit Punkten aus: „M. L. B.“ Setz sie mit einem StringBuilder zusammen.
  • Versteckte E-Mail. Mach aus maria.becker@firma.de die Adresse m***********@firma.de: der erste Buchstabe, dann so viele Sternchen, wie es andere Zeichen vor dem @ gibt. Nimm indexOf und repeat.
  • Eine Runde durch String. Erzeuge einen String mit Anführungszeichen ("Beispiel eins") und einen anderen mit new String("Beispiel eins"). Vergleiche sie mit == und mit equals, nimm substring(0, 8), trenne den zweiten beim Leerzeichen und gib die Teile aus, mach aus "100" eine Zahl und gib sie mit String.format("Wert: %d", ...) aus, und hänge schließlich " und mehr" an einen StringBuilder mit dem ersten String an. Sag jede Zeile voraus, bevor du das Programm startest.
  • Sortieren ohne Groß- und Kleinschreibung. Ändere den Bubblesort von der Karte zu compareTo so, dass er compareToIgnoreCase nimmt, und probier ihn mit „lena“, „Jonas“ und „anna“ aus.

Test

Teste dich

1. Warum ist report += line in einer Schleife mit 50.000 Durchläufen langsam?
2. Was gibt "Bauer".compareTo("Albrecht") zurück?
3. StringBuilder a = new StringBuilder("12"); StringBuilder b = a; b.append("3"); Was steht in a?
4. Wie machst du aus einem StringBuilder einen String?
5. Was ist der Unterschied zwischen String.format(...) und "...".formatted(...)?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·