Subsections of Fortgeschrittene Java-Themen und Umgang mit JVM

Serialisierung von Objekten und Zuständen

TL;DR

Objekte lassen sich mit der Methode void writeObject(Object) in ObjectOutputStream einfach in einen Datenstrom schreiben. Dies kann beispielsweise eine Datei o.ä. sein. Mit Hilfe von Object readObject() in ObjectInputStream lassen sich Objekte aus dem Datenstrom auch wieder herstellen. Dies nennt man Serialisierung und De-Serialisierung.

Um Objekte einer Klasse serialisieren zu können, muss diese das leere Interface Serializable implementieren ("Marker-Interface"). Damit wird quasi die Unterstützung in Object*Stream freigeschaltet.

Wenn ein Objekt serialisiert wird, werden alle Attribute in den Datenstrom geschrieben, d.h. die Typen der Attribute müssen ihrerseits serialisierbar sein. Dies gilt für alle primitiven Typen und die meisten eingebauten Typen. Die Serialisierung erfolgt ggf. rekursiv, Zirkelreferenzen werden erkannt und aufgebrochen.

static und transient Attribute werden nicht serialisiert.

Beim De-Serialisieren wird das neue Objekt von der Laufzeitumgebung aus dem Datenstrom rekonstruiert. Dies geschieht direkt, es wird kein Konstruktor involviert.

Beim Serialisieren wird für die Klasse des zu schreibenden Objekts eine serialVersionUID berechnet und mit gespeichert. Beim Einlesen wird dann geprüft, ob die serialisierten Daten zur aktuellen Version der Klasse passen. Da dies relativ empfindlich gegenüber Änderungen an einer Klasse ist, wird empfohlen, selbst eine serialVersionUID pro Klasse zu definieren.

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Was ist ein Marker-Interface und warum ist dies eine der großen Design-Sünden in Java?
  • (K2) Erklären Sie den Prozess der Serialisierung und De-Serialisierung. Worauf müssen Sie achten?
  • (K3) Serialisierung von Objekten und Programmzuständen
  • (K3) Serialisierung eigener Klassen und Typen

Motivation: Persistierung von Objekten und Spielzuständen

public class Studi {
    private final int credits = 42;
    private String name = "Hilde";

    ...
}

Wie kann ich Objekte speichern und wieder laden?

Ich möchte ein Spiel (einen Lauf) im Dungeon abspeichern, um es später fortsetzen zu können. Wie kann ich den aktuellen Zustand (also Level, Monster, Held, Inventar, XP/Health/...) so speichern, dass ich später das Spiel nach einem Neustart einfach fortsetzen kann?

Serialisierung von Objekten

  • Klassen müssen Marker-Interface Serializable implementieren

    "Marker-Interface": Interface ohne Methoden. Ändert das Verhalten des Compilers, wenn eine Klasse dieses Interface implementiert: Weitere Funktionen werden "freigeschaltet", beispielsweise die Fähigkeit, Klone zu erstellen (Cloneable) oder bei Serializable Objekte serialisierbar zu machen.

    Das ist in meinen Augen eine "Design-Sünde" in Java (neben der Einführung von null): Normalerweise definieren Interfaces eine Schnittstelle, die eine das Interface implementierende Klasse dann erfüllen muss. Damit agiert das Interface wie ein Typ. Hier ist das Interface aber leer, es wird also keine Schnittstelle definiert. Aber es werden damit stattdessen Tooling-Optionen aktiviert, was Interfaces vom Konzept her eigentlich nicht machen sollten/dürften - dazu gibt es Annotationen!

  • Schreiben von Objekten (samt Zustand) in Streams

    ObjectOutputStream: void writeObject(Object)

    Die Serialisierung erfolgt dabei für alle Attribute (außer static und transient, s.u.) rekursiv.

    Dabei werden auch Zirkelreferenzen automatisch aufgelöst/unterbrochen.

  • Lesen und "Wiedererwecken" der Objekte aus Streams

    ObjectInputStream: Object readObject()

    Dabei erfolgt KEIN Konstruktor-Aufruf!

Einfaches Beispiel

public class Studi implements Serializable {
    private final int credits = 42;
    private String name = "Hilde";

    public static void writeObject(Studi studi, String filename) {
        try (FileOutputStream fos = new FileOutputStream(filename);
            ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(studi);    oos.close();
        } catch (IOException ex) {}
    }

    public static Studi readObject(String filename) {
        Studi studi = null;
        try (FileInputStream fis = new FileInputStream(filename);
            ObjectInputStream ois = new ObjectInputStream(fis)) {
            studi = (Studi) ois.readObject();    ois.close();
        } catch (IOException | ClassNotFoundException ex) {}
        return studi;
    }
}

Bedingungen für Objekt-Serialisierung

  • Klassen implementieren Marker-Interface Serializable
  • Alle Attribute müssen ebenfalls serialisierbar sein (oder Deklaration "transient")
  • Alle primitiven Typen sind per Default serialisierbar
  • Es wird automatisch rekursiv serialisiert, aber jedes Objekt nur einmal (bei Mehrfachreferenzierung)
  • Serialisierbarkeit vererbt sich

