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);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());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öhnlichenString, 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
- report zeigt auf einen leeren String.
- Das erste += macht einen neuen String mit einer Zeile: 19 Zeichen.
- 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.
- Das dritte kopiert 38 Zeichen. Mit jeder Zeile wird mehr kopiert: bei 50.000 Zeilen sind es über eine Million Zeichen pro Durchlauf.
- StringBuilder ist ein einziges Objekt mit einem Puffer. Hier hat der Puffer Platz für 64 Zeichen.
- append schreibt direkt in den Puffer. Nichts wird kopiert, das Objekt bleibt dasselbe.
- 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
StringBuildereinen etwa doppelt so großen an und kopiert einmal. Deshalb wird selten kopiert. - Einen einzelnen Ausdruck wie
"Auftrag " + i + ": " + pricemacht der Compiler selbst zu effizientem Code. Das Problem ist nur+=in einer Schleife, wo jeder Durchlauf ein eigener neuer String ist. - Es gibt auch
StringBuffermit 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 nimmStringBuilder.
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());4532 0151 1283 0366**** **** **** 03660888123456lager| 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);StringBuilder ist ein gewöhnliches Objekt. copy = note kopiert den Pfeil, beide Variablen zeigen auf dasselbe Objekt, und append ändert es direkt. Bei einem String ist es anders: other += ... macht einen neuen String und lenkt nur other darauf um. text zeigt weiter auf den alten.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"));-10320- 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));Albrecht, Bauer, Neumann, WeberString.join(", ", names) klebt die Elemente des Arrays mit einem Trennzeichen dazwischen zusammen.
Genauer, für Neugierige
compareTovergleicht Zeichen für Zeichen, nach ihren Codes in der Zeichentabelle. Großbuchstaben kommen vor Kleinbuchstaben, deshalb ist"neumann".compareTo("Neumann")positiv.compareToIgnoreCaseachtet 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)));Text wird Zeichen für Zeichen verglichen. Die ersten vier Zeichen sind gleich, dann kommen „1“ und „9“, und „1“ kommt vor „9“. Deshalb kommt "INV-10" vor "INV-9", genau wie „ba“ im Wörterbuch vor „c“ kommt. Die Zahlen 10 und 9 werden erst nach parseInt als Zahlen verglichen. Wenn du Nummern als Text sortierst, fülle sie mit Nullen auf: INV-009, INV-010.
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));maria.beckerfirma.de18Maria Becker | maria.becker@firma.de | Köln====================strip()entfernt die Leerzeichen vorne und hinten, wietrim(), kennt aber auch die „breiten“ Leerzeichen aus anderen Sprachen. Nimm es in neuem Code.indexOf('@')gibt den Index des ersten Vorkommens, undindexOf('.', at)sucht erst ab dem Indexat.lastIndexOfsucht von hinten.substringundsplitkennst du schon.String.joinist das Gegenteil vonsplit."=".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());true falsetrue trueisEmpty() 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);42-2.4Integer.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));Kaffee 2 x 2.40 = 4.80Croissant 1 x 3.10 = 3.10Mineralwasser 3 x 1.50 = 4.50-----------------------------------Gesamt 12.40 €%-16sist Text in 16 Zeichen, linksbündig.%5.2fist eine Zahl in 5 Zeichen mit 2 Stellen nach dem Punkt, rechtsbündig.%2dist eine ganze Zahl in 2 Zeichen."vorlage".formatted(...)macht dasselbe wieString.format("vorlage", ...). Du rufst es direkt auf der Vorlage auf.
Genauer, für Neugierige
a.concat(b)ist dasselbe wiea + 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")wirftNumberFormatException: Hier ist das Dezimalzeichen immer ein Punkt. AberString.format("%.2f", 2.4)ergibt auf einem Computer mit deutschen Einstellungen2,40. Für Text, den ein anderes Programm lesen wird, nimmString.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.
reversed ist ein StringBuilder, und word ist ein String. Das equals von String gibt nur für einen anderen String mit denselben Zeichen true zurück. Für ein Objekt einer anderen Klasse ist es false, egal was darin steht. Die Korrektur ist word.equals(reversed.toString()).Ü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.
Erika feuert nur untreue FakirePalindromErika feuert nur treue FakireKein Palindrom2. 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.
3Brot;1.20;2Käse;9.80;1Eier;0.35;10Brot 2 x 1.20 = 2.40Käse 1 x 9.80 = 9.80Eier 10 x 0.35 = 3.50------------------------------------Gesamt 15.703. 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.
Morgen um 9 ist eine BesprechungBesprechung eine ist 9 um MorgenWeitere Aufgaben
- Initialen. Lies einen vollen Namen („Maria Luise Becker“) und gib die Initialen mit Punkten aus: „M. L. B.“ Setz sie mit einem
StringBuilderzusammen. - Versteckte E-Mail. Mach aus
maria.becker@firma.dedie Adressem***********@firma.de: der erste Buchstabe, dann so viele Sternchen, wie es andere Zeichen vor dem@gibt. NimmindexOfundrepeat. - Eine Runde durch String. Erzeuge einen String mit Anführungszeichen (
"Beispiel eins") und einen anderen mitnew String("Beispiel eins"). Vergleiche sie mit==und mitequals, nimmsubstring(0, 8), trenne den zweiten beim Leerzeichen und gib die Teile aus, mach aus"100"eine Zahl und gib sie mitString.format("Wert: %d", ...)aus, und hänge schließlich" und mehr"an einenStringBuildermit 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
compareToso, dass ercompareToIgnoreCasenimmt, und probier ihn mit „lena“, „Jonas“ und „anna“ aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Strings: mit Text arbeiten · Klassen und Objekte · Instanzmethoden · Vererbung
- Geht weiter in: Reguläre Ausdrücke · Reguläre Ausdrücke: Gruppen und Ersetzen · Dateien · Daten in Dateien
- Aufgaben zum Thema: Text und Dateien