1. Zeichenkettenverarbeitung
Zum Speichern von Zeichen und Zeichenfolgen stehen in Java die Typen char
, Character
, String
und StringBuilder
bereit. Der Einsatz der Datentypen muss geübt werden, denn jeder Typ hat seine Berechtigung. Die Aufgaben bringen den Lesern die Vor- und Nachteile der einzelnen Datentypen nahe.
Voraussetzungen
API von
String
undStringBuilder
sicher nutzen könnenAPI von
String
undStringBuilder
sicher nutzen könnenerkennen, wann
String
undStringBuilder
besser geeignet ist
Verwendete Datentypen in diesem Kapitel:
1.1. Die Klasse String und ihre Eigenschaften
String
ist nicht nur ein Datentyp, der für immutable Zeichenfolgen steht, sondern die Klasse bietet auch eine große Anzahl von Methoden. Wer die Methoden kennt und zu nutzen weiß, kann sich viel Arbeit sparen.
1.1.1. Quiz: Ist String ein eingebautes Schlüsselwort? ⭐
Java hat eingebaute Datentypen, darunter int
, double
, boolean
. Ist String
ebenfalls ein primitiver eingebauter Datentyp? Ist String
ein Schlüsselwort in Java?
1.1.2. HTML-Elemente aufbauen mit einfacher Konkatenation ⭐
Zur Erinnerung: In HTML werden für Auszeichnungen Tags eingesetzt, ein Beispiel ist <strong><em>Emphasized and Italics</em></strong>
.
Aufgabe:
Schreibe eine neue Methode
htmlElement(String tag, String body)
, die einen Stringbody
mit einem Start- und End-Tagtag
einrahmt und als neuenString
zurückgibt. Es gibt folgende Sonderbehandlung:Ist
tag
gleichnull
oder leer (""
) so wird nur derbody
betrachtet und keine Start-End-Tags geschrieben.Ist
body
gleichnull
, so gilt das wie ein übergebener Leer-String.
Schreibe zwei neue Methoden
strong(String)
undemphasized(String)
, die im Hintergrund mithtmlElement(…)
arbeiten und ein<strong>
respektive<em>
erzeugen.
Beispiel:
htmlElement( "strong", "strong is bold" )
→"<strong>strong is bold</strong>"
strong( emphasized( "strong + emphasized" ) )
→"<strong><em>strong + emphasized</em></strong>"
htmlElement( "span", null )
→"<span></span>"
htmlElement( "", "no" )
→"no"
htmlElement( null, "not strong" )
→"not strong"
htmlElement( null, null )
→""
Hinweis: Es gibt durchaus einige Anforderungen an Tag-Namen, die ein richtiges Programm prüfen könnte. So dürfen Tag-Namen nur die Ziffern 0 bis 9 sowie Groß- und Kleinbuchstaben enthalten. Diese Fälle können ignoriert werden.
1.1.3. Sichere Übermittlung durch Verdoppelung der Zeichen prüfen ⭐
Bamboo Blobfish nutzt einen Typendrucktelegraf, um Bonny Brain wichtige Nachrichten mitzuteilen. Da es auf jedes Zeichen ankommt, sendet Bamboo alle Zeichen zur Sicherheit zweimal hintereinander.
Aufgabe:
Schreibe eine Methode
int isEveryCharacterTwice(String)
, die prüft, ob jedes Zeichen im String zweimal hintereinander vorkommt.Ist die Anzahl der Symbole ungerade, ist die Nachricht falsch und die Rückgabe der Methode
0
.Kommt jedes Zeichen zweimal vor, ist die Antwort eine beliebige positive Zahl. ˗ Wenn ein Zeichen nicht zweimal hintereinander vorkommt, gibt die Methode die Position mit der ersten falschen Stelle zurück, allerdings negiert.
Beispiele:
isEveryCharacterTwice("eehhrrwwüürrddiiggeerr$$ccaappttaaiinn")
→1
isEveryCharacterTwice("ccapptttaaiinn")
→-3
isEveryCharacterTwice("222")
→0
isEveryCharacterTwice(null)
→NullPointerException
Dass ein negativer Index gewisse Stellen markiert, ist auch in der Java-Bibliothek zu finden. Die |
1.1.4. Y und Z vertauschen ⭐
Captain CiaoCiao tippt auf seiner Tastatur einen längeren Text, und spät fällt ihm auf, dass statt des deutschen Tastaturlayouts das englische aktiviert ist. Jetzt sind »y« und »z«, bzw. »Y« und »Z« vertauscht. Der Text muss korrigiert werden.
Aufgabe:
Lege eine neue Klasse
YZswapper
an.Setze in die Klasse eine neue Klassenmethode
void printSwappedYZ(String string)
, die einen übergebenenString
auf den Bildschirm bringt, aber den Buchstaben »y« als »z«, »z« als »y«, »Y« als »Z« und »Z« als »Y« ausgibt. Es geht nicht darum, einen String aus der Methode zurückzuliefern!Schreibe nicht nur eine Variante, sondern versuche, mindestens zwei Varianten zu programmieren. Es gibt zum Beispiel, die Möglichkeit die Zeichen mit
if
-else
zu prüfen oder mitswitch
-case
.
Beispiele:
printSwappedYZ("yootaxz")
ergibt auf dem Bildschirm die Ausgabezootaxy
undprintSwappedYZ("Yanthoxzl")
die AusgabeZanthoxyl
.
1.1.5. Trotzige Antworten geben ⭐
Tony der Trotzige ist für die Aktivitäten auf dem Schwarzmarkt von Captain CiaoCiao zuständig, doch er wird geschnappt und von der Polizei verhört. Um die Polizisten zu nerven, wiederholt er alles, was sie sagen, und setzt ein »No idea!« hintendran. Fragt der Polizist: »Where is the illegal whiskey distillery?«, sagt Tony: »Where is the illegal whiskey distillery? No idea!«
Aufgabe:
Lege eine neue Klasse an, und frage von der Kommandozeile eine Eingabe ab.
Abhängig von der Eingabe sollen drei Fälle unterschieden werden:
Wenn die Eingabe mit einem ? endet, dann gib auf dem Bildschirm aus, was von der Eingabe kommt, und hänge aber
" No idea!"
hinten an.Wenn von der Polizei keine Frage gestellt wird — die Eingabe endet nicht mit
?
— hält Tony der Trotzige ganz seinen Mund.Kommt von der Eingabe die spezielle Eingabe
"No idea?"
, und das unabhängig von der Groß-/Kleinschreibung, erwidert Tony trotzig:"Aye!"
1.1.6. Quiz: String-Vergleiche mit == und equals(…) ⭐
Strings sind Objekte, und daher gibt es prinzipiell zwei Möglichkeiten, sie zu vergleichen:
mit dem Vergleich der Referenzen über
==
über die für Objekte typische Methode
equals(Object)
Welchen Unterschied macht das?
1.1.7. Quiz: Ist equals(…) symmetrisch? ⭐
Unter der Annahme, dass s
ein String
ist: Gibt es einen Unterschied zwischen s.equals("tutego")
und "tutego".equals(s)
?
1.1.8. Zeichenfolgen auf Palindrom-Eigenschaft testen ⭐
Ein Palindrom ist ein Wort, das sich von vorne wie von hinten gleich liest, etwa »Otto« oder auch »121«.
Dass es überhaupt solche Wörter und sogar Sätze gibt, amüsiert Captain CiaoCiao, kann er damit doch die Gesellschaft unterhalten. Allerdings legt man ihm immer wieder Zeichenfolgen vor, die keine Palindrome sind. Daher müssen alle Wörter vorher getestet werden.
Aufgabe:
Schreibe ein Java-Programm, das untersucht, ob eine Zeichenkette ein Palindrom ist.
Lege eine neue Klasse
PalindromeTester
an.Implementiere eine statische Methode
boolean isPalindrome(String s)
.Erweitere das Programm um eine Klassenmethode
boolean isPalindromeIgnoringCase(String s)
, sodass der Test unabhängig von der Groß-/Kleinschreibung wird.Jetzt sollen auch alle Zeichen ignoriert werden, die keine Buchstaben oder Ziffern sind. Bei der Feststellung hilft
Character.isLetterOrDigit(char)
. Damit kann man auch Sätze wie »A man a plan a canal Panama« oder »Pepe in Tahiti hat nie Pep« oder »Sei fies – stets sei fies!« prüfen. Nennen wir die MethodeisPalindromeIgnoringNonLettersAndDigits(String)
.
1.1.9. Prüfen, ob Captain CiaoCiao in der Mitte steht ⭐
Captain CiaoCiao ist der Mittelpunkt der Welt, also erwartet er, dass er in allen Texten auch im Zentrum steht.
Aufgabe:
Schreibe eine Methode
boolean isCiaoCiaoInMiddle(String)
, dietrue
liefert, wenn die Zeichenfolge"CiaoCiao"
in der Mitte steht.
Beispiele:
isCiaoCiaoInMiddle("CiaoCiao")
→true
isCiaoCiaoInMiddle("!CiaoCiao!")
→true
isCiaoCiaoInMiddle("SupaCiaoCiaoCute")
→true
isCiaoCiaoInMiddle("x!_CiaoCiaoabc")
→true
isCiaoCiaoInMiddle("\tCiaoCiao ")
→true
isCiaoCiaoInMiddle("BambooCiaoCiaoBlop")
→false
isCiaoCiaoInMiddle("Bernie und Ert")
→false
1.1.10. Den kürzesten Namen im Array finden ⭐
Bonny Brain nutzt nur den kürzesten Rufnamen einer Person.
Aufgabe:
Schreibe eine Methode
String shortestName(String... names)
, die von allen vollständigen Namen den kürzesten Teil-String zurückgibt. Der String kann auch genau ein Leerzeichen enthalten, wenn der Name zusammengesetzt ist. Anders gesagt: Es gibt Strings mit einem Namen oder Strings mit zwei Namen.Wenn es keine Namen gibt, ist die Antwort ein leerer String.
Das Vararg-Array darf nicht
null
sein, und kein String im Array darfnull
sein.
Beispiel:
shortestName("Albert Tross", "Blowfish", "Nick Olaus", "Jo Ker")
→"Jo"
1.1.11. String-Vorkommen zählen ⭐
Captain CiaoCiao hat in einer unbedachten Aktion den Entwickler Dev David ausgeschaltet. Der war gerade dabei, eine Methode zu schreiben; die Javadoc ist fertig, aber die Implementierung fehlt.
/**
* Counts how many times the substring appears in the larger string.
*
* A {@code null} or empty ("") String input returns {@code 0}.
*
* <pre>
* StringUtils.countMatches(null, *) = 0
* StringUtils.countMatches("", *) = 0
* StringUtils.countMatches("abba", null) = 0
* StringUtils.countMatches("abba", "") = 0
* StringUtils.countMatches("abba", "a") = 2
* StringUtils.countMatches("aaaa", "aa") = 2
* StringUtils.countMatches("abba", "ab") = 1
* StringUtils.countMatches("abba", "xxx") = 0
* </pre>
*
* @param string the String to check, may be null
* @param other the substring to count, may be null
* @return the number of occurrences, 0 if either String is {@code null}
*/
public static int countMatches( String string, String other ) { return null; }
Hinweis: Das *
in der Javadoc symbolisiert eine beliebige Übergabe.
Aufgabe:
Implementiere die Methode.
1.1.12. Die größere Mannschaft ermitteln ⭐
Bonny Brain studiert alte Logbücher, in denen die Stärke ihrer Mannschaft und die der gekaperten Schiffe steht:
|-||| |-|| |||-||| |||||-||
Jedes Crew-Mitglied ist durch einen Strich symbolisiert, ein Minuszeichen trennt die Mannschaftsgröße. Links steht die Anzahl Personen auf dem eigenen Schiff, rechts die Anzahl des überfallenen Schiffs.
Aufgabe:
Die Striche sind für Bonny Brain nicht gut zu lesen. Schreibe ein Programm, das die Kodierung deutlich macht:
|-||| => Raided ship had a larger crew, difference 2 |-|| => Raided ship had a larger crew, difference 1 |||-||| => Ships had the same crew size |||||-|| => Pirate ship had a larger crew, difference 3
1.1.13. Diamanten bauen ⭐⭐
Captain CiaoCiao mag Diamanten, je größer desto besser.
Aufgabe:
Schreibe ein Programm, das folgende Ausgabe generiert:
A ABA ABCBA ABCDCBA ABCBA ABA A
Über eine Abfrage auf der Konsole soll es möglich sein, die maximale Breite des Diamanten festzulegen. In unserem Beispiel ist das die 7 — die Länge des String
ABCDCBA
. Es sollen nur Angaben für die Breite akzeptiert werden, die mit Strings aus auf- und wieder absteigenden Großbuchstaben zu erreichen sind, also maximal die Länge vonABCDEFGHIJKLMNOPQRSTUVWXYZYXWVU…BA
.
1.1.14. Auf ein gutes Passwort prüfen ⭐
Alle schmutzigen Geheimnisse verschlüsselt Captain CiaoCiao, aber zu oft war sein Passwort zu einfach und wurde erraten. Er hat gelernt, dass ein sicheres Passwort für seine Geschäfte wichtig ist, doch kann er sich die Regeln nicht so richtig merken: Ein gutes Passwort hat eine gewisse Länge, enthält Sonderzeichen usw.
Aufgabe:
Lege eine neue Klasse
PasswordTester
an.Schreibe eine Methode
isGoodPassword(String)
, die einige Kriterien testet. Die Methode sollfalse
zurückgeben, wenn das Passwort nicht gut ist, undtrue
, wenn das Passwort einen guten Aufbau hat. Schlägt ein Test fehl, so soll überSystem.err
eine Meldung erscheinen und es sollen keine weiteren Prüfungen stattfinden.
1.1.15. Erdnussbutterkekse backen ⭐⭐
Kurz vor dem Fest trifft sich die Crew bei Captain CiaoCiao, um Erdnussbutterkekse zu backen. Die Hobbybäcker bringen entweder Erdnussbutter, Zucker oder Eier mit. Als sie ankommen, setzen sie sich der Reihe nach an einen Tisch und möchten anfangen zu backen.
Gegeben ist eine Liste aller Zutaten, die die Hobbybäcker mitgebracht haben. Die Liste wird als String repräsentiert, wobei P
für Erdnussbutter steht, S
für Zucker und E
für Ei. Der String könnte lauten: PSESEPESP
oder auch PPPEEESSS
.
Gebacken werden kann nur, wenn drei Hobbybäcker mit Erdnussbutter, Zucker und Ei nebeneinander sitzen, wobei die Reihenfolge egal ist.
Aufgabe:
Ermittele, wie oft Erdnussbutterkekse gebacken werden können, also die drei Zutaten direkt nebeneinander in beliebiger Reihenfolge zusammenkommen.
Einige Hobbybäcker haben nicht richtig zugehört und bringen ganz andere Zutaten mit, wie Rum, Lakritz oder marinierten Aal. Mit ihnen lassen sich keine Erdnussbutterkekse backen.
Beispiele:
"PSESEPESP"
→ 3"PPPEEESSS"
→ 0"SEPEPLSEE"
→ 1
1.1.16. Quersumme berechnen ⭐
Da Bonny Brain oftmals Zahlungen anweist und befürchtet, jemand könnte die Beträge verändern, greift sie zu einem Trick: Neben dem Betrag übermittelt sie in einem getrennten Kanal die Quersumme.
Die Quersumme einer Zahl bildet man durch die Addition jeder Ziffer der Zahl. Wenn die Zahl etwa 10938 lautet, so ist die Quersumme 1 + 0 + 9 + 3 + 8 = 21.
Aufgabe:
Lege eine neue Klasse
SumOfTheDigits
an.Schreibe eine Klassenmethode
int digitSum(long value)
, die die Quersumme einer Zahl berechnet.Schreibe eine überladene Klassenmethode
int digitSum(String value)
dazu, die die Ziffern in einem String annimmt.
Welche Methode ist leichter zu implementieren? Welche Methode sollte die andere als Unterprogramm aufrufen?
1.1.17. Texte entspalten ⭐⭐
Captain CiaoCiao scannt alte Logbücher ein, doch die waren ursprünglich in Spalten. Nach der OCR-Texterkennung bleiben die Spalten erhalten.
Da das nicht gut zu lesen ist, sollen die zwei Spalten erkannt und in regulären Fließtext ohne Spalten übersetzt werden.
Aufgabe:
Schreibe eine Methode
decolumnize(String)
, die nach der Weißraum für die Trennung der Spalten sucht und aus einem String mit Text in zwei Spalten einen Text ohne Spalten liefert. Es ist im Vorfeld nicht bekannt, wo die Trennung der Spalten ist, das muss das Programm ermitteln.
Beispiel:
I’m dishonest, and a to watch out for, dishonest man you because you can can always trust to never predict when be dishonest. they’re going to do Honestly, it’s the something incredibly honest ones you want stupid.
→
I’m dishonest, and a dishonest man you can always trust to be dishonest. Honestly, it’s the honest ones you want to watch out for, because you can never predict when they’re going to do something incredibly stupid.
Jede Spalte ist durch mindestens ein Leerzeichen getrennt. Bedenke, dass die rechte und linke Seite »halbe« Leerzeilen haben können!
1.1.18. Eine Wiese mit Lieblingsblumen zeichnen ⭐⭐
Captain CiaoCiao möchte sein Schiff verschönern und mit Blumen verzieren. Er findet eine Grafik von Joan G. Stark als Vorlage und überlegt, wie er den Malern und Anstreichern mitteilen kann, welche Muster er in der Kabine wünscht.
_ _(_)_ wWWWw _ @@@@ (_)@(_) vVVVv _ @@@@ (___) _(_)_ @@()@@ wWWWw (_)\ (___) _(_)_ @@()@@ Y (_)@(_) @@@@ (___) `|/ Y (_)@(_) @@@@ \|/ (_)\ / Y \| \|/ /(_) \| |/ | \ | \ |/ | / \ | / \|/ |/ \| \|/ \\|// \\|// \\\|//\\\|/// \|/// \\\|// \\|// \\\|// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aufgabe:
Kopiere die Blumen in einen String. Tipp: Lege einen String wie
String flower = "";
an, setze die Blumenreihe in die Zwischenablage, und füge sie in der IDE zwischen die Anführungszeichen ein; IntelliJ und Eclipse werden dann die Sonderzeichen wie\
und\n
selbständig kodieren. Mit den Textblöcken wird es noch einfacher.Es gibt 8 Sorten von Blumen, und wir können sie durchnummerieren von 1 bis 8. Die Reihenfolge kodieren wir als String, zum Beispiel
"12345678"
. Es soll nun möglich sein, die Reihenfolge zu ändern und Blumen mehrfach vorkommen zu lassen, etwa durch die Kodierung"8383765432"
. Ist eine Kennung falsch, soll für sie automatisch immer die erste Blume erscheinen.
Beispiele:
"838"
führt zu_ _(_)_ _ _(_)_ (_)@(_) _(_)_ (_)@(_) (_)\ (_)@(_) (_)\ `|/ (_)\ | \| | \|/ | / \|/ \\\|/ \\\|// \\\|/ ^^^^^^^^^^^^^^^^^^^^^^^
"ABC9"
führt zu@@@@ @@@@ @@@@ @@@@ @@()@@ @@()@@ @@()@@ @@()@@ @@@@ @@@@ @@@@ @@@@ / / / / \ | \ | \ | \ | \\|// \\|// \\|// \\|// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Halte in einem Array die Stellen für den Übergang zwischen den Blumen fest. |
1.1.19. Wiederholungen erkennen ⭐⭐⭐
Captain CiaoCiao blättert in einem Buch und findet Muster der Art 🌼🌻🌼🌻🌸🌼🌻🌼🌻🌸🌼🌻🌼🌻🌸. Wie beruhigend für ihn. Er möchte Stempel für den Druck herstellen, um sich solche Musterfolgen selbst drucken zu können. Natürlich sollen die Kosten reduziert werden, und der Stempel soll selbst keine Wiederholungen von Symbolen enthalten. Für gegebene Musterfolgen soll ein Programm entwickelt werden, das die minimale Folge von Symbolen ermittelt, die auf den Stempel müssen.
Aufgabe:
Schreibe eine Methode
String repeatingStrings(String)
, die im Fall einer Wiederholung die sich wiederholende Zeichenfolge als Rückgabe liefert, sonstnull
, falls sich keine Teilfolge wiederholt.
Beispiele:
repeatingStrings("🌼🌼🌼")
liefert"🌼"
.repeatingStrings("🌼🌻""🌼🌻""🌼🌻")
liefert"🌼🌻"
.repeatingStrings("Ciao"+"Ciao")
liefert"Ciao"
.repeatingStrings("Captain CiaoCiaoCaptain CiaoCiao")
liefert"Captain CiaoCiao"
.repeatingStrings("🌕🌔🌓🌑")
liefertnull
repeatingStrings("CaptainCiaoCiaoCaptain")
liefertnull
repeatingStrings("🌼")
liefertnull
repeatingStrings("")
liefertnull
repeatingStrings(null)
liefertnull
Hinweis: repeatingStrings(…)
soll die kürzeste sich wiederholende Zeichenfolge liefern.
1.1.20. Zeilengrenzen beschränken und Zeilen umbrechen ⭐⭐
Bonny Brain steigt in der Kommunikation auf Brieftauben um, und da kann das Papier nicht sehr groß sein. Jetzt müssen alle Texte in der Breite reduziert werden.
Aufgabe:
Schreibe eine Klasse
WordWrap
mit einer statischen MethodeString wrap(String string, int width)
, die eine Zeichenkette ohne Zeilenumbrüche in kleine Teilzeichenketten der maximalen Breitewidth
zerlegt und mit\n
getrennt wieder zurückgibt. Innerhalb von Wörtern — und Satzzeichen gehören zum Wort — soll nicht zwangsumbrochen werden!
Beispiel:
Der Aufruf von
String s = "Live now; make now always the most precious time. " + "Now will never come again."; System.out.println( wrap( s, 10 ) );
liefert bei einer maximalen Zeilenlänge von 30 folgende Ausgabe:
Live now; make now always the most precious time. Now will never come again.
1.1.21. Quiz: Wie viele String-Objekte? ⭐
Wie viele String
-Objekte gibt es im folgenden Programmcode?
String str1 = "tutego";
String str2 = new String( "tutego" );
String str3 = "tutego";
String str4 = new String( "tutego" );
1.1.22. Testen, ob die Frucht schokoladig umhüllt ist ⭐⭐
Captain CiaoCiao mag Obstspieße mit Schokolade überzogen. Sara bekommt die Aufgabe, die Früchte zu glasieren, wobei sie unterschiedliche Schichten von dunkler und heller Schokolade bildet.
Bambi prüft, ob die Schichten korrekt sind. Sieht sie dhFhd
, weiß sie, dass die Frucht F
erst eine Schicht helle, dann eine Schicht dunkle Schokolade bekommen hat. Bei dhhd
fehlt die Frucht, und das entspricht nicht der Erwartung von Captain CiaoCiao. Bei ddhFh
ist die Schicht kaputt, was auch nicht richtig ist. Und bei F
fehlt die Schokolade komplett, was für eine Enttäuschung!
Aufgabe:
Schreibe eine rekursive Methode
checkChocolate(String)
, die prüft, ob der String symmetrisch ist, also links und rechts der gleiche Typ Schokolade ist und sich in der Mitte die FruchtF
befindet.
1.1.23. Von oben nach unten, von links nach rechts ⭐⭐⭐
In einer Höhle findet Bonny Brain einen Text, allerdings läuft der Text nicht von links nach rechts, sondern ist von oben nach unten geschrieben.
s u ey! ao
Vertikal geschrieben ist das die Zeichenfolge sea you!
— und das ist viel einfacher zu lesen!
Aufgabe:
Schreibe eine Methode
printVerticalToHorizontalWriting(String)
, die einen String wieder in die horizontale Lage bringt und ausgibt. Das Argument ist ein String, in dem Zeilenvorschübe die Zeilen trennen.
Beispiel:
Bleiben wir bei dem String von oben:
String s = "s u\ney!\nao "; printVerticalToHorizontalWriting( s );
Die Ausgabe auf dem Bildschirm wird sein:
sea you!
.
Drei wichtige Annahmen sollen gelten:
Zeilen werden nur mit
\n
getrenntJede Zeile ist gleich lang; die letzte Zeile hat zum Beispiel ein Leerzeichen, damit alle Zeilen 3 Zeichen (in diesem Beispiel) lang sind (Zeilenumbruch nicht eingerechnet),
Am Ende des Strings steht kein
\n
.
1.2. Dynamische Strings mit StringBuilder
Während String
-Objekte immutable sind, lassen sich Objekte vom Typ java.lang.StringBuilder
modifizieren. Das Gleiche gilt für StringBuffer
, doch dieser Typ ist API-gleich und für die Übungen nicht relevant.
1.2.1. Strings füllen ⭐
Captain CiaoCiao liebt die Freiheit, und Abstand ist ihm sehr wichtig. Auch bei Texten, findet er, könnten die Buchstaben ruhig ein wenig mehr Abstand haben.
Aufgabe:
Schreibe eine Methode
mix(String, String)
, die einen String aufspreizt und Füllzeichen zwischen alle Zeichen setzt.Die Parameter dürfen
null
sein.
Beispiele:
mix( "We’re out of rum!", "-" )
→"W-e-'-r-e- -o-u-t- -o-f- -r-u-m-!"
mix( "Blimey", "👻" )
→"B👻l👻i👻m👻e👻y"
mix( "HI", "♥" )
→"H♥I"
mix( "♥", "!!" )
→"♥"
mix( "", "👻" )
→""
1.2.2. Mit dem Papagei das Alphabet üben ⭐
Captain CiaoCiao lehrt seinen Papagei das Alphabet. Damit er sich die Mühe spart, greift er auf eine Software zurück, die ihm das Alphabet generiert.
Gegeben ist folgende Methode:
static String abcz() {
String result;
result = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return result;
}
Die Methode liefert eine Zeichenkette mit allen Zeichen im Alphabet von 'A'
bis 'Z'
. So kann Captain CiaoCiao die Zeichenkette auf https://ttsreader.com/de/ in ein Eingabefeld kopieren und vorlesen lassen. Allerdings müssen dann zwischen den Buchstaben Leerzeichen stehen, damit die Tonausgaben besser klingen.
Die Methode ist zwar performant in ihrer Aufgabe, aber nicht sonderlich flexibel, wenn es zum Beispiel darum geht, nur gewisse Bereiche zu generieren, etwa von '0'
bis '9'
. Denn der Papagei kann ABC
schon sehr gut, aber von G
bis Z
wird es eng.
Aufgabe:
Ändere die Methode
abcz()
, sodass derString
dynamisch über eine Schleife generiert wird.Ergänze eine Methode
String abcz(char start, char end)
, die einen String mit allen Symbolen generiert, die zwischen dem Anfangsbuchstabenstart
und dem Endbuchstabenend
liegen; das Endezeichen ist inklusiv und gehört mit zum String.Schreibe dann die Methode
String abcz(char start, int length)
, dielength
Zeichen abstart
liefert. Lässt sich eine der Methoden auf die andere abbilden?Überlege, wie mit fehlerhaften Parametern umzugehen ist, wenn etwa der Endindex vor dem Startindex liegt.
Der primitive Datentyp |
1.2.3. Quiz: Leicht angehängt ⭐
Möchte man Zeichenfolgen dynamisch aufbauen, hat man in Java prinzipiell zwei Möglichkeiten:
über die Klasse
String
und dieString
-Konkatenation mit+
über
StringBuilder
(StringBuffer
wollen wir hier nicht explizit mit erwähnen — die beiden Klassen sind API-identisch.)
In Code:
String s = "";
s += "Ay ";
s += "Captain";
StringBuilder sb = new StringBuilder();
sb.append( "Ay " ).append( "Captain" );
String t = sb.toString();
Inwiefern unterscheiden sich beide Lösungen? Wie viele Objekte werden generiert?
1.2.4. Zahl in textuelle unäre Kodierung konvertieren ⭐
Captain CiaoCiao macht sich Sorgen um die Sicherheit seiner Crew und er möchte die Beutemenge der Raubzüge verschleiern. Dabei sollen nur zwei Symbole verwendet werden.
Die unäre Kodierung stellt eine natürliche Zahl n wie folgt dar:
n | unäre Kodierung |
---|---|
0 | 0 |
1 | 10 |
2 | 110 |
3 | 1110 |
4 | 11110 |
Eine natürliche, also positive Ganzzahl n wird dargestellt durch n Einsen, gefolgt von einer Null. Ein Code dieser Art nennt sich übrigens präfixfrei, weil kein Wort das Präfix eines anderen Wortes ist. Bei der Kodierung könnten wir auch problemlos 0 und 1 vertauschen, die Präfixeigenschaft ändert sich dadurch nicht.
Durch die unäre Kodierung entstehen unterschiedlich lange Codes.
Aufgabe:
Schreibe eine Methode
String encode(int... values)
, die aus einem Vararg-Array mit Ganzzahlen einen String erzeugt, in dem alle unärkodierten Werte aus dem Array aneinandergehängt werden.Ergänze eine Methode
int[] decode(String value)
, die aus einem unärkodierten String wieder einint
-Array macht.
Beispiel:
encode( 0, 1, 2, 3, 0, 1 )
→"0101101110010"
encode( 0, 0, 0, 0 )
→"0000"
encode()
→""
Arrays.toString( decode("0101101110010") )
→[0, 1, 2, 3, 0, 1]
1.2.5. Gewicht durch Vertauschung verlieren ⭐
Bonny Brain ist aufgefallen, dass es sich bei den Frachtgebühren prima betrügen lässt. Die Verantwortlichen im Büro vergessen oft das genaue Gewicht, können sich aber die vorkommenden Ziffern perfekt merken, und es fällt nicht auf, wenn maximal zwei Ziffern vertauscht werden. Bonny Brain nutzt das zu ihrem Vorteil, indem sie die kleinste Ziffer in der Zahl nach vorne setzt, um ein kleineres Gewicht zu erzielen. 0 muss allerdings ignoriert werden, da sich sonst die Länge der Zahl ändert, und das fällt auf.
Aufgabe:
Schreibe eine Methode
int cheatedWeight(int weight)
, die genau diese Transformation macht.
Beispiele:
cheatedWeight(0)
→0
cheatedWeight(1234)
→1234
cheatedWeight(4321)
→1324
cheatedWeight(100)
→100
cheatedWeight(987654321)
→187654329
1.2.6. Vokale entfernen ⭐
Captain CiaoCiao diktiert seine Memoiren, und es sprudelt aus ihm heraus. So schnell kann Kiko Kokopu gar nicht mitschreiben! Aber kann man nicht alle Vokale weglassen und es trotzdem später noch verstehen? Ein Sprachwissenschaftler sagte mal, dass ein Text auch nach dem Entfernen der Vokalbuchstaben noch verständlich bleibt. Als Vokalbuchstaben gelten im Deutschen: A, Ä, E, I, O, Ö, U, Ü, Y. Stimmt es, was die Wissenschaftler sagen?
Aufgabe:
Lege eine neue Klasse
RemoveVowel
an.Schreibe eine Klassenmethode
String removeVowels(String string)
, die aus einem übergebenenString
die Vokalbuchstaben entfernt. Es ist in Ordnung, wenn die Umlaute fehlen.Löse die Aufgabe mit mindestens zwei verschiedenen Varianten.
Beispiele:
"Hallo Javanesen"
→"Hll Jvnsn"
"BE NICE"
→"B NC"
1.2.7. Don’t shoot the Messenger ⭐
Bonny Brain übermittelt eine geheime Nachricht und hat Angst, dass der Bote überfallen und die Nachricht dadurch offenbart wird. Also schickt sie mehrere Boten, die jeweils einen Teil der Nachricht zustellen. Das Schema ist wie folgt:
Ein Text wird in Buchstaben zerlegt
Bote 1 bekommt den 1. Buchstaben
Bote 2 bekommt den 2. Buchstaben
Bote 1 bekommt den 3. Buchstaben
Bote 2 bekommt den 4. Buchstaben
usw.
Der Empfänger der Nachricht muss nun auf die beiden Boten warten, die ursprüngliche Reihenfolge kennen und die Nachricht wieder zusammensetzen.
Aufgabe:
Schreibe eine Methode
String joinSplitMessages(String...)
, die eine beliebige Anzahl der zerlegten Botschaften entgegennimmt und den zusammengesetzten String zurückgibt.Falls Nachrichtenteile fehlen, soll das zu keinem Fehler führen.
Beispiele:
joinSplitMessages("Hoy", "ok")
→"Hooky"
joinSplitMessages("Hooky")
→"Hooky"
joinSplitMessages("Hk", "oy", "o")
→"Hooky"
joinSplitMessages( "H", "", "ooky" )
→"Hooky"
1.2.8. Wiederholte Leerzeichen komprimieren ⭐⭐
Bubbles hört für Captain CiaoCiao ein Gespräch ab und transkribiert es. Doch weil Bubbles immer so viele Erdnüsse schält, ist die Leertaste verklemmt und löst sich schlecht; so verdoppelt sich schnell ein Leerzeichen. Auf keinem Fall kann sie so den Text Captain CiaoCiao geben.
Aufgabe:
Schreibe eine statische Methode
StringBuilder compressSpace(StringBuilder string)
, die mehr als zwei Leerzeichen im übergebenenstring
zu einem Leerzeichen zusammenschmelzen lässt.Die Übergabe
string
soll mitreturn string;
zurückgegeben werden, die Veränderung soll direkt amStringBuilder
erfolgen.
Beispiel:
"Will you shut up, man! This is the way!"
→"Will you shut up, man! This is the way!"
1.2.9. Knacken und Knistern einfügen und entfernen ⭐
Meldungen über Funk haben oft ein Knistern, das stört Captain CiaoCiao.
Aufgabe:
Schreibe zwei Methoden:
String crackle(String)
soll in willkürlichen Abständen »♬CRACK♪« einbauen und den Knisterstring zurückgeben.String decrackle(String)
soll das Knistern wieder entfernen.
1.2.10. CamelCase-Strings zerlegen ⭐
Um bei der telegrafischen Übertragung von Text Volumen einzusparen, wendet Funker Frogfish einen Trick an: Er schreibt das nächste Zeichen hinter dem Leerzeichen groß und löscht dann das Leerzeichen. Aus ciao ciao
wird ciaoCiao
. Ist das nächste Zeichen schon ein Großbuchstabe, wird einfach nur das Leerzeichen entfernt, aus Ciao Ciao
wird so CiaoCiao
. Da die Großbuchstaben innerhalb der Serie von Kleinbuchstaben wie Höcker aussehen, wurde die Schreibweise CamelCase getauft.
Captain CiaoCiao empfängt die Zeichenfolgen, aber leicht zu lesen ist so ein Text nicht.
Aufgabe:
Schreibe eine neue Methode
String camelCaseSplitter(String)
, die alle CamelCase-Segmente wieder trennt.
Beispiele:
camelCaseSplitter("List")
→"List"
camelCaseSplitter("CiaoCiao")
→"Ciao Ciao"
camelCaseSplitter("numberOfElements")
→"number Of Elements"
camelCaseSplitter("CiaoCiaoCAPTAIN")
→"Ciao Ciao CAPTAIN"
Falls Wörter komplett in Großbuchstaben sind, wie im letzten Fall, gilt nur der Wechsel zwischen Klein- und Großbuchstaben.
1.2.11. Wörter unterstreichen ⭐⭐
Immer wieder muss Bonny Brain die Crew auf Regeln aufmerksam machen. Sie schreibt dazu eine Nachricht und unterstreicht die wichtigen Wörter. Bonny Brain hat im folgenden Text das Wort »treasure« unterstrichen:
There is more treasure in books than in all the pirates' loot on Treasure Island -------- --------
Aufgabe:
Lege eine neue Klasse
PrintUnderline
an.Schreibe eine neue statische Methode
printUnderline(String string, String search)
, die jede Zeichenkettesearch
instring
wie im oberen Beispiel gezeigt unterstreicht. Bedenke, dasssearch
mehrmals im String vorkommen kann oder auch keinmal.Die Groß-/Kleinschreibung des Such-Strings soll keine Rolle spielen, wie es auch das Beispiel zeigt.
1.2.12. Caesar-Verschlüsselung implementieren ⭐⭐⭐
Captain CiaoCiao hat von einer Verschlüsselung erfahren, die schon Gaius Julius Caesar verwendet haben soll. Da er den römischen Feldherrn bewundert, will auch er seine Texte so verschlüsseln.
Bei der sogenannten Caesar-Verschlüsselung verschiebt man jedes Zeichen um drei Positionen im Alphabet, das heißt, aus A
wird D
, aus B
wird E
und so weiter. Am Ende des Alphabets beginnen wir wieder von vorne, und so ergibt X
→ A
, Y
→ B
, Z
→ C
.
Aufgabe:
Lege eine neue Klasse
Caesar
an.Implementiere eine Methode
String caesar(String s, int rotation)
, die die Verschlüsselung vornimmt.rotation
ist dabei die Verschiebung, die beliebig sein sollte, nicht nur 3 wie aus dem Eingangsbeispiel.Schreibe eine Methode
String decaesar(String s, int rotation)
, die die Verschlüsselung wieder zurücknimmt.Die Caesar-Verschlüsselung fällt in die Klasse der Verschiebechiffren. Ist sie Captain CiaoCiao zu empfehlen?
Beispiel:
caesar( "abxyz. ABXYZ!", 13 )
→"noklm. NOKLM!"
decaesar( caesar( "abxyz. ABXYZ!", 13 ), 13 ) )
→"abxyz. ABXYZ!"
1.3. Lösungsvorschläge
1.3.1. Quiz: Ist String ein eingebautes Schlüsselwort?
String
ist kein eingebauter Datentyp. Alle Schlüsselwörter in Java werden kleingeschrieben und alle Referenztypen nach der Namenskonventionen groß. java.lang.String
ist eine Klasse, und String
-Objekte liegen zur Laufzeit als Exemplare vor. Die Klasse String
erbt wie alle anderen Klassen von java.lang.Object
.
1.3.2. HTML-Elemente aufbauen mit einfacher Konkatenation
public static String htmlElement( String tag, String body ) {
if ( tag == null )
tag = "";
if ( body == null )
body = "";
if ( tag.isEmpty() )
return body;
else
return "<" + tag + ">" + body + "</" + tag + ">";
}
public static String strong( String body ) {
return htmlElement( "strong", body );
}
public static String emphasized( String body ) {
return htmlElement( "em", body );
}
Auf die allgemeine Methode String htmlElement(String tag, String body)
können die anderen Methoden zurückgreifen.
Tag und Body dürfen laut Aufgabenstellung null
sein, dann werden sie wie ein Leer-String behandelt. Anschließend kommt die Hauptabfrage: Ist das Tag leer, kommt nur der Body zurück; wenn das Tag nicht leer ist, erzeugt unsere Methode ein Start-Tag, konkateniert den body
und hängt ein End-Tag an.
Bei den Methoden strong(…)
und emphasized(…)
greifen wir auf unsere vorher definierte Methode zurück, füllen den Tag-Namen mit strong
bzw. em
und reichen den body
weiter.
1.3.3. Sichere Übermittlung durch Verdoppelung der Zeichen prüfen
private static int isEveryCharacterTwice( String string ) {
int FAILURE_CODE = 0;
int SUCCESS_CODE = 1;
if ( string.length() % 2 != 0 )
return FAILURE_CODE;
for ( int i = 0; i < string.length(); i += 2 ) {
char first = string.charAt( i );
char second = string.charAt( i + 1 );
if ( first != second )
return -(i + 1);
}
return SUCCESS_CODE;
}
Die Methode bekommt einen String und testet zunächst, ob die Eingabe korrekt ist. Die Anzahl der Zeichen muss gerade sein, denn wenn die Anzahl ungerade ist, kann es nicht sein, dass jedes Zeichen doppelt vorkommt. Falls null
übergeben wird, gibt es eine NullPointerException
bei dem Versuch, die Länge abzufragen; das ist zu erwarten.
Im nächsten Schritt laufen wir in Zweierschritten über das Array. Wir extrahieren das Zeichen an der Stelle i
und das folgende Zeichen an der Stelle i + 1
. Stimmen die beiden Zeichen nicht überein, dann ist i + 1
die Stelle, an der das Zeichen nicht wie das Zeichen davor ist. Wir negieren den Ausdruck und melden die Stelle. Das ergibt immer einen negativen Rückgabewert, denn wir invertieren nicht i
(der Index beginnt bei 0
), sondern i + 1
, was zu -1
im Ergebnis führt. Die negativen Rückgaben sind immer ungerade.
Falls es aus der Schleife keinen Ausbruch gibt, liefert die Methode 1
zurück.
1.3.4. Y und Z vertauschen
Java bietet verschiedene Möglichkeiten für Vergleiche: if-else
, switch-case
und Bedingungsoperator. Das spiegeln die Lösungen wieder:
static void printSwappedYZ1( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( c == 'y' ) c = 'z';
else if ( c == 'z' ) c = 'y';
else if ( c == 'Y' ) c = 'Z';
else if ( c == 'Z' ) c = 'Y';
System.out.print( c );
}
}
Beim ersten Lösungsvorschlag laufen wir den String
von vorne nach hinten ab. Wir können die Anzahl der Zeichen mit length()
erfragen und jedes Zeichen an einer Position mit chatAt(…)
. Wenn wir das Zeichen an einer Position haben, lässt es sich prüfen und durch ein anderes Zeichen ersetzen, das wir schlussendlich ausgeben. Diese Variante enthält eine Bildschirmausgabe, und wir erzeugen temporär keine neuen String.
static void printSwappedYZ2( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
switch ( string.charAt( i ) ) {
case 'y': System.out.print( 'z' ); break;
case 'z': System.out.print( 'y' ); break;
case 'Y': System.out.print( 'Z' ); break;
case 'Z': System.out.print( 'Y' ); break;
default: System.out.print( string.charAt( i ) );
}
}
}
Auch die zweite Variante läuft von Hand über den String. Statt jedoch die einzelnen Zeichen über ==
zu vergleichen, nutzt diese Lösung eine switch-case
-Konstruktion. Dass mehrmals System.out.print(char)
vorkommt, ist nicht schön.
static void printSwappedYZ3( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
System.out.print( c == 'y' ? 'z' :
c == 'Y' ? 'Z' :
c == 'z' ? 'y' :
c == 'Z' ? 'Y' :
c );
}
}
Die dritte Variante nutzt einen verschachtelten Bedingungsoperator zum Vergleich, ob der Buchstabe Y oder Z ist.
Ab Java 14 gibt es zudem neue switch
-Schreibweisen mit einem ->
, was weitere Varianten erlaubt. Möglichkeit 1:
static void printSwappedYZ5( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
switch ( string.charAt( i ) ) {
case 'y' -> System.out.print( 'z' );
case 'z' -> System.out.print( 'y' );
case 'Y' -> System.out.print( 'Z' );
case 'Z' -> System.out.print( 'Y' );
default -> System.out.print( string.charAt( i ) );
}
}
}
Möglichkeit 2:
static void printSwappedYZ6( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
System.out.print(
switch ( string.charAt( i ) ) {
case 'y' -> 'z'; case 'Y' -> 'Z';
case 'z' -> 'y'; case 'Z' -> 'Y';
default -> string.charAt( i );
} );
}
}
Der ein oder andere Leser wird sich eine Methode zum Vertauschen wünschen, doch die gibt es für Strings nicht. String sind unveränderbar, sie sind immutable. Das bedeutet, es gibt keine Methode, die eine Ersetzung im aktuellen String vornimmt. Es gibt eine replace(…)
-Methode, die uns allerdings nicht weiterhilft, denn wenn wir zum Beispiel »y« durch »z« ersetzen, sind später die »ursprünglichen« »z«-Zeichen nicht mehr zu erkennen und müssen dann in »y« umgewandelt werden. Prinzipiell könnten wir dreistufig vorgehen und etwa ein Sonderzeichen als Platzhalter nutzen:
Ersetze alle »y« durch »$«.
Ersetze alle »z« durch »y«.
Ersetze alle »$« durch »z«.
Elegant ist das nicht, zumal wir sichergehen müssen, dass ein Platzhaltersymbol wie »$« frei ist.
Wir haben gesehen, dass es mehrere Möglichkeiten gibt, die Aufgabe zu lösen. Immer dann, wenn wir eine Variable gegen mehrere zur Compilezeit bekannte Konstanten prüfen müssen, dann ist switch
eine gute Wahl. Wer die aktuellen Java-Versionen nutzen kann, sollte zu der Schreibweise mit switch
und Pfeil greifen. Sie ist nicht nur kürzer, sondern beugt auch unbeabsichtigtem Durchfallen vor.
1.3.5. Trotzige Antworten geben
String input = new Scanner( System.in ).nextLine().trim();
if ( input.equalsIgnoreCase( "no idea?" ) )
System.out.println( "Aye!" );
else if ( input.endsWith( "?" ) ) {
System.out.println( input + " No idea!" );
}
Wir fragen von der Kommandozeile über den Scanner
nach einer Zeile, und schneiden dann Weißraum vorne und hinten ab. Ist die Eingabe No idea?
ist die Ausgabe Aye!
. Zum Testen unabhängig von der Groß-/Kleinschreibung greifen wir auf equalsIgnoreCase(…)
zurück, das ist schneller als ein input.toLowerCase().equals("no idea?")
, denn equalsIgnoreCase(…)
kann direkt vergleichen, während toLowerCase()
erst ein neues String
-Objekt erzeugt, um es dann mit equals(…)
zu vergleichen. Das String
-Objekt ist aber nach dem equals(…)
wieder Speichermüll. Unnötig neue Objekte aufzubauen kostet Speicher und Laufzeit und sollte vermieden werden.
Ist die Eingabe nicht No idea?
, testen wir, ob die Eingabe mit einem Fragezeichen endet. Wenn ja, wiederholen wir die Eingabe und hängen ` No idea!` hinten an. Wenn keiner der beiden Fälle zutrifft, passiert nichts. Es ist wichtig, dass wir nicht erst testen, ob die Eingabe mit einem Fragezeichen endet, denn wenn wir die Fallunterscheidungen umdrehen, würde das Programm bei der Eingabe von No idea?
das Fragezeichen hinten erkennen und No idea? No idea!
ausgeben, was nicht korrekt ist.
1.3.6. Quiz: String-Vergleiche mit == und equals(…)
Prinzipiell können Strings mit ==
verglichen werden, doch in der Regel wird das nicht funktionieren, denn das würde voraussetzen, dass die String
-Objekte nur intern in der Virtual Machine aufgebaut werden. Normalerweise bauen wir aber selbst Objekte auf, indem wir z. B. eine Datei zeilenweise auslesen oder von der Kommandozeile einlesen. Dann sind jede Zeile und Eingabe ein neues String
-Objekt. Java-Entwickler sollten es sich zur Angewohnheit machen, Strings immer mit der equals(…)
-Methode zu vergleichen. Es gibt hier auch noch Varianten, z. B. equalsIgnoreCase(…)
.
Umsteiger von anderen Programmiersprachen tun sich am Anfang schwer. Die meisten Programmiersprachen erlauben es, Strings mit ==
zu vergleichen.
1.3.7. Quiz: Ist equals(…) symmetrisch?
Vergleiche mit der equals(Object)
-Methode sollten symmetrisch sein, das heißt, es sollte keinen Unterschied ergeben, ob es heißt a.equals(b)
oder b.equals(b)
. Allerdings wird die Symmetrie durch null
gebrochen. So führt String s = null; s.equals("tutego")
zu einer NullPointerException
, aber "tutego".equals(s)
zu false
. In der Praxis ist es daher robuster, wenn man auf einem String-Literal equals(…)
aufzurufen und einen anderen String zu übergeben.
Wenn null
im Vergleich vorkommen kann und NullPointerException
verhindert werden soll, lässt sich gut auf Objects.equals(…)
zurückgreifen:
Objects.equals(…)
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
1.3.8. Zeichenfolgen auf Palindrom-Eigenschaft testen
public static boolean isPalindrome( String string ) {
for ( int index = 0; index < string.length() / 2; index++ ) {
char frontChar = string.charAt( index );
char backChar = string.charAt( string.length() - index - 1 );
if ( frontChar != backChar )
return false;
}
return true;
}
public static boolean isPalindromeIgnoringCase( String string ) {
return isPalindrome( string.toLowerCase() );
}
public static boolean isPalindromeIgnoringNonLettersAndDigits(String string) {
for ( int startIndex = 0, endIndex = string.length() - 1;
startIndex < endIndex;
startIndex++, endIndex-- ) {
while ( ! Character.isLetterOrDigit( string.charAt( startIndex ) ) )
startIndex++;
while ( ! Character.isLetterOrDigit( string.charAt( endIndex ) ) )
endIndex--;
char frontChar = Character.toLowerCase( string.charAt( startIndex ) );
char backChar = Character.toLowerCase( string.charAt( endIndex ) );
if ( frontChar != backChar )
return false;
}
return true;
}
public static boolean isPalindromeRecursive( String string ) {
if ( string.length() < 2 )
return true;
if ( string.charAt( 0 ) != string.charAt( string.length() - 1 ) )
return false;
return isPalindromeRecursive( string.substring( 1, string.length() - 1 ) );
}
Die Aufgabe zum Testen mit Palindromen ist ein Klassiker in der Informatik. Für diese Aufgabe gibt es unterschiedliche Herangehensweisen; sie lässt sich relativ leicht mit einer Methode der Java-Bibliothek in einem Einzeiler beantworten oder auch iterativ über eine Schleife und auch rekursiv lösen.
Die hier gezeigte Lösung ist einfach und performant. Wollen wir testen, ob eine Zeichenfolge ein Palindrom ist, müssen wir lediglich das erste mit dem letzten Zeichen vergleichen, dann das zweite mit dem vorletzten und so weiter. Wir gehen mit einer Schleife von 0
bis zur Hälfte des Strings und tun genau das: Mit string.charAt(index)
laufen wir von links bis zur Mitte und mit string.charAt( string.length() - index - 1 )
von rechts zur Mitte. Haben wir die Zeichen extrahiert, vergleichen wir sie; sind die Zeichen nicht gleich, beenden wir die Methode mit return false
. Überlebt das Programm die Schleife, ist string
ein Palindrom. Die Lösung funktioniert für Strings mit einer geraden und auch ungeraden Anzahl Zeichen.
Unsere Methode isPalindromeIgnoringCase(String)
testet, ob der String auch unabhängig von der Groß- und Kleinschreibung ein Palindrom ist. In diesem Fall konvertieren wir zunächst die Zeichenfolge in Kleinbuchstaben und geben sie zum Testen an unsere vorhandene Methode isPalindrome(String)
.
Bei der Methode isPalindromeIgnoringNonLettersAndDigits(String)
könnten wir so vorgehen wie bei der Methode isPalindromeIgnoringCase(String)
, also über eine Java-Methode alles das herauslöschen, was kein Buchstabe oder Ziffer ist, oder wir verzichten auf einen Extra-Durchlauf zum Aufräumen des Strings und machen die Abfrage direkt. Diesen Ansatz wählt die gezeigte Implementierung. Wir laufen den String von links und rechts ab und definieren dafür zwei Schleifenzähler startIndex
und endIndex
. Der Startindex wird größer und der Endindex kleiner. Wir starten mit dem Startindex auf dem ersten Zeichen und mit dem Endindex beim letzten Zeichen. Jetzt kann es vorkommen, dass eines der beiden Zeichen weder Buchstabe noch Ziffer ist. Wir müssen also von links so weit suchen, bis wir auf ein gültiges Symbol kommen, und wir müssen von rechts das Gleiche tun. Das machen die beiden while
-Schleifen. Da die Prüfung jetzt unabhängig von der Groß-/Kleinschreibung erfolgen kann, konvertieren wir die beiden Zeichen in Kleinbuchstaben und vergleichen sie anschließend. Falls die Zeichen nicht gleich sind, beendet return false
die Methode. Ist jeder Vergleich wahr, ist die Antwort der Methode return true
.
Zum Schluss schauen wir auf die rekursive Implementierung. Hier gibt es prinzipiell zwei Möglichkeiten. Hier wird die Möglichkeit gezeigt, dass zunächst ein Test prüft, ob in dem String kein oder ein Zeichen steht. In dem Fall sind wir am Ende der Rekursion angelangt und können true
zurückgeben. Als Nächstes extrahieren wir das erste und das letzte Zeichen und vergleichen sie. Wenn die Zeichen nicht gleich sind, verlassen wir die Methode mit false
. Stimmen die Zeichen überein — Groß-/Kleinschreibung wird hier nicht ignoriert — extrahieren wir einen Teil-String und gehen eine Rekursionsstufe tiefer. Da die Rekursion hier am Ende steht sprechen wir auch von einer Endrekursion, die prinzipiell von Laufzeitumgebungen optimiert werden kann, wobei die Standard-JVM diese Optimierung bisher nicht macht.
Eine alternative Lösung mit einer Rekursion wäre, die Methode mit einem Start- und Endindex zu parametrisieren, der dann jeweils innerhalb der Methode verschoben wird, ohne temporäre Strings zu generieren. Das heißt, statt einen Teil-String zu bilden, werden lediglich der Start- und Endindex angepasst. Diese Lösung ist allerdings nicht mehr besonders entfernt von einer iterativen Lösung.
Zum Schluss schauen wir uns einen Einzeiler mit der Java-Methode reverse(…)
an:
String s = "otto";
boolean isPalindrome = new StringBuilder(s).reverse().toString().equals(s);
Alternativ sogar etwas kürzer:
boolean isPalindrome = s.contentEquals(new StringBuilder(s).reverse());
1.3.9. Prüfen, ob Captain CiaoCiao in der Mitte steht
public static boolean isStringInMiddle( String string, String middle ) {
if ( middle.length() > string.length() )
return false;
int start = string.length() / 2 - middle.length() / 2;
return string.regionMatches( start, middle, 0 /* middle offset */, middle.length() );
}
public static boolean isCiaoCiaoInMiddle( String string ) {
return isStringInMiddle( string, "CiaoCiao" );
}
Die Kernidee des Algorithmus ist es, den Mittelpunkt des Haupt-Strings zu bestimmen, dann die halbe Länge von dem in der Mitte gewünschten String abzuziehen und zu vergleichen, ob der Mittel-String ab dieser Position steht.
Letztendlich ist die Frage, ob der String "CiaoCiao"
vorkommt, recht speziell. Daher verallgemeinern wir die Frage und schreiben eine allgemeine Methode isStringInMiddle(String string, String middle)
, die für beliebige Strings funktioniert, die in der Mitte stehen sollen.
Im ersten Schritt prüfen wir die Parameter. Wurde für einen der Parameter null
übergeben, knallt es mit einer NullPointerException
beim Zugriff auf die Länge. Die Reaktion ist gut. Sind beides gültige String
-Objekte, werden die Längen ermittelt und verglichen. Wenn der Mittel-String länger ist als der Haupt-String selbst, ist das ein Fehler, und wir geben direkt false
zurück.
Die folgenden Zeilen folgen dem beschriebenen Algorithmus. regionMatches(…)
ist in diesem Fall nützlich. Die Signatur der Methode ist:
boolean regionMatches(int toffset, String other, int ooffset, int len)
Das ist ein besseres Vorgehen, als mit substring(…)
erst den String herauszuschneiden und dann mit equals(…)
zu vergleichen.
1.3.10. Den kürzesten Namen im Array finden
private static final int INDEX_NOT_FOUND = -1;
private static String shortest( String s1, String s2 ) {
return s1.length() <= s2.length() ? s1 : s2;
}
private static String shortestName( String... names ) {
if ( names.length == 0 )
return "";
String result = names[ 0 ];
for ( String name : names ) {
int spacePos = name.indexOf( ' ' );
if ( spacePos == INDEX_NOT_FOUND )
result = shortest( result, name );
else {
String part1 = name.substring( 0, spacePos );
String part2 = name.substring( spacePos + 1 );
result = shortest( result, shortest( part1, part2 ) );
}
}
return result;
}
Im Algorithmus müssen wir später mehrfach den kürzesten von zwei Strings ermitteln, sodass wir diese Aufgabe in eine eigene kleine Methode auslagern. Die Methode shortest(String, String)
liefert den kürzeren der beiden übergebenen Strings zurück; sind beide Strings gleich lang, gibt die Methode den ersten String zurück — die Entscheidung ist aber willkürlich.
Die eigentliche Methode shortestName(…)
bekommt ein Vararg von Strings, und wie üblich kann der Parameter null
sein. Durch den Zugriff auf length
wird es bei null
mit einer NullPointerException
knallen — ein gewünschtes Verhalten. Wurde der Methode kein Element übergeben, kann die Methode nichts tun und liefert den Leer-String zurück.
Da es im Array mindestens einen String gibt, initialisieren wir damit die Variable result
, die wir auch zum Schluss zurückgeben werden. Die erweiterte for
-Schleife läuft alle Strings des Arrays ab, und im Rumpf wird result
vielleicht aktualisiert. Schauen wir als Erstes, ob ein Leerzeichen im String vorkommt. Jetzt gibt es zwei Alternativen:
Kommt im String kein Leerzeichen vor, dann liefert
shortest(result, name)
den kürzeren String, und überschreibt mit dem Ergebnisresult
.Falls ein Leerzeichen im Namen vorkommt, zerlegt
substring(…)
den Namen in zwei Teilepart1
undpart2
. Hier verschachteln wirshortest(…)
, dass zunächst vonpart1
undpart2
der kürzere String ermittelt wird und dann wiederum von diesem Ergebnis undresult
der kürzere String ermittelt und als Ergebnis inresult
abgelegt wird.
Am Ende der Schleife haben wir den kürzesten String ermittelt, den wir zurückgeben.
1.3.11. String-Vorkommen zählen
private static final int INDEX_NOT_FOUND = -1;
public static int countMatches( String string, String other ) {
if ( string == null || other == null || string.isEmpty() || other.isEmpty() )
return 0;
int result = 0;
for ( int index = 0;
(index = string.indexOf( other, index )) != INDEX_NOT_FOUND;
index += other.length() )
result++;
return result;
}
Die Methode countMatches(…)
muss bei fehlerhaften Werten 0
zurückgeben. Fehlerhaft ist es, wenn ein String null
ist oder keine Zeichen enthält. Diese Prüfung wird am Anfang gemacht.
Die Variable result
wird später die Anzahl der Fundstellen anzeigen und die Rückgabe bilden. In der for
-Schleife suchen wir wiederholt mit der String
-Methode indexOf(…)
nach diesem Teil-String other
. Die Schleife initialisiert dafür eine Variable index
, die immer die letzten Fundstellen enthält, und falls es keine Fundstelle mehr gibt, ist das damit auch ein Abbruchkriterium für die for
-Schleife. Dieser Index wird dann immer um die Länge von other
erhöht, sodass die indexOf(…)
-Methode bei der nächsten Iteration hinter der dem Teil-String other
weitersuchen kann.
Falls so eine Methode in echten Java-Programmen nötig sein sollte, könnten Entwickler sie aus Apache Commons beziehen. Die Implementierung selbst liegt unter https://commons.apache.org/proper/commons-lang/apidocs/src-html/org/apache/commons/lang3/StringUtils.html.
1.3.12. Die größere Mannschaft ermitteln
Fassen wir zunächst zusammen, was wir bewerkstelligen müssen: In einem String müssen wir das Separatorzeichen (ein Minuszeichen) finden und feststellen, wie viel Striche links und rechts vom Minuszeichen stehen. Diese Werte müssen verglichen werden. Es gibt drei Ausgänge für den Vergleich: Der rechte Wert kann größer oder kleiner sein als der linke, oder beide Werte können gleich sein.
Zwei einfache Lösungen bieten sich an:
die Eingabe mit
split("-")
in zwei Strings zerlegen und dann mitlength()
diese Stringlänge abfragen.die Position des Minuszeichens im String mit
indexOf(…)
zu ermitteln und dann die Länge links und rechts vom Separatorzeichen berechnen.
Hier noch eine Speziallösung für Freunde des Mottos: »Warum einfach, wenn es auch umständlich geht«. Für die Praxis taugt das wenig und hat höchstens eine Berechtigung, wenn der Profiler anzeigt, dass eine Codestelle einen Engpass darstellt, der optimiert werden muss. Sonst geht Klarheit immer vor!
Nehmen wir kurz an, wir haben das Separatorzeichen an der Stelle i gefunden und löschen es, weil wir das eine Separatorzeichen nicht mitzählen wollen. Die Gesamtlänge schrumpft damit um eins.
Wenn P (Piraten) und V (Victims, deutsch: Opfer) Mengen sind, dann soll |P| die Anzahl Piraten und |V| die Anzahl Überfallener sein. Diese drei Informationen (0, i und length - 1) geben uns Antworten:
Wie viele Piraten gibt es? Es sind i - 0, also i viele.
Wie viele Überfallene gibt es? Es sind length - 1 - i viele.
Wir könnten diese Werte ermitteln und vergleichen und die Aufgabe wäre gelöst.
Allerdings lässt sich mit den Informationen noch mehr anfangen. Die Aufgabe fragt nach der Differenz der Mannschaftsstärken. Wäre es nicht möglich, die Differenz zu berechnen und über das Vorzeichen herauszufinden, welche Mannschaft stärker ist? Das geht!
|P| < |V| daraus folgt |P| - |V| < 0
|P| > |V| daraus folgt |P| - |V| > 0
|P| = |V| daraus folgt |P| - |V| = 0
Bei einem Vergleich von zwei Zahlen lässt sich immer die Differenz bilden und das Vorzeichen beobachten.
Der Ausdruck |P| - |V| wiederholt sich, wir können das berechnen:
|P| - |V| = i - (length - 1 - i) = i - (-i + length - 1) = i + i - (length - 1) = 2×i - (length - 1).
Das ist die Differenz in der Mannschaftsgröße und das Vorzeichen sagt uns, welche Mannschaft größer ist.
Es wird Zeit für das Java-Programm:
public static void printDecodedCrewSizes( String string ) {
int index = string.indexOf( '-' );
if ( index < 0 )
throw new IllegalArgumentException( "Separator - is missing in " + string );
System.out.print( string + " => " );
int diff = 2 * index - (string.length() - 1);
switch ( Integer.signum( diff ) ) {
case -1 -> System.out.printf(
"Raided ship had a larger crew, difference %d%n", -diff );
case 0 -> System.out.println( "Ships had the same crew size" );
case +1 -> System.out.printf(
"Pirate ship had a larger crew, difference %d%n", diff );
}
}
Zunächst wird die Fundstelle i
ermittelt; gibt es kein Minuszeichen, folgt eine Ausnahme. Anschließend wird die Differenz berechnet. Es lässt sich nun eine Fallunterscheidung mit if-else
nachschieben, doch das Programm macht etwas anderes: Es bildet alle negativen Zahlen auf -1
ab, alle positiven Zahlen auf 1
, und 0
bleibt 0
. Damit bleiben drei Möglichkeiten, die switch-case
behandeln kann. Als Letztes muss für die Ausgabe im Fall einer negativen Differenz das Vorzeichen für die Ausgabe umgekehrt werden.
1.3.13. Diamanten bauen
private static void printDiamondCore( char character, char stopCharacter ) {
if ( character == stopCharacter ) {
System.out.print( character );
return;
}
System.out.print( character );
printDiamondCore( (char) (character + 1), stopCharacter );
System.out.print( character );
}
public static void printDiamond( int diameter ) {
if ( diameter < 1 )
return;
final int ALPHABET_LENGTH = 26;
diameter = Math.min( diameter, 2 * ALPHABET_LENGTH - 1 );
int radius = diameter / 2;
for ( int indentation = radius; indentation >= -radius; indentation-- ) {
int absIndentation = Math.abs( indentation );
System.out.print( " ".repeat( absIndentation ) );
printDiamondCore( 'A', (char) ('A' + radius - absIndentation) );
System.out.println();
}
}
Die eigentliche Lösung der Aufgabe steckt in der Methode printDiamond(int)
, allerdings wollen wir auf eine Hilfsmethode zurückgreifen, um eine Zeile des Diamanten zu schreiben. Für die Einrückung durch Leerzeichen greifen wir auf die Methode repeat(int)
zurück.
printDiamondCore(char, char)
zeichnet eine Diamantenzeile auf den Bildschirm, wobei die Methode ein Startzeichen bekommt und ein Stoppzeichen. Ist das Startzeichen z. B. A
und das Stoppzeichen C
, dann gibt die Methode die Folge ABCBA
auf dem Bildschirm aus. Die Implementierung ist rekursiv. Es gibt zwei Szenarien: Wenn das Startzeichen gleich dem Stoppzeichen ist, dann wird das Zeichen nicht zweimal gesetzt, sondern nur einmal. Andernfalls wird das Zeichen gesetzt und dann rekursiv die Methode mit dem nächsten Zeichen aufgerufen, wobei das Endzeichen das gleiche bleibt. Nach dem rekursiven Abstieg wird noch einmal das Zeichen gesetzt. Die Fallunterscheidung könnten wir prinzipiell auch so umkehren, dass wir erst fragen, ob das Zeichen nicht mit dem Stoppzeichen übereinstimmt, und früher in die Rekursion gehen, doch Abbruchbedingungen von Rekursionen sind am Anfang der Methode üblicher.
Die eigentliche Methode printDiamond(int)
prüft zunächst, ob der Umfang von Diamanten gültig ist, und verlässt andernfalls die Methode. Da wir den Durchmesser durchgeben, uns jedoch der Radius interessiert, teilen wir den Durchmesser durch 2
. Jetzt kann der eigentliche Programmablauf beginnen. Doch vorher schauen wir uns eine besondere Eigenschaft an, das Verhältnis zwischen Einrückung und Diamantengröße. Der Diamant mit dem Durchmesser 7 soll zur Veranschaulichung dienen, und die Diamantengröße geben wir als Radius an. Die Einrückung durch Leerzeichen symbolisiert der Unterstrich _
:
Zeilen | Einrückung | Radius |
---|---|---|
| 3 | 1 |
| 2 | 2 |
| 1 | 3 |
| 0 | 4 |
| 1 | 3 |
| 2 | 2 |
| 3 | 1 |
Man kann ablesen, dass die Summe aus Radius und Einrückung immer gleich ist, 4 in unserem Beispiel. Wir können jetzt mithilfe einer Schleife von 3 bis 0 herunter und wieder herauf bis 3 zählen und den Radius ableiten. Oder wir gehen über den Radius des Diamanten mit einer Schleife von 1 bis 4 und wieder zurück zu 1 und leiten daraus die Einrückung ab.
Der Lösungsvorschlag nutzt die Einrückung indentation
als Schleifenzähler. Und es wird ein Trick angewendet, sodass nicht zwei Schleifen notwendig sind: Die Schleife lässt indentation
mit dem Radius beginnen und beim negativen Radius enden. Wir laufen damit einmal durch den Nullpunkt. Im Inneren der Schleife sind wir nicht an den negativen Zahlen interessiert, also wählen wir den Absolutwert und haben damit eine absteigende und aufsteigende indentation
. Aus diesen indentation
-Werten können wir, wie wir gerade gesehen haben, den Radius berechnen und damit die Zeile ausgeben. Am Anfang ist indentation
gleich radius
, das heißt radius - indentation
ist am Anfang 0
. Also schreibt die Methode printDiamondCore('A', 'A')
nur das A
. Im nächsten Durchlauf ist die indentation
um eins vermindert, das heißt, die Differenz zwischen dem Radius, der sich nicht ändert, und indentation
ist 'A' + 1
= 'B'
, also zeichnen wir den Kern ABA
.
1.3.14. Auf ein gutes Passwort prüfen
public static final int MIN_PASSWORD_LEN = 8;
public static boolean isGoodPassword( String password ) {
if ( password.length() < MIN_PASSWORD_LEN ) {
System.err.println( "Password is too short" );
return false;
}
if ( ! containsUppercaseLetter( password ) ) {
System.err.println( "Must contain uppercase letters" );
return false;
}
if ( ! containsLowercaseLetter( password ) ) {
System.err.println( "Must contain lowercase letters" );
return false;
}
if ( ! containsDigit( password ) ) {
System.err.println( "Must contain a number" );
return false;
}
if ( ! containsSpecialCharacter( password ) ) {
System.err.println( "Must contain special characters like .," );
return false;
}
return true;
}
private static boolean containsUppercaseLetter( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( Character.isUpperCase( c ) )
return true;
}
return false;
}
private static boolean containsLowercaseLetter( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( Character.isLowerCase( c ) )
return true;
}
return false;
}
private static boolean containsDigit( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( Character.isDigit( c ) )
return true;
}
return false;
}
private static boolean containsSpecialCharacter( String string ) {
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( c == '.' || c == ',' )
return true;
}
return false;
}
public static void main( String[] args ) {
System.out.println( isGoodPassword( "toshort" ) );
System.out.println( isGoodPassword( "justlowercase" ) );
System.out.println( isGoodPassword( "noDigits" ) );
System.out.println( isGoodPassword( "1specialChar" ) );
System.out.println( isGoodPassword( "MoreSpecialChars.$#&" ) );
}
Unsere Methode führt hintereinander diverse Tests durch. Schlägt ein Test fehl, beendet return false
die Methode. Sind alle Tests positiv, steht am Ende ein return true
.
Bis auf den ersten Test sind die einzelnen Kriterien in Methoden ausgelagert. Das erhöht die Übersichtlichkeit. Die einzelnen Methoden bekommen jeweils einen String, laufen ihn von vorne nach hinten ab und testen gewisse Eigenschaften. Die Herangehensweise ist auch hier, dass wir, wenn wir eine Entscheidung fällen können, direkt die Methode verlassen können mit einem entsprechenden return true
. Nehmen wir containsUppercaseLetter(String)
als Beispiel. Die Methode läuft den String von vorne nach hinten ab und prüft mit Character.isUpperCase(char)
, ob ein Großbuchstabe dabei ist. Wenn ja, müssen wir keine weiteren Zeichen testen, sondern können die Methode sofort beenden.
1.3.15. Erdnussbutterkekse backen
Die Aufgabe wird mit zwei Methoden gelöst. Die erste Methode countCookies(…)
läuft über den String und die andere Methode hasRecipeIngredients(…)
testet, ob eine Teilzeichenfolge die Backzutaten enthält.
static void countCookies( String input ) {
int cookies = 0;
for ( int i = 0; i < input.length() - 2; ) {
String triplet = input.substring( i, i + 3 );
if ( hasRecipeIngredients( triplet ) ) {
cookies++;
i += 3;
}
else
i++;
}
System.out.printf( "Cookies: %d, Ingredients remaining: %d%n",
cookies,
input.length() - 3 * cookies );
}
private static boolean hasRecipeIngredients( String triplet ) {
return triplet.length() == 3
&& triplet.contains( "P" )
&& triplet.contains( "S" )
&& triplet.contains( "E" );
}
Beginnen wir mit der Hauptmethode. Der String wird abgelaufen und dann immer Teilstrings der Länge 3 extrahiert. Die Methode hasRecipeIngredients(…)
ermittelt, ob der Teilstring die gültigen Backzutaten enthält. Wenn ja, wird die Variable cookies
hochgezählt und die Position um 3 Stellen weitergesetzt, weil die Backzutaten "verbraucht" sind. Falls der Teilstring keine gültigen Zutaten enthält, geht das Programm mit der Position nur einen Schritt nach rechts. Die for
-Schleife enthält keinen Fortschaltausdruck. Zum Schluss werden die Anzahl Kekse ausgegeben, und wir können berechnen, wie viele Zutaten insgesamt übrig bleiben, die nicht für einen Backvorgang hintereinander vorkamen.
Die zweite Methode hasRecipeIngredients(…)
muss lediglich testen, ob die Buchstaben P, E, S in einer beliebigen Reihenfolge im String vorkommen. Natürlich könnten wir z. B. mit einem switch
-Ausdruck alle Kombinationen abfragen, etwa case "PES", "PSE", "EPS", …
und so weiter. Alternativ können wir fragen, ob der Teilstring auf jeden Fall die Buchstaben P, S und E enthält. Wenn ja, und der String ist exakt drei Zeichen lang, kommen nur die drei Buchstaben in einer beliebigen Kombination vor. Da es keine Methode contains(char)
gibt, nutzt die Lösung die Methode boolean contains(String)
, erlaubt auch indexOf(char) >= 0
den Test.
Da wir die Methode ausschließlich mit Zeichenfolgen der Länge 3 aufrufen, kann natürlich die Längenabfrage entfallen, aber das ist ein Beispiel für defensives Programmieren, dass man dem Aufrufer nicht trauen kann …
1.3.16. Quersumme berechnen
static int digitSum( long value ) {
return digitSum( String.valueOf( value ) );
}
static int digitSum( String value ) {
int sum = 0;
for ( int i = 0; i < value.length(); i++ )
// sum += value.charAt( i ) - '0';
sum += Character.getNumericValue( value.charAt( i ) );
return sum;
}
Zunächst einmal ist festzuhalten, dass nur eine der beiden Methoden implementiert werden muss, weil wir jeweils die andere Methode aufrufen können. Rufen wir digitSum(long)
auf, können wir aus der ganzen Zahl einen String machen, und dann digitSum(String)
aufrufen. Umgekehrt: Wenn wir digitSum(String)
aufrufen, können wir den String mit Long.parseLong(String)
in eine Ganzzahl konvertieren und digitSum(long)
aufrufen.
Es ist ein wenig Geschmackssache, welche der beiden Methoden man implementiert. Die Herangehensweise ist anders. Implementieren wir die Methode mit dem Parametertyp long
, müssen wir immer durch 10 teilen, um die Zahl Schritt-für-Schritt zu zerlegen. Hier benötigen wir etwas Mathematik, und diese Lösung hat noch einen zweiten Nachteil, nämlich dass wir das Ergebnis von rechts nach links bekommen. Das ist für die Quersumme egal, bei manchen Konvertierungen ist das eher unpraktisch. Also implementieren wir digitSum(String)
.
Wie üblich laufen wir mit der for
-Schleife den String von links nach rechts ab. Jedes Zeichen müssen wir nun als Ziffer betrachten. Damit ein Unicode-Zeichen mit einer Ziffer in einen numerischen Wert übertragen werden kann, lässt sich die Methode Character.getNumericValue(char)
einsetzen. Aus einem char
wie '1'
wird 1
, und aus '7'
wird 7
. Prinzipiell könnten wir diese Berechnung auch selbst durchführen, indem wir von dem Unicode-Zeichen '0'
abziehen, doch getNumericValue(…)
arbeitet allgemein auf allen Unicode-Zeichen. So liefert zum Beispiel getNumericValue('٢')
das Ergebnis 2.
1.3.17. Texte entspalten
Der Algorithmus muss Folgendes tun: Im ersten Schritt muss er ermitteln, wo in allen Zeilen an der gleichen Stelle ein Leerzeichen steht, was ein Indiz für die Spalte ist. Im nächsten Schritt müssen wir an dieser Stelle trennen und erst die ganzen Zeilen der linken Spalten untereinandersetzen, anschließend alle Zeilen der rechten Spalte.
Die eigentliche Methode decolumnize(…)
greift auf die interne Methode findColumnIndex(String[])
zurück, die für ein Array von Strings die Spalte mit dem Leerzeichen findet. Auch findColumnIndex(…)
greift auf eine weitere interne Methode isSpaceAt(…)
zurück, mit der wir anfangen wollen.
private static boolean isSpaceAt( String string, int index ) {
if ( index >= string.length() )
return true;
return string.charAt( index ) == ' ';
}
isSpaceAt(String, int)
prüft, ob im übergebenen String string
an der Stelle index
ein Leerzeichen steht oder nicht. Zusätzlich wertet diese Methode alles als Weißraum, was »hinter« dem String steht. Anschaulich bedeutet das, dass hinter dem eigentlichen String unendlich viele Leerzeichen stehen.
Diese Methode wird für die eigentliche Methode findColumnIndex(String[])
verwendet:
private final static int COLUMN_NOT_FOUND = -1;
private static int findColumnIndex( String[] lines ) {
int length = lines[ 0 ].length();
for ( String line : lines )
length = Math.max( length, line.length() );
mainLoop:
for ( int column = 1; column < length - 1; column++ ) {
for ( String line : lines )
if ( ! isSpaceAt( line, column ) )
continue mainLoop;
return column;
}
return COLUMN_NOT_FOUND;
}
Bevor wir alle Zeilen durchlaufen und fragen, ob jede Zeile an einer Stelle ein Leerzeichen hat, müssen wir herausfinden, wie weit wir laufen dürfen. Daher wird im ersten Schritt die längste Zeile ermittelt. Es muss die längste Zeile sein, weil durchaus einige Zeilen komplett leer sein dürfen, und einige Zeilen können auch kurz sein, dann steht hinter diesen kurzen Zeilen gedanklich wieder Weißraum.
Haben wir die längste Zeile ermittelt, laufen wir mit einer Schleife alle möglichen Spalten ab. Die Spalte mit dem Index 0
kann es nicht sein, auch kann es die letzte Spalte nicht sein, weil ja weder links noch rechts von diesen Positionen eine wirkliche Spalte sein kann. Unabhängig davon ergibt es wenig Sinn, wenn die Spalten nur ein Symbol bereit sind, sodass man sicherlich auch mit einem anderen Index anfangen könnte, vielleicht in der Umgebung der Mitte.
Die Funktion der beiden ineinandergeschachtelten Streifen ist die folgende: die äußere Schleife läuft alle möglichen Spalten ab, während die innere Schleife dafür sorgt, dass für jede Spalte alle möglichen Zeilen untersucht werden. Wenn in einer Zeile in der Spalte column
kein Leerzeichen ist, dann brauchen wir die anderen Zeilen erst gar nicht zu berücksichtigen, sondern machen weiter mit der nächsten Spalte. Diese Möglichkeit wird mit dem Schlüsselwort continue
erreicht — wir müssen hier eine Sprungmarke verwenden, denn ohne würde continue
nur in der inneren Schleife weitermachen, wir wollen aber in der äußeren Schleife weiterkommen.
Überlebt das Programm die innere Schleife, haben wir für alle Zeilen eine Stelle gefunden, in der ein Leerzeichen steht. Die Variable column
enthält die Fundstelle, die zurückgegeben wird. Es wird nicht darauf geachtet, ob diese Fundstelle vielleicht mittig ist, vielleicht ist auch nur ganz zufällig recht weit am Anfang eine Spalte komplett mit Leerzeichen, dann wird fälschlicherweise eine Spalte erkannt, die keine sein sollte. Laufen wir über alle Spalten und Zeilen und stehen nirgends Leerzeichen untereinander, ist die Rückgabe COLUMN_NOT_FOUND
, also -1
.
Jetzt können wir zur eigentlichen Methode decolumnize(String)
kommen:
public static void decolumnize( String string ) {
String[] lines = string.split( "\n" );
if ( lines.length < 2 ) {
System.out.println( string );
return;
}
int column = findColumnIndex( lines );
if ( column == COLUMN_NOT_FOUND ) {
System.out.println( string );
return;
}
// Left column
for ( String line : lines )
System.out.println(
line.substring( 0, Math.min( line.length(), column ) ).trim()
);
// Right column
for ( String line : lines )
if ( column < line.length() )
System.out.println( line.substring( column + 1 ).trim() );
else
System.out.println();
}
Sie zerlegt zunächst den großen String in viele Zeilen und greifen dabei auf die split(…)
-Methode zurück. Eine Zerlegung in Spalten ergibt nur bei mindestens zwei Zeilen Sinn, sodass bei einer Zeile diese als solches ausgegeben wird, ohne die Suche nach einer Spalte zu starten.
Andernfalls beginnt die eigentliche Arbeit, das Suchen der Spalte. Findet die Methode findColumnIndex(…)
keine Spalte, dann geben wir den String aus und beenden die Methode.
Falls wir einen Spaltenindex gefunden haben, gibt eine erste Schleife die linke Spalte aus und die zweite Schleife die rechte Spalte. Die beiden Schleifen laufen also beide Male über alle Zeilen, aber berücksichtigen im ersten Fall nur alles, was links vom Spaltenindex steht, und im zweiten Fall nur alles das, was rechts vom Spaltenindex steht.
Bei der linken Spalte ist zu berücksichtigen, dass die Zeilen kürzer als der Spaltenindex sein können, da die Zeilen insgesamt auch kürzer sein können; wir erinnern uns: isSpaceAt(…)
ist so programmiert, dass alles, was hinter der eigentlichen Zeichenkette ist als Weißraum gewertet wird. Wenn man die Zeile der linken Spalte ausgibt, dann darf die substring(…)
-Methode nicht von 0
bis zum Spaltenindex gehen, sondern vom kleineren Wert, der Zeilenlänge und dem Spaltenindex. Mögliche Leerzeichen schneiden wir hinten und vorne für die Ausgabe noch ab.
Bei der rechten Spalte gehen wir ähnlich vor. Nur müssen wir jetzt schauen, ob die rechte Zeile überhaupt existiert. Wenn ja, liefert substring(…)
eine Teilzeichenfolge beginnend beim Spaltenindex bis zum Ende der Zeile. Andernfalls geben wir eine Leerzeile aus.
Das Programm achtet nicht darauf, unnötige Leerzeilen am Ende abzuschneiden. Wenn die Spalten nicht balanciert sind und links und rechts die gleiche Anzahl Zeilen stehen, werden für die rechte Spalte so viele Leerzeilen erscheinen, wie es in der linken Spalte noch Zeilen gibt.
1.3.18. Eine Wiese mit Lieblingsblumen zeichnen
private static final String FLOWERS = """
_
_(_)_ wWWWw _
@@@@ (_)@(_) vVVVv _ @@@@ (___) _(_)_
@@()@@ wWWWw (_)\\ (___) _(_)_ @@()@@ Y (_)@(_)
@@@@ (___) `|/ Y (_)@(_) @@@@ \\|/ (_)\\
/ Y \\| \\|/ /(_) \\| |/ |
\\ | \\ |/ | / \\ | / \\|/ |/ \\| \\|/
\\\\|// \\\\|// \\\\\\|//\\\\\\|/// \\|/// \\\\\\|// \\\\|// \\\\\\|//
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
""";
private static final int[] FLOWER_START_POS={0, 7, 13, 22, 29, 37, 44, 50, 57};
private static final String[] FLOWER_LINES = FLOWERS.split( "\n" );
private static final int FLOWER_HEIGHT = FLOWER_LINES.length;
private static final int LONGEST_LINE_LEN=FLOWER_LINES[FLOWER_HEIGHT-1].length();
private static String flowerLine( int flower, int line ) {
String s = FLOWER_LINES[ line ] + " ".repeat( LONGEST_LINE_LEN );
return s.substring( FLOWER_START_POS[flower], FLOWER_START_POS[flower + 1] );
}
private static int flowerFromId( char id ) {
return id >= '1' && id <= '8' ? Character.getNumericValue( id ) - 1 : 0;
}
public static void printFlowers( String string ) {
for ( int line = 0; line < FLOWER_HEIGHT; line++ ) {
for ( int i = 0; i < string.length(); i++ )
System.out.print( flowerLine( flowerFromId( string.charAt( i ) ), line ) );
System.out.println();
}
}
Die große Zeichenfolge für die Blumen steht in einem String, wir nutzen mehrzeilige Strings, die es seit Java 15 gibt. Der String steht im Lösungsvorschlag in einer statischen Variablen; eine lokale Variable ist prinzipiell auch möglich, aber unübersichtlich.
Ein paar zusätzliche statische Variablen wollen wir deklarieren. Damit wir später auf die einzelnen Blumen zurückgreifen können, merken wir uns in einem eigenen Array FLOWER_START_POS
die Positionen, an denen die Blumen anfangen. Die erste Blume fängt z. B. ab Index 0 an, die zweite Blume ab Index 7 und so weiter. Eine weitere Konstante FLOWER_LINES
ergibt sich aus dem Blumen-String und speichert die Zeilen aller Blumen in einem Array — so können wir später leicht eine Zeile erfragen. Die Anzahl der Zeilen verrät uns gleichzeitig die Höhe der Blumen. Die letzte Zeile ist zudem auch die längste Zeile, was wir uns ebenfalls in einer Konstanten LONGEST_LINE_LEN
merken wollen. Falls sich die Blumen einmal ändern sollten, müssten wir einige dieser Konstanten anpassen bzw. neu berechnen.
Kommen wir zu den einzelnen Methoden. String flowerLine(int flower, int line)
greift intern auf das Array FLOWER_LINES
zurück und extrahiert von einer gewünschten flower
genau diesen Teil-String aus der Zeile mit allen Blumen. Dazu berücksichtigt die Methode eine Besonderheit, und zwar können einige Zeilen kürzer sein als die längste Zeile (d. h. nicht alle Zeilen sind gleich lang), und dann würde das Bilden einer Teilzeichenfolge schnell zu einer Ausnahme führen. Der Trick ist, erst einmal an jede Zeile Leerzeichen anzuhängen. Die Anzahl der Leerzeichen wissen wir, denn es sind maximal LONGEST_LINE_LEN
viele. Den String mit vielen Leerzeichen bildet die Methode repeat(…)
der String
-Klasse.
Die Methode flowerLine(…)
liefert somit für jede Blume die individuelle Zeile. Jetzt müssen wir eine Dekodierung vornehmen von einem Zeichen zur Blume. Blume "1" muss abgebildet werden auf 0
. Das übernimmt eine neue Methode int flowerFromId(char id)
. Wir übersetzen ein Zeichen mit der Blumenkennung in eine Ganzzahl, sodass wir später intern auf das Array zurückgreifen können.
Die letzte Methode ist printFlowers(String)
. Sie greift auf die anderen beiden Methoden zurück, sodass für den eigentlichen Algorithmus nur wenige Zeilen nötig sind. Die Grundidee ist einfach: Wir gehen über alle Zeilen und setzen dann den Teil-String für jede Blume und jede Zeile hintereinander. Das heißt, wir müssen zwei Schleifen ineinanderschachteln. Die äußere Schleife geht über alle Zeilen. Für jede Zeile zerlegen wir dann den String mit der Reihenfolge in einzelne Zeichen, was uns die Blumenkennungen in id
gibt. toCharArray()
liefert ein Array mit allen Zeichen, das die erweiterte for
-Schleife ablaufen kann. Nun laufen wir über alle Blumenkennungen durch die innere Schleife und alle Zeilen durch die äußere Schleife. In der inneren Schleife konvertieren wir zunächst die Blumenkennung in die interne Position und holen dann den Teil-String der Blume für die Zeile, den wir mit System.out.print(…)
ausgeben. Nach der inneren Schleife beenden wir die Zeile mit einem Zeilenvorschub.
1.3.19. Wiederholungen erkennen
Bevor wir uns die Lösung im Code anschauen, muss der Algorithmus klar sein, der das Problem löst. Nehmen wir als Beispiel den einfachen String aaa
. Als Mensch haben wir kein Problem, sofort das Muster zu erkennen, dass sich a
dreimal wiederholt. Ein Programm könnte Folgendes tun: Es könnte das erste Zeichen von aaa
nehmen — also a
— und es so lange wiederholen, bis der String die Länge drei hat. Dann vergleichen wir, ob er dem Ausgangs-String aaa
gleicht. Das tut er! Was ist mit ababab
? Beginnt das Programm auch mit der Wiederholung des ersten Zeichens a
entsteht aaaaaa
, und der Vergleich mit dem Original ababab
fällt negativ aus. Im nächsten Schritt kann das Programm die ersten beiden Zeichen ab
nehmen und wiederholen, und es entsteht ababab
, was mit dem Original übereinstimmt. Wir haben einen Treffer.
Mit dem einfachen Algorithmus können wir beliebig lange Zeichenfolgen testen: Bilde Wiederholungen von Teil-Strings der Länge 1, 2, 3, 4. … Allerdings hilft uns eine Überlegung, wenn wir uns vor Augen führen, welches Format die gültigen Lösungen haben werden.
Teilfolge | der Länge 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
| — |
| — |
| — |
|
| — | — |
| — | — |
|
| — | — | — |
| — | — |
| — | — | — | — |
| — |
| — | — | — | — | — |
|
Aus der Tabelle geht hervor, dass mit ab
zum Beispiel keine Zeichenketten der Länge 3 gebildet werden können, und mit abcd
keine Strings der Länge 2.
Wenn wir eine Teilzeichenfolge mit zwei Zeichen haben, dann können die daraus generierten Folgen ein Vielfaches von 2 sein, also 2 Zeichen lang sein, 4 Zeichen lang sein, 6 Zeichen lang sein und so weiter. Haben wir eine Zeichenfolge mit drei Zeichen, können wir daraus Wiederholungen mit 3 Zeichen, 6 Zeichen, 9 Zeichen, und so weiter bauen.
Drehen wir das Spiel um. Wir wissen nicht, wie die Teilzeichenfolge aussieht und wie lang sie ist, sondern wir kennen den Ausgangs-String. Wenn der Ausgangs-String zum Beispiel 12 Zeichen lang ist, welche sich wiederholenden Zeichenfolgen sind dann möglich?
aaaaaaaaaaaa abababababab abcabcabcabc abcdabcdabcd abcdefabcdef
Es sind Wiederholungen von a
, ab
, abc
, abcd
, abcdef
; die Länge dieser Strings sind die Teiler von 12, das sind 1, 2, 3, 4 und 6. Andere Kombinationen sind nicht möglich. Insbesondere kann die Teilzeichenkette nicht länger als die Hälfte der String-Länge sein, denn sonst ist die Verdopplung länger als der ursprüngliche String. Bei einem naiven Ansatz könnten wir also bei einer Gesamt-String-Länge von n von 1 bis n/2 hochlaufen; wollen wir es besser machen, gehen wir nur die Teiler hoch.
public static String repeatingStrings( String string ) {
if ( string == null || string.length() < 2 )
return null;
for ( int i = 1; i <= string.length() / 2; i++ ) {
if ( string.length() % i == 0 ) {
String substring = string.substring( 0, i );
String repeatedSubstring = substring.repeat( string.length() / i );
if ( string.equals( repeatedSubstring ) )
return substring;
}
}
return null;
}
Wenn die Methode eine null
-Referenz oder Strings, die nur aus einem einzelnen Zeichen bestehen, bekommt, gibt sie direkt null
zurück. Im Weiteren können wir voraussetzen, dass die Strings mehr als zwei Zeichen lang sind.
Die for
-Schleife generiert alle Zahlen i
von 1 bis n/2; ob es Teiler der Stringlänge sind, wissen wir bisher nicht. Der Restwertoperator (%
) hilft dabei zu überprüfen, ob die Länge des Strings durch i
ohne Rest teilbar ist. Falls ja, haben wir ein Vielfaches gefunden und die String können gebildet werden.
Im Block erstellt string.substring(0, i)
den Teilstring, der string.length() / i
Mal wiederholt wird. Wenn diese generierte Wiederholung mit dem string
übereinstimmt, haben wir eine Übereinstimmung gefunden. Stimmen die beiden Strings nicht überein, kehren wir zur Schleife zurück.
Wurde die for
-Schleife vollständig durchlaufen und keine Übereinstimmung gefunden, gibt die Methode null
zurück.
1.3.20. Zeilengrenzen beschränken und Zeilen umbrechen
public static String wrap( String string, int width ) {
if ( string.length() <= width )
return string;
int breakIndex = string.lastIndexOf( ' ', width );
if ( breakIndex == -1 )
breakIndex = width;
String firstLine = string.substring( 0, breakIndex );
String remaining = wrap( string.substring( breakIndex ).trim(), width );
return firstLine + "\n" + remaining;
}
Die Methode wrap(…)
hat zwei Parameter: zum einen der Text, zum anderen die maximale Breite der Zeile. Zu Beginn der Methode testen wir, ob die Zeichenfolge komplett in eine Zeile passt. Wenn, dann haben wir nichts zu tun — wir müssen nicht umbrechen, und wir können diese Zeile direkt zurückgeben. Nur dann, wenn der Text länger ist, müssen wir etwas tun. Wir beginnen ab der Maximallänge von links nach einem Leerzeichen zu suchen. Hierfür eignet sich die Methode lastIndexOf(char, int)
perfekt. Denn der zweite Parameter sagt aus, ab welcher Stelle nach links gehend gesucht werden soll.
Die Suche könnte nun eine Fundstelle ergeben oder nicht. Das Leerzeichen könnte nicht in der Zeile sein, weil z. B. ein extrem langes Wort in der Zeile ist, oder wir haben eine Fundstelle. Nennen wir die Fundstelle breakIndex
, dann wäre das die Position, an der wir den Text umbrechen müssen.
Wir müssen uns entscheiden, was mit zu langen Wörtern in einer Zeile geschehen soll. Eine Lösung wäre, dass wir Wörter nicht zerlegen und sie länger als die Breite sein dürfen, oder das Wort muss zwangsgetrennt werden. Wir entscheiden uns (gemäß Aufgabenstellung) für den harten Umbruch. Wenn also der breakIndex
gleich -1
war, also kein Leerzeichen gefunden wurde, setzen wir den breakIndex
auf die maximal gewünschte Zeilenbreite width
.
Wir haben jetzt unsere Stelle mit dem Umbruch, also legen wir zwei Variablen an, eine für die erste Zeile und eine andere für den Rest. Die erste Zeile ist leicht berechnet: Wir bauen eine Teilzeichenfolge auf, die vom Anfang des Textes bis zum breakIndex
geht. Vom breakIndex
bis zum Ende der Zeichenfolge steht wiederum eine vielleicht sehr lange Zeichenfolge, die ebenfalls umgebrochen werden muss. Wiederholungen lassen sich durch Schleifen realisieren, aber auch durch Rekursion. Und Rekursion ist in unserem Fall sehr praktisch. Diese neuen Teil-Strings geben wir erneut in die wrap(…)
-Methode, die Schritt für Schritt immer neue erste Zeilen liefert.
Rekursionen brauchen immer ein Abbruchkriterium. In unserem Fall ist das ein String, der kleiner als die maximale Breite ist. Dann geht es aus der Rekursion heraus zurück zum Aufrufer. Der letzte Aufrufer von wrap(…)
bekommt also die letzte Zeile aus dem String in remaining
. Bleiben wir gedanklich am Ende der Zeichenfolge. Wir müssen die vorletzte und letzte Zeile mit einem Zeilenumbruch verbinden und dann zurückspringen zum Aufrufer der wrap(…)
-Methode. Da das rekursiv war, bekommt der Aufrufer die letzten beiden Zeilen und kann sie mit der drittletzten Zeile zusammenbringen. Das Spiel geht weiter, bis die Rekursion aufgelöst ist.
Rekursion ist für viele nicht ganz einfach. Es ist herausfordernd, sich die einzelnen Schritte verschachtelt vorzustellen. Auch können veränderbare Datenstrukturen schnell zum Problem werden, und man verliert die Übersicht, an welcher Stelle eigentlich was gelesen oder geschrieben wird. Die Empfehlung an dieser Stelle ist, Breakpoints oder Konsolenausgaben hinter die Belegung von firstLine
und hinter die Belegung von remaining
zu setzen sowie am Anfang der Methode wrap(…)
, wenn der String in die Methode hineinkommt.
1.3.21. Quiz: Wie viele String-Objekte?
Alle Zeichenfolgen in doppelten Anführungszeichen sind automatisch String
-Objekte. Die Java Virtual Machine legt Strings dieser Art in einem sogenannten Konstanten-Pool ab. Solche Strings existieren als Objekt nur ein einziges Mal. Das heißt, str1
, str3
und der String, der im Konstruktor übergeben wird, sind identisch. Befehlen wir der Laufzeitumgebung, mit new
ein neues Objekt zu bauen, dann bekommen wir am Ende auch ein neues Objekt, das heißt, str2
und str4
sind neue Objekte. Insgesamt hätten wir in dem Szenario ein String-Objekt im Konstanten-Pool und zwei neue aufgebaute Objekte durch das Schlüsselwort new
, die durch den GC auch wieder verschwinden würden, wenn sie nicht referenziert werden; die Strings im Konstanten-Pool bleiben bis zum Ende der Laufzeit bestehen.
1.3.22. Testen, ob die Frucht schokoladig umhüllt ist
private static final String FRUIT = "F";
public static boolean checkChocolate( String string ) {
return checkChocolate( string, 0 );
}
private static boolean checkChocolate( String string, int layer ) {
if ( string.isEmpty() )
return false;
if ( string.length() == 1 )
return string.equals( FRUIT ) && layer != 0;
if ( string.charAt( 0 ) != string.charAt( string.length() - 1 ) )
return false;
return checkChocolate( string.substring( 1, string.length() - 1 ),
layer + 1 );
}
Für die Aufgabe bietet sich eine rekursive Lösung an. Wir haben eine öffentlich zugängliche Methode, wie von der Aufgabe gewünscht, checkChocolate(string)
, und eine zweite private Methode checkChocolate(String, int)
, die von der Rekursion genutzt wird. Zusätzlich haben wir eine Variable FRUIT
für die Frucht selbst deklariert, sodass wir das Symbol verändern könnten.
Die interne Methode wird mit einem String aufgerufen und mit einer Ganzzahl, die bei jeder Schachtelung erhöht wird. Wir brauchen diese Variable, damit wir unterscheiden können, ob der String nur die Frucht enthält, aber keine Schokolade.
In der Methode checkChocolate(String, int)
prüfen wir, ob der String leer ist, in dem Fall fehlen die Schokolade, die Frucht und alles, und wir geben false
zurück. Die Übergabe von null
eskaliert in einer NullPointerException
. Ist der String ein Zeichen lang, gilt es, zwei Dinge zu testen: zunächst, ob es die gewünschte Frucht ist — bitte keine Nüsse —, aber auch, ob mindestens einmal rekursiv die Methode aufgerufen wurde, das heißt, mindestens eine Schicht Schokolade existiert. Denn wenn der allererste Aufruf mit einem String der Länge 1 ist, muss die Methode false
liefern.
Ist der String mehr als ein Zeichen lang, prüfen wir das erste und letzte Zeichen, ähnlich wie ein Palindromtest. Stimmen die Zeichen nicht überein, wird false
zurückgegeben. Stimmte das erste und das letzte Zeichen überein, dann geht es in die nächste Rekursionsrunde. Wir bauen einen Teil-String auf, der von der zweiten bis zur zweitletzten Stelle geht, erhöhen die Schachtelungstiefe um 1 und rufen damit checkChocolate(…)
rekursiv auf.
1.3.23. Von oben nach unten, von links nach rechts
Zum Lösen der Aufgabe studieren wir noch einmal den gegebenen String:
s u ey! ao
Es gibt verschiedene Lösungsansätze. Einer wäre, den String in Zeilen zu zerlegen, etwa mit String[] lines = string.split("\\n");
, und dann mit zwei ineinandergeschachtelten Schleifen erst über alle Spalten zu laufen (Breite lines[0].length()
), dann über die Zeilen.
Hier soll ein anderer Weg gezeigt werden. Legen wir die Zeilen aneinander und schauen uns an, welches Symbol an welchem Index steht:
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
Zeichen |
|
|
|
|
|
|
|
Am Ende soll das Ergebnis sea you!
erscheinen, also ist die Frage, welche Indizes wir dafür ansprechen müssen. Die Antwort:
0, 3, 6, 1, 4, 7, 2, 5
Die Folge ist nicht willkürlich, sie folgt einem Muster. Wir müssen eine Abbildung finden, die eine Zahl auf den Index überträgt, sodass wir das Zeichen unter dem Index auslesen können:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
i / 3 | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 2 |
i % 3 | 0 | 1 | 2 | 0 | 1 | 2 | 0 | 1 |
i % 3 × 3 | 0 | 3 | 6 | 0 | 3 | 6 | 0 | 3 |
i/3 + i % 3 × 3 | 0 | 3 | 6 | 1 | 4 | 7 | 2 | 5 |
Jetzt haben wir alles zusammen um die Lösung zu programmieren. Die Lösung besteht aus zwei Schritten:
static void printVerticalToHorizontalWriting( String string ) {
String oneliner = string.replace( "\n", "" );
int numberOfLines = string.length() - oneliner.length() + 1;
for ( int i = 0; i < oneliner.length(); i++ ) {
char c = oneliner.charAt( (i / numberOfLines)
+ (i % numberOfLines) * numberOfLines );
System.out.print( c );
}
}
Der Zeilenumbruch (Newline) wird durch die Escape-Sequenz \n
kodiert. Da der String aus mehreren Zeilen besteht, die durch \n
getrennt sind, wollen wir im ersten Schritt diese \n
-Zeichen entfernen.
Für den Algorithmus brauchen wir noch eine andere Kennzahl: die Anzahl der Zeilen. Hier können wir auf einen schönen Trick zurückgreifen. Haben wir die \n
-Zeichen gelöscht, ist der String kürzer, und zwar genau um die Anzahl, die es \n
-Zeichen gibt. Wenn wir also die Differenz aus der Originallänge und der Länge des Strings ohne \n
-Zeichen bilden, haben wir die Anzahl der Zeilen. Da die letzte Zeile nicht mit einem \n
abgeschlossen ist, müssen wir eins addieren. Bei unserem String aus dem Beispiel wären das zwei \n
-Zeichen + 1, also 3 Zeilen.
Die for
-Schleife erzeugt einen Zähler, den wir über die Formel auf die Position des Zeichens übertragen. Das Zeichen wird ausgelesen und auf dem Bildschirm ausgegeben.
1.3.24. Strings füllen
private static boolean hasText( String string ) {
return string == null || string.isEmpty();
}
private static String mix( String string, String fill ) {
if ( hasText( string ) ) return "";
if ( hasText( fill ) ) return string;
StringBuilder result = new StringBuilder();
for ( int i = 0; i < string.length() - 1; i++ ) {
char c = string.charAt( i );
result.append( c ).append( fill );
}
result.append( string.charAt( string.length() - 1 ) );
return result.toString();
}
Beginnen wir mit einer Prüfung der Parameter. Falls der String null
ist, oder leer, geben wir einen Leer-String zurück. Ist der string
nicht null
und hat er mindestens ein Zeichen, aber der Füll-String ist null
oder leer, so gibt es nichts zu tun, und wir nehmen eine Abkürzung, indem wir string
direkt zurückgeben. Prinzipiell wäre auch eine NullPointerException
angebracht, wenn string
oder fill
gleich null
wäre, denn wenn keine gültigen Objekte hereingereicht werden, sollten Methoden besser eine Ausnahme auslösen. Da der Test auf null
und auf die Existenz von Zeichen zweimal gemacht wird, ist diese Funktionalität in eine Extramethode hasText(String)
ausgelagert.
Wir beginnen mit einem Leer-String, den wir in einer Schleife füllen. Nachdem wir uns ein Zeichen extrahiert haben, hängen wir dieses Zeichen und die Zeichenfolge fill
an das bisherige Ergebnis. Wir laufen mit dem Index allerdings nicht bis zum allerletzten Zeichen, sondern nur bis zum vorletzten, damit wir immer die Füllzeichenfolge setzen können, nur nicht hinter das letzte Element. Daher ist die Abbruchbedingung < string.length() - 1
. Das letzte Zeichen hängen wir nach der Schleife zum Schluss an result
an und geben anschließend result
an den Aufrufer zurück.
Über die erste Fallunterscheidung haben wir getestet, dass der String mindestens ein Zeichen lang ist, deswegen haben wir auch die Möglichkeit, von 0
bis zum Index < string.length() - 1
zu laufen, denn wenn die String-Länge 1 ist, ist 1 - 1
gleich 0
, und wir haben keinen Schleifendurchlauf, nur das eine »letzte« Zeichen wird an result
gehängt.
Dass zwischen den Zeichen die Füllzeichenfolge gesetzt wird, kann auch anders gelöst werden: wir könnten den Index abfragen und schauen, ob er nicht für das letzte Zeichen steht, und nur dann die Füllzeichen anhängen. Steht der Index auf dem letzten Zeichen, kommen die Füllzeichen nicht hintendran. Eine andere Lösung wäre, zunächst ein StringBuilder
mit dem Originalstring aufzubauen und dann von rechts (Ende) bis links (Anfang) über den StringBuilder
zu laufen und mit insert(…)
den Teilstring einzufügen.
1.3.25. Mit dem Papagei das Alphabet üben
static String abcz() {
StringBuilder result = new StringBuilder();
for ( char c = 'A'; c <= 'Z'; c++ )
result.append( c );
return result.toString();
}
static String abcz( char start, char end ) {
if ( end < start )
return "";
StringBuilder result = new StringBuilder( end - start + 1 );
for ( char c = start; c <= end; c++ )
result.append( c );
return result.toString();
}
static String abcz( char start, int length ) {
return abcz( start, (char) (start + length - 1) );
}
Für die Lösung der Methode abcz()
greifen wir auf die praktische Besonderheit von Java zurück, dass char
ein numerischer Datentyp ist, mit dem man zählen kann. Eine Schleife kann so 'A'
bis 'Z'
generieren, und es entsteht das gesamte Alphabet. Alle Zeichen werden erst in einem StringBuilder
aneinandergehängt, bis zum Schluss der StringBuilder
über toString()
in einen String
konvertiert und zurückgegeben wird. StringBuilder
als Rückgabetyp ist ungewöhnlich und eher unpraktisch, da andere Stellen in der Regel wiederum String
-Objekte erwarten.
Bei der zweiten Methode, abcz(char start, char end)
, parametrisieren wir einfach start
und end
, und jetzt könnten wir sogar die erste Methode abcz()
umschreiben, sodass intern steht abcz('a', 'z')
. Da bei abcz(char start, char end)
fehlerhafte Argumente übergeben werden könnten, prüfen wir sie im ersten Schritt: Es darf nicht end
kleiner als start
sein. Gleich dürfen die Belegungen durchaus sein, denn wenn z. B. die Methode abcz('a', 'a')
aufgerufen wird, soll am Ende ein 'a'
erscheinen. Bei fehlerhaften Werten könnten wir auch eine Ausnahme auslösen, hier entscheiden wir uns für einen Leer-String als Rückgabe. Den StringBuilder
bauen wir mit einem int
-parametrisierten Konstruktor auf. Der Konstruktor bekommt eine Startgröße des internen Puffers übergeben; in unserem Fall ist die Gesamtlänge bekannt.
Die dritte Methode delegiert auf die zweite Methode, indem sie zum Startzeichen die Länge addiert und 1 abzieht, weil das letzte Zeichen ja schon inklusive ist. Der Aufruf von abcz('a', 1)
soll zu "a"
und nicht zu "ab"
führen.
Die letzten beiden Methoden hängen zusammen. Es ist nämlich völlig egal, welche der beiden Methoden wir implementieren; die eine Methode lässt sich immer auf die andere Methode abbilden. Hier haben wir die Methode mit den zwei Zeichen in der Parameterliste implementiert, und die andere Methode delegiert lediglich auf diese Implementierung.
Prinzipiell sind Methoden fehleranfällig, die sich nicht in der Anzahl der Parameter deutlich unterscheiden und deren Parametertypen sehr nah beieinander liegen. char
und int
sind beides numerische Datentypen. Entwickler müssen sehr genau darauf achten, dass sie nicht aus Versehen die falsche Methode aufrufen. Daher steht in der Implementierung von abcz(char, int)
auch eine Typumwandlung in char
, denn die Addition von einem char
und einem int
liefert ein int
und kein char
. Würden wir die Typumwandlung nicht dort einsetzen, würden wir in einer Rekursion endlos verhungern. Aufrufer der Methode haben das möglicherweise nicht im Blick und schreiben z. B. abcz('a', 'b' + 1)
— der Aufruf dürfte nicht gewollt sein. Mit gutem API-Design können wir Fehler reduzieren.
1.3.26. Quiz: Leicht angehängt
Die Konkatenation von String
-Objekten erzeugt temporär immer neue String
-Objekte. String
-Objekte referenzieren intern ein weiteres Objekt, und zwar ein Array mit den Zeichen. Das heißt, hinter einem String
-Objekt steht immer ein weiteres Objekt, das mit angelegt wird und vom Garbage Collector wieder entfernt werden muss. Hat man Schleifen mit vielen Durchläufen, sollte man es vermeiden, zur Konkatenation den +
-Operator einzusetzen.
Anders sieht es mit der StringBuilder
-Methode append(…)
aus. Auch sie nutzt intern ein Array für die Zeichen, doch es werden beim Anhängen (im besten Fall) keine neuen Objekte aufgebaut. Natürlich kann es sein, dass der interne Puffer des StringBuilder
-Objekts nicht ausreicht, sodass ein neues internes Array für die Zeichen aufgebaut werden muss, doch wenn man die Größe vom StringBuilder
abschätzen kann, entstehen bei der Konkatenation keine temporären Objekte. Natürlich muss, wenn am Ende ein String
-Objekt stehen muss, noch einmal toString()
aufgerufen werden, was erneut zu einem neuen Objekt führt. Insgesamt gibt es also drei Objekte in unserem Szenario: Wir bauen selbst ein StringBuilder
-Objekt mit new
auf, intern baut der StringBuilder
ein Array für die Symbole auf, und zum Schluss haben wir mit toString()
ein drittes Objekt.
1.3.27. Zahl in textuelle unäre Kodierung konvertieren
private static int ensurePositive( int value ) {
if ( value < 0 )
throw new IllegalArgumentException(
"Value is negative, but must be positive" );
return value;
}
static String encode( int... values ) {
StringBuilder codes = new StringBuilder( values.length );
for ( int value : values )
codes.repeat( "1", ensurePositive( value ) ).append( '0' );
return codes.toString();
}
Um Entwicklern fehlerhaften Eingaben zu melden, wird für jeden Eintrag im Array geprüft, ob er positiv ist. Das übernimmt die Hilfsmethode ensurePositive(int)
, die eine Ausnahme auslöst, wenn der Wert negativ ist; andernfalls liefert die Methode den übergebenen Wert zurück.
encode(int... values)
baut einen internen StringBuilder
auf. Es ist unklar, wie groß die zusammengesetzte Zeichenkette wird, denn sie wächst abhängig von den übergebenen Werten. Wir wissen allerdings, dass sie mindestens so groß ist wie die Anzahl der Array-Elemente, das heißt, wir können das als die Startkapazität des StringBuilder
-Objekts nutzen. Die äußere erweiterte for
-Schleife läuft über das Array und extrahiert jeden Wert value
. Mit "1".repeat( ensurePositive( value ) )
bekommen wir so viele Einsen wir nötig; ist value
gleich 0, ergibt der Ausdruck einen Leerstring. Es folgt grundsätzlich eine 0.
Zur Methode decode(…)
:
static int[] decode( String string ) {
if ( string.isEmpty() )
return new int[0];
if ( ! string.endsWith( "0" ) )
throw new IllegalArgumentException(
"String must end with 0 but did end with "
+ string.charAt( string.length() - 1 ) );
int arrayLength = 0;
for ( int i = 0; i < string.length(); i++ ) {
if ( string.charAt( i ) == '0' )
arrayLength++;
else if ( string.charAt( i ) != '1' )
throw new IllegalArgumentException(
"String can only contain 0 or 1 but found " + string.charAt( i ) );
}
int[] result = new int[ arrayLength ];
int resultIndex = 0;
int count = 0;
for ( int i = 0; i < string.length(); i++ ) {
if ( string.charAt( i ) == '1' )
count++;
else {
result[ resultIndex++ ] = count;
count = 0;
}
}
return result;
}
Die Methode decode(String)
prüft zunächst den eingehenden String. Wenn er leer ist, brauchen wir unseren Algorithmus erst gar nicht zu starten und können mit einem leeren Array zurückkehren. Außerdem muss die Zeichenfolge zwingend mit einer Null abgeschlossen werden — auch das prüfen wir und lösen andernfalls eine Ausnahme aus.
Ist der eingehende String korrekt, stehen wir von dem Problem, dass wir durch das Betrachten des Strings nicht wissen, wie groß das Array für die Rückgabe ist. Daher zählt die erste Schleife die Anzahl Nullen, denn sie stimmt mit der Anzahl Einträge im zu erzeugenden Array überein. Zeichen für Zeichen läuft die for
-Schleife über die Zeichenfolge und zählt arrayLength
hoch, immer dann, wenn eine Null gefunden wurde. Wenn das andere Zeichen keine Eins ist, löst die Methode eine Ausnahme aus.
Nach dem ersten Schleifendurchlauf ist die Größe des Arrays bekannt, und das Array kann mit dieser Größe angelegt werden. Es folgt eine weitere for
-Schleife, die die Anzahl der Einsen zählt. Wenn eine Null folgt, findet die Folge der Einsen ihren Abschluss, und der Zähler count
wird in das Array geschrieben. Der Zähler wird zurückgesetzt, und es beginnt eine neue Suche nach den Einsen.
1.3.28. Gewicht durch Vertauschung verlieren
private static void swap( StringBuilder string, int i, int j ) {
if ( i == j ) return;
char temp = string.charAt( i );
string.setCharAt( i, string.charAt( j ) );
string.setCharAt( j, temp );
}
public static int cheatedWeight( int weight ) {
StringBuilder weightString = new StringBuilder().append( weight );
char smallestDigit = weightString.charAt( 0 );
int smallestDigitIndex = 0;
for ( int i = 1; i < weightString.length(); i++ ) {
char c = weightString.charAt( i );
if ( c != '0' && c < smallestDigit ) {
smallestDigit = c;
smallestDigitIndex = i;
}
}
swap( weightString, smallestDigitIndex, 0 );
return Integer.parseInt( weightString, 0, weightString.length(), 10 );
}
Unsere Methode cheatedWeight(…)
bekommt eine Ganzzahl und liefert eine Ganzzahl zurück. Prinzipiell lässt sich das Problem über einen arithmetischen Weg lösen, doch das ist aufwändig. Einfacher ist es, die ganze Zahl in eine Zeichenfolge zu konvertieren, die kleinste Ziffer zu finden und sie nach vorne zu setzen. Damit wir in einer Zeichenfolge Symbole vertauschen können, greifen wir auf einen veränderbaren StringBuilder
zurück. Wir deklarieren uns eine Hilfsmethode swap(…)
, die zwei Symbole an den gegebenen Stellen vertauscht. Zwar wird in unserem Fall immer mit der ersten Stelle getauscht, doch diese Utility-Methode könnte vielleicht für spätere Einsatzfelder relevant werden. Daher ist die Methode allgemein gehalten. Sie prüft am Anfang, ob die beiden Positionen nicht vielleicht gleich sind, dann muss nichts vertauscht werden. Anderenfalls wird das Zeichen an der Stelle i
extrahiert und in einer Zwischenvariablen abgelegt, anschließend das Zeichen an der Stelle j
an die Stelle i
und dann das zwischengespeicherte Symbol an die Stelle j
gesetzt.
Bei der Methode cheatedWeight(…)
finden wir in der Schleife die Ziffer mit dem kleinsten Wert und merken uns auch gleich die Position. Ignorieren müssen wir allerdings 0
; sie ist natürlich kleiner als alle anderen Ziffern, allerdings verbietet die Aufgabenstellung das Nachvornesetzen der 0
.
Nach dem Durchlauf der Schleife vertauschen wir die Ziffer an der Stelle smallestDigitIndex
mit der ersten Ziffer. Zum Schluss müssen wir den StringBuilder
in eine Ganzzahl konvertieren; die Integer-Klasse bietet dafür die Methode:
static int parseInt(CharSequence s, int beginIndex, int endIndex, int radix) throws NumberFormatException
StringBuilder
ist eine besondere CharSequence
. Die Methode sagt im Prinzip Folgendes: Gib mir eine beliebige Zeichenfolge, eine Startposition und eine Endposition sowie eine Radix — 10
für das gewöhnliche Dezimalsystem —, und ich konvertiere dir diesen Bereich in eine Ganzzahl.
1.3.29. Vokale entfernen
Unterschiedliche Lösungsansätze sollen aufgeführt werden:
public static String removeVowels1( String string ) {
string = string.replace( "a", "" ).replace( "A", "" );
string = string.replace( "ä", "" ).replace( "Ä", "" );
string = string.replace( "e", "" ).replace( "E", "" );
string = string.replace( "o", "" ).replace( "O", "" );
string = string.replace( "ö", "" ).replace( "Ö", "" );
string = string.replace( "u", "" ).replace( "U", "" );
string = string.replace( "ü", "" ).replace( "Ü", "" );
string = string.replace( "i", "" ).replace( "I", "" );
string = string.replace( "y", "" ).replace( "Y", "" );
return string;
}
Die erste Lösung ist recht simpel, aber auch mit einem hohen Anteil Codeduplikation. Die replace(…)
-Methode ist überladen: Mit einer Variante können wir Zeichen durch andere Zeichen ersetzen, mit der anderen Variante lassen sich Zeichenfolgen durch Zeichenfolgen ersetzen. Die replace(char, char)
-Methode ist nicht in der Lage, ein Zeichen zu löschen, wohl aber die zweite Variante, wo wir einen String, egal wie lang er ist, durch einen Leer-String ersetzen und somit entfernen können.
Variante Nummer 2:
public static String removeVowels2( String string ) {
char[] chars = new char[ string.length() ];
int len = 0;
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( "aeiouöäüyAEIOUÄÖÜY".indexOf( c ) < 0 )
chars[ len++ ] = c;
}
return new String( chars, 0, len );
}
Die zweite Methode baut sich einen temporären char
-Puffer auf, indem sie zunächst alle Zeichen sammelt, die keine Vokale sind. Dieser Puffer von Zeichen kann kleiner sein als der String, aber nicht größer. Zum Start bauen wir daher ein char[]
mit der maximalen Anzahl zu erwartenden Zeichen auf, und das ist die Länge des eingehenden Strings. In einer neuen Variablen len
merken wir uns die Größe des neuen entstehenden Arrays. Eine Schleife läuft jetzt über alle Zeichen des Strings. Im nächsten Schritt müssen wir testen, ob das Zeichen ein Vokal ist oder nicht. Diese Lösung, wie auch die folgenden, nutzen hier ganz andere Herangehensweisen. Eine gute Möglichkeit ist der Test mit indexOf(char)
. Wir sammeln zunächst alle die Zeichen, die wir finden wollen, in einem String. Anschließend testet indexOf(char)
, ob das Zeichen, das wir betrachten, in diesem Teil-String ist oder nicht. Antwortet indexOf(…)
mit einem positiven Ergebnis, wissen wir, dass das Zeichen in der Zeichenfolge vorkam, also ein Vokal war. Da wir alle Vokale entfernen wollen, drehen wir die Bedingung einfach um; indexOf(char)
liefert - 1, wenn das Zeichen nicht im String war. Und wenn das Zeichen nicht im String war, setzen wir das Zeichen in das Array und erhöhen die Position. Am Ende der Schleife sind wir einmal über den Eingangs-String gelaufen, und haben ausgewählte Zeichen in das Array gesetzt. Nun müssen wir das Array wieder in einen String konvertieren. Dazu bietet die String
-Klasse einen passenden Konstruktor.
Die dritte Variante unterscheidet sich in zwei Details von der vorherigen Variante:
public static String removeVowels3( String string ) {
StringBuilder result = new StringBuilder( string.length() );
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
switch ( c ) {
case 'a', 'e', 'i', 'o', 'u', 'y', 'ä', 'ö', 'ü' -> { }
default -> result.append( c );
}
}
return result.toString();
}
Das erste Unterschied ist, dass kein Array als Zwischenspeicher benutzt wird, sondern ein StringBuilder
, an den mit append(…)
das Zeichen gehängt wird, das kein Vokal ist. Die zweite Änderung betrifft die Frage, ob das Zeichen ein Vokal ist. Hier greifen wir auf die moderne switch
-Anweisung mit der Pfeilschreibweise zurück.
Die vierte Lösung greift auf eine eigene Methode isVowel(char)
zurück, die testet, ob ein Zeichen ein Vokal ist.
private static boolean isVowel( char c ) {
return "aeiouyäöüAEIOUYÄÖÜ".indexOf( c ) >= 0;
}
public static String removeVowels4( String string ) {
StringBuilder result = new StringBuilder( string.length() );
for ( int i = 0; i < string.length(); i++ ) {
char c = string.charAt( i );
if ( ! isVowel( c ) )
result.append( c );
}
return result.toString();
}
Die Überlegung ist nämlich, ob eine Methode, die Vokale entfernt, auch die Entscheidung treffen sollte, was ein Vokal ist. Wollen wir gut programmieren, dann sollte eine einzelne Methode nicht zu viel können. Daher ist es vernünftig, eine Methode zu haben, die testet, ob ein Zeichen ein Vokal ist, und eine andere Methode, die Vokale aus einer Zeichenfolge entfernen kann. Beide haben unterschiedliche Aufgaben.
Die folgenden beiden Lösungen greifen thematisch schon etwas vor und nutzen ganz geschickt reguläre Ausdrücke.
public static String removeVowels5( String string ) {
return string.replaceAll( "[aeiouyäöüAEIOUYÄÖÜ]", "" );
}
Mit der entsprechenden replaceAll(…)
-Methode lässt sich die Aufgabe mit einem Einzeiler lösen. replaceAll(String, String)
bekommt als erstes Argument einen regulären Ausdruck, der hier für eine Gruppe von Zeichen steht. Passt der reguläre Ausdruck auf ein Zeichen, wird das Zeichen durch einen Leer-String ersetzt, also entfernt.
Die letzte Lösung geht einen anderen, recht kreativen Weg.
public static String removeVowels6( String string ) {
String result = "";
String[] tokens = string.split( "[aeiouyäöüAEIOUYÄÖÜ]" );
for ( String value : tokens )
result += value;
return result;
}
Statt die Zeichen durch nichts zu ersetzen, sind die Vokale hier Trennzeichen. Die split(…)
-Methode liefert uns folglich alle Teilzeichenfolgen vor oder hinter einem Vokal. Diese Teilzeichenfolgen können wir zu einem Ergebnis-String wieder zusammensetzen.
1.3.30. Don’t shoot the Messenger
private static String charAtOrEmpty( String string, int index ) {
return index < string.length() ? string.substring( index, index + 1 ) : "";
}
private static String joinSplitMessages( String... parts ) {
int maxStringLength = 0;
for ( String part : parts )
maxStringLength = Math.max( maxStringLength, part.length() );
StringBuilder result = new StringBuilder();
for ( int index = 0; index < maxStringLength; index++ )
for ( String part : parts )
result.append( charAtOrEmpty( part, index ) );
return result.toString();
}
Wenn wir später über alle Teile der Boten laufen, könnten Zeichen fehlen, weil die Übertragung nicht vollständig ist; ein Beispiel ist "H", "", "ooky"
, wo der zweite String kein Zeichen an der Stelle 0 hat. Um möglichen Ausnahmen entgegenzuwirken, führen wir eine eigene Methode charAtOrEmpty(String, int)
ein, die von einem String
auf ein Zeichen an einer Position zurückgreift; wenn das Zeichen an dieser Stelle nicht existiert, weil der String nicht so lang ist, wird ein Leer-String zurückgegeben.
Die Methode |
Die eigentliche Methode joinSplitMessages(…)
nimmt ein Vararg von Zeichenfolgen entgegen. Wir prüfen nicht auf null
, sondern lassen es auf eine NullPointerException
ankommen, die dann folgen wird, wenn die erweiterte for
-Schleife auf das Array zurückgreift.
Der Algorithmus besteht aus zwei Schritten. Im ersten Schritt wird von allen parts
die maximale String-Länge ermittelt. Der Hintergrund ist, dass Boten weniger Daten übermitteln könnten, wir uns also an dem Booten orientieren, der die größte Anzahl von Zeichen hat. Die Abfrage ist also für ein Szenario wie joinSplitMessages("H", "", "ooky")
gedacht. Am Ende der Schleife steht in der Variablen maxStringLength
die Länge des längsten Strings.
Im zweiten Schritt erfragen wir das erste Zeichen des ersten Teils, dann das erste Zeichen des zweiten Teils, das erste Zeichen des dritten Teils und so weiter. Im nächsten Schritt erfahren wir das zweite Zeichen des ersten Teils, das zweite Zeichen des zweiten Teils und so weiter. Die äußere Schleife generiert Indizes von 0 bis zur maximalen Länge aller Zeichenfolgen, und die innere Schleife läuft über alle Teile der Boten. Um ein Zeichen der Zeichenfolge des Teils zu erfragen, greifen wir auf unsere eigene Methode zurück, die dafür sorgt, dass es zu keiner Ausnahme kommt, wenn an der Stelle index
kein Zeichen existiert.
Nach dem Durchlauf der Schleifen konvertieren wir den StringBuilder
in einen String
, und geben den String
zurück.
1.3.31. Wiederholte Leerzeichen komprimieren
public static final String TWO_SPACES = " ";
static String compressSpace( String string ) {
return compressSpace( new StringBuilder( string ) ).toString();
}
static StringBuilder compressSpace( StringBuilder string ) {
int index = string.lastIndexOf( TWO_SPACES );
while ( index >= 0 ) {
string.delete( index, index + TWO_SPACES.length() ); // Remove both spaces
index = string.lastIndexOf( TWO_SPACES, index - 1 ); // Continue searching backwards
}
return string;
}
Aus praktischen Gründen schreiben wir eine überladene compressSpace(…)
-Methode, eine Variante mit dem Parametertyp String
und dem Rückgabetyp String
, ein zweites Mal mit dem Parametertyp StringBuilder
, und diese Variante liefert einen StringBuilder
zurück. Die String
-Variante ist für Nutzer praktischer, denn Strings sind als Typ häufiger aufzutreffen als StringBuilder
. Die Variante mit dem String
-Parameter macht sich dann die Mühe der Konvertierung in einen StringBuilder
und zurück. Neben den beiden Methoden legen wir eine Konstante TWO_SPACES
an, die zwei Leerzeichen enthält.
Wenn wir an einer Zeichenfolge Veränderungen vornehmen müssen, haben wir prinzipiell zwei Möglichkeiten: Die eine ist, einen neuen String Zeichen für Zeichen neu aufzubauen, in unserem Fall also mit allen Zeichen, aber nicht zwei Leerzeichen hintereinander. Die andere Möglichkeit ist, eine existierende Zeichenfolge zu verändern. Diese Lösung wählen wir.
Wir gehen mit lastIndexOf(…)
von rechts nach links, also von hinten nach vorne über den StringBuilder
, und suchen nach zwei Leerzeichen. Haben wir als Ergebnis eine Fundstelle größer oder gleich 0, dann löschen wir beide Leerzeichen an dieser Stelle mit delete(index, index + 2)
. Wie bei Bereichsangaben in Java üblich ist auch bei der Methode delete(int start, int end)
der end
-Parameter exklusiv — das bedeutet, wenn wir an index
das erste der beiden Leerzeichen gefunden haben, müssen wir index + 2
als end
-Parameter übergeben, damit der Bereich beide Leerzeichen umfasst.
Anschließend suchen wir mit lastIndexOf(…)
weiter nach dem nächsten Vorkommen von zwei Leerzeichen. Sobald keine zwei Leerzeichen mehr gefunden werden, also lastIndexOf(…)
das Ergebnis -1 zurückgibt, ist der gesamte StringBuilder
von rechts nach links durchsucht und die Methode beendet die Schleife.
Es ist vielleicht merkwürdig, dass die Methode lastIndexOf(…)
verwendet wird und nicht indexOf(…)
, die von links nach rechts läuft. Beides funktioniert. Allerdings ist es bei Löschoperationen performanter, wenn weniger Daten gelöscht werden. Würden wir also von links über die Zeichenkette laufen und würden zwei Leerzeichen finden, müssten wir alles, was dahintersteht, um eine Position nach links verschieben. Da sich rechts aber noch weitere zwei Leerzeichen befinden können, würden wir sie mit verschieben, auch wenn sie später sowieso verschwinden. Laufen wir von rechts nach links, finden wir rechts von unserer aktuellen Position keine zwei Leerzeichen mehr, das heißt, wir verschieben keine unnötigen Leerzeichen.
1.3.32. Knacken und Knistern einfügen und entfernen
private static final String CRACK = "♬CRACK♪";
public static String crackle( String string ) {
int capacity = string.length() + string.length() * CRACK.length() / 10;
StringBuilder result = new StringBuilder( capacity );
result.append( string );
for ( int i = string.length() - 1; i >= 0; i-- )
if ( Math.random() < 0.1 )
result.insert( i, CRACK );
return result.toString();
}
public static String decrackle( String string ) {
return string.replace( CRACK, "" );
}
Wie deklarieren zunächst eine private finale Konstante CRACK
mit dem Knisterstring, sodass wir die Zeichenfolge leicht ändern können. Zwei Stellen greifen später auf CRACK
zurück, beim Einbauen des Knisterns und auch beim Löschen des Knisterns.
Die crackle(…)
-Methode bekommt einen String
und liefert einen String
zurück. Es gibt unterschiedliche Herangehensweisen an die Aufgabe. Die Lösung hier kopiert den Übergabe-String in einen dynamischen StringBuilder
, um an entsprechenden Stellen das Knistern einzubauen. Wir generieren mit einer for
-Schleife potenzielle Einfügepositionen und entscheiden zufällig, ob wir das Knistern einbauen oder nicht. Zwei Besonderheiten gibt es bei dem Programm. Die erste ist, dass wir nicht von links nach rechts, also nicht vom Anfang bis zum Ende die Indizes generieren, sondern von rechts nach links gehen und uns nach vorne vorarbeiten. Der Grund dieses Herangehens ist, dass wir so vermeiden können, dass ein Knistern ein anderes Knistern überschreibt. Denn wenn wir an einer Stelle ein Knistern einfügen, dann wächst der String vom Index aus gesehen nach rechts, aber nicht nach links. Wenn wir mit dem Index weiter nach links laufen und dort später ein Knistern einfügen, kann es niemals zu einer Überschneidung kommen. Natürlich könnten wir, wenn wir von links nach rechts laufen und ein Knistern einbauen, auch den Index um die Länge von CRACK
erhöhen.
Der zweite Trick besteht darin, zu entscheiden, ein Knistern einzufügen. Eine Fallunterscheidung mit Math.random() < 0.1
wird mit einer zehnprozentigen Wahrscheinlichkeit ausgeführt. Das Einfügen übernimmt die insert(…)
-Methode des StringBuilder
-Objekts.
Die Methode decrackle(…)
ist einfacher, denn wir können hier auf die bekannte replace(…)
-Methode der String
-Klasse zurückgreifen. Wir lassen nach dem Knister-String suchen und ersetzen ihn durch den Leer-String.
1.3.33. CamelCase-Strings zerlegen
private static String camelCaseSplitter( String string ) {
StringBuilder result = new StringBuilder( string );
for ( int i = 1; i < result.length(); i++ ) {
char previousChar = result.charAt( i - 1 );
char currentChar = result.charAt( i );
boolean isPreviousCharLowercase = Character.isLowerCase( previousChar );
boolean isCurrentCharUppercase = Character.isUpperCase( currentChar );
if ( isPreviousCharLowercase && isCurrentCharUppercase ) {
result.insert( i, " " );
i++;
}
}
return result.toString();
}
Für die Aufgabe gibt es unterschiedliche Lösungen. Der hier gewählte Ansatz kopiert die Zeichenfolge in einen StringBuilder
und fügt, falls nötig, Leerzeichen ein. Eine Korrektur von Groß- in Kleinbuchstaben ist nicht vorzunehmen, lediglich Leerzeichen sind an der richtigen Stelle einzufügen. Die Stelle, die wir finden müssen, ist ein Wechsel zwischen einem Klein- und einem Großbuchstaben. Folgt ein Kleinbuchstabe auf einen anderen Kleinbuchstaben oder folgt ein Großbuchstabe auf einen anderen Großbuchstaben, so können wir das ignorieren.
Haben wir den StringBuilder results
mit dem Original-String aufgebaut, dann wird der Konstruktor eine NullPointerException
auslösen, wenn der übergebene String null
war. Das ist eine akzeptable Reaktion. Wir beginnen anschließend, die Zeichenfolge abzulaufen, doch da sich die Länge der Zeichenfolge durch die hinzugefügten Leerzeichen ändert, laufen wir nicht den Parameter string
ab, sondern result
, den StringBuilder
. Nun betrachten wir immer Pärchen. Die Schleife generiert uns Indizes i
und an der Stelle i
steht das aktuelle Zeichen und an der Stelle i - 1
das vorherige Zeichen. Daher muss die Schleife auch bei 1
beginnen, andernfalls hätten wir einen Index von -1
generiert, was am Anfang zu einem Fehler führt. Ist das Argument kein oder ein Zeichen lang, wird der Schleifenrumpf nicht betreten.
Die beiden Zeichen previousChar
und currentChar
werden nun getestet, ob das vorherige Zeichen ein Kleinbuchstabe war und das folgende Zeichen ein Großbuchstabe. Wir verlassen uns auf die Antwort der Character
-Methoden, die für alle Unicode-Zeichen korrekt Antworten liefern. Die Fallunterscheidung prüft das Kriterium, dass ein Großbuchstabe auf einen kleinen Buchstaben folgen muss. In diesem Fall setzen wir an der Stelle ein Leerzeichen. Damit verschiebt sich der ganze nachfolgende Block von Zeichen um eine Stelle nach rechts. Haben wir einen Wechsel erkannt, dann zeigt der Index i
auf den Großbuchstaben. Haben wir an dieser Stelle ein Leerzeichen eingeschoben, steht der Index danach auf dem Leerzeichen. Leerzeichen müssen wir über unseren Algorithmus allerdings nicht prüfen. Wir können daher den Index für das Leerzeichen um eins erhöhen, sodass anschließend der Index auf dem folgenden Großbuchstaben steht. Da es anschließend im Fortschaltausdruck der Schleife weitergeht, wird der Index noch einmal erhöht. Damit geht der Index hinter den Großbuchstaben, und der Test wird für die folgenden Zeichen fortgeführt. Fehlt i++
, wird das nicht auffallen, da Leerzeichen keine Buchstaben sind, doch mit der Erhöhung von i
spart der Algorithmus einen Vergleich.
Am Ende der Schleife wird der dynamische StringBuilder
in einen String
konvertiert und zurückgegeben.
Alternative Implementierungen können so aussehen, dass sie einen StringBuilder
erst aufbauen und nicht später modifizieren, es gibt auch eine Lösung mit regulären Ausdrücken.
Eine Lösung mit einem regulären Ausdruck ist möglich, aber nicht trivial:
|
In Java ist die übliche Schreibweise für Bezeichner CamelCase; jedes neue Segment beginnt mit einem Großbuchstaben. Beispiele sind: ArrayList
, numberOfElements
.
1.3.34. Wörter unterstreichen
public static void printUnderline( String string, String search ) {
System.out.println( string );
string = string.toLowerCase();
search = search.toLowerCase();
StringBuilder secondLine = new StringBuilder();
for ( int index = 0;
(index = string.indexOf( search, index )) >= 0;
index += search.length() ) {
secondLine.append( " ".repeat( index - secondLine.length() ) )
.append( "-".repeat( search.length() ) );
}
System.out.println( secondLine );
}
Die Aufgabe hat eine interessante Logik, was auf den ersten Blick das Verständnis erschwert. Wir müssen Folgendes schaffen: alle Teil-Strings finden und dann überall dort, wo der Such-String nicht auftaucht, Leerzeichen setzen und unter alle TeilStrings Minuszeichen.
Die printUnderline(…)
-Methode gibt zunächst den Text gefolgt vom Zeilenumbruch aus. Um die Suche unabhängig von der Groß-/Kleinschreibung zu realisieren, konvertieren wir string
und search
in Kleinbuchstaben. indexOf(…)
sucht dann in einem kleingeschriebenen String nach einem kleingeschriebenen Teil-String.
Mit secondLine
haben wir eine Variable, die im Laufe der Zeit wächst, sodass sie zum Schluss ausgegeben werden kann. Der Vorteil ist, dass wir, wenn die Methode printUnderline(…)
einmal String
zurückgeben soll, wenig Code ändern müssen. Der zweite Vorteil liegt in der Möglichkeit, die Länge abzufragen — so lässt sich ermitteln, wie viele Zeichen schon geschrieben wurden.
Eine Schleife findet alle Positionen mit dem gesuchten zu unterstreichenden String search
. Die for
-Schleife ist komplex, weil im Bedingungsausdruck für den Abbruch der Schleife auch eine Zuweisung steht. Die Idee ist folgende: Wir aktualisieren die Variable index
mit der Fundstelle, und wenn dieser größer gleich 0
ist, haben wir eine Fundstelle und führen den Rumpf der Schleife aus. In dem Moment, wo indexOf(…)
keinen Fund mehr meldet, brechen wir die Schleife ab. Es gibt also genau so viele Durchläufe, wie es Fundstellen von search
gibt.
Der Rumpf der Schleife sind zwei Dinge nötig:
Leerzeichen produzieren, und zwar genauso viele, dass wir von der letzten Stelle (
secondLine.length()
) bis zur aktuellen Fundstelle (index
) kommen. Anders gesagt: wir setzenindex - secondLine.length()
viele Leerzeichen.so viele Minuszeichen an die
secondLine
anhängen wie derString
search
lang ist.
Beides lässt sich prima mit der repeat(…)
-Methode realisieren, die schnell ein gewünschtes Zeichen (oder Zeichenfolge) vervielfacht.
Als Letztes aktualisieren wir im Fortschaltausdruck der for
-Schleife die Variable index
um die Länge des zu suchenden Strings, denn nach dem letzten Fund muss direkt nach dem Such-String weitergemacht werden. Anders gesagt: indexOf(…)
startet bei der nächsten Suche am Ende des letzten Strings, ab index
.
1.3.35. Caesar-Verschlüsselung implementieren
Der Lösungsvorschlag besteht aus den Methoden caesar(…)
für die Verschlüsselung und decaesar(…)
zur Entschlüsselung sowie einer weiteren privaten Methode mit dem Namen rotate(…)
:
public static final int ALPHABET_LENGTH = 26;
private static int rotate( int c, int rotation ) {
if ( rotation < 0 )
throw new IllegalArgumentException(
"Rotation is not allowed to be negative, but was " + rotation );
if ( c >= 'A' && c <= 'Z' ) // Character.isUpperCase( c ) is too broad
return 'A' + (c - 'A' + rotation) % ALPHABET_LENGTH;
else if ( c >= 'a' && c <= 'z' )
return 'a' + (c - 'a' + rotation) % ALPHABET_LENGTH;
else
return c;
}
Die private Methode int rotate(int c, int rotation)
verschiebt ein Zeichen um eine gewisse Anzahl Positionen — wir nennen das auch Distanz. Da wir Groß- sowie Kleinbuchstaben betrachten wollen, gibt es zwei Fallunterscheidungen. Außerdem kann es sein, dass das Zeichen weder ein Groß- noch ein Kleinbuchstabe ist, und dann wird das Originalzeichen unverändert zurückgegeben. In der Konstante ALPHABET_LENGTH
lagern wir die Länge des Alphabets aus, also 26
. Negative Verschiebungen sind nicht erlaubt und führen zu einer Ausnahme.
Die Programmlogik ist für Groß- und Kleinbuchstaben prinzipiell gleich, daher wollen wir uns den Ausdruck stellvertretend für die Großbuchstaben anschauen. Auf den ersten Blick ist die Lösung einfach: Wir addieren die Distanz zur Unicode-Position des Zeichens c
. Haben wir ein Zeichen wie 'W'
und addieren 3, landen wir bei 'Z'
. Probleme bereitet uns der Umbruch, dass es also hinter 'Z'
wieder mit 'A'
weitergehen muss. Natürlich könnte eine Fallunterscheidung prüfen, ob wir über das 'Z'
hinauslaufen, und dann die Länge des Alphabets, also 26, abziehen, doch gibt es eine andere Lösung für das Problem, die ohne Fallunterscheidung auskommt: Bei dieser Lösung addieren wir nicht die Distanz zum Zeichen c
. Die Überlegung ist vielmehr, was wir zum Startbuchstaben 'A'
addieren müssen, um zum Buchstaben c
zu kommen, und dann auch noch um die Distanz verschoben. Das sind zwei Teile. Mit c - 'A'
haben wir genau den Abstand berechnet, den wir addieren müssen, um vom Startbuchstaben 'A'
auf c
zu kommen. 'A' + (c - 'A')
ist gleich c
. Da wir vom Startbuchstaben einen Abstand von rotation
haben möchten, addieren wird die Distanz, also 'A' + (c - 'A' + rotation)
. Das sieht gekürzt wie c + rotation
aus, doch es gibt einen feinen Unterschied, nämlich dass wir jetzt den geklammerten Ausdruck % ALPHABET_LENGTH
nehmen können, sodass uns 'Z' + 1
wieder zum 'A'
führt.
public static String caesar( String s, int rotation ) {
StringBuilder result = new StringBuilder( s.length() );
for ( int i = 0; i < s.length(); i++ )
result.append( (char) rotate( s.charAt( i ), rotation ) );
return result.toString();
// Freaky solution
// IntUnaryOperator rotation = c -> rotate( c, rotation );
// return s.chars().map( rotation ).mapToObj( Character::toString )
// .collect( Collectors.joining() );
}
public static String decaesar( String s, int rotation ) {
return caesar( s, ALPHABET_LENGTH - rotation );
}
Die Methode String caesar(String s, int rotation)
ist dann selbst ohne Überraschung. Wir bauen einen internen StringBuilder
auf, in dem wir das Ergebnis sammeln, laufen dann einmal von vorne nach hinten über den Eingangs-String, schnappen jedes Zeichen und lassen es rotieren und setzen es dann in denen Container. Zum Schluss konvertieren wir den StringBuilder
in einen String
und geben ihn zurück.
Die Methode decaesar(…)
nutzt eine schöne Eigenschaft, und zwar, dass wir nach einer gewissen Anzahl Verschiebungen wieder beim Ursprungszeichen landen. Und diese Anzahl Verschiebungen ist gerade die Größe des Alphabets, also 26. Was passiert, wenn wir das Zeichen nicht um 26 Positionen verschieben, sondern nur um 25? Dann hätten wir das Zeichen nicht nach »rechts geschoben«, sondern nach »links«; aus einem B
würde dann kein C
mehr, sondern aus dem B
ein A
. Wir können folglich entschlüsseln mit der Position ALPHABET_LENGTH - rotation
, was das Zeichen wieder nach links zurück in die Ursprungsposition schiebt. Einen Unterschied zu caesar(…)
ist allerdings, dass wir durch die Subtraktion nicht ins Negative gehen dürfen, weil sonst eine Exception droht. Das ist natürlich nicht symmetrisch zu caesar(…)
, weil die Distanz beliebig sein darf.