Part 10

Weitere nützliche Techniken

Nun werden wir uns einige nützliche Programmiertechniken und Klassen ansehen.

StringBuilder

Betrachten wir folgendes Programm.

String numbers = "";
for (int i = 1; i < 5; i++) {
    numbers = numbers + i;
}
System.out.println(numbers);
Beispielausgabe

1234

Die Struktur des Programms ist einfach. Ein String, der die Zahl 1234 enthält, wird erstellt und anschließend ausgegeben.

Das Programm funktioniert, aber es gibt ein kleines Problem, das für die Benutzer unsichtbar ist. Der Aufruf von numbers + i erzeugt einen neuen String. Sehen wir uns das Programm Zeile für Zeile an, indem wir den Wiederholungsblock aufschlüsseln.

String numbers = ""; // Erstellen eines neuen Strings: ""
int i = 1;
numbers = numbers + i; // Erstellen eines neuen Strings: "1"
i++;
numbers = numbers + i; // Erstellen eines neuen Strings: "12"
i++;
numbers = numbers + i; // Erstellen eines neuen Strings: "123"
i++;
numbers = numbers + i; // Erstellen eines neuen Strings: "1234"
i++;

System.out.println(numbers); // Ausgabe des Strings

Im obigen Beispiel werden insgesamt fünf Strings erstellt.

Betrachten wir das gleiche Programm, bei dem nach jeder Zahl ein Zeilenumbruch hinzugefügt wird.

String numbers = "";
for (int i = 1; i < 5; i++) {
    numbers = numbers + i + "\n";
}
System.out.println(numbers);
Beispielausgabe

1 2 3 4

Jede +-Operation bildet einen neuen String. In der Zeile numbers + i + "\n"; wird zunächst ein String erstellt, danach ein weiterer, der einen neuen Zeilenumbruch an den vorherigen String anhängt. Lassen Sie uns dies ebenfalls aufschlüsseln.

String numbers = ""; // Erstellen eines neuen Strings: ""
int i = 1;
// Zuerst wird der String "1" erstellt und dann der String "1\n"
numbers = numbers + i + "\n";
i++;
// Zuerst wird der String "1\n2" erstellt und dann der String "1\n2\n"
numbers = numbers + i + "\n"
i++;
// Zuerst wird der String "1\n2\n3" erstellt und dann der String "1\n2\n3\n"
numbers = numbers + i + "\n"
i++;
// und so weiter
numbers = numbers + i + "\n"
i++;

System.out.println(numbers); // Ausgabe des Strings

Im obigen Beispiel werden insgesamt neun Strings erstellt.

Die Erstellung von Strings ist – auch wenn sie in kleinem Maßstab unmerklich ist – keine schnelle Operation. Für jeden String wird im Speicher Platz reserviert, in den der String dann eingefügt wird. Wenn der String nur als Teil eines größeren Strings benötigt wird, sollte die Leistung optimiert werden.

Die in Java integrierte StringBuilder-Klasse bietet eine Möglichkeit, Strings zu verketten, ohne neue Strings zu erstellen. Ein neues StringBuilder-Objekt wird mit einem Aufruf von new StringBuilder() erstellt, und Inhalte werden dem Objekt mit der überladenen append-Methode hinzugefügt, d. h., es gibt Varianten davon für verschiedene Variablentypen. Schließlich liefert das StringBuilder-Objekt einen String mit der toString-Methode.

Im folgenden Beispiel wird nur ein String erstellt.

StringBuilder numbers = new StringBuilder();
for (int i = 1; i < 5; i++) {
    numbers.append(i);
}
System.out.println(numbers.toString());

Die Verwendung von StringBuilder ist effizienter als das Erstellen von Strings mit dem +-Operator.

Quiz

Loading...

Quiz

Loading...

Reguläre Ausdrücke

Ein regulärer Ausdruck definiert eine Menge von Strings in kompakter Form. Reguläre Ausdrücke werden unter anderem verwendet, um die Korrektheit von Strings zu überprüfen. Wir können mit einem regulären Ausdruck, der die als korrekt betrachteten Strings definiert, bewerten, ob ein String in der gewünschten Form vorliegt.

Betrachten wir ein Problem, bei dem überprüft werden muss, ob eine vom Benutzer eingegebene Matrikelnummer im richtigen Format vorliegt. Eine Matrikelnummer sollte mit "01" beginnen, gefolgt von 7 Ziffern zwischen 0 und 9.

