| home | suche | kontakt/johner | institut | hinweise studierende | tech-docs | blog | mindmailer |
![]() |
2 Vertiefung Java
2.1 Assertions
Assertions werden eingesetzt um Vor- und Nachbedingungen prüfen zu können (z.B. dass Preise positiv sein müssen). Das Schlüsselwort dafür heisst assert (gibt es seit Java 1.4). Die Syntax lautet folgendermaßen
assert <BEDINGUNG>:<[BESCHREIBUNG]>;
wobei die Beschreibung optional ist und die Bedingung immer vom Typ boolean sein muss, ansonsten gibt es einen Compilerfehler.
Um Assertions zu aktivieren muss der JVM Parameter ea mit angegeben werden (z.B. java -ea assertions.AssertionTest). Wird ein Programm, das assert-Anweisungen enthält, mit deaktivierten Assertions aufgerufen, verhält es sich, als wären die entsprechenden Zeilen nicht vorhanden.
Wenn Assertions aktiviert sind überprüft das Programm die Bedingung und fährt fort, wenn sie erfüllt ist, andernfalls wird ein AssertionError erzeugt. Wenn die <BESCHREIBUNG> fehlt, hat der AssertionError eine leere Fehlermeldung, ansonsten wird der dort beschriebene Ausdruck an den Konstruktor von AssertionError übergeben und dient als Meldungstext für das Fehlerobjekt.
Auch wenn die selben Tests auch mit einer if-Anweisung gemacht werden könnten, ist es oft empfehlenswert auf Assertions zurückzugreifen:
- Der Programmcode ist kürzer.
- Auf den ersten Blick ist zu erkennen, dass es sich um einen Korrektheits-Check handelt und nicht um eine Verzweigung zur Steuerung des Programmablaufs.
- Assertions lassen sich zur Laufzeit wahlweise an- oder ausschalten. Im Gegensatz zu einer einfachen if-Anweisung verursachen sie praktisch keine Verschlechterung des Laufzeitverhaltens, wenn sie deaktiviert sind.
Jedoch sollten Assertions nur bei der Entwicklung eingesetzt werden und außerdem nie bei public-Methoden oder Kommandozeilenparametern. Diese Überprüfungen sollten immer aktiv sein und nicht abschaltbar sein.
package progstruk.assertions;
|
Bitte eine Zahl zwischen 0
und 10 eingeben: -7
Exception in thread "main"
java.lang.AssertionError: falsche Zahl: -7
at progstruk.assertions.AssertTest.main(AssertTest.java:13)
Diese Fehlermeldung wird ausgegeben, wenn Assertions aktiviert sind und eine Zahl eingegeben wird, die nicht zwischen 0 und 8 eingeben wird.
2.2 Reflection und Annotation
Das Reflection-Modell erlaubt es uns, Klassen und Objekte, die zur Laufzeit von der JVM im Speicher gehalten werden, zu untersuchen und in begrenztem Umfang zu modifizieren. Reflection gehören in die Kategorie Meta-Programming, da sie auf den Klassen und Objekten anderer Programme operieren, es werden sogenannte Metadaten verwendet. Ein Metadatum ist eine Information über eine Information. In Java beschreibt ein Class-Objekt, was Klassen »können«, also welche Konstruktoren und Methoden sie haben, welche Attribute sie besitzen und wie die Erweiterungsbeziehungen sind. Um an die entsprechenden Daten zu gelangen, stehen folgende Methoden auf den Klassen java.lang.reflect.Method und java.lang.Class zur Verfügung. Weil man dabei in die Klasse "hereinschaut" und prüft was darauf definiert ist, spricht man dabei von Introspection.
Methoden auf "Method":
- getName():String
- getParameterTypes():Class[]
- getReturnType():Class
Methoden auf "Class":
- getConstructors():Constructor[]
- getMethods():Method[] //öffentliche und vererbte Methoden
- getDeclaredMethods():Method[] //private und öffentliche Methoden der Klasse, keine vererbten Methoden
- isAbstract():boolean
"Declared" in der Methode getDeclaredMethods() bedeutet welche Methoden sind tatsächlich in der Klasse programmiert, also stehen in der Java-Klasse, egal ob private oder public. Methoden aus der Oberklasse werden hier nicht zurückgeliefert, weil diese sind nicht in der abgefragten Klasse beschrieben.
Hier ein Beispiel wie eine Klasse nur mit dem Klassenname als String instanziert und eine Methode mit Hilfe des Namens aufgerufen wird über Reflections.
Vor- und Nachteile von Reflections
| Vorteile | Nachteile |
|---|---|
| Flexibilität - erst zur Laufzeit wird entschieden, welche Klasse/Methode instaziert/aufgerufen wird | Performance |
| compilierter Code kann inspiziert werden (eventl. auch Nachteil) | fehleranfällig (keine Kompiliersicherheit, Fehler erst zur Laufzeit) |
| kann in verteilten Anwendungen zum Einsatz kommen |
Auch bei Annotations geht es um Metadaten. Mit Annotations ist es dem Programmiere möglich Metadaten Klassen, Methoden, Attributen usw. hinzuzufügen. Annotations haben verschiedene Nutzen, z.B.
- Informationen für den Compiler (Fehlerentdeckung, Warnings ausblenden...)
- Verarbeitung während der Entwicklungs- und Kompilierzeit, z.B. nutzen Softwaretools Annotations um Code, XML Dateien usw. zu generieren
- Informationen währen der Laufzeit mit Annotations
Bekannte Annotations sind z.B. @Deprecated (nicht mehr benutzen, abgelehnt) oder @Override (eine Methode der Oberklasse wird überschrieben), diese Annotations werden vor eine Methode gestellt.
Annotations stehen erst mit Java 5.0 zur Verfügung.
Um etwas als Annotations zu deklarieren, wird immer das @-Zeichen vorangestellt.
Folgendes Beispiel zeigt wie man Informationen (Metadaten) in diesem Fall einer Klasse hinzufügt.
package annots;
|
//Hier werden den oben definierten Informationen Werte zugewiesen,
|
Die einzelnen Informationsteile dürfen String, primitive, Annotations, Enums, Class und Arrays (aus den entsprechenden Typen) als Rückgabewerte haben.
Man hat die Möglichkeit bei der Definition der Anntotaionstruktur bestimmte Wertigkeitsbereiche mitanzugeben, was wiederrum mit Annotations passiert, man annotiert sozusagen Annotations.
- @Retention - Sichtbarkeit (SOURCE, CLASS, RUNTIME)
- @Target - was damit annotiert werden darf (TYPE, METHOD...)
Die Annotations und ihre Informationen kann man jederzeit über die Methode getAnnotations() auf der Klasse java.lang.Class auslesen.
package annots;
|
2.3 JavaDocs
Javadoc ist ein Programm, das aus Java-Quelltexten Dokumentationen im HTML-Format erstellt. Die Dokumentation besteht dabei aus den öffentlichen Klassen-, Interface- und Methodendeklarationen und eventuell zusätzlichen Dokumentationskommentaren. Ein Dokumentationskommentar beginnt mit /** und endet mit */ und muss im Quelltest immer unmittelbar vor dem zu dokumentierenden Item (Klassendefiniton, Methode...) platziert werden. Dieser Kommentar darf neben normalem Text auch HTML-Tags enthalten. Außerdem erkennt javadoc markierte Absätze, die mit dem @-Zeichen eingeleitet werden. Die Markierung leitet einen neuen Absatz ein und haben einen vordefinierten Wert. Zum Beispiel wird die Markierung @param beim generieren als Parameters: übersetzt.
package javadoc;
|
Bei der Generierung der Dokumentation im HTML-Format wird für jede Klasse eine HTML-Seite generiert, die den gleichen Namen trägt wie die Klasse selbst. In Eclipse kann man sich einfach über File-->Export-->Javadoc die Seiten vom JavaCompiler erzeugen lassen. In der Konsole erfolgt der Aufruf mit dem Befehl javadoc. Für das Beispiel würde die erzeugte Seite so aussehen.
2.4 Logging
Ein Logger bietet dem Programmierer deutlich mehr Möglichkeiten die Ausgaben eines Programms zu variieren. Es kann dort definiert werden, wo die Meldungen ausgegeben werden (Konsole, Schreiben in Datei....), man kann beeinflussen, welche Meldungen tatsächlich ausgegeben werden oder auch die Nachrichten vor der Ausgaben formatieren.
Die Filterung, welche Nachrichten ausgegeben werden geschieht über sogenannte LogLevels. Es gibt folgende LogLevel:
- severe
- warn
- info
- debug
- fine
- finer
- finest
Jeder Meldung, die man dem Logger übergibt, fügt man ein LogLevel hinzu. Das ist entweder möglich direkt über die zum LogLevel zugehörige Methode oder als Übergabeparameter auf der log()-Methode.
logger.severe("Schhade ein Fehler");
logger.log(Level.SEVERE, "Schade ein Fehler", e);
Um jetzt zu entscheiden, wieviele Nachrichten man ausgeben will, also wie detailliert sollen meine Informationen sein, wird die Methode setLevel() auf dem Logger genutzt. Als Übergabeparameter werden die static Attribute der Klasse Level genutzt.
logger.setLevel(Level.INFO);
Der Übergabeparameter gibt ab welchem Level der Nachrichten, diese ausgegeben werden. Es werden hier also nicht nur Nachrichten mit dem Level "info" ausgegeben sondern auch alle Nachrichten mit übergeordneteren ("wichtigeren") Leveln.
Bei der Arbeit mit einem Logger ist es unkompliziert Formatierungen der Meldungen anzugeben. Dafür steht die Methode addHandler() zur Verfügung. Dieser Methode wird ein Handler, z.B. ein FileHandler, vom Typ java.util.logging.Handler übergeben. Dieser Handler kann wiederrum einen Filter und Formatter besitzen. Dort kann zum Beispiel das Datumsformat angegeben werden. Es gibt bereits schon fertige Formatter wie z.B. java.util.logging.SimpleFormatter oder java.util.logging.XMLFormatter, die man natürlich durch eine abgeleitete Klasse noch weiter anpassen kann.
package progstruk.logging;
|
2.5 Arbeiten mit Dateien
Einlesen von Dateien
- java.io.BufferedReader, bietet z.B. die Methode readLine()
- java.io.Scanner, parsen von primitiven Typen und Strings mit der Methode next() oder nextInt()
Verarbeitung von eingelesenen Daten
- java.util.StringTokenizer, ein String kann durch in einzelne Token aufgeteilt werden, durch ein angegebenes Trennzeichen, mit nextToken() weiterlesen
2.6 XML und Dom4J
Dom4j ist eine Programmierschnittstelle für Java für den Zugriff und die Verarbeitung von XML-Dokumenten.
Ein neues Dokument wird mit dem DocumentHelper erzeugt über die statische Methode createDocument(). Auf diesem Dokument kann über addElement() ein neues Wurzelelement hinzugefügt werden. Diese Methode liefert dabei den erzeugten und angehängten Knoten als org.dom4j.Element zurück. Auf diesem Element können wiederrum Kindknoten angehängt werden mit der Methode addElement(). Mit den Methoden addText(String) und addAttribute(String, String) können Werte zwischen den Tags eingetragen werden und dem Element Attribute zugewiesen werden. Bei der Methode addAttribute() wird als erster Übergabeparameter der Name des Attributs und als Zweiter den Wert des Attributs angegeben.
public Document createDocument(){
|
Die Klasse org.dom4j.Document stellt nur eine Baumstruktur dar, in der Elemente und eventuell ihre Werte stehen, dies ist noch keine XML-Datei. Mit Hilfe des org.dom4j.io.XMLWriter kann aber nun aus einem Document eine XML-Datei geschrieben werden.
XMLWriter writer;
|
Es besteht die Möglichkeit dem XMLWriter ein OutputFormat zu übergeben. Dabei wird dann die XML-Datei schön formatiert und nicht der Inhalt einfach aneinander gehängt. Dafür muss man dem XMLWriter im Konstruktor ein OutputFormat übergeben, dabei gibt es schon default Formatierungen.
OutputFormat format = OutputFormat.createPrettyPrint();
XMLWriter writer = new XMLWriter(new FileWriter(path), format);
Wenn kein leeres Document erzeugt werden soll, sonder ein Document das bereits mit Inhalt aus einem String gefüllt sein soll wird die Methode parseText(String) vom DocumentHelper genutzt. Natürlich muss der String bereits im XML-Format bestehen.
String xmlstring = "<html><body>Hallo</body></html>"
Document htmldocument = DocumentHelper.parseText(xmlstring);
Ein Document kann man auch mit Hilfe von XPath Ausdrücken durchsuchen und nach bestimmten Knoten oder Werten suchen.
List<Node> nodes = freizeit.selectNodes("//Hobby[text()-'Programmieren']");
|
2.7 Threads
2.7.1 Nebenläufigkeit
Als Nebenläufigkeit beziechnet man die Fähigkeit eines Systems, zwei oder mehr Vorgänge gleichzeitg oder quasi-gleichzeitig ("parallel") ausführen zu können. Dabei unterscheidet man von Multitasking und Multithreading. Von Multitasking spricht man, wenn auf einem Rechner mehrere Programme (Prozesse) "parallel" ausgeführt werden. Durch Multithreading kann die Nebeläufigkeit innerhalb (!) eines Prozesses realisiert werden, zum Beispiel wartet die GUI auf einen Mausklick. Multithreading erlaubt es, dass das Programm mehrere CPUs nutzt.
Ein wichtiger Unterschied zwischen Threads und Prozessen ist der, dass alle Threads eines Programms sich einen gemeinsamen Adressraum teilen, also auf dieselben Variablen zugreifen, während die Adressräume unterschiedlicher Prozesse streng voneinander getrennt sind.
2.7.2 Realisierung von Multithreading in Java
Threads werden in Java durch die Klasse Thread und das Interfaces Runnable implementiert. In beiden Fällen wird der Thread-Body, also der prallel auszuführende Code, in Form der überlagerten Methode run zur Verfügung gestellt.
package parallel;
|
package parallel;
|
Nicht immer ist es möglich, eine Klasse, die als Thread laufen soll, von Thread abzuleiten. Dies ist insbesondere dann nicht möglich, wenn die Klasse Bestandteil einer Vererbungshierrachie ist, die eingentlich nichts mit Multithreading zu tun hat. Da Java keine Mehrfachvererbung kennt, kann eine bereits abgeleitete Klasse nicht von einer weiteren Klasse erben. Um trotzdem das Multithreading-Konzept nutzen zu können wird einfach das Interface Runnable implementiert. Dieses Interface besteht nur aus der run-Methode. Tatsächlich muss jede Klasse, deren Instanzen als Thread laufen sollen , das Interface Runnable implementieren (sogar die Klasse Thread selbst).
Um eine nicht von Thread abgeleitete Instanz als Thread laufen zu lassen, sind folgende Schritte zu befolgen.
- Zunächst ein Thread-Objekt erzeugen
- An den Konstruktor wird das Objekt übergeben das parallel ausgeführt werden soll
- Die Methode start des neuen Thread-Objekts aufrufen
Dieses Beispiel zeigt die Vorgehensweise:
package parallel;
|
2.7.3 Arbeiten mit Threads
Die Methode run sollte vom Programm niemals direkt aufgerufen werden. Um einen Thread zu starten, ist immer start aufzurufen! Dadurch wird der neue Thread erzeugt und initialisiert und ruft schließlich selbst run auf, um den Anwendungscode auszuführen. Ein direkter Aufruf von run würde dagegen keinen neuen Thread erzeugen, sonder wäre ein normaler Methodenaufruf wie jeder andere und würde direkt aus dem bereits laufenden Thread des Aufrufers erfolgen.
Es gibt 3 Möglichkeiten, dass ein Thread zuende ist
- das Ende der run()-Methode ist erreicht
- run() bricht mit Fehlern ab
- der Thread wird von außen mit interrupt() unterbrochen, allerdings nur wenn innerhalb des Threads eine Überprüfung stattfindet.
Durch Aufruf von interrupt wird ein Flag gesetzt, das eine Unterbrechungsanforderung signalisiert. Durch Aufruf von isInterrupted kann der Thread feststellen, ob das Abbruchflag gesetzt wurde und der Thread beendet werden soll.
package parallel;
|
Mit der while-Schleife wird überprüft, ob der Thread von außen noch nicht abgebrochen wurde.
Die Bildschirmausgabe in diesem Programm ist vermutlich deutlich kürzer, als die Pause nach der Bildschirmausgabe. Deswegen ist es recht wahrscheinlich, dass der Aufruf von interrupt während des Aufrufs von sleep erfolgt. Ist das der Fall, wird sleep mit einer Interrupted-Exception abgebrochen (auch wenn die geforderte Zeitspanne noch nicht vollständig verstrichen ist). Wichtig ist hier, dass das Abbruchflag von des Exception zurückgesetzt wird (isInterrupted liefert dann wieder false) und der Aufruf von interrupt somit eigentlich verlorengehen würde, wenn er nicht direkt in der catch-Klausel behandelt würde. Wir rufen daher innerhalb der catch-Klausel interrupt erneut auf, um das Flag wieder auf true zu setzen und run die Abbruchanforderung zu signalisieren.
Bei endlos laufenden Threads besteht die Möglichkeit über die Methode setDeamon(true) zu signalisieren, dass beim Beenden der main() der Thread ebenfalls beendet werden soll.
Thread infinity = new Thread(new InfinityCommand());
|
2.7.4 Synchronisierung von Threads
Zur Synchronisierung nebenläufiger Prozesse hat Java das Konzept des Monitors implementiert. Die Klasse Start gibt den beiden Threads einen gemeinsamen Monitor mit. Über diesen Monitor (vom Typ Object) können sich die beiden Threads mit Hilfe der Methoden wait() und notify() verständigen. Die Methoden wait() und notify() erbt jede Klasse von Object. Beide Methoden müssen in einem Block stehen, der synchornized ist.
Durch synchronized kann entweder eine komplette Methode oder ein Block innerhalb einer Methode geschützt werden. Das folgende Codebeispiel zeigt einen Thread, der einen Teil der run-Methode über das Monitor-Objekt synchonisiert. Das Monitor-Objekt wird ihm im Konstruktor übergeben. Ein anderer Thread, der mit diesem Thread zusammenarbeitet, muss das gleiche Objekt ebenfalls im Konstruktor übergeben bekommen.
package parallel2;
|
2.7.5 Synchronisierung von Threads mit Java 5
In Java 5 läuft die Synchronisation nicht mehr über ein normales Object, sondern über die spezielle Klassen java.util.concurrent.locks.Condition und java.util.concurrent.locks.Lock.
- Condition wird von Lock erzeugt
- condition.await() ist Pendant zu object.wait()
- condition.signal() ist Pendant zu object.notify()
signal() und wait() müssen im synchronisierten Block innerhalb des locks stehen. Die Synchronisation erfolgt über den Aufruf der Methode lock.lock() und beim Beenden lock.unlock().
package parallel3;
|
2.8 Reguläre Ausdrücke
Ab Java 5 gibt es ein neues Package java.util.regex. Mit Hilfe dieses Package können Strings nach bestimmten Pattern ("Textmustern") durchsucht werde und diese auch ersetzen.
Für die Beschreibung von diesen Pattern stehen Zeichenklassen zur Verfügung:
- \d - Ziffern
- \D - Nicht-Ziffern
- \w - Wortzeichen: 0-9, a-z, A-Z
- \W - Nicht-Wortzeichen
- \s - Whitespaces: Leerzeichen, Tabulator, Zeilenumbruch...
- \S - Nicht-Whitespaces
- \[] - Selbstdefinierte mit beliebigen Zeichen
- (a|b) - entweder oder
- . - beliebiges Zeichen
Diese Muster können auch wiederholt werden, dafür gibt es weitere vordefinierten Zeichen:
- * - Vorstehendes Muster 0 oder mehrmals wiederholen
- ? - Vorstehendes Muster 0 oder einmal wiederholen
- + - Vorstehendes Muster 1 oder mehrmals wiederholen
Die Verwendung der Zeichenklassen und die Überprüfung eines String auf bestimmte Textmustern wird im folgenden Beispiel gezeigt.
package reggis;
|
Der String regex beschreibt ein Textmuster mit Hilfe der oben beschriebenen Zeichenklasse. In diesem Beispiel darf der String mit beliebigen und beliebig vielen Zeichen beginnen, auch kein Zeichen ist möglich. Anschließend muss die Zeichenkette "HTWG" mit mindestens einer Zahl im Anschluss folgen. Der Rest ist optional und es gibt keine Einschränkungen.
Nun gibt es die Möglichkeit den String text auf dieses Textmuster zu untersuchen. Dabei kann einfach die Methode machtes(String) auf der Klasse String aufgerufen werden, diese Methode liefert entweder true oder false. Eine andere Möglichkeit ist es die neuen Java-Klassen java.util.regex.Matcher und java.util.regex.Pattern zu nutzen. Mit der statischen Methode Pattern.compile(String textmuster) kann ein beschriebenes Pattern kompiliert. Diese Methode liefert ein Pattern zurück. Über dieses Objekt kann jetzt einfach die Methode matcher(String) aufgerufen werden. Der Vorteil hier ist, dass das Textmuster nur einmal kompiliert werden muss und bei langen Texten mehrere Textmuster überprüft werden können.
Soll das Ergebnis der Überprüfung als boolean zurückgegeben werden, kann über das Matcher Objekt (wird von der Methode machter(String) zurückgegeben) die Methode machtes() aufgerufen werden.
2.9 PDF Generierung mit iText
Um iText nutzen zu können muss die Library iText-2.0.7.jar erst in das Projekt integriert werden (download on http://www.lowagie.com/iText/download.html). Diese jar ist auch im JFreeChart download im Order libs enthalten.
Einen ersten Einblick, wie iText verwendet wird, zeigt das kommentierte Beispiel.
package pdf;
|
2.10 Grafiken mit JFreeChart
Um JFreeChart nutzen zu können müssen erst jfreechart-1.0.8.jar und jcommon-1.0.12.jar in das Projekt eingebunden werden ( download on: http://sourceforge.net/project/showfiles.php?group_id=15494). Mit JFreeChart können alle möglichen Diagramm schnell in Java dargestellt werden. JFreeChart ist auch in Kombination mit iText nützlich, so können Diagramm in einen pdf-Dokument generiert werden. Ein Beispiel mit JFreeChart ist unter Skripts und Fragen in Stunde 15 zu finden.



