Ausarbeitung Fragenkatalog APR Angewandte Programmierung 5AHITS 2013/14 1/89 Inhalt Programmerstellung in C und Java Sprachumfang von C und Java Datentypen in C und Java UML Relationen zwischen Klassen in Java Streams in Java Fehlerbehandlung in C und Exceptions in Java Nebenläufige Programmierung in Java – Threads Dateiarten - Lesen und Schreiben von Dateien in C und in Java Dynamische Datenstrukturen Collections in Java Graphische Oberfläche in Java JDBC RMI Java und XML Java und Sockets Java Generics Drucken in Java Android Programmierung Logging und Unit-Testing in Java 2/89 Programmerstellung in C und Java Die Erstellung von Programmen in C und Java ist in den wesentlichen Aspekten (Editor, Präprozessor, Compiler, Objekt-/Byte-Code, Linker, make, Bibliothek, Dokumentation, Sourcecode-Verwaltung, Programm, Ausführung, ...) zu erklären. Anfertigen C und Java Programme können ansich in jedem gewöhnlichen ASCII - Texteditor geschrieben werden. Gespeichert werden C Programme (Quellcodedateien) mit der Endung .c und Java Programme mit .java Übersetzung eines C Programmes ● ● ● ● C Programmcode mit .c gespeichert. Der Präprozessor verarbeitet alle Zeilen, die mit einer Direktive (#) beginnen. ○ Header Files einfügen (#include…) ○ definierte Konstanten einfügen (Makros) ○ bedingte Kompilierungen (#if, #endif) Der Compiler (gcc) übersetzt den Quellcode in eine Objektdatei (Modul, .obj) Es enthält den Maschinencode. Jedes C File wird zu einem .obj File übersetzt. Der Linker bindet alle Objektdateien sowie statische und dynamische Bibliotheken zusammen -> man bekommt eine exe-Datei (Ausführbare Datei). Übersetzung eines Java Programmes ● ● ● Java Programmcode mit .java speichern Der Compiler (javac aus JDK) erstellt eine gleichnamige .class Datei, diese enthält Bytecode und ist somit Plattformunabhängig Die JVM (Java Virtuell Machine) interpretiert den Bytecode während der Ausführung Ausführen Das C Programm kann einfach über die .exe ausgeführt werden. Für Java benötigt man zum Ausführen das Java Runtime Enviroment (ist im JDK enthalten), um es auszuführen. Compiler vs. Interpreter Ein Compiler übersetzt das ganze Programm auf einmal. Alle Befehle werden in Maschinensprache übersetzt und das fertige Maschinen-Programm wird zur Ausführung gespeichert. Der Interpreter hingegen übersetzt während der Ausführung einen Programmteil, führt ihn aus, übersetzt den nächsten usw. (Schritt für Schritt). Sourcecodeverwaltung Versionsmanagemantprogramme (z.B.: SVN - Subversion, GIT) verwalten die verschiedenen Versionen der Programme. Somit können mehrere Benutzer gleichzeitig an einem Projekt arbeiten. 3/89 Man kann auch auf verschiedene Versionen vom Programmcode zugreifen und diese Wiederherstellen. make Das Compilieren von Quelltext Dateien kann mit dem Programm make und einem makefile organisiert werden. Mann kann eigenständig im makefile Abhängigkeiten von Dateien und diverse Schritte wie löschen und kopieren zusammenfassen. Man gibt z.B. an, dass das ein Programm aus den Quelldateien “Datei1.c”,“Datei2.c”, usw. erstellt wird. Bibliotheken Bibliotheken stellen bereits programmierte Datenstrukturen, Funktionen, Methoden etc. zur Verfügung. Statische Bibliotheken (in C) Nach dem Kompilieren verbindet der Linker die Objektdatei mit dem Code aus den Bibliotheken. Danach setzt der Linker das ausführbare Programm zusammen, dass alle statischen Bibliotheken beinhaltet. Dynamische Bibliotheken (in C) Der Linker lädt die Programmteile erst bei Bedarf in den Arbeitsspeicher. Beim dynamischen Binden werden Code und Bibliothek nur lose mit Referenzen verknüpft. Das auflösen der Referenzen geschieht dann entweder vor Start des eigentlichen Programms oder erst beim Laden der Bibliothek. Vorteil von dynamischen Bibliotheken ist es, dass Programme von nachträglichen Fehlerbehebung in der dyn. Bibliothek profitieren und nicht neu augeliefert werden müssen. Außerdemo muss eine Bibliothek nur einmal für mehrere Anwendungen auf dem System vorhanden sein. Nachteil, die Bibliotheken können auch so verändert werden, dass das Programm nicht mehr funktioniert . Dokumentation C Doxygen: Wenn man die Kommentierung richtig formatiert, kann man sich eine HTML Seite (vergleichbar zur API Doc in Java), LATEX-Code, RTF-Dateien,... generieren lassen. Java Javadoc: ein Programm welches aus Quelltexten und den entsprechenden Kommentaren eine Dokumentation im HTML - Format erstellt. abc.java → abc.html Tag & Parameter Ausgabe Verwendung in @author name Beschreibt den Autor. Klasse, Interface 4/89 @serial Bei einem Serializeable Objekt Klasse beschreibt es die serialisierten Daten des Objekts. @param name Parameterbeschreibung einer description Methode. @return description Beschreibung des Methode Methode Rückgabewerts einer Methode. Beispiel: 5/89 Sprachumfang von C und Java Was bedeuten prozedural bzw. objektorientiert? (Siehe VSDB Ausarbeitung Frage 22 Softwaretechnik) Bestandteile der beiden Sprachen (Kontrollstrukturen, Datentypen, …). Was ist nicht Teil der Sprache? Prozedurale Programmierung Das Programm wir solange in Teilprogrammen unterteilt (Prozeduren), biskurze zusammengehörige Befehlsfolgen übrig sind. Diese werden in Prozedur zusammengefasst, als solche aufgerufen und dann Schritt für Schritt abgearbeitet. Ein wichtiger Bereich hier sind die Algorithmen (Folge von Anweisungen und Anleitung zum lösen eines Problems). Des weiteren gibt es diverse Methoden, um einen Algorithmus zu beschreiben. (Pseudocode - Programmablaufpläne - Flussdiagramme) call-by-value Bei CBV werden die an eine Funktion übergebenen Daten KOPIERT. Es besteht dann keine Verbindung mehr zwischen den Daten beim Aufrufer und den Daten in der Funktion. Werden große Objekte auf diese Weise übergeben, so ist das sehr kostspielig in Bezug auf Rechenzeit (der Kopiervorgang) und Speicherplatz (Daten sind mehrmals vorhanden). call-by-reference Anstatt die Daten zu kopieren werden Referenzen auf die Daten übergeben. Man kann sich das als Zeiger vorstellen: Beim Aufrufer zeigt eine Variable auf ein bestimmtes Datum, in der Funktion zeigt eine andere Variable auf dasselbe Datum. Methodenaufrufe an einem so übergebenen Objekt arbeiten also auf demselben Objekt, das auch außerhalb sichtbar ist. Übergabe Parameter der Main-Funktion in C Die Übergabe von Funktionsargumenten ist Ihnen sicherlich bekannt. Sie haben die Möglichkeit, z.B. Werte, Strings, Zeiger und jegliche Datentypen bei einem Funktionsaufruf zu übergeben. Selbstverständlich können Sie auch an die Funktion main() Parameter übergeben. Dies erfolgt beim Aufruf der Funktion, also beim Programmaufruf. Die bisherige Definition der Funktion: int main(void) wird ersetzt durch die vollständige Definition: int main(int argc, char *argv[]) Das Argument int argc repräsentiert dabei die Anzahl der übergebenen Parameter, die char *argv[] beinhaltet. Bei char *argv[] handelt es sich um einen Vektor mit Zeigern auf Strings, also auf die übergebenen Parameter. 6/89 argv[0] beinhaltet immer den String des Programmaufrufes wie z.B. C:\Programme\main.exe, argc hätte somit den Wert 1, da kein zusätzlicher Parameter übergeben wurde. Lautet der Programmaufruf stattdessen: C:\Programme\main.exe -debug -1000, dann hätte argc den Wert 3, und der Inhalt von char *argv[] sähe wie folgt aus: argv[0] enthält: C:\Programme\main.exe argv[1] enthält: -debug argv[2] enthält: -1000 Objektorientierte Programmierung Ziel ist nicht, die Funktionsweise von Rechnern, sondern eine Abbildung der “realen” Welt abzubilden. Im besten Fall soll ein reales “Ding” in der Programmiersprache beschrieben werden. Klassen, welche den Bauplan für diverse reale Gegenstände liefern, beinhalten desweiteren ● einen Zustand, beschrieben durch die Werte ihrer Attribute ● ein Verhalten, beschrieben durch die Methoden ● eine Identität, Name des Objekte ● Methoden können wie Variablen gekapselt werden (heißt: public, private, protected, … sein) Dynamische Bindung: Bei der Vererbung haben wir eine Form der Ist-eine-Art-von-Beziehung, sodass die Unterklassen immer auch vom Typ der Oberklassen sind. Die sichtbaren Methoden, die die Oberklassen besitzen, existieren somit auch in den Unterklassen. Der Vorteil bei der Spezialisierung ist, dass die Oberklasse eine einfache Implementierung vorgeben und eine Unterklasse diese überschreiben kann. Bietet eine Oberklasse eine sichtbare Methode an, so wissen wir immer, dass alle Unterklassen diese Methode haben werden, egal, ob sie die Methode überschreiben oder nicht. Kontrollstrukturen in C und Java Sind Verzweigungen und Schleifen, welche in C und Java von der Syntax her identisch sind. Schleifen ● ● ● for - Schleife ○ for(Definierung ; Bedingung ; Ereignis) ○ for( int i = 0; i < 10 ; i++) while - Schleife ○ while(Bedingung) ○ while (i < 10 ) do - while - Schleife ○ do { }while(Bedingung) ○ gleich wie While, wird aber mindestens einmal ausgeführt. Bedingungen ● ● if - else ○ if(Bedingung) {} else{} if … else switch/case 7/89 ○ switch (Ausdruck){ case Vergleich1: Anweisung1; break; case Vergleich2: Anweisung2; break; default: Anweisung2; break; }; Break und Continue lässt in C und Java Kontrollstrukturen beeinflussen. Mit break kann man Schleifen verlassen, mit continue überspringt man den restlichen schleifen Körper bleibt aber n der Schleife. In C gibt es noch goto, um im Programm hin und her zu springen. Dies sollte aber nicht verwendet werden, da es unübersichtlichtlich wird. (helf): Nicht Teil der Sprache: Ein-/Ausgabe, Parallelisierung, mathem. Funktionen, (michi): Nicht Teil der Sprache: Alles wofür man ein #include braucht 8/89 Datentypen in C und Java Welche Arten von Datentypen (Werte, Byteanzahl, Zeichenkette, zusammengesetzte Datentypen, Verweise auf Variablen) unterstützen die beiden Sprachen? Was ist ein Datentyp Definierte Variablen welche einen Bestimmten Wert speichern. Es wird zwischen vordefinierten Datentypen und selbst erstellten unterschieden. Ein Datentyp besteht im Wesentlichen aus zwei Teilen. Dem Typ, dementsprechend wird der Speicher zugewiesen und der Name, mit dem man den Datentyp ansprechen kann. (helf): Datentyp: Name, Speicherbedarf, Wertebereich, interne darstellung Berechnung der Größen 1 Byte = 8 Bit, 2^8 = 256 z.B. short (2 Byte) … 2^16 short (von – bist + ) positiver Bereich 2^16 / 2 (0 - ((2^16)/2-1)) unsigned short(nur plus Bereich) 2^16 ~65.000 Hinweis: Wenn man unsigned vor einen Datentyp setzt, bleibt der Speicherplatz gleich, nur der Wertebereich wird verändert. Datentypen in C Elementare Datentypen ● ● ● ● ● ● char (1 Byte) short (2 Byte) int (2/4 Byte) long (4 Byte) long long (8 Byte) (helf): eigentl. nur hinsichtlich Länge zueinander festgelegt: char <= short <= ... Gleitpunkttypen ● ● ● float (4 Byte) double (8 Byte) long double (10 Byte) Strukturen Sammlung von Verschiedenen Datentypen => ergibt einen neuen Datentyp. Schlüsselwort: struct 9/89 Anlegen des neuen Datentyps Struct StructName {typ1 komp1; ...}; typedef … variablendeklaration Zugreifen auf eine Komponente variable.komponentenName ZeigerAufStruktur->KomponentenName (*ZeigerAufStruktur).KomponentenName Pointer Eine Variable, in der die Speicheradresse einer anderen Variable gespeichert ist. (helf): Felder/Zeichenketten (helf): Wert-/Referenzsemantik Datentypen in Java Elementare (Primitive) Datentypen ● ● ● ● ● ● ● ● ● boolean (1 Byte) char (2 Byte) short (2 Byte) int (4 Byte) long (8 Byte) float (4 Byte) double (8 Byte) mit/ohne Vorzeichen? (helf): String Klassen in Java Klassen sind das wichtigste Merkmal objektorientierter Programmiersprachen. Eine Klasse definiert einen neuen Typ, beschreibt die Eigenschaften der Objekte und gibt somit den Bauplan an. Jedes Objekt ist ein Exemplar (auch ‚Instanz‘) einer Klasse. Was enthält eine Klasse Attribute: Bestehend aus den elementaren Datentypen Konstruktor: Funktion, welche bei der Initialisierung aufgerufen wird. Methode, welche den Namen der Klasse hat und keinen Rückgabewert hat. Wird ein Objekt initialisiert, wird eine Referenz auf das Objekt gelegt (ähnlich wie ein Zeiger in C) Haus meinHaus = new Haus(); meinHaus hat nun eine „Referenz“ auf Haus Haus einAnderesHaus = meinHaus; einAnderesHaus und meinHaus sind eigtl. gleich, es wird nicht komplett Kopiert sondern nur die Referenz auf das Haus Null Referenz: Jede Referenz kann auf null gesetzt werden 10/89 Wrapper-Klassen Für alle primitiven Datentypen gibt es in Java auch eine Wrapper Klasse (Adapter). Diese sind (wie alle Klassen) eine Unterklasse von Object, d.h.: Sie können auch wie ein Objekt behandelt werden. primitiver Datentyp boolean char int … Wrapper-Klasse Boolean Character Integer … 11/89 UML (Unified Modeling Language) Durch welche Konzepte/Werkzeuge/... kann UML die Software-Entwicklung unterstützen? Wichtige Diagramme sind exemplarisch zu erklären. Welche Möglichkeiten zur Synchronisation mit dem Quellcode bestehen? Was ist UML? ● ● ● ● ● Notation für die Modellierung Allgemeine Diagramme, Software, Hardware, ... Verschiedene Diagrammtypen Umfangreiche Notation Einsatzgebiete: ○ Spezifizierung ○ Visualisierung ○ Dokumententation ○ Konstruktion eines Entwurfs ○ Konstruieren der Architektur einer Anwendung Diagramme im Überblick Strukturdiagramme Strukturdiagramme stellen den technischen Aufbau dar. Zum Beispiel den Aufbau von Datenbanken oder Klassen. ● Klassendiagramm (class diagram) ● Objektdiagramm (object diagram) ● Kompositionsstrukturdiagramm (composite structure diagram) ● Komponentendiagramm (component diagram) ● Verteilungsdiagramm (deployment diagram) ● Paketdiagramm (package diagram) ● Profildiagramm Klassen Diagramm (class diagram) ● Darstellung von Klassen und deren Beziehungen ● Verwendet in der OOP ● Dargestellt wird: ○ Klassenname ○ Eigenschaften (Attribute) ○ Methoden (Operationen) ○ Beziehungen (Assoziationen) ○ Vererbungen (Generalisierungen) ○ (helf): Realisierung/Implementierung ● Sichtbarkeit: 12/89 ● ○ + public ○ # protected ○ ~ package ○ - private Kardinalität: ○ 1 für genau ein Objekt ○ * für viele, d.h. Null oder mehr Objekte ○ 0..1 für höchstens 1 Objekt ○ m..n für mindestens m aber höchstens n Objekte Beispiel Objektdiagramm (object diagram) ● Darstellung von Objekten und deren Assoziationen ● Möglichkeit nur bestimmte Attribute aufzulisten ● Aufbau ähnlich dem Klassendiagramm ● Änderungen: ○ Anstatt Klassenname: Instanzname+ : [Classifiziername bzw. Typ] ● Darunter steht anschließend: ○ Wertepaar (Variablenname : Wert der Variable) ● Kardinalität: ○ Beziehung zwischen 2 Attributen Beispiel Verhaltensdiagramme Im Verhaltensdiagramm wird beschrieben WER WAS macht. Danach wird erst ein Strukturdiagramm erstellt. ● Anwendungsfalldiagramm (use case diagram) ● Sequenzdiagramm (sequence diagram) 13/89 ● ● ● ● ● Aktivitätsdiagramm (activity diagram) Kommunikationsdiagramm (communication diagram) Interaktionsübersicht (interaction overview diagram) Zeitverlaufsdiagramm (timing diagram) Zustandsdiagramm (state chart diagram) Anwendungsfalldiagramm (use case diagram) ● Stellt Szenarien und Akteure mit ihren Abhängigkeiten und Beziehungen dar ● zeigt eine bestimmte Sicht auf das erwartete Verhalten eines Systems (Verwendung für die Spezifikation) ● Beschreibt was zwischen Akteure und System passiert, nicht wie es im System passiert ● Akteure können von einander erben ● Notation: ○ <include> ... unbedingter Aufruf von Teilfunktionalität (immer) ○ <extends> ... bedingter Aufruf einer Teilfunktionalität (nur wenn notwendig) Beispiel Sequenzdiagramm (sequence diagram) ● Stellt den zeitlichen Ablauf des Austausch von Nachrichten dar ● Stellt einen Weg durch einen Entscheidungsbaum innerhalb eines Systemablaufes dar ● Entscheidungsbaum = aufeinanderfolgende, hierarchische Entscheidungen ● Zeigt welche Objekte zu welcher Zeit erstellt werden und deren Lebensdauer ● Zeigt Methodenaufrufe zwischen den Objekten ● Beispiel, Message-Typen, async/sync/verzögerte Message Programmieren mit Hilfe von UML und Synchronisation von Quellcode ● ● ● ● Grafische Darstellung von Codeteilen Erzeugen von Programmcode von einem UML Diagramm ○ In Java und C++ Erzeugen von UML Diagrammen aus dem Quellcode (Reverse Engineering) Software: ○ von IBM Rational Rose 1 Lizenz für Java entwickler 6050 $ ○ Mit Argo UML oder Star UML ist es möglich Quellcode aus einem Diagramm oder ein Diagramm aus einem Quellcode zu generieren. 14/89 Relationen zwischen Klassen in Java Welche Relationen bestehen zwischen Klassen und welche Attribute können diese haben? Was bedeuten Polymorphie bzw. ORM? (LINK VOM GAMS: http://timpt.de/topic95.html ) Eine Relation zwischen zwei Klassen lässt sich am besten mit einer Assoziation beschreiben. Die Assoziation beschreibt desweiteren, wie oft eine Verbindung besteht. Die Assoziation wird mit den Phrasen ● benutzt eine ● ist zugeordnet zu ● hat eine Beziehung zu beschrieben. In einer Verbindung stehen immer zwei unabhängige Objekte welche durch den Aufruf der Methoden “verbunden” werden. Nachdem das ende der Methode und somit das Ziel der Beziehung erreicht wurde können beide Objekte weiterbestehen. Kardinalität ● ● ● ● 1:1 1:0 1:n n:n eines hat genau ein anderes eines hat genau ein anderes oder auch keines eines hat eines oder mehrere eines oder mehrere können eines oder mehrere haben Beziehungen von Klassen Vererbung Die Verbung von Klassen untereinander wird durch eine durchgezogene Linie mit einem offenen Pfeil, der auf die Oberklasse zeigt, dargestellt. 15/89 Schnittstellenimplementation Die Implementation einer Schnittstelle wird mit einer gestrichelten Linie dargestellt. An der Schnittstelle wird ein geschlossener, nicht ausgefüllter Pfeil angebracht. Assoziation Eine Assoziation von Klassen wird durch eine durchgezogene Linie, die beide Klassen miteinander verbindet, dargestellt. Multiplizität Die Multiplizität eines Objektes gibt an, wieviele Objekte des einen Typs mit Objekten des anderen Typs (oder Objekten anderer Typen) verbunden sein können oder verbunden sein müssen. Folgende Angaben zur Multiplizität können angegeben werden: 16/89 Beschreibung Beispiel Bedeutung Angabe einer definierten Zahl 3 Genau diese Anzahl von Objekten Angabe von Minimum und 3 .. 7 Mindestens 3 Objekte und maximal 7 Maximum Objekte Der Joker * Beliebig viele Objekte Die Angabe von Multiplizitäten könen durch Trennung von Kommata auch kombiniert werden. Beispiele: 1,3,6 Ein, drei oder 6 Objekte 0, 4 .. 9 Entwender kein Objekt oder vier bis neun Objekte 0 .. * Beliebig viele Objekte * Beliebig viele Objekte 4 Genau vier Objekte Das nachfolgende Diagramm zeigt die Assoziation von Auto, Fahrer und Mitfahrer bei einer Autofahrt. Es wird dabei davon ausgegangen, dass der Wagen für 5 Personen zugelassen ist. Aggregationen Desweiteren gibt es noch Aggregationen. Dies sind besondere Assoziationen. Es sind Teile-einerganzen-Beziehung. Unterschieden wird zwischen starker (die Teile sind Existenzabhängig vom Ganzen) und schwacher (jedes Teil kann selbst existieren) Aggregation. Jedes Teil kann nur eine Aggregation haben. Als Aggregation Unternehmen - Abteilung - Mitarbeiter, wenn sich die Abteilung auflöst gehören die Mitarbeiter noch zum Unternehmen. Kompositionen Ein weiterer Sonderfall der Assoziation ist die Komposition. Der Unterschied hier ist, dass alle abhängig von einander sind. Unternehmen und Abteilung, wenn sich das unternehmen auflöst wird es die Abteilung auch nicht mehr geben. 17/89 Als Aggregation Unternehmen - Abteilung - Mitarbeiter, wenn sich das Unternehmen auflöst, kann die Abteilung nicht weiter existieren. Beschreibung mit Bildern 1. Assoziation 1.a. ungerichtete Assoziation Bild Code Code class Buch{ Verlag v; } class Verlag{ ArrayList<Buch> buecher; } Einfache Beziehung der Klassen, die Klassen kennen sich und greifen auf die Methoden voneinander zu. 1.b. gerichtete Assoziation Bild Code class Buch{ Verlag v; } Das Buch hat einen Verlag. (hat es doch bei ungerichteter auch? what kind of sorcery is this?) 2. Schwache Aggregation Bild Code class Buch{ ArrayList<Artikel> artikel; public void addArtikel(Artikel artikel){ … } } Ist eine besondere Form der Assoziation. Bedeutet “Ist Teil von”. Artikel ist Teil eines Buches. Ein Buch kann mehrere Artikel haben. Wird das Buch gelöscht, besteht der Artikel weiter, und umgekehrt. 3. Komposition Bild Code class Buch{ ArrayList<Verzeichnis> verzeichnisse; } “Besteht aus” Verzeichnis ist Teil des Buches. Ein Buch kann mehrere Verzeichnisse haben. Wird das Buch gelöscht, wird auch das Verzeichnis gelöscht und umgekehrt. 18/89 Polymorphismus Bezieht sich auf die Methoden von Java Klassen und bedeutet Vielgestaltigkeit. Wenn Klassen den selben Methodennamen verwenden aber die Implementierung unterschiedlich ist. Beispiel die Klasse Hund und die Klasse Katze erben von Tier die abstrakte Funktion gibLaut(), bei Hund würde man “Wuff” und bei Katze “Miau” programmieren. Je nachdem, ob das Objekt Tier eine Katze oder ein Hund ist, wird eine andere Funktion aufgerufen. Tier a = new Hund(); a.gibLaut(); Tier b = new Katze(); b.gibLaut(); Object Relational Mapping (ORM) (Von Wikipedia: object-relational mapping) Object-relational-mapping sind Techniken, um Assoziationen aus Programmen in Datenbanken zu übertragen. Implementiert wird diese Technik über Bibliotheken. Eine standardisierte Schnittstelle für Java ist z.B. Java Persistence API. Abgebildet werden die Objekte und Relationen in einem relationalen Datenbankmodell. SIEHE VSDB AUSARBEITUNG UNTER DEM PUNKT DATENMODELLE. Weiter unten gibt es zwei Unterpunkte namens Objekt rationales Mapping (ORM) und Impedance mismatch 19/89 Streams in Java Wie unterstützen Streams den Zugriff auf Daten? Welche Arten von Streams sind wofür gut geeignet? Wie können Streams kombiniert werden? ● ● ● ● Java verwirklicht die I/O Verarbeitung mithilfe von Streams Ein Stream ist entweder eine Quelle (Input) oder ein Ziel (Output) Streams können Filezugriffe, Daten von Sockets sowie auch diverse Peripheriegerät sein. Wichtig ist die richtige Fehlerbehandlung: Immer im finally Block den Stream wieder schließen Beispiel BufferedReader in = null; try { in = new BufferedReader(new FileReader("textfile.txt")); String line = null; while ((line = in.readLine()) != null) { System.out.println(line); ... } } catch (IOException e) { System.err.println(e.getMessage()); } finally { try { in.close(); } catch (IOException e) { } } File mit ObjectOutputStream ● ● ● Objekt muss serializeable sein Schreiben kein Problem Beim Lesen muss der Typ des Objekts bekannt sein ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("object.data")); MyClass object = new MyClass(); output.writeObject(object); output.close(); Wie unterstützen Streams den Zugriff auf Dateien? Bytes, primitive Datentypen, Unicode Character oder serialisierte Objekte. InputStream : Lesen von einer Datei (binär) 20/89 OutputStream : Schreiben in eine Datei (binär) Reader Writer Lesen Schreiben - : : Unicode Unicode Gegenüberstellung der verschiedenen Klassen Char Streams Reader Writer FileReader FileWriter InputStreamReader BufferedReader BufferedWriter CharArrayReader CharArrayWriter Byte Streams InputStream OutputStream FileInputStream FileOutputStream OutputStreamWriter BufferedInputStream BufferedOutputStream ByteArrayInputStream ByteArrayOutputStream Welche Arten von Streams sind wofür gut? Standard Streams : System.in System.out System.err - Konsole InputStreamReader(System.in) System.out.println Byte Streams : liest je ein Byte -> Objektdaten(Sockets,Grafiken,Objects,Videos,....) Char Streams : liest je ein Zeichen -> Textdaten Object Streams: Schreibt ein ganzes Objekt. Um schreiben/lesen zu könen, muss das Objekt serialized sein. Beim Lesen (InputStream) muss man wissen, um welchen Objekttyp es sich handelt. Wie können Streams kombiniert werden? Da alle Streams von nur 4 abstrakten Basisklassen abgeleitete sind, bieten einige Streams die Möglichkeit, sich mit anderen Streams verketten zu lassen => Erweiterung der Funktionen. Byte für byte lesen einer Datei -> Langsam Großen Block an Daten lesen und diesen nachher byte für byte verarbeiten -> buffering z.B. InputStream in BufferedInputStream BufferedInputStream input = new BufferedInputStream( new FileInputStream("input-file.txt")); Inputstream auf Reader 21/89 InputStream inputStream = new FileInputStream("c:\\data\\input.txt"); Reader reader = new InputStreamReader(inputStream); int data = reader.read(); while(data != -1){ char theChar = (char) data; data = reader.read(); } reader.close(); 22/89 Fehlerbehandlung in C und Exceptions in Java Fehlerbehandlung wird in C und Java unterschiedlich unterstützt. Wie läuft Fehlerbehandlung ab und wie sieht gute Fehlerbehandlung aus? Fehlerbehandlung in C Da C (standardmäßig) keine Exceptions oder ähnliche Mechanismen unterstützt, muss jeder mögliche Fehlerfall manuell überprüft werden. Dazu gibt es mehrere Möglichkeiten. Rückgabewert überprüfen Die meisten Funktionen in C geben einen speziellen Rückgabewert zurück, wenn ein Fehler aufgetreten ist. Zeiger überprüfen Nach jeder Aktion die einen Zeiger zurückgibt, muss überprüft werden, ob dieser Zeiger nicht NULL (0) ist. Die meisten Funktionen (fopen, malloc, …) geben 0 zurück wenn die Aktion fehlgeschlagen ist. Nachdem eine Datei geöffnet wurde, muss überprüft werden, ob die Aktion auch erfolgreich war. Die Funktion fopen(...) gibt NULL (0) zurück, wenn die Datei nicht geöffnet werden konnte. Wenn das der Fall ist, sollte eine Fehlermeldung ausgegeben und das Programm beendet werden. Die Fehlermeldung wird in den Standard-Error-Stream stderr geschrieben. int main(void) { FILE *fp = fopen("keinedatei.dat", "r"); if (fp == NULL) { fputs("Datei konnte nicht geöffnet werden!", stderr); return 1; } return 0; } Das Makro assert Das Makro assert(...) ist in der Header Datei assert.h definiert. Es wird verwendet, um zu überprüfen, ob eine Bedingung Wahr ist. Wenn dem Makro assert ein wahrer Wert (TRUE) übergeben wird, läuft das Programm normal weiter. Wenn der Wert hingegen falsch (FALSE) ist, wird das Programm mit einer Fehlermeldung abgebrochen. Diese Fehlermeldung enthält den Dateinamen und die Zeilennummer, in der das assert-Makro steht, wodurch die Fehlersuche erleichtert wird. int main(void) { FILE *fp = fopen("keinedatei.dat", "r"); assert(fp != NULL) return 0; } 23/89 Fehlercodes mit errno Eine weitere Möglichkeit, Fehler in C zu behandeln, sind Error Codes. In der Header Datei errno.h ist eine globale Variable errno definiert. Viele Funktionen aus der Standardbibliothek verwenden diese Variable, um über aufgetretene Fehler zu informieren. Außerdem sind in der Header Datei Makros für die einzelnen Fehlerfälle definiert. Je nachdem, welcher Fehler auftritt, wird der Variable errno ein entsprechender Wert zugewiesen. Beispiele für Error Codes: EDOM (Domain error) Unzulässiges Argument für eine mathematische Funktion. EILSEQ (Illegal sequence) Es wurden Bytes entdeckt, die keine gültigen Zeichen darstellen. ERANGE (Range error) Das Ergebnis liegt außerhalb des darstellbaren Bereichs. 0 (NULL) Kein Fehler aufgetreten. Diese vier Werte sind standardisiert, allerdings definieren die meisten Compiler noch viele weiter Werte. Die Funktion perror(...) gibt die übergebene Fehlermeldung und eine zu errno gehörende Fehlermeldung auf stderr aus. #include <errno.h> #include <math.h> #include <stdio.h> int main(void) { sqrt(-1); if(errno != 0) perror("Fehler bei sqrt"); return 0; } Ausgabe: Fehler bei sqrt: Domain error Exceptions in Java In Java werden (die meisten) Fehler als Exceptions (“Ausnahmen”) dargestellt. Exceptions können in einem try-catch Block abgefangen und verarbeitet werden. Wobei Programmteile, die eine Exception werfen können, in einem try-Block eingeschlossen werden, und die Behandlung dieser Exceptions im nachfolgenden catch-Block geschieht. Wenn im try ein Fehler auftritt, wird die Programmausführung sofort unterbrochen und der passende catch-Block ausgeführt. Danach läuft das Programm aber normal weiter. 24/89 Wenn im try mehrere verschiedene Exceptions auftreten können, sollten auch mehrere catch-Blöcke für die verschiedenen Exceptions vorhanden sein. Nachdem ein Fehler aufgetreten ist, wird nach dem ersten passenden catch-Block gesucht, darum sollten auch die speziellen vor den generellen Fällen stehen. In Java zwingt einen der Kompiler dazu, bestimmte Funktionen mit einem try-catch zu versehen. Allerdings ist es nicht sinnvoll, den catch-Block leer zu lassen, da dann die Fehlermeldung einfach unterdrückt wird. Vorteile ● Keine Fehlercodes, die manuell verwaltet werden müssen ● Genaue Lokalisierung des Fehlers mit Hilfe des Stacktrace ● Trennung von Programmlogik und Fehlerbehandlung ● Threadsicher Beispiel String string = "19%"; try{ Integer.parseInt( string ); } catch ( NumberFormatException e ){ //Spezielle Exception System.err.println( "Konvertierung fehlgeschlagen!" ); } catch ( Exception e ){ //Generelle Exception e.printStackTrace(); } Der Stacktrace Im Stacktrace wird der Name der Exception, die Klasse, die Funktion sowie die Zeilennummer angezeigt, in der der Fehler aufgetreten ist. Exceptions fangen Exceptions sollten immer dort gefangen werden, wo sie am besten behoben werde können. Aber sie sollten spätestens dann gefangen werden, bevor sie den Einflussbereich des Programmierers verlassen. Exceptions sollten spätestens gefangen werden in der ● main() Methode., ● run() Methode von Threads ● event() Methoden von GUIs ● doGet() oder doPost() Methode von Servlets Exceptions weiterreichen Wenn eine Exception nicht sofort behandelt werden soll, kann sie an den Aufrufer weitergegeben werden. Dazu wird die Funktion mit einem throws versehen. Dadurch muss sich der Aufrufer um die Behandlung der Exception kümmern. int convertString(String string) throws NumberFormatException { return Integer.parseInt( string ); } 25/89 Abschlussbehandlung mit finally Der finally-Bock kommt nach dem catch-Block und wird in jedem Fall ausgeführt. Er wird ausgeführt, egal ob eine Exception aufgetreten ist oder nicht und sogar wenn im try oder catch-Block ein return steht. Der finally-Block sollte dazu verwendet werden, um z.B. alle geöffneten Ressourcen wieder zu schließen oder Änderungen aus dem try-Block rückgängig zu machen. Im nachfolgenden Beispiel wird im finally-Block überprüft, ob die Datei geöffnet ist. Wenn das der fall ist, wird versucht sie zu schließen. Allerdings muss die Methode close() auch wieder von einem try-catch umrahmt werden. RandomAccessFile f = null; try{ f = new RandomAccessFile( "randomFile.dat", "r" ); f.close(); } catch ( FileNotFoundException e ){ System.err.println( "Datei ist nicht vorhanden!" ); } catch ( IOException e ){ System.err.println( "Allgemeiner Ein-/Ausgabefehler!" ); } finally{ if ( f != null ) try { f.close(); } catch ( IOException e ) { } } Exception und RuntimeException Die Klasse RuntimeException ist eine Unterklasse von Exception mit einer Besonderheit: sie muss nicht abgefangen werden. RuntimeExceptions werden genutzt, um Programmierfehler, wie z.B. IndexOutOfBound, anzuzeigen. Diese Exceptions können zwar mit einem catch-Block abgefangen werden, sollten es aber nicht, da sie einen Programmierfehler anzeigen. RuntimeExceptions werden auch als ungeprüfte Ausnahmen und Exceptions als geprüfte Ausnahmen bezeichnet. Ungeprüfte ausnahmen sollen für Fehler verwendet werden, die nicht mehr behoben werden können (z.B. Programmierfehler, …). Exception Objekte Alle Exceptions sind vom Interface Throwable abgeleitet und alle speziellen Exceptions (z.B. NumberFormatException) sind wiederum von Exception abgeleitet. Dadurch lassen sich mehrere Exceptions mit ihrer Oberklasse abfangen Methoden von Exceptions Einige wichtige Methoden von Exceptions sind: e.getClass().getName(); //Name der Exception (java.lang.NumberFormatException) 26/89 e.getMessage(); //genauere Beschreibung (For input string: "19%") e.toString(); //Name und Beschreibungjava.lang.NumberFormatException: For input Exception werfen Um eine Exception auszulösen, muss zuerst ein Exception Objekt erzeugt und dieses dann mit throw geworfen werden. Player( int age ){ if ( age <= 0 ) throw new IllegalArgumentException("Alter <= 0!" ); this.age = age; } Achtung: In diesem Fall muss kein throws IllegalArgumentException angegeben werde, da RuntimeExceptions nicht extra angegeben werden müssen. Eigene Exceptions Um eine eigene Exception-Klasse zu bilden, muss diese von Exception (oder einer Unterklasse davon) abgeleitet sein. Außerdem sollte es einen Konstruktor geben, der einen String als Argument annimmt und ihn an die Oberklasse weiterleitet. public class MyException extends Exception { public MyException () { } public MyException ( String s ) super( s ); } { } Aufruf Diese Exception kann dann mit throw geworfen werden. if ( age <= 0 ) throw new MyException( "Kein Alter <= 0 erlaubt!" ); 27/89 Nebenläufige Programmierung in Java – Threads Nebenläufigkeit bekommt durch Vielkernprozessoren an Bedeutung. Was bietet Java für eine multithreaded Anwendung? Welche Probleme ergeben sich und wie kann diesen begegnet werden? Nebenläufigkeit allgemein Konkurrierender Zugriff auf eine begrenzte Ressource. Die Prozesse in einem Betriebssystem konkurrieren um den Prozessor des Computers. Das Betriebssystem gibt jedem Prozess für eine bestimmte Zeit Zugriff auf die Ressource. → Keine tatsächliche Nebenläufigkeit. Jedes Programm an sich ist bereits ein Thread, hat jedoch die Möglichkeit weitere Threads zu starten. In Java werden diese Threads dann der JVM zugeteilt. Prozess vs Thread Prozesse laufen innerhalb eines Betriebssystems und werden von diesem verwaltet - Threads sind Bestandteil eines solchen Prozesses. Da jeder Prozess seinen eigenen Speicherbereich zur Verfügung hat, kann kein Prozess die Daten eines anderen lesen, sie kooperieren nicht und müssen voreinander geschützt werden. Threads innerhalb eines Prozesses haben den selben Heap-Speicher zur Verfügung (Speicher in dem die Java Objekte liegen) über welchen sie untereinander kommunizieren können. Scheduling Der Scheduler hat die Aufgabe den Threads Prozessoren für eine gewisse Zeit zuzuweisen. Der Scheduler ist in Java Bestandteil der JVM. Das Betriebssystem hat für seine Prozesse ebenfalls einen Scheduler. 28/89 Java Komponenten für Nebenläufige Programmierung Java bietet zur Erstellung von Threads im wesentlichen 2 Komponenten an: ● Klasse Thread ○ Vorteil: Code in run() kann andere Thread Methoden nutzen ○ Nachteil: Klasse kann nicht mehr von anderen erben ● Interface Runnable ○ Vorteil: Die Klasse kann noch erben ○ Nachteil: Kein direkter Zugriff auf Thread Methoden Beispielcode Runnable Thread class workThread implements Runnable{ public void run(){ //Do things } } class workThread extends Thread{ public void run(){ //Do things } } //Start Thread t = new Thread(new workThread()); t.start(); //Start Thread t = new workThread(); t.start(); //Stoppen t.interrupt(); //Stoppen t.interrupt(); Warum nicht stoppen mit stop()? Es ist bei Threads, die eine endlosschleife abarbeiten sinnvoll im Kopf der Schleife das interne Flag mittels isInterruped() abzufragen, um bei einer Unterbrechung von außen noch alle notwendigen Aktionen (Ressourcen wieder freigeben, Persistieren, …) ausführen zu können. Die veraltete (deprecated) Methode stop() beendet den Thread abrupt, egal in welcher Phase seiner Ausführung er sich befindet. Probleme und Lösungen bei Nebenläufiger Programmierung Synchronisation Damit 2 Threads das selbe Objekt bearbeiten können, ist es notwendig, jeweils nur einem Thread die Bearbeitung zu erlauben und den anderen auszuschließen. Wird dies nicht berücksichtigt, können unvorhersehbare Ergebnisse auftreten. Bei reinen Lesezugriffen ist es nicht notwendig, 2 Threads zu synchronisieren, da keine Daten Inkonsistenzen entstehen können. Race Condition = Wettbewerbssituation 29/89 Mittel zur Vermeidung von Datenverlusten: ● “synchronized” Mutex (Mutual Exclusion) = Wechselseitiger Ausschluss ● wait(), notify(), notifyAll() (helf): unsicher ● Semaphore ● (helf): synchronisierte Klassen wie ArrayBlockingQueue aus java.util.concurrent Mutual Exclusion Kritischer Abschnitt: Abfolge von Befehlen, die ein Thread ausführen muss bevor ein anderer Thread die Befehle ausführen darf, auch wenn der Prozessor an einen anderen Thread abgegeben wird. ● Es darf sich maximal 1 Thread in diesem Abschnitt befinden ● Die Threads schließen sich gegenseitig aus Java: “synchronized”-Schlüsselwort (Monitorkonzept) Beim Eintritt in einen synchronized Block oder eine synchronized Methode wird eine Sperre auf das Objekt (= Monitor) gesetzt, damit jeder andere Thread ausgesperrt wird. Jedes Objekt kann ein Monitor sein. Ausgesperrte Threads werden von Scheduler nicht mehr berücksichtigt. Beim Austreten wird diese Sperre wieder aufgehoben. Bedingte kritische Abschnitte: Ein Thread, der (z.B. wegen vollem Puffer) nichts machen kann, hat die Möglichkeit sich selbst in eine Warteliste (eigentlich Wartemenge, weil Reihenfolge nicht beachtet wird) zu setzen mittels wait() (Wait akzeptiert auch die maximale Wartezeit in Millisekunden als Parameter) 30/89 Ein Thread, der wieder Ressourcen bereitstellt (z.B. Element aus Puffer nehmen) kann die wartenden Threads mittels notify() / notifyAll() darauf hinweisen, dass wieder Arbeit vorhanden ist. Diese Methoden dürfen nur innerhalb eines synchronized Blocks / Methode aufgerufen werden. Beispiel class Puffer{ private int[] feld = new int[10]; private int anzahl = 0; public synchronized void erzeuge(int w ){ while (anzahl == 9) wait(); feld[anzahl] = w; anzahl++; if (anzahl==9) notifyAll(); } public synchronized int verbrauche(){ while(anzahl == 0) wait(); ergebnis = feld[anzahl]; anzahl--; // Feld umkopieren if (anzahl==0) notifyAll(); return ergebnis; } (helf): wait/notify sind Semephoren unterlegen (Deadlocks, …) Semaphore Der vorhin bei wait(), notify(), notifyAll() beschriebene Puffer kann auch durch Semaphoren realisiert werden. Ein Semaphore stellt sicher, dass sich nur eine definierte Anzahl von Prozessen in einem Programmabschnitt befinden. Erstellen eines neuen Semaphore mit 2 zu vergebenden Plätzen. Semaphore s = new Semaphore(2); Der aktuelle Thread verlangt einen freien Platz, ist keiner frei, wird an dieser Stelle gewartet. s.aquire(); Der aktuelle Thread gibt seinen beanspruchten Platz frei (Sollte immer im finally-block stehen). s.release(); Mittels Semaphoren kann auch ein Mutex wie bei Synchronized nachgebildet werden, dazu wird einfach nur 1 freier Platz vergeben. 31/89 Ein Semaphore beachtet ohne den Parameter “true” im Konstruktor nicht wie lange ein Thread schon wartet, sondern vergibt die Plätze willkürlich. (Unfaire Semaphore) new Semaphore(1, true) erzeugt eine faire Semaphore. Thread Befehle sleep(long ms) sleep(long ms, int ns) Thread schläft für mindestens ms Millisekunden (und ns Nanosekunden) Wirft “InterruptedException” wenn das Schlafen von einem anderen Thread unterbrochen wird. yield() Thread setzt sich in die Wartemenge zurück (“Ich setze eine Runde aus”) interrupt() Setzt ein Flag im Thread Objekt welches mit “isInterruped” abgefragt werden kann. sleep(), wait(), und join() werfen eine InterruptedException, wenn sie durch die interrupt()-Methode unterbrochen werden join() join(long ms) Wenn mehrere Threads Teilaufgaben bearbeiten, kommt irgendwann die Zeit, wenn die Teilergebnisse zum Gesamten zusammengesetzt werden müssen. t.start() //Der Thread t wird gestartet t.join() //An dieser Stelle wird (maximal ms Millisekunden) gewartet, bis der Thread fertig ist Deadlock Ein Deadlock kommt zustande, wenn Thread A eine Ressource belegt, die Thread B Braucht und umgekehrt B eine Ressource belegt, die A haben möchte. Thread Pooling Einen Threadpool erzeugen: ExecutorService threadpool = Executors.newCachedThreadPool(); Thread im Pool ausführen lassen: threadpool.execute(t); 32/89 Dateiarten - Lesen und Schreiben von Dateien in C und in Java Das sequentielle bzw. wahlfreie Lesen und Schreiben von Dateiinhalten in C und Java ist zu erklären. Sequentielles vs. wahlfreies Lesen und Schreiben Bei sequentiellem Lesen und Schreiben wird eine Datei vom Anfang durchlaufen und byte für byte sequentiell, der Reihe nach, gelesen bzw. geschrieben. Sowohl in C als auch in Java läuft das Lesen und schreiben normalerweise so ab. Beide Sprachen bieten allerdings die Möglichkeit, sich frei in einer Datei zu positionieren, wobei in C dafür die Datei im update-Modus geöffnet sein muss. Dateizugriff in C Funktionen zum erstellen, öffnen und bearbeiten von Dateien stehen in C in der Standard Ein- und Ausgabe Bibliothek zur Verfügung. Um diese in seinem Programm zu verwenden muss das stdio.hHeader-File inkludiert werden. Umbenennen und Löschen von Dateien kann ohne öffnen der Datei mit den Funktionen int remove(const char* filename) und int rename(const char* oldname, const char* newname) zur Verfügung. Im Fehlerfall wird ein Wert ungleich 0 zurückgegeben. Zum Bearbeiten von Dateien muss eine Datei zunächst geöffnet werden. Dazu steht die Funktion FILE* fopen(const char* filename, const char* mode) zur Verfügung. Der Rückgabewert ist im Erfolgsfall ein Stream (FILE*) zum angegeben Dateinamen, ansonsten NULL. Modi "r" "w" "a" "r+" "w+" "a+" b read: Datei zum Lesen öffnen. Die Datei muss existieren. write: Erstellt eine neue leere Datei zum Schreiben. Existiert die Datei bereits, so wird der Inhalt der Datei gelöscht. append: Datei zum Schreiben am Ende der Datei öffnen. Existiert die Datei nicht, wird sie erstellt. read/update: Eine Datei zum Lesen und updaten öffnen. Die Datei muss existieren. write/update: Datei zum Schreiben und Updaten (Sowohl Lesen als Schreiben ist erlaubt). append/update: Datei zum Schreiben am Ende der Datei und Lesen öffnen. Wird die Position in der Datei verschoben wird sie mit der nächsten Ausgabeoperation wieder ans Ende der Datei verschoben. binary: Kann nur in Verbindung mit einem der anderen Modi angegeben werden. Öffnet eine Datei im binären Modus. Es wird keine Transformation von Zeilenenden auf Windows durchgeführt. \n -> \r\n Ein Dateistream wird mit int fclose(FILE* filepointer) wieder geschlossen. Im Erfolgsfall wird 0 zurückgegeben. 33/89 Beispiel Datei öffnen und schließen: int doSomething() { FILE * fp; fp = fopen("testdatei.txt", "w"); //oeffnen der Datei im Schreibmodus if (fp == NULL) { printf("Fehler beim Oeffnen der Datei!"); return 1; } /* * Schreiben in die Datei * */ fclose(fp); //schließen der Datei } Textdatei einlesen Text aus einer Textdatei wird am besten zeilenweise über die Funktion char * fgets(char * str, int num, FILE * stream) eingelesen, wobei maximal num, normalerweise bis zum Zeilenende inklusive Stringendezeichen, Zeichen aus dem Datei-Stream eingelesen und in den String str geschrieben. Der String wird auch noch zurückgegeben. Beispiel ... char str[BUFSIZ]; while(!feof(fp)) // solange das Dateiende nicht erreich wurde { fgets(str, BUFSIZ, fp); printf("Eingelesene Zeile: %s\n", str); } … Textdatei schreiben Zum zeilenweise Schreiben steht analog zu fgets() die Funktion int fputs(const char * str, FILE * stream) zur Verfügung. Es wird der String str in den Stream stream hinausgeschrieben und im Erfolgsfall 0 zurückgeliefert. Beispiel ... char str[] = "Hallo Welt"; int i; for(i = 0; i < 100; i++) //100 Zeilen schreiben { fputs(str, fp); } 34/89 ... (helf): scanf und printf zum formatierten Einlesen bzw. Ausgeben von Variablen/... Binärdatei lesen und schreiben Zum lesen und schreiben von Binärdateien werden in C normalerweise die Funktionen size_t fread(void * ptr, size_t size, size_t count, FILE * stream) und size_t fwrite(const void * ptr, size_t size, size_t count) verwendet. Es wird die Zahl der tatsächlich gelesen bzw. geschriebenen Blöcke zurückgeliefert. Bei fread werden aus dem Stream size*count Bytes gelesen und in den Buffer ptr geschrieben. Bei fwrite werden size*count Bytes des Buffers ptr in den Stream geschrieben. Beispiel: //Lesen: //Dateigröße bestimmen ... fseek (pFile , 0 , SEEK_END); //zum Dateiende springen lSize = ftell (pFile); //aktuelle Bytepositione auslesen rewind (pFile); //und wieder zurück zum Anfang // Speicher für die Datei reservieren buffer = (char*) malloc (sizeof(char)*lSize); // Buffer in die Datei schreiben fread (buffer, 1, lSize, fp); ... //Schreiben ... char buffer[] = "Hallo Welt"; fwrite (buffer , sizeof(char), strlen(buffer), fp); ... Wahlfreier Dateizugriff In C steht für den Positionswechsel die Funktion int fseek ( FILE * stream, long int offset, int origin ); zur Verfügung, wobei die Position um offset Bytes von origin aus verschoben wird. Danach kann über fread/fwrite normal gelesen werden. (helf): fseek gefolgt von fread/fwrite 35/89 Dateizugriff in Java Zum Bearbeiten der Metadaten einer Datei, wie Dateiname, Pfad, Änderungsdatum, Zugriffsrechte, … steht in Java die Klasse java.io.File zur Verfügung. Mit dieser Klasse ist es auch möglich eine Datei zu erstellen. Außerdem ist es mit Hilfe dieser Klasse möglich durch Verzeichnisstrukturen zu laufen, da es sowohl möglich ist alle Dateien und Verzeichnisses in einem Verzeichnis aufzulisten über die Methode Files[] listFiles(), als auch auf übergeordnete Verzeichnisse mit der Methode getParentFile() zuzugreifen. Um festzustellen ob ein File eine Datei oder Verzeichnis ist stehen die Methoden isFile() bzw. isDirectory() zur Verfügung. Beispiel File f = new File("info.txt"); f.createNewFile(); //erstellt die Datei, falls sie nicht existiert f.renameTo(new File(“info2.txt”); // umbenennen einer Datei f.setLastModified(System.getCurrentMillis()); //Änderungsdatum f.delete(); //löschen der Datei Um Inhalte einer Datei in Java zu verändern, muss mit Streams gearbeitet werden. Für Textdateien stehen die Klassen FileReader und FileWriter zur Verfügung. Will man zeilenweise einlesen oder hinausschreiben, so kann mit BufferedReader bzw. BufferedWriter gearbeitet werden. Beispiel //Lesen: try (BufferedReader br = new BufferedReader(new FileReader("C:\\testing.txt"))) //try-with-resorce konstrukt { String sCurrentLine; //lesen solange Zeilen verfügbar sind while ((sCurrentLine = br.readLine()) != null) { System.out.println(sCurrentLine); } } catch (IOException e) { e.printStackTrace(); } //Schreiben: try { String content = "This is the content to write into file"; File file = new File("/users/mkyong/filename.txt"); file.createNewFile(); //Datei erzeugen falls sie nicht existiert FileWriter fw = new FileWriter(file.getAbsoluteFile()); BufferedWriter bw = new BufferedWriter(fw); bw.write(content); bw.close(); } catch (IOException e) { e.printStackTrace(); } 36/89 (helf): Pfadtrennzeichen aus File.separator[Char] nehmen Für das Schreiben und Lesen von Binärdateien sind die Klassen FileInputStream und FileOutputStream vorhanden. Mit ihnen können Dateien byteweise eingelesen bzw. geschrieben werden. Beispiel //Lesen File file = new File("C:/robots.txt"); try (FileInputStream fis = new FileInputStream(file)) { //try-with-resource int content; while ((content = fis.read()) != -1) { // convert to char and display it System.out.print((char) content); } } catch (IOException e) { e.printStackTrace(); } //Schreiben File file = new File("c:/newfile.txt"); String content = "This is the text content"; try (FileOutputStream fop = new FileOutputStream(file)) { // if file doesn't exists, then create it file.createNewFile(); // get the content in bytes byte[] contentInBytes = content.getBytes(); fop.write(contentInBytes); fop.flush(); fop.close(); } catch (IOException e) { e.printStackTrace(); } Wahlfreier Dateizugriff In Java muss ich für wahlfreien Dateizugriff auf die Klasse RandomAccessFile zurückgreifen, die ähnlich wie der File-Pointer in C arbeitet und die Methode seek(int pos) zum freien Positionieren in der Datei bietet. Konstruktoren: RandomAccessFile(File file, String mode) und RandomAccessFile(String filename, String mode). Über die Methode read() können einzelne Bytes einfach eingelesen werden. Mit close() wird die Datei geschlossen. 37/89 Dynamische Datenstrukturen Neben Feldern und Strukturen spielen verkettende Strukturen eine wichtige Rolle in der Verwaltung von Datenbeständen. Die wesentlichen Vertreter solcher dynamischen Datenstrukturen sind zu erklären. Dynamische Datenstrukturen bezeichnen in der Informatik und Programmierung Datenstrukturen, die eine flexible Menge an Arbeitsspeicher reservieren. Anders als statische Variablen, bei denen die fixe Größe bekannt ist und die während der Ausführung nicht verändert werden können, können dynamische Datenstrukturen während der Laufzeit erzeugt und in ihrer Größe verändert werden. Da man in der Programmierung häufig in Situationen kommt, bei denen im Vorhinein nicht klar ist, wie viel Speicher benötigt wird, sind dynamische Datenstrukturen für viele Programmieraufgaben unumgänglich. Die Dynamik liegt im Einfügen neuer Listenelemente - auch Knoten (Node) genannt dem Entfernen vorhandener Elemente und in der Änderung der Nachbarschaftsbeziehungen (Verschieben eines Knoten). Die Beziehung zwischen den einzelnen Knoten wird über Zeiger hergestellt, die jeweils auf den Speicherort des logischen Nachbarn zeigen. Jeder Knoten enthält daher neben den zu speichernden Nutzdaten mindestens einen Zeiger auf die jeweiligen Nachbarknoten. Man nennt solche Strukturen daher auch verkettete oder rekursive Datenstrukturen. Der Vorteil gegenüber anderen Datenstrukturen, beispielsweise Arrays, liegt also darin, dass das Einfügen und Löschen eines Knoten an jedem Punkt in der Liste möglich ist. Dies liegt daran, dass nur Verweise (Zeiger) neu gesetzt werden und nicht wie bei Arrays alle nachfolgenden Elemente verschoben werden müssen. Die Liste verbraucht jedoch mehr Speicher, da die Verweise auf andere Listenelemente gespeichert werden müssen. Zusammengefasst: Dynamische Datenstrukturen stellen eine flexible und Größenunabhängige Art der Datenspeicherung dar, der Speicher wird während der Laufzeit angefordert und kann auch während der Laufzeit wieder freigegeben werden. Je nach Art der Datenstruktur werden die Daten innerhalb der Struktur unterschiedlich angeordnet und miteinander verknüpft. Die Beziehung zwischen den Elementen wird über Zeiger hergestellt, was Änderungen innerhalb der Struktur vereinfacht, da lediglich Verweise verändert werden müssen und die Daten selbst unberührt bleiben. Die wichtigsten Datenstrukturen: Einfach verkettete Liste Bei der einfach verketteten Liste enthält jeder Knoten einen Zeiger auf sein Nachfolgeelement, einzig das letzte Element in der Liste verweist auf NULL. Das erste Element der Liste wird auch als RootElement bezeichnet, von dem aus jede Operation (Löschen, Einfügen, Suchen) in der Liste begonnen wird. 38/89 Doppelt verkettete Liste Im Gegensatz zur einfach-verketteten Liste hat bei der doppelt verketteten Liste jedes Element sowohl einen Zeiger auf das nachfolgende als auch auf das vorhergehende Element. Der VorgängerZeiger des ersten und der Nachfolger-Zeiger des letzten Elementes zeigen auf den Wert NULL. Diese besonderen Elemente dienen zum Auffinden des Anfangs und des Endes einer doppelt verketteten Liste. Stack Der Stack (Stapel) realisiert die so genannte LIFO-Strategie (Last In, First Out). Das bedeutet, dass jenes Element, welches zuletzt eingefügt wurde auch wieder zuerst entfernt wird. ● Init: Initialisiert einen neuen Stack. ● Push: Legt ein Element auf dem Stack ab. ● Pop: Holt das oberste Element des Stacks, dabei wird es vom Stapel gelöscht. ● Top/Peek: Liest das oberste Element im Stack, jedoch ohne es zu löschen. ● Empty: Überprüft ob der Stack leer ist. Realisierung: Liste mit entsprechenden Zugriffsmethoden Queue Die Queue (deutsch Warteschlange) ist eine lineare dynamische Datenstruktur. Eine Queue funktioniert nach dem so genannten FIFO-Prinzip (First In, First Out). Das heißt die Elemente werden am Ende hinzugefügt und am Anfang entfernt. Die Operationen zum Einfügen neuer und Entfernen bestehender Elemente werden enqueue und dequeue bezeichnet. Realisierung: Liste mit entsprechenden Zugriffsmethoden 39/89 Baum Ein Baum ist eine besteht aus einem Wurzelknoten (root). Dieser Knoten enthält neben den Daten diverse Zeiger auf unterliegende Elemente. Jedes dieser unterliegenden Elemente kann wiederum entweder der Wurzelknoten eines sog. Teilbaumes sein oder ein Leaf (Blatt), welches nur Daten und keinen Verweis auf unterliegende Elemente. Ein Baum kann z.B. zur Darstellung einer Verzeichnisstruktur verwendet werden. Binärer Baum Eine spezielle Form des Baums ist der binäre Baum. Hierbei verweist jeder Knoten auf 2 Elemente: left und right. Alle Elemente des linken Teilbaums sind hierbei kleiner als die des jeweiligen Knotens und alle Elemente rechts sind größer. (helf): Algorithmen im Pseudocode/PAP/… für Einfügen/Aushängen/… bei leerer/einelementiger/mehrelementiger Liste/... 40/89 Collections in Java Die Verwaltung von Sammlungen von Objekten ist ein zentraler Aspekt der Programmierung. Es ist ein Überblick über Java-Collections und deren Verwendungsmöglichkeiten zu geben. Collections ermöglichen eine einfache Verwaltung beliebiger Objekte, es ist dynamisches hinzufügen und entfernen mögliches und effizientes Suchen. Im Packet java.util sind alle wichtigen Klassen zur Verwendung von Collections enthalten. Interface von Collections: add(); clear(); contain(); remove(); size(); Iterator ● ● ● ● ● ● Eines der wichtigsten Interfaces für Collections ein Iterator “wandert” durch die Collection Object next() liefert nächstes Object zurück boolean hasNext() schaut ob es ein nächstes Object gibt remove() entfernt das zuletzt aufgelistete Element (helf): ermöglicht mehrere Iterationen gleichzeitig Arten von Collection ● Set (HashSet, TreeSet, ...) ○ enthält alle Collection Methoden ○ kann Elemente nicht doppelt enthalten ○ keine bestimmte Reihenfolge ○ schnelle suche ● List (ArrayList, LinkedList, Vector, Stack) ○ enthält alle Collection Methoden ○ kann Elemente doppelt enthalten ○ haben bestimmte Reihenfolge ○ schnelles einfügen/löschen ● Map (HashTable, TreeMap,...) ○ schnelles auffinden von Elementen über einen “key” ○ jedes Element muss eindeutigen key haben ○ Methoden: containsKey(), containsValue(); get(); put(); remove(); keySet(); ● Queue ○ FIFO Prinzip 41/89 Erweiterte for-Schleife Bei jedem Durchgang wird das nächste Element der Liste in “e” gespeichert, bis jedes Element einmal durchgegangen wurde. ArrayList<Element> elements = new ArrayList<Element>(); for(Element e : elements){ //Do sth with e } Wichtige Methoden des Collection Interfaces: ● ● ● ● ● ● ● ● toArray(); //wandelt die Daten der collection in einen Array um removeAll(Collection<?> c); //Alle Elemente der Collection die auch in Collection c enthalten sind werden entfernt isEmpty(); //liefert true oder false zurück wenn die Collection leer oder nicht leer ist clear(); //alle Elemente in der Collection werden gelöscht contains(Object o); //sieht nach ob das Objekt in der collection vorhanden ist liefert true,false zurück containsAll(Collection c); //liefert true zurück wenn alle objekte von c vorhanden sind add(Object o); //hinzufügen eines Objektes zur coll. addAll(Collection c); //alle elemente der collection c werden zur aktuellen hinzugefügt HINWEIS: Mengenoperation sind solche die mit mehrern Objekten , Collections arbeiten wie: containsAll, clear, removeAll 42/89 Graphische Oberfläche in Java (Swing) Welche Klassen bietet Swing für die Erstellung einer GUI. Wie erfolgt die Reaktion auf Benutzerinteraktionen? Welche Unterstützung für die Platzierung von Komponenten wird angeboten? Seit Java JDK Version 1.0 bietet Java mit dem Abstract Window Toolkit (AWT) die Möglichkeit grafische Benutzeroberflächen zu gestalten. Mit Version 1.2 wurden die Java Foundation Classes und damit Swing in Java integriert. Die Java Foundation Classes enthalten neben Swing noch Bibliotheken für 2D Programmierung, Drag&Drop, Accessability und das AWT. Swing baut auf AWT auf. Die Hauptalternative zu Swing ist das von IBM für Eclipse entwickelete Standard Widgets Toolkit, dass im Gegensatz zu Swing auf die Betriebssystemeigenen GUI-Komponenten zurückgreift. Welche Klassen bietet Swing für die Erstellung einer GUI? Grundlegende Hierarchie (helf): Swing: mit J beginnende Klassen -> jeder JButton, JTextField ist ein Container, ... Container Container sind Komponenten, die weitere Komponenten enthalten können. Container können wiederum auch weitere Container enthalten. Zu Containern zählen beispielsweise JScrollPane, JToolBar, JSplitPane (in mehrere Flächen geteilte Pane), JPanel, JTabbedPane (mehrere Tabs), JRootPane und JLayeredPane, aber auch GUI-Elemente wie JButton, JTextField…, sowie die folgenden Top-Level Container: 43/89 JDialog - Dialogfenster JFrame - Fenster JApplet - Erweiterung der Applets um Swing-Komponenten Aufbau der Pane Hierarchie 44/89 GUI-Komponenten JComboBox: JList: JMenu: JSlider: JTextField: JCheckBox: JLabel: JRadioButton: JButton: (helf): essentielle Methoden (setText, setSelected, …) Selektionsmodi, ButtonGroup, Mit Hilfe der Methode setText kann der Text auf zahlreichen Komponenten, wie JLabel, JButton, JTextField…. Mit der Methode setSelected kann der Selektionszustand einer Komponente geändert werden. Mit der Methode doClick kann ein Mausklick simuliert werden. Um zu verhindern, dass beispielsweise mehrere JRadioButtons gleichzeitig selektiert werden, können diese in einer ButtonGroup mit der Methode add() hinzugefügt werden. Wird dann ein Element der ButtonGroup selektiert werden automatisch alle anderen Elemente der ButtonGroup deselktiert. Wie erfolgt die Reaktion auf Benutzerinteraktionen? Das Reagieren auf Benutzerinteraktion erfolgt in Swing über das Listener-Konzept. Das heißt man registriert bei der Komponente, auf die man reagieren will, eine Listener-Implementierung, die den auszuführenden Programmcode enthält. Wird nun die gewünschte Aktion ausgeführt, so kümmert sich die Komponente darum, die entsprechende Methode des Listeners aufzurufen. Ein Listener stellt lediglich ein Interface dar und muss selbst implementiert werden.m Um nicht alle Methoden eines Listeners selbst implementieren zu müssen, stehen sogenannte Adapter zur Verfügung. Diese haben bereits alle Methoden des entsprechenden Listeners als leere 45/89 Methode implementiert und so kann von der AdapterKlasse abgeleitet werden und nur die Methode zur Aktion, auf die reagiert werden soll, muss überschrieben werden. Ereignisse Es existieren verschiedenste Ereignisse und dazugehörende Listener: Listener/Event Beschreibung ActionListener/ActionEvent für Buttonklicks, Menüklicks, Kombobox/Radiobutton Selektion, Entertaste in einem Textfeld ChangeListener/ChangeEvent Veränderung bei Slider, Spinner, Progress Bar,Toggle Button, Checkbox MouseListener/MouseEvent Für Mausklicks, kann auf jeder Swingkomponente registriert werden MouseMotionListener/MouseMotionEvent Für Mausbewegungen, kann auf jeder Swingkomponente registriert werden KeyListener/KeyEvent Für Tastatureingaben ... ... Welche Unterstützung für die Platzierung von Komponenten wird angeboten? Zur Platzierung und Anordnung von Komponenten bietet Swing verschiedenste StandardLayoutManager an. Ein solcher kann auch selbst implemtiert werden. Es ist außerdem möglich jede Komponente in einem Container anhand genauer Pixelangaben fix zu positionieren, was allerdings beim Skalieren von Fenstern oder Verwendung anderer Look-And-Feels Probleme bereitet. Jedem Container kann ein eigener Layoutmanager zugewiesen werden, wobei sich der LayoutManager um die Anordnung und Skalierung der Komponenten in diesem Container kümmert und automatisch anpasst. Folgende Layout-Manager stehen in Swing zur Verfügung: ● FlowLayout ● BorderLayout ● GridLayout ● GridBagLayout ● BoxLayout ● kein Layout ● eigens definiertes Layout 46/89 FlowLayout Es werden so viele Komponenten in einer Zeile wie möglich angezeigt. Hat eine Komponente in einer Zeile nicht mehr Platz wird eine Zeile begonnen. BorderLayout Der Container wird in 5 Bereiche eingeteilt. Oben PAGE_START, unten PAGE_END, links LINE_START, zentral CENTER und links LINE_END. In alten Java Versionen wurden die Himmelsrichtungen (NORTH, SOUTH, …) verwendet. GridLayout Der Container wird in n*m Felder unterteilt, die jeweils eine Komponente enthalten. 47/89 GridBagLayout Der Container wird in mehrere Felder unterteilt, wobei einzelne Komponenten auch mehrere Felder in Anspruch nehmen können. BoxLayout Entspricht einem erweiterten FlowLayout, wobei das BoxLayout erlaubt zu entscheiden, ob die Komponenten übereinander angezeigt werden sollen oder nacheinander zeilenweise wie beim FlowLayout. Beispiel import import import import import java.awt.Component; java.awt.Container; javax.swing.BoxLayout; javax.swing.JButton; javax.swing.JFrame; public class BoxLayoutDemo { public static void addComponentsToPane(Container pane) { pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); addAButton("Button 1", pane); addAButton("Button 2", pane); addAButton("Button 3", pane); addAButton("Long-Named Button 4", pane); addAButton("5", pane); } private static void addAButton(String text, Container container) { JButton button = new JButton(text); 48/89 button.setAlignmentX(Component.CENTER_ALIGNMENT); container.add(button); } public static void main(String []args) { //Create and set up the window. JFrame frame = new JFrame("BoxLayoutDemo"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Set up the content pane. addComponentsToPane(frame.getContentPane()); //Display the window. frame.pack(); frame.setVisible(true); } } 49/89 JDBC (Java Database Connectivity) Der Zugriff auf Datenbanken ist ein wesentlicher Aspekt moderner Anwendungen. Welche Architekturen, unterstützende Klassen und zentrale Code-Fragmente können hier genannt werden? Wie wird auf eine Datenbank über Java zugegriffen? Java Database Connectivity (JDBC) ist eine Datenbankschnittstelle der Java-Plattform, die eine einheitliche Schnittstelle zu Datenbanken verschiedener Hersteller bietet und speziell auf relationale Datenbanken ausgerichtet ist. JDBC ist in seiner Funktion als universelle Datenbankschnittstelle vergleichbar mit z.B. ODBC unter Windows. Der Datenbankzugriff erfolgt unabhängig vom verwendeten DBMS. Der Datenbank-Client braucht nicht mit einem bestimmten DBMS entwickelt werden. Alle DBMS- spezifischen Details übernimmt der JDBC- Treiber. Dieser ist ebenfalls in Java geschrieben und wird von den Datenbankherstellern oder von Dritten angeboten, um bessere und performantere Treiber entwickeln zu können. Zu den Aufgaben von JDBC gehört es, Datenbankverbindungen aufzubauen und zu verwalten, SQL-Anfragen an die Datenbank weiterzuleiten und die Ergebnisse in eine für Java nutzbare Form umzuwandeln und dem Programm zur Verfügung zu stellen. Beispiel ... try { Class.forName("Treiber“).newInstance( ); // Treiber laden Connection conn = DriverManager.getConnection("jdbc:DBMS:Datenbank",“User",“Passwort"); // Verbindung aufbauen /* Verbindung verwenden*/ conn.close(); // Verbindung schließen } catch (SQLException ex) { ... } Welche Treibertypen gibt es? Grundsätzlich gibt es 4 verschiedene Treiberarten wobei Typ-1 und Typ-2 nicht Netzwerkfähig sind. Die Treibertypen Typ-3 und Typ-4 sind Netzwerkfähig. 50/89 JDBC-ODBC-Bridge-Treiber (Typ-1) JDBC-Anweisungen werden in native Anweisungen des ODBC Treibers übersetzt, dieser Treiber ist nur ein mal vorhanden. Hauptsächlich unter Windows. Für jedes Datenbanksystem ist ein eigener ODBC-Treiber erforderlich JDBC-Native-API-Treiber (Typ-2) Dieser Treiber übersetzt JDBC-Aufrufe an eine nativ implementierte datenbankspezifische Bibliothek. Dazu wird nativer Code ausgeführt und somit ist die Platformunabhängigkeit nicht mehr gegeben. Dafür ist diese Lösung sehr schnell und wird für zeitkritische Anwendungen eingesetzt. Dieser Treiber kann nicht von Applets verwendet werden, da Applets keinen nativen Code von anderen Quellen laden können. JDBC-Pure-Java-Treiber (Typ-3) JDBC-Anweisungen werden von Java über ein Netzwerkprotokoll an einen auf dem Datenbankserver vorhandenen JDBC-Server übergeben und von diesem in Datenbankanweisungen übersetzt Native-Java-Protokoll-Treiber(Typ-4) Direkte Kommunikation zwischen dem JDBC-Treiber und dem Datenbankmanagementsystems. Ein Middleware-Treiber wird dabei nicht verwendet. Typ-4-Treiber eignen sich gut für IntranetLösungen, die schnelle Netzprotokolle nutzen wollen. Wie funktioniert das Auslesen von Metadaten? Dient zum Ermitteln über die Struktur einer Datenbank z.B. Auslesen der Tabellen in einer Datenbank oder der Datentypen einer Tabelle usw. ● DatabaseMetaData: ermittelt Informationen der Datenbank ● ResultSetMetaData: ermittelt Informationen über ein ResultSet ● ParameterMetaData: ermittelt Informationen zum Typ und Eigenschaften von Parametern 51/89 ResultSetMetaData meta = rs.getMetaData(); int numberOfColumns = meta.getColumnCount(); // Anzahl der Spalten auslesen DatabaseMetaData meta = con.getMetaData(); // URL zur verbundenen Datenbank String url = meta.getURL(); // URL zur verbundenen Datenbank Wie führt man SQL-Anweisungen aus? Um einen SQL-Befehl an die Datenbank zu schicken, benötigt man eine Instanz der Klasse Statement. Mit der Instanz von Statement kann man nun eine Anfrage mit der Methode executeQuery() ausführen. Als Ergebnis erhält man eine Instanz der Klasse ResultSet, welche das gesamte Ergebnis enthält. Mit der Methode executeUpdate() der Klasse Statement kann man INSERT, UPDATE und DELETE SQL Anweisungen abschicken. Diese Methode liefert keine Ergebnismenge zurück, wie ein SQL DDL Statement sondern liefert die Anzahl der eingefügten, gelöschten oder veränderten Datensätzen zurück, oder 0 wenn das SQL Statement nichts zurückgeben soll. Die Methode executeBatch() ermöglicht es, mehrere SQL-Anweisungen gleichzeitig abzuschicken. Mit der Methode addBatch() können weitere Anweisungen angehängt werden, dies hat den Vorteil, dass nicht jede Anweisung einzeln abgeschickt werden muss und nur so einmal eine Verbindung aufgebaut werden muss. Beispiel ... Statement state = conn.createStatement(); // Statement erstellen ResultSet rs = state.executeQuery(“SELECT * FROM Table”); // Datensatz auslesen String name = rs.getString(1); // String weiterverarbeiten String id = rs.getInt(2); // Int weiterverarbeiten state.executeUpdate(„DELETE FROM table WHERE id = 1“); // Datensatz löschen ... Was sind PreparedStatements und warum verwendet man sie? PreparedStatements bieten die Möglichkeit mittels ? einen Platzhalter in der SQL-Anweisung zu platzieren, welcher später nachträglich eingefügt werden kann. Dies ist nützlich wenn man sehr oft dieselbe Abfrage tätigen möchte und die Abfrage von den Eingaben des Benutzers abhängen. Außerdem sind PreparedStatements vor SQL-Injections sicher. Beispiel ... PreparedStatement ps= conn.prepareStatement(“select * from table where id = ?”); // PreparedStatement erstellen ps.setInt (1,3158); // Parameter setzten ResultSet rs = ps.executeQuery(); // Statement abschicken ... 52/89 Was sind Scrollable ResultSets? Bei Scrollable ResultSets, kann das Ergebnis vom Statement frei bearbeitet werden. Beim Statement muss man den Typ und die Rechte setzten. Man kann sich frei im ResultSet bewegen und auch Datensätze ändern. Diese werden auch in der Datenbank übernommen. resultSetType ● ● ● ResultSet.TYPE_FORWARD_ONLY ResultSet.TYPE_SCROLL_INSENSITIVE ResultSet.TYPE_SCROLL_SENSITIVE (Änderungen, während das ResultSet offen ist) resultSetConcurrency ● ● ResultSet.CONCUR_READ_ONLY ResultSet.CONCUR_UPDATABLE Beispiel ... Statement state = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); // Statement erstellen ResultSet rs = state.executeQuery(„SELECT * FROM table“); // ResultSet erstellen rs.absolute(5); // auf die 5. Zeile positionieren rs.updateInt("ID", 5893); // neuen Wert setzten rs.updateRow(); // Wert updaten ... Was versteht man unter Transaktionen? Technisch sind Transaktionen immer nur eine Anweisung, welche abgearbeitet wird. Transaktionen werden in der Informatik als eine Folge von Operationen, die als eine logische Einheit betrachtet werden, bezeichnet. Insbesondere wird für eine Transaktion gefordert, dass sie entweder vollständig ausgeführt wird oder keine Auswirkung hat (atomarität). Transaktionen stellen Konsistenz sicher. ACID-Prinzip ● A – Atomicity Unteilbarkeit: alle oder keine Anweisung wird ausgeführt. ● C – Consistency Konsistenz: Wenn das System vor der Transaktion in einem gültigen Zustand war, dann ist es dies auch nachher. ● I – Isolation Isolierung: Transaktionen laufen ungestört von anderen Transaktionen ab (es können nicht die selben Daten von verschiedenen Transaktionen bearbeitet werden). ● D – Durability 53/89 Dauerhaftigkeit: Mit commit abgeschlossene Änderungen sind persistent. Probleme Wenn Transaktionen dem ACID-Prinzip entsprechen sollen, müssen sie serialisiert nacheinander durchgeführt werden. Aus Performance-Gründen wird hiervon oft abgewichen und ein niedrigerer "Transaction Isolation Level" eingestellt und dies kann zu folgenden Fehler führen: ● Dirty Read Es können von anderen Transaktion geschriebene Daten gelesen werden, für die noch kein "Commit" erfolgte und die eventuell per "Rollback" zurückgesetzt werden. ● Non-repeatable Read (Stale Data) Während einer laufenden Transaktion können Daten von anderen Transaktionen geändert und committed werden, so dass in der ersten Transaktion ein zweites Auslesen zu anderen Daten führt. ● Phantom Read Eine erste Transaktion liest über eine "Where-Klausel" eine Liste von Datensätzen. Eine zweite Transaktion fügt weitere Datensätze hinzu (inkl. Commit). Wenn die erste Transaktion wieder über die gleiche "Where-Klausel" Datensätze liest oder bearbeitet, gibt es mehr Datensätze als vorher. Lösung: Isolationsstufen ● Read Uncommitted Geringste Isolation, höchste Performance, es können Dirty Reads, Non-repeatable Reads und Phantom Reads auftreten ● Read Committed Es gibt keine Dirty Reads mehr, aber es gibt weiterhin Non-repeatable Reads und Phantom Reads ● Repeatable Read Keine Dirty Reads und keine Non-repeatable Reads, aber weiterhin Phantom Reads ● Serializable Keine Dirty Reads, keine Non-repeatable Reads und keine Phantom Reads, höchste Isolation, geringste Performance Ablauf in Java Die Default-Einstellung für eine JDBC-Connection (diese verwaltet die Transaktionen) ist auf autoCommit gesetzt, was so viel heißt, wie jedes Statement ist eine eigene Transaktion. Um dies zu ändern, muss autoCommit auf false gesetzt werden. Anschließend werden alle Statements erst bei der Anweisung commit abgeschickt. Falls ein Fehler auftritt, können über rollback alle Änderungen 54/89 rückgängig gemacht werden. Es gibt auch noch Savepoints um nicht die gesamte Transaktion rückgängig zu machen. Beispiel ... try { conn.setAutoCommit(false); // AutoCommit deaktivieren state.executeUpdate(„UPDATE table SET ID = … “); // Werte ändern state.executeUpdate(„UPDATE table SET NAME = … “); // Werte ändern conn.commit( ); // Transaktion abschicken } catch (SQLException e) { conn.rollback(); } ... // Bei einem Fehler, Änderungen rückgängig machen ... SavePoint sp1 = conn.setSavePoint(„Sicherung1“); // Sicherungspunkt erstellen conn.rollback(sp1); // Sicherungspunkt zurückspielen ... 55/89 RMI (Remote Method Invocation) Wie kann der Zugriff auf Java-Anwendungen auf entfernten Computern realisiert werden? Wie muss ein Server aufgesetzt werden bzw. wie wird auf den Server zugegriffen?Wie sehen die beiden Enden der Verbindung aus und wie werden Argumente übertragen? Was ist RMI? RMI steht für Remote Method Invocation, was übersetzt entfernter Methodenaufruf bedeutet. Remote Method Invocation (RMI) ist ein Modell für die objektorientierte Client-ServerKommunikation bei verteilten Java-Anwendungen. RMI ermöglicht es einem Client, serverseitige (entfernte) Objekte zu nutzen und deren Methoden aufzurufen. Wie kann der Zugriff auf Java-Anwendungen auf entfernten Computern realisiert werden? Sockets Der Zugriff kann über Sockets realisiert werden. Der Weg über Sockets ist relativ kompliziert da die verschiedenen Datentypen und Objekte mit dem ObjectOutputStream verschickt und mit den ObjectInputStream empfangen werden müssen. Alle Objekte die man über Sockets versenden möchte müssen das Interface Serializable implementieren. RMI Mit RMI funktioniert das ganze wesentlich einfacher. Dem Client wird vorgespielt er verwende eine lokale Methode. Der Client verwendet also eine Methode die in einem Interface liegt. Zu diesem Interface gibt es am Server eine Implementierung. Der Client hat also nur den Namen der Methode und ihre Parameter. Wichtig ist, dass das Interface das Interface Remote implementiert und jede Methode im Interface die RemoteException werfen kann. Wie muss ein Server aufgesetzt werden bzw. wie wird auf den Server zugegriffen? 56/89 Die Codierung und Decodierung der Aufrufparameter und Rückgabewerte von Methodenaufrufen und die Übermittlung der Daten zwischen Client und Server wird vom RMI-System und den generierten Klassen, beim Client Stub und beim Server Skeleton genannt, geregelt. Der Stub fungiert als lokaler Stellvertreter (Proxy) des entfernten Objekts. Stubs und Skeleton, die Java Programme, die auf Grund der Interface Definitionen erstellt werden (wobei Skeletons bei neueren Releases, ab 1.5.x, nicht mehr benötigt werden), sind sozusagen die Schnittstelle zum Anwendungsprogramm und dem Reference Layer. Der Reference Layer ist nötig, da Referenzen in der einen JVM auf Referenzen in einer eventuell entfernten JVM abgebildet werden müssen. Der Stub und der Skeleton übertragen die Daten am Netzwerk. Das Einpacken der Objekte wird Marshalling und das Auspacke Unmarshalling genannt. Bei RMI wird ein Objekt mit bestimmten Anforderungen, dass exportiert und ans Netzwerk gebunden wird, erstellt. Dieses muss in die Registry eingetragen werden, also registriert werden. Am Server gibt es einen Skeleton der ähnlich wie ein Proxy funktioniert. Er weiß wo die Anfrage herkommt, berechnet und schickt sie zum Client zurück. Remote Object Das RemoteObject stellt das entfernte Objekt dar und liegt auf dem Server. Es implementiert das RemoteInterface und das Verhalten der für die Clients zur Verfügung stehenden entfernten Methoden. Vom Server können eine oder mehrere Instanzen des Remote-Objekts erstellt werden. Ein Remote Object muss von UnicastRemoteObject abgeleitet sein und muss einen parameterlosen Konstruktor haben, denn dieser ruft nur den Konstruktor von UnicastRemoteObject auf und könnte eine RemoteException auslösen. Jede Methode muss eine RemoteException deklarieren, auch der parameterlose Konstruktor. Registry Die Registry, ein Namensdienst, ist ein serverseitiger Vermittlungsdienst, der einem Client auf Anfrage mitteilt, welche Remote-Objekte auf dem Server vorhanden und nutzbar sind. Der Namensdienst kann als ein eigenständiger (nebenläufiger) Serverprozess laufen oder er kann von innerhalb des Serverprogramms gestartet werden. 57/89 Ablauf Das Serverprogramm registriert seine Remote-Objekte beim Namensdienst mit bind(...) bzw. rebind(...). Ein Client kann dann mit list(...) bzw. lookup(...) den Namensdienst befragen und erhält eine Liste der registrierten Remote-Objekte bzw. eine Referenz auf das gewünschte Remote-Objekt. Übertragung Der Stub ist so etwas wie Object, also muss man wieder casten, um seinen gewünschten Datentyp zu erhalten. Die Dienste die zwischen Server und Client vorhanden sein sollten, sollten in einem Interfaces erstellt werden. Die Implementierung ist nicht am Client möglich, denn die Funktionen können/sollen nur am Server aufgerufen werden. Der Client weiß nur die Aufrufe. Am Server wird dieses Interface implementiert. Das Interface, muss wiederum das Interface Remote implementieren. Da die Methoden über das Netzwerk übertragen werden, kann ein Fehler beim Übertragen auftreten. Darum müssen die Methoden RemoteException werfen, da sie sonst nicht aufgerufen werden können. Lokal und über das Netzwerk wird meistens auf dasselbe Interface zugegriffen, deshalbe sollte auch beim Lokalen Interface Remote implementiert werden. Die Methoden der Interfaces sollen die RemoteException werfen. Die Methode rebind() liegt am Server und man kann damit die OID und den Namen verbinden. Dies wird zum Beispiel bei der Registry benötigt. Was sind Stub und Skeleton Klassen? Stub ● ● ● Proxy an der Clientseite “Beinhaltet” alle Netzwerkfähigkeiten (Serialisierung, Sockethandling,...) Restlicher Java-Code “sieht” Stub wie einfache Methoden Skeleton ● ● Wie Stub, nur auf Serverseite Teilweise andere Funktionalität (Nachladen, ...) Wie sehen die beiden Enden der Verbindung aus und wie werden Argumente übertragen? Allgemeine Beschreibung der Enden Client ● Hat nur das Interface um die Methoden aufrufen zu können ● Cast auf das Interface beim Empfangen der Daten ● hat den Stub Server ● Implementiert das Interface ● hat den Skeleton 58/89 Eclipse ● 3 Projekte: ○ Projekt mit Serverklassen ○ Projekt mit Clientklassen ○ Projekt mit Interfaces, dieses Projekt wird auf die anderen Projekte referenziert ○ Durch diese Trennung sind am Server und am Client nur die benötigten Methoden und Klassen verfügbar Impelemtierung der Enden 2 Varianten beim Remote Interface Entweder erbt das Interface von Remote oder von UnicastRemoteObject. Erbt es von Remote dann sieht die Implementierung wie im folgenden Beispiel aus. D.h. im Server muss noch UnicastRemoteObject.exportObject() aufgerufen werden. Würde das Remote-Interface einfach von UnicastRemoteObject erben, dann würde die Erweiterung mit einer weiteren Klasse nicht möglich sein. Also ist der saubere Weg über die UnicastRemoteObject.exportObject() Variante, weil man sich selbst die Hierarchie zusammenstellen kann. Remote Interface Zuerst erstellt man ein Java Interface. Dieses erweitert das Interface java.rmi.Remote. Das Remote Interface definiert die Sicht des Clients auf das entfernte Objekt. In dem neuen Interface werden alle Methoden deklariert, die Remote aufrufbar sein sollen. Da die Methoden auch unter Umständen über das Netzwerk aufgerufen werden, kann es natürlich zu Kommunikationsproblemen kommen, daher muss zu jeder Methodendefinition ein throws java.rmi.RemoteException hinzugefügt werden. public interface OperationsCalc extends Remote { int sum(int a, int b) throws RemoteException; int sub(int a, int b) throws RemoteException; int mul(int a, int b) throws RemoteException; } Remote Klasse Nun wird eine weitere Klasse hinzugefügt, die dieses Interface implementiert. public class OperationsCalcImpl implements OperationsCalc{ @Override public int sum(int a, int b) throws RemoteException { return a+b; } @Override public int sub(int a, int b) throws RemoteException { return a-b; } 59/89 @Override public int mul(int a, int b) throws RemoteException { return a*b; } } Server public static void main(String[] args) { try { String name = "CalcServer"; OperationsCalcImpl server = new OperationsCalcImpl(); OperationsCalc stub = (OperationsCalc) UnicastRemoteObject.exportObject(server, 0); Registry registry = LocateRegistry.createRegistry(12345); //Port registry.rebind(name, stub); System.out.println("CalcServer gebunden"); } catch (RemoteException e) { e.printStackTrace(); } } Client Nun wird natürlich noch ein Client benötigt, der die Methoden des Servers aufruft. Hier verwendet man die Klasse java.rmi.Naming. Mit folgendem Code holt man sich eine Remote Referenz des Servers und kann dann die Methoden des Remote - Interfaces aufrufen. public static void main(String[] args) { OperationsCalc op; int a=3; int b=2; try { Registry registry = LocateRegistry.getRegistry("localhost",12345); op = (OperationsCalc) registry.lookup("CalcServer"); System.out.println("Addieren von "+a+" und "+b+": "+op.sum(a, b)); System.out.println("Subtrahieren von "+a+" und "+b+": "+op.sub(a, b)); System.out.println("Multiplizieren von "+a+" und "+b+": "+op.mul(a, b)); } catch (RemoteException e) { e.printStackTrace(); } catch (NotBoundException e) { e.printStackTrace(); } } Argumente übertragen Bei RMI gibt es 3 verschiedene Arten der Parameterübergabe und Rückgabe: 60/89 Basisdatentypen int, double, boolean,etc. - Werte werden über das Netzwerk übertragen Auf Senderseite werden Werte „gemarshaled“ (Kodierung, Übersetzung von z.B. Linux auf Windows nennt man Marshalling) und über das Netzwerk gesendet. Auf der Empfängerseite werden die Werte wieder entpackt und an Client geliefert. Serialisierbaren Objekten Objekte die das Interface Serializable implementieren: Objekte werden serialisiert über das Netzwerk übertragen und auf der anderen Seite eine Kopie aufgebaut. Inhalt des Objekts wird über den Java-Serialisierungsmechanismus serialisiert und über das Netzwerk übertragen ● ● auf der Empfängerseite wird ein neues Objekt mit gleichem Inhalt aufgebaut Problem, wenn Klasse des serialisierten Objekts auf Empfängerseite nicht bekannt ist! Remote Objekten Objekte die das Interface Remote implementieren: Es wird auf der empfangenden Seite ein Stub für das Objekt aufgebaut. Es wird kein Wert übergeben sondernhy ● über das RMI-System wird eine Remote-Referenz auf das Server-Objekt übermittelt ● beim Client wird dafür ein Stub erzeugt und Referenz auf den Stub dem Empfänger übergeben 61/89 Andere Keine Basisdatentyp, nicht serialisierbar oder nicht Remote - können nicht als Parameter übergeben werden 62/89 Java und XML (Extended Markup Language) Die Nutzung von XML in Java-Anwendungen ist anhand der wesentlichen Verfahren der XML-Verarbeitung in Java zu erläutern. Dabei ist sowohl die Typisierung von XMLDokumenten als auch die Transformation von XML-Dokumenten zu erklären. XML document Ein XML Dokument gilt als wohlgeformt, wenn es ein oder mehrere Elemente enthält und genau ein Wurzelelement. Ein Wurzelelement befindet sich in keinem anderen Element. Jedes andere Element wird durch Start- und End-Tag begrenzt, welche sich im Kontext des selben Elements befinden müssen und daher richtig geschachtelt sind. XML-Document-Kopf: <?xml version="1.0" encoding="UTF-8"?> Schema Einbinden: <ns:Element xmlns:ns="http://www..example.com/schemalocation"> ns <- frei wählbar Beispiel <tag1> <tag2>text</tag2> <tag3 id=“1“ /> </tag1> Element Ein Element wird in XML entweder mit Start- und Endtag (<tag> </tag>) begrenzt oder mit einem Empty-Element-Tag (<etag />) definiert. In einem Element kann es auch mehrere Attribute geben, diese sind im Start- oder im EmptyElement-Tag zu finden. Sie sind in Name und Wert aufgeteilt (attr=“val“), dabei ist darauf zu achten das der Wert in Doppelte Anführungszeichen zu setzen ist und der Attribut-Name nicht doppelt im selben Tag vorkommen darf. Empty-Element-Tags haben den Sinn, dass Werte in Attributen gespeichert werden können, ohne einen Content zu haben (man spart sich das End-Tag). Beispiel <tag1 id=“2“ name=“Hugo“> <tag3 id=“1“ /> </tag1> 63/89 XPath Ist eine Abfragesprache für XML-Dokumente. Man navigiert in der Baum-Struktur über child/parent/root/following/etc. Und kann auch auf Attribute oder Texte abfragen. Beispiele //child::Buch/Kapitel Wählt alle „Kinder“ von allen Büchern die Kapitel sind. //child::Buch[2]/Kapitel[1] Wählt das 1. Kapitel vom 2. Buch //Kapitel[@titel='Kapitel Eins'] Wählt alle Kapitel die das Attribute „titel“ mit dem Wert „Kapitel Eins“ haben. /Buch[count(Seite)>10] Erstes Buch mit mehr als 10 Seiten Schema Mit einem Schema kann man bestimmen ob ein Dokument nicht nur wohlgeformt, sondern auch korrekt/valide ist. Hier legt man fest, welche Attribute/Kinder/etc. eine Element haben darf und welchen Restriktionen diese unterliegen. Beispiele <xs:complexType name=“schueler“> //definiert einen Typen <xs:element name=“name“ type=“xs:string“ minOccurs=“1“ maxOccurs=“1“ /> //schueler hat genau ein Kinderelement „name“ ... <xs:attribute name=“id“ type=“xs:integer“ /> //schueler hat ein Attribute namens id </xs:complexType> Ein korrekter Schueler im XML sieht daher so aus: <schueler id=“1“> <name>Hugo</name> ... </schueler> Man kann unter type auch selbst definierte Typen verwenden. Beispiel Einbindung eigener Typen <xs:complexType name=“klasse“> <xs:sequenz> //mehrere Objekte <xs:element name=“schueler“ type=“schueler“> </xs:sequenz> </xs:complexType> Beispiel für simple Typen <xs:simpleType name=“monatInt“> <xs:restriction base=“xs:integer“> 64/89 <xs:minInclusive value=“1“/> <xs:maxInclusive value=“12“/> </xs:restriction> </xs:simpleType> einfache Typen xs:string/decimal/integer/float/boolean/date/time/anyType xs:QName/anyURI/language/ID/IDREF XML in Java-Anwendungen Bei der DOM-API wird das gesamte Document in den Speicher geladen und ist daher sehr speicherintensiv. Laden Beispiel Document doc = new SAXBuilder().build(file); Element root = doc.getRootElement() List<Element> elemente = root.getChildren("tag1"); elemente.get(1).getAttributeValue(„id“); Ändern Beispiel element.setAttribute(name,value); element.setAttribute(attribute); element.setText(text); element.setContent(child); ... Speichern Beispiel Document doc = new Document(); //... doc befüllen mit .addContent(), etc. XMLOutputter out = new XMLOutputter(Format); //normalerweise Format.getPrettyFormat(); out.output(doc, new FileWriter(new File(file)); XPath-Anwendung in Java XPath xPath = XPath.newInstance(XPath-Expression als String); List<?> nodeList = xPath.selectNodes(doc.getRootElement()); JDOMXPath xpath = new JDOMXPath(XPath-Expression als String); List<?> ergebnis = xpath.selectNodes(doc); 65/89 Java und Sockets Erklären Sie die Grundsätze der Netzwerkprogrammierung, Welche Socket-Arten gibt es und wie unterscheiden sie sich? Auf welcher Netzwerkschicht operieren welche Socket-Arten? Nennen die wichtigsten Klassen? Wie schreibe ich in ein Socket und wie lese ich von einem Socket? Skizziere einen rudimentären Client/Server Zugriff. Vergleich TCP UDP UDP ● ● ● ● TCP ● ● ● ● Verbindungslos Ankunft der Pakete nicht garantiert Empfangsreihenfolge != Sendereihenfolge Wenig Overhead (billig) Verbindungsorientiert Ankunft der Pakete garantiert (oder Fehlermeldung bei Unzustellbarkeit) Empfangsreihenfolge = Sendereihenfolge Viel Overhead (teuer) Protokoll = Vereinbarung der Kommunikationsabwicklung TCP und UDP operieren auf der Transportschicht (4) des OSI Modells. Sockets Das Programm, welches Dienste anbietet: Server Das Programm, welches Dienste benutzt: Client Der Client sendet dabei Aktiv eine Anfrage an den Server, der passiv auf Anfragen wartet. Sockets bilden eine Plattformunabhängige, standardisierte API zwischen dem Netzwerkprotokoll des OS und der Anwendungssoftware. Charakterisierung Eine Socket Verbindung kann mit den folgenden Parametern charakterisiert werden: ● Name des Quellrechners (IP-Adresse) ● Port des Quellrechners ● Name des Zielrechners (IP-Adresse) ● Port des Zielrechners Komponenten (wichtige Klassen) ● ● ● Adress- und Namensauflösung ○ InetAddress UDP Kommunikation ○ DatagramPacket (Beinhaltet die Daten, die gesendet werden) ○ DatagramSocket (Senden und Empfangen von Packets) TCP Kommunikation 66/89 ● ○ ○ ServerSocket (Auf Verbindung warten) Socket (Senden und Empfangen) ○ Java Abbildung einer URL URL UDP Der Server und der Client besitzen beiderseits ein DatagramSocket, über welches sie Pakete (DatagramPackets) versenden (send-Methode) und empfangen (receive-Methode). Die Größe der Pakete ist limitiert. Ein UDP-Socket wird an einen Port des lokalen Rechners gebunden. Er kann nun Datagramme senden und empfangen. new DatagramSocket(int port). Die zu sendenden Daten werden in ein DatagramPaket gespeichert. new DatagramPacket(byte[] daten, int laenge, InetAddress empfaenger, int port); Beispiel Server DatagramSocket socket = new DatagramSocket( 4711 ); while ( true ){ DatagramPacket packet = new DatagramPacket( new byte[1024], 1024 ); //Auf Anfrage warten socket.receive( packet ); //Paket verarbeiten packet.getData() } Client InetAddress ia = InetAddress.getByName("localhost"); int port = 4711; String s = "Hallo"; byte[] data = s.getBytes(); packet = new DatagramPacket(data, data.length, ia, port); DatagramSocket toSocket = new DatagramSocket(); toSocket.send(packet); TCP Sockets sind eine Programmierschnittstelle für Streambasierte Kommunikation. Das Lesen und Schreiben von Daten über Sockets erfolgt über Input- bzw. Outputstreams. Ablauf der Kommunikation 1. Aufbau einer Verbindung über IP und Port 2. Lesen und Schreiben 67/89 3. Schließen der Verbindung Die Verbindung wird aufgebaut, indem man ein neues Socket Objekt anlegt. new Socket(String address, int port) new Socket(InetAddress address, int port) Zur Kommunikation wird ein Input oder Outputstream verwendet. socket.getInputStream(); socket.getOutputStream(); Am Ende werden die Streams geschlossen und die Verbindung beendet. Dies sollte im Finally-Block geschehen, damit auch bei einem auftretenden Fehler die Verbindung getrennt wird. inputstream.close(); outputstream.close(); socket.close(); Beispiel Server ServerSocket serverSocket = new ServerSocket(6789); while(true){ //Auf Anfrage warten Socket connectionSocket = serverSocket .accept(); InputStream is = connectionSocket.getInputStream(); OutputStream os = connectionSocket.getOutputStream(); //Daten Verarbeiten } (helf): Request in eigenem Thread abwickeln Client String nachricht = “Hallo”; Socket clientSocket = new Socket("localhost", 6789); OutputStream os = clientSocket.getOutputStream(); InputStream is = clientSocket.getInputStream(); outToServer.writeBytes(nachricht.getBytes()); Datenaustausch Der Datenaustausch ist streambasiert bei (TCP) Sockets (InputStream, OutputStream). Der Ablauf einer Verbindung ist: Verbindung aufbauen, Daten austauschen, Schließen der Verbindung. Bei UDP (Datagrampackets) werden die Daten in einem Bytearray gesendet. Obwohl es eine Klasse Socket (für Client) und ServerSocket (für Server) gibt, erfolgt die Kommunikation auf beiden Seiten über eine Instanz der Klasse Socket. (serversocket.accept() liefert ein Socket Objekt zur Kommunikation) 68/89 Java Generics Motivation für generische Typen, Beispiel für parametrisierte Klassen in Java Bibliotheken, Beispiel für eine eigene generische Klasse, Generische Methoden, Beispiel und Einsatzmöglichkeit, Generische Methoden mit Typparameter, Generische Methoden mit Wildcards, Einschränkungen für den Einsatz von Wildcards. Die generischen Typ-Variablen repräsentieren zum Zeitpunkt der Implementierung unbekannte Typen. Erst bei der Verwendung der Klassen, Schnittstellen und Methoden werden diese TypVariablen durch konkrete Typen ersetzt. Das bedeutet, dass während der Entwicklung von Klassen und Methoden der genaue Typ von Variablen nicht feststehen muss. Erst bei der Verwendung wird der Typ festgelegt. Durch die Verwendung von generischen Klassen kann typsicher programmiert werden, da der Compiler die Typen kontrollieren kann. Beispiel mit ArrayList Wenn zum Beispiel die Java Collection Klasse ArrayList ohne Generics verwendet wird, kann jedes beliebige Element hinzugefügt werden. Daher, in die ArrayList al könnte zu den Integer Werten auch ein String hinzugefügt werden, ohne dass der Compiler einen Fehler meldet. Wenn die Elemente aus der Liste herausgenommen werden, müssen sie wieder auf ihren ursprünglichen Wert gecastet werden. ArrayList al = new ArrayList(); int sum = 0; al.add(1234); al.add(5678); al.add("test"); //Keine Typ-Kontrolle for (int i = 0; i < al.size(); i++) { sum += (Integer)al.get(i); //Ein Cast ist notwendig //Fehler bei String "test" } Durch die Verwendung der generischen Klasse ArrayList fallen diese zwei Probleme weg. Denn durch die generizität kann nur noch der vorgegebene Typ zur ArrayList hinzugefügt werden und beim Herausnehmen der Elemente aus der Liste ist auch kein Cast mehr notwendig, da der Typ bereits festgelegt wurde. ArrayList<Integer> al = new ArrayList<Integer>(); int sum = 0; al.add(1234); al.add(5678); //al.add("test"); //Nicht mehr möglich 69/89 for (int i = 0; i < al.size(); i++) { sum += al.get(i); //KEIN Cast notwendig } Eigene generische Klasse Eine eigene Generische Klasse kann folgendermaßen erstellt werden. Der Typparameter T kann jeden beliebigen Namen haben, allerdings ist es Konvention, einen großen Buchstaben zu verwenden. Generische Klassen können beispielsweise für Sortieralgorithmen verwendet werden. public class Point<T> { private T x; private T y; public Point( T x, T y ) { this.x = x; this.y = y; } public T getX() { return x; } public T getY() { return y; } } Verwendung Unsere eigene Klasse kann wie folgt verwendet werden. Point<Integer> = new Point<Integer>(2, 3); Point<Double> = new Point<Double>(4.5, 5.6); Die Doppelte Angabe der Typen kann weggelassen werden, wenn die Typen zuvor angegeben wurden. List<Map<Date, String>> listOfMaps = new ArrayList<Map<Date, String>>(); Die folgende Zeile ist ebenfalls gültig. List<Map<Date, String>> listOfMaps = new ArrayList<>(); Generische Methode Eine generische Methode wird wird wie in einer generischen Klasse definiert. public T compare(T m, T n) { return m > n; } 70/89 Probleme bei generischen Typen Folgende Konstrukte sind mit generischen Typen nicht möglich: ● new new T(); ● instanceof p instanceof Pocket<Number> ● casten (Pocket<String>) new Pocket<Integer>(); ● .class Pocket<String>.class; ● Exceptions class MyException<T> extends Exception { } ● statische Methoden public static boolean isEmpty( T value ) ● Arrays von generischen Klassen pockets = new Pocket<String>[1]; Einschränkung von generischen Typen Um den generischen Typ zu beschränken, kann mit extends eine Klasse angegeben werden die der generische Typ erweitern soll. Zum Beispiel können mit extends Number nur Klassen die Number erweitern (Integer, Float, Double, …) verwendet werden. public class Point<T extends Number> { private T x; private T y; public Point( T x, T y ) { this.x = x; this.y = y; } } Es kann auch vorgeschrieben werden, dass der generische Typ ein bestimmtest Interface implementieren muss. Zum Beispiel sollte der generische Typ T das Interface Comparable implementieren. public static <T extends Comparable<T>> T max( T m, T n ) { return m.compareTo( n ) > 0 ? m : n; } Mehrere Einschränkungen mit & Mit & können mehrere Einschränkenungen verknüpft werden. <T extends Serializable & Comparable> Wildcards Das Wildcard ? wird verwendet, um Klassen mit verschiedenen generischen Typen zu unterstützen. Der Funktion printPoints können Objekte mit jedem beliebigen generischen Typ übergeben werden. void printPoints(Point<?> points) { for (Object p : points) { System.out.println(p); 71/89 } } Wildcards einschränken Wildcards können wie generische Typen mit extends eingeschränkt werden. Außerdem kann mit super bestimmt werden, dass der Typ des Wildcard über dem angegebenen Typ stehen muss. void printPoints(Point<? extends Number> points) Alle Typen die Number erweitern sind erlaubt void printPoints(Point<? super Integer> points) Alle Typen über Integer sind erlaubt (also Integer, Number, Object) 72/89 Drucken in Java Die Erzeugung von Druckwerken muss auch in Java-Anwendungen möglich sein. Das Ausdrucken von mehrseitigem, formatiertem Text, Bildern im Posterdruck bzw. skaliert und Texten mit Bildern ist zu erklären. In Java ist es möglich, Daten über eine Schnittstelle zum Drucker zu senden damit diese dann gedruckt werden. Der erste Schritt um dies durchzuführen ist ein neues Objekt der Klasse „Printerjob“ zu erstellen. PrinterJob job = PrinterJob.getPrinterJob(); Dieses Objekt ist sozusagen der Controller des gesamten Druckauftrages, es kontrolliert welche Seitenformate auf welche Seite angewandt werden und weißt jeder Seite den definierten Inhalt zu. Es gibt 2 Ansätze in Java zu drucken: 1. Möglichkeit Das Betriebssystem eigene Dialogfeld zum drucken wird Aufgerufen und die Daten werden an das Java-Programm weitergegeben und damit gedruckt. Dieser Weg ist der einfachste und braucht nur einen Befehl: job.printDialog(); Folgendes Feld wird unter Windows gestartet: Nun kann man in einer übersichtlichen Benutzeroberfläche den Drucker und andere Formatierungsoptionen auswählen. 73/89 Wenn der „Abbrechen“ Button gedrückt wird liefert die Methode false zurück. 2. Möglichkeit: Ohne dem Standard Dialogfeld vom Betriebssystem, also alle Einstellungen werden vom Programm selbst durchgeführt und der Benutzer hat keinen Einfluss darauf. Drucker einstellen Um einen Drucker einzustellen müssen wir uns ein Feld mit allen installierten Druckern vom System holen dies geschieht mit dem Folgenden Befehl: PrintService[] printers = PrinterJob.lookupPrintServices(); Um unserem Druckauftrag letztenendes einen Drucker zuzuweisen wird folgender Befehl benötigt: job.setPrintService(printers[1]); PageFormat Als nächstes muss noch das Seitenformat eingestellt warden in dem gedruckt warden soll, in meinem Fall A4. Dazu erstellt man ein „PageFormat“ Objekt und setzt die gewünschte Orientierung ( PageFormat.LANDSCAPE – Querformat , PageFormat.PORTRAIT – Hochformat ) PageFormat normalFormat = new PageFormat(); normalFormat.setOrientation(PageFormat.LANDSCAPE); Nun kann man noch den Bereich der zu Drucken ist auf der Seite festlegen. Hierzu erstellt man ein neues Objekt der Klasse „Paper“ und ruft die Methode „setImagableArea“ auf. Zu letzt wird das Objekt dem Pageformat zugewiesen. Paper paper = new Paper(); paper.setSize(594.936, 841.536); //Set to A4 Size paper.setImageableArea(0, 0, 800 , 841.536); //Parameter: X-Startpunkt, Y-Startpunkt, breite, höhe normalFormat.setPaper(paper); 74/89 Inhalt des Ausdruck Eine Printable sieht dann wie folgt aus , wichtig ist das links oben der Punkt (0/0) ist: Grundsätzlich ist das Drucken des Inhaltes nur von einer Klasse möglich die das Interface Printable implementiert. In der Nachfolgenden Grafik sieht man wie das Zusammenspiel zwischen Printerjob und Printable abläuft: Eine leere Klasse mit dem Printable interface implementiert sieht dann wie folgt aus: import java.awt.print.Printable; public class testprint implements Printable{ @Override 75/89 public int print(Graphics graphics, PageFormat pf, int pageIndex) throws PrinterException { return 0; } } Die „print“ Methode wird dabei automatisch während des Druckvorgangs aufgerufen. Des Weiteren implementiert das Interface 2 Konstanten: ● NO_SUCH_PAGE ● PAGE_EXISTS Diese werden ausschließlich als Return-Werte benutzt und sagen dem Printerjob ob diese Seite gedruckt werden soll „PAGE_EXISTS“ oder nicht „NO_SUCH_PAGE“ (z.B. weil einFehler beim Befüllen des Printables aufgetreten ist). (helf): NO_SUCH_PAGE auch bei der ersten nicht mehr befüllten Seite Befüllen des Printables Das Befüllen des Printables ist ähnlich wie bei Erstellung der GUI. Man zeichnet bestimmte Komponenten bei bestimmten Koordinaten auf die Seite, doch zunächst wandeln wir das übergebene „Graphics“ Objekt in ein „Graphics2D“ Objekt um: Graphics2D g2d = (Graphics2D) graphics; Auf diesem g2d Objekt kann dann ganz einfach mit verschieden Methoden geometrische Formen und Bilder gezeichnet werden: g2d.drawImage(bufferedImage , 10 , 10 , null); g2d.drawLine(20, 20, 50, 50); g2d.drawRect(30, 30, 200, 100); 1. Es wird ein Buffered Image auf die Grafik des Printables gezeichnet an der Position (10/10) 2. Es wird eine Linie gezeichnet vom Punkt (20, 20) zu (50, 50) 3. Es wird ein Rechteck gezeichnet mit einem Linkenoberen Eckpunkt (30, 30) und einer Seitenlänge von 200 und Höhe von 100 Drucken von Text Das Drucken von Text fällt etwas schwieriger aus , hier ein fertiger Code der im einzelnen erklärt wird: public int print(Graphics g, PageFormat format, int pageIndex) { Graphics2D g2d = (Graphics2D) g; //Beginn in den Druckbaren Bereich verschieben g2d.translate(format.getImageableX(), format.getImageableY()); g2d.setPaint(Color.black); 76/89 AttributedString myStyledText = new AttributedString(“TEST”); //LineBreakMeasurer um die Zeilenlänge zu ermitteln Point2D.Float pen = new Point2D.Float(); AttributedCharacterIterator charIterator = mStyledText.getIterator(); LineBreakMeasurer measurer = new LineBreakMeasurer(charIterator,g2d.getFontRenderContext()); float wrappingWidth = (float) format.getImageableWidth(); //Über die Seite laufen und sie mit Text füllen while (measurer.getPosition() < charIterator.getEndIndex()) { TextLayout layout = measurer.nextLayout(wrappingWidth); pen.y += layout.getAscent(); float dx = layout.isLeftToRight() ? 0 : (wrappingWidth – layout.getAdvance()); layout.draw(g2d, pen.x + dx, pen.y); pen.y += layout.getDescent() + layout.getLeading(); return Printable.PAGE_EXISTS; } Kurz gesagt muss man hier die Y-Koordinate nach jeder Zeile um eine gewisse Höhe nach unten Schieben nämlich die Höhe der Schriftart (Ascent + Descent) + Zeilenabstand (Leading), die nächste Grafik sollte das ganze verdeutlichen: Bilder im Posterdruck Um ein Poster zu drucken das über mehrere Seiten gehen soll ist ein Pageable notwendig. Der erste Schritt der gemacht werden muss ist das zerteilen des Bildes in kleinere Teile die genauso groß sind wie der druckbare Bereich (ImageableArea) der Seite. Ein Code zum teilen sieht man hier: 77/89 Code Erklärung: 1. wir holen die breite und Höhe des bildes 2. wer berechnen wieviele Seiten wir brauchen und speichern sie in “anzhor” (anzahlhorizontal) und “anzvert” (anzahlvertikal) ab. Werte sind GANZZAHLEN weil int/int. Bsp. wir wissen dann das wir z.b 3x4 Seiten brauchen 3. Der restliche Bereich wird berechnet der nicht mehr eine ganze Seite gebraucht hat. 4. Nun muss bei unsere “Matrix” von vorhin bei horizontal und vertikal 1nes dazugezählt werden wenn die restbreite und resthöhe größer 0 sind 5. Zu guter letzt wird mit einer doppelschleife durchgegangen und die einzelnen Teilbilder mit “.getSubimage()” vom Orginalbild in eine Arraylist (bilder) gespeichert. Der Restliche Aufbau des Pageables ist nicht sehr kompliziert. Die getNumberofPages Methode kann diesmal die Länge der Arraylist zurückgeben damit das Pageable weiß wenn es keine Printables mehr holen soll. 78/89 Die getPrintable() Methode liefert nur ein Printable zurück das ich als nächstes erkläre: Es wird dabei nur das Graphics2D objekt um ImageableX und ImageableY verschoben (translate) und dann wird das bild auf(0/0) gezeichnet. Wichtig: 0/0 ist jetzt ImageableX/ImageableY durch das translate wird der 0/0 punkt verschoben 79/89 Was ist Android?, Aufbau der Architektur, AVD-Emulator, Zusammenhänge: Activity, View, Event, Intent, GUIs mit Android, File und Dateizugriff, Anwendungen auf‘s Handy bringen. Was ist Android? Android ist ein offenes und freies Betriebssystem, welches von der Open Handset Alliance (OHA) entwickelt und kostenlos zur Verfügung gestellt wird. Hauptsächlich wird es auf Mobiltelefonen und Tablets verwendet, aber auch Wearables (Uhren, Brillen, …) basieren zunehmend auf diesem OS. Nebem dem Betriebssystem, welches auf einem Linux-Kernel aufbaut, werden auch gleich die notwendigen Hardware-Treiber und umfassende Standard-Apps als “Gesamtpaket” mitgeliefert. Da Android sehr modular aufgebaut ist, ermöglicht dies dem Entwickler, einzelne Komponenten des Systems (wie den Launcher, Mailprogramme, …) problemlos gegen eigene Applikationen auszutauschen. Aufbau der Architektur Android ist als Schichtenmodell konzipiert und in 4 Ebenen unterteilt: (von oben nach unten) 4. Applications 3. Application Framework 2. Libaries + Android Runtime 1. Linux Kernel 1. Linux Kernel Der Linux-Kernel bildet die Basis von Android und kümmert sich um die Prozesskommunikation und die Prozessverwaltung. Außerdem sind auf dieser Ebene die notwendigen Hardware-Treiber vorhanden, so dass diese eine Schnittstelle zwischen Software und Hardware darstellen und sich bequem von den darüber liegenden Schichten ansteuern lassen. 2. Libaries + Android Runtime Diese Schicht ist in 2 Bereiche unterteilt, einerseits in die Libaries (= Systembibliotheken) und andererseits in die Android Runtime (= Android Laufzeitumgebung). Libaries / Systembibliotheken Hier sind die Basisbibliotheken beheimatet, welche meist in C oder C++ geschrieben sind und den Entwicklern fertige Methoden und Eigenschaften bereitstellen. Ein Beispiel hierfür sind der Browser(Webkit), Datenbanken (SQLite), Verschlüsselung, Touch-Gesten oder Grafik und Multimedia. Android Runtime/Laufzeitumgebung Hier ist die Dalvik Virtuelle Maschine beheimatet, welche ähnlich wie die JAVA Virtuelle Maschine dafür sorgt, dass der Java Code interpretiert wird. Jede App, die der Benutzer am Smartphone ausführt, wird in einer eigenen Instanz einer DVM gestartet. Dadurch läuft jede App als ein Betriebssystemprozess mit eigenen Speicherbereich und somit ist eine höhere 80/89 Sicherheit/Verfügbarkeit der App gewährleistet. Sollte eine App abstürzen, funktioniert der Rest problemlos weiter. 3. Application Framework (Programmierschnittstelle) Eine Schicht weiter oben liegt das Application Framework. Hier werden von Android viele Programmierschnittstellen zur Verfügung gestellt, welche es den einzelnen Anwendungen erlauben, dass sie miteinander kommunizieren. Beispiele sind: ● Activity Manager (verwaltet Anwendungen und steuert deren Lebenszyklus) ● Dienste für Telefonie ● Ortung ● Notification Manager (Informationen werden in der Statusleiste gesammelt/angezeigt) ● View System (also Eingabefelder, Textboxen, Buttons, …) Besonders ist hier der Activity Manager hervorzuheben, welcher sich um alles kümmert, das mit der Anzeige der Activity zu tun hat. “Surft” man durch die Activity und betätigt beispielsweise die “Zurück”-Taste, so wird automatisch die vorherige Activity aufgerufen, ohne dass sich der Entwickler darum kümmern muss. 4. Applications (Anwendungsschicht) Auf dieser obersten Ebene werden die Anwendungen (= Apps) installiert. Dies ist die Fläche, welche der Anwender letztendlich zu Gesicht bekommt. All diese Apps (Kamera, Kalender, Mails, Browser, …) sind i.d.R. in JAVA programmiert. AVD-Emulator Mithilfe des AVD-Emulators lassen sich unter Windows/... virtuelle Androidgeräte erzeugen und somit die Android-Oberfläche emulieren. Dazu notwendig sind die Bibliotheken, welche über den SDK Manager bezogen werden können. Der AVD-Emulator ist dann sinnvoll, wenn man kein Android-Gerät zur Verfügung hat, aber dennoch gerne auf ein Androidsystem zugreifen möchten, zB aus Testzwecken. Vor allem selbst entwickelte Apps lassen sich auf Funktionstüchtigkeit und Benutzerfreundlichkeit der GUI prüfen. Allerdings ist zu beachten, dass der Emulator viele Ressourcen benötigt und daher meist sehr langsam ist. Zudem werden nicht alle Hardwarekomponenten bzw. -funktionen emuliert oder unterstützt (zB GPS, …). Daher kann man ihn nur bedingt zum Testen einsetzen. Ein schnellerer Emulator ist geny-Motion. Grundbausteine Activity Repräsentiert ein sichtbares Benutzerinterface, also das, was der Benutzer am Bildschirm angezeigt bekommt. Eine Applikation kann aus mehreren "Activitys" bestehen. Jedoch kann im Lebenszyklus einer Applikation immer nur eine "Activity" zur selben Zeit aktiv sein. Das bedeutet, wenn man in Activity A die Activity B aufruft, A auf den Back-Stack gelegt wird, bis dass B wieder geschlossen wird 81/89 und A erneut angezeigt wird. Jeder Activity muss ein Layout-File (XML-File) zugeordnet werden und jede Activity muss in der Manifest-Datei vermerkt sein. (helf): bsp-code View Ist ein bestimmtes GUI-Element (Tabelle, Button, Text, …), welches von der Activity über eine eindeutige ID angesprochen und verändert werden kann (eingefärbt, befüllt, …). Es kann entweder in einem XML-File beschrieben werden oder programmtechnisch (also durch Java-Code) erzeugt werden. 82/89 Event Werden ausgelöst, sobald der Benutzer mit dem Smartphone interagiert (z.B. einen Button drücken, Text eingeben, …) oder andere Ereignisse (Anrufe, SMS, …) auftreten. Diese Events können überwacht werden, indem man zuvor einen entsprechenden Listener registriert. Sobald ein Listener anschlägt (z.B. empfang eine SMS), wird der sich in der Listener-Methode befindliche Code ausgeführt. Intent Rufen andere Activity auf und können Parameter zwischen 2 Apps austauschen. Zum Beispiel in einer eigenen entwickelten App die Kamerafunktion aufrufen oder ein Spielergebnis per SMS mit anderen Teilen. GUIs mit Android GUIs in Android zu erstellen kann generell über 2 Methoden erfolgen. Entweder über ein XML-File (Standardweg) oder programmtechnisch. In jedem Fall muss aber zumindest pro Activity 1 Layout-XML-File mit eindeutiger ID vorhanden sein. In diesem Layout-File können nun die verschiedenen Layouts eingefügt werden: ● Linear Layout alles wird untereinander (horizontal) oder nebeneinander (vertical) angeordnet ● Relative Layout die genaue Position eines Elements wird angegeben (10px von rechtem Rand) ● Table Layout ist eine Tabelle, bei der beliebig viele Zeilen erstellt werden können, welche anschließend mit Inhalten befüllt werden können. Jedes dieser Layouts kann beliebig viele Layouts als Unterelemente haben. Man kann aber auch normale Layoutkomponenten in ein Layout einfügen. Mit jeder neuen Android-Hauptversion wächst auch die Anzahl der Layoutkomponenten, so dass es für beinahe jeden Anlass etwas gibt: Buttons, Textfelder (nur Anzeige von Text), veränderbare Textfelder (Anzeige + Eingabe), Checkboxen, RadioButtons, TextView, Picker (zur Auswahl von Datum, Zeit, …), Fortschrittsanzeige, etc. Jede dieser Komponenten kann wieder programmtechnisch zur View hinzugefügt werden oder statisch im XML-File eingetragen werden. Dabei ist zu beachten, dass die Komponenten später über eine ID angesprochen werden müssen, um sie zu ändern oder die eingegeben Werte abzurufen. File / Dateizugriff Da Android auf Java aufbaut, ist auch der Filezugriff halbwegs ident. Der einzige Unterschied besteht darin, dass man verschiedene Speicherbereiche zur Verfügung hat: ● internal storage immer vorhanden, auf die Daten kann nur über deine App zugegriffen werden, wenn die App deinstalliert wird, werden auch die Daten gelöscht ● external storage auf die Dateien kann Systemweit zugegriffen werden, sie werden auch bei einer Deinstallation nicht gelöscht 83/89 Mithilfe des Names und des Pfad kann nun die Datei verwendet werden: Verzeichnis holen → File öffnen → Reader/Writer setzen → lesen schreiben → Reader/Writer schließen → File schließen Beispiel // Verzeichnis holen File verzeichnis = context.getDir("testDir", MODE_PRIVATE); // Referenz auf File holen test = new File(verzeichnis, "test.txt"); // Reader öffnen FileInputStream inputStream = new FileInputStream(test); BufferedReader fileInReader = new BufferedReader(new InputStreamReader(inputStream)); // Eine Zeile einlesen und Ausgeben System.out.println(fileInReader.readLine()); // Reader schließen fileInReader.close(); ACHTUNG: in der Manifest muss die Berechtigung gesetzt sein!! <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> Anwendungen aufs Handy bringen Fertig entwickelte Apps werden über sogenannte Marketplaces an die Anwender verteilt. Allem voran ist hier der Google Play Store als größer Appmarkt für Android zu erwähnen. Die fertige App muss vom Entwickler mit einer Signatur versehen werden, wird dann anschließend in den Google Play Store hochgeladen und kann nach positivem Abschluss diverser Prüfungen (Malware, Inhalte, …) verteilt werden. Für Apps, welche sich noch in der Entwicklungsphase befinden, ist ein anderer Weg möglich. Man verbindet das Smartphone/Tablet mit dem PC und installiert die nötigen Treiber. Anschließend aktiviert man auf dem Gerät die “Debugging-Funktion”, damit man die Konsolenaushaben unter Eclipse einsehen kann. Nun wird in Eclipse auf “Ausführen” geklickt, danach wird ausgewählt, auf welchem Gerät die App installiert werden soll. Eine gleiche oder höhere Androidversion auf Gerät wie in Manifestdatei ist nötig. Ein anderer Weg wäre noch, eine .apk-Datei zu erstellen, diese manuell aufs Gerät zu spielen und dort zu installieren (“Installation von Fremdquellen” muss aktiviert sein). 84/89 Logging und Unit-Testing in Java Einsatz von Logging?, Log Levels, Logger Objekt, Handler, Formatter, Beispiel, Was sind Unit-Tests?, Wie werden sie eingesetzt?, JUnit, Prinzipien, Nenne und erkläre die wichtigsten Annotationen Allgemeines zu Logging Das Loggen (Protokollieren) von Informationen über Programmzustände ist ein wichtiger Teil, um später den Ablauf und Zustände von Programmen rekonstruieren und verstehen zu können. Mit einer Logging-API lassen sich Meldungen auf die Konsole oder in externe Speicher wie Text- oder XML-Dateien bzw. in eine Datenbank schreiben oder über einen Chat verbreiten. Des Weiteren können so auch Fehler protokolliert werden und somit die Fehlerquelle gefunden werden. Logging-APIs Bei den Logging-Bibliotheken und APIs ist die Java-Welt leider gespalten. Da die JavaStandardbibliothek in den ersten Versionen keine Logging-API anbot, füllte die Open-SourceBibliothek log4j schnell diese Lücke. Sie wird heute in nahezu jedem größeren Java-Projekt eingesetzt. Als Sun dann in Java 1.4 eine eigene Logging-API (JSR-47) einführte, war die JavaGemeinde erstaunt, dass java.util.logging (JUL) weder API-kompatibel zum beliebten log4j noch so leistungsfähig wie log4j ist. Kurz gesagt gibt es eine Standardbibliothek (java.util.logging) von SUN für Logging, dennoch wird meist die Open-Source-Bibliothek log4j benutzt, denn diese hat sich etabliert und ist leistungsfähiger. Log Levels Loglevels stellen verschiedene Dringlichkeitsstufen für die jeweiligen Nachrichten dar. Das Log Level kann also als zentrale Steuerung gesehen werden, die steuert welche Nachricht von welcher Klasse mit welcher Dringlichkeit mitprotokolliert wird. Die logging.properties Datei liegt im Java Installationsverzeichnis im lib Ordner. Log Levels in absteigender Ordnung ● ● ● ● ● ● ● SEVERE (höchste Dringlichkeit) WARNING INFO CONFIG FINE FINER FINEST (niedrigste Dringlichkeit) Das Log Level muss natürlich auch gesetzt werden. LOGGER.setLevel(Level.INFO) //Level auf INFO setzten 85/89 Das Logger-Objekt Das zentrale Logger-Objekt wird über Logger.getAnononymousLogger(), oder über Logger.getLogger(String name) geholt. Wobei für name in der Regel der voll qualifizierte Klassenname steht. In der Regel ist der Logger als private static final in der Klasse deklariert. Beispiel private static final Logger jlog= Logger.getLogger(“test”); jlog.log(Level.SEVERE, “Nachricht”); Ausgabe: 11.01.2007 14:41:48 test <clinit> SCHWERWIEGEND: Nachricht Imports ● ● java.util.logging.Level java.util.logging.Logger Handler Jeder Logger kann mehrere Handler haben. Ein Handler kann mit setLevel() ein- und mit setLevel(Level.OFF) ausgeschaltet werden. Es gibt verschiedene Handler: ● ConsoleHandler (Schreibt die log Nachricht in die Konsole) ● FileHandler (Schreibt die log Nachricht in ein File) ● StreamHandler (Schreibt die log Nachricht an einen OutputStream) ● SocketHandler (Schreibt die log Nachricht an TCP Ports) ● MemoryHandler (Buffert die log Nachrichten in den Speicher) Wobei ab LogLevel INFO automatisch an die Konsole gemeldet wird. Beispiel zum setzen eines FileHandlers Handler fileHandler = new FileHandler("log.txt"); log.addHandler(fileHandler); Formatter Jede Handler-Ausgabe kann mittels eines Formatierers konfiguriert werden. Der Formatter wird sozusagen auf den Handler gesetzt. Dies geschieht mit der Methode setFormatter: fileHandler.setFormatter(new SimpleFormatter()); Was sind Unit-Tests Unit-Tests sind Tests möglichst kleiner Code-Stücke. Also am besten einzelne Funktionen/Methoden. Im Deutschen werden sie auch “Modultest” oder “Komponenten-Test” genannt, da wirklich immer 86/89 nur Code Stücke/Module/Komponenten getestet werden. Ganz im Gegensatz zum Integrations- und Systemtest. Inhalt von Unit-Tests ● ● ● Logisch: 1 Testfall pro Verhalten einer Funktion aber auch alle Sonderfälle, Randfälle und Fehlerfälle Technisch: 1 Testfall pro möglichen Codepfad Weiters: 1 Testfall für jeden bekannten Bug: Hilft dem Entwickler beim Reproduzieren Testet die Behebung des Bugs Schützt davor, dass sich derselbe Bug in Zukunft nochmals einschleicht Was ist JUnit Junit ist ein Test-Framework. Es bietet Klassen an um geschriebenen Quelltext leicht zu prüfen. Außerdem verlangt JUnit während des Testen keine Benutzerinteraktion und ist einfach anzuwenden, aber verlangt dafür ein wenig Disziplin. Prinzipien ● ● ● Erst denken, dann programmieren. Erst testen, dann programmieren. “Test a little, write a little, test a little, write a little, ….” JUnit ersetzt jedoch nicht das Schreiben der Testklassen! Wie werden JUnit Tests eingesetzt Für JUnit Tests werden eigene Testklassen geschrieben, welche mit Annotationen gekennzeichnet werden (Dazu mehr im nächsten Unterpunkt). Die Tests ansich werden mit sogenannten Assertions durchgeführt. Assertions assertEquals(Object expected, Object actual) Vergleicht zwei Objekte auf Gleichheit (Vergleichbar mit equals) assertEquals(double expected, double actual, double delta) Vergleicht zwei double Werte. Mit delta kann eine Abweichung angegeben werden assertSame(Object expected, Object actual) Vergleicht zwei Objekte auf totale Gleichheit (Vergleichbar mit == ) 87/89 assertTrue(boolean expression) Liefert true wenn der Parameter true ist assertFalse(boolean expression) Liefert false wenn der Parameter true ist assertNull(Object actual) Liefert true wenn der Parameter null ist assertNotNull(Object actual) Liefert true wenn der Parameter nicht null ist Annotationen @Test Testmethoden werden mit der @Test Annotation markiert. @Test public void addition() { assertEquals(12, simpleMath.add(7, 5)); } //Liefert true @Before und @After Methoden die mit @Before oder @After markiert werden, werden entweder vor oder nach jedem Testfall durchlaufen. @Before public void runBeforeEveryTest() { //Wird vor jeden Testfall durchlaufen simpleMath = new SimpleMath(); } @After public void runAfterEveryTest() { simpleMath = null; } //Wird nach jeden Testfall durchlaufen @BeforeClass und @AfterClass: Methoden die mit @BeforeClass und @AfterClass markiert sind, werden einmal vor bzw. nachdem alle Testfälle durchlaufen sind ausgeführt. @BeforeClass public static void runBeforeClass() { // run for one time before all test cases } @AfterClass public static void runAfterClass() { 88/89 // run for one time after all test cases } 89/89