Sie könnten das Format der Matrikelnummer beispielsweise überprüfen, indem Sie den String, der die Matrikelnummer darstellt, mit der Methode charAt Zeichen für Zeichen durchgehen. Eine andere Möglichkeit wäre, zu überprüfen, ob das erste Zeichen "0" ist, und die Methode Integer.valueOf aufzurufen, um den String in eine Zahl zu konvertieren. Dann könnten Sie überprüfen, ob die von der Methode Integer.valueOf zurückgegebene Zahl kleiner als 20000000 ist.

Die Überprüfung der Korrektheit mit Hilfe regulärer Ausdrücke erfolgt, indem zuerst ein geeigneter regulärer Ausdruck definiert wird. Dann können wir die Methode matches der Klasse String verwenden, die überprüft, ob der String mit dem als Parameter angegebenen regulären Ausdruck übereinstimmt. Für die Matrikelnummer ist der passende reguläre Ausdruck "01[0-9]{7}", und die Überprüfung der vom Benutzer eingegebenen Matrikelnummer erfolgt wie folgt:

System.out.print("Geben Sie eine Matrikelnummer ein: ");
String nummer = scanner.nextLine();

if (nummer.matches("01[0-9]{7}")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}

Lassen Sie uns die gebräuchlichsten Zeichen in regulären Ausdrücken durchgehen.

Alternation ("oder", Vertikale Linie)

Eine vertikale Linie zeigt an, dass Teile eines regulären Ausdrucks optional sind. Zum Beispiel definiert 00|111|0000 die Strings 00, 111 und 0000. Die Methode matches gibt true zurück, wenn der String mit einer der angegebenen Alternativen übereinstimmt.

String string = "00";

if (string.matches("00|111|0000")) {
    System.out.println("Der String enthielt eine der drei Alternativen");
} else {
    System.out.println("Der String enthielt keine der Alternativen");
}
Beispielausgabe

Der String enthielt eine der drei Alternativen

Der reguläre Ausdruck 00|111|0000 verlangt, dass der String genau so ist, wie er ihn spezifiziert – es gibt keine "enthält"-Funktionalität.

String string = "1111";

if (string.matches("00|111|0000")) {
    System.out.println("Der String enthielt eine der drei Alternativen");
} else {
    System.out.println("Der String enthielt keine der drei Alternativen");
}
Beispielausgabe

Der String enthielt keine der drei Alternativen

Eingrenzung auf einen Teil des Strings (Klammern)

Klammern können verwendet werden, um zu bestimmen, welcher Teil eines regulären Ausdrucks von den Regeln innerhalb der Klammern betroffen ist. Angenommen, wir wollen die Strings 00000 und 00001 zulassen. Wir können dies tun, indem wir eine vertikale Linie zwischen sie setzen, wie folgt 00000|00001. Klammern ermöglichen es uns, die Option auf einen bestimmten Teil des Strings zu beschränken. Der Ausdruck 0000(0|1) spezifiziert die Strings 00000 und 00001.

Ähnlich definiert der reguläre Ausdruck car(|s|), oder auch der reguläre Ausdruck car(s|), die Singular- (car) und Pluralformen (cars) des Wortes „car“.

Quantifizierer

Es wird oft gewünscht, dass ein bestimmter Substring in einem String wiederholt wird. Die folgenden Ausdrücke stehen in regulären Ausdrücken zur Verfügung:

  • Der Quantifizierer * wiederholt 0 ... mal, zum Beispiel:
String string = "trolololololo";

if (string.matches("trolo(lo)*")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}
Beispielausgabe

Richtiges Format.

  • Der Quantifizierer + wiederholt 1 ... mal, zum Beispiel:
String string = "trolololololo";

