Subsections of Fortgeschrittene Java-Themen und Umgang mit JVM
Serialisierung von Objekten und Zuständen
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.
- (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 beiSerializable
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
undtransient
, 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:
- Tutorials:
- API:
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
undstatic
Attribute werden nicht serialisiert- De-Serialisierung: KEIN Konstruktor-Aufruf!
- Serialisierbarkeit vererbt sich
- Objekt-Referenz-Graph wird automatisch beachtet
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.
- [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
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.
- (K1) Wichtigste Methoden von
java.util.regex.Pattern
undjava.util.regex.Matcher
- (K2) Unterschied zwischen
Matcher#find
undMatcher#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
- Schritt 1: Ein Pattern compilieren (erzeugen) mit
-
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
: erfolgreichMatcher#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
- Gruppe 0:
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
undjava.util.regex.Matcher
- Unterschied zwischen
Matcher#find
undMatcher#matches
! - Quantifizierung ist möglich, aber greedy (Default)
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
undfirma
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 Teildomain
folgt direkt auf das@
-Zeichen. - Der Bestandteil
domain
besteht aus 2 oder 3 Kleinbuchstaben.
Hinweis: Sie dürfen keinen Oder-Operator verwenden.
- [Java-SE-Tutorial] The Java Tutorials
Oracle Corporation, 2022.
Essential Java Classes \> Regular Expressions
Aufzählungen (Enumerations)
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.
- (K2) Vorgänge beim Initialisieren von Enum-Klassen (Hinweis:
static
) - (K3) Erstellung komplexer Enumerationen mit Feldern und Konstruktoren
- (K3) Nutzung von
name()
,ordinal()
undvalues()
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
}
- Enum-Konstanten (
IFM
, ...) sind implizitstatic
undfinal
- Enumerations (
Fach
) nicht instantiierbar - Enumerations stellen einen neuen Typ dar: hier der Typ
Fach
- 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)
- Objektattribute (Instanzvariablen): ab
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 werdenvoid 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()
=> Ruftname()
auf, überschreibbar
- Konstanten:
public final T[] values()
=> Alle Konstanten der Aufzählungpublic final int ordinal()
=> Interne Nummer der Konstanten (Reihenfolge des Anlegens der Konstanten!)public static T valueOf(String)
=> Zum String passende Konstante (vianame()
)
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
undstatic
-
Compiler stellt Methoden
name()
,ordinal()
undvalues()
zur Verfügung- Name der Konstanten
- Interne Nummer der Konstanten (Reihenfolge des Anlegens)
- Array mit allen Konstanten der Enum-Klasse
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.
- [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
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).
- (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?
-
Parameter beim Start mitgeben: Kommandozeilenparameter (CLI)
-
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 inmain()
- 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!
- Apache Commons bietet die CLI-Bibliothek zum Umgang mit Kommandozeilenparametern an: commons.apache.org/cli
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!
- Unix:
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
- Definition der Optionen
- Je Option eine Instanz der Klasse
Option
- Alle Optionen in Container
Options
sammeln
- Je Option eine Instanz der Klasse
- Parsen der Eingaben mit
DefaultParser
- Abfragen der Ergebnisse:
CommandLine
- 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
undProperties#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
undProperties#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[]
inmain()
-Methode - Manuelle Auswertung komplex => Apache Commons CLI
- Schlüssel-Wert-Paare mit
java.util.Properties
aus/in Dateien laden/speichern
- [Java-SE-Tutorial] The Java Tutorials
Oracle Corporation, 2022.
Essential Java Classes \> The Platform Environment \> Configuration Utilities