Das Java-Projekt Einführung Das Ziel des vorliegenden Projekts ist es, Programme in der Programmiersprache Java analysieren und modifizieren bzw. erstellen zu können. Dies ist u. a. nützlich • beim Erstellen von Metriken (Operationen/Methoden pro Klasse, Klassen pro Package, Anweisungen pro Methode u. s. w.) • beim Finden von angewandten Entwurfs-Mustern in fremdem Code • beim Finden von Fehlern, etwa im Bereich Nebenläufigkeit (Verklemmungen, fehlende Synchronisation u. s. w.) • zur Generierung von Programmen oder Programmteilen aus formalen Spezifikationen (vgl. Modell-getriebene Entwicklung!), etwa Datenbank-Anbindungen • zum Einbauen von Idiomen (etwa Getter/Setter statt Direktzugriff auf Attribute) • zum Einbauen von Entwurfs-Mustern (etwa das Befehls-Muster durch Kapselung von MethodenAufrufen in Befehlsklassen) • zur Refaktorisierung (also Veränderung innerer Struktur unter Beibehaltung äußerer Schnittstellen) von Programmen unter Berücksichtigung und Mit-Veränderung vorhandener Datenbestände Der letzte Punkt ist aktives Forschungsthema an der FHDW Hannover. Im Rahmen des Forschungsprojekts soll das hier entwickelte Programm bzw. Rahmenwerk seine Anwendung finden in der praktischen Validierung theoretischer Erkenntnisse im Bereich Refaktorisierung von Informationssystemen. Das Dokument ist folgendermaßen gegliedert: Die Kapitel sind den einzelnen Entwicklungsteams zugeordnet und beinhalten in den untergeordneten Abschnitten allgemeine Rahmenbedingungen, die einzelnen Aufgaben der Entwickler und Ausbaustufen des Projekts sowie die Zuordnung der Entwickler zu den Aufgaben. Entwicklungsumgebung Das Projekt wird in Java entwickelt, vorzugsweise in der Entwicklungsumgebung Eclipse. Die Dokumentation und Quelltexte sind auf dem FHDW-eigenen Entwicklungsserver versioniert abgelegt und können mit Hilfe eines Subversion-Klienten unter der URL https://svn.ha.bib.de/svn/repos/programgeneration/ abgerufen werden; dieser Server ist auch von außen erreichbar. Ein SubversionKlient ist in der von der FHDW bereitgestellten Eclipse-Umgebung bereits enthalten. Nähere Informationen zum Umgang mit der Versionsverwaltung werden in der Veranstaltung mitgeteilt. Ebenfalls Teil des Projekts ist ein Fehlerverfolgungssystem (Bugzilla), das den Entwicklern behilflich sein soll, gefundene Fehler zu kategorisieren, an Entwickler zu verteilen und deren Behebung nachzuhalten. Das System ist unter der URL https://svn.ha.bib.de/bugzilla/ zu erreichen. Auch hierzu erfolgen in der Veranstaltung gesonderte Hinweise. Gruppe HFI402 Rahmenbedingungen für das Java-Projekt Generelle Vorgabe: Jedes Programm, das von dem zu entwickelnden Java-Framework als gültig akzeptiert wird, muss auch den Regeln der Java-Programmiersprache entsprechen! (Das Umgekehrte ist nicht der Fall: Nicht jedes korrekte Java-Programm muss auch vom Framework als korrekt erkannt werden. Insbesondere wird zuerst nur eine Teilmenge der Java-Programmiersprache unterstützt werden.) Diese Regel gilt selbstverständlich nur insoweit, als dass entsprechende Komponenten existieren, welche die erforderlichen Überprüfungen durchführen können. Solange bspw. keine Typüberprüfung entwickelt ist, können Java-Programme akzeptiert werden, welche die Typregeln verletzen. Jede Gruppe ist angehalten, ihren Quellcode ausreichend zu kommentieren. Insbesondere sollten für jede Operation/Methode JavaDoc-Kommentare geschrieben werden. Absolutes Minimum ist die Dokumentation der öffentlichen Schnittstellen einer Komponente. Gruppe “Scanner” • Die von der Scanner-Komponente erkannten lexikalischen Elemente sollen in strukturierten Token-Objekten gekapselt werden, erweitert um eine Positionsangabe (Zeile und Spalte des TokenAnfangs innerhalb der untersuchten Datei). • Der erzeugte Token-Strom soll mit einem expliziten Ende-Token abgeschlossen sein. • Die Scanner-Komponente soll keine Ausnahmen werfen, vielmehr sollen Fehler über spezielle (Fehler-)Token weiteren Komponenten signalisiert werden. • Zum Erkennen von lexikalischen Elementen bieten sich u. U. verschiedene Methoden der JavaKlasse Character an, etwa isJavaIdentifierStart, isJavaIdentifierPart oder isDigit. • Die Token-Schnittstelle muss mit der Parser-Gruppe abgesprochen werden! Gruppe “Parser” • Die syntaktischen Elemente sollten über entsprechende Methoden der Parser-Komponente erkannt werden, etwa parseClass für das Parsen von Klassen oder parseMember für das Parsen von Elementen innerhalb der Klasse. Diese Methoden sollen entweder das entsprechende Objekt der Modell-Komponente zurückliefern oder eine geeignete Ausnahme auswerfen. • Ausnahmen können vorerst unstrukturiert bleiben. Es bietet sich an, die grundlegende Ausnahmeklasse ParserException zu nennen und von Exception abzuleiten. Es sollte ein Konstruktor vorhanden sein, der eine Nachricht sowie ein Token-Objekt (zur Ermittlung des Fehlerkontextes) entgegennimmt. • Die Nachrichten für die verschiedenen Ausnahmen, die während des Parsens auftreten können, sollen in der Parser-Komponente an zentraler Stelle gekapselt werden. • Die Modell-Schnittstelle muss mit der Modell-Gruppe abgesprochen werden! Gruppe “Modell” • Die Klassen, die syntaktische Elemente von Java repräsentieren, sollen Konstruktoren enthalten, die alle zur Erzeugung benötigten Informationen als Argumente entgegennehmen. (Objekte des Modells werden also über genau einen Konstruktor-Aufruf komplett erzeugt.) • Es bietet sich an in Hinblick auf zukünftige Erweiterungen, sich Gedanken über eine geeignete Abbildung des Java-Typ-Mechanismus in die Modellklassen zu machen. (Eine Basisklasse Type mit einer Spezialisierung PrimitiveType sollte eine gute Basis für das weitere Vorgehen sein.) • Es müssen die notwendigen Voraussetzungen geschaffen werden, um Besucher des Modells (im Sinne des Visitor-Patterns) zu unterstützen. Die Zusammenarbeit mit der Drucker-Gruppe muss gewährleistet sein! Gruppe “Drucker” • Es muss lediglich ein Layout unterstützt werden. Gruppenzusammensetzung der Studenten: Gruppe 1: Sascha Meyer, Bert Peters, Lisa Shekhter Gruppe 2: Lars Ente, Carsten Feldhaus, Marcel Lohmann, Michael Peters Gruppe 3: Axel Evers, Daniel Gattermann, Olaf Strauß, Tobias Jüttner Gruppe 4: Mario Moldenhauer, Dennis Linke, Maxim Astafev, Michael Krüger 1. Ausbaustufe: Klassendefinition mit Attributen • nur eine Datei • nur eine Klasse pro Datei • nur Sichtbarkeit public für die Klassendefinition • nur Attribut-Definitionen innerhalb der Klasse (keine Operationen/Methoden, keine inneren Klassen) • nur Basistypen int und boolean • nur Attribut-Definitionen ohne Initialisierung • nur package-scoped Attribut-Definitionen (ohne explizit angegebene Sichtbarkeit) Der zu akzeptierende Code hat also folgendes Aussehen: public class class-name { type1 attr-name1; type2 attr-name2; // ... (beliebig viele (*) Attribut-Definitionen) } Zieltermin: 02.08.2004 Gruppenzuordnung: Scanner: Parser: Modell: Drucker: Gruppe 4 Gruppe 3 Gruppe 2 Gruppe 1 2. Ausbaustufe: Abstrakte Operationen • Operationen ohne explizit angegebene Sichtbarkeit • void • Parameterlisten • Attribute und Operationen können gemischt auftreten • abstract im Klassenkopf, darf vor und hinter public stehen Beispiel: public abstract class class-name { abstract int getSomething(); abstract void setSomething (int something); abstract void doSomething (int first, boolean second, int third); } Zieltermin: 23.08.2004 Gruppenzuordnung: Scanner: Parser: Modell: Drucker: Gruppe 3 Gruppe 2 Gruppe 1 Gruppe 4 3. Ausbaustufe: Initialisierung von Attributen In dieser Ausbaustufe soll es möglich sein, Attribute zu initialisieren. Beispiel: public class SectorCache { int items = 0; int itemSize = 512; int maxSize = 1024 * 1024; int maxItems = (maxSize+itemSize–1) / itemSize; boolean haveFewMemory = false; boolean preallocMemory = (maxSize <= 64 * 1024) || (!haveFewMemory && 2048 * 1024 >= maxSize) ? true : false; } Folgende Einschränkungen sollen dabei gelten: • Ausdrücke können folgende unäre Operatoren enthalten: ! (nicht), + (Plus), - (Minus) • Ausdrücke können folgende binäre Operatoren enthalten: + (Addition), - (Subtraktion), * (Multiplikation), / (Division), % (Modulo), == (gleich), != (ungleich), < (kleiner), > (größer), <= (kleiner oder gleich), >= (größer oder gleich), && (logisches UND), || (logisches ODER) • Ausdrücke können folgende ternäre Operatoren enthalten: ?: (entweder-oder) • Die Rangordnung der Operatoren ist wie folgt: (höchste) !, + (unär), - (unär) *, /, % + (binär), - (binär) <, >, <=, >= ==, != && || (niedrigste) ?: • (Runde) Klammern ((, )) erlauben das Gruppieren von (Unter-)Ausdrücken • An Literalen sollen unterstützt werden: ganze Zahlen ohne Vorzeichen (z. B. 0, 1234) und boolesche Literale (true, false) Zieltermin: 13.09.2004 Gruppenzuordnung: Scanner: Parser: Modell: Drucker: Gruppe 2 Gruppe 1 Gruppe 4 Gruppe 3 4. Ausbaustufe: Methoden und einfache Anweisungen Diese Ausbaustufe erweitert das System um die Verarbeitung einfacher Methoden. Insbesondere sollen folgende Konstrukte unterstützt werden: • Methoden(-rümpfe) • (eventuell verschachtelte) Anweisungsblöcke • Definitionen von lokalen Variablen • return-Anweisungen • Zuweisungen • Methodenaufrufe • public-, protected- und private-Modifizierer bei Attribut- und Operations-/MethodenDefinitionen Beispiel 1 (Einfacher Zähler): public class Counter { private int counter = 0; public void increment (int delta) { int newValue = counter + delta; set (newValue); } public void decrement (int delta) { int newValue = counter – delta; set (newValue); } public void set (int value) {counter = value;} public int get () {return counter;} } Beispiel 2 (Wochentagsbestimmung; 0=Sonntag, 1=Montag usw.): public class WeekDay { public int fromDate (int day, int month, int year) { int k = month <= 2 ? 1 : 0; int l = year – k; int o = k * 12 + month; int p1 = l / 400; int p2 = l / 100; int p3 = 5 * l / 4; int p4 = 13 * (o + 1) / 5; return (p4 + p3 – p2 + p1 + day – 1) % 7; } } Zieltermin: 01.10.2004 Gruppenzuordnung: Scanner: Parser: Modell: Drucker: Gruppe 1 Gruppe 4 Gruppe 3 Gruppe 2 5. Ausbaustufe (Praxisphase): Erweiterte Syntaxprüfung, mehrere Klassen und Kommentare In dieser Ausbaustufe wird das System um die folgenden Eigenschaften erweitert: • Verarbeitung (Ignorieren bzw. Speichern) von (normalen bzw. JavaDoc-)Kommentaren • Neue Komponente “Identifier” führt die Namensauflösung von Objekt-, Operations- und TypBezeichnern durch (z. B. ordnet sie einem Operationsaufruf die zugehörige Operation zu) • Eine oder mehrere neue Komponenten zum Prüfen syntaktischer und semantischer Regeln, die bisher unberücksichtigt geblieben sind (s. u.) • Erweiterung des Treiber-Programms um die Fähigkeit, alle Java-Dateien in einem Verzeichnis und mehrere Klassen pro Java-Datei in einem Aufruf verarbeiten zu können (d. h. eine ModellInstanz überdauert eventuell mehrere Scanner- und Parser-Durchläufe, s. u.) Es werden wieder vier Gruppen gebildet. Jede Gruppe bearbeitet eine der oberen Aufgaben. Die zusätzlich zu prüfenden syntaktischen und semantischen Eigenschaften von Java-Programmen sind von der jeweiligen Gruppe zu erarbeiten und sinnvoll auf Komponenten zu verteilen. Es sind jeweils zwei syntaktische und zwei semantische Bedingungen auszuwählen und zu implementieren. Es folgt jeweils ein Beispiel zur Verdeutlichung: • syntaktisch: Prüfung auf eine verbotene mehrfache Anwendung von Modifizierern: public public class X { // zweimal public public private int x; // public und private } • semantisch: Prüfung der Angemessenheit des abstract-Modifizierers: // Klasse hat abstrakte Operationen, aber keinen abstract-Modifier im Klassenkopf public class Y { public abstract void op(); } Die Gruppe, die das Treiber-Programm erweitert, muss dafür Sorge tragen, dass folgende Java-Regeln eingehalten werden: • höchstens eine public-Klasse pro Datei • der Name einer eventuell enthaltenen public-Klasse muss mit dem Dateinamen (abgesehen von der Dateinamen-Erweiterung) übereinstimmen • weitere Klassen innerhalb einer Datei müssen package-scoped sein (d. h. nicht public) • auf oberster Ebene sind “sporadische” Semikola erlaubt Beispiel 1: A.java: ;;class B{}; public class A {};; class C{} Beispiel 2: B.java: class B {}; Beispiel 3: C.java: (Leere Dateien sind auch erlaubt!) Zieltermin: Erster Projekttermin in der Woche 03.01.–07.01.2005 Gruppenzuordnung: Kommentare: Identifier: Erweiterte Regelprüfung: Erweiterung des Treibers: Gruppe 2 Gruppe 3 Gruppe 1 Gruppe 4 6. Ausbaustufe: Refaktorisierung und Code-Review Die entwickelte Anwendung soll gründlich überarbeitet werden. Ziel ist es, gemäß definierter Vorgaben ein “einheitliches Bild” aller Komponenten zu erhalten. Um das zu erreichen, wollen wir Konventionen festlegen, die wir in drei Gruppen einteilen wollen: Kommentare, syntaktische Richtlinien und semantische/strukturelle Vorgaben. 1. Kommentare • durchgehend englisch • kommentiert werden: ➔ alle Operationen (sowohl öffentliche als auch private) ➔ Klassen und Schnittstellen ➔ Attribute • der Kommentar einer Operation dokumentiert die Schnittstelle so, dass die Benutzung der Operation ohne ein Studium des Quelltextes möglich ist, d. h. Eingabe, Ausgabe, Funktionsweise und Ausnahmen sind ausreichend dokumentiert (letzteres beinhaltet auch Beschränkungen oder Annahmen, die bei bestimmten Parametern gelten!) • die Kommentare erfüllen die JavaDoc-Syntax (Tags verwenden wenn angebracht: @param, @return, @exception) • innerhalb von Methoden werden keine Kommentare geschrieben; wenn sie notwendig erscheinen → Code in neue Methode verschieben • verwendete Muster werden explizit dokumentiert (beteiligte Klassen, Operationen, Attribute; Rollen-Zuordnung); Referenz ist dabei “Design-Patterns” von Gamma et al. • triviale get- und set-Operationen (s. u.) müssen nicht kommentiert werden • bei redefinierenden Methoden ohne Ergänzung: Kommentar weglassen; ansonsten @seeTag benutzen 2. Syntaktische Richtlinien • Bezeichner durchgehend englisch • die üblichen Java-Benennungs-Konventionen beachten: ➔ Klassen mit Großbuchstaben beginnen ➔ Konstanten durchgängig groß, bei mehreren Wörtern durch Unterstriche trennen ➔ Operations- und Attribut-Namen klein beginnen ➔ bei mehreren Wörtern innerhalb eines Bezeichners: jedes Wort groß beginnen • die Länge einer Methode sollte etwa 25-30 Zeilen nicht überschreiten • maximal vier Einrückungs-Ebenen sind erlaubt • keine geschachtelten Iterationen • geschachtelte Fallunterscheidung höchstens bis zur dritten Ebene • keine leeren catch-Blöcke; wenn doch notwendig (etwa Parser), Methode aufrufen, die “nichts tut”, um Code einfacher erweitern zu können • keine protected- und public-Attribute • Konstanten werden in einer eigenen Klasse gesammelt (Eigenschaften: public abstract; Bennenung: ...Constants; enthält nur Konstanten-Definitionen) • Konstruktor-Parameter, die zur Initialisierung von Attributen gedacht sind, heißen genauso wie die Attribute • eine triviale get-Operation für ein Attribut x vom Typ T wird folgendermaßen realisiert: T getX () { return this.x; } • eine triviale set-Operation für ein Attribut x vom Typ T wird folgendermaßen realisiert: void setX (T x) { this.x = x; } • auf Attribute und Operationen der Klasse wird generell über this zugegriffen; der direkte Zugriff auf Attribute sollte dabei nur innerhalb der entsprechenden get- und set-Methoden erfolgen • boolesche Operationen heißen vorzugsweise is... oder has...; Ausnahmen müssen entsprechend begründet werden! 3. Semantische/strukturelle Vorgaben • • Ausnahmen ➔ Ausnahme-Klassen existieren in einer einheitlichen Klassen-Hierarchie mit geeigneten Abstraktionen ➔ jede konkrete Ausnahme wird über eine eigene Klasse abgebildet ➔ jede Ausnahme-Klasse enthält so viel Kontext-Information wie nur möglich Visitoren ➔ Typ-Abfrage und behandelnden Code in Visitoren auslagern; keine is...-Operationen verwenden (Ausnahme: Token-Hierarchie) ➔ Visitoren verwenden Attribut als Rückgabetyp (mit zugehöriger Zugriffs-Operation) und verwenden für jeden rekursiven Aufruf ein neues Objekt. Beispiel: public abstract class Component { public abstract void accept (CountVisitor v); } public class Leaf extends Component { public void accept (CountVisitor v) { v.accept (this); } } public class Composite extends Component { public void accept (CountVisitor v) { v.accept (this); } public Iterator iterator () { // ... } } public class CountVisitor { private int count; // Rückgabewert des Visitors CountVisitor () { this.count = 0; // Initialisierung } int getCount () { // Operation zum Zugriff auf das Ergebnis return this.count; } void handleLeaf (Leaf leaf) { this.count = this.count + 1; } void handleComposite (Composite composite) { Iterator it = composite.iterator (); while (it.hasNext ()) { // Rekursion über neues Objekt CountVisitor v = new CountVisitor (); // accept liefert nichts direkt zurück it.next ().accept (v); // Ergebnis wird aus dem verwendeten Visitor gelesen this.count += v.getCount (); } } } ➔ Visitoren kapseln Ausnahmen aus besuchten Objekten in einer Wrapper-Klasse namens ...VisitorException. Alle Operationen in einer abstrakten ...Visitor-Klasse haben eine entsprechende throws-Klausel, ebenso die accept-Operation in der abstrakten Modell-Klasse. Beispiel: // kapselt Modell-Ausnahmen für das Tunneln aus dem Visitor heraus public class ModelVisitorException { private ModelException exception; // gekapselte Ausnahme public ModelVisitorException (ModelException exception) { this.exception = exception; // Ausnahme speichern } public ModelException getException () { return this.exception: } } // abstraktes Modell-Objekt public abstract class ModelObject { public abstract void accept (ModelVisitor v) throws ModelVisitorException; } // Ausdruck als konkretes Modell-Objekt public class Expression extends ModelObject { public String evaluate () throws DivisionByZeroModelException { // ... } public void accept (ModelVisitor v) throws ModelVisitorException { v.handleExpression (this); } } // abstrakter Visitor public abstract class ModelVisitor { public abstract void handleExpression (Expression expression) throws ModelVisitorException; } // konkreter Visitor public class EvaluateModelVisitor extends ModelVisitor { private String result; public String getResult () { return this.result; } public void handleExpression (Expression expression) throws ModelVisitorException { try { this.result = expression.evaluate (); } catch (DivisionByZeroModelException x) { // Ausnahme wird eingepackt und getunnelt throw new ModelVisitorException (x); } } } // Benutzung: try { EvaluateModelVisitor v = new EvaluateModelVisitor (); expression.accept (v); return v.getResult (); } catch (ModelVisitorException e) { // Modell-Ausnahme wird ausgepackt ModelException modelException = e.getException (); // Beispiel 1: Ausgabe System.out.println ("Modell-Ausnahme: " + modelException.getMessage ()); // Beispiel 2: Weiterreichen throw modelException; } Zieltermin: 21.01.2005 Gruppenzuordnung: Scanner: Parser: Modell + Visitor-Schnittstellen: Treiber + “Rest”: Gruppe 4 Gruppe 3 Gruppe 2 Gruppe 1 7. Ausbaustufe: Anweisungen In dieser Ausbaustufe wird der potentielle Inhalt von Methoden um die folgenden Anweisungen erweitert: • die leere Anweisung (;) • Fallunterscheidung (if mit optionalem else) • Schleifen (while-, do-, for-Schleife) • break und continue (ohne Sprungmarken) Beispiel: public class FactorialProvider { public int facIter1 (int n) { int result = 1; while (n > 0) { result = result * n; n = n – 1; } return n; } public int facIter2 (int n) { int result = 1; for (int i = n; i > 0; i = i - 1) result = result * n; return n; } public int facIter3 (int n) { int result = 1; do { if (n == 0) break; result = result * n; n = n – 1; } while (n > 0); return result; } public int facRec (int n) { if (n == 0) return 1; else return n * facRec (n – 1); } } In for-Schleifen sind im Initialisierungs-Teil sowohl ein Ausdruck als auch die Definition einer lokalen Variable erlaubt, die nur innerhalb der for-Schleife existiert. Beispiel: { int i; for (i = 1; i < 10; i = i + 1) // for mit Ausdruck handle (i); reached (i); // OK, i ist bekannt } { for (int i = 1; i < 10; i = i + 1) // for mit lokaler Definition handle (i); // reached (i); // falsch, i hier unbekannt } Zieltermin: 04.02.2005 Gruppenzuordnung: Scanner: Parser: Modell + Identifier-Visitor: Drucker: Gruppe 3 Gruppe 2 Gruppe 1 Gruppe 4 Analyse-Zusatzaufgabe für die Drucker-Truppe: Es sollen alle Verstöße der derzeitigen Implementierung gegen die Java-Syntax und -Semantik systematisch analysiert und katalogisiert werden. Ziel ist es, die Abweichungen zu dokumentieren, um sie in einer späteren Version der Software leichter korrigieren zu können. Beispiel: Der Ausdruck boolean b = 1 + 2; wird akzeptiert, obwohl die Typen nicht zueinander passen → Kategorie “Typ-Kompatibilität”, Eintrag “Zuweisungen werden nicht auf Typ-Kompatibilität geprüft”. 8. Ausbaustufe: Gültigkeitsbereiche und Typen Diese Ausbaustufe erweitert das System um grundlegende Fähigkeiten im Bereich der Objekttypen. Weiterhin wird die Identifier-Komponente in der Funktionalität verbessert und für weitere Ausbaustufen entsprechend vorbereitet. Die einzelnen Aufgaben sind zum Teil eng aneinander gekoppelt und erfordern deshalb eine schnelle Absprache der Schnittstellen in der Anfangsphase der Entwicklung, damit danach die Aufgaben unabhängig voneinander gelöst werden können. 8.1 Gültigkeitsbereiche Die Aufgabe ist, den Identifier so zu verändern, dass die Auflösung von Namen über mehrere Gültigkeitsbereiche hinweg einheitlich funktioniert. Dazu muss ein geeignetes Scope-Konzept (Scope = Gültigkeitsbereich) entwickelt werden. Jede Deklaration findet innerhalb eines Gültigkeitsbereiches statt; das Suchen einer passenden Deklaration findet “aufwärts” vom “aktuellen” Scope (Ort der Verwendung) bis hin zum “globalen” Scope (außerhalb aller Top-Level-Klassen) statt. Gegeben sei folgendes Beispiel: public class A { private int x; // scope(x) == "A" private int y; // scope(y) == "A" public int f ( // scope(f) == "A" int x // scope(x) == "A.f" ) { int z = y + x; // scope(z) == "A.f"; y --> A.y, x --> A.f.x int y = z + x; // scope(y) == "A.f"; z --> A.f.z, x --> A.f.x return z * y; // z --> A.f.z, y --> A.f.y } public int g () { // scope(g) == "A" return x; // x --> A.x } public int h () { // scope(h) == "A" int result = 0; // scope(result) == "A.h" for (int x = 1; // scope(x) == "A.h.for" x < 10; x = x + 1) // ; x --> A.h.for.x { int y // scope(y) == "A.h.for.{" = x * x; // x --> A.h.for.x result = result // result --> A.h.result + y; // y --> A.h.for.{.y } return result; // result --> A.h.result } } In diesem Beispiel werden die Namen x und y je nach Kontext (= je nach Scope) unterschiedlich aufgelöst. Die hypothetische scope-Funktion weist beispielhaft jeder Deklaration den zugehörigen Gültigkeitsbereich zu; die Namensauflösung wird über Pfeile symbolisiert. 8.2 Typ-Referenzierung und Typen von Ausdrücken So wie bisher Namen von Variablen, Parametern, Attributen und Operationen aufgelöst wurden, sollen jetzt Typ-Namen aufgelöst werden, d. h. die Verwendung eines Typs (z. B. einer Klasse) soll der entsprechenden Definition zugeordnet werden. Dazu muss die bestehende Identifier-Komponente um die Behandlung von Typ-Verwendungen ergänzt werden. Danach soll das System um die Funktionalität erweitert werden, zu jedem bisher unterstützten Ausdruck den passenden Typ zu ermitteln. Wir benötigen also (abstrakt betrachtet) die Funktion: getType : Expression -> Type die beispielsweise folgende Ergebnisse zurückliefert: // Beispiel-Klasse zur Verdeutlichung class A { private int i; public A f (int x, int y) { // hier werden die getType-Aufrufe (s.u.) durchgeführt } } getType getType getType getType (i) == "int" (2 + 3) == "int" (2 == i ? true : false) == "boolean" (f (42, i * 7)) == "A" 8.3 Typ-Kompatibilität Aufbauend auf der Schnittstelle zum Ermitteln des Typs eines Ausdrucks soll hier an allen Stellen im Java-System, an denen eine Typ-Prüfung notwendig ist, diese durchgeführt und bei Nicht-Einhaltung der Java-Regeln zur Typ-Kompatibilität ein entsprechender Fehler generiert werden. TypPrüfungen sind unter anderem bei der Argument-Übergabe an Methoden, bei Zuweisungen und anderen Ausdrücken notwendig. Zuerst soll analysiert werden, was Typ-Kompatibilität in Java bedeutet (natürlich im Rahmen des bisher implementierten Sprachumfangs). Danach sollen alle Fälle lokalisiert werden, in denen eine Typ-Prüfung notwendig ist. Schließlich soll die Typ-Überprüfung an diesen Stellen implementiert werden. 8.4 Korrekturen Diese Gruppe hat die Aufgabe, Schwächen im Modell und anderen Komponenten des Systems auszumerzen und eine stabile Grundlage für die weitere Erweiterung des Systems zu schaffen. Grundlage für die Änderungen ist zum einen die Liste der Syntax- und Semantik-Verletzungen, die in der letzten Ausbaustufe von der Drucker-Gruppe erarbeitet wurde. Zum anderen ist Folgendes zu korrigieren: • Modell: EmptyExpression: Diese Abstraktion ist problematisch und sollte eliminiert werden. • Scanner: Der Scanner darf gemäß einer Design-Entscheidung keine Ausnahmen auswerfen. Es ist zu prüfen, ob diese Entscheidung konsequent beibehalten wurde, da zumindest eine Ausnahme-Klasse in dem Scanner-Paket existiert und benutzt wird. • Parser: Der Parser ist auf fehlerhafte Gruppierungen von Operatoren zu überprüfen. In Ausbaustufe 7 verarbeitete der Parser Ausdrücke mit mehreren relationalen Operatoren nicht korrekt (etwa 2 > 3 < true <= false). • System: Die Entscheidung der 5. Ausbaustufe, leere Dateien zu akzeptieren, ist nicht Java-konform. Das System ist so umzubauen, dass mindestens eine Klasse in einer Datei enthalten sein muss. Analyse-Zusatzaufgabe: Die Korrekturen-Gruppe soll sich zusätzlich mit dem Thema “Überladung von Operationen” beschäftigen. Insbesondere soll geklärt werden, welche Komponenten des Systems angepasst werden müssen, um Überladung zu ermöglichen. Bei diesen Überlegungen soll von einem System der 8. Ausbaustufe ausgegangen werden (also mit angepasstem Identifier, Typ-Identifizierung, Typ-Zuweisung zu Ausdrücken und Typ-Kompatibilität)! Zieltermin: 18.02.2005 Gruppenzuordnung: Gültigkeitsbereiche: Typ-Zuordnung: Typ-Kompatibilität: Korrekturen + Analyse “Überladung”: Gruppe 3 Gruppe 4 Gruppe 1 Gruppe 2 8.5 Allgemeine Hinweise 8.5.1 Richtlinien 1. Allgemeine Entwurfsentscheidungen werden für das Gesamtprojekt festhalten! 2. Jeder entdeckte “Fehler” wird festgehalten! (ToDo-Liste für “Korrekturen”) 3. Jeder Punkt auf dieser Liste wird in der Architektur-Gruppe diskutiert und entschieden! • kein Problem → erledigt mit Datum und Begründung • ein Problem! → Lösungsstrategie und Verteilung von Unteraufgaben mit Fertigstellungskontrolle mit abschließender Erledigung (Datum) 8.5.2 Sanktionen 1. Entdeckt die annehmende Gruppe oder die Leitung einen Fehler in einem Systemteil, erhält die abgebende Gruppe Punktabzug: –5 % 2. Entdeckt die annehmende Gruppe oder die Leitung Verstöße gegen unsere Richtlinien, erhält die abgebende Gruppe Punktabzug: –3 % 3. Bei kriminellen, subversiven Fehlern erhält die abgebende Gruppe Punktabzug: –20 % 9. Ausbaustufe: Überladung und Vererbung In dieser Ausbaustufe wird das System um die Fähigkeiten zur Überladung und Vererbung erweitert. Dies umfasst im Detail: • Erweiterungen des Scanners, Parsers und Modells • Erweiterung der Scope-Komponente • Anpassung des Typ-Systems und der Typ-Verträglichkeit • Anpassung der Identifier-Komponente Beispiel: class A { public int i = 5; private int j = 1; public void f () {} } class B extends A { private int j = 2; protected int f (int i) {return i;} public int f (boolean b) {return b ? i : j;} public int g () {f (); return f (1) + f (false) * f (true) – i ();} public int i () {return i + 1;} } Eine (hypothetische) Auswertung des Ausdrucks new B ().g () ergibt 5. Zusätzlich wird das System um Zeichen und Zeichenketten erweitert. 9.1 Erweiterungen des Scanners, Parsers, Modells und Typ-Systems Der Scanner und Parser müssen angepasst werden, um das neue Schlüsselwort extends bzw. die hierarchische Ordnung der Vererbung auf Klassen verarbeiten bzw. abbilden zu können. Im gleichen Zug muss das Modell um die Typ-Ordnung erweitert werden. Schließlich müssen die Komponenten, die das Typ-System überwachen, angepasst werden. Die Regeln der Typ-Kompatibilität können nun nämlich durch die Definition entsprechender Klassen und extends-Beziehungen verändert werden. Es ist sinnvoll, Operationen wie isAssignableTo und isPassableTo für andere Komponenten zur Verfügung zu stellen (etwa für die Scope-Komponente, die einen Operationsaufruf auflösen muss, dessen Name überladen ist). Zum Unterschied zwischen isAssignableTo und isPassableTo sollten die Abschnitte 5.2 und 5.3 der Java-Spezifikation zu Rate gezogen werden. Alle equals-Aufrufe auf Typen müssen daraufhin überprüft werden, ob nicht isAssignableTo o. ä. verwendet werden muss! 9.2 Erweiterung der Scope-Komponente Die Scope-Komponente muss angepasst werden, um Überladung und Vererbung unter einen Hut zu bringen. Im obigen Beispiel sind einige Stellen gezeigt, an denen die Scope-Komponente “mehr tun muss als bisher”. Insbesondere muss Überladung über alle Scopes einer Vererbungslinie hinweg funktionieren (siehe Operation f in den Klassen A und B). Die Auswahl der korrekten Operation bei der Anwendung erfordert die Ermittlung der “spezifischsten” Operation. (Achtung: Analysebedarf! Siehe hierzu die Ergebnisse der letzten Analyse-Aufgabe und Abschnitt 15.12.2 in der Java-Spezifikation!) Das Scope-Konzept soll auch so umgebaut werden, dass die Modell-Objekte ihren Scope kennen (wie im Review besprochen). 9.3 Anpassung der Identifier-Komponente Die Identifier-Komponente muss angepasst werden, um Gebrauch von der neuen Funktionalität in den Scope- und Typ-Komponenten zu machen. Außerdem muss in diesem Zusammenhang auch die getType-Operation auf Ausdrücken aktualisiert werden, damit diese auch im Kontext von Überladung korrekt funktioniert. (Je nach aktuellem Ausbau-Stand des Systems ist dazu keine bis viel Arbeit erforderlich!) Die zugehörige Gruppe übernimmt dabei die “Regie”-Funktion, d. h. sie ist für die Integration der einzelnen Komponenten zu einem “sinnvollen Ganzen” verantwortlich. Anwendungen von instanceof werden eliminiert! 9.4 Erweiterung der Basistypen Das System soll um die Basistypen char und String sowie die passenden Operatoren erweitert werden. Dabei sollen auch Zeichen- und Zeichenketten-Literale verarbeitet werden können. Weiterhin sollen bei den Basis-Operationen Definition und Applikation wie besprochen getrennt werden. Analyse-Zusatzaufgabe “Interfaces”: Die Gruppe analysiert, wie das System erweitert werden muss, um Interfaces zu unterstützen. Insbesondere sollen die notwendigen Erweiterungen der Regel zur Typ-Kompatibilität sowie die Namensauflösung in Interfaces (auch in Hinblick auf Überladung!) unter die Lupe genommen werden. Analyse-Zusatzaufgabe “Konstruktoren”: Die Gruppe analysiert, welche Änderungen am System vorgenommen werden müssen, um Konstruktoren zu unterstützen. Dabei sollen unter anderem auch super- und this-Aufrufe von Konstruktoren, Überladung und Vererbung von Konstruktoren sowie Referenzierung von Konstruktoren durch new-Ausdrücke betrachtet werden. Zieltermin: 08.04.2005, 16.30 Uhr Gruppenzuordnung: Scanner + Parser + Modell + Typ-Kompatibilität: Gruppe 4 Scope-Komponente: Gruppe 2 Identifier-Komponente + Regie: Gruppe 1 (+ Analyse “Interfaces”) Erweiterung der Basistypen: Gruppe 3 (+ Analyse “Konstruktoren”) 10. Ausbaustufe: Konsolidierung und Konstruktoren Diese “Ausbaustufe” ist hauptsächlich der Fehlerbeseitigung und Restrukturierung des Programms gewidmet. Sie umfasst die folgenden Aufgaben: • Restrukturierung und Vervollständigung der Komponenten zur Prüfung von Syntax und Semantik (gemeinhin als “Checker” bekannt) • Korrektur der Behandlung von String und char (insbesondere werden Literale nicht korrekt erkannt) • Konstruktoren (erst einmal ohne super- und this-Aufrufe) Es wird von den bearbeitenden Gruppen erwartet, dass das übliche Maß an Qualität eingehalten wird (JavaDoc-Kommentare, eventuell zusätzliche Dokumentation und vor allem Testfälle!) 10.1 Restrukturierung und Vervollständigung der Checker-Komponenten Auszug aus einer Analyse der Checker-Komponenten (Referencer-Komponente = Identifier-Komponente): In der ursprünglichen Konzeption war der Checker dafür vorgesehen Konsistenzbedingungen im Modell zu prüfen und sicherzustellen. Da der Scanner und der Parser nicht alle Konsistenzbedingungen erfüllen können und auch das Modell inkonsistente Zustände erlaubt, ist es notwendig eine Komponenten zu haben, die nach Aufbau des Modells überprüft, ob es sich um ein standardkonformes Modell handelt. Die Gruppe, die sich mit dem Entwurf des Checkers auseinandergesetzt hatte, machte bereits zu Beginn den Fehler, Konsistenzbedingungen zu prüfen, die zu der Zeit gar nicht prüfbar waren. So wurde zum Beispiel ein Modul für den Checker geschrieben, der den Rückgabetyp von return-Instruktionen prüft, ob er mit dem Rückgabe-Typ der Methode übereinstimmt. Da es allerdings zu dieser Zeit keinen Auswerter für Ausdrücke gab, konnten komplexe Ausdrücke gar nicht überprüft werden und führten unweigerlich zu Fehlern. Ausschließlich einfachste Ausdrücke wie return 4 konnten geprüft werden. Weitere Module des Checkers haben keine Existenzberechtigung mehr, da die Aufgaben von anderen Komponenten effektiver und schneller überprüft werden können. So gibt es ein Modul, welches dafür sorgt, dass es keine Attribute mit gleichem Namen in einer Klasse gibt, und ein Modul, welches dafür sorgt, dass es keine Parameter von Operationen mit gleichem Namen gibt. Diese beiden Aufgaben übernimmt nun der Scope-Builder, der beim erstellen des Scopes sicherstellt, dass es keine doppelt definierten Namen gibt. Die einzige Aufgabe, die der Checker derzeit noch sinnvoll erfüllt ist, dass er mehrfach vorhandene Modifikatoren von Attributen, Operationen und Klassen erkennt und eine Ausnahme generiert. Da sich für die Weiterentwicklung des Checkers keine Entwicklergruppe verantwortlich fühlte, befindet sich der Checker mittlerweile in einem rudimentären Zustand und benötigt dringend eine Überarbeitung. Bei dieser Gelegenheit sollte der Checker insoweit angepasst werden, dass er alle bisher bekannten Konsistenzbedingungen überprüft und deren Verstöße meldet. Bei der Überarbeitung sollte man sich dann weiterhin Gedanken machen, ob es nicht sinnvoll ist, den Checker zu unterschiedlichen Zeiten mit unterschiedlichen Überprüfungen zu beauftragen, denn zur Zeit wird der Checker vor dem Referencer aufgerufen und anschließend nicht wieder. Einige Aufgaben des Checkers müssten vor dem Referencer aufgerufen werden, da der Referencer ein Modell erwartet, bei dem zum Beispiel die Modifikatoren auf Korrektheit geprüft wurden. Andere Aufgaben des Checkers dürfen erst nach dem Referencer aufgerufen werden, da erst dann die Typen aufgelöst und bestimmt wurden, um Rückgabewerte von return-Instruktionen auf Kompatibilität mit den Rückgabetypen von Operationen zu prüfen. Die Konsistenzbedingungen, die zu Beginn der Diplomarbeit überprüft werden müssen, lassen sich wie folgt dokumentieren: Vor dem Referencer: • Attribute dürfen keinen abstract-Modifikator besitzen • In Klassen ohne abstract-Modifikator (konkrete Klasse) dürfen keine Operationen mit abstract-Modifikator existieren • Methoden dürfen keinen abstract-Modifikator besitzen • Operationen müssen einen abstract-Modifikator besitzen • Es darf keine zwei Modifikatoren von der gleichen Modifikator-Art vor Methoden, Operationen, Attributen und Klassen geben. (“public private” ist verboten, “abstract static” ist verboten, auch “public public” ist nicht gestattet, “public abstract” ist erlaubt) • Der private-Modifikator darf nicht vor Klassen existieren • Klassen dürfen nicht von Basistypen abgeleitet werden • Alle geerbten Operationen müssen in konkreten Klassen implementiert sein • Alle im Modell gekennzeichneten Komposita müssen zyklenfrei sein. • Definierende Modell-Objekte dürfen sich nicht selbst referenzieren. So ist beispielsweise public class A extends A und public int a = a nicht erlaubt. Nach dem Referencer: • Zuweisungen müssen auf Typkompatibilität geprüft werden. Einer Variablen darf nur ein Wert zugewiesen werden, der in der Typhierarchie gleich oder spezieller ist. Bei den bisher vorhandenen Basistypen muss Typgleichheit herrschen. • Variablen- und Attributinitialisierungen müssen auch auf Typkompatibilität geprüft werden. • In Funktionsapplikationen muss sichergestellt werden, dass die Operanden-Typen zu dem Operator passen. (Der Referencer bestimmt zur Zeit den Typ einer Funktionsapplikation an Hand des Operators und nicht an Hand der Typen der Operanden. Dieses Verhalten muss bei Einführung von Operatorüberladung geändert werden.) • In Schleifen-Anweisungen (wie while und for) muss der Bedingungsausdruck einen boolschen Rückgabewert besitzen. Auf Grund des Umfangs werden zwei Gruppen für diese Aufgabe angesetzt: Die eine Gruppe befasst sich mit Regelprüfungen vor dem Durchlauf der Identifier-Komponente und die andere mit Regelprüfungen danach. 10.2 String und char Die letzte Umsetzung der entsprechenden Aufgabe war fehlerhaft. In dieser Ausbaustufe soll dies korrigiert werden. 10.3 Konstruktoren Das System soll um Konstruktoren und “alles drumherum” (etwa Überladung, new-Ausdrücke und Konstruktor-Applikationen in new-Ausdrücken) erweitert werden. Die Behandlung von superund this-Aufrufen soll dabei in dieser Ausbaustufe außen vor bleiben. Beispiel: class A { private int i; public A () { i = 0; } public A (int iValue) { i = iValue; } public int getI () {return i;} } class B { public B () { A a1 = new A (); A a2 = new A (42); A a3 = new A (a1.getI () + a2.getI ()); } } Zieltermin: 13.07.2005, 08.00 Uhr (!) Gruppenzuordnung: String und char: Konstruktoren: Prüfungen vor dem Identifier-Durchlauf: Prüfungen nach dem Identifier-Durchlauf: Gruppe 3 Gruppe 2 Gruppe 1 Gruppe 4 11. Ausbaustufe: Refaktorisierungen (I) Diese Ausbaustufe hat zum Ziel, eine Basis zu schaffen, auf deren Grundlage Refaktorisierungen von Java-Programmen durchgeführt werden können. Weiterhin sollen einige Refaktorisierungen bereits umgesetzt werden. 11.1 Refaktorisierungs-Framework Es soll ein Refaktorisierungs-Framework entwickelt werden, das die Grundlage für alle zukünftigen Refaktorisierungen bilden soll und gemeinsam genutzte Dienste und Schnittstellen umfassen soll. In dieser Ausbaustufe soll das Framework folgende Funktionalität enthalten: • Es soll eine Plug-in-Schnittstelle bereitgestellt werden, um Refaktorisierungs-Komponenten in das Framework “einklinken” und nutzen zu können. • Ein Dienst zum Protokollieren der durchgeführten Aktivitäten der Refaktorisierungs-Komponenten ist ebenfalls Teil des Frameworks. Dabei sollen nicht nur durchgeführte Aktivitäten, sondern auch unterlassene Aktivitäten protokolliert werden. Ein Beispiel für letzteres wäre bei einer Refaktorisierung, die für Attribute get- und set-Methoden hinzufügt, die Meldung, dass die hinzufügenden Methoden bereits unter diesem Namen in der jeweiligen Klasse existieren und somit nicht erzeugt werden konnten. • Das Framework soll die Möglichkeit anbieten, durchgeführte Refaktorisierungen zurücknehmen zu können (Undo-Funktionalität). Dabei sollen nach Möglichkeit die Refaktorisierungs-Komponenten entlastet werden und das Framework den Löwenanteil der Verwaltung und Durchführung des Undo-Mechanismus übernehmen. Eine Möglichkeit ist, sich das Modell in seiner Gesamtheit vor den Refaktorisierungen zu merken und hinterher wiederherzustellen. Eine sinnvolle Variante ist, von den Refaktorisierungs-Komponenten zu fordern, vor irgendwelchen Änderungen an einem Modell-Objekt das Framework zu benachrichtigen, so dass das Framework dieses Objekt sichern und später wiederherstellen kann. (Letzteres erfordert natürlich ein Protokoll zum Austausch von Undo-Informationen zwischen Framework und Komponenten über die Plug-inSchnittstelle.) 11.2 R1: Zugriff auf Attribute über get- und set-Methoden umleiten Diese Refaktorisierung hat zum Ziel, innerhalb einer Klasse alle direkten Zugriffe auf ein Attribut über entsprechende get- und set-Methoden umzuleiten und nur innerhalb dieser beiden Methoden den direkten Zugriff aufs Attribut zu belassen. Beispiel: public class A { private int x = 0; public int inc () { return this.x = this.x + 1; } } wird zu: public class A { private int x = 0; public int getX () { return this.x; } public void setX (int value) { this.x = value; } public int inc () { this.setX (this.getX () + 1); return this.getX (); } } Dabei soll die Komponente davon ausgehen, dass keine get- und set-Operationen existieren, die bewahrt werden müssten. Es werden also immer neue get- und set-Operationen erzeugt und alle bisherigen Attribut-Zugriffe darüber umgeleitet. Wenn die neuen Operationen auf Grund von Namenskollisionen nicht erstellt werden können, soll das protokolliert und die Refaktorisierung nur teilweise durchgeführt (wenn z. B. nur die set-Operation nicht generiert werden konnte) oder ggfs. ganz abgebrochen werden. Die Wahl der Namen für die get- und set-Operation ist ggfs. genauer zu prüfen, um auch in Vererbungssituationen das Richtige zu tun, etwa bei diesem Beispiel: class A { public int x = 0; public int Atest () {return x + 42;} } class B extends A { private int x = 0; public int Btest () {return x + 23;} } Ein Aufruf der Atest- oder Btest-Methode auf einem passenden A- oder B-Objekt soll nach der Refaktorisierung dasselbe liefern wie davor. 11.3 R2: Schnittstellen extrahieren Diese Refaktorisierung soll aus jeder Nicht-Schnittstellen-Klasse die enthaltene öffentliche Schnittstelle extrahieren und unter einem eigenen Namen als Schnittstellen-Klasse anbieten. Private, geschützte und package scoped-Operationen sollen dabei außen vor gelassen werden. Weiterhin sollen nach Möglichkeit alle Referenzen auf die Klassen durch Referenzen auf die generierten Schnittstellen ersetzt werden. Bereits existierende Schnittstellen sowie Vererbungsbeziehungen sollen dabei berücksichtigt werden. Beispiel: public interface I1 { void i1 (A1 a1); } public interface I2 { void i2 (C1 c1); } public interface I3 { void i3 (); } public interface I4 { void i4 (); } public interface I12 extends I1, I2 {} public abstract class A1 implements I1, I2, I3, I12 { public abstract A1 a1 (); private void p1 () {} } public class C1 extends A1 implements I1, I3, I4 { public void i1 (A1 a1) {} public void i2 (C1 c1) {} public void i3 () {} public void i4 () {} public A1 a1 () {return new C1();} public C1 c1 (C1 c) {return c;} protected void g1 () {} private void p2 () {} } führt zu folgenden zusätzlichen Schnittstellen und Änderungen in den Klassen-Köpfen: public interface I1 { void i1 (IA1 a1); } public interface I2 { void i2 (IC1 c1); } public interface IA1 extends I3, I12 { IA1 a1 (); } public abstract class A1 implements IA1 { public abstract IA1 a1 (); ... } public interface IC1 extends IA1, I4 { IC1 c1 (IC1 c); } public class C1 extends A1 implements IC1 { public void i1 (IA1 a1) {} public void i2 (IC1 c1) {} ... public IA1 a1 () {return new C1();} public IC1 c1 (IC1 c) {return c;} ... } Für die Bestimmung der Schnittstellen in der extends-Klausel einer generierten Schnittstelle (etwa IA1 im obigen Beispiel) soll ein Algorithmus entwickelt werden, um die minimale Anzahl an Schnittstellen einfließen zu lassen. (Im obigen Beispiel konnten I1 und I2 in der extends-Klausel weggelassen werden, weil beide in der – ebenfalls verwendeten – Schnittstelle I12 bereits enthalten sind.) Die im Beispiel verwendeten Namen für die Schnittstellen sind keine “offizielle” Vorgabe; andere, sinnvollere Namenskonventionen sollten gewählt werden. 11.4 R3: Inline-Substitution von Methodenaufrufen Diese Refaktorisierung soll nach Möglichkeit Methodenaufrufe eliminieren, indem deren Code in den Aufruf “hineinsubstituiert” wird. Beispiel: class A { public int berechne (int x) {return x + this.f(x) + 42;} public int f (int z) {return z + 23;} } class B { A a = new A (); public int test () { int y = 23; return this.a.berechne (this.y); } } wird im ersten Schritt zu: class A { public int berechne (int x) {return x + (x + 23) + 42;} public int f (int z) {return z + 23;} } class B { A a = new A (); public int test () { int y = 23; int r1 = (this.y + this.a.f(this.y) + 42); return r1; } } und in einem zweiten Schritt zu: class A { public int berechne (int x) {return x + (x + 23) + 42;} public int f (int z) {return z + 23;} } class B { A a = new A (); public int test () { int y = 23; int r1 = this.y + (this.y + 23) + 42; return r1; } } Wie man sieht, muss eine solche Substitution eventuell mehrfach durchgeführt werden (natürlich nicht unbedingt in dieser Reihenfolge). Wichtig ist nur, dass so viele Substitutionen wie nur möglich durchgeführt werden. (Zum Behandeln von Rekursion siehe unten.) Dabei muss die Substitution die Semantik erhalten. Das bedeutet, dass z. B. keine Substitution durchgeführt werden kann, wenn die betrachtete Methode zu einer anderen Klasse gehört und auf deren private Elemente zugreift, auf welche die aufrufende Klasse keinen Zugriff hat. Vorsicht ist auch geboten beim Behandeln der Parameter: Weil Parameter immer kopiert werden, müssen eventuell temporäre Variablen eingeführt werden. Beispiel: class A { public void tuNixSinnvolles (int n) { n = n + 1; } public void aufrufTest () { int z = 42; tuNixSinnvolles (z); } } Hier muss z nach der Substitution von tuNixSinnvolles beim Rücksprung aus der Methode aufrufTest immer noch den Wert 42 enthalten. Generell ist das Einführen temporärer Variablen erlaubt, auch wenn die temporären Variablen wegoptimiert werden könnten. Das Wichtige ist lediglich, dass die Semantik durch deren Einführung nicht verändert wird. Insbesondere dürfen keine Namenskonflikte mit bereits existierenden Variablen auftreten. Es ist auch – entgegen der Programmierrichtlinien – erlaubt, derart eingeführte temporäre Variablen bei der Definition nicht zu initialisieren, sofern gewährleistet ist, dass sie vor ihrer Verwendung einen Wert zugewiesen bekommen. (Siehe die temporären Variablen r1 in dem folgenden Beispiel für eine solche Situation.) Direkte Rekursion soll verbleiben. Indirekte Rekursion soll in direkte aufgelöst werden. Beispiel: class A { public int fac(int n) { if (n == 0) return 1; else return n*fac2(n – 1); } public int fac2(int n) { if (n == 0) return 1; else return n*fac(n – 1); } } wird zu: class A { public int fac(int n) { if (n == 0) return 1; else { int r1; if ((n – 1) == 0) r1 = 1; else r1 = (n – 1)*fac((n – 1) – 1); return n*r1; } } public int fac2(int n) { if (n == 0) return 1; else { int r1; if ((n – 1) == 0) r1 = 1; else r1 = (n – 1)*fac2((n – 1) – 1); return n*r1; } } } Zieltermin: 15.08.2005 Gruppenzuordnung: Framework: R1 (Umleiten des Zugriffs auf Attribute): R2 (Schnittstellen extrahieren): R3 (Inline-Substitution): Gruppe 2 Gruppe 1 Gruppe 4 Gruppe 3 12. Ausbaustufe: Refaktorisierungen (II) und Erweiterungen In dieser Ausbaustufe sollen zum einen die Refaktorisierungen vorangetrieben und zudem eine Programm-Komponente entwickelt werden, mit der man benutzerfreundlich über eine graphische Schnittstelle die Funktionalität der Refaktorisierungs-Komponenten und des Frameworks nutzen kann. Zum anderen wird das Java-Modell um weitere syntaktische und semantische Elemente ergänzt. Weiterhin soll eine komplette Dokumentation des Ist-Zustandes erarbeitet bzw. zusammengestellt werden. Schließlich sollen einige Fehler, die im Laufe der Entwicklung “verbrochen” aber noch nicht korrigiert worden sind, ausgemerzt werden. 12.1 Modell-Browser und Anbindung an das Refaktorisierungs-Framework Es soll ein System entwickelt werden, mit dem ein Java-Objektmodell dargestellt und bearbeitet werden kann. Das Java-Objektmodell wurde dabei vorher über einen Durchlauf des Java-Systems erzeugt. Die Funktionalität zur Bearbeitung des Java-Modells soll sich dabei bewusst auf Refaktorisierungs-Komponenten stützen, die das Refaktorisierungs-Framework anbietet. Die Darstellung und Bearbeitung des Modells soll mit Hilfe eine graphischen Benutzungsoberfläche erfolgen; die strikte Trennung von Modell- und Controller/View-Funktionalität soll besonders berücksichtigt werden. Eine einfache Navigation zwischen den einzelnen Modell-Objekten ist ausreichend; weiter reichende Funktionalität (etwa Suchen von Objekten über deren Namen o. ä.) ist nicht notwendig. 12.2 Refaktorisierungen (II) und super/this Das Refaktorisierungs-System soll um grundlegende Refaktorisierungen erweitert werden, die das Arbeiten mit dem Modell-Browser sinnvoll ergänzen. Wichtig sind insbesondere Erstellung, Umbenennung und Löschung (sofern möglich) von den möglichen Modell-Objekten (Klassen, Operationen, Methoden, Attribute u. s. w.) Des Weiteren soll das Java-System syntaktisch und semantisch in allen Komponenten um die Schlüsselwörter super und this erweitert werden, sofern dies noch nicht geschehen ist. Ziel dabei ist es, mit Hilfe von super Elemente der Basisklasse(n) und mit this Elemente der eigenen Klasse referenzieren zu können. Schließlich soll erreicht werden, dass vom Identifier identifizierte Attribute, auf die ohne this zugegriffen wird, im Modell zu AttributeSelectionExpression-Ausdrücken umgewandelt werden. 12.3 Ausnahme-Behandlung Das Java-Modell soll syntaktisch und semantisch um Elemente zur Ausnahme-Behandlung ergänzt werden. Hierzu gehören: • Scannen und Parsen der Schlüsselwörter try, catch, throw, throws (finally soll dabei außen vor bleiben) • Abbildung von Java-Klassen, die für die Java-Ausnahme-Behandlung eine Rolle spielen (Throwable, Exception, RuntimeException, Error) • Semantische Überprüfung der Korrektheit von throws-Klauseln bei Methoden und im Falle von Vererbung (Fragestellungen sind z. B.: Sind alle Ausnahmen, die eine Methode verlassen können, durch die throws-Klausel abgedeckt (abgesehen von den in der Java-Spezifikation erlaubten Abweichungen zu dieser Regel)? Ist eine throws-Klausel einer spezielleren Operation unerlaubterweise allgemeiner als die der allgemeineren Operation?) • Semantische Überprüfung der Korrektheit von catch-Blöcken (Fragestellungen sind z. B.: Kann eine durch einen catch-Block aufgefangene Ausnahme wirklich auftreten? Ist der deklarierte Typ des catch-Parameters vom Typ Throwable oder spezieller?) • Semantische Überprüfung der Korrektheit von throw-Anweisungen (eine Fragestellung ist z. B., ob der Typ des Arguments Zuweisungs-kompatibel zu Throwable ist) 12.4 Dokumentation Es soll eine Dokumentation des Ist-Zustandes angefertigt werden, aus der ersichtlich ist, welche Eigenschaften und Funktionen das Java-System und das Refaktorisierungs-Framework (samt Komponenten) zur Zeit beinhalten. Außerdem soll die Aufstellung derjenigen Java-Sprachelemente bzw. -Konzepte, die vom System noch nicht bzw. unzulänglich unterstützt werden, auf den neuesten Stand gebracht werden. Die Grundlage für die Dokumentation ist dabei der erreichte Stand dieser Ausbaustufe. 12.5 Offene Punkte In dieser Ausbaustufe sollen schließlich einige der noch offenen Punkte behoben werden. Details zu den Fehlern sind im Bugzilla des Projekts zu finden. Die Zuordnung der Gruppen zu den einzelnen Code-Teilen bei Projekt-übergreifender Fehlerbehebung (Fehler 57 & 58) ist wie folgt: Scanner, Scope-Builder, Refakt.-Framework: Parser, Checker, R2: Treiber, Printer, R1: Referencer, Modell & Modelldienste, R3: Gruppe 1 Gruppe 2 Gruppe 3 Gruppe 4 • Fehler 13: wird in der Dokumentation als “nicht erledigt” vermerkt • Fehler 32: wird vorerst so belassen und entsprechend dokumentiert • Fehler 42: Dokumentation des gewählten Work-arounds durch die Dokumentationsgruppe • Fehler 52: Gruppe 4 + Marcel • Fehler 56: Gruppe 3 • Fehler 57: betrifft gesamtes Projekt, Aufteilung der Gruppen siehe oben • Fehler 58: betrifft gesamtes Projekt, Aufteilung der Gruppen siehe oben; bis zum 29.08.2005 Prüfung, ob vorgeschlagene Vorgehensweise (Entfernung sämtlichen “No-Operation”-Codes in catch-Blöcken außer im Parser) möglich, danach Implementierung der Lösung Zieltermin: 05.09.2005 (Dokumentation: 12.09.2005) Gruppenzuordnung: Modell-Browser: Refaktorisierungen (II) und super/this: Ausnahme-Behandlung: Dokumentation: Gruppe 1 Gruppe 4 Gruppe 2 Gruppe 3 Gruppe HFI404 / HFW404 Organisatorische Rahmenbedingungen Das Integrationsprojekt findet innerhalb der drei Theoriequartale des Hauptstudiums durchgängig statt. Es findet keine Klausur statt, vielmehr errechnet sich die Benotung aus den Einzelbewertungen der einzelnen Aufgaben pro Quartal. Da keine Klausur geschrieben wird, werden volle zwölf Wochen des Quartals genutzt; die letzte Aufgabe reicht also in die Klausurphase hinein, was bei der Zeiteinteilung beachtet werden sollte. Die Entwicklung wird von vier Gruppen durchgeführt, wobei jede Gruppe aus vier bis fünf Entwicklern besteht. Im ersten Quartal werden die Gruppen nach der Hälfte des Quartals neu kombiniert, ab dem zweiten Quartal wird gelost. Ganz im Sinne von “Pair Programming” im Rahmen von Extreme Programming soll dies erreichen, dass sich keine “eingefahrenen Gleise” aufbauen und dass durch den Kontakt mit verschiedenen Entwicklern das gegenseitige Lernen voneinander gefördert und Synergie-Effekte ausgenutzt werden. Jede Gruppe bekommt für den Zeitraum von zwei bis drei Wochen – je nach Schwierigkeitsgrad – eine Aufgabe zugeteilt. Diese Aufgabe besteht in der Regel aus dem Anfertigen von Programmcode zum Lösen einer bestimmten Problemstellung und dem Erstellen entsprechender Testfälle und Dokumentation. Am Ende des Zeitraums präsentiert jede Gruppe ihre Ergebnisse vor allen versammelten Entwicklern. Die (Teil-)Note berechnet sich dann aus der Qualität der Lösung inklusive der Dokumentation sowie aus der Präsentation. Gelegentlich sind die Aufgaben analytischer Natur, so dass kein Programmcode geschrieben werden muss; stattdessen muss eine Problemstellung oder vorhandener Programmcode gründlich unter die Lupe genommen werden. 1. Aufgabe: Einarbeitung Diese Aufgabe hat das Ziel, dass alle Entwickler sich in das bestehende Projekt einarbeiten. Dabei soll jede Gruppe einen bestimmten Teilaspekt des Projekts untersuchen und ihn den anderen Entwicklern präsentieren. Dadurch soll jeder Entwickler am Ende einen Überblick über das Gesamtsystem bekommen und grob wissen, welche Funktionalität an welcher Stelle angesiedelt ist und wie der allgemeine Programm-Ablauf aussieht. Zusätzlich wird jede Gruppe sich in bestimmte (Entwurfs-)Muster einarbeiten, die in ihrem Kontext von Bedeutung sind, und diese Entwurfs-Muster den anderen präsentieren. Dadurch soll erreicht werden, dass alle Entwickler die verwendeten Entwurfs-Muster durchschauen und sie in späteren Aufgaben problemlos einsetzen können. Es werden vier Referate verteilt: 1.1 Scanner + Parser (Entwurfsmuster: State + Grammatik-Umsetzung) Diese Gruppe hat die Aufgabe, sich in die Komponenten Scanner und Parser des Java-Projekts einzuarbeiten und über die wesentlichen Strukturen zu referieren, insbesondere die Funktionsweise des Scanners und Parsers. Für ersteren ist das State-Muster wesentlich, für letzteren ist wichtig zu verstehen, nach welchem Rezept die formale Grammatik der Programmiersprache Java in Programmkonstrukte umgesetzt wird. 1.2 Modell (Entwurfsmuster: Proxy, Composite, Singleton) Diese Gruppe soll die Zusammenhänge zwischen den verschiedenen Klassen aufzeigen, die zum Modell gehören, also die Informationen des analysierten Programmcodes kapseln. Den in diesem Zusammenhang verwendeten Muster Proxy, Composite und Singleton ist besondere Beachtung zu schenken. 1.3 Scope-Builder + Referencer (Entwurfsmuster: Visitor + Programmier-Richtlinien) Diese Gruppe beschäftigt sich mit den wichtigen Komponenten Scope-Builder und Referencer, deren Aufgabe es ist, in einem geparsten Programm Namen korrekt aufzulösen und ihnen die richtige Bedeutung zuzuweisen. Hierbei soll das Visitor-Muster behandelt werden, da es wesentlich für das Arbeiten am vorhandenen Modell ist, wie es der Referencer tut, um im Programm verwendete Namen zu finden und diese geeignet aufzulösen. Im Zusammenhang mit Visitoren sind auch einige spezielle Richtlinien einzuhalten, über die ebenfalls referiert werden soll. 1.4 Refaktorisierung + Drucker (Entwurfsmuster: Observer) Diese Gruppe soll sich mit dem Drucker sowie dem Refaktorisierungs-Rahmenwerk inklusive zweier Refaktorisierungen (Getter/Setter einbauen + Schnittstellen herausziehen) auseinandersetzen. Zusätzlich ist das Observer-Muster abzuhandeln, das im Refaktorisierungs-Rahmenwerk und den entsprechenden Plug-ins Verwendung findet. Zieltermin: 25.07.2006 Gruppenzuordnung: Scanner + Parser: Modell: Scope + Referencer: Refaktorisierung + Drucker: Dennis, Christoph, Markus S., Stefan Hu. Markus K., Hendrik, Julia, Stefanie Tobias, Artur, Michael, Stefan R. Stefan Ho., Johannes, Sebastian, Janko, Norman 2. Aufgabe: 13. Ausbaustufe: Refaktorisierung, Pakete und Analysen Diese Ausbaustufe erweitert die Fähigkeiten des Java-Werkzeugs um die folgenden Punkte: 1. Vereinheitlichung von Operatoren und Operationen 2. Unterstützung von Paketen und qualifizierten Namen 3. Kontrollflussanalyse (I) 4. Analyse-Plug-ins Die einzelnen Themen werden in den nächsten Abschnitten detaillierter erläutert. 2.1 Vereinheitlichung von Operatoren und Operationen Das bisherige Konzept, (eingebaute) Operatoren und (Benutzer-definierte) Operationen voneinander zu trennen, bewährt sich nicht, wenn die Vielfalt der eingebauten Java-Operatoren ins Spiel kommt. Die Trennung sorgt für Probleme bei der semantischen Analyse, da pro Operator-Symbol nur eine Operator-Definition existiert, auch wenn der Operator semantisch mehrere Funktionen besitzt (Beispiele: “+” dient der Addition von Zahlen und der Verkettung von Zeichenketten; “==” vergleicht entweder zwei Zahlen oder zwei Wahrheitswerte oder zwei Objekt-Referenzen). Besser ist es, die eingebauten Operatoren als spezielle (freie) Funktionen im globalen Gültigkeitsbereich zu sehen, die entsprechend typisierte Parameter und einen passenden Rückgabetyp besitzen; dies erlaubt es nun, die obigen Beispiele als entsprechende Überladungen desselben Operators anzusehen. Dadurch können Sonderbehandlungen von eingebauten Operatoren gänzlich vermieden werden. Zum Zwecke der Vereinheitlichung bietet es sich an, bestehende Operationen, Methoden und Konstruktoren ebenfalls als freie Funktionen anzusehen, die einen zusätzlichen Parameter, das Empfänger-Objekt, erhalten. Somit kann die gesamte Namensauflösung und Auflösung der Überladung für Operatoren, Operationen und Konstruktoren über einen einheitlichen Mechanismus geregelt werden. Beispiel: Der Additions-Operator “+” bekommt die Signaturen int +(int x, int y); String +(String x, String y); Der Vergleichsoperator “==” erhält die Signaturen: boolean ==(int x, int y); boolean ==(boolean x, boolean y); boolean ==(Object x, Object y); In den Klassen public class Number { private int x; public Number (int x) {this.x = x;} public int value () {return this.x;} public Number square () {return new Number(this.x * this.x);} public Number add (int delta) {return new Number(this.x + delta);} public Object clone() {return new Number(this.x);} } public class MyNumber extends Number { public MyNumber (int x) {super(x);} public Object clone() {return new MyNumber(this.value());} } bekommen die Operationen und der Konstruktor die folgenden Signaturen: /* class Number */ public Number(int x); public int value(Number this); public Number square(Number this); public Number add (Number this, int delta); public Object clone(Number this); /* class MyNumber */ public MyNumber(int x); public Object clone(MyNumber this); wobei this der neue, implizite Empfänger-Parameter ist, den nur Operationen und Methoden, nicht aber Konstruktoren erhalten. 2.2 Unterstützung von Paketen und qualifizierten Namen Das Java-Werkzeug soll um Pakete und qualifizierte Namen erweitert werden. Ein qualifizierter Name ist ein Name, der mindestens einen “.”-Separator enthält, etwa java.lang.String. Generell haben Namen folgenden Aufbau: name ::= simple-name + qualified-name simple-name ::= identifier qualified-name ::= name "." identifier Die Schwierigkeit hierbei ist es, zum einen zu entscheiden, ob ein qualifizierter Name oder ein “.”-Operator vorliegt: a.B b; // ein qualifizierter Name: die Klasse B des Pakets a b.c = 5; // der Punkt-Operator: das Attribut c des Objekts b (zur Klasse B) Zum anderen können Teile eines qualifizierten Namens unterschiedliche Entitäten sein: Bei dem Namen a.b.c.d können die einzelnen Teile folgendermaßen zusammenhängen: • a.b.c.d ist ein Paket • a.b.c ist ein Paket, d ist eine Klasse • a.b ist ein Paket, c ist eine Klasse, d ist ein statisches Attribut • a.b ist ein Paket, c und d sind Klassen (letztere ist eine geschachtelte oder innere Klasse) • a ist ein Paket, b und c sind Klassen, d ist ein statisches Attribut • a, b, c und d sind alles statische Attribute vom Typ einer Klasse In dieser Ausbaustufe müssen nur die ersten beiden Verwendungen unterstützt werden; es soll aber versucht werden, die notwendigen Änderungen am Modell, am Parser und an den zugehörigen Algorithmen der Namensauflösung möglichst allgemein zu halten, um die späteren Verwendungsweisen später leicht integrieren zu können. Die Unterstützung von Paketen ist dahingehend anzufertigen, als dass jede definierte globale Komponente (Klasse, Schnittstelle) über eine package-Klausel “ihr” Paket zugewiesen bekommt und deren Name über einen entsprechend qualifizierten Namen aufgelöst werden kann. Auch das Konzept des Standard-Paketes soll umgesetzt werden; das bedeutet, dass eine package-Angabe möglich, aber nicht erforderlich ist. Es ist aber nicht erforderlich, import-Klauseln zu unterstützen. Gegebenenfalls ist zu analysieren, welche Modell-Elemente für die Aufgabe bereits zur Verfügung stehen und welche Komponenten – wenn auch nur teilweise – bereits jetzt Teile der Aufgabenstellung unterstützen. 2.3 Kontrollflussanalyse (I) Der bisher vernachlässigte Bereich der Kontrollflussanalyse soll vorangetrieben werden. In dieser Ausbaustufe lautet die Zielsetzung, folgende ungültige Konstrukte zu erkennen: • “Toter” Code: Code, der nie ausgeführt werden kann, weil bestimmt werden kann, dass die Methode vorher auf jeden Fall verlassen wird. Beispiel: int f () { return 5; return 6; // toter Code, wird nie erreicht! } • Fehlende return-Anweisungen. Beispiel: boolean greater(int x, int y) { if (x > y) return true; // Achtung: return fehlt! } In jedem Fall sind in dieser Ausbaustufe die Werte von (konstanten) Ausdrücken nicht in die Analyse einzubeziehen. Das bedeutet beispielsweise, dass der tote Code im folgenden (inkorrekten) JavaCode von der Kontrollflussanalyse nicht erkannt wird: void doNothing() { while (false) { int i = 1; // toter Code, wird aber nicht erkannt } } Hier wird der tote Code nicht erkannt, weil der Wert der Bedingung (false) nicht in die Analyse einfließt. 2.4 Analyse-Plug-ins Das Java-Werkzeug unterstützt bisher Refaktorisierungen über sog. Refaktorisierungs-Plug-ins, die mit Hilfe einer Browser-ähnlichen Oberfläche angestoßen und durchgeführt werden. Dieses Vorgehens-Konzept soll nun auf Analyse-Werkzeuge ausgeweitet werden. Das bedeutet im Einzelnen: • die Entwicklung einer entsprechenden Analyse-Plug-in-Abstraktion, da die Plug-in-Abstraktion für Refaktorisierungen auf Grund unterschiedlicher Anforderungen nicht geeignet ist, • die Entwicklung einer passenden Plug-in-Verwaltung ähnlich dem bestehenden Plug-in-Framework für Refaktorisierungen, • Einbindung der Verwaltung in die Browser-Anwendung und • das Anfertigen von einigen ersten Analyse-Plug-ins. Anzufertigen sind folgende Analysen: 1. Maximale und Ø Anzahl von Operationen und Methoden pro Klasse und Schnittstelle 2. Maximale Tiefe und Breite der Vererbungshierarchie 3. Maximale und Ø Anzahl von Anweisungen pro Methode 4. Maximale und Ø Schachtelungstiefe von Anweisungen Zieltermin: 29.08.2006 Gruppenzuordnung: Operatoren und Operationen: Pakete und qualifizierte Namen: Kontrollflussanalyse: Analyse-Plug-ins: Markus K., Hendrik, Julia, Stefanie Dennis, Christoph, Markus S., Stefan Hu. Stefan Ho., Johannes, Sebastian, Janko, Norman Tobias, Artur, Michael, Stefan R. 3. Aufgabe: 14. Ausbaustufe: Importe, Operatoren, Analysen und Felder In dieser Ausbaustufe soll hauptsächlich die in der letzten Ausbaustufe integrierte Funktionalität erweitert werden: • zusätzliche Operatoren erweitern den Sprachumfang • Importe vereinfachen die Benutzung von Namen aus anderen Paketen • die Kontrollfluss-Analyse wird durch die Behandlung konstanter Ausdrücke verbessert • der Sprach-Kern wird um eingebaute Felder (Arrays) erweitert Die einzelnen Themen werden in den nächsten Abschnitten detaillierter erläutert. 3.1 Zusätzliche Operatoren + Koordination Diese Aufgabe umfasst das Analysieren und Hinzufügen fehlender Signaturen zu bereits existierenden und erkannten Operatoren (relevante Teile von §15) sowie das Implementieren der Inkrementund Dekrement-Operatoren in den Präfix- und Postfix-Varianten (§15.14.1, §15.14.2, §15.15.1, §15.15.2). Beispiel zur ersten Teilaufgabe: Der Operator “+” wird unterstützt, allerdings fehlt beispielsweise die Signatur String +(String lhs, String rhs) Beispiel zur zweiten Teilaufgabe: Unter Annahme der Definition int i; sollen die Ausdrücke ++i i++ --i i-- übersetzt und im Modell angemessen dargestellt werden. Die Gruppe, welche die Umsetzung dieser Aufgabe übernimmt, soll zusätzlich als Koordinator für die verschiedenen Gruppen fungieren und Arbeiten “neben der Reihe”, also dringende Fehler-Bereinigungen, Pflege der Bugzilla-Datenbank etc. übernehmen. 3.2 Importe Das Programm soll in der Lage sein, Import-Deklarationen (§7.5) beim Referenzieren von Namen zu berücksichtigen. Beispiel: // Datei a/X.java package a; import b.T; // single-type import declaration class X extends T {} // Datei b/T.java package b; import a.*; // type-import-on-demand declaration class T { private X x; } 3.3 Kontrollflussanalyse (II) Bisher wurde die Kontrollfluss-Analyse (§14.20) ohne Berücksichtigung von Ausdrücken durchgeführt. Nun soll die Analyse durch die Betrachtung konstanter Ausdrücke verbessert werden. Beispiel 1: Die Schleife int i = 0; while (false) { i = i + 1; } muss vom Übersetzer zurückgewiesen werden, weil der Inhalt der Schleife nicht erreichbar ist. Beispiel 2: Auch der Inhalt der Schleife int i = 0; while (2 – (4 > 5 ? 3 : 4) + 2 != 0) { i = i + 1; } ist nicht erreichbar, obwohl es auf den ersten Blick nicht offensichtlich scheinen mag. In beiden Fällen ist jedoch der -Kontroll-Ausdruck ein sog. konstanter Ausdruck (§15.28), der zur Übersetzungszeit auswertbar ist und in die Analyse einfließen kann. Details zu dem Thema, bei welchen Konstrukten während der Kontrollfluss-Analyse Ausdrücke analysiert werden, sind §14.20 der JavaSpezifikation zu entnehmen. Zusätzlich soll eine detaillierte Analyse von throw-Anweisungen und catch-Blöcken erfolgen. Beispiel: public class AException extends Exception { ... } public class BException extends Exception { ... } public class X { public int f () throws AException { ... } public int g () throws BException { ... } public int test1 () throws Exception { try { return f () + g (); } catch (AException e) { return 42; } return 5; // Unreachable Code } public int test2 () { try { return f () + g (); } catch (AException e) { return 42; } catch (BException e) { } return 5; // kein Unreachable Code } public int test3 () { int result = 0; try { result = f () + g (); } catch (AException e) { return 42; } catch (BException e) { return 43; } return result; // kein Unreachable Code } public int test4 () { try { throw new AException (); } catch (Exception e) { } return 42; // kein Unreachable Code } public int test5 () { try { return f (); } catch (Exception e) { return 42; } catch (AException e) { // Unreachable Code return 43; } } } 3.4 Unterstützung von Feldern (Arrays) Diese Aufgabe umfasst das Umsetzen von eingebauten Feldern (Arrays), und zwar in folgendem Umfang: • Array-Typen (z.B. int[], aber auch mehrdimensionale Felder) sollen geparst und im Modell abgelegt werden können, • Definitionen von Array-Variablen und -Attributen sollen erlaubt sein, • Initialisierung von Arrays mit new-Ausdrücken soll möglich sein, • Indizierung von Array-Objekten soll ermöglicht werden. Folgende Funktionalität soll erst einmal nicht umgesetzt werden: • das Feld length und die Methode clone sowie alle anderen Object-Methoden, • Literale zur Initialisierung von Feldern (z.B. {1,2}), • alternative Schreibweise von Feldern (int a[] statt int[] a). Beispiel: Das folgende kleine Programm sollte fehlerfrei akzeptiert werden: class Screen { private int height; private int width; private char[][] buffer; public Screen(int height, int width) { this.height = height; this.width = width; this.buffer = new char[height][width]; for (int line = 0; line < height; line = line + 1) for (int col = 0; col < width; col = col + 1) this.set (width, col, ' '); } public int getWidth() { return this.width; } public int getHeight() { return this.height; } public char get(int line, int col) { return this.buffer[line][col]; } public void set(int line, int col, char c) { this.buffer[line][col] = c; } } Zieltermin: 12.09.2006 Gruppenzuordnung: Operatoren + Koordination: Importe: Kontrollflussanalyse: Stefanie, Markus K., Stefan Hu., Dennis Janko, Stefan Ho., Markus S., Christoph Sebastian, Norman, Johannes, Artur, Michael Felder (Arrays): Julia, Hendrik, Tobias, Stefan R. Anmerkung: Wie angekündigt werden die Gruppen für die Bearbeitung dieser und der nächsten Aufgaben rekombiniert. Dabei wird wie folgt vorgegangen: 1. Jede Gruppe teilt sich selbständig in zwei Teilgruppen à zwei Leute auf, mit Ausnahme der Fünfer-Gruppe, die sich in eine Zweier- und eine Dreier-Gruppe aufteilt. 2. Die Gruppe bestimmt unter sich, welche Teil-Gruppe bleibt (und somit die Bearbeitung der ursprünglichen Aufgabenstellung fortsetzt) und welche Teil-Gruppe sich einer anderen Aufgabe anschließt. Nur entschärft gilt dies auch für die Analyse-Plug-in-Gruppe, da die Aufgabe ohnehin wechselt (Arrays). 3. Die Teil-Gruppe, die zu einer anderen Aufgabe wechselt, sucht sich diese selbst aus. Bei mehreren Interessenten für eine Aufgabe wird wie bei der anfänglichen Aufgabeverteilung gelost. 4. Aufgabe: 15. Ausbaustufe: Konsolidierung In dieser Ausbaustufe sind alle Entwickler aufgefordert, bestehende Fehler und Entwurfsschwächen auszumerzen. Das Ziel ist, alle bekannten Fehler zu beheben. Dazu ist folgende Vorgehensweise einzuhalten: 1. Alle Gruppen erarbeiten bis zum Ende der Vorlesungszeit eine Liste aller zu behebenden Fehler zusammen mit einer groben Lösungsstrategie, die mit den jeweiligen Betreuern abgesprochen ist. Das Nichtbearbeiten eines Fehlers muss mit beiden Betreuern abgesprochen sein. 2. Die Aufgaben werden von den Entwicklern selbständig und gerecht auf die vier Gruppen verteilt. 3. Die einzelnen Gruppen bearbeiten in der vorlesungsfreien Zeit alle ihnen zugeordneten Fehler. Während der Bearbeitungsphase werden Besprechungstermine mit dem jeweiligen Betreuer abgesprochen. Die ersten drei Termine werden alle zwei Wochen, die folgenden alle drei Wochen durchgeführt. Wenn weiterer Gesprächsbedarf besteht, muss dies mit dem jeweiligen Betreuer abgesprochen werden. Die Bewertung orientiert sich nach der erbrachten Leistung. Dazu werden die Fehler grob in drei Schwierigkeitsgrade (leicht, mittelschwer, schwer) eingeordnet; diese Kategorien dienen der Gewichtung der einzelnen Aufgaben. Zieltermin für Punkte (1) und (2): 29.09.2006 Zieltermin für Punkt (3): 01.01.2007 Gruppenzuordnung: Siehe Bugzilla 5. Aufgabe: 16. Ausbaustufe: Java ++: Pre-Prozessor für Java zur Unterstützung von Standardmustern (1) Viele objektorientierte Muster (OOM) sind soweit standardisiert, dass sie in konkrete Syntax gegossen und durch geeignete Werkzeuge weitgehend unterstützt werden können. Eine solche Erweiterung der Syntax erfordert keine semantische Anpassung, da die Semantik durch ein standardisiertes Zusammenspiel verschiedener OOM's erklärt werden kann. Zur Verarbeitung bietet sich also ein Pre-Prozessor an, der die erweiterte Syntax versteht, der dem Anwender viele hilfreiche Hinweise zur korrekten Benutzung der Konzepte geben kann und der schließlich die erweiterte Syntax nach Java übersetzt. In diesem Quartal wollen wir einen solchen Pre-Prozessor konzipieren und entwickeln. Wir beginnen in dieser Aufgabe mit der Konzeption. Präsentation der Ergebnisse: 22. Januar 2007 5.1 Ereignisse Java unterstützt Ausnahmeereignisse durch die throw-, throws- und catch-Klauseln. Wir wollen ein ähnliches Konzept zur allgemeinen Kommunikation über Ereignisse entwickeln. Dazu sollen Objekte zu speziellen Ereignisklassen (analog zu Exception) die Ereignisse darstellen. Analoge Konzepte zu den throw-, throws- und catch-Klauseln sind zu konzipieren. Damit der Mechanismus ein Kommunikationsmittel auch zwischen Objekten wird, sollen die Ereignisse, die Objekte einer Klasse erzeugen können, auch an der Schnittstelle der Klasse spezifiziert werden. Nutzer solcher Objekte können an der Nutzungsstelle (Attribute) auf diese Ereignisse reagieren. Dazu ist eine geeignete Syntax zu entwickeln. Die Semantik für dieses Konzept soll durch die Übersetzung in geeignete Observer-Muster bereitgestellt werden. 5.2 Spezialisierung/Generalisierung von Operationen in Argumenten und Rückgabetypen Java lässt nur die Spezialisierung des Empfängers einer Nachricht zu. Wir wollen auch die Spezialisierung bzw. Generalisierung der Parameter und des Rückgabetyps von Operationen zulassen. Der Pre-Prozessor muss dazu die Korrektheit der Spezialisierungsbeziehungen prüfen und ggf. Fehlermeldungen liefern. Die Semantik soll durch eine geeignete Übersetzung in Visitoren geliefert werden. 5.3 Mehrfachvererbung Wir erweitern Java um Mehrfachvererbung. Der syntaktische Eingriff dazu ist relativ klein. Die Semantik soll durch eine Auflösung der Vererbung durch Delegation definiert werden. Achtung: Hier ist insbesondere der Fall der Mehrfachvererbung einer Ressource über mehrere Erweiterungspfade zu berücksichtigen! 5.4 Monitore und Synchronisationsbedingungen Java verfügt über kein wirkliches Monitor-Konzept. Allerdings lassen sich das Monitorkonzept und entsprechende Synchronisationsbedingungen mit den Primitiven von Java zur Synchronisation darstellen, siehe Skript zur Nebenläufigkeit von M. Löwe. Es soll eine geeignete Syntax für Monitore entwickelt werden, in denen Synchronisationsbedingungsobjekte definiert werden können, die im Wesentlichen aus der boole'schen Bedingung bestehen. Für diese Bedingungen soll es eine explizite Operation zum Warten auf das Eintreffen geben. Das Eintreffen selber soll automatisch signalisiert werden, wenn ein Thread den Monitor verlässt und die Bedingung gilt. Zieltermin: 22.01.2007 Gruppenzuordnung: Ereignisse: Spezialisierung/Generalisierung: Markus S., Christoph, Johannes, Norman, Sebastian Hendrik, Tobias, Stefan R., Julia Mehrfachvererbung: Monitore: Steffi, Markus, Dennis, Stefan Hu. Stefan Ho., Janko, Michael, Artur 6. Aufgabe: 17. Ausbaustufe: Java-Komplettierung, Präprozessor und OO-Konzept In dieser Ausbaustufe sind sowohl konzeptionelle als auch praktische Ausarbeitungen gefordert. Ziel der praktischen Ausarbeitungen ist es, das Projekt näher an das Java-Sprachniveau zu bringen. Ziel der konzeptionellen Ausarbeitungen ist es zum einen, vorbereitende Überlegungen zur Integration der Umsetzungen der Java++-Konzepte aus der vorhergehenden Ausbaustufe zu tätigen. Zum anderen soll ein Papier über die verwendeten objektorientierten Konzepte erarbeitet werden, das als Grundlage zur Behebung einiger Schwächen in der Umsetzung dieser Konzepte im Projekt dienen soll. 6.1 OO-Konzept Es soll ein Grundlagen-Papier verfasst werden, das zum einen die grundlegenden objektorientierten Begriffe und Konzepte wie Operation, Methode, Nachricht, Klasse, Schnittstelle, statische und dynamische Bindung etc. definiert und in Beziehung setzt. Zum anderen sollen die variablen Anteile der Konzepte herausgestellt werden. Das Ziel ist es, die Begriffe und Konzepte so zu verallgemeinern, dass dadurch eine Art Rahmenwerk entsteht, aus dem man durch Instantiierung die OO-Semantik verschiedener Programmiersprachen erhalten kann. Dabei ist es nur notwendig, sich auf Java, Java++ und LOMF zu konzentrieren; das Einbringen von Kenntnissen über andere Sprachen ist aber von Vorteil. 6.2 Präprozessor Eine Konzeption soll erarbeitet werden, die aufzeigt, wie man das bisherige Projekt so um Java++ geeignet erweitern kann, dass keine Vermischung zwischen Java und Java++ im Quellcode auftritt. Einerseits müssen die Komponenten identifiziert werden, die in Java und Java++ unterschiedliches Verhalten aufweisen. Andererseits müssen Mechanismen entwickelt werden, um die Arbeitsweise dieser neuen Java++-Komponenten so zu gestalten, dass nicht das Rad neu erfunden werden muss – dass man also vieles von dem, was bereits da ist und sich nicht verändert hat, benutzen kann. Dabei kann das auszuarbeitende OO-Konzept eventuell weiterhelfen. Das Ziel der Konzeption ist, den Präprozessor so in das bestehende Projekt zu integrieren, dass man sowohl Java- als auch Java++Code verarbeiten kann, ohne dass der resultierende Projekt-Code sich verdoppelt und ohne dass der Projekt-Code vor zusätzlichen booleschen Flags, expliziten Fallunterscheidungen und ähnlichem strotzt. 6.3 Basistypen Diese Aufgabe hat die Erweiterung der vom Projekt bisher erkannten Java-Teilmenge um die fehlenden Basistypen byte, short, long, float und double samt den zugehörigen Operationen zum Ziel. Außerdem müssen alle von Java unterstützten Formen von Literalen für numerische Konstanten umgesetzt werden (hexadezimale und oktale Notation für Ganzzahlen, FließkommazahlKonstanten, Suffixe zur genaueren Spezifikation des Typs einer Konstante) 6.4 Sprungmarken (Labels) Das Projekt soll um die Behandlung von Sprungmarken erweitert werden, die in Java von break und continue benutzt werden, um gezielt die jeweils zu verlassende oder fortzusetzende Anweisung auswählen zu können. Ebenso soll das Schlüsselwort goto erkannt und mit derselben Semantik wie in Java versehen werden. 6.5 final In dieser Aufgabe soll die Funktionalität des Schlüsselwortes final für Variablen, Parameter, Attribute, Methoden und Klassen in das Projekt integriert werden, zusammen mit den notwendigen Komponenten zum Überprüfen der Einhaltung der geforderten Semantik (soweit innerhalb unseres Projekts umsetzbar). Zieltermin: 26.02.2007 Gruppenzuordnung: OO-Konzept: Präprozessor: Basistypen: Sprungmarken: final: Tobias, Hendrik Stefan Ho., Stefan Hu., Dennis, Janko Markus K., Stefanie, Julia Artur, Michael, Norman, Stefan R. Johannes, Sebastian, Christoph, Markus S. 7. Aufgabe: 18. Ausbaustufe: Java ++: Pre-Prozessor für Java zur Unterstützung von Standardmustern (2) In dieser Ausbaustufe sollen die Konzepte, die in der 16. Ausbaustufe erarbeitet wurden, umgesetzt werden. Dabei soll beachtet werden, dass Java und Java++ in der Umsetzung getrennt werden (siehe Präprozessor-Konzept aus Ausbaustufe 17!) Schließlich soll eine Gesamt-Dokumentation verfasst werden, in welcher sowohl alle Java++-Erweiterungen erläutert werden als auch die Aktivierung und Steuerung der Java++-Funktionalität “von außen” beschrieben wird. Zieltermin für die Implementierung der Einzelkonzepte (inklusive Präsentation): 23.07.2007 Zieltermin für die Integration der Umsetzungen (inklusive Präsentation): 20.08.2007 Zieltermin für die Abschluss-Dokumentation: 14.09.2007 Gruppenzuordnung: Ereignisse: Spezialisierung/Generalisierung: Mehrfachvererbung: Monitore: Julia, Stefan Hu., Norman, Artur Stefanie, Tobias, Markus S., Janko, Sebastian Hendrik, Stefan R., Johannes, Dennis Markus K., Michael, Christoph, Stefan Ho.