Ausnahmen

  • Als static deklarierte Attribute werden nicht serialisiert
  • Als transient deklarierte Attribute werden nicht serialisiert
  • Nicht serialisierbare Attribut-Typen führen zu NotSerializableException

Version-UID

static final long serialVersionUID = 42L;
  • Dient zum Vergleich der serialisierten Version und der aktuellen Klasse
  • Über IDE generieren oder manuell vergeben
  • Wenn das Attribut fehlt, wird eine Art Checksumme von der Runtime-Umgebung berechnet (basierend auf diversen Eigenschaften der Klasse)

Dieser Wert wird beim Einlesen verglichen: Das Objekt wird nur dann wieder de-serialisiert, wenn die serialVersionUID mit der einzulesenden Klasse übereinstimmt!

Bei automatischer Berechnung der serialVersionUID durch die JVM kann jede kleine Änderung an der Klasse (beispielsweise Refactoring: Änderung der Methodennamen) eine neue serialVersionUID zur Folge haben. Das würde bedeuten, dass bereits serialisierte Objekte nicht mehr eingelesen werden können, auch wenn sich nur Methoden o.ä. verändert haben und die Attribute noch so vorhanden sind. Deshalb bietet es sich an, hier selbst eine serialVersionUID zu definieren - dann muss man aber auch selbst darauf achten, diese zu verändern, wenn sich wesentliche strukturelle Änderungen an der Klasse ergeben!

Bemerkungen

Es existieren diverse weitere Fallstricke und Probleme, siehe [Bloch2018] Kapitel 11 "Serialization".

Man kann in den ObjectOutputStream nicht nur ein Objekt schreiben, sondern mehrere Objekte und Variablen schreiben lassen. In dieser Reihenfolge muss man diese dann aber auch wieder aus dem Stream herauslesen (vgl. Object Streams).

Man kann die zu serialisierenden Attribute mit der Annotation @Serial markieren. Dies ist in der Wirkung ähnlich zu @Override: Der Compiler prüft dann, ob die markierten Attribute wirklich serialisierbar sind und würde sonst zur Compile-Zeit einen Fehler werfen.

Weitere Links:

Wrap-Up

  • Markerinterface Serializable schaltet Serialisierbarkeit frei

  • Objekte schreiben: ObjectOutputStream: void writeObject(Object)

  • Objekte lesen: ObjectInputStream: Object readObject()

  • Wichtigste Eigenschaften:

    • Attribute müssen serialisierbar sein
    • transient und static Attribute werden nicht serialisiert
    • De-Serialisierung: KEIN Konstruktor-Aufruf!
    • Serialisierbarkeit vererbt sich
    • Objekt-Referenz-Graph wird automatisch beachtet
Challenges

Implementieren Sie die beiden Klassen entsprechend dem UML-Diagram:

Objekte vom Typ Person sowie Address sollen serialisierbar sein (vgl. Vorlesung). Dabei soll das Passwort nicht serialisiert bzw. gespeichert werden, alle anderen Eigenschaften von Person sollen serialisierbar sein.

Hinweis: Verwenden Sie zur Umsetzung java.io.Serializable.

Erstellen Sie in Ihrem main() einige Instanzen von Person und speichern Sie diese in serialisierter Form und laden (deserialisieren) Sie diese anschließend in neue Variablen.

Betrachten Sie die ursprünglichen und die wieder deserialisierten Objekte mit Hilfe des Debuggers. Alternativ können Sie die Objekte auch in übersichtlicher Form über den Logger ausgeben.

Quellen
  • [Bloch2018] Effective Java
    Bloch, J., Addison-Wesley, 2018. ISBN 978-0-13-468599-1.
  • [Java-SE-Tutorial] The Java Tutorials
    Oracle Corporation, 2022.
    Essential Java Classes \> Basic I/O \> Object Streams

Reguläre Ausdrücke

TL;DR

Mit Hilfe von regulären Ausdrücken kann man den Aufbau von Zeichenketten formal beschreiben. Dabei lassen sich direkt die gewünschten Zeichen einsetzen, oder man nutzt Zeichenklassen oder vordefinierte Ausdrücke. Teilausdrücke lassen sich gruppieren und über Quantifier kann definiert werden, wie oft ein Teilausdruck vorkommen soll. Die Quantifier sind per Default greedy und versuchen so viel wie möglich zu matchen.

Auf der Java-Seite stellt man reguläre Ausdrücke zunächst als String dar. Dabei muss darauf geachtet werden, dass ein Backslash im regulären Ausdruck im Java-String geschützt (escaped) werden muss, indem jeweils ein weiterer Backslash voran gestellt wird. Mit Hilfe der Klasse java.util.regex.Pattern lässt sich daraus ein Objekt mit dem kompilierten regulären Ausdruck erzeugen, was insbesondere bei mehrfacher Verwendung günstiger in der Laufzeit ist. Dem Pattern-Objekt kann man dann den Suchstring übergeben und bekommt ein Objekt der Klasse java.util.regex.Matcher (dort sind regulärer Ausdruck/Pattern und der Suchstring kombiniert). Mit den Methoden Matcher#find und Matcher#matches kann dann geprüft werden, ob das Pattern auf den Suchstring passt: find sucht dabei nach dem ersten Vorkommen des Patterns im Suchstring, match prüft, ob der gesamte String zum Pattern passt.

