Elmars Musterlösungen der „Textaufgaben“ (Blatt 1-10) Blatt 1 Es gibt im wesentlichen drei verschiedene Parameterübergabe-Methoden: Call-by-value: Argumente werden zunächst ausgewertet. Die resultierenden Werte werden dann an die Parametervariablen übergeben. Änderungen der Werte dieser Parameter innerhalb der Prozedur haben keine Auswirkung auf die im aufrufenden Kontext vorhandenen Argumente. Falls die Parameterwerte selbst Referenzen sind (z.B. Objekte in Java) haben Änderungen von Werten, auf die diese Parameter verweisen, allerdings Rückwirkungen auf den aufrufenden Kontext. Java unterstützt nur call-by-value. Allerdings kann man durchaus die Semantik von call-by-reference "simulieren", indem man z.B.: - Aggregate (d.h. Objekte aus mehreren Komponenten) zurückliefert: statt: nun: PROCEDURE getDimension(VAR width, height : INTEGER); Dimension getDimension(); mit class Dimension { public int width, heigth; } - Referenzen auf Objekte (z.B. Arrays) übergibt, deren Inhalt(!) innerhalb der Prozedur verändert werden kann: statt: nun: PROCEDURE getDimension(VAR width, height : INTEGER); void getDimension(int width[], int height[]); void getDimension(Dimension dim); // wird gefüllt - Fehler auf andere Arten als den Rückgabewert berichtet (z.B. als Exceptions, kommt noch im Laufe der Vorlesung) Call-by-reference: Die Referenz (Adresse) der Argument-Variablen wird an die Prozedur übergeben. Ausführung der Prozedur kann Seiteneffekte auf die Werte der Argumente im aufrufenden Kontext haben (wenn Zuweisungen an die Parameter erfolgen). Der Variablenname innerhalb der Prozedur ist praktisch ein "Alias" für den Namen der Argument-Variablen (das Argument muss eine Variable sein!), dieselbe Speicherzelle ist also unter mehreren Namen erreichbar. Call-by-name: Argumentausdrücke werden unausgewertet (als Referenz auf Code, der den Wert des Arguments liefert, zusammen mit einer Umgebung für die Werte von freien Variablen) übergeben. Die Auswertung der Argumentausdrücke findet erst bei Verwendung innerhalb der Prozedur statt (dabei kann es Probleme mit Mehrfachauswertung geben). Auch komplexe Argumentausdrücke sind erlaubt. Call-by-reference ist eigentlich nur ein Spezialfall von call-by-name (nur eine Variablenname statt beliebigem Ausdruck als Argument). Wie beschaffen Sie sich auf einem Unix-System Informationen über Kommandos? (Unix wird in der Klausur nicht verlangt, Anm. d. R.) Es gibt im wesentlichen zwei Informationsquellen: man: Anzeigen der Dokumentation zu einem Programm. Es gibt verschiedene Kapitel (1-8), spezielle Abfrage ist z.B. mit `man 1 date' möglich. Normalerweise sieht man nur den ersten Treffer, `man -a ...' zeigt die gefundenen Seiten aus allen Kapiteln (hintereinander) an. Wichtige Kapitel sind: 1 Benutzer-Kommandos 6 Spiele :-) info: Menügeführtes Informationssystem mit Baumstruktur und Querverweisen Ähnliche Informationen wie `man', aber meist besser strukturiert. Zum Drucken eher weniger geeignet. Zu empfehlen sind grafische Versionen wie `tkinfo'. Welche Informationen liefert Ihnen apropos? apropos: Suche nach einem Schlüsselwort in Index aller Manualseiten. Gibt Liste der Fundstellen aus. `apropos' ist immer dann hilfreicht, wenn man nach einen Kommando für eine bestimmte Aufgabe sucht. Blatt 2 Klasse, Objekt, Nachricht und Methode: Ein Objekt ist eine Datenstruktur, die Zustand (Daten) verkapselt und Funktionen (Methoden) zur Verfügung stellt, um diesen Zustand ansehen bzw. verändern zu können. Die Komponenten der Datenstruktur nennt man Instanzvariablen oder Instanzfelder. Eine Klasse ist praktisch eine Beschreibung von Objekten mit gleicher Struktur und gleichen Methoden, entspricht also etwa einem Typ. Die Klasse legt die Struktur und die Methoden der Objekte fest. Ein Objekt nennt man eine Instanz einer Klasse. In Java ist eine Klasse selbst auch ein Objekt ("Klassenobjekt"). Eine Nachricht ist (im Sprachgebrauch der Objekt-Orientierung) eine "Aufforderung" an ein Objekt, eine Aktion auszuführen. In Java bedeutet das typischerweise einen Methodenaufruf bei dem betreffenden Objekt. Eine Methode ist praktisch eine Funktion, die jedoch an ein bestimmtes Objekt gerichtet ist. Sie ändert häufig den Zustand (die Daten) des Empfängers und kann je nach Empfänger verschieden wirken (Polymorphie). Bedeutung der Schlüsselworte static, final (bei Variablen) und this: Das Schlüsselwort `static' bei Komponenten einer Klasse (z.B. Variablen, Methoden oder inneren Klassen) markiert, daß diese mit der Klasse als Ganzem assoziiert sind und nicht mit einzelnen Instanzen dieser Klasse. -> "Klassenvariable" oder "Klassenfeld", "Klassenmethode" Eine Variable, die als `final' deklariert worden ist, hat in Java etwa den Stellenwert einer Konstanten. Der erste im Programmverlauf an die Variable zugewiesene Wert ist sozusagen "entgültig", d.h. er darf nicht mehr durch nachfolgende Zuweisungen an die Variable geändert werden. Innerhalb einer Instanzmethode (bzw. einem Konstruktor) bezeichnet `this' das Empfängerobjekt der gerade ausgeführten Methode, also das Objekt, bei dem die gerade aktive Methode aufgerufen wurde. Über diese Referenz können wie üblich alle (sichtbaren) Komponenten des Objektes und seiner Klasse erreicht werden. Referenztypen in Java: Es gibt in Java die Unterscheidung zwischen den primitiven (einfachen, skalaren) Datentypen und den Referenztypen (Objekt-wertigen Typen). Während eine Variable eines einfachen Datentyps direkt den spezifischen Wert enthält, wird in einer Variablen eines Referenztyps nur ein Verweis ("Referenz", "Objekt-ID") auf ein - an anderer Stelle abgespeichertes Objekt abgelegt. Mehrere Variablen können auf dasselbe Objekt verweisen. einfache Typen Referenztypen -----------------------------------------------------------------------Werterzeugung durch Zuweisung Werterzeugung mit new(), automatische Freigabe durch den Garbage-Collector Kopie des Wertes bei Zuweisung oder Parameterübergabe "Kopie" der Referenz bei Zuweisung oder Parameterübergabe, Referenzen verweisen aber auf dasselbe Objekt kein explizites Kopieren nötig echte Kopie des Objektes mit clone() Vergleich der Werte mit `==' Vergleich der Referenzen ("IDs") mit `==', Inhaltsvergleich mit equals() ("Äquivalenz von Objekten") keine Beziehungen zwischen Typen Typen sind hierarchisch organisiert Arrays sind in Java immer als Objekte repräsentiert, werden also stets als Referenzen (rechte Spalte) behandelt. Bedeutung der Methoden equals() und clone(): "Identität" von Objekten: Zwei (oder mehr) Objekt-Referenzen verweisen auf exakt dasselbe Objekt. Beispiel: Object a = new Object(); Object b = a; Dann gilt: `a == b' (und natürlich auch `a.equals(b)') "Äquivalenz" von Objekten: Zwei (oder mehr) Objekt-Referenzen verweisen auf (möglicherweise verschiedene) Objekte mit "gleichem" (im Sinne der equals()-Funktion) Inhalt. Beispiel: Integer a = new Integer(5); Integer b = new Integer(5); Dann gilt: `a.equals(b)', aber nicht `a == b' `equals()' bei Object vergleicht nur die "Objekt-IDs", prüft also auf Identität. `equals()' bei String z.B. vergleicht dagegen die Inhalte der Strings auf Gleichheit (gleiche Zeichenfolgen). Die Methode clone() dient zum Kopieren von Objekten. Nur Instanzen, deren Klasse das (leere) Interface `Cloneable' implementieren, können geklont (dupliziert) werden. Die Implementierung von clone() in der Klasse Object zieht eine flache Kopie. Bei einer flachen Kopie (shallow copy) werden bei Referenzen auf andere Objekte in dem zu kopierenden Objekt einfach nur die Referenzen kopiert. Bei einer tiefen Kopie (deep copy) degegen würden alle referenzierten Objekte selber auch (rekursiv) kopiert. Da clone() in Object leider als `protected' deklariert ist, muß man in einer eigenen Klasse, die Cloneable sein soll, nicht nur das leere Interface implementieren, sondern auch noch die clone() Methode als `public' definieren. In der Implementierung darf man dann natürlich super.clone() die Arbeit machen lassen. Wichtige Begriffe aus der objekt-orientierten Programmierung: Klassenmethode: Eine Methode, die nicht an eine einzelne Instanz einer Klasse gerichtet ist, sondern an die Klasse als Ganzes. Daher ist der "Empfänger" der Methode (`this') innerhalb einer Klassenmethode nicht verfügbar. Eine Klassenmethode kann über den Namen der Klasse (Klasse.methode()) oder auch über einen Objektverweis (objekt.methode()) aufgerufen werden (was aber kein guter Programmierstil ist). Klassenmethoden werden mit dem Schlüsselwort `static' vereinbart. Instanzmethode (Objektmethode): Eine Methode, die an ein bestimmtes Objekt (eine Instanz) einer Klasse gerichtet ist. Eine Instanzmethode kann nur über einen Objektverweis aufgerufen werden (objekt.methode()), dieses Objekt ist dann im Körper der Methode über den Namen `this' anzusprechen. Konstruktor (Initializer): Ein Konstruktor ist eine (Instanz-)Methode, die implizit beim Erzeugen eines neuen Objektes aufgerufen wird und typischerweise die zu dieser Klasse gehörigen Instanzvariablen mit Anfangswerten füllt. Damit das Objekt vollständig initialisiert wird, muß als erste Anweisung in jedem Konstruktor entweder ein weiterer Konstruktor derselben Klasse oder ein Konstruktor der Basisklasse aufgerufen werden (wenn das fehlt, erzeugt Java automatisch einen `super()'-Aufruf). Ein Konstruktor liefert kein Resultat und man deklariert auch keinen Rückgabetyp, er hat immer den Namen der Klasse. Klassenvariable: Eine Variable, die nicht an eine einzelne Instanz einer Klasse gebunden ist, sondern an die Klasse als Ganzes, damit also (anders als eine Instanzvariable) nur genau einmal vorhanden ist. Eine Klassenvariable kann über den Namen der Klasse (Klasse.variable) oder über einen Objektverweis (objekt.variable) angesprochen werden (auch hier kein guter Programmierstil). Klassenvariablen werden mit dem Schlüsselwort `static' vereinbart. Instanzvariable: Eine Variable, die an ein Objekt einer Klasse gebunden ist und damit für jede neue Instanz der Klasse neu erzeugt wird (jedes Objekt einer Klasse hat seine eigenen Instanzvariablen). Eine Instanzvariable kann nur über einen Objektverweis (objekt.variable) angesprochen werden. Vererbung: Damit bezeichnet man die Möglichkeit, eine neue Klasse von einer anderen, bereits existierenden Klasse "abstammen" zu lassen (`extends'). Das bedeutet, daß die neue Klasse (Unterklasse, Subklasse) Zustand bzw. Eigenschaften (d.h. Instanzvariablen) und Verhalten (d.h. Methoden, jedoch keine Konstruktoren) der Oberklasse (Basisklasse) übernimmt. Eine Unterklasse kann dabei selektiv einzelne Methodenimplementierungen ersetzen sowie neue Methoden und Klassen- bzw. Instanzvariablen hinzufügen. Lebenszyklus eines Objektes: x = new A(); Speicherbereich für das Objekt wird reserviert. Konstruktoren werden aufgerufen (durch die explizite oder implizite Konstruktorverkettung: von der Klasse A aus nach oben entlang der Klassenhierarchie). Dabei werden in jeder Klasse vorhandene Initialisierungsblöcke für Instanzen oder direkte Initialisierungen für Instanzvariablen quasi "als Teil des Konstruktors" unmittelbar nach dem super()Aufruf ausgeführt. Die Konstruktorverkettung garantiert, daß Oberklassen stets vor ihrem Unterklassen initialisiert werden. .... x = null; Sofern keine weiteren Referenzen auf das Objekt mehr existieren, ruft der Garbage-Collector bei diesem Objekt die finalize()-Methode auf. finalize()-Aufrufe müssen explizit nach oben verkettet werden (super.finalize()). Falls durch den finalize()-Aufruf wieder eine Referenz entsteht, bleibt das Objekt am Leben, sonst verschwindet das Objekt, und der Speicher wird wieder freigegeben. Blatt 3 Bedeutung von `final' bei Klassen und Methoden: Eine Klasse, die mit dem Schlüsselwort `final' deklariert worden ist, darf nicht mehr abgleitet werden (d.h. es kann keine Unterklassen geben). Im Gegenzug kann der Java-Compiler (möglicherweise) für Methodenaufrufe bei solchen Klassen effizienteren Code erzeugen. Analog dazu kann man auch einzelne Methoden einer Klasse als `final' deklarieren, was dann zur Folge hat, daß diese Methoden in Unterklassen nicht überschrieben werden dürfen. Auch hier könnte der Java-Compiler effizienteren Code erzeugen. Eine kleine Warnung: Man sollte sich allerdings beim Design einer Klasse nicht sonderlich auf die Ausführungsgeschwindigkeit des Programmcodes konzentrieren, sondern eher auf eine möglichst flexible Wiederverwendbarkeit (was eher gegen die Verwendung von `final' spricht). Wenn überhaupt, sollte man `final' bei Klassen und Methoden nur sparsam (und gut überlegt) verwenden. Überschreiben vs. Überdecken, "dynamic method lookup": In Java kann eine Unterklasse sowohl Methoden als auch Variablen (Felder), die in der Oberklasse bereits definiert sind, neu vereinbaren. In einem solchen Fall muss man zwei Verhaltensweisen unterscheiden: Überschreiben (overriding): Instanzmethoden (und nur diese!) werden dabei "überschrieben" (präziser formuliert: die aus der Oberklasse geerbte Implementierung der Methode wird überschrieben). Das bedeutet, das Objekt kennt zu jeder definierten Instanzmethode nur eine Implementation und wird diese immer verwenden, wenn diese Methode auf das Objekt angewendet wird. Bei dem Aufruf einer Instanzmethode wird zur Laufzeit für den "realen" Typ des Objektes die möglichst spezifische Implementierung der Methode verwendet, unabhängig vom Typ der Referenz, über den der Aufruf erfolgt. (In Java kann allerdings ein Methodenaufruf syntaktisch nur über eine Referenz auf einen Typ erfolgen, der die entsprechende Methode auch deklariert.) Überdecken (shadowing): Klassenmethoden sowie Felder (Klassen- und Instanzvariablen) werden dagegen nur "überdeckt", d.h. die neue Vereinbarung in der Unterklasse verdeckt die aus der Oberklasse geerbte Definition für den Namen. Beide Definitionen sind aber vorhanden und können alternativ verwendet werden. Maßgeblich für die Auswahl ist dabei der Typ der Referenz, über die der Zugriff auf die Klassenmethode bzw. das Feld erfolgt (siehe das Beispiel dazu im Skript). Der "reale" Typ des Objektes zur Laufzeit hat keine Bedeutung! Polymorphismus/Polymorphie, Overloading: Polymorphie (man spricht auch von Polymorphismus oder "Typ-Polymorphie") bezeichnet allgemein die Möglichkeit, Werte verschiedener konkreter Typen (in Java: Klassen) unter einem gemeinsamen, weniger spezifischem Typ zusammenzufassen. In Java z.B. kann man Referenzen auf Objekte beliebiger Klassen in einer Variablen von Typ `Object' speichern. Polymorphie wird häufig für die Parametertypen von Methoden eingesetzt, um Programmcode allgemein formulieren zu können, der kein Wissen über den konkreten Typ eines Parameters benötigt. Overloading bezeichnet dagegen die Möglichkeit, für dasselbe "Symbol" (in der Regel ein Methodenname) mehrere Implementierungen zu hinterlegen. Beim Aufruf wird dann je nach Anzahl der Parameter oder der verwendeten Typen die dazu passende Implementierung ausgewählt. Diese Auswahl findet schon bei der Übersetzung statt, d.h. es zählen dabei bei Objekten nur die Typen der Referenzen, nicht die der "realen" Objekte zur Laufzeit. Wenn der Compiler nicht eindeutig eine Implementierung auswählen kann, gibt es einen Fehler bei der Übersetzung. Operator instanceof: Der Operator `instanceof' kann für zwei unterschiedliche Zwecke benutzt werden (der zweite Fall ist in der Vorlesung noch nicht behandelt worden): - um zu prüfen, ob ein Objekt eine Instanz einer bestimmten Klasse (oder einer Unterklasse dieser Klasse) ist: Objektreferenz instanceof Klassenname - um zu prüfen, ob ein Objekt (bzw. seine Klasse) die Methoden eines bestimmten Interfaces implementiert: Objektreferenz instanceof Interfacename Beispiel (zum ersten Fall): public boolean equals (Object value) { return value instanceof Complex && // is Complex? ((Complex) value).re == this.re && // equal values? ((Complex) value).im == this.im } Pakete in Java, import-Anweisung: Klassen werden in Java häufig in Paketen organisiert, um eine bessere Strukturierung zu erreichen und Zugriffsrechte einzuschränken (Felder ohne Sichtbarkeits-Modifikator sind z.B. nur innerhalb desselben Pakets sichtbar). Um mehrere Klassen zu einem Paket zusammenzufassen, müssen: - alle Klassen dieses Pakets im selben Verzeichnis leben - und alle Dateien, die zum Paket gehören sollen, als erste Anweisung (nach Kommentar) die Anweisung `package <paketname>;' enthalten. Achtung: - Der Verzeichnisname muss exakt dem gewählten Paketnamen entsprechen! - Wenn der Paketname aus mehreren Komponenten besteht (wie `java.io'), muss es eine entsprechende Verzeichnis-Hierarchie geben, also z.B.: java/io/InputStream.java java/io/OutputStream.java ... - Die `package'-Anweisung gilt für alle innerhalb der Datei definierten Klassen (das kann mehr als eine sein). - Die Klassennamen der entsprechenden Java-Klassen sind dann jeweils <paketname>.<klassenname>, allerdings darf (nur) innerhalb des Pakets der Paket-Präfix weggelassen werden. Beim Aufruf einer Klasse aus dem Paket über das Java-Kommando ist z.B. in jedem Fall der volle Name anzugeben. - Um eine Klasse zu laden, wird ein voll-qualifizierter Klassenname von Java wieder in den entsprechenden Pfad umgewandelt (also my.pkg.Foo in my/pkg/Foo.class) und dann relativ zu den Katalogen im CLASSPATH gesucht. Wenn also der CLASSPATH z.B. auf den Katalog /home/elmar/java zeigt, würde die Klasse my.pkg.Foo an folgender Stelle gesucht: /home/elmar/java/my/pkg/Foo.class Normalerweise setzt man in einem Makefile für ein Projekt den CLASSPATH (mindestens) auf die Wurzel der eigenen Paket-Pfade. Die `import'-Anweisung dient in Java nur zum Abkürzen der Schreibweise von Klassennamen (d.h. erlaubt die Verwendung eines Klassennamens ohne den dazugehörigen Paket-Präfix): import my.pkg.Foo; [...] Foo foo = new Foo(); // meint jeweils my.pkg.Foo `import <paketname>.*' erlaubt die Verwendung von *allen* Klassennamen in einem Paket ohne dazugehörigen Paket-Präfix. Wenn ein Name dabei in mehr als einer import-Anweisung auftaucht, wird - je nach verwendeter Java-Version - einfach der "erste" genommen oder es gibt einen Fehler bei der Übersetzung. Man sollte die `*'-Syntax bei import eigentlich nicht verwenden (und lieber alle notwendigen Klassen einzeln aufzählen), um solche Probleme zu vermeiden. Blatt 4 Zum Java-Paket java.io: Wichtige (abstrakte) Basisklassen: java.io.InputStream java.io.OutputStream java.io.Reader java.io.Writer (Bytes lesen, möglicherweise positionieren) (Bytes schreiben, Ausgabepuffer leeren) (Zeichen lesen, möglicherweise positionieren) (Zeichen schreiben, Ausgabepuffer leeren) Viele Ströme (allerdings nicht die Basisklassen) enthalten Verweise auf andere Ströme, aus denen sie ihre Informationen lesen, bzw. an die sie ihre Ausgabe weiterleiten. Man kann eigene Ströme dieser Art konstruieren, indem man von java.io.FilerReader/Writer etc. ableitet. Byte/Character-Ströme: In Java werden Zeichen stets als 16-Bit Unicode-Zeichen repräsentiert. Daher muß man immer sorgfältig unterscheiden zwischen Byte-Ein/Ausgabe (Grafik, Sound, Zip-Archiv etc.) und Zeichen-Ein/Ausgabe (Strings, Text). Zeichen können mit fixer Länge (z.B. 2 Byte pro Zeichen) oder variabler Länge (z.B. UTF-8) kodiert sein. `System.in' ist ein Byte-Strom, da man damit die Möglichkeit hat, sowohl Bytes (InputStream) als auch Zeichen (mittels InputStreamReader) aus der Standardeingabe zu lesen. Wäre `System.in' ein Character-Strom, könnte man nur Zeichen lesen. Beispiele für Kombinationen von Strömen: new BufferedInputStream( new SequenceInputStream( new FileInputStream("..."), ...)) new BufferedReader( new InputStreamReader( new SequenceInputStream( new FileInputStream("..."), ...))) new PushbackInputStream( new BufferedInputStream( new SequenceInputStream( new FileInputStream("..."), ...))) new PushbackReader( new BufferedReader( new InputStreamReader( new SequenceInputStream( new FileInputStream("..."), ...)))) Blatt 5 Keine “Textaufgabe” in diesem Blatt Blatt6 Interface: Ein Interface ist im Prinzip einfach nur eine Sammlung von Methodenköpfen (Methodendeklarationen), deklariert also nur "Operationen", die von einer Klasse implementiert werden können. Ein Interface kann keine Methodendefinitionen (d.h. Implementierungen) beinhalten, es darf allerdings mit `final' markierte Klassenvariablen -- d.h. benannte Konstanten -- geben. Der Sinn eines Interfaces besteht darin, daß mehrere (nicht zwangsläufig verwandte) Klassen die Methoden eines Interfaces implementieren können. Objekte dieser Klassen können dann eine identische Schnittstelle anbieten. Interfaces können von anderen Interfaces "abgeleitet werden" (`extends'), das bedeutet dann, daß sie die Methoden des Ober-Interfaces mit enthalten. Ein Interface ist (wie eine Klasse) ein Typ-Name in Java, daß heißt, man kann ein Objekt einer Klasse, die ein bestimmtes Interface implementiert, auf den Typ des Interfaces "casten" (umwandeln). abstrakte Klasse und abstrakte Methode: Eine abstrakte Klasse ist eine noch unvollständige Klasse, in der nicht alle Methoden implementiert sein müssen. Daher kann man von einer solchen Klasse keine "Werte" (Objekte) erzeugen. Im Gegenzug darf eine abstrakte Klasse dafür Methoden nur deklarieren statt sie zu definieren: In diesem Fall steht vor dem jeweiligen Methodenkopf das Schlüsselwort `abstract' (das eine abstrakte Klasse bzw. Methode markiert), und an Stelle des Rumpfes steht nur ein `;'. Eine solche Klasse ist natürlich nur dann sinnvoll, wenn es eine (oder auch mehrere) "konkrete", d.h. nicht abstrakte, Unterklassen gibt, von denen dann später auch Objekte erzeugt werden können. Im Gegensatz zu einem Interface kann also eine abstrakte Basisklasse dann bereits schon Verhalten der Objekte (in Form von Methodenimplementierungen) festlegen. Blatt7 Innere Klassen: [Achtung: Dies ist nur eine kurze Zusammenfassung der wichtigsten Punkte. Alle Details kann (und sollte!) man in Kapitel 10 im Skript nachlesen.] Es gibt im wesentlichen zwei Kategorien von inneren Klassen: - static member classes (nested top-level classes) Diese verhalten sich im Prinzip ganz genauso wie gewöhnliche Klassen, werden aber im Rumpf einer anderen Klasse vereinbart, d.h. der Name der umschließenden Klasse wird vorne an den Klassennamen der inneren Klasse angefügt. Die innere Klasse hat Zugriff auf Felder der äußeren Klasse (ist aber nicht mit einer Instanz assoziiert, kann also nicht direkt auf Instanzfelder zugreifen), und die äußere Klasse hat Zugriff auf Felder der inneren Klasse. Analog zu einer inneren Klasse kann man so auch ein verschachteltes Interface vereinbaren. - member classes ("real" member classes) Die "echten" inneren Klassen in Java sind die nicht mit `static' vereinbarten inneren Klassen. Sie unterscheiden sich in wesentlichen Punkten von den top-level Klassen bzw. den static member classes: o Sie sind mit einer Instanz der umschließenden Klasse assoziiert (verbunden), d.h. sie können nur erzeugt werden. wenn es bereits ein Objekt der umschließenden Klasse gibt. Diese Assoziation wird beim Erzeugen des Objekts der inneren Klasse hergestellt, daher muß man manchmal (wenn etwas anderes als `this' verwendet werden soll) explizit eine äußere Instanz angeben: Enumeration enum = intlist.new Enumerator(); o Da jedes Objekt der inneren Klasse stets mit einem Objekt der umschließenden Klasse verbunden ist, kann die innere Klasse auch auf die Instanzfelder dieses Objekts der äußeren Klasse zugreifen (nicht nur auf die Felder der Klasse). o Solche Klassen dürfen selbst keine `static'-Komponenten enthalten (außer Konstanten mit `static final'). o Es ist nicht erlaubt, ein Interface als "echte" innere Klassen zu vereinbaren. Desweiteren gibt es noch zwei Spezialfälle von inneren Klassen, die sich eigentlich nur durch die Stelle der Deklaration im Programmtext unterscheiden (ansonsten aber die gleichen Eigenschaften haben): - lokale Klassen verhalten sich prinzipiell wie nicht-lokale innere Klassen, werden aber innerhalb eines Blocks vereinbart (d.h. ihr Name ist nur lokal sichtbar) und haben daher auch Zugriff auf alle in diesem Block sichtbaren mit `final' deklarierten Variablen und Parameter. Man kann (wie bei lokalen Variablen) keine Sichtbarkeitsmodifikatoren angeben. - anonyme Klassen sind eine weitere Variante der lokalen inneren Klassen, die aber in einem Ausdruck (mit spezieller Syntax) definiert werden. Zugriff auf `final' deklarierte Variablen und Parameter ist auch hier möglich. Eine anonyme Klasse hat keinen Namen und kann daher auch keine eigenen Konstruktoren definieren, sie erbt aber praktisch - ausnahmsweise die Konstruktoren ihrer Oberklasse (normalerweise werden Konstruktoren nicht vererbt und müssen in Unterklassen immer neu vereinbart werden). return new java.util.Enumeration() { ... }; String filelist[] = f.list(new java.io.FilenameFilter() { ... }); Syntaktisch wird Objekt-Erzeugung und Klassendefinition kombiniert, dabei wird entweder der Konstruktor-Aufruf einer Oberklasse angegeben (es wird dann aber ein Objekt der anonymen Unterklasse dieser Klasse erzeugt), oder es wird ein "Konstruktor" für ein Interface angegeben (in diesem Fall wird die anonyme Klasse von `Object' abgeleitet und implementiert dieses Interface). Hi.java: http://www.vorlesungen.uos.de/informatik/java00/html/skript/2__07.htmld/ enthält einige Zeichnungen und Erläuterungen zu dem Programm. Blatt 8 abstrakter Datentyp, Collection: Ein abstrakter Datentyp (ADT) ist eine Sammlung von Werten eines Typs (in Java typisch: Objekte), die in einer festen Struktur organisiert sind, und auf die bestimmte, festgelegte Operationen angewendet werden können (z.B. Stack mit push(), pop(), top(), isEmpty()). Die Funktionalität der Operationen wird abstrakt (häufig über mathematische Axiome) definiert. Abstrakt bedeutet dabei, daß zwar die Struktur (Stack, Menge, Baum) und das genaue Verhalten der Operationen festgelegt werden, aber keinerlei Aussage über die konkrete Art der Realisierung/Implementierung gemacht wird (vgl. `abstract' bei Methoden in Java). Eine Collection ist ein Beispiel für einen (sehr) abstrakten Datentyp, der zunächst kaum Aussagen über die Struktur macht (außer, daß man eine Menge von Objekten speichern und wiederfinden kann) und nur Operationen zum Eintragen, Finden, Löschen und Zählen der Elemente in der Struktur anbietet. Viele der aus der Vorlesung Informatik A bekannten Datentypen lassen sich auf diese Weise beschreiben, wobei allerdings ein Teil der spezifischen Funktionalität (wie z.B. bei Stack oder Queue) über diese Collection-Schnittstelle nicht ansprechbar ist. Für eine konkrete Realisierung muß man natürlich noch die Funktionalität der oben genannten Basis-Operationen genauer definieren (einfach/mehrfach einfügen, Ordnung, Index-Zugriff möglich etc.). Design-Pattern Visitor und Enumeration (Iterator): public interface Enumeration { /** return true if there are elements left. */ public boolean hasMoreElements(); /** get next element from enumeration. */ public Object nextElement(); } public interface Visitable { /** receive a visitor, manage the visit. return true if the visitor always replies true. */ boolean visit (Visitor v); } public interface Visitor { /** visit an object. return true to continue visiting. */ boolean visit (Object x); } Beide Design-Patterns eignen sich zum Ansehen (Aufzählen, Suchen) der Objekte in einer Collection (Sammlung) von Objekten. Die Enumeration (wird auch als "Iterator" bezeichnet) bietet dem Benutzer des ADTs die Möglichkeit, Objekte "der Reihe nach" aus der Datenstruktur abzuholen und anzusehen. Die Kontrolle über die Traverse liegt beim Benutzer, der die Objekte ansehen möchte ("aktiv"); er muß explizit jedes weitere Objekt anfordern und bestimmt, wann die Traverse endet. Beim Visitor-Modell liegt die Kontrolle über den Ablauf der Traverse dagegen bei der Datenstruktur selbst ("passiv" aus Sicht des Benutzers). Um aber wenigstens beeinflussen zu können, wie lange die Traverse läuft, sollte der "Besucher" die Möglichkeit haben, immer dann, wenn ihm ein Objekt "gezeigt" (vorgestellt) wird, die Traverse abbrechen zu können. Dies wird hier durch ein entsprechendes Resultat der visit()-Methode signalisiert (alternativ könnte man z.B. auch eine Exception verwenden). [siehe dazu auch Kapitel 11.8 im Skript] Blatt 9 ! Alle Begriffe in dieser Aufgabe sind recht ausführlich im Skript erklärt, ! daher gebe ich hier jeweils nur eine kurze Zusammenfassung bzw. Erläuterung. ! Weitere Details kann (und sollte) man bei Unklarheiten im Skript nachlesen! Sequentialität, Nebenläufigkeit, deterministisch, determiniert: Bei einem sequentiellen Programm werden alle Schritte im Programmablauf sequentiell (d.h. in fester Reihenfolge) nacheinander ausgeführt. Damit ist der Ablauf natürlich vorhersagbar, denn es gibt stets genau einen nächsten Ausführungsschritt. Wenn man Nebenläufigkeit (== Verzicht auf Sequentialität) zuläßt, können Anweisungen echt parallel und/oder in beliebiger Reihenfolge ausgeführt werden (für theoretische Betrachtungen betrachtet man Parallelität oft als zufällige Auswahl aus der Menge der möglichen Sequentialisierungen). Parallele Abarbeitung macht natürlich nur dann Sinn, wenn die Anweisungen im wesentlichen voneinander unabhängig sind. Ein deterministisches Programm hat (für eine feste Eingabe) einen genau (Schritt für Schritt) vorherbestimmten und damit auch reproduzierbaren Ablauf, und liefert immer das gleiche Ergebnis. Sequentielle Programme sind stets deterministisch. Ein determiniertes Programm liefert (für eine feste Eingabe) immer ein identisches Resultat, auch wenn der Ablauf eventuell nicht vorhersagbar ist bzw. bei jedem Ablauf anders sein kann (also nicht reproduzierbar!). Ein deterministisches Programm ist immer auch determiniert. Schreib/Schreib-Konflikt, Schreib/Lese-Konflikt: Ein Schreib/Schreib-Konflikt kann immer dann auftreten, wenn zwei oder mehr Aktivitäten (d.h. Threads, Prozesse) gleichzeitig den Wert einer gemeinsamen Variablen ändern wollen. Dabei können Zuweisungen an die Variable "verloren gehen" (wird auch "lost update" genannt), oder es kann sogar der Wert der Variable "undefiniert" werden, d.h sie enthält weder den Wert, den die Aktivität 1 setzen wollte, noch den Wert, den die Aktivität 2 setzen wollte (wird auch als "dirty write" bezeichnet). Ein Schreib/Lese-Konflikt kann immer dann auftreten, wenn eine Aktivität den Wert einer gemeinsamen Variablen ändert, während eine andere Aktivität den Wert ausliest. Dabei kann der gelesene Wert "undefiniert" sein, d.h. es weder der alte noch der neue Wert richtig gelesen ("dirty read"). In solchen Fällen ist explizite Synchronisation notwendig, damit die Zugriffe auf die gemeinsamen Daten sequentiell (und nicht gleichzeitig bzw. nebenläufig) erfolgen. Beispiel: 2 Threads, long-Variable in Java (64 bit) - zum Setzen sind 2 Maschinenbefehle nötig, die jeweils 32 bit des Wertes im Speicher ändern Threads in Java, Interface Runnable: In Java werden neue Threads mit Hilfe der Klasse java.lang.Thread erzeugt. Das Thread-Objekt steuert den Auflauf eines Java-Threads und kann z.B. zum Starten, Unterbrechen, Pausieren, Ändern der Priorität etc. verwendet werden. Wenn ein Thread gestartet wird, beginnt die Ausführung (anders als beim main-Thread!) in der Methode `run()' einer Klasse, die das Interface `Runnable' implementiert (das die run()-Methode enthält). Wenn ein neuer Thread erzeugt wird, muß also ein Runnable-Objekt übergeben werden. Alternativ kann man allerdings auch eine eigene Unterklasse von java.lang.Thread bilden, dort die `run()'-Methode überschreiben, und bei der Konstruktion des Threads kein extra Runnable-Objekt angeben. In diesem Fall wird dann das Thread-Objekt selbst als Runnable-Objekt verwendet. Blatt 10 ! Alle Begriffe in dieser Aufgabe sind recht ausführlich im Skript erklärt, ! daher gebe ich hier jeweils nur eine kurze Zusammenfassung bzw. Erläuterung. ! Weitere Details kann (und sollte) man bei Unklarheiten im Skript nachlesen! Synchronisation, Monitor, Semaphore, Deadlock: Synchronisation meint die kontrollierte (gezielte) Sequentialisierung von Programmabschnitten paralleler Aktivitäten, in denen Konflikte (Schreib/Schreib-Konflikt, Schreib/Lese-Konflikt) auftreten können, mit dem Ziel, diese Konflikte zu vermeiden. Ein Monitor ist ein Objekt, das den Eintritt in einen "kritischen" Code-Bereich kontrolliert (d.h. ein Bereich, in dem es zu Konflikten kommen kann), also praktisch so etwas wie ein "Eintrittskarte" für den Programmabschnitt. Es wird jeweils nur eine Aktivität vom Monitor in den (bzw. die) von diesem Monitor geschützten kritischen Bereich(e) gelassen (man sagt: die Aktivität wird "Besitzer" des Monitors), alle anderen müssen warten, bis der Monitor wieder "frei" wird. In Java kann jedes Objekt als Monitor verwendet werden. Verschiedene Bereiche können durch denselben Monitor geschützt werden, und es kann auch mehrere Monitore für denselben Code-Bereich geben. Eine Semaphore ist praktisch ein gezählter Monitor, d.h. es wird eine bestimmte Anzahl von Aktivitäten zur gleichen Zeit in den kritischen Bereich gelassen. Die Maximalanzahl muß beim Erzeugen der Semaphore angegeben werden, und der aktuelle Zähler wird durch die Operationen P() und V() manipuliert. Eine Semaphore kann genau wie ein Monitor zur Synchronisation eines kritischen Abschnitts eingesetzt werden (dazu wird der Bereich mit P() und V() umgeben) oder auch zur Signalisierung von Ereignissen, auf die andere Threads warten müssen (wird auch als "einseitige Synchronisation" bezeichnet). Als `Deadlock' bezeichnet man eine "Verklemmungssituation", in der mehrere Aktivitäten auf Ressourcen (wie z.B. Monitore oder andere Dinge) warten und dabei eine Situation eintritt, in der die Ressourcen so verteilt sind, daß alle Aktivitäten gegenseitig aufeinander warten müssen bzw. blockiert sind. Damit ein Deadlock auftreten kann, müssen vier Bedingungen erfüllt sein: - exklusive Belegung (Ressourcen können nicht gemeinsam genutzt werden) - Belegen und Warten (Beim Warten auf weitere Ressourcen werden die schon zugeteilten nicht wieder abgegeben) - kein zwangsweises Freigeben (Ressourcen können nicht entzogen werden) - zyklische Wartebedingung (Es gibt einen Ring von Aktivitäten, in dem jeder Prozeß auf von seinen Nachfolgern belegte Ressourcen wartet) synchronized, wait(), notify() bzw. notifyAll(): Ein kritischer Bereich in einem Java-Programm, der durch einen Monitor bewacht ist, wird mit dem Schlüsselwort `synchronized' markiert: synchronized (objekt) { ... } Alle `synchronized'-Blöcke, die sich auf denselben Monitor beziehen, können insgesamt nur von einem Thread zur Zeit ausgeführt werden. Man kann auch eine komplette Methode mit `synchronized' markieren, dabei wird dann das Empfänger-Objekt des Methodenaufrufs der Monitor (bei Klassenmethoden das entsprechende Klassenobjekt), der geschützte Bereich ist der gesamte Körper der Methode: public void synchronized lock () { ... } Die Methoden `wait()', `notify()' und `notifyAll()' dürfen in Java nur innerhalb eines `synchronized'-Blocks stehen (d.h. der ausführende Thread muß Besitzer des Monitors sein) und sind als Objektmethoden an das jeweilige Monitor-Objekt gerichtet. wait() gibt einen Monitor temporär wieder ab und legt den Thread so lange schlafen, bis er durch ein notify() bzw. notifyAll() an diesen Monitor wieder aufgeweckt wird. Dann wird versucht, den Monitor wieder zu belegen und der wait()-Aufruf anschließend verlassen. notify() weckt _irgendeinen_ Thread auf, der auf diesen Monitor wartet (im Sinne von wait()), notifyAll() weckt _alle_ Threads auf, die auf diesen Monitor warten. Es können viele Threads gleichzeitig auf einen Monitor warten.