1. Zeichenkettenverarbeitung

Im Folgenden wollen wir uns mit Zeichen und Zeichenfolgen beschäftigen und insbesondere mit den Datentypen char, Character, String und StringBuilder.

1.1. String-Objekte

1.1.1. Zugriff auf bestimmte Zeichen ☆

Man sagt Japanern nach, dass sie statt "l" ein "r" sprechen. Das wollen wir ausprobieren, wie sich das "anhört".

  1. Lege eine neue Klasse TalkLikeAJapanese an.

  2. Setze in die Klasse eine neue Klassenmethode void talkJapanese(String s), die einen übergebenen java.lang.String auf dem Bildschirm bringt, aber den Buchstaben "r"/"R" als "l"/"L" ausgibt. Es gib nicht darum einen String zurückzuliefern.

  3. Teste die Methode mit einer ´main`-Methoden.

  4. Schreibe nicht nur eine Variante, sondern versuche mindestens zwei Varianten zu programmieren.

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

public class TalkLikeAJapanese {

static void talkJapanese1( String text ) {
text = text.replace( 'r', 'l' );
text = text.replace( 'R', 'L' );
System.out.println( text );
}

static void talkJapanese2( String text ) {
for ( int i = 0; i < text.length(); i++ ) {
char c = text.charAt( i );

if ( c == 'r' )
c = 'l';
else if ( c == 'R' )
c = 'L';

System.out.print( c );
}
System.out.println();
}

static void talkJapanese3( String text ) {
char[] c = text.toCharArray();

for ( int i = 0; i < c.length; i++ ) {
if ( c[ i ] == 'r' )
c[ i ] = 'l';
else if ( c[ i ] == 'R' )
c[ i ] = 'L';
}
String result = new String( c );
System.out.println( result );
}

static void talkJapanese4( String text ) {
for ( int i = 0; i < text.length(); i++ ) {
switch ( text.charAt( i ) ) {
case 'r':
System.out.print( 'l' );
break;
case 'R':
System.out.print( 'L' );
break;
default:
System.out.print( text.charAt( i ) );
}
}
}

public static void main( String[] args ) {
talkJapanese1( "Riesenrad" );
talkJapanese2( "Riesenrad" );
talkJapanese3( "Riesenrad" );
talkJapanese4( "Riesenrad" );
}
}

String sind unveränderbar, sie sind immutable. Das bedeutet, es gibt keine Methode, die eine Ersetzung im aktuellen String vornimmt. Es gibt jedoch eine replace(…​)-Methode, die uns sehr entgegenkommt. Sie liefert, wie so viele andere String-Methoden auch, ein modifiziertes String-Objekt zurück. Das ist üblich im API-Design mit immitable Datentypen. Es gibt unterschiedliche replaceXXX(…​)-Methode. Wir interessieren uns für die Version, die ein Zeichen durch ein anderes Zeichen ersetzt und das ist replace(char, char). damit ist die erste Lösung fertig. Sie hat auch den Vorteil, dass zum Schluss ein String entsteht, den wir prinzipiell mit return wieder zurückgeben können.

Die zweite Lösung geht etwas anders an die Aufgabe heran, denn hier laufen wir den spring selbständig von vorne nach hinten ab. Wir können die Anzahl der Zeichen erfragen, und jedes Zeichen an einer Position. Wenn wir das Zeichen an einer Position haben, können wir das Zeichen überprüfen und durch ein anderes Zeichen ersetzen, was wir schlussendlich ausgeben. Bei dieser Variante ist eine Ausgabe fest verbaut, und wir erzeugen temporär keine neuen String.

Die dritte Variante ist eine Erweiterung der zweiten Variante, allerdings mit dem Unterschied, dass wir am Ende ein String generieren, den wir ausgehen. Da Strings nicht veränderbar sind, nutzen wir als Zwischenspeicher ein char-Array. Bei dieser Lösung wäre es prinzipiell nicht nötig am Ende ein String zu produzieren, weil es println(char[]) gibt, aber mit dieser Schreibweise lässt sich die Methode leicht erweitern, wenn das Ergebnis als String auch wiederum zurückgegeben werden soll.

Auch die vierte Variante läuft von Hand über den String. Statt jedoch die einzelne Zeichen über == zu vergleichen, nutzt diese Lösung eine switch- case-Konstruktion.

1.1.2. Wörter unterstreichen ☆☆

Um in ASCII-Texte Teile hervorzuheben, können sie unterstrichen werden. Das kann in zwei Zeilen so aussehen:

Man wird nicht dadurch besser, dass man andere schlecht macht.
                       ------
  1. Lege eine neue Klasse PrintUnderline an.

  2. Schreibe eine neue statische Methode printUnderline(String text, String search), die jede Zeichenkette underline in s wie im oberen Beispiel gezeigt unterstreicht. Bedenke, dass s mehrmals im String vorkommen kann.

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

public class PrintUnderline {

public static void main( String[] args ) {
String text = "Hallo super Welt, heute ist das Wetter aber super schön";
underline( text, "super" );
}

private static void outputChar( char c, int count ) {
for ( int i = 0; i < count; i++ )
System.out.print( c );
}

static void underline( String text, String search ) {
System.out.println( text );

for ( int nextIndex = 0, lastIndex = 0;
(nextIndex = text.indexOf( search, lastIndex )) >= 0; ) {

outputChar( ' ', nextIndex - lastIndex );
outputChar( '-', search.length() );
lastIndex = nextIndex + search.length();
}
}
}

Die Aufgabe ist anspruchsvoll wegen ihrer Logik, und weniger wegen der Java Sprachmittel und Rückgriff auch besondere Bibliotheks-Methoden. Wir müssen folgendes schaffen: alle Teilstrings finden, und dann überall dort, wo der Suchstring nicht auftaucht, Leerzeichen setzen, und unter alle Teilstrings Minuszeichen. Neben der zu implementierenden Methode underline(…​) stellen wir eine weitere Methode outputChar(…​), die ein Zeichen c eine gewünschte Anzahl count ausgibt. Wir können auf diese Methode genau dann zurückgreifen, wenn wir Weißraum und auch die Minuszeichen ausgeben müssen.

Die underline(…​)-Methode gibt zunächst den Text gefolgt vom Zeilenumbruch aus. Anschließend muss der Weißraum und die Minuszeichen geschrieben werden. Eine Schleife findet alle Positionen mit dem zu suchenden und zu unterstreichen in 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 nextIndex mit der Fundstelle, und wenn dieser größer gleich 0 ist, haben wir eine Fundstelle, und wir führen den Rumpf der for-Schleife aus. In dem Moment wo indexOf(…​) keinen Fund mehr meldet, brechen wir die Schleife ab. Es gibt also so viel Durchläufe, wie es Fundstellen für unseren String search gibt.

Im Rumpf der Schleife müssen wir Leerzeichen produzieren, und zwar genauso viele, dass wir von der letzten Stelle bis zur aktuellen Fundstelle kommen, und danach müssen wir so viel Minuszeichen produzieren, wie der Suchstring lang ist. Als letztes aktualisieren wir die Variable lastIndex, die auf das Ende des zuletzt gefunden Suchstrings zeigt. indexOf(…​) startet bei der nächsten Suche am Ende des letzten Strings, ab lastIndex.

1.1.3. Vokale entfernen ☆

Sprachwissenschaftler sind der Ansicht, dass auch nach dem Entfernen von Vokalen der Text noch lesbar bleibt. Stimmt es, was die Wissenschaftler sagen?

  1. Lege eine neue Klasse `RemoveVowel`an.

  2. Schreibe eine Klassenmethode String removeVowels(String s), die aus einem übergebenen java.lang.String die Vokale entfernt.

  3. Löse die Aufgabe mit mindestens zwei verschiedenen Varianten.

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

import java.util.Arrays;

public class RemoveVowel {

public static void main( String[] args ) {
System.out.println( removeVowels1( "Hallo Javanesen" ) );
System.out.println( removeVowels2( "Hallo Javanesen" ) );
System.out.println( removeVowels3( "Hallo Javanesen" ) );
System.out.println( removeVowels4( "Hallo Javanesen" ) );
System.out.println( removeVowels5( "Hallo Javanesen" ) );
System.out.println( removeVowels6( "Hallo Javanesen" ) );
}

/////////////////////////////////////////////////////////////////////////////////

public static String removeVowels1( String s ) {
s = s.replace( "a", "" );
s = s.replace( "e", "" );
s = s.replace( "o", "" );
s = s.replace( "u", "" );
s = s.replace( "i", "" );
s = s.replace( "A", "" );
s = s.replace( "E", "" );
s = s.replace( "O", "" );
s = s.replace( "U", "" );
s = s.replace( "I", "" );
return s;
}

/////////////////////////////////////////////////////////////////////////////////

public static String removeVowels2( String s ) {
char[] chars = new char[s.length()];
int len = 0;

for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );

if ( "aeiouöäüyAEIOUÄÖÜY".indexOf( c ) < 0 )
chars[ len++ ] = c;
}

return new String( chars, 0, len );
}

/////////////////////////////////////////////////////////////////////////////////

public static String removeVowels3( String s ) {
final char[] VOWELS = { 'a', 'e', 'i', 'o', 'u', 'ä', 'ö', 'ü' };
String result = "";
for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );
int pos = Arrays.binarySearch( VOWELS, Character.toLowerCase( c ) );
if ( pos < 0 )
result = result + c;
}
return result;
}

/////////////////////////////////////////////////////////////////////////////////

private static boolean isVowel( char c ) {
return "aeiouäöüAEIOUÄÖÜ".indexOf( c ) >= 0;
}

public static String removeVowels4( String s ) {
StringBuilder result = new StringBuilder( s.length() );
for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );
if ( !isVowel( c ) )
result.append( c );
}
return result.toString();
}

/////////////////////////////////////////////////////////////////////////////////

public static String removeVowels6( String s ) {
return s.replaceAll( "[aeiouäöüAEIOUÄÖÜ]", "" );
}

/////////////////////////////////////////////////////////////////////////////////

public static String removeVowels5( String s ) {
String result = "";
String[] arr = s.split( "a|e|o|u|i|A|E|O|U|I" );
for ( int x = 0; x < arr.length; x++ ) {
result += arr[ x ];
}
return result;
}
}

Die erste Lösung ist recht simpel, aber auch mit einem hohen Anteil Codeduplikation. Die replace(…​)-Methode ist überladen: mit einen 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 lange ist, durch einen Leerstring ersetzen können, und somit entfernen können.

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 ist 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, und 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, was wir betrachten, in diesem Teilstring 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 Eingangsstring gelaufen, und haben ausgewählte Zeichen in das Array gesetzt. Nun müssen wir dass er Array wieder in einem String konvertieren. Dazu bietet die String-Klasse einen passenden konstruktor.

Dritte Variante unterscheidet sich in zwei Details von der vorherigen Variante. Das erste ist, dass kein Array als Zwischenspeicher benutzt wird, sondern ein String, an dem mit dem Plus-Operator das Zeichen gehängt wird, was kein Vokal ist. Die zweite Änderung ist die Frage, ob das Zeichen ein Vokal ist. Wir greifen hier auf die Methode Arrays.binarySearch(…​) zurück, die in einem Array nach einem Zeichen sucht, und ähnlich wie indexOf(…​) nur dann etwas Positives liefert, wenn es auch einen Fund gab. Ist das Ergebnis der Methode negativ, haben wir kein Vokal, und wir hängen das Zeichen an den Ergebnisstring an.

Die vierte Lösung unterscheidet sich von den vorherigen Lösungen dadurch, dass ein StringBuilder verwendet wird. StringBuilder erlauben den dynamischen Aufbau von Zeichenfolgen. Der zweite Unterschied besteht darin, dass wir auf eine eigene neue Methode isVowel(char) zurückgreifen, die testet, ob ein Zeichen ein Vokal ist. 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 zuviel 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.

Beiden Lösungen greifen thematisch schon etwas vor, und nutzen ganz geschickt reguläre Ausdrücke. 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. Matched der reguläre Ausdruck auf ein Zeichen, wird das Zeichen durch einen Leerstring ersetzt, also entfernt.

Die letzte Lösung geht einen anderen, recht kreativen Weg. 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 Ergebnissrting wieder zusammensetzen.

1.1.4. Gutes Passwort? ☆

Es ist wichtig gute und sichere Passwörter zu verwenden. Ein gutes Passwort hat eine gewisse Länge, enthält Sonderzeichen, usw.

  1. Lege eine neue Klasse PasswordTester an.

  2. Schreibe eine Methode isGoodPassword(String), die übliche Kriterien testet. Die Methode soll false zurückgeben, wenn das Passwort nicht gut ist, und true, wenn das Passwort einen guten Aufbau hat. Schlägt ein Test fehl, so soll über über System.err eine Meldung erscheinen. Ein Test fiel, werden die anderen Tests nicht mehr durchgeführt.

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

public class PasswordTester {

public static boolean isGoodPassword( String s ) {

if ( s.length() < 8 ) {
System.err.println( "Passwort zu kurz" );
return false;
}

if ( !containsUppercaseLetter( s ) ) {
System.err.println( "Muss Großbuchstaben enthalten" );
return false;
}

if ( !containsLowercaseLetter( s ) ) {
System.err.println( "Muss Kleinbuchstaben enthalten" );
return false;
}

if ( !containsDigit( s ) ) {
System.err.println( "Muss Ziffer enthalten" );
return false;
}

if ( !containsSpecialCharacter( s ) ) {
System.err.println( "Muss Sonderzeichen wie ., enthalten" );
return false;
}

return true;
}

private static boolean containsUppercaseLetter( String s ) {
for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );
if ( Character.isUpperCase( c ) )
return true;
}
return false;
}

private static boolean containsLowercaseLetter( String s ) {
for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );
if ( Character.isLowerCase( c ) )
return true;
}
return false;
}

private static boolean containsDigit( String s ) {
for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );
if ( Character.isDigit( c ) )
return true;
}
return false;
}

private static boolean containsSpecialCharacter( String s ) {
for ( int i = 0; i < s.length(); i++ ) {
char c = s.charAt( i );
if ( ".,".indexOf( c ) >= 0 )
return true;
//      switch (c) {
//        case '.' :
//        case ',' :return true;
//      }
}
return false;
}

public static void main( String[] args ) {
System.out.println( isGoodPassword( "hallo" ) );
System.out.println( isGoodPassword( "1234" ) );
System.out.println( isGoodPassword( "1h" ) );
System.out.println( isGoodPassword( "11234H.allo" ) );
}
}

Unsere Methode führt hintereinander diverse Tests durch. Schlägt ein Test fehl, beendet return false die Methode. Sind alle Test 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 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, braucht man keine weiteren Zeichen zu testen, sondern kann die Methode sofort beenden.

1.1.5. 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.

  1. Lege eine neue Kasse SumOfTheDigits an.

  2. Schreibe eine Klassenmethode int sumOfTheDigits(long value), die die Quersumme einer Zahl berechnet.

  3. Schreibe eine überladene Klassenmethode int sumOfTheDigits(String value) dazu, die die Ziffern in einem String annimmt.

Welche Methode ist leichter zu implementieren? Welche Methode sollte die andere als Unterprogramm aufrufen?

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

public class SumOfTheDigits {

static int sumOfTheDigits( long value ) {
return sumOfTheDigits( String.valueOf( value ) );
}

static int sumOfTheDigits( String value ) {
int sum = 0;

for ( int i = 0; i < value.length(); i++ )
sum += Character.getNumericValue( value.charAt( i ) ); // sum += value.charAt( i ) - '0';

return sum;
}

public static void main( String[] args ) {
System.out.println( sumOfTheDigits( "12345" ) ); // 15
System.out.println( sumOfTheDigits( 12345 ) ); // 15
}
}

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 sumOfTheDigits(long) auf, kann man aus der ganzen Zahl einen String machen, und dann sumOfTheDigits(String) aufrufen. Umgekehrt: wenn wir sumOfTheDigits(String) aufrufen, können wir den String mit Long.parseLong(String) in eine Ganzzahl konvertieren, und ´sumOfTheDigits(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 auch noch einen zweiten Nachteil, dass wir das Ergebnis von rechts nach links bekommen. Das ist unpraktisch. Praktisch ist folglich sumOfTheDigits(String) zu implementieren.

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 wir ein Unicode-Zeichen mit einer Ziffer in einen numerischen Wert übertragen, greifen wir auf Character.getNumericValue(char) zurück. 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 char '0' abziehen, doch getNumericValue(…​) arbeitet allgemein auf allen Unicodezeichen, so liefert getNumericValue('٢') das Ergebnis 2.

1.1.6. Ausgabe im Morse-Code ☆

Ein Morse-Funkspruch besteht aus kurzen und langen Symbolen, die mit . und - angedeutet sind.

Kopiere die folgende Definition in eine neue Klasse MorseCodeEncoder:

// A .-      N -.      0 -----
// B -...    O ---     1 .----
// C -.-.    P .--.    2 ..---
// D -..     Q --.-    3 ...--
// E .       R .-.     4 ....-
// F ..-.    S ...     5 .....
// G --.     T -       6 -....
// H ....    U ..-     7 --...
// I ..      V ...-    8 ---..
// J .---    W .--     9 ----.
// K -.-     X -..-
// L .-..    Y -.--
// M --      Z --..

Schreibe eine statische Methode morseCode(String), die einen String annimmt und dann in Morse-Code konvertiert. Jedes Zeichen der Zeichenfolge soll im entsprechende Morse-Code ausgeben werden. Jeder Block soll dabei in der Ausgabe durch ein Leerzeichen getrennt sein. Nicht bekannte Zeichen werden übersprungen. Kleinbuchstaben sollen wie Großbuchstaben gewertet werden.

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

public class MorseCodeEncoder {

public static void morseCode( String text ) {
for ( int i = 0; i < text.length(); i++ ) {
String morse = "";
switch ( Character.toUpperCase( text.charAt( i ) ) ) {
case 'A': morse = ".- "; break;
case 'B': morse = "-... "; break;
case 'C': morse = "-.-. "; break;
case 'D': morse = "-.. "; break;
case 'E': morse = ". "; break;
case 'F': morse = "..-. "; break;
case 'G': morse = "--. "; break;
case 'H': morse = ".... "; break;
case 'I': morse = ".. "; break;
case 'J': morse = ".--- "; break;
case 'K': morse = "-.- "; break;
case 'L': morse = ".-.. "; break;
case 'M': morse = "-- "; break;
case 'N': morse = "-. "; break;
case 'O': morse = "--- "; break;
case 'P': morse = ".--. "; break;
case 'Q': morse = "--.- "; break;
case 'R': morse = ".-. "; break;
case 'S': morse = "... "; break;
case 'T': morse = "- "; break;
case 'U': morse = "..- "; break;
case 'V': morse = "...- "; break;
case 'W': morse = ".-- "; break;
case 'X': morse = "-..- "; break;
case 'Y': morse = "-.-- "; break;
case 'Z': morse = "--.. "; break;
case '0': morse = "----- "; break;
case '1': morse = ".---- "; break;
case '2': morse = "..--- "; break;
case '3': morse = "...-- "; break;
case '4': morse = "....- "; break;
case '5': morse = "..... "; break;
case '6': morse = "-.... "; break;
case '7': morse = "--... "; break;
case '8': morse = "---.. "; break;
case '9': morse = "----. "; break;
}
System.out.print( morse );
}
}

public static void main( String[] args ) {
morseCode( "Morse code is a method used in telecommunication to encode text characters as standardized sequences of two different signal durations, called dots and dashes or dits and dahs." );
}
}

Das Codevolumen lässt erahnen, dass die Abbildung in erster Linie Fleißarbeit ist. Wir werden später spezielle Datenstrukturen kennenlernen, mit denen sich Abbildungen dieser Art viel besser realisieren lassen, doch grundsätzlich ist switch-case ein guter Helfer, wenn es darum geht ein Zeichen auf eine Zeichenfolge zu übertragen.

Die for-Schleife läuft über die Zeichenfolge, das Programm extrahiert jedes Zeichen, konvertiert es in einen Großbuchstaben für den switch-case-Vergleich. Im Rumpf der Schleife initialisieren wir eine Variable ´morse´ anfangs mit einem Leerstring, den wir genau dann überschreiben, wenn wir ein gültiges Zeichen in der Eingabe erkennen und auf den Mose-Code übertragen können.Haben wir im String Zeichen, die wir nicht berücksichtigen können, bleibt ´morse´ bei dem Leerstring, der bei der Ausgabe folglich auch nicht ausgegeben wird.

1.1.7. Die Caesar-Verschlüsselung ☆☆

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.

  1. Lege eine neue Klasse Caesar an.

  2. 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.

  3. Schreibe eine Methode String decaesar(String s, int rotation), die die Verschlüsselung wieder zurücknimmt.

Lösungsvorschlag
package com.tutego.insel.solutions.lang;

public class CaesarCipher {

public static String caesar( String s, int distance ) {
StringBuilder result = new StringBuilder( s.length() );

for ( int i = 0; i < s.length(); i++ )
result.append( (char) rotate( s.charAt( i ), distance ) );

return result.toString();

// Freaky solution
// IntUnaryOperator rotation = c -> rotate( c, distance );
// return s.chars().map( rotation ).mapToObj( Character::toString ).collect( Collectors.joining() );
}

private static int rotate( int c, int distance ) {
if ( c >= 'A' && c <= 'Z' )   // Character.isUpperCase( c ) is too broad
return 'A' + (c - 'A' + distance) % 26;
else if ( c >= 'a' && c <= 'z' )
return 'a' + (c - 'a' + distance) % 26;
else
return c;
}

public static String decaesar( String s, int rotation ) {
return caesar( s, 26 - rotation );
}

public static void main( String[] args ) {
String s = "abxyz. ABXYZ!";

char distance = 4;
System.out.println( s );
System.out.println( caesar( s, distance ) );
System.out.println( decaesar( caesar( s, distance ), distance ) );
}
}

Der Aufgabe besteht aus drei Methoden. Neben der geforderten Methode zum Verschlüsseln und Entschlüsseln gibt es eine private Methode int rotate(int c, int distance) die ein Zeichen um eine gewisse Anzahl Positionen, wir nennen das Distanz, verschiebt. Da wir Groß- sowie Kleinbuchstaben betrachten wollen, gibt es zwei Fallunterscheidungen. Außerdem kann es sein, dass das Zeichen weder einen Groß-, noch ein Kleinbuchstabe ist und dann wird das Originalzeichen unverändert zurückgegeben.

Die Programmlogik ist für Groß- und Kleinbuchstaben prinzipiell gleich, daher wollen wir uns den Ausdruck stellvertretend für die Großbuchstaben anschauen. Auf dem ersten Blick ist die Lösung einfach: wir addieren die Distanz auf die Unicode-Position des Zeichens c. Haben wir ein Zeichen wie 'W', und addieren drei, landen wir bei 'Z'. Probleme bereiten uns der Umbruch, das ist 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 auf das Zeichen c. Die Überlegung ist vielmehr, was wir auf den 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 wird vom Startbuchstaben einen Abstand von distance haben möchten, addieren wird die Distanz auf, also 'A' + (c - 'A' + distance). Das sieht gekürzt wie c + distance aus, Doch es gibt einen feinen Unterschied, dass wir jetzt den geklammerten Ausdruck % 26 nehmen können, sodass uns 'Z' + 1 wieder zum 'A' führt.

Die Methode String caesar( String s, int distance ) ist dann selbst ohne Überraschung. Wir bauen einen internen StringBuilder auf, indem wir das Ergebnis sammeln, laufen dann einmal von vorne nach hinten über den eingang 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. Dazu muss man wissen, dass man nach einer gewissen Anzahl Verschiebungen wieder beim Ursprung String landet. Und diese Anzahl Verschiebungen ist gerade die Größe des Alphabets, also 26. was passiert aber, wenn wir den Stream nicht um 26 Positionen verschieben, sondern nur um 25? Dann hätten wir dadurch den spring nicht nach rechts geschoben sondern nach links, aus einem B würde dann kein C mehr werden, sondern aus dem B dann ein A. Wir können folglich Verschlüsseln mit der Position 26 - rotation, was den String wieder nach link zurück in die Ursprungsposition schiebt.

1.1.8. Komprimieren ☆☆☆

Um die Länge von Zeichenketten zu verkürzen kann eine einfache Kompression eingesetzt werden, die sogenannte Lauflängenkodierung. Das Prinzip dabei ist, die Anzahl der Zeichen zusammen mit dem Zeichen zu schreiben. Nehmen wir an, ein String besteht aus einer Folge von . (Punkt) und - (Minuszeichen), etwa "…​----…​.--.-.-.". Die Zeichenfolge könnte verkürzt werden zu ".3-4.4-2.1-1.1-1.1". Man merkt aber, dass sich die Kompression nur dann lohnt, wenn viele gleiche Zeichen vorkommen.

Schreibe ein Programm, was Folgen von . und - nach folgendem einfachen Algorithmus kodiert: erst kommt das Zeichen, dann die Anzahl.

Erweiterungen:

  • Das Programm soll alle nicht-Ziffern verarbeiten können.

  • Verfeinere das Programm, dass die Zahl ausbleibt, wenn das Zeichen nur genau einmal vorkommt.

  • Schreibe einen Dekodierer.

Lösungsvorschlag

1.1.9. Frequenzkonvertierung ☆

  1. Schreibe eine Methode String onlyNumbers(String), die aus einem beliebigen String nur Zahlen, + , - und Dezimaltrenner (also . und ,) zurückgibt. Beispiel onlyNumbers("ABC 12,324 ZB") ergibt "12,324".

  2. Schreibe eine neue Methode double stringDoubleToDouble(String), die eine Zeichenkette in ein double konvertiert. Als Dezimaltrenner sind , und auch . erlaubt.

  3. Nutze die neue Methode um double toFrequence(String) so zu implementieren, dass toFrequence("12,3Mhz") das Ergebnis 12.3 vom Typ double liefert.

Lösungsvorschlag

1.1.10. Der Palindrom-Test ☆

Ein Palindrom ist ein Wort, das sich von vorne wie von hinten gleich liest, etwa Otto oder auch 121.

Schreibe ein Java-Programm, welches untersucht, ob die als Kommandozeilen-Argumente angegebenen Wörter Palindrome sind.

  • Implementiere dazu eine Methode boolean isPalindrom(String s).

  • Erweitere das Programm um eine Klassenmethode isPalindromCaseInsensitive(String s), sodass es unabhängig von der Groß-/Kleinschreibung wird.

  • Jetzt sollen auch noch Leerzeichen überlesen werden, sodass A man a plan a canal Panama erkannt wird. Nenne die Methode isPalindromSpaceIrrelevant(String s).

Lösungsvorschlag

1.1.11. Einfaches Pattern Matching ☆☆

Peter Pan sucht eine einfache Methode zum Pattern Matching. Sie soll, ähnlich wie strstr(…​) in C, in einem String nach einem Suchstring suchen. Der Suchstring kann

  • * (Ersatz für mehrere Zeichen) sowie

  • ? (Platzhalter für beliebiges Zeichen) enthalten.

Peter bekommt eine Antwort, doch allerdings in C:

int match ( char *pat, char *str ) {
switch ( *pat ) {
case '\0' : return !*str;
case '*'  : return match( pat+1, str ) || *str && match( pat, str+1 );
case '?'  : return *str && match( pat+1, str+1 );
default   : return *pat == *str && match( pat+1, str+1 );
}
}

Wie kann man das in Java übersetzen?

Lösungsvorschlag

1.2. Dynamische Strings mit StringBuilder

Während String-Objekte immutable sind, lassen sich Objekte vom Typ StringBuilder modifizieren. Gleiches gilt für StringBuffer, doch dieser Typ ist API-gleich und für die Übungen nicht relevant.

1.2.1. Leerzeichenkomprimierer ☆☆

Oftmals sind zu viele Leerzeichen im Text überflüssig und können auf nur ein Leerzeichen verkürzt werden. Aus einem String "Hallo Welt " kann dann "Hallo Welt " werden.

Schreibe eine statische Methode StringBuilder compressSpace(StringBuilder sb), die mehr als zwei Leerzeichen in dem übergebenen java.lang.StringBuilder zu einem Leerzeichen zusammenschmelzen lässt. Die Übergabe sb soll mit return sb; zurückgegeben werden, die Veränderung soll direkt am StringBuilder erfolgen.

Optionale Erweiterungen:

  • Leerzeichen ganz am Anfang und ganz am Ende sollen komplett weggeschnitten werden.

  • Wenn ein Tabulator vorkommt, soll dieses wie ein Leerzeichen gewerten werden, sodass ein String wie "hallo \t welt" zu "hallo welt" wird.

Lösungsvorschlag

1.2.2. Knack ☆

Meldungen über Funk haben oft ein Knacksen.

Simuliere dies, in dem in einem beliebigen gegebenen String in willkürlichen Abständen "♬KNACK♪" eingebaut wird.

public static String knack( String text ) { ... }
public static String deKnack( String text ) { ... }

Die Methode knack fügt das Knacken ein, und deKnack entfernt das Knacken wieder.

Lösungsvorschlag

1.3. Einfache Zerlegung von Zeichenketten, Tokenizer

1.3.1. Die längste Zeile einer Datei ☆

Laufe mit einem java.util.Scanner durch eine Datei und gib die Länge der längsten Zeile aus.

Lösungsvorschlag

1.3.2. StringTokenizer ☆☆

Schreibe eine Klasse WordWrapper mit einer statischen Methode String wrapWord(String s, int len), die eine Zeichenkette s in kleine Teilzeichenketten der Länge len zerlegt und mit Return getrennt wieder zurückgibt. Mit einem String

String s = "Der Sehnsucht entgegen, sie erleben. Wer kann das schon. Den Träumen entgegen, " +
"sie erleben. Wer macht das schon. Den Gefühlen entgegen, sie erleben. Wer wagt das schon. " +
"Der Liebe entgegen, sie erleben. Wer darf das schon.";

wird zum Beispiel bei einer Zeilenlänge von 20 folgender String zurückgegeben:

Der Sehnsucht
entgegen, sie
erleben. Wer kann
das schon. Den
Träumen entgegen,
sie erleben. Wer
macht das schon.
Den Gefühlen entgegen,
sie erleben. Wer
wagt das schon. Der
Liebe entgegen, sie
erleben. Wer darf
das schon.
Lösungsvorschlag