Videos (HSBI-Medienportal)
Lernziele
  • (K1) Wichtigste Methoden von java.util.regex.Pattern und java.util.regex.Matcher
  • (K2) Unterschied zwischen Matcher#find und Matcher#matches
  • (K2) Unterscheidung zwischen greedy und non-greedy Verhalten
  • (K3) Bildung einfacher regulärer Ausdrücke
  • (K3) Nutzung von Zeichenklassen und deren Negation
  • (K3) Nutzung der vordefinierten regulären Ausdrücke
  • (K3) Nutzung von Quantifizierern
  • (K3) Zusammenbauen von komplexen Ausdrücken (u.a. mit Gruppen)

Suchen in Strings

Gesucht ist ein Programm zum Extrahieren von Telefonnummern aus E-Mails.

=> Wie geht das?

Leider gibt es unzählig viele Varianten, wie man eine Telefonnummer (samt Vorwahl und ggf. Ländervorwahl) aufschreiben kann:

030 - 123 456 789, 030-123456789, 030/123456789,
+49(30)123456-789, +49 (30) 123 456 - 789, ...

Definition Regulärer Ausdruck

Ein regulärer Ausdruck ist eine Zeichenkette, die zur Beschreibung von Zeichenketten dient.

Anwendungen

  • Finden von Bestandteilen in Zeichenketten
  • Aufteilen von Strings in Tokens
  • Validierung von textuellen Eingaben => "Eine Postleitzahl besteht aus 5 Ziffern"
  • Compilerbau: Erkennen von Schlüsselwörtern und Strukturen und Syntaxfehlern

Einfachste reguläre Ausdrücke

Zeichenkette Beschreibt
x "x"
. ein beliebiges Zeichen
\t Tabulator
\n Newline
\r Carriage-return
\\ Backslash

Beispiel

  • abc => "abc"
  • A.B => "AAB" oder "A2B" oder ...
  • a\\bc => "a\bc"

Anmerkung

In Java-Strings leitet der Backslash eine zu interpretierende Befehlssequenz ein. Deshalb muss der Backslash i.d.R. geschützt ("escaped") werden. => Statt "\n" müssen Sie im Java-Code "\\n" schreiben!

Zeichenklassen

Zeichenkette Beschreibt
[abc] "a" oder "b" oder "c"
[^abc] alles außer "a", "b" oder "c" (Negation)
[a-zA-Z] alle Zeichen von "a" bis "z" und "A" bis "Z" (Range)
[a-z&&[def]] "d","e" oder "f" (Schnitt)
[a-z&&[^bc]] "a" bis "z", außer "b" und "c": [ad-z] (Subtraktion)
[a-z&&[^m-p]] "a" bis "z", außer "m" bis "p": [a-lq-z] (Subtraktion)

Beispiel

  • [abc] => "a" oder "b" oder "c"
  • [a-c] => "a" oder "b" oder "c"
  • [a-c][a-c] => "aa", "ab", "ac", "ba", "bb", "bc", "ca", "cb" oder "cc"
  • A[a-c] => "Aa", "Ab" oder "Ac"

Vordefinierte Ausdrücke

Zeichenkette Beschreibt
^ Zeilenanfang
$ Zeilenende
\d eine Ziffer: [0-9]
\w beliebiges Wortzeichen: [a-zA-Z_0-9]
\s Whitespace (Leerzeichen, Tabulator, Newline)
\D jedes Zeichen außer Ziffern: [^0-9]
\W jedes Zeichen außer Wortzeichen: [^\w]
\S jedes Zeichen außer Whitespaces: [^\s]

Beispiel

  • \d\d\d\d\d => "12345"
  • \w\wA => "aaA", "a0A", "a_A", ...

Nutzung in Java

  • java.lang.String:

    public String[] split(String regex)
    public boolean matches(String regex)
  • java.util.regex.Pattern:

    public static Pattern compile(String regex)
    public Matcher matcher(CharSequence input)
    • Schritt 1: Ein Pattern compilieren (erzeugen) mit Pattern#compile => liefert ein Pattern-Objekt für den regulären Ausdruck zurück
    • Schritt 2: Dem Pattern-Objekt den zu untersuchenden Zeichenstrom übergeben mit Pattern#matcher => liefert ein Matcher-Objekt zurück, darin gebunden: Pattern (regulärer Ausdruck) und die zu untersuchende Zeichenkette
  • java.util.regex.Matcher:

    public boolean find()
    public boolean matches()
    public int groupCount()
    public String group(int group)
    • Schritt 3: Mit dem Matcher-Objekt kann man die Ergebnisse der Anwendung des regulären Ausdrucks auf eine Zeichenkette auswerten

      Bedeutung der unterschiedlichen Methoden siehe folgende Folien

      Matcher#group: Liefert die Sub-Sequenz des Suchstrings zurück, die erfolgreich gematcht wurde (siehe unten "Fangende Gruppierungen")