if (string.matches("tro(lo)+")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("F

alsches Format.");
}
Beispielausgabe

Richtiges Format.

String string = "nananananananana Batmaan!";

if (string.matches("(na)+ Batmaan!")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}
Beispielausgabe

Richtiges Format.

  • Der Quantifizierer ? wiederholt 0 oder 1 Mal, zum Beispiel:
String string = "You have to accidentally the whole meme";

if (string.matches("You have to accidentally (delete )?the whole meme")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}
Beispielausgabe

Richtiges Format.

  • Der Quantifizierer {a} wiederholt a-mal, zum Beispiel:
String string = "1010";

if (string.matches("(10){2}")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}
Beispielausgabe

Richtiges Format.

  • Der Quantifizierer {a,b} wiederholt a ... b-mal, zum Beispiel:
String string = "1";

if (string.matches("1{2,4}")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}
Beispielausgabe

Falsches Format.

  • Der Quantifizierer {a,} wiederholt a ...-mal, zum Beispiel:
String string = "11111";

if (string.matches("1{2,}")) {
    System.out.println("Richtiges Format.");
} else {
    System.out.println("Falsches Format.");
}
Beispielausgabe

Richtiges Format.

Sie können mehr als einen Quantifizierer in einem einzigen regulären Ausdruck verwenden. Zum Beispiel definiert der reguläre Ausdruck 5{3}(1|0)*5{3} Strings, die mit drei Fünfen beginnen und enden. Eine unbegrenzte Anzahl von Einsen und Nullen ist dazwischen erlaubt.

Zeichenklassen (Eckige Klammern)

Eine Zeichenklasse kann verwendet werden, um eine Menge von Zeichen auf kompakte Weise zu spezifizieren. Zeichen werden in eckige Klammern gesetzt, und ein Bereich wird durch einen Bindestrich angezeigt. Zum Beispiel bedeutet [145] dasselbe wie (1|4|5) und [2-36-9] bedeutet (2|3|6|7|8|9). Ebenso definiert die Angabe [a-c]* einen regulären Ausdruck, der verlangt, dass der String nur a, b und c enthält.

Quiz

Loading...

Loading

Heutzutage unterstützen fast alle Programmiersprachen reguläre Ausdrücke. Die Theorie der regulären Ausdrücke ist eines der Themen, die in späteren Kursen auch behandelt werden, weil sie z.B. sehr wichtig für die Theoretsiche Informatik oder für den Complilerbau sind. Weitere Informationen zu regulären Ausdrücken finden Sie zum Beispiel, indem Sie nach regular expressions java googeln.

Enumerierter Typ - Enum

Wenn die möglichen Werte einer Variablen im Voraus bekannt sind, kann eine Klasse vom Typ enum, d. h. ein Enumerated Type, verwendet werden, um die Werte darzustellen. Enumerierte Typen sind neben normalen Klassen und Schnittstellen eigene Typen. Ein Enumerated Type wird mit dem Schlüsselwort enum definiert. Zum Beispiel definiert die folgende Enum-Klasse Suit vier konstante Werte: DIAMOND, SPADE, CLUB und HEART.

public enum Suit {
    DIAMOND, SPADE, CLUB, HEART
}

In ihrer einfachsten Form listet enum die definierten konstanten Werte auf, getrennt durch Kommas. Enum-Typen, d. h. Konstanten, werden üblicherweise in Großbuchstaben geschrieben.

Ein Enum wird (meistens) in einer eigenen Datei geschrieben, ähnlich wie eine Klasse oder Schnittstelle. IDEs bietet üblicherweise die Möglichkeit ein Enum (semi)-automatisiert zu erstellen.

Folgendes ist eine Card-Klasse, bei der die Farbe (Suit) durch ein Enum dargestellt wird:

public class Card {

    private int value;
    private Suit suit;

    public Card(int value, Suit suit) {
        this.value = value;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return suit + " " + value;
    }

    public Suit getSuit() {
        return suit;
    }

    public int getValue() {
        return value;
    }
}

Die Karte wird folgendermaßen verwendet:

Card first = new Card(10, Suit.HEART);

System.out.println(first);

if (first.getSuit() == Suit.SPADE) {
    System.out.println("is a spade");
} else {
    System.out.println("is not a spade");
}

Die Ausgabe:

Beispielausgabe

HEARTS 10 is not a spade

Wir sehen, dass die Enum-Werte schön ausgegeben werden! Oracle hat eine Webseite zum enum-Datentyp unter http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html.

Objekt-Referenzen in Enums

Enumerierte Typen können Objekt-Referenzvariablen enthalten. Die Werte der Referenzvariablen sollten in einem internen Konstruktor der Klasse, die den Enumerated Type definiert, gesetzt werden, d. h. in einem Konstruktor mit einem private-Zugriffsmodifikator. Enum-Klassen können keinen public-Konstruktor haben.

Im folgenden Beispiel haben wir ein Enum Color, das die Konstanten RED, GREEN und BLUE enthält. Die Konstanten wurden mit Objekt-Referenzvariablen deklariert, die auf ihre Farbcodes verweisen:
public enum Color {
    // Die Konstruktorparameter werden beim Aufzählen der Konstanten definiert
    RED("#FF0000"),
    GREEN("#00FF00"),
    BLUE("#0000FF");

    private String code;        // Objekt-Referenzvariable

    private Color(String code) { // Konstruktor
        this.code = code;
    }

    public String getCode() {
        return this.code;
    }
}

Das Enum Color kann folgendermaßen verwendet werden:

System.out.println(Color.GREEN.getCode());
Beispielausgabe
#00FF00

Iterator

Betrachten wir die folgende Hand-Klasse, die die Karten darstellt, die eine Spielerin oder ein Spieler in der Hand hält:

public class Hand {
    private List<Card> cards;

    public Hand() {
        this.cards = new ArrayList<>();
    }

    public void add(Card card) {
        this.cards.add(card);
    }

    public void print() {
        this.cards.stream().forEach(card -> {
            System.out.println(card);
        });
    }
}

Die print-Methode der Klasse druckt jede Karte in der aktuellen Hand aus.

ArrayList und andere „Objektcontainer“, die das Collection Interface implementieren, implementieren auch das Iterable Interface und können auch mit einem Iterator durchlaufen werden – ein Objekt, das speziell dafür entwickelt wurde, eine bestimmte Art von Objekt-Sammlungen zu durchlaufen. Folgendes ist eine Version des Druckens der Karten, die einen Iterator verwendet:

public void print() {
    Iterator<Card> iterator = cards.iterator();

    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

Der Iterator wird von der cards-Liste, die Karten enthält, angefordert. Der Iterator kann als ein „Finger“ betrachtet werden, der immer auf ein bestimmtes Objekt in der Liste zeigt. Zunächst zeigt er auf das erste Element, dann auf das nächste und so weiter, bis alle Objekte mit Hilfe des „Fingers“ durchlaufen wurden.

Der Iterator bietet einige Methoden. Die Methode hasNext() wird verwendet, um zu fragen, ob noch Objekte zum Durchlaufen vorhanden sind. Wenn ja, kann das nächste Objekt in der Reihe mit der Methode next() vom Iterator angefordert werden. Diese Methode gibt das nächste Objekt in der Reihe zurück und bewegt den Iterator, oder „Finger“, um auf das folgende Objekt in der Sammlung zu zeigen.

Die von der next-Methode des Iterators zurückgegebene Objekt-Referenz kann natürlich auch in einer Variablen gespeichert werden. So könnte die print-Methode auch folgendermaßen geschrieben werden:

public void print(){
    Iterator<Card> iterator = cards.iterator();

    while (iterator.hasNext()) {
        Card nextInLine = iterator.next();
        System.out.println(nextInLine);
    }
}

Betrachten wir nun einen Anwendungsfall für einen Iterator. Zunächst werden wir das Problem auf problematische Weise angehen, um eine Motivation für die kommende Lösung zu schaffen. Wir versuchen, eine Methode zu erstellen, die Karten aus einem gegebenen Stream entfernt, deren Wert unter dem gegebenen Wert liegt.

public class Hand {
    // ...

    public void removeWorst(int value) {
        this.cards.stream().forEach(card -> {
            if (card.getValue() < value) {
                cards.remove(card);
            }
        });
    }
}

Das Ausführen der Methode führt zu einem Fehler.

Beispielausgabe

Exception in thread "main" java.util.ConcurrentModificationException at ... Java Result: 1

Der Grund für diesen Fehler liegt darin, dass beim Durchlaufen einer Liste mit der forEach-Methode davon ausgegangen wird, dass die Liste während des Traversierens nicht verändert wird. Das Ändern der Liste (in diesem Fall das Löschen von Elementen) führt zu einem Fehler – wir können uns vorstellen, dass die forEach-Methode hier „verwirrt“ ist.

Wenn Sie einige der Objekte während eines Traversierens aus der Liste entfernen möchten, können Sie dies mit einem Iterator tun. Der Aufruf der remove-Methode des Iterator-Objekts entfernt das Element aus der Liste, das zuvor vom Iterator mit dem vorherigen next-Aufruf zurückgegeben wurde. Hier ist eine funktionierende Version der Methode:

public class Hand {
    // ...

    public void removeWorst(int value) {
        Iterator<Card> iterator = cards.iterator();

        while (iterator.hasNext()) {
            if (iterator.next().getValue() < value) {
                // Entfernen des Elements aus der Liste, das vom vorherigen next-Aufruf zurückgegeben wurde
                iterator.remove();
            }
        }
    }
}
Loading
Loading
Sie haben das Ende dieses Abschnitts erreicht! Weiter zum nächsten Abschnitt: