Musterlösungen der „Textaufgaben“ von Elmar (Blatt

Werbung
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.
Herunterladen