Hinweis:

In Java-Strings leitet der Backslash eine zu interpretierende Befehlssequenz ein. Deshalb muss der Backslash i.d.R. extra geschützt ("escaped") werden.

=> Statt "\n" (regulärer Ausdruck) müssen Sie im Java-String "\\n" schreiben!

=> Statt "a\\bc" (regulärer Ausdruck, passt auf die Zeichenkette "a\bc") müssen Sie im Java-String "a\\\\bc" schreiben!

Unterschied zw. Finden und Matchen

  • Matcher#find:

    Regulärer Ausdruck muss im Suchstring enthalten sein. => Suche nach erstem Vorkommen

  • Matcher#matches:

    Regulärer Ausdruck muss auf kompletten Suchstring passen.

Beispiel

  • Regulärer Ausdruck: abc, Suchstring: "blah blah abc blub"
    • Matcher#find: erfolgreich
    • Matcher#matches: kein Match - Suchstring entspricht nicht dem Muster

Quantifizierung

Zeichenkette Beschreibt
X? ein oder kein "X"
X* beliebig viele "X" (inkl. kein "X")
X+ mindestens ein "X", ansonsten beliebig viele "X"
X{n} exakt $n$ Vorkommen von "X"
X{n,} mindestens $n$ Vorkommen von "X"
X{n,m} zwischen $n$ und $m$ Vorkommen von "X"

Beispiel

  • \d{5} => "12345"
  • -?\d+\.\d* => ???

Interessante Effekte

Pattern p = Pattern.compile("A.*A");
Matcher m = p.matcher("A 12 A 45 A");

if (m.matches())
    String result = m.group(); // ???

Matcher#group liefert die Inputsequenz, auf die der Matcher angesprochen hat. Mit Matcher#start und Matcher#end kann man sich die Indizes des ersten und letzten Zeichens des Matches im Eingabezeichenstrom geben lassen. D.h. für einen Matcher m und eine Eingabezeichenkette s ist m.group() und s.substring(m.start(), m.end()) äquivalent.

Da bei Matcher#matches das Pattern immer auf den gesamten Suchstring passen muss, verwundert das Ergebnis für Matcher#group nicht. Bei Matcher#find wird im Beispiel allerdings ebenfalls der gesamte Suchstring "gefunden" ... Dies liegt am "greedy" Verhalten der Quantifizierer.

Nicht gierige Quantifizierung mit "?"

Zeichenkette Beschreibt
X*? non-greedy Variante von X*
X+? non-greedy Variante von X+

Beispiel

  • Suchstring "A 12 A 45 A":
    • A.*A findet/passt auf "A 12 A 45 A"

      normale greedy Variante

    • A.*?A

      • findet "A 12 A"
      • passt auf "A 12 A 45 A" (!)

      non-greedy Variante der Quantifizierung; Matcher#matches muss trotzdem auf den gesamten Suchstring passen!

(Fangende) Gruppierungen

Studi{2} passt nicht auf "StudiStudi" (!)

Quantifizierung bezieht sich auf das direkt davor stehende Zeichen. Ggf. Gruppierungen durch Klammern verwenden!

Zeichenkette Beschreibt
X|Y X oder Y
(C) Gruppierung

Beispiel

  • (A)(B(C))
    • Gruppe 0: ABC
    • Gruppe 1: A
    • Gruppe 2: BC
    • Gruppe 3: C

Die Gruppen heißen auch "fangende" Gruppen (engl.: "capturing groups").

Damit erreicht man eine Segmentierung des gesamten regulären Ausdrucks, der in seiner Wirkung aber nicht durch die Gruppierungen geändert wird. Durch die Gruppierungen von Teilen des regulären Ausdrucks erhält man die Möglichkeit, auf die entsprechenden Teil-Matches (der Unterausdrücke der einzelnen Gruppen) zuzugreifen:

  • Matcher#groupCount: Anzahl der "fangenden" Gruppen im regulären Ausdruck

  • Matcher#group(i): Liefert die Subsequenz der Eingabezeichenkette zurück, auf die die jeweilige Gruppe gepasst hat. Dabei wird von links nach rechts durchgezählt, beginnend bei 1(!).

    Konvention: Gruppe 0 ist das gesamte Pattern, d.h. m.group(0) == m.group(); ...

Hinweis: Damit der Zugriff auf die Gruppen klappt, muss auch erst ein Match gemacht werden, d.h. das Erzeugen des Matcher-Objekts reicht noch nicht, sondern es muss auch noch ein matcher.find() oder matcher.matches() ausgeführt werden. Danach kann man bei Vorliegen eines Matches auf die Gruppen zugreifen.

(Studi){2} => "StudiStudi"

Gruppen und Backreferences

Matche zwei Ziffern, gefolgt von den selben zwei Ziffern

(\d\d)\1

  • Verweis auf bereits gematchte Gruppen: \num

    num Nummer der Gruppe (1 ... 9)

    => Verweist nicht auf regulären Ausdruck, sondern auf jeweiligen Match!

    Anmerkung: Laut Literatur/Doku nur 1 ... 9, in Praxis geht auch mehr per Backreference ...

  • Benennung der Gruppe: (?<name>X)

    X ist regulärer Ausdruck für Gruppe, spitze Klammern wichtig

    => Backreference: \k<name>

Beispiel Gruppen und Backreferences

Regulärer Ausdruck: Namen einer Person matchen, wenn Vor- und Nachname identisch sind.

Lösung: ([A-Z][a-zA-Z]*)\s\1

Umlaute und reguläre Ausdrücke

  • Keine vordefinierte Abkürzung für Umlaute (wie etwa \d)

  • Umlaute nicht in [a-z] enthalten, aber in [a-ü]

    "helloüA".matches(".*?[ü]A");
    "azäöüß".matches("[a-ä]");
    "azäöüß".matches("[a-ö]");
    "azäöüß".matches("[a-ü]");
    "azäöüß".matches("[a-ß]");
  • Strings sind Unicode-Zeichenketten

    => Nutzung der passenden Unicode Escape Sequence \uFFFF

    System.out.println("\u0041 :: A");
    System.out.println("helloüA".matches(".*?A"));
    System.out.println("helloüA".matches(".*?\u0041"));
    System.out.println("helloü\u0041".matches(".*?A"));
  • RegExp vordefinieren und mit Variablen zusammenbauen ala Perl nicht möglich => Umweg String-Repräsentation

Wrap-Up

  • RegExp: Zeichenketten, die andere Zeichenketten beschreiben
  • java.util.regex.Pattern und java.util.regex.Matcher
  • Unterschied zwischen Matcher#find und Matcher#matches!
  • Quantifizierung ist möglich, aber greedy (Default)
Challenges

Schreiben Sie eine Methode, die mit Hilfe von regulären Ausdrücken überprüft, ob der eingegebene String eine nach dem folgenden Schema gebildete EMail-Adresse ist:

name@firma.domain

Dabei sollen folgende Regeln gelten:

  • Die Bestandteile name und firma können aus Buchstaben, Ziffern, Unter- und Bindestrichen bestehen.
  • Der Bestandteil name muss mindestens ein Zeichen lang sein.
  • Der Bestandteil firma kann entfallen, dann entfällt auch der nachfolgende Punkt (.) und der Teil domain folgt direkt auf das @-Zeichen.
  • Der Bestandteil domain besteht aus 2 oder 3 Kleinbuchstaben.

Hinweis: Sie dürfen keinen Oder-Operator verwenden.

Quellen
  • [Java-SE-Tutorial] The Java Tutorials
    Oracle Corporation, 2022.
    Essential Java Classes \> Regular Expressions

Aufzählungen (Enumerations)

TL;DR

Mit Hilfe von enum lassen sich Aufzählungstypen definieren (der Compiler erzeugt intern passende Klassen). Dabei wird den Konstanten eine fortlaufende Nummer zugeordnet, auf die mit ordinal() zugegriffen werden kann. Mit der Methode values() kann über die Konstanten iteriert werden, und mit name() kann eine Stringrepräsentation einer Konstanten erzeugt werden. Es sind keine Instanzen von Enum-Klassen erzeugbar, und die Enum-Konstanten sind implizit final und static.

Es lassen sich auch komplexe Enumerations analog zu Klassendefinition definieren, die eigene Konstruktoren, Felder und Methoden enthalten.

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Vorgänge beim Initialisieren von Enum-Klassen (Hinweis: static)
  • (K3) Erstellung komplexer Enumerationen mit Feldern und Konstruktoren
  • (K3) Nutzung von name(), ordinal() und values() in Enum-Klassen

Motivation

public class Studi {
    public static final int IFM = 0;
    public static final int ELM = 1;
    public static final int ARC = 2;

    public Studi(String name, int credits, int studiengang) {
        // Wert für studiengang muss zwischen 0 und 2 liegen
        // Erwünscht: Konstanten nutzen
    }

    public static void main(String[] args) {
        Studi rainer = new Studi("Rainer", 10, Studi.IFM);
        Studi holger = new Studi("Holger", 3, 4);   // Laufzeit-Problem!
    }
}

Probleme:

  • Keine Typsicherheit
  • Konstanten gehören zur Klasse Studi, obwohl sie in anderem Kontext vermutlich auch interessant sind

Verbesserung: Einfache Aufzählung

public enum Fach {
    IFM, ELM, ARC
}


public class Studi {
    public Studi(String name, int credits, Fach studiengang) {
        // Typsicherheit für studiengang :-)
    }

    public static void main(String[] args) {
        Studi rainer = new Studi("Rainer", 10, Fach.IFM);
        Studi holger = new Studi("Holger", 3, 4);   // Syntax-Fehler!
    }
}

Einfache Aufzählungen: Eigenschaften

public enum Fach {
    IFM, ELM, ARC
}
  1. Enum-Konstanten (IFM, ...) sind implizit static und final
  2. Enumerations (Fach) nicht instantiierbar
  3. Enumerations stellen einen neuen Typ dar: hier der Typ Fach
  4. Methoden: name(), ordinal(), values(), toString()

Wiederholung static

Attribute:

  • static Attribute sind Eigenschaften/Zustände der Klasse
  • Gelten in jedem von der Klasse erzeugten Objekt
  • Unterschiedliche Lebensdauer:
    • Objektattribute (Instanzvariablen): ab new bis zum Garbage Collector
    • Statische Variablen: Laufzeitumgebung (JVM) lädt und initialisiert die Klasse (static Attribute existieren, bis die JVM die Klasse entfernt)

Methoden:

  • static deklarierte Methoden sind Klassenmethoden
  • Können direkt auf der Klasse aufgerufen werden
  • Beispiele: Math.max(), Math.sin(), Integer.parseInt()
  • Achtung: In Klassenmethoden nur Klassenattribute nutzbar (keine Instanzattribute!), d.h. keine this-Referenz nutzbar

Wiederholung final: Attribute/Methoden/Klassen nicht änderbar

  • Attribute: final Attribute können nur einmal gesetzt werden

    void foo() {
        int i = 2;
        final int j = 3;
        final int k;
        i = 3;
        j = 4;  // Compilerfehler
        k = 5;
        k = 6;  // Compilerfehler
    }
  • Methoden: final deklarierte Methoden können bei Vererbung nicht überschrieben werden

  • Klassen: von final deklarierten Klassen können keine Unterklassen gebildet werden

Einfache Aufzählungen: Eigenschaften (cnt.)

// Referenzen auf Enum-Objekte können null sein
Fach f = null;
f = Fach.IFM;

// Vergleich mit == möglich
// equals() unnötig, da Vergleich mit Referenz auf statische Variable
if (f == Fach.IFM) {
    System.out.println("Richtiges Fach :-)");
}

// switch/case
switch (f) {
    case IFM:   // Achtung: *NICHT* Fach.IFM
        System.out.println("Richtiges Fach :-)");
        break;
    default:
        throw new IllegalArgumentException("FALSCHES FACH: " + f);
}

Außerdem können wir folgende Eigenschaften nutzen (u.a., s.u.):

  • Enumerations haben Methode String toString() für die Konstanten
  • Enumerations haben Methode final T[] values() für die Iteration über die Konstanten

Enum: Genauer betrachtet

public enum Fach {  IFM, ELM, ARC  }

Compiler sieht (in etwa):

public class Fach extends Enum {
    public static final Fach IFM = new Fach("IFM", 0);
    public static final Fach ELM = new Fach("ELM", 1);
    public static final Fach ARC = new Fach("ARC", 2);

    private Fach( String s, int i ) { super( s, i ); }
}

=> Singleton-Pattern für Konstanten

Enum-Klassen: Eigenschaften

public enum Fach {
    IFM,
    ELM("Elektrotechnik Praxisintegriert", 1, 30),
    ARC("Architektur", 4, 40),
    PHY("Physik", 3, 10);

    private final String description;
    private final int number;
    private final int capacity;

    Fach() { this("Informatik Bachelor", 0, 60); }
    Fach(String descr, int number, int capacity) {
        this.description = descr;  this.number = number;  this.capacity = capacity;
    }
    public String getDescription() {
        return "Konstante: " + name() + " (Beschreibung: " + description
                + ", Kapazitaet: " + capacity + ", Nummer: " + number
                + ", Ordinal: " + ordinal() + ")";
    }
}

Konstruktoren und Methoden für Enum-Klassen definierbar

  • Kein eigener Aufruf von super (!)
  • Konstruktoren implizit private

Compiler fügt automatisch folgende Methoden hinzu (Auswahl):

  • Strings:
    • public final String name() => Name der Konstanten (final!)
    • public String toString() => Ruft name() auf, überschreibbar
  • Konstanten:
    • public final T[] values() => Alle Konstanten der Aufzählung
    • public final int ordinal() => Interne Nummer der Konstanten (Reihenfolge des Anlegens der Konstanten!)
    • public static T valueOf(String) => Zum String passende Konstante (via name())

Hinweis: Diese Methoden gibt es auch bei den "einfachen" Enumerationen (s.o.).

Wrap-Up

  • Aufzählungen mit Hilfe von enum (Compiler erzeugt intern Klassen)

  • Komplexe Enumerations analog zu Klassendefinition: Konstruktoren, Felder und Methoden (keine Instanzen von Enum-Klassen erzeugbar)

  • Enum-Konstanten sind implizit final und static

  • Compiler stellt Methoden name(), ordinal() und values() zur Verfügung

    • Name der Konstanten
    • Interne Nummer der Konstanten (Reihenfolge des Anlegens)
    • Array mit allen Konstanten der Enum-Klasse
Challenges

Im Dungeon sollen Key-Codes aus libGDX (Integer-Werte) als Konstanten zugreifbar sein. Zusätzlich soll es es noch einen String geben, der beschreibt, wo und wie diese Taste im Spiel eingesetzt wird. Aus historischen Gründen ist dies im Dungeon recht komplex gelöst.

Definieren Sie eine neue Enum-Klasse, die Konstanten für Tasten aufzählt (beispielsweise ESCAPE, W, A oder LEFT). Jede dieser Konstanten soll den der Taste zugeordneten Integerwert speichern können und einen String haben, der als Hilfestring verstanden werden könnte (nutzen Sie hier einfach Phantasiewerte). Zeigen Sie in einer kleinen Demo, wie Sie mit diesem Enum arbeiten würden: Zugriff auf die Konstanten, Zugriff auf den Zahlenwert und/oder den String, Übergabe als Funktionsparameter.

Quellen
  • [Java-SE-Tutorial] The Java Tutorials
    Oracle Corporation, 2022.
    Trail: Learning the Java Language :: Classes and Objects :: Enum Types
  • [Ullenboom2021] Java ist auch eine Insel
    Ullenboom, C., Rheinwerk-Verlag, 2021. ISBN 978-3-8362-8745-6.
    Abschnitt 6.4.3: Aufzählungstypen, Abschnitt 10.7: Die Spezial-Oberklasse Enum

Konfiguration eines Programms

TL;DR

Zu Konfiguration von Programmen kann man beim Aufruf Kommandozeilenparameter mitgeben. Diese sind in der über den ParameterString[] args in der main(String[] args)-Methode zugreifbar.

Es gibt oft eine Kurzversion ("-x") und/oder eine Langversion ("--breite"). Zusätzlich können Parameter noch ein Argument haben ("-x 12" oder "--breite=12"). Parameter können optional oder verpflichtend sein.

Um dies nicht manuell auswerten zu müssen, kann man beispielsweise die Bibliothkek Apache Commons CLI benutzen.

Ein anderer Weg zur Konfiguration sind Konfigurationsdateien, die man entsprechend einliest. Hier findet man häufig das "Ini-Format", also zeilenweise "Key=Value"-Paare. Diese kann man mit der Klasse java.util.Properties einlesen, bearbeiten und speichern (auch als XML).

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Auswertung von Kommandozeilenparametern in einem Programm
  • (K3) Apache Commons CLI zur Verarbeitung von Kommandozeilenparametern
  • (K3) Laden von Konfigurationsdaten mit java.util.Properties

Wie kann man Programme konfigurieren?

  1. Parameter beim Start mitgeben: Kommandozeilenparameter (CLI)

  2. Konfigurationsdatei einlesen und auswerten

Varianten von Kommandozeilenparameter

  • Fixe Reihenfolge

    java MyApp 10 20 hello debug

  • Benannte Parameter I

    java MyApp -x 10 -y 20 -answer hello -d

  • Benannte Parameter II

    java MyApp --breite=10 --hoehe=20 --answer=hello --debug

Häufig Mischung von Kurz- und Langformen

Häufig hat man eine Kurzform der Optionen, also etwa "-x". Dabei ist der Name der Option in der Regel ein Zeichen lang. Es gibt aber auch Abweichungen von dieser Konvention, denken Sie beispielsweise an java -version.

In der Langform nutzt man dann einen aussagekräftigen Namen und stellt zwei Bindestriche voran, also beispielsweise "--breite" (als Alternative für "-x").

Wenn Optionen Parameter haben, schreibt man in der Kurzform üblicherweise "-x 10" (trennt also den Parameter mit einem Leerzeichen von der Option) und in der Langform "--breite=10" (also mit einem "=" zwischen Option und Parameter). Das sind ebenfalls Konventionen, d.h. man kann prinzipiell auch in der Kurzform das "=" nutzen, also "-x=10", oder in der Langform mit einem Leerzeichen trennen, also "--breite 10".

Hinweis IntelliJ: "Edit Configurations" => Kommandozeilenparameter unter "Build and run" im entsprechenden Feld eintragen

Auswertung Kommandozeilenparameter

  • Kommandozeilenparameter werden als String-Array an main()-Methode übergeben:

    public static void main(String[] args) { }
    public static void main(String... argv) { }

    => Müssen "händisch" ausgewertet werden

Anmerkung: Nur Parameter! Nicht Programmname als erster Eintrag wie in C ...

Beispiel Auswertung Kommandozeilenparameter

public static void main(String[] args) {
    int x = 100;
    String answer = "";
    boolean debug = false;

    // Parameter: -x=10 -answer=hello -debug
    // => args = ["-x=10", "-answer=hello", "-debug"]
    for (String param : args) {
        if (param.startsWith("-x")) { x = Integer.parseInt(param.substring(3)); }
        if (param.startsWith("-a")) { answer = param.substring(8); }
        if (param.startsWith("-d")) { debug = true; }
    }
}

Kritik an manueller Auswertung Kommandozeilenparameter

  • Umständlich und unübersichtlich
  • Große if-else-Gebilde in main()
  • Kurz- und Langform müssen getrennt realisiert werden
  • Optionale Parameter müssen anders geprüft werden als Pflichtparameter
  • Überlappende Parameternamen schwer aufzufinden
  • Prüfung auf korrekten Typ nötig bei Parametern mit Werten
  • Hilfe bei Fehlern muss separat realisiert und gepflegt werden

Apache Commons: CLI

Rad nicht neu erfinden!

Annäherung an fremde API:

  • Lesen der verfügbaren Doku (PDF, HTML)
  • Lesen der verfügbaren Javadoc
  • Herunterladen der Bibliothek
  • Einbinden ins Projekt

Exkurs: Einbinden fremder Bibliotheken/APIs

Eclipse

  • Lib von commons.apache.org herunterladen und auspacken
  • Neuen Unterordner im Projekt anlegen: libs/
  • Bibliothek (.jar-Files) hinein kopieren
  • Projektexplorer, Kontextmenü auf .jar-File: "Add as Library"
  • Alternativ Menü-Leiste: "Project > Properties > Java Build Path > Libraries > Add JARs"

IntelliJ

  • Variante 1:
    • Lib von commons.apache.org herunterladen und auspacken
    • Neuen Unterordner im Projekt anlegen: libs/
    • Bibliothek (.jar-Files) hinein kopieren
    • Variante 1 (a):Projektexplorer, Kontextmenü auf .jar-File: "Build Path > Add to Build Path"
    • Variante 1 (b): Projekteigenschaften, Eintrag "Libraries", "+", "New Project Library", "Java" und Jar-File auswählen
  • Variante 2:
    • Projekteigenschaften, Eintrag "Libraries", "+", "New Project Library", "From Maven" und "commons-cli:commons-cli:1.5.0" als Suchstring eingeben und die Suche abschließen

Gradle oder Ant oder Maven

  • Lib auf Maven Central suchen: "commons-cli:commons-cli" als Suchstring eingeben
  • Passenden Dependency-Eintrag in das Build-Skript kopieren

Kommandozeilenaufruf

  • Class-Path bei Aufruf setzen:

    • Unix: java -cp .:<jarfile>:<jarfile> <mainclass>
    • Windows: java -cp .;<jarfile>;<jarfile> <mainclass>

    Achtung: Unter Unix (Linux, MacOS) wird ein Doppelpunkt zum Trennen der Jar-Files eingesetzt, unter Windows ein Semikolon!

Beispiel: java -classpath .:/home/user/wuppy.jar MyApp

Vorgriff auf Build-Skripte (spätere VL): Im hier gezeigten Vorgehen werden die Abhängigkeiten manuell aufgelöst, d.h. die Jar-Files werden manuell heruntergeladen (oder selbst kompiliert) und dem Projekt hinzugefügt.

Alle später besprochenen Build-Skripte (Ant, Gradle) beherrschen die automatische Auflösung von Abhängigkeiten. Dazu muss im Skript die Abhängigkeit auf geeignete Weise beschrieben werden und wird dann beim Kompilieren des Programms automatisch von spezialisierten Servern in der im Skript definierten Version heruntergeladen. Dies funktioniert auch bei rekursiven Abhängigkeiten ...

Überblick Umgang mit Apache Commons CLI

Paket: org.apache.commons.cli

  1. Definition der Optionen
    • Je Option eine Instanz der Klasse Option
    • Alle Optionen in Container Options sammeln
  2. Parsen der Eingaben mit DefaultParser
  3. Abfragen der Ergebnisse: CommandLine
  4. Formatierte Hilfe ausgeben: HelpFormatter

Die Funktionsweise der einzelnen Klassen wird in der Demo kurz angerissen. Schauen Sie bitte zusätzlich in die Dokumentation.

Laden und Speichern von Konfigurationsdaten

#ola - ein Kommentar
hoehe=2
breite=9
gewicht=12
  • Konfigurationsdaten sind i.d.R. Schlüssel-Wert-Paare (String/String)

    => java.util.Properties

    Tatsächlich verbirgt sich ein Hashtable dahinter:

    public class Properties extends Hashtable<Object,Object>;

Laden und Speichern von Konfigurationsdaten (cnt.)

  • Properties anlegen und modifizieren

    Properties props = new Properties();
    props.setProperty("breite", "9");
    props.setProperty("breite", "99");
    String value = props.getProperty("breite");
  • Properties speichern: Properties#store und Properties#storeToXML

    public void store(Writer writer, String comments)
    public void store(OutputStream out, String comments)
    public void storeToXML(OutputStream os, String comment, String encoding)
  • Properties laden: Properties#load und Properties#loadFromXML

    public void load(Reader reader)
    public void load(InputStream inStream)
    public void loadFromXML(InputStream in)

java.util.Properties sind eine einfache und im JDK bereits eingebaute Möglichkeit, mit Konfigurationsdateien zu hantieren. Deutlich umfangreichere Möglichkeiten bieten aber externe Bibliotheken, beispielsweise "Apache Commons Configuration" (commons.apache.org/configuration).

Wrap-Up

  • Kommandozeilenparameter als String[] in main()-Methode
  • Manuelle Auswertung komplex => Apache Commons CLI
  • Schlüssel-Wert-Paare mit java.util.Properties aus/in Dateien laden/speichern
Quellen
  • [Java-SE-Tutorial] The Java Tutorials
    Oracle Corporation, 2022.
    Essential Java Classes \> The Platform Environment \> Configuration Utilities