C
R
D
J
Dissertation zur Erlangung des
akademischen Grades des
Doktors der Naturwissenschaften (Dr. rer. nat.)
der Fakultät für Mathematik und Informatik
der FernUniversität in Hagen
Vorgelegt von
Andreas Thies
geboren in der Freien und Hansestadt Hamburg
Constraintbasierte Refaktorisierung von Deklarationen in Java
Dissertation,
Andreas Thies
Inhaltsangabe
Refaktorisierungen sind Änderungen an einem Programm, ohne sein nach außen hin beobachtbares Programmverhalten zu ändern. Sie unterstü en Softwareentwickler dabei, bestehenden Programmcode zu warten und zu erweitern. Refaktorisierungswerkzeuge können bei dieser
Aufgabe helfen, indem sie die für eine Refaktorisierung benötigten Quelltex ransformationen berechnen, anwenden und dabei sicherstellen, dass das Programmverhalten tatsächlich
unverändert bleibt. Allerdings weist nach heutigem Stand ein Großteil der im Industrieeinsa befindlichen Refaktorisierungswerkzeuge hohe Fehlerraten auf, was im Wesentlichen der
Komplexität moderner Programmiersprachen geschuldet ist. Die Folge ist, dass teilweise die
refaktorisierten Programme entweder ungültig sind oder in ihrem Programmverhalten vom
jeweiligen Eingabeprogramm abweichen.
Vorliegende Arbeit verfolgt den Ansa der constraintbasierten Refaktorisierung. Die Grundidee besteht dabei in der Überführung von Refaktorisierungsproblemen in Bedingungserfüllungsprobleme. In diesen werden mögliche Codeänderungen durch Variablen dargestellt. Die
durch diese anzunehmenden Werte werden durch Bedingungen derart eingeschränkt, dass
die Menge der möglichen Variablenwerte und somit Programmtransformationen ausschließlich gültigen Refaktorisierungen entspricht. In Verbindung mit einem herkömmlichen Constraintlöser kann dann zu einem derart umgeformten Refaktorisierungsproblem bestimmt
werden, ob eine bedeutungserhaltende Programmtransformation möglich ist und welche Änderungen am Quelltext dazu durchgeführt werden müssen.
Anhand der Implementierung einer Suite von fünf Refaktorisierungswerkzeugen zur Refaktorisierung von Deklarationen in Java werden die Stärken des constraintbasierten Ansatzes demonstriert. Insbesondere wird gezeigt, dass sich Autoren von Refaktorisierungswerkzeugen beim verwendeten Ansa auf die Formulierung nur gering komplexer und vor allem
wiederverwendbarer Regeln beschränken können, während die eigentliche Komplexität beim
Zusammenwirken aller Bedingungen an bestehende Constraintlöser delegiert wird. Evaluiert werden die implementierten Werkzeuge anhand einer Testsuite bestehend aus mehr als
.
handgeschriebenen Tests sowie automatisierten Testläufen, in denen auf quelloffenen,
frei verfügbaren Java-Projekten mehr als
.
Refaktorisierungen durchgeführt wurden.
Die Testergebnisse zeigen, dass die entwickelten Werkzeuge viele Fälle korrekt behandeln, in
denen herkömmliche Werkzeuge aufgrund der Komplexität der Sprache Java bisher scheitern.
Danksagungen
Auch wenn bei der Erstellung einer Dissertation die Arbeitsphasen, die man allein im stillen
Studierzimmer verbringt, immer noch beträchtlich sind, so ist Wissenschaft heute doch wesentlich durch den Gedankenaustausch, die Zusammenarbeit und das voneinander Lernen
geprägt. An dieser Stelle möchte ich einigen Personen danken, die mir in dieser Hinsicht zur
Seite gestanden haben.
Ein großer Dank gebührt meinem Doktorvater Friedrich Steimann, der mich vom ersten Tag
an mit viel Energie an meine Aufgabe herangeführt und mich mit einer guten Mischung aus
Stößen ins kalte Wasser und dem Zureichen von Schwimmhilfen begleitet hat. Insbesondere
danke ich ihm für seine große Bereitschaft, mir alle Vorausse ungen für eine gute Vereinbarkeit von Familie, Beruf und Promotion zu bieten.
Christoph Beierle, Jörg Desel sowie Daniela Keller danke ich dafür, dass sie die nicht immer
dankbare Aufgabe als Mitglieder der Prüfungskommission übernommen haben.
Weiterhin danke ich Jan Jürjens, Max Schäfer sowie Ma hias Riebisch, die mir allesamt
große Gastfreundschaft entgegengebracht haben. Sie haben mir nicht nur an ihren jeweiligen
Wirkungsstä en für teils mehrere Monate Tisch und Stuhl, sondern auch einen herzlichen
Empfang und großartige Arbeitsbedingungen geboten. Für eine besonders intensive Zusammenarbeit danke ich weiterhin Eric Bodden. Er und Max Schäfer haben mir besonders tiefen
Einblick in ihre Arbeitsweisen und Methodik gegeben, was meine Arbeit wesentlich geprägt
hat.
Mein Dank gilt ebenso meinen Kollegen Christian Kollee, Marcus Frenkel und Bastian Ulke.
Sie ha en stets eine offene Tür und eine freie Ecke an ihrer Wandtafel, um mit mir die großen
oder kleinen Probleme zu besprechen (und zu lösen!), die einem Doktoranden täglich so begegnen. Ebenso danke ich Hauke Col au, der als DoRF-Ältester und einer der Gründerväter
der Doktorandenrunde der FernUni den Zusammenhalt und Austausch der Doktoranden quer
über den Campus hinweg gefördert hat.
Einen besonders herzlichen Dank möchte ich Daniela Keller aussprechen, die mich in den
vergangenen Jahren bei enger und vertrauensvoller Zusammenarbeit oft in der Lehre entlastet
hat, um mir mehr Zeit für die Forschung einzuräumen, und die stets ein offenes Ohr und eine
helfende Hand ha e, wenn es um stilistische Fragen oder ums Korrekturlesen ging.
Meinen Eltern danke ich, dass sie mir auf meinem Ausbildungsweg größtmögliche Freiheit
gelassen und mich bis zum Abschluss dieser Arbeit in vielen Belangen unterstü t haben.
Meiner Ehefrau Sandra danke ich für das Durchstehen der vielen Entbehrungen, die ihr ein
promovierender Mann beschert hat. Sie hat mir mehr Unterstü ung gegeben, als sie glauben
möchte.
Eingereicht am: . .
Tag der mündlichen Prüfung:
Berichtersta er:
Prof. Dr. Friedrich Steimann
Prof. Dr. Jörg Desel
. .
Inhaltsverzeichnis
1. Einführung
. . Refaktorisierung . . . . . . . . . . . . . . . . . . . . . . . . .
. . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge
. . Herausforderungen . . . . . . . . . . . . . . . . . . . . . . .
. . Der constraintbasierte Lösungsansa
. . . . . . . . . . . .
. . Beitrag der Arbeit . . . . . . . . . . . . . . . . . . . . . . . .
. . Au au der Arbeit . . . . . . . . . . . . . . . . . . . . . . . .
13
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2. Literaturbetrachtungen
. . Die Anfänge . . . . . . . . . . . . . . . . . . . . . . .
. . Vor- und Nachbedingungen für Refaktorisierungen
. . Refaktorisierungen für dynamische Sprachen . . . .
. . Referenzkonstruktion . . . . . . . . . . . . . . . . . .
. . Constraintbasierte Refaktorisierungen . . . . . . . .
. . Weitere constraintbasierte Werkzeuge . . . . . . . .
. . Formale Darstellungen von Programmiersprachen .
. . Refaktorisierungen als Graphtransformationen . . .
. . Testen von Refaktorisierungswerkzeugen . . . . . .
. . Einordnung der vorliegenden Arbeit . . . . . . . . .
. . . Vorarbeiten . . . . . . . . . . . . . . . . . . .
. . . Folgearbeiten . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3. Constraintbasierte Refaktorisierungen
. . Eine formale Darstellung für Programmtransformationen . . . .
. . . Transformationen von Syntaxbäumen . . . . . . . . . . .
. . . Transformationen als Variablenzuweisungen . . . . . . .
. . Programmtransformationen als Bedingungserfüllungsproblem
. . Constraintregeln . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Constraintgenerierung . . . . . . . . . . . . . . . . . . . . . . . .
. . Constraintlösung . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Refacola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Sprachdefinitionen . . . . . . . . . . . . . . . . . . . . . .
. . . Constraintregeln . . . . . . . . . . . . . . . . . . . . . . .
. . . Refaktorisierungsdefinition . . . . . . . . . . . . . . . . .
. . . Das Refacola-Framework . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
27
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
39
4. Eine Sprachdefinition für Java in Refacola
. . Angebotene Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
71
Inhaltsverzeichnis
. . Taxonomie der Programmelemente . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Fakten und Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Unveränderliche Programmeigenschaften und funktionale Einschränkungen .
5. Constraintregeln für Java
. . Orts-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Zugrei arkeits-Constraints . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Zugrei arkeit als Constraintvariable . . . . . . . . . . . . . . .
. . . Zugrei arkeiten zur Zugriffskontolle . . . . . . . . . . . . . . .
. . . Zugrei arkeit und Vererbung . . . . . . . . . . . . . . . . . . .
. . . Zugrei arkeit bei Methodenüberschreibung und -verdeckung
. . . Weitere Zugrei arkeitsregeln . . . . . . . . . . . . . . . . . . .
. . Typ-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Typen als Constraintvariable . . . . . . . . . . . . . . . . . . . .
. . . Constraintregeln zur Typinferenz . . . . . . . . . . . . . . . . .
. . . Member-Zugriff und Zuweisungskompatibilität . . . . . . . . .
. . . Überschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Zugriff auf statische Member . . . . . . . . . . . . . . . . . . . .
. . . Weitere Regeln . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Typ-Constraints für generische Typen . . . . . . . . . . . . . . . . . . .
. . . Typinferenz generisch typisierter Ausdrücke . . . . . . . . . . .
. . . Beschränkt parametrische Typen . . . . . . . . . . . . . . . . . .
. . . Zuweisungen an mit generischen Typen typisierte Ausdrücke .
. . . Wildcards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Raw-Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Namens-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Bindung anhand von Namen . . . . . . . . . . . . . . . . . . . .
. . . Eindeutigkeit von Namen . . . . . . . . . . . . . . . . . . . . . .
. . . Namen überschreibender Methoden . . . . . . . . . . . . . . . .
. . . Weitere Namensregeln . . . . . . . . . . . . . . . . . . . . . . . .
. . . Ungültige Namen . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Verdunkeln und Verscha en . . . . . . . . . . . . . . . . . . . .
. . Unerwünschtes Überschreiben, Verdecken und Überladen . . . . . . .
. . Unveränderliche Bibliotheksfunktionen . . . . . . . . . . . . . . . . . .
. . Offene Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6. Berücksichtigung reflektiver Zugriffe
. . Die Reflection-API . . . . . . . . . . . . .
. . Refaktorisierung reflektiven Codes . . . .
. . Programmelemente und Fakten . . . . . .
. . Constraintregeln . . . . . . . . . . . . . .
. . Zurückschreiben in reflektive Ausdrücke
. . Einschränkungen des vorgestellten Ansa
85
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
143
. .
. .
. .
. .
. .
es
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Inhaltsverzeichnis
7. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse
. . Deklaration der Refaktorisierungswerkzeuge . . . . . . . . . . . . . . . . . . .
. . . Move Compilation Unit . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Pull Up Member . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Rename . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Generalize / Specialize Declared Type . . . . . . . . . . . . . . . . . . .
. . . Change Accessibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Validierung der Benu ereingaben . . . . . . . . . . . . . . . . . . . . .
. . . Auswahl einer Constraintlösung . . . . . . . . . . . . . . . . . . . . . .
. . . Benu erschni stelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Faktengenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Namensqualifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Rückschreibekomponente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
163
8. Evaluation
. . Forschungsfragen . . . . . . . . . . . . . . . . . . . . . . . .
. . Korrektheitsbeweise . . . . . . . . . . . . . . . . . . . . . .
. . Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Refactoring Tool Testing der implementierten Werkzeuge .
. . Übermäßig starke Einschränkungen . . . . . . . . . . . . .
. . Auswirkungen erlaubter Änderungen . . . . . . . . . . . .
. . Laufzeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . Evaluation der Constraintregeln für reflektive Zugriffe . .
.
.
.
.
.
.
.
.
173
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9. Weitere constraintbasierte Werkzeuge
. . Quick Fixes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . Ein prototypischer Quick Fix zum Anpassen von Zugrei arkeiten
. . . Probleme bei der Implementierung beliebiger Quick Fixes . . . . .
. . Constraintbasiertes Mutationstesten . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
201
.
.
.
.
.
.
.
.
.
.
.
.
10. Zusammenfassung
207
A. Java-Sprachdefinition
209
B. Constraintregeln für Java
223
C. Refaktorisierungsdefinitionen
Move Compilation Unit . . . . . . . .
Pull Up Member . . . . . . . . . . . . .
Rename . . . . . . . . . . . . . . . . . .
Generalize / Specialize Declared Type
Change Accessibility . . . . . . . . . .
267
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Literaturverzeichnis
271
Index
283
1. Einführung
Der umfangreiche Einsa von Programmierwerkzeugen ist aus dem Alltag der heutigen Softwareentwicklung nicht mehr wegzudenken. Während in den Pioniertagen der computergestü ten Programmierung lediglich ein einfacher Texteditor und Compiler zum Werkzeugkasten der Softwareentwicklung gehörten, haben sich nach und nach immer mehr Werkzeuge
entwickelt, die heute wie selbstverständlich innerhalb von integrierten Entwicklungsumgebungen ihre Rolle bei der Programmierung spielen. Oft ist die Rolle dieser Werkzeuge eine
passive, indem sie Softwareentwicklerinnen und Softwareentwicklern zusä liche Informationen bereitstellen, angefangen bei einfachen Syntaxhervorhebungen bis hin zu komplexen
Navigationshilfen durch ganze Programmbibliotheken. Häufig sind Programmierwerkzeuge
aber auch aktiv an der Programmerstellung und -bearbeitung beteiligt. So beschreibt Teitelman [Tei ] zum Beispiel bereits im Jahre
ein Programmiersystem, welches bei Vorliegen eines Kompilierfehlers von sich aus Programmtransformationen anbietet, mi els derer
das Ausgangsprogramm (wieder) in eine den syntaktischen und semantischen Regeln der
Programmiersprache entsprechende Form gebracht werden kann. Heute ist es eine Selbstverständlichkeit, dass gute Entwicklungsumgebungen derartige Funktionen – meist unter dem
Begriff quick fix bekannt – anbieten. Auch in der Folgezeit wurden für neu au ommende Vorgehensweisen bei der Softwareentwicklung zügig entsprechende Werkzeuge entwickelt. So
hat Griswold [Gri ] Anfang der neunziger Jahre in der ersten Arbeit zu Refaktorisierungen
bereits direkt ein entsprechendes Werkzeug mitgeliefert, welches imstande ist, bedeutungserhaltende Programmtransformationen durchzuführen.
Programmierwerkzeugen, die Einfluss auf bestehenden Code nehmen und somit bestehendes Programmverhalten ändern können, kommt besondere Verantwortung zu! In keinem Falle sollte ein Programmierwerkzeug das Verhalten bestehender Programmteile ändern, ohne dass dies ausdrücklich der Intention der Programmiererin oder des Programmierers entspricht. Doch unglücklicherweise machen moderne objektorientierte Programmiersprachen mit ihren vielen Sprachkonzepten wie statischer und dynamischer Bindung, Typprüfungen oder ausgefeilten Sichtbarkeitskonzepten es Werkzeugentwicklerinnen und Werkzeugentwicklern nicht nur schwer, den Benu erwünschen entsprechende Codeänderungen
zu berechnen, sondern fördern auch noch die Möglichkeit, unabsichtliche Änderungen im
Programmverhalten zu provozieren. Einen guten – wenn auch mit einem Augenzwinkern
vermi elten – Eindruck, zu welch unintuitiven und fehleranfälligen Situationen Java als moderne Programmiersprach mit ihren vielen Features führen kann, liefern zum Beispiel Joshua
Bloch und Neal Gafter [BG ], die mit geschickt gewählten Beispielprogrammen bei der Frage
nach deren Verhalten selbst gestandene Java-Programmierer¹ ins Schlingern bringen.
¹ Es steht außer Frage, dass nicht nur Programmierer, sondern ebenso auch Programmiererinnen von der Sprache Java in die Irre geleitet werden können. Zugunsten einer kompakteren Darstellung wird allerdings in der
folgenden Arbeit auf die explizite Angabe beiderlei Geschlechts verzichtet. Da in dieser Arbeit oftmals die
Schwächen der Programmiererinnen und Programmierer im Fokus stehen – sie machen Fehler – empfand es
. Einführung
Diese Arbeit widmet sich der Entwicklung korrekter, aktiv an der Codeerstellung beteiligter Programmierwerkzeuge und unter ihnen insbesondere den Refaktorisierungswerkzeugen.
Die Wahl der dabei durchgängig betrachteten Programmiersprache fiel auf Java, alleine aufgrund ihres Verbreitungsgrades und ihrer Relevanz für die heutige Softwareindustrie.
1.1. Refaktorisierung
Eine Refaktorisierung (engl. refactoring) ist eine Änderung (oder Umstrukturierung) eines Programms, ohne sein beobachtbares Programmverhalten zu ändern [Fow ]. Das dabei verfolgte Ziel ist meist, die Lesbarkeit und Verständlichkeit des Quellcodes zu verbessern. Ebenso ist
es aber auch möglich, mi els Refaktorisierung Redundanzen zu entfernen oder bessere Erweiterungsmöglichkeiten zu schaffen [Fow ].
Der Begriff der Refaktorisierung – obwohl stets Ähnliches meinend – ist mehrdeutig. Einerseits dient er zur Beschreibung der allgemeinen Tätigkeit, andererseits aber auch zur Beschreibung eines Musters, nach dem bedeutungserhaltend Programmänderungen durchgeführt werden können. Bekanntheit erlangte hier insbesondere der durch Fowler zusammengetragene Katalog von Refaktorisierungen [Fow ]. Weiterhin können als Refaktorisierung
auch entweder konkrete Transformationen eines bestimmten Programms oder Programmierwerkzeuge gemeint sein, die bei einer solchen Refaktorisierung unterstü en [DKWB ]. In
le terem Fall wird zur Abgrenzung in dieser Arbeit der Begriff des Refaktorisierungswerkzeugs
(engl. refactoring tool) genu t.
Problematisch bei obiger Definition für Refaktorisierung ist, dass unklar bleibt, wann zwei
Programme sich in ihrem beobachtbaren Programmverhalten gleich verhalten. Die Idee, dass
gleiches Programmverhalten vorliegt, wenn bei gleicher Eingabe auch gleiche Ausgabe erfolgt, trägt nicht. Sie berücksichtigt weder nicht-funktionale Anforderungen, wie zum Beispiel
solche an die Laufzeit, noch, dass je nach Aufgabenstellung auch bei identischen Programmen
Ausgaben nach identischen Eingaben Unterschiede aufweisen können. Auf Ideen von Schäfer
[Sch ] au auend werden innerhalb dieser Arbeit verschiedene sogenannte Abhängigkeiten
(engl. dependencies) spezifiziert, die während einer Refaktorisierung zu erhalten sind und eine
sinnvolle und formale Grundlage für den Begriff des unveränderten Programmverhaltens liefern.
Refaktorisierungen, insbesondere im Kleinen – wie zum Beispiel beim Umbenennen einer Variable – sind heute gängige Praxis und selbstverständlicher Teil des Programmieralltags [MHPB ]. Daneben hat das Refaktorisieren durch die Verbreitung agiler Entwicklungsmethoden – zum Beispiel dem Extreme Programming [Bec ] – noch einmal an Bedeutung gewonnen. Dort wird das Refaktorisieren gar als ein so mächtiges Instrument betrachtet, dass
eine vollständige Entwurfsphase (wie zum Beispiel im Wasserfallmodell vorgesehen) vor Implementierungsbeginn entfallen kann, da dank Refaktorisierungen Änderungen im Design
auch noch nachträglich möglich seien. Dieser Vorteil kann allerdings nur zum Tragen kommen, soweit beim Refaktorisieren nicht versehentlich Fehler in das zu refaktorisierende Programm eingeschleust werden, die wiederum zeit- und kostenintensiv gefunden und behoben
werden müssen. Eine Lösung hierfür können Refaktorisierungswerkzeuge bieten, die Programmierer nicht nur dahingehend entlasten, die nötigen Quelltex ransformationen durchzuführen, sondern dabei auch prüfen, ob das Programmverhalten unverändert bleibt. Wie
der Autor als höflich, die männliche Form zu wählen.
. . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge
der folgende Abschni zeigt, sind derzeit verfügbare Werkzeuge allerdings noch weit davon
entfernt, vor allem le tere Aufgabe zuverlässig zu erfüllen.
1.2. Probleme derzeit verfügbarer Refaktorisierungswerkzeuge
Um die Schwierigkeiten der werkzeuggestü ten Programmänderung konkret erläutern zu
können, gibt dieser Abschni Beispiele für einzelne Refaktorisierungswerkzeuge, wie sie in
drei sehr verbreiteten Entwicklungsumgebungen für Java – NetBeans [NBE ], IntelliJ [IJI ]
und Eclipse [ECL ] – implementiert sind. Es liegt in der Natur der Sache, dass diese Aufzählung nur exemplarisch Einzelfälle aufführt, hier größtenteils am Beispiel von Zugrei arkeitsproblemen in Java. Ab Kapitel wird deutlich werden, dass der in dieser Arbeit vorgestellte
Lösungsansa nicht nur die gezeigten Einzelfälle löst, sondern generisch genug ist, auch darüber hinausgehende Probleme zu adressieren.
Doch bevor es an die ersten Beispiele geht, ein weiterer Hinweis: Wenn in dieser Arbeit von
der Sprache Java die Rede ist, dann ist durchgängig die Java-Sprachversion SE . gemeint.
Wenn von der Java-Sprachspezifikation die Rede ist, dann ist stets die zugehörige The Java
Language Specification, Third Edition [GJSB ] gemeint. Es ergibt sich aus dem Thema dieser
Arbeit, dass bereits in den einführenden Beispielen, spätestens aber im technischen Teil eine
Vielzahl von Referenzen auf die Java-Sprachspezifikation gemacht werden. Diese Arbeit nu t
eine Kurzschreibweise für solche Referenzen, in denen in Klammern stehend die jeweilige
Paragraphenangabe dem Java-Logo – einer Kaffeetasse – folgt. Wird also beispielsweise Bezug
auf § . der Java-Sprachspezifikation genommen, liest sich die Kurzschreibweise ( § . ).²
Berechnung von Zugreifbarkeiten in Java Java bietet für Typen, Methoden und Felder
mi els der Zugrei arkeitsmodifizierer (engl. access modifier)³ public, protected und private Mechanismen zum Verbergen von Implementierungsdetails. Ihr disziplinierter Einsa hilft, Programme zu modularisieren, Abhängigkeiten zwischen Programmteilen deutlich zu machen
und so le tendlich das Design des Programms zu verbessern [Mül ]. Ihre Allgegenwärtigkeit (denn auch ein fehlender Zugrei arkeitsmodifizierer impliziert einen bestimmten Zugrei arkeitsbereich, die default-Zugrei arkeit) macht es zwingend notwendig, dass Refaktorisierungswerkzeuge mit den unterschiedlichen Zugrei arkeitsstufen umgehen können. Da
neben dem entsprechenden Schlüsselwort an einer Deklaration weiterhin über ihre Zugreifbarkeit entscheidet, von welchem Ort aus sie referenziert werden soll, sind hierbei insbesondere Refaktorisierungswerkzeuge betroffen, die Codeteile verschieben.
Verschieberefaktorisierungen (M
) zählen zu den in der Praxis fünf häufigst ausgeführten Refaktorisierungen [MHPB ]. Sowohl Eclipse, als auch NetBeans und IntelliJ bringen entsprechende Werkzeuge mit, so auch eines, mit welchem ein deklarierter Typ (also eine Klasse
oder ein Interface) zwischen Paketen verschoben werden kann. Wendet man diese Werkzeu² Leser einer digitalen Ausgabe dieser Arbeit dürfen sich über in den Referenzen verborgene Hyperlinks auf die
online-Ausgabe der Java-Sprachspezifikation unter http://docs.oracle.com freuen.
³ Häufig findet man in der Literatur den Begriff der accessibility ebenso als Sichtbarkeit beziehungsweise den des
access modifier als Sichtbarkeitsmodifizierer überse t [KH , Ull ]. Diese Überse ung ist aber fehlleitend, da in
Java ebenso das Konzept der visibility ( § . . ) existiert, welches sta dessen treffender als Sichtbarkeit zu
überse en ist.
. Einführung
1
2
3
4
package a;
public class A {
void m(){};
}
5
6
package a ;
7
8
9
10
11
12
13
public class B extends A {
void n(){
A myVar = new B();
myVar.m();
}
}
(a)
package a;
public class A {
void m(){};
}
⇒
package b ;
import a.A;
public class B extends A {
void n(){
A myVar = new B();
myVar.:::
m();
}
}
(b)
Abbildung . .: Werden die Refaktorisierungswerkzeuge von Eclipse, NetBeans und IntelliJ
angewiesen, im Programm (a) die Klasse B in ein neues Paket b zu verschieben, erhält man als Ausgabe das rechtsseitig gezeigte Programm (b). In diesem
gelingt der Zugriff auf die nur paketweit zugrei are Methode m in A in Zeile
nicht mehr und es kommt zu einem Kompilierfehler.
ge auf das in Abbildung . (a) gezeigte Programm⁴ an, indem man den Typen B in ein neues
Paket b verschieben möchte, haben die Werkzeuge – neben der eigentlichen Verschiebung –
zwei weitere Änderungen am Quellcode zu tätigen. Einerseits kann vom Paket b aus nicht
mehr unqualifiziert auf den Typen A zugegriffen werden, sodass dieser entweder bei jeder
Verwendung qualifiziert oder einmalig für B importiert werden muss. Andererseits ist zu beachten, dass die in Zeile
aufgerufene Methode m keinen Zugrei arkeitsmodifizierer hat
und deswegen nur innerhalb desselben Pakets aufgerufen werden kann. Während alle drei
Werkzeuge einen passenden Import des Typen A einfügen, scheitern sie gleichzeitig an einem Anpassen der Zugrei arkeit, indem sie das in Abbildung . (b) gezeigte Programm
erzeugen. Das Resultat ist in allen drei Fällen ein Kompilierfehler in Zeile . Dabei offenbart
sich, dass Zugrei arkeiten durch die Eclipse-Implementierung des M
scheinbar gar keine
Berücksichtigung finden. IntelliJ und NetBeans hingegen warnen vor der Quelltex ransformation wenigstens mit einem entsprechenden Hinweisfenster und geben an, dass von B aus
noch Zugriffe auf die Methode a bestehen. Gleichzeitig überlassen sie es aber beide dennoch
dem Programmierer, die dadurch entstandenen Kompilierfehler von Hand zu beheben.
Dem Programmierer allerdings macht es die Sprache Java keineswegs leicht, zu entscheiden,
welcher Zugrei arkeitsmodifizierer für die Methode m zu wählen ist: private-Deklarationen
sind nur innerhalb derselben Klasse zugrei ar, Deklarationen ohne Zugrei arkeitsmodifi⁴ Ein Hinweis zur Darstellung von Programmcode in der vorliegenden Arbeit: Java und teils auch Implementierungen entsprechender virtueller Maschinen verlangen, dass als public deklarierte Typen in eigenen Dateien und Typen unterschiedlicher Pakete in getrennten Verzeichnissen liegen. Zugunsten einer vereinfachten
Darstellung wird in den Programmbeispielen dieser Arbeit auf diesen Umstand keine Rücksicht genommen.
Sta dessen werden Typdeklarationen und ihnen zugehörige Importe stets nur durch eine Leerzeile getrennt
untereinander gelistet. Der Leser sei gebeten, stets eine geeignete Aufteilung in Dateien und Verzeichnisse
hinzuzudenken.
. . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge
14
15
16
17
package a;
public class A {
public void m(){}
}
18
19
20
21
22
23
24
25
26
package b;
import a.A;
public class B extends A {
void n(){
A myVar = new B();
myVar.m();
}
}
(a)
package a;
public class A {
protected void m(){}
}
⇒
package b;
import a.A;
public class B extends A {
void n(){
A myVar = new B();
myVar.:::
m();
}
}
(b)
Abbildung . .: Sowohl Eclipse als auch NetBeans lassen bei einem C
S
eine
Änderung der Zugrei arkeit von m hin zu protected für das linke Programm
(a) zu, obwohl dies zu einem Kompilierfehler in Zeile
führt (b).
zierer nur innerhalb desselben Pakets, weswegen beide in obigem Beispiel für m einen Zugriff
aus dem Paket b verbieten würden. Die protected-Zugrei arkeit wird in gängigen Hand- und
Lehrbüchern so beschrieben, dass ein Zugriff auf eine solch annotierte Deklaration nur aus
demselben Paket oder Subklassen heraus zulässig ist. Demnach würde für obiges Beispiel die
Wahl eines protected-Modifizierers für m genügen. Was dieselben Lehr- und Handbücher hingegen häufig verschweigen (exemplarisch seien [Eck , KH , Ull ] genannt), ist, dass eine
Subklassenbeziehung noch nicht hinreichend ist. So reicht ein protected-Modifizierer für m
nämlich nicht aus, denn zusä lich fordert die Java-Sprachspezifikation, dass der Aufruf auf
eine protected-Methode auch im Kontext der abgeleiteten Klasse sta finden muss ( § . . ).
In obigem Beispiel müsste demnach der Empfänger des Aufrufs also vom Typ B oder einem
seiner Subtypen sein. Die einzig korrekte Lösung wäre also, die Methode m als public zu deklarieren.
Aus Sicht eines Programmierers ist es ärgerlich, dass die ausgeführten M
-Werkzeuge
die nötige Änderung der Zugrei arkeiten nicht gleich mit ausgeführt haben, insbesondere,
da es in allen drei Entwicklungsumgebungen weitere Refaktorisierungswerkzeuge gibt, die
sehr wohl den Anspruch erheben, mit Änderungen von Zugrei arkeitsmodifizierern umgehen zu können. So erlaubt zum Beispiel ein in allen drei Umgebungen enthaltenes C
S
(beziehungsweise in NetBeans C
M
P
und in Eclipse C
M
S
) genanntes Werkzeug unter anderem das Erhöhen und Verringern von
Zugrei arkeitsmodifizierern von Methoden. Allerdings arbeiten auch diese Werkzeuge mitunter fehlerhaft, was sich zeigt, wenn man obiges Beispiel fortführt. Angenommen, der Entwickler hä e in diesem den entstandenen Kompilierfehler (gegebenenfalls nach erfolglosem
Ausprobieren eines protected-Modifizierers) durch Einfügen eines public-Modifizierers behoben. Es ergibt sich somit das Programm in Abbildung . (a).
Wendet man auf dieses eine C
S
-Refaktorisierung an, bei der die Zugreifbarkeit der Methode m hin zu protected verringert werden soll, scheitert sowohl Eclipse als
. Einführung
27
28
29
30
31
32
33
34
package a;
public class A {
void m(){};
void n(){
A myVar = new B();
myVar.m() ;
}
}
35
36
37
38
package b;
import a.A;
public class B extends A {
39
40
41
42
43
}
(a)
package a;
public class A {
protected void m(){};
⇒
}
package b;
import a.A;
public class B extends A {
void n(){
A myVar = new B();
myVar.m();
:::
}
}
(b)
Abbildung . .: Weist man das Werkzeug P
D
in Eclipse an, in dem links gezeigten
Programm (a) die Methode n in die Subklasse B zu verschieben, wird die Zugrei arkeit der von dieser referenzierten Methode m zwar angepasst, allerdings nur unzureichend auf protected-Stufe.
auch NetBeans. Beide führen ohne weitere Warnungen die Codeänderungen aus und erzeugen das nicht kompilierbare Programm aus Abbildung . (b), obwohl – wie oben erörtert –
ein protected-Modifizierer nicht genügt. Alleine IntelliJ vermag eine passende Warnung auszugeben, die den Programmierer von der fehlerhaften Refaktorisierung abhält.
Das Problem der Redundanz Die beobachteten Fehler in den Refaktorisierungswerkzeugen scheinen um so ärgerlicher, wenn man sich vergegenwärtigt, dass fehlerhafte Routinen
einzelner Refaktorisierungswerkzeuge in anderen Werkzeugen derselben Entwicklungsumgebung durchaus korrekte Behandlung erfahren. So scheint M
T
in Eclipse generell
keine Zugrei arkeiten zu berücksichtigen. Stellt man das Beispiel aus Abbildung . hingegen ein wenig um, sodass der Codeblock mit der Referenz auf die Methode m nicht mi els
eines M
T , sondern eines P
D
in das Paket b verschoben wird, finden in Eclipse sehr wohl Prüfungen zur Zugrei arkeit sta . Zwar scheitert das Werkzeug le tendlich
immer noch, denn es erhöht die Zugrei arkeit von m nur auf ein ungenügendes protected,
wie in Abbildung . gezeigt, immerhin fanden aber anscheinend überhaupt Prüfungen zur
Zugrei arkeit sta .
Ein umgekehrter Fall ist bei IntelliJ zu beobachten. Während das dortige C
S
mit der protected-Zugrei arkeit in Java umzugehen wusste und die Refaktorisierung aus Abbildung . korrekterweise abgelehnt hat, wurde dieses Wissen offensichtlich nicht in das
ebenso verfügbare U I
W
P
-Werkzeug eingebracht. Für dieses zeigt Abbildung . (a) ein Beispiel, in welchem genau das nicht kompilierbare Ausgabeprogramm er-
. . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge
44
45
46
47
package a;
public class A {
protected void m(){};
}
48
49
50
51
52
53
54
55
56
package b;
import a.A;
public class B extends A {
void n(){
B myVar = new B();
myVar.m();
}
}
(a)
package a;
public class A {
protected void m(){};
}
⇒
package b;
import a.A;
public class B extends A {
void n(){
A myVar = new B();
myVar.:::
m();
}
}
(b)
Abbildung . .: Wendet man im Programm (a) das in IntelliJ eingebaute U I
W
P
-Werkzeug an, um den deklarierten Typen der Variable myVar
in Zeile
von B in A zu ändern, kommt es zu einem Kompilierfehler in der
Folgezeile, ohne dass das Werkzeug hiervor warnt. Das C
S
Werkzeug derselben Entwicklungsumgebung hat die Ausgabe des nicht kompilierenden Programms (b) in ähnlicher Situation hingegen korrekt verhindert (siehe Abbildung . ).
zeugt wurde, welches das C
S
-Werkzeug zu verhindern wusste (Abbildung .
(b)). Die Vermutung liegt nahe (und für das quelloffene Eclipse bestätigt auch ein Blick in den
Programmtext), dass jeweils gänzlich unterschiedliche Implementierungen für sehr ähnliche
Aufgaben existieren, was gleich zweifach negativ zu Buche schlägt: in mehrfachem Entwicklungsaufwand einerseits und unnötigen Fehlern andererseits.
Folgeänderungen und Folgefehler Selbst wenn ein Refaktorisierungswerkzeug die für eine angestrebte Refaktorisierung nötigen Folgeänderungen wie ein Anpassen von Zugrei arkeiten oder Importieren von Typen korrekt bestimmt, ist damit noch nicht zwingend eine gültige Programmtransformation gefunden. Denn auch diese Folgeänderungen können wiederum weitere Änderungen im Programmtext erzwingen. Abbildung . (a) zeigt ein Programm,
S
-Werkzeugs
in welchem die Methode m(String) der Klasse A mi els eines C
auf public-Zugrei arkeit angehoben werden soll. Was alle drei betrachteten Entwicklungsumgebungen dabei korrekt erkennen, ist, dass Java für überschreibende Methoden fordert, dass
diese die Zugrei arkeit im Vergleich zur überschriebenen Methode nicht verringern dürfen
( § . . . ). NetBeans erkennt dies zwar und teilt es über eine entsprechende Warnung mit,
verweigert aber, automatisch die Zugrei arkeit der überschreibenden Methode m(String) in B
mit anzupassen. Dem Benu er bleibt nur die Option, den angezeigten Refaktorisierungsdialog zu schließen und den Programmtext von Hand zu ändern. IntelliJ und Eclipse hingegen
passen ebenso auch die überschreibende Methode in ihrer Zugrei arkeit mit an und geben
das in Abbildung . (b) gezeigte Programm aus. Was beide dabei aber verkennen, ist, dass
es nun zu einer Änderung im Programmverhalten kommt: der Methodenaufruf in Zeile
. Einführung
57
58
59
60
package a;
public class A {
void m(String s) { /*...*/ }
}
package a;
public class A {
public void m(String s) { /*...*/ }
}
package a;
public class B extends A {
void m(String s) {
throw new Error();
}
public void m(Object o) { /*...*/ }
}
package a;
public class B extends A {
public void m(String s) {
throw new Error();
}
public void m(Object o) { /*...*/ }
}
61
62
63
64
65
66
67
68
⇒
69
70
71
72
73
74
75
package x;
public class X {
void n() {
new a.B().m("abc");
}
}
(a)
package x;
public class X {
void n() {
new a.B().m("abc");
}
}
(b)
Abbildung . .: Soll im Programm (a) die Methode m(String) in A einen public-Modifizierer erhalten, muss in der Folge auch die überschreibende Methode m(String) in B als
public deklariert werden. Dies führt aber zu einer Änderung des Programmverhaltens, da der Aufruf von m in der Klasse X nun an die Methode m(String)
sta wie zuvor m(Object) bindet.
. . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge
bindet nun nämlich nicht mehr an die aus der Klasse X einzig zugrei are Methode m(Object),
sondern an die neuerdings ebenso zugrei are und von den Methodenparametern her spezifischere Methode m(String). Je nach konkretem Verhalten der m-Methoden kann dies zu beliebigen Änderungen im Programmverhalten – also einer Nicht-Refaktorisierung – führen. Dabei stehen die Chancen gut, dass dadurch ins Programm eingebrachte Fehler zunächst nicht
auffallen, findet doch nun keine augenblickliche Fehlermeldung durch den Compiler sta ,
wie es in den vorhergehenden Beispielen der Fall war. Ebenso steht der Zugriff aus Klasse X
in keinem räumlichen Zusammenhang mit der Refaktorisierung in den Klassen A und B, sondern kann von überall im Programm herrühren, also auch Programmteilen, die der Entwickler
nicht unmi elbar im Blick hat. Es kann also zu tückischen Fehlern kommen, die erst zur Laufzeit auffallen. Dann allerdings ist die Ursache des Problems – eine Refaktorisierung mit zwei
kleinen Änderungen – lange abgeschlossen und womöglich in Vergessenheit geraten, was eine
Fehlerfindung erschwert. Eclipse und IntelliJ haben mit ihren Refaktorisierungswerkzeugen
den Benu er somit in trügerischer Sicherheit gelassen, was die Beibehaltung des Programmverhaltens angeht. Ob der Benu er von NetBeans bei seinen von Hand ausgeführten Quelltextänderungen die Weitsicht gehabt hä e, nicht denselben Fehler wie diese Werkzeuge zu
begehen, sei dahingestellt.
Reflektives Programmverhalten Ein Fall, in dem die Refaktorisierungswerkzeuge aller betrachteten Entwicklungsumgebungen gänzlich die Segel streichen, sind introspektive Programme [Rok ], also Programme, die zur Laufzeit dynamisch Eigenschaften von sich selbst
abfragen und in Abhängigkeit davon ihren Programmfluss verändern können [Rok ]. Java erlaubt die introspektive Programmierung mi els der Reflection-API [McC , REF ]. Sie
ermöglicht (unter anderem), zur Laufzeit Objekte nach ihren deklarierten Methoden und Feldern zu fragen, Werte von Feldern zu lesen und zu schreiben sowie Methoden aufzurufen
[FF ].
Abbildung . zeigt eine Verwendung der Reflection API, in welcher innerhalb der mainMethode von Klasse B auf die Klasse A reflektiv zugegriffen wird. Zunächst wird in Zeile
eine Referenz auf das Class-Objekt der Klasse A geholt, welches den Einstiegspunkt für reflekeine Referenz auf die Methode m erzeugt,
tive Zugriffe bildet. Anschließend wird in Zeile
welche in Zeile
genu t wird, um diese Methode auf einer neuen (ebenso dynamisch erzeugten) Instanz von A aufzurufen.
Für Aufrufe über die Reflection API gelten ähnliche Einschränkungen wie für reguläre Zugriffe, so sind insbesondere auch Zugrei arkeiten zu berücksichtigen [FF ]. Reduziert ein
Refaktorisierungswerkzeug die Zugrei arkeit von m, wäre eine IllegalAccessException und somit eine Änderung des Programmverhaltens in Zeile die direkte Folge. Dennoch ist keines
der in den drei betrachteten Entwicklungsumgebungen vorhandenen Refaktorisierungswerkzeuge in der Lage, reflektive Aufrufe zu berücksichtigen. Die Ursache dafür ist, dass für eine
statische Analyse – auf welcher die Refaktorisierungswerkzeuge au auen – reflektive Aufrufe häufig unentscheidbar bleiben (man überlege sich, dass in Zeile von Abbildung . auch
ein Aufruf Class.forName(args[0]) enthalten sein könnte). Um reflektive Aufrufe vollständig
zu erfassen, ist also eine Auswertung von Laufzeitinformationen unumgänglich. Bisher bietet
aber kein bekanntes Refaktorisierungswerkzeug für Java eine solche an.
. Einführung
76
77
78
79
80
81
82
83
84
85
86
87
package a;
public class A {
public void m() { /*...*/ }
}
package b;
public class B {
public static void main(String[] args) throws Exception {
Class<?> a = Class.forName("a.A");
java.lang.reflect.Method m = a.getDeclaredMethod("m");
m.invoke(a.newInstance());
}
}
Abbildung . .: Reflektiver Zugriff auf eine Klasse A. In Zeile
wird eine Referenz auf
ein Class-Objekt der Klasse A erzeugt, Ausgangspunkt, um in Zeile
eine
Referenz auf die Methode m zu erhalten und diese in Zeile
dynamisch
aufzurufen.
1.3. Herausforderungen
Wie der vorhergehende Abschni gezeigt hat, können selbst kleine und scheinbar einfache
Änderungen von gegebenen Programmtexten komplexe Auswirkungen haben, die bei gegenwärtigen Refaktorisierungswerkzeugen keine ausreichende Berücksichtigung finden. Dieser
Abschni fasst die wesentlichen Herausforderungen noch einmal zusammen.
Nötige Programmtransformationen Nicht immer lässt sich aus den Eingabeparametern eines Refaktorisierungswerkzeugs direkt die durchzuführende Programmtransformation ableiten. Wie bei den einführenden Beispielen gesehen, müssen neben der eigentlich angestrebten
Änderung (zum Beispiel dem Verschieben einer Deklaration) noch weitere nötige Quellcodeänderungen ermi elt werden, wie zum Beispiel das Hinzufügen von Import-Anweisungen
oder Qualifizierern und Anpassen von Zugrei arkeitsmodifizierern. Je nach Refaktorisierung
können aber auch weitere Arten von Folgeänderungen nötig werden. Wird zum Beispiel ein
Feld in eine Superklasse verschoben, kann es nötig werden, weitere Felder mit zu verschieben,
mi els derer es bei seiner Deklaration initialisiert wird [SP a]. Bei Typ-Refaktorisierungen,
bei denen die Typen von Deklarationen oder Ausdrücken geändert werden (was auch geschehen kann, indem eine this-Referenz innerhalb einer Methode in eine andere Klasse verschoben
wird), sind häufig Anpassungen weiterer deklarierter Typen nötig [TFK+ ]. Erschwerend
kommt hinzu, dass nötige Folgeänderungen ihrerseits Folgeänderungen bewirken können.
Erzwingt das Verschieben einer Deklaration das Verschieben einer weiteren Deklaration, kann
es hierfür nötig werden, Zugrei arkeiten anzupassen. Dies kann aber – wie im Beispiel aus
Abbildung . gesehen – erneut weitere Änderungen von Zugrei arkeiten bedingen. Insgesamt sind beliebig lange Ke en von Folgeeffekten denkbar [TKB , ST , SKP ].
. . Der constraintbasierte Lösungsansa
Erkennung unerwünschter Änderungen im Programmverhalten Die komplexen Binderegeln von Java eröffnen vielfach Möglichkeiten, unbeabsichtigt Methoden- und Variablenbindungen zu beeinflussen oder das Programm in anderer Weise entgegen der Intention des
Programmierers zu modifizieren [ST , SVEM , Sch ]. Im harmlosesten Fall wird das Programm dadurch ungültig, erzeugt also einen Kompilierfehler. Weitaus schlimmer wird es,
wenn sich das Programmverhalten unbemerkt ändert, ein Beispiel hierfür lieferte Abbildung
. . Soweit kein geeigneter Testfall diese Änderung im Programmverhalten aufdeckt, führt
ein so entstandener Programmfehler zu erhöhtem Zeitaufwand (und somit Kosten) und stellt
den Einsa von Refaktorisierungswerkzeugen generell in Frage [DDGM , SGM ].
Vermeidung von Redundanzen Refaktorisierungswerkzeuge überschneiden sich in ihrem
konkreten Funktionsumfang. So hat zum Beispiel beinahe jedes Refaktorisierungswerkzeug
in irgendeiner Art mit Zugrei arkeiten zu tun. Dennoch sind momentan innerhalb derselben
Entwicklungsumgebungen ähnliche Funktionalitäten in unterschiedlichen Werkzeugen häufig mehrfach, teils unterschiedlich, und damit fehlerhaft implementiert. Das Beispiel aus Abbildung . zeigte, dass bei der Implementierung des U I
W
P
in IntelliJ
eine Zugrei arkeitsregel der Sprache Java offensichtlich unberücksichtigt blieb, die im Werkzeug C
S
hingegen korrekt implementiert ist. Wünschenswert wäre ein gemeinsames Framework, auf welches alle Refaktorisierungswerkzeuge zurückgreifen könnten
und welches die Berechnung nötiger Änderungen sowie Folgeänderungen und die Überprüfung auf Beibehaltung des Programmverhaltens für alle Werkzeuge einheitlich durchführen
würde. Für einen umfassenden Einsa müsste ein solches Framework allerdings Schni stellen bieten, über die unterschiedlichste Quellcodeänderungen angefordert werden könnten,
was die Komplexität im Inneren dieses Frameworks (im Vergleich zu den bisherigen Einzelwerkzeugen) drastisch erhöhen würde.
Schwächen der statischen Analyse Alle untersuchten Werkzeuge gründen ihre Berechnungen allein auf eine statische Analyse des Quellcodes [TB ]. Während die Vorteile dessen
auf der Hand liegen – auch ohne eine Laufzeitanalyse steht die Funktionalität der Refaktorisierungswerkzeuge zur Verfügung – bleibt die Frage nach reflektivem Programmverhalten
unentscheidbar [TB ] und damit die Frage nach Aufrufen an die Reflection-API in Java faktisch unberücksichtigt. Das zu entwickelnde Framework müsste also so modular aufgebaut
sein, dass sich – wo notwendig – eine Laufzeitanalyse zuschalten ließe.
1.4. Der constraintbasierte Lösungsansatz
Diese Arbeit stellt eine Lösung vor, welche die im vorhergehenden Abschni . genannten
Probleme adressiert. Aufgebaut wird dabei auf den Ideen der constraintbasierten Typrefaktorisierungen, die von einer Arbeitsgruppe um Tip [TKB , DKTE , FTK+ , KETF , Tip ]
entwickelt und schon für verschiedene Werkzeuge erfolgreich eingese t wurden.
Die Grundidee dabei ist, das Berechnen einer bedeutungserhaltenden Programmtransformation auf ein Bedingungserfüllungsproblem (engl. constraint satisfaction problem) [Tsa ] zu
reduzieren. Mögliche, durch das jeweilige Refaktorisierungswerkzeug durchführbare Programmänderungen werden in Form von Constraintvariablen modelliert, die jeweils einzelnen
. Einführung
Programmelementen zugeordnet sind. So werden beispielsweise einer Felddeklaration Constraintvariablen zugeordnet, die ihren Namen [SKP ], ihre Zugrei arkeitsstufe [ST ], ihren
deklarierten Typen [TKB ] sowie den Ort ihrer Deklaration beschreiben. Möchte ein Refaktorisierungswerkzeug eine bestimmte Änderung durchführen, so beispielsweise eine Änderung der Zugrei arkeitsstufe eines Feldes, formuliert es dies in Form einer Bedingung (engl.
constraint). Diese Bedingung bezieht sich auf nur eine Variable, nämlich die, die die Zugreifbarkeitsstufe des gewählten Feldes repräsentiert. Ihr Wert wird gemäß der vom Benu er gewünschten Zugrei arkeit beschränkt.
Ausgehend von einem wohlgeformten Programm ist ein solches Bedingungserfüllungsproblem zunächst immer erfüllbar. Sämtliche Variablen werden dazu mit den Werten belegt, die
dem Zustand vor der Refaktorisierung im Quellcode entsprechen. Einzig die entsprechende
Constraintvariable für die gemäß dem Benu erwunsch zu ändernde Deklaration erhält einen geänderten Wert zugewiesen. Damit ist aber noch nicht sichergestellt, dass die dadurch
repräsentierte Programmtransformation tatsächlich ein gültiges und bedeutungsgleiches Programm erzeugt. Daher werden in einem zweiten Schri über einen Sa Constraintregeln
(engl. constraint rule) auf Grundlage von Abfragen an das Programm (den sogenannten Faktenabfragen) weitere Bedingungen generiert. Diese schränken die Menge der Constraintvariablen
darüber hinaus in ihren möglichen Werten derart ein, dass als Lösungen des Bedingungserfüllungsproblems nur solche verbleiben, die gültige und bedeutungserhaltende Programmtransformationen repräsentieren. Die Constraintregeln bilden also den Teil der Java-Sprachspezifikation ab, der bei Änderungen der zur Verfügung stehenden Constraintvariablen berücksichtigt werden muss.
Der Mechanismus der Constraintgenerierung und der dafür benötigte Regelsa sind zunächst unabhängig von der konkreten Aufgabenstellung eines Refaktorisierungswerkzeugs
formulierbar. Ob ein bestimmtes Refaktorisierungswerkzeug auf einem Constraintsystem, also einer Menge von Constraints und zugehöriger Variablen, au auen kann, hängt davon ab,
ob alle angestrebten Programmtransformationen des Werkzeugs (und alle zu erlaubenden
Folgeänderungen) durch entsprechende Variablenbelegungen ausgedrückt werden können.
Ein möglichst breites Spektrum an Variablen erhöht somit die mögliche Anwendungsdomäne eines Constraintsystems. Für die Autoren des Regelsa es besteht die Herausforderung
darin, die Regeln in Abhängigkeit von den sich aus den Variablen ergebenden Transformationen derart zu formulieren, dass sie erstens vollständig sind, also keine ungültigen oder bedeutungsverändernden Programmtransformationen zulassen, und zweitens die Menge der
erlaubten Programmtransformationen nicht unnötig einschränken. Anders formuliert sollte
jede Constraintregel für sich genommen notwendig und die Regelmenge in ihrer Gesamtheit
hinreichend sein.
Die Stärke des constraintbasierten Ansa es ergibt sich aus der einfachen Formulierbarkeit
der einzelnen, einzuhaltenden Bedingungen in Form von Constraintregeln, von denen jede
einzelne nur einen Teilaspekt des Problems löst und sich in einem durch den Programmierer
überschaubaren Rahmen formulieren lässt. Das komplexe Zusammenwirken der aus allen
Regeln abgeleiteten Bedingungen ist hingegen nicht mehr Sorge des Werkzeugentwicklers,
denn dies kann an einen Constraintlöser delegiert werden. Durch die konjunktive Natur der
Constraints (alle Constraints müssen erfüllt sein) lassen sich auch nachträglich ohne weiteren Aufwand zusä liche Bedingungen formulieren, wie es in dieser Arbeit am Beispiel der
optionalen Berücksichtigung von reflektiven Aufrufen geschieht.
. . Beitrag der Arbeit
1.5. Beitrag der Arbeit
Im Folgenden sind die wesentlichen Beiträge dieser Arbeit in Kürze zusammengefasst:
• Den Haup eil dieser Arbeit bildet die Herleitung und Entwicklung eines Sa es von
Constraintregeln für Java, wobei sich deren mögliches Einsa gebiet aus den durch die
Regeln referenzierten Constraintvariablen ergibt. Durch die Vielseitigkeit der in dieser
Arbeit berücksichtigten Variablen (Namen, Zugrei arkeiten, deklarierte Typen und Deklarationsorte) ist auch eine große Vielfalt implementierbarer Refaktorisierungswerkzeuge gegeben, von einem einfachen R
über ein P
U bis hin zu einem M
T , welches optional auch Zugrei arkeitsmodifizierer mit anpasst.
• Sämtliche Constraintregeln und Variablen sind in Refacola [SKP ] formuliert, einer
formalen Sprache, welche feste Syntax- und Semantikregeln für die Formulierung constraintbasierter Refaktorisierungswerkzeuge und deren Constraintregeln vorgibt. Damit liegen die Constraintregeln gleichzeitig in einer für menschliche Leser kompaktübersichtlichen und für Maschinen interpretierbaren Form vor, was die in vielen bisherigen Arbeiten vorhandene Gefahr einer Diskrepanz zwischen formaler Notation und
tatsächlicher Implementierung [SM ] behebt.
• Ein erweiterter, auf einer Laufzeitanalyse au auender Regelsa erlaubt (optional) die
Constrainterzeugung in Abwesenheit statisch ermi elbarer Abhängigkeiten von Programmelementen.
• Au auend auf diesen Regelsä en wird ein Framework vorgestellt, um so die zur Constraintgenerierung benötigten Faktenabfragen an Java-Projekte zu ermöglichen. Intern
baut das Framework auf den Java Development Tools [JDT ] auf und fügt sich damit
nahtlos in die Eclipse-IDE [ECL ] ein.
• Au auend auf diesem Framework wurden die folgenden Refaktorisierungswerkzeuge
implementiert:
– M
C
U
– P
{M
,F
}
– R
{M
,F
,T
– C
– C
T
{F
,M
{F
,C
,C
U
,M
}
}
,T
}A
• Ein formaler Beweis für die Korrektheit der hergeleiteten Constraintregeln ist nicht möglich. Ein solcher Beweis müsste nämlich als Eingabe eine formale Beschreibung der Sprache Java erhalten, eine solche wird mit der vorliegenden Arbeit aber erst geliefert. Daher
wurden die erstellten Refaktorisierungswerkzeuge anhand von massiven Anwendungen evaluiert. Es wurden mehr als
.
Refaktorisierungen auf acht verschiedenen
Open-Source-Projekten durchgeführt.
• Im Rahmen eines Ausblicks auf zukünftige Weiterentwicklungen wurde weiterhin eine
Implementierung eines Quick-Fix-Werkzeugs zum Anpassen von Zugrei arkeitsmodifizierern implementiert, welches ebenso auf den hergeleiteten Constraintregeln au aut.
. Einführung
1.6. Aufbau der Arbeit
Das folgende, zweite Kapitel dieser Arbeit bildet eine Übersicht verwandter Literaturbeiträge
zu Refaktorisierungen und Refaktorisierungswerkzeugen sowie eine Auflistung derjenigen
Publikationen des Autors, die dieser Arbeit zugrunde liegen. Das dri e Kapitel dieser Arbeit
führt noch einmal detailliert in die in Abschni . nur grob umrissene Idee der constraintbasierten Refaktorisierung ein. Abschni . geht insbesondere auf die Refacola ein, eine domänenspezifische Sprache zur Formulierung constraintbasierter Refaktorisierungswerkzeuge. Ab Kapitel widmet sich diese Arbeit der konkreten Umse ung constraintbasierter Refaktorisierungswerkzeuge. Zunächst wird die Wahl geeigneter Variablen beschrieben, bevor
im nachfolgenden Kapitel ein Sa Constraintregeln für Java entwickelt wird. Die zur Berücksichtigung reflektiven Programmverhaltens nötigen Regeln sind dabei in ein eigenes Kapitel
ausgelagert. Die im Rahmen dieser Arbeit erfolgte Einbindung des Constraintregelwerkes in
die Entwicklungsumgebung Eclipse, sodass dieses dort in Refaktorisierungswerkzeugen unmi elbar nu bar wird, beschreibt Kapitel . Kapitel widmet sich einer Evaluation des Regelsa es anhand von Tests dieser Refaktorisierungswerkzeuge. Das le te Kapitel offeriert
eine Zusammenfassung.
Konkrete Constraintregeln werden innerhalb dieser Arbeit nur dort aufgelistet, wo sie unmi elbar zur Beschreibung eines bestimmten Sachverhaltes dienen. Eine komple e Auflistung der Deklarationen aller Constraintvariablen findet sich in Anhang A, eine vollständige
Auflistung aller Constraintregeln in Anhang B sowie eine vollständige Auflistung der verwendeten Refaktorisierungsdefinitionen in Anhang C.
2. Literaturbetrachtungen
Dieser Abschni listet zugrundeliegende und verwandte Arbeiten zu Refaktorisierungen und
Refaktorisierungswerkzeugen auf. Da die Literaturlandschaft zu diesem Thema mi lerweile
als üppig gelten kann (zum Jahre
bereits mehr als
Publikationen [MT ]), wurde sich
einerseits auf eine Darstellung der wichtigsten Meilensteine bei der Entwicklung von Refaktorisierungswerkzeugen, sowie andererseits auf Vorarbeiten zu constraintbasierten Refaktorisierungswerkzeugen und den zu ihnen alternativen Ansä en konzentriert. Ebenso findet
sich eine Auflistung von Arbeiten des Autors, die Grundlage dieser Arbeit waren oder Teilergebnisse dieser Arbeit bereits verwendet haben.
2.1. Die Anfänge
Die erste Arbeit zu Refaktorisierungen und auch Refaktorisierungswerkzeugen ist Griswold
[Gri ] zuzuschreiben. Er entwickelte in seiner Dissertation die Idee, Änderungen an der
Struktur eines Programms von anderen Entwicklungstätigkeiten zu trennen. Lässt man während Strukturänderungen eines Programms das Programmverhalten unverändert, erleichtert
dies die Erkennung von dabei eingebrachten Programmierfehlern, die sich dann in geändertem Programmverhalten widerspiegeln müssen [Gri ].
Bereits mit dieser ersten Arbeit zum Refaktorisieren machte Griswold die Bedeutung der
Werkzeugunterstü ung deutlich und lieferte einen Sa von Werkzeugen für die Refaktorisierung von Scheme-Programmen. Die nötige Programmanalyse zur Berechnung der notwendigen Quelltex ransformationen sowie zur Prüfung auf unverändertes Programmverhalten geschah anhand von Programmabhängigkeitsgraphen (engl. program dependence graph)
[KKP+ ], also Graphen, in denen die Knoten die Anweisungen des Programms und die Kanten ihre Abhängigkeiten, hauptsächlich Kontroll- oder Datenfluss, repräsentieren. Griswold
baute dabei auf eine gegebene Implementierung von Programmabhängigkeitsgraphen auf,
die in zusä lichen Kanten weitere indirekte Abhängigkeiten repräsentiert ha e und die für
Refaktorisierungen relevant sind. So betrachtete er zum Beispiel Kanten, die beizubehaltende
Reihenfolgen für Anweisungen angeben. Mit diesen kann zum Beispiel verhindert werden,
dass eine Anweisung vor eine andere Anweisung verschoben wird, die ihren Effekt au eben
würde. Dies könnte der Fall sein, wenn einer Variablen ein neu berechneter Wert zugewiesen
werden soll, wobei nicht die Zuweisung vor der Neuberechnung sta finden darf.
2.2. Vor- und Nachbedingungen für Refaktorisierungen
Der eigentliche Begriff der Refaktorisierung wurde dann ein Jahr später durch die Arbeit von
Opdyke [Opd ] geprägt. Während Griswold den Fokus (bedingt durch die Wahl der Programmiersprache Scheme) auf Anweisungen innerhalb von Blöcken se te [Gri ], betrachtete
. Literaturbetrachtungen
Opdyke mit C++ eine objektorientierte Sprache und für diese insbesondere Refaktorisierungen, die Typhierarchien restrukturieren. Au auend auf vergleichsweise einfach durchzuführenden Teilrefaktorisierungen, wie zum Beispiel dem Löschen oder Einfügen einer Deklaration, beschreibt er drei komplexe Refaktorisierungen, so zum Beispiel das Einfügen einer
abstrakten Superklasse, die gemeinsames Verhalten zweier (dann) Subklassen zusammenfasst. Richtungsweisend ist dabei die Betrachtung der bei den Refaktorisierungen einzuhaltenden Vorbedingungen (engl. precondition), die sich insbesondere durch das Betrachten der
kleineren Teilrefaktorisierungen einfacher überblicken und somit prüfen lassen.
Auf der Arbeit von Opdyke au auend nu t Roberts [Rob ] (ebenso später Koch und
Kniesel [KK ]) in seiner Dissertation zusä lich Nachbedingungen (engl. postcondition), von
deren Erfüllung nach einer Refaktorisierung ausgegangen werden kann. Mit diesen lässt sich
die Komplexität von Refaktorisierungen (und damit Refaktorisierungswerkzeugen) reduzieren. Findet nämlich eine Aufteilung einer Refaktorisierung in einzelne Teilrefaktorisierungen
sta und entsprechen dabei die Nachbedingungen einer vorhergehenden Refaktorisierung
den Vorbedingungen der nachfolgenden Refaktorisierung, kann eine weitere Prüfung dieser
Bedingungen entfallen.
2.3. Refaktorisierungen für dynamische Sprachen
Hervorzuheben an der Arbeit von Roberts [Rob ] ist insbesondere, dass das entstandene
Tool – der Refactoring Browser [RBJ ] – Smalltalk-Programme refaktorisiert. Smalltalk ist dynamisch typisiert [GR ]. Da somit schon bei einfachen Methodenaufrufen unklar ist, an welche Methode zur Laufzeit gebunden wird (oder in Smalltalk-Sprechweise: bei Nachrichtenversand erst zur Laufzeit klar ist, wer der Nachrichtenempfänger sein wird), lassen sich nur
bedingt Aussagen über das Programmverhalten aus dem reinen Quelltext entnehmen, wie es
zum Beispiel bei Griswold durch Extraktion eines Programmabhängigkeitsgraphen aus dem
Quellcode geschehen konnte [Gri ]. Zwar konnten Feldthaus et al. [FMM+ ] zeigen, dass
eine statische Quelltextanalyse durchaus sinnvoll Refaktorisierungen dynamischer Sprachen
unterstü en kann, dennoch bleibt die Frage nach dem Programmverhalten im allgemeinen
Fall unentscheidbar. Sta dessen geschieht beim Refactoring Browser die Programmanalyse
zur Laufzeit, was auch die Ausführung der nötigen Änderungen (zumindest in Teilen) erst
zur Laufzeit erlaubt. Diese Idee eines sich zur Laufzeit selbst ändernden Programms mag aus
heutiger Sicht seltsam erscheinen, fügte sich aber in das – durch und durch reflektive – Konzept Smalltalks nahtlos ein, in welchem dank Just-in-time-Kompilierung sogar die jeweilig
zum Programmieren verwendete Smalltalk-Umgebung selbst verändert und erweitert werden konnte.
2.4. Referenzkonstruktion
Sollen Refaktorisierungswerkzeuge für Java entwickelt werden, lässt sich wegen der Ähnlichkeit von C++ und Java am ehesten auf Arbeiten von Opdyke au auen. Allerdings hat dieser
nur Teilaspekte der Sprache C++ berücksichtigt. Entstehen bei seinem R
zum Beispiel
Namenskonflikte, wird die Refaktorisierung abgelehnt [Opd ], ohne gegebenenfalls mit Hilfe von den in C++ verfügbaren namespaces gegenzusteuern. Für die Sprache Java, welche mit
. . Referenzkonstruktion
dem Verdecken (engl. hiding), Überladen (engl. overloading) und Verscha en (engl. shadowing)
gleich mehrere Konzepte anbietet, verschiedene deklarierte Elemente gleich zu benennen, ist
ein pauschales Ablehnen von Refaktorisierungen bei Namenskonflikten eine sehr starke Einschränkung. Sta dessen sollten die vorhandenen Mi el der Programmiersprache – im Falle
von Namenskonflikten die Qualifizierung von Referenzen – jeweils voll und geeignet zum
Einsa kommen.
Schäfer et al. [SEM ] haben sich dieses Problems angenommen und eine Lösung präsentiert, die durch Ausnu en der Java-Sprachmi el zur Qualifikation von Referenzen unnötig rigorose Refaktorisierungen verhindert. Dabei bauen sie ebenso wie Opdyke [Opd ] auf Teilrefaktorisierungen, also kleinere Refaktorisierungen, aus denen sich die Gesam ransformation
zusammense t. Diese Teilrefaktorisierungen können dann jeweils für sich implementiert, verifiziert, getestet und ausgeführt werden, was sowohl die Entwicklungsarbeit, als auch Laufzeit verringern kann.
Dem Problem, dass die Teiltransformationen bei einer Refaktorisierung für sich genommen
nicht zwangsläufig ein gültiges Programm ergeben – man denke an ein R
, bei dem die
Referenzen auf eine umbenannte Deklaration gleichzeitig mit dieser angepasst und gegebenenfalls qualifiziert werden müssen – begegnen Schäfer et. al [SEM ] mit Spracherweiterungen. In der hierfür genu ten Zwischensprache NameJava [Sch ] (beziehungsweise ihrer
Weiterentwicklung JL [STST ]) können Referenzen nicht nur, wie in Java üblich, über (gegebenenfalls qualifizierte) Namen erfolgen, sondern ebenso auch fest verzeigert werden (sogenanntes locking [Sch ]). Dies ermöglicht es zum Beispiel, nach vorherigem locking aller Referenzen erst eine Deklaration umzubenennen und anschließend in separaten Schri en die
jeweils betroffenen Referenzen zu behandeln. Währenddessen bleibt das Programm jeweils
ein gültiges NameJava-Programm, bis alle Referenzen wieder zu regulären Java-Referenzen
zurückgewandelt wurden (das sogenannte unlocking).
Der schwierige Teil ist dabei der Le tgenannte, das unlocking. Es müssen Abbildungsvorschriften gefunden werden, mit denen sich für einen gegebenen Programmpunkt und eine gegebene Deklaration eine gültige Referenz auf diese erzeugen lässt. Das Verfahren dazu nennt
sich Referenzkonstruktion (engl. reference construction) [STST ]. Betrachtet man die Binderegeln
der Sprache Java als eine partielle Funktion
lookup : ProgramPoint × Reference ⇀ Declaration
[STST ], welche zu einer gegebenen Referenz r am Programmpunkt p die referenzierte Deklaration d = lookup(p, r) gemäß der Binderegeln in Java findet, so muss für das unlocking also
genau eine Rechtsinverse dieser Funktion zum Einsa kommen
access : ProgramPoint × Declaration ⇀ Reference
[STST ], für die dann der Ausdruck
∀p, d : lookup(p, access(p, d)) = d
[STST ] erfüllt sein muss. Der Beitrag von Schäfer liegt vor allem darin, für die komplexen
Bindealgorithmen in Java solche inversen Funktionen gefunden, implementiert und verifiziert
zu haben [SEM , Sch ]. Steht eine solche Funktion zur Verfügung, können nicht nur Umbenennungen, sondern auch viele weitere – insbesondere auch Verschieberefaktorisierungen
– mit korrekter Beibehaltung aller Namensbindungen implementiert werden [SM ].
. Literaturbetrachtungen
88
89
class A {
int m(Object o) { return 1; }
class A {
int m m1 (Object o) { return 1; }
private int mm2 (String s) { return 2; }
}
90
91
}
92
93
94
95
96
class B extends A {
int i = new B().m ("abc");
private int m(String s) { return 2; }
}
⇒
class B extends A {
int i = new B().↑ m2 ("abc");
}
97
98
99
100
class C extends A {
int j = new C().m("def");
}
(a)
class C extends A {
int j = new C().↑ m1 ("def");
}
(b)
Abbildung . .: Soll im linken Programm (a) die Methode m(String) von der Klasse B in die
Klasse A verschoben werden, ergibt sich nach Schäfer [Sch ] während des
unlocking das rechts gezeigte Programm (b) mit (noch teilweise) locked bindings. Entsprechend der Notation aus [STST ] bilden die Superskripte m1
und m2 dabei eindeutige Namen, die mi els der durch Pfeile markierten Referenzen ↑ m1 und ↑ m2 referenziert werden.
Der Vorteil des Ansa es nach Schäfer et. al – nämlich bei Refaktorisierungen alle Referenzen für sich genommen betrachten und behandeln zu können – birgt allerdings auch einen
Nachteil. Alle während des unlocking durchgeführten Änderungen dürfen nur lokal Auswirkungen haben, aber in keinem Fall Einfluss auf das unlocking weiterer Referenzen haben. Anderenfalls wären wechselseitige Beeinflussungen möglich, die eine unabhängige Bearbeitung
verbieten. Aus diesem Grunde sind bei Schäfer et. al beim unlocking auch nur solche Änderungen gesta et, die sich jeweils lokal auswirken, was mögliche Folgeänderungen einer Refaktorisierung auf das Einfügen von Qualifizierern für Referenzen beschränkt. Dies kann aber nun
wiederum zu unnötigen Einschränkungen der Werkzeuge führen. Abbildung . (a) zeigt ein
Beispiel, in welchem die Methode m(String) mi els eines P
U M
aus der Klasse B
in die Klasse A verschoben werden soll. Während der Refaktorisierung ergibt sich das in Abbildung . (b) gezeigte JL -Programm, in welchem noch das unlocking der Referenzen ↑ m1
respektive ↑ m2 aussteht. Für ↑ m1 würde die access-Funktion wieder die ursprüngliche JavaReferenz m("def") einse en, ohne dass sich eine Änderung der Methodenbindung ergäbe.
Für ↑ m2 ist ein unlocking nicht möglich, da die private Methode m2 nicht mehr zugrei ar
ist, worauf auch eine qualifizierte Referenz keinen Einfluss nehmen kann. Ein unlocking, welches die Zugrei arkeit von m2 anpasst (indem zum Beispiel der private-Modifizierer entfernt
wird) wäre wünschenswert, untergräbt aber das vorher durchgeführte unlocking von ↑ m1 .
Dieses ha e nur Erfolg, solange m2 nicht zugrei ar war. Im Falle einer zugrei aren Methode m2 wäre ein zusä licher Type Cast notwendig gewesen, sodass die access-Funktion den
Ausdruck m((Object)"def") hä e erzeugen müssen, um die Bindung an m1 beizubehalten.
Die Idee des unlocking funktioniert also gut, um Änderungen zu berechnen, die nur lokal
Auswirkungen im Programm haben, wie es insbesonderen für Qualifizierungen von Referen-
. . Constraintbasierte Refaktorisierungen
zen der Fall ist. Ebenso konnten Schäfer et al. zeigen, dass ihre locking/unlocking-Mechanismen
auch geeignet sind, um Bedingungen zu parallelem Programmverhalten zu erhalten [SDS+ ],
wobei hier im Wesentlichen ohnehin nur Bedingungen zu prüfen sind, deren Nichterfüllung
dann zu einem Abbruch der Refaktorisierung führen muss. Für die Behandlung von Folgeänderungen, die sich global im Programm auswirken können, ist der Ansa von Schäfer allein
nicht tauglich.
2.5. Constraintbasierte Refaktorisierungen
Möchte man die bei Refaktorisierungen einzuhaltenden Bedingungen nicht mehr isoliert betrachten, sondern ihre Wechselwirkungen zusä lich berücksichtigen (können), ergibt sich der
Übergang von der Erfüllung einzelner Bedingungen zur Erfüllung eines komplexen Bedingungserfüllungsproblems.
Den Grundstein für die ersten constraintbasierten Refaktorisierungswerkzeuge legten Palsberg und Schwarzbach [PS ], welche für eine Modellsprache (die sogenannte BOPL – Basic
Object Programming Language) ein System von Typ-Constraints einführten, mit dem sich prüfen lässt, ob ein gegebenes Programm die Typregeln der Sprache erfüllt. Darauf au auend
entwickelten Tip et. al [TKB ] die Idee, den Schri von einer reinen Bedingungsprüfung hin
zu einer Bedingungserfüllung zu gehen, wie sie beim Refaktorisieren von Bedeutung ist und
wie sie bereits in Abschni . einführend beschrieben ist. Tip et al. konzentrierten sich dabei ebenso wie Palsberg und Schwarzbach auf Bedingungen, die sich aus dem Typsystem der
Sprache Java ergeben. Ähnlich wie bei Änderungen der Zugrei arkeiten von Deklarationen
gilt ebenso für das Ändern deklarierter Typen von Deklarationen, dass hierfür auch an weiteren Programmstellen Auswirkungen auf das Programmverhalten zu berücksichtigen sind,
weswegen eine isolierte Betrachtung solcher Quellcodeänderungen nicht funktionieren kann.
Die Übertragung der Typ-Constraints von Palsberg und Schwarzbach auf die Typregeln von
Java führte zunächst zur praxistauglichen Implementierung der constraintbasierten Refaktorisierungswerkzeuge U S
W
P
und G
T
[TKB ], später
folgten noch weitere Werkzeugentwicklungen [STD , BTF , Tip , TFK+ ].
Ganz ähnlich zu den Arbeiten von Tip et al., ohne die verwendeten Prüfungen formal als
Constraintsystem zu beschreiben, entwickelten Steimann [Ste ] et al. mit I
T
ein Werkzeug, das nicht nur Refaktorisierungen unter Zuhilfenahme der vorhandenen, deklarierten
Typen erlaubt, sondern die Einführung neuer, an die jeweilige Verwendung eines Typen angepasster, Interfaces erlaubte. Auf denselben Typprüfungen basierend folgte auch durch die
Gruppe um Steimann die Entwicklung weiterer Werkzeuge [SM , KS ]. Mit der Einführung
generischer Typen in Java war es hingegen wieder die Arbeitsgruppe um Tip, die entsprechende Erweiterungen der Typ-Constraints vornahm, um auch generische Typen zu berücksichtigen, und die insbesondere auch Refaktorisierungswerkzeuge entwickelte, die helfen, in
bestehendem Code Typen geeignet zu parametrisieren [KETF , FTK+ ].
Ein Problem der bis dahin entwickelten constraintbasierten Refaktorisierungswerkzeuge ergibt sich aus der Diskrepanz zwischen formaler Notation der Constraintregeln und fehlender
Formalisierung der eigentlichen implementierten Refaktorisierungen [SM ]. Die Entwicklung der domänenspezifischen Sprache Refacola [SKP ] durch Steimann et al. schließt diese
Lücke, indem sie nicht nur eine formale Darstellung von Constraintregeln erlaubt, sondern
. Literaturbetrachtungen
auch die formale Beschreibung der erwarteten Eingabeprogramme und zu entwickelnden
Werkzeuge gesta et und aus diesen automatisch Code zu generieren vermag. In Zusammenarbeit mit dem Autor der vorliegenden Arbeit sind verschiedene Abschlussarbeiten entstanden, die Teilaspekte der Sprache Java in Refacola abbilden [Wag , Mai , Spe ].
Vorübergehend offen geblieben ist bei den Arbeiten der Arbeitsgruppen um Tip und Steimann das Problem der Vorausschau [ST ]. Bedingungen, aufgrund derer ein Constraint aus
einer Constraintregel generiert werden muss, können sich mitunter erst während des Constraintlösens erfüllen. Während sich dieses Problem bei isoliert betrachteten Einzelaspekten –
wie es bei Tip mit den deklarierten Typen der Fall war – noch umgehen lässt, indem entweder
keine weiteren Änderungen zugelassen werden oder alle weiteren Änderungen (vorausschauend) als Teil der vom Benu er gewünschten Änderungen bekannt sein müssen und somit
unveränderlich sind, gewinnt das Problem zunehmend Oberhand, sobald unterschiedliche
Arten von Variablen gleichzeitig zum Einsa kommen. Steimann und von Pilgrim [SP a]
haben dieses Problem mi els automatischer Regeltransformationen gelöst, auf welche auch
Abschni . dieser Arbeit noch einmal eingehen wird.
2.6. Weitere constraintbasierte Werkzeuge
Die Technologie der constraintbasierten Refaktorisierungen lässt sich auch auf weitere Arten
von Programmierwerkzeugen übertragen. Noch im engen Verwandtschaftsverhältnis zu den
in dieser Arbeit vorgestellten Refaktorisierungswerkzeugen steht die Arbeit von Steimann
und v. Pilgrim zu einem Werkzeug für unbenannte Refaktorisierungen [SP b, SP ]. Für
diese ist eine Refaktorisierung nicht mehr an das Korse eines spezifischen Werkzeugs gebunden, viel mehr erhält das Werkzeug als Eingabe zwei Programmversionen, zwischen denen
beliebige Änderungen sta gefunden haben können. Als Ausgabe liefert ein solches Werkzeug
dann entweder die Information, dass sich das Programmverhalten zwischen beiden Versionen
unweigerlich unterscheidet, oder eine Menge von Folgeänderungen, die nötig sind, um dies
zu verhindern. Allerdings müssen Steimann und v. Pilgrim einräumen, dass der Vergleich beliebiger Programme hinsichtlich ihres Verhaltens schwierig ist [SP b, SP ] (im allgemeinen
Fall sogar unentscheidbar [BA ]). Daher beschränken sie sich bei ihren Betrachtungen auf
solche Programmtransformationen, die sich durch eine vorgegebene Menge von Constraintvariablen beschreiben lassen und für welche dann der constraintbasierte Ansa direkt angewendet werden kann.
Dass sich constraintbasiert nicht nur Programmtext, sondern auch Modelle refaktorisieren
lassen, zeigte unlängst Steimann [Ste , Ste ]. Modelle als Abstraktion konkreten Programmtextes büßen naturgemäß an Informationsgehalt ein, zumeist, was konkretes Programmverhalten angeht. Somit liegt bei den anzuwendenden Constraintregeln der Schwerpunkt eher bei
der Wohlgeformtheit der Modelle, als der Bedeutung der durch sie repräsentierten Programme. Dies ändert sich, wenn man nicht nur isoliert die Modelle, sondern auch aus ihnen generierten Code mit refaktorisiert. Wird nämlich derart generierter Code nachträglich von Hand
ergänzt und angereichert, verbietet sich nach einer Refaktorisierung eine Neugenerierung,
da diese die erfolgten Änderungen im Java-Code überschreiben würde. Sta dessen müssen
simultan das Modell und der Code transformiert werden, was – wie in [PUTS ] gezeigt –
constraintbasiert gut gelingt.
. . Formale Darstellungen von Programmiersprachen
Das enge Verwandtschaftsverhältnis von Refaktorisierungswerkzeugen zu solchen Assistenzprogrammen, die für gegebene Programme verle te Eigenschaften der Wohlgeformtheit
(wieder-) herstellen, konnten Steimann und Ulke [SU ] demonstrieren. Au auend auf den
Arbeiten zu constraintbasierten Refaktorisierungen für Modelle entwickelten sie Werkzeuge,
die mi els eines Constraintlösers nach einer solchen Menge von Änderungen im Modell suchen, welche die verle ten Wohlgeformtheitseigenschaften wiederherstellt. Damit haben sie
Quick-Fix-Werkzeuge beschrieben, die analog zu denen arbeiten, wie sie im Ausblick dieser
Arbeit in Abschni . beschrieben sind.
2.7. Formale Darstellungen von Programmiersprachen
Constraintbasierte Refaktorisierungswerkzeuge schaffen mit den Constraintregeln – zumindest in Ausschni en – eine formale Darstellung einer Programmiersprache und den in ihr geschriebenen Programmen. Auch in dieser Hinsicht kann auf verwandte Literatur verwiesen
werden. So existieren verschiedene Arbeiten, in denen einzelne Eigenschaften der Sprache Java formal nachgewiesen wurden. Als Grundlage für solche Beweise wird naturgemäß ebenso
eine formale Darstellung der Sprache benötigt, aus welcher dann erst die nachzuweisenden
Eigenschaften abgeleitet werden können.
Erste Arbeiten für Java konzentrierten sich auf den Nachweis der Korrektheit des Typsystems. Sie sollten ausschließen, dass tro der statischen Typprüfung zur Laufzeit nicht dennoch Typfehler auftreten können [DE , NO ], ohne dass dies durch geeignete LaufzeitAusnahmebehandlungen behandelt wird. Damit haben diese Arbeiten Ähnlichkeit mit den
oben erwähnten Arbeiten von Tip [Tip ] und deren Typ-Constraints, die ebenso das Typsystem von Java formalisieren. Im direkten Vergleich lesen sich – abgesehen von den unterschiedlichen Weisen der Notation – viele Regeln beinahe identisch, zum Beispiel eine solche,
dass der Typ eines Konstruktoraufrufs der instanziierten Klasse entsprechen muss ([TKB ]
und [NO ]). Ein direkter Abgleich aller Regeln eins-zu-eins fällt dennoch schwer, da bei Korrektheitsbeweisen für Java bisher stets nur Teilaspekte der Sprache berücksichtigt wurden
[DE , NO , ON , Ohe , Sch , KN ]. So wurde zum Beispiel in [NO ] die Anzahl der
Methodenparameter einer Methode auf einen einzigen beschränkt, die Typregeln von Tip et
al. [Tip ] hingegen berücksichtigen auch Parameterlisten. Dennoch lassen sich bei der Formalisierung einige Gemeinsamkeiten erkennen, so wird in beiden Arbeiten der in Java nur
implizit vorhandene Typ eines Ausdrucks explizit gemacht – auf der einen Seite mi els einer
Constraintvariablen [TKB ], auf der anderen Seite durch zusä liche sogenannte type annotations [NO ].
2.8. Refaktorisierungen als Graphtransformationen
Refaktorisierungen sind Transformationen von Programmen. Gleichzeitig lassen sich Programme entsprechend der für sie geltenden Grammatiken gut als Graphen darstellen. Somit
können Refaktorisierungen als Graphtransformationen betrachtet werden, eine Disziplin, in
welcher bereits auf umfassende Forschungsergebnisse zurückgegriffen werden kann [Roz ].
Dieser Idee folgend haben Mens et al. Refaktorisierungen mi els Graphtransformationen
beschrieben [MvEDJ , MDJ ]. Am Beispiel zweier Werkzeuge für E
V
. Literaturbetrachtungen
respektive P
U M
machen sie ganz ähnlich zum constraintbasierten Ansa Bedingungen aus, die für die jeweilige Transformation erhalten werden müssen und geben Transformationsregeln für die jeweiligen Refaktorisierungen an. Interessant an diesem Ansa ist,
dass mit dem Handwerkszeug der Graphtransformationen bereits auf viele Erkenntnisse zurückgegriffen werden kann, beispielsweise zur Analyse von Relationen zwischen verschiedenen Transformationen. So lassen sich beim Einsa von Vorschlagsgeneratoren für Refaktorisierungen (zum Beispiel [TR ]) die einzelnen Vorschläge zueinander in Bezug bringen
und Aussagen ableiten, welche Vorschläge sich gegenseitig ausschließen oder voneinander
unabhängig sind [MTR ].
Problematisch an der Arbeit von Mens et al. [MvEDJ , MDJ ] gestaltet sich indessen,
dass sie auf einem recht abstrakten Niveau bleibt, insbesondere betrachtet sie keine Programmiersprache im Besonderen, sondern objektorientierte Sprachen im Allgemeinen, sodass viele
Sprachkonstrukte – wie zum Beispiel innere Klassen in Java – keine Berücksichtigung finden [MvEDJ ]. Zwar lassen sich aus der von ihnen selbst als Machbarkeitsstudie [MvEDJ ,
MDJ ] bezeichneten Arbeit auch konkrete Werkzeuge für Java ableiten [FYBT ], doch es
bleibt unklar, ob der Ansa auch erfolgreich ist, wenn es alle Eigenheiten einer konkreten
Programmiersprache zu berücksichtigen gilt.
2.9. Testen von Refaktorisierungswerkzeugen
Die formale Verifikation von Refaktorisierungswerkzeugen ist schwierig, wie in Abschni .
thematisiert werden wird. Daher bietet es sich an, das korrekte Verhalten der Werkzeuge mittels Tests zu überprüfen. Neben handgeschriebenen Regressionstests, die speziell hierfür erdachte Programme refaktorisieren und die Ausgabe mit Erwartungswerten vergleichen, bieten sich ebenso automatisierte Tests an, die sich die spezifischen Eigenschaften von Refaktorisierungswerkzeugen zunu e machen.
Eine erste Arbeit hierzu lieferten Dig et al. [DDGM ], die mit ASTGen ein Werkzeug zum
automatischen Generieren von zu refaktorisierenden Probanden lieferten. Die Probanden ergeben sich dabei aus Kombinationen einzelner Teilprobanden, für die nach der Refaktorisierung einzuhaltende Bedingungen bekannt sind. So lässt sich mit begrenztem manuellen Einsa (der Spezifikation der Teilprobanden und der zu erhaltenden Bedingungen) durch viele
Kombinationsmöglichkeiten dieser eine große Anzahl Testfälle generieren. Obwohl mit diesem Ansa eine beachtliche Zahl von Fehlern in verbreiteten Refaktorisierungswerkzeugen
gefunden werden konnte [DDGM ], verbleibt unklar, ob diese auch bei tatsächlicher Verwendung der Werkzeuge auf realen Softwareprojekten Relevanz haben.
An diesem Punkt se t die Idee des Refactoring Tool Testing (RTT) [TS , EH ] an. Ein solches Werkzeug nu t als Probanden reale, quelloffene Projekte und wendet auf diese systematisch Refaktorisierungen an. Es besteht im Wesentlichen aus drei Komponenten [TS , EH ].
Die erste ist eine zentrale Ablaufsteuerung, die die Probanden verwaltet und in einer Schleife
über alle möglichen Anwendungsstellen des jeweils zu testenden Werkzeugs auf den Probanden iteriert und die dabei gemessenen Ergebnisse protokolliert. Wie diese Anwendungsstellen
zu finden sind, ist in Abhängigkeit vom zu testenden Werkzeug zu entscheiden. Die hierfür
erforderliche zweite Komponente – der Refaktorisierungs-Adapter [TS , EH ] – ist für jedes
Werkzeug individuell zu entwickeln. Für einen gegebenen Probanden sucht dieser Adapter
.
. Einordnung der vorliegenden Arbeit
alle möglichen Anwendungsstellen für das gegebene Werkzeug und berichtet entsprechende
Belegungen der Eingabeparameter. Die dri e Komponente besteht aus einem Sa von Orakeln [TS , EH ], der zu beurteilen versucht, ob die durchgeführten Werkzeuganwendungen
korrekt waren. Von wesentlicher Bedeutung ist dabei einerseits der Compiler, welcher nach
der Refaktorisierung (für ein zuvor fehlerfrei kompilierendes Programm) keine Fehler ausgeben darf, andererseits können aber auch den Probanden beiliegende Regressionstests genu t
werden, um auf geändertes Programmverhalten schließen zu können. Insgesamt wurde die
Idee des Refactoring Tool Testing bereits bei der Entwicklung einer Vielzahl von Refaktorisierungswerkzeugen [Ste , KS , ST , Ikk , SKP , STST ] und auch einem weiteren Programmierwerkzeug [BFS ] genu t, wobei Gligoric et al. [GBO+ ] später die Technik mi els
weiterer Orakel nochmals verfeinert haben.
Ein Problem beider genannter Verfahren zum automatisierten Test von Refaktorisierungswerkzeugen ist, dass sie jeweils nur einseitig den Blick auf die ausführbaren Refaktorisierungen richten und nicht die wegen nicht erfüllter Vorbedingungen abgelehnten Refaktorisierungen hinterfragen. Soares et al. [SMG , SGM ] liefern mit ihrem Testwerkzeug eine
Funktion, die Ergebnisse unterschiedlicher Implementierungen von Werkzeugen (beispielsweise aus verschiedenen Entwicklungsumgebungen) miteinander vergleicht und aus dabei
ermi elten Diskrepanzen Rückschlüsse auf zu strenge Vorbedingungen zieht.
2.10. Einordnung der vorliegenden Arbeit
Teile der in dieser Arbeit vorgestellten Forschungsergebnisse wurden bereits vorab publiziert.
Umgekehrt bauten bereits Arbeiten sowohl mit, als auch ohne Beteiligung des Autors vorliegender Arbeit auf den in dieser Arbeit präsentierten Ergebnissen auf. Die folgenden beiden
Unterabschni e listen diese Vor- und Folgearbeiten auf.
2.10.1. Vorarbeiten
From Public to Private to Absent: Refactoring Java Programs under Constrained Accessibility [ST ]: Diese Veröffentlichung überträgt erstmals die Idee der constraintbasierten Refaktorisierungen auf eine Domäne abseits deklarierter Typen, nämlich auf Zugrei arkeiten.
Für diese, als Variablen formuliert, wurde ein Sa von Constraintregeln abgeleitet, mit welchem für drei verschiedene Refaktorisierungswerkzeuge (M
T ,P
U M
sowie
C
A
) erstmals die korrekte Anpassung von Zugrei arkeiten berechnet werden konnte. Die verwendeten Constraintregeln gehen dabei teilweise auch auf frühere Arbeiten zu nicht constraintbasierten Werkzeugen zurück [BGS ]. Die in der vorliegenden Arbeit
genannten Constraintregeln zu Zugrei arkeiten in Kapitel . beinhalten zum Großteil die
Constraintregeln aus [ST ].
RefaFlex: Safer Refactorings for Reflective Java Programs [TB ]: Diese Arbeit löst erstmalig das Problem, wie reflektives Programmverhalten in Java-Programmen bei Refaktorisierungswerkzeugen korrekt berücksichtigt werden kann. Da das Programmverhalten reflektiver Java-Anweisungen im schlimmsten Falle zum Zeitpunkt des Kompilierens unentscheidbar ist, kommt im Rahmen dieser Lösung eine Laufzeitanalyse zum Einsa , wie es schon bei
. Literaturbetrachtungen
Roberts [Rob ] der Fall war. Im Gegensa zu seiner Lösung für Smalltalk werden die zur
Laufzeit gesammelten Informationen allerdings nicht zu dieser in Programmtransformationen umgese t. Sta dessen wird die Laufzeitanalyse vor der Transformation des Quellcodes
vollständig abgeschlossen. Die dabei gesammelten Daten bilden die Grundlage für eine Constraintgenerierung gemäß einem entsprechenden Regelsa , die insgesamt fünf verschiedene
Werkzeuge der Eclipse JDT constraintbasiert erweitert, dass auch reflektive Programme korrekt refaktorisieren werden können. Kapitel der vorliegenden Arbeit gibt große Teile dessen
noch einmal wieder.
A Comprehensive Approach to Naming and Accessibility in Refactoring Java Programs
[STST ]: Während der Ansa des locking und unlocking von Schäfer bei der Referenzkonstruktion eine elegante Lösung darstellt, ist er ungeeignet, bei Refaktorisierungen solche Folgeänderungen zu unterstü en, die sich nicht nur lokal für die jeweils geänderte Anweisung
auswirken (siehe Abschni . ). Umgekehrt ist eine Referenzkonstruktion mit dem constraintbasierten Ansa beinahe aussichtslos. In dieser Arbeit werden beide Ansä e miteinander
verbunden, indem am Beispiel zweier Werkzeuge (P
U M
und E
I
)
einerseits mi els Zugrei arkeitsconstraints die hierfür notwendigen Anpassungen von Zugrei arkeitsmodifizierern berechnet sowie Zugrei arkeitsbedingungen geprüft wurden und
andererseits mit dem Ansa nach Schäfer die Korrektheit der Namensbindungen sichergestellt wurde. Neben teilweiser Übernahme von Ergänzungen und Weiterentwicklungen verschiedener Constraintregeln schlagen sich die Ergebnisse dieser Vorarbeit vor allem in der
Implementierung und Wahl der Constraintvariablen nieder, wobei nach diesen Erkenntnissen
auf eine Referenzkonstruktion verzichtet werden konnte. Die Abschni e . . und . gehen
hierauf jeweils ein.
From behaviour preservation to behaviour modification: Constraint-based mutant generation [ST ]: Der constraintbasierte Ansa lässt sich nicht nur nu en, um Programmverhalten
wie im Fall der Refaktorisierungswerkzeuge beizubehalten, sondern im Gegenteil auch gezielt zu verändern, wie es die Herausforderung beim Mutationstesten ist. Die Ideen zum in
Abschni . beschriebenen constraintbasierten Mutationstesten wurden erstmalig in dieser
Arbeit publiziert.
2.10.2. Folgearbeiten
Constraint-Based Refactoring with Foresight [SP a]: Diese Arbeit, in welcher Steimann und
v. Pilgrim das Problem der Vorausschau gelöst haben (siehe auch oben in Abschni . ), wurde anhand eines P
U F
-Werkzeugs evaluiert, bei dem große Teile der Implementierung – insbesondere der Refacola-Sprachdefinition für Java sowie Faktenextraktion – vom
Autor der vorliegenden Arbeit beigesteuert wurden und direkte Vorläufer der in Kapitel ,
und vorgestellten Implementierung sind.
Model/Code Co-Refactoring: An MDE Approach [PUTS ]: Vorstehende Arbeit betrachtet
Co-Refaktorisierungen von Modellen und aus ihnen generiertem Java-Code. Die dabei zum
Einsa gekommenen Constraintregeln für die drei betrachteten Co-Refaktorisierungswerkzeuge G
D
T ,P
U und R
sowie die Sprachdefinition in Re-
.
. Einordnung der vorliegenden Arbeit
facola und die zugehörige Faktengenerierung entsprechen beinahe unverändert der in vorliegender Arbeit entwickelten. Es konnte somit direkt auf den Ergebnissen der vorliegenden
Arbeit aufgebaut werden.
3. Constraintbasierte Refaktorisierungen
Dieses Kapitel stellt das Konzept der constraintbasierten Refaktorisierung vor. Es werden die nötigen Grundlagen erläutert sowie die Eignung für die gegebene Aufgabenstellung begründet
und alternative Konzepte genannt.
3.1. Eine formale Darstellung für Programmtransformationen
Bevor sich der Frage gewidmet werden kann, wie Programmtransformationen unter bestimmten Randbedingungen berechnet werden können, ist zunächst eine geeignete Definition dessen zu liefern, was eine Programmtransformation eigentlich ist.
Sei Σ das Alphabet der möglichen Eingabezeichen, aus denen ein Programm bestehen kann
und Σ∗ die kleenesche Hülle über diesem Alphabet, also die Menge aller endlichen Zeichenke en, die sich mi els dieser Eingabezeichen bilden lässt. Auf unterster Ebene lässt sich eine
Programmtransformation dann beschreiben als eine Funktion TΣ∗ , welche eine Zeichenkette s ∈ Σ∗ eines Eingabeprogramms auf eine Zeichenke e eines Ausgabeprogramms s′ ∈ Σ∗
abbildet:⁵
TΣ∗ : Σ∗ → Σ∗
3.1.1. Transformationen von Syntaxbäumen
Auch wenn ein Refaktorisierungswerkzeug le tendlich immer eine solche Abbildung zwischen Zeichenke en implementieren muss, bietet sich zur internen Repräsentation eine komfortablere Darstellung an, welche ausnu t, dass eine Programmtransformation nicht beliebige
Zeichenke en aus Σ∗ transformieren soll, sondern nur Programme der Programmiersprache,
welche einer entsprechenden Grammatik gehorchen. So lässt sich ein Programmtext mi els
eines Parsers entsprechend der zugrundeliegenden Grammatik der Programmiersprache zunächst in einen konkreten und anschließend in einen abstrakten Syntaxbaum (engl. abstract syntax tree) [Wil ], auch abgekürzt als AST, transformieren.
parse : Σ∗ → AST
Abbildung . zeigt einen kurzen Programmausschni und einen zugehörigen abstrakten
Syntaxbaum. Umgekehrt zum Parsen des Eingabeprogramms liefert ein Tiefendurchlauf über
einen abstrakten Syntaxbaum und ein Berichten der Knotenwerte wieder das originale Programm, indem zu jedem Knoten der von ihm repräsentierte Quelltext ausgegeben wird:⁶
parse−1 : AST → Σ∗
⁵ Dass bestimmte Änderungen an Programmen auch Änderungen an Dateinamen und Verzeichnispfaden erfordern können (zum Beispiel ein Verschieben von Typen in Java), bleibe hier der Einfachheit halber unberücksichtigt.
⁶ Eine solche Umkehrfunktion kann nur funktionieren, soweit im abstrakten Syntaxbaum alle Informationen des
Ursprungsprogramms enthalten sind, was aber in der hier gezeigten Formalisierung nicht für überschüssige
. Constraintbasierte Refaktorisierungen
.
…
v1 :VarDeclStatement
v2 :ModifierList
'public'
public String s,t;
(i)
v3 :Type
v5 :VarDeclFragment
v7 :VarDeclFragment
v4 :SimpleName
'String'
v6 :SimpleName
's'
v8 :SimpleName
't'
(ii)
Abbildung . .: Eine Variablendeklaration (i) und ein zugehöriger abstrakter Syntaxbaum (ii).
Zusä liche Knoten für das Komma und Semikolon wurden beim Übergang
vom konkreten zum abstrakten Syntaxbaum bereits eliminiert.
Somit kann eine Programmtransformation auch als Graphtransformation [Hec ] TAST des
abstrakten Syntaxbaumes ausgedrückt werden,
TAST : AST → AST
wobei eine Hintereinanderausführung von Generierung des abstrakten Syntaxbaumes, Transformation des Graphen und Zurückschreiben des Quellcodes erneut einer Programmtransformation entspricht:
parse
T
parse−1
AST
−→ AST −−−−−→ Σ∗
Σ∗ −−−→ AST −−
Ein wesentlicher Vorteil dieser Transformationske e ist, dass schon allein durch die zwischenzeitliche Darstellung des Ausgabeprogramms als abstrakter Syntaxbaum die Menge der
möglichen Programmtransformationen auf genau solche eingeschränkt wird, die den syntaktischen Regeln der Programmiersprache genügen, solange auch der Baum eine gültige Form
wahrt und nicht etwa dort Kindknoten eingehangen werden, wo es die Grammatik der Programmiersprache verbietet. Weiterhin vereinfacht eine Baumdarstellung die Beschreibung
typischerweise von Refaktorisierungswerkzeugen durchgeführten Transformationen. So entspricht das Verschieben einer Deklaration dem Umhängen eines Teilbaums, das Einfügen von
Deklarationen dem Einfügen neuer Knoten sowie das Löschen von Ausdrücken dem Entfernen von Knoten aus dem Baum.
Tro dieser Vorteile gehen mit der Darstellung von Programmtransformationen als Graphtransformationen wesentliche Nachteile einher, die allesamt daher rühren, dass Grammatiken für Programmiersprachen zum Zweck des Compilerbaus entworfen werden und andere
Anforderungen erfüllen, als sie von Refaktorisierungswerkzeugen gestellt werden. Mit der
syntaktischen Analyse steht die Erstellung eines abstrakten Syntaxbaumes ganz am Anfang
Leerräume (engl. white spaces) oder Kommentare gilt. Während beides in dieser Arbeit auch nicht das Thema
sein soll, kann sich bei den Leerräumen dennoch mi els allgemeiner Formatierungsregeln beholfen werden,
wohingegen der sinnvolle Umgang mit Programmkommentaren Sonderbehandlungen erfordert, die für diese
Arbeit in eingeschränktem Maße schon durch den Mechanismus des ASTRewrite der JDT [JDT ] bereitgestellt
werden.
. . Eine formale Darstellung für Programmtransformationen
des Kompiliervorganges. Viele Programmeigenschaften, die für Refaktorisierungen von Belang sind, ergeben sich aber erst in späteren Kompilierschri en. So werden durch den JavaCompiler zum Beispiel default-Konstruktoren für Klassen angelegt, für die im abstrakten Syntaxbaum keine anderen Konstruktoren definiert sind, Zugrei arkeitsmodifizierer werden für
Interface-Member und Enum-Konstruktoren im Nachhinein eingefügt, wo sie nicht explizit
angegeben sind und Aufrufe von Superkonstruktoren werden nachgetragen, wo diese nicht
vorgegeben sind. In den genannten drei Fällen ließe sich ein Eingabeprogramm noch entsprechend zu einem angereicherten abstrakten Syntaxbaum transformieren, um die impliziten Elemente explizit zu machen. Schwieriger wird es bei Programmeigenschaften, die üblicherweise
von einem Compiler berechnet werden, die für Refaktorisierungswerkzeuge Relevanz haben
und für welche die Grammatik der Programmiersprache keine Knotentypen im abstrakten
Syntaxbaum vorsieht. Zu nennen wären hier zum Beispiel Bindungen von Referenzen an Deklarationen oder inferierte Typen von Ausdrücken. Da sich während einer Refaktorisierung
diese Eigenschaften ebenso wie das Eingabeprogramm ändern können, wäre es wünschenswert, beide in einer einheitlichen Datenstruktur vorzuhalten, auf der die Refaktorisierungswerkzeuge arbeiten können. Alternative Ansä e wären hierfür eine weitere Anreicherung
des abstrakten Syntaxbaumes teilweise hin zu Syntaxgraphen [MDJ , MvEDJ , Sch ]. Bei
constraintbasierten Refaktorisierungen wird hingegen von der Darstellung als Baum beziehungsweise Graph abgerückt und sta dessen das zu refaktorisierende Programm im Wesentlichen in Form von Variablen und ihnen zugewiesenen Werten dargestellt.
3.1.2. Transformationen als Variablenzuweisungen
Für ein zu refaktorisierendes Programm beschreibe
V = (v1 , v2 , . . . , vn )
die Variablen vi , der durch das jeweilige Refaktorisierungswerkzeug änderbaren Programmeigenschaften. Jede Variable vi habe dabei einen initialen Wert (engl. initial value) viinit , der dem
initialen Zustand der Variable vi vor der Transformation des Programms entspricht, sodass
sich zusä lich die Folge
(
)
V init = v1init , v2init , . . . , vninit
der initialen Werte ergibt. Die konkreten Berechnungsvorschriften, nach denen aus einem Programm P (beziehungsweise dessen abstraktem Syntaxgraph) die Elemente von V und V init
erzeugt werden können, richten sich jeweils danach, welche Programmeigenschaften durch
das spätere Refaktorisierungswerkzeug änderbar sein sollen, also was für ein Refaktorisierungswerkzeug konkret zu implementieren ist. Formal ähneln die Variablen stark den A ributen a ributierter Grammatiken [Knu ]. Mi els dieser werden Symbolen einer Grammatik
(also im Endeffekt den Knotentypen eines abstrakten Syntaxbaumes) Eigenschaften zugeordnet, die konkrete Werte annehmen können und die sich wiederum aus einer Menge von Produktionsregeln ergeben [Knu ]. Somit lassen sich für eine formale Darstellung der Variablengenerierung und ihrer initialen Werte a ributierte Grammatiken heranziehen. Die Unterschiede ergeben sich erst bei der späteren Verwendung. Während A ribute im Rahmen des Compilerbaus Verwendung finden, um das Programm mit (unveränderlichen) Teilergebnissen des
Kompiliervorganges anzureichern, bilden die Variablen während einer Refaktorisierung veränderliche Programmeigenschaften ab. Ändert sich der Wert einer Variable, entspricht dies
. Constraintbasierte Refaktorisierungen
einer lokalen Änderung des Programmtextes (oder analog einer Transformation des abstrakten Syntaxbaumes). Der Ort der Quellcodeänderung ergibt sich dann aus dem Knoten des
abstrakten Syntaxbaumes, dem die Variable zugeordnet ist.
Für eine n-elementige Folge von Variablen V sei eine Programmtransformation T nun definiert
als eine Menge von Zuweisungen
T = {vi := valT,i | vi ∈ V }
bei der jeder Programmeigenschaft vi des Eingabeprogramms genau ein Wert valT,i zugewiesen wird. In der Praxis nimmt ein Refaktorisierungswerkzeug zumeist nur Einfluss auf geringe Teile des Eingabeprogramms [MHPB ]. Es liegt also nahe, dass für die sich ergebenden
Programmtransformationen die Gleichung valT,i = viinit für viele i gilt. Im Folgenden werden solche Zuweisungen bei Angabe von Programmtransformationen der Einfachheit halber
ausgelassen:
{
}
T = vi := valT,i | vi ∈ V ∧ valT,i ̸= viinit
Die Darstellung von Programmtransformationen als Mengen von Zuweisungen ermöglicht,
Mengenoperationen direkt auf Programmtransformationen übertragen zu können. Ist eine
Transformation T die leere Menge, handelt es sich um eine Nicht-Transformation des Programmtextes, denn alle Zuweisungen an Variablenwerten entsprechen dem initialen Zustand
des Programms. Weiterhin lässt sich sagen, dass eine Transformation T in einer anderen Transformation T ′ enthalten ist, wenn T eine Teilmenge von T ′ ist. Ebenso können sich Transformationen überdecken sowie disjunkt oder widersprüchlich sein. Le teres wäre der Fall für
zwei Transformationen T und T ′ mit vx := valT,x ∈ T , vx := valT ′ ,x ∈ T ′ und valT,x ̸= valT ′ ,x .
Zuordnungen von Variablen an Programmelemente Um – gerade bei einer großen Anzahl von Variablen – eine sinnvolle Strukturierung zu schaffen, werden Variablen in zwei Dimensionen geordnet. Einerseits wird jede Variable einem konkreten Programmelement (engl.
program element) p ∈ P zugeordnet. Die Menge P der Programmelemente umfasst dabei alle im Programm enthaltenen Deklarationen und Referenzen, Ausdrücke und Anweisungen.
Damit geht die Menge P der Programmelemente noch über die reine Menge der Knoten im
abstrakten Syntaxbaum hinaus, denn nicht jedem Programmelement lässt sich eine konkrete
Quellcodeposition zuordnen, so können auch Programmelemente für implizite Deklarationen
wie default-Konstruktoren ( § . . ) existieren, die nur in einem angereicherten abstrakten
Syntaxbaum zu finden wären.
In zweiter Dimension werden Variablen verschiedenen Variablenarten v ∗ ∈ V ∗ zugeordnet.
So gibt es zum Beispiel Variablen, die einen Zugrei arkeitsmodifizierer, einen Namen oder
den deklarierten Typen einer Deklaration repräsentieren. Die Menge der Variablen v ∈ V
wird somit Partioniert durch das Kreuzprodukt der Mengen der Programmelemente und Variablenarten P × V ∗ .
Da jede Variable einem Programmelement zugeordnet ist, lässt sich ebenso eine umgekehrte Zuordnung finden, welche zu einem Programmelement die jeweiligen Variablen benennt.
Darüber hinaus lässt sich sogar eine Abbildung finden, die für ein konkretes Programmelement die Arten der für sie zu generierenden Variablen benennt:
applyingKinds : P → 2V
∗
. . Eine formale Darstellung für Programmtransformationen
∗
Diese bildet die Menge der Programmelemente P auf die Potenzmenge 2V der Variablenarten ab. In der Praxis ergeben sich die hierfür nötigen Abbildungsvorschriften aus der Art des
jeweils betrachteten Programmelements. So lässt sich bereits aus der Art eines Programmelements ableiten, welche Variablenarten für dieses infrage kommen. Aus der Tatsache, dass ein
Programmelement eine lokale Variable repräsentiert, ergibt sich beispielsweise, dass dieses
Variablen für Namen, umschließende Deklaration oder deklarierten Typen – nicht aber für
Zugrei arkeiten – hat.
Benennung von Variablen Wie gezeigt lässt sich also jede Variable v ∈ V mi els eines
Tupels (p, v ∗ ) ∈ (P × V ∗ ) und v ∗ ∈ applyingKinds(p) beschreiben. Frühe Arbeiten zu constraintbasierter Programmanalyse haben als Schreibweise für die Variablenarten v ∗ ∈ V ∗ verschiedenartige Klammerungen genu t, so zum Beispiel [[f ]] oder [f ] um den deklarierten
Typen [PS , TKB ] und ⟨f ⟩ um die Zugrei arkeit des Feldes f zu referenzieren [ST ].
Abgesehen von der unintuitiven Darstellung verbietet sich eine solche Notation spätestens
mit Einführung einer größeren Menge von Variablenarten, welche die Menge verfügbarer
Klammersymbole bald erschöpft. Andere Publikationen wählten eine Schreibweise mit einer
funktionsähnlichen Notation declaredT ype(f ) beziehungsweise accessibility(f ) [TB ] oder
einer an die Objektorientierung angelehnten Punktnotation, mit teilweise durch griechische
Buchstaben kodierten Variablenarten f.τ beziehungsweise f.α [SKP , SP a]. In dieser Arbeit findet le tgenannte Schreibweise Verwendung, wobei die Variablenarten ausgeschrieben
werden, zum Beispiel f.accessibility und f.declaredType. Dies geschieht, um Konsistenz zur Implementierung zu schaffen, deren zugrundeliegende Sprache – die Refacola [SKP ] – ebenso
eine ausgeschriebene Punktnotation benu t.
Domänen Der Wert, den eine Variable v ∈ V annehmen darf, ist nicht beliebig. Vielmehr
existiert zu jeder Variablenart v ∗ ∈ V ∗ eine zugehörige Domäne (engl. domain) Dv∗ , welche die
möglichen Werte für eine Variable v dieser Variablenart beschreibt. Es lassen sich dabei insgesamt drei Arten von Domänen unterscheiden: Aufzählungsdomänen, programmabhängige
Domänen und offene Domänen.
Aufzählungsdomänen (engl. enumeration domain) [SP a] haben einen endlichen Wertebereich
unabhängig vom konkreten Transformationsproblem. Ihre Wertebereiche ergeben sich zumeist aus der Sprachspezifikation der zugrundeliegenden Programmiersprache. In Java ist
zum Beispiel die Menge der Zugrei arkeiten eine Aufzählungsdomäne, nämlich genau die
Menge {public, protected, default, private}. Aufzählungsdomänen können geordnet sein, was
für die Zugrei arkeiten in Java zutrifft, denn in der gegebenen Auflistung der Zugrei arkeiten schließt der von jedem genannten Zugrei arkeitsmodifizierer erzeugte Zugrei arkeitsbereich den vom danach genannten Modifizierer erzeugten Zugrei arkeitsbereich ein. Ein
Beispiel für eine partiell geordnete Aufzählungsdomäne liefern die Zugrei arkeiten in C#,
bei denen zwar public-Zugrei arkeit alle weiteren Zugrei arkeitsbereiche einschließt, aber
weder die internal-Zugrei arkeit die protected-Zugrei arkeit umfasst, noch der umgekehrte Fall gilt [HWG , § . . ]. Beispiele für vollständig ungeordnete Domänen finden sich
zumeist in den zweielementigen Mengen, zum Beispiel der Domäne einer Variablen, welche
die An- oder Abwesenheit eines static-Modifizierers in Java repräsentiert. Zwar ist es technisch möglich, einen solchen Booleschen Wertebereich zu ordnen, es ergibt aber keinen Sinn,
. Constraintbasierte Refaktorisierungen
denn Ordnungen finden Anwendung, um später mi els Operatoren wie < oder ≤ vereinfacht Bedingungen auszudrücken, bei denen für Boolesche Domänen auch der Gleichheitsund Ungleichheitsoperator genügen.
Domänen, deren Wertebereich zwar endlich ist, sich aber erst aus dem konkreten Transformationsproblem ergibt, sind sogenannte programmabhängige Domänen (engl. program-dependent
domain) [SKP ] (beziehungsweise bei [Pal ] abstract domain). Als Beispiel sei eine Variablenart genannt, die den deklarierten Typen eines Feldes beschreibt. Die Werte, die eine solche
Variable annehmen kann, ergeben sich erst aus dem zu transformierenden Programm, denn
mit jedem weiteren deklarierten Typen des Programms steigt potentiell auch die Anzahl möglicher Typannotationen des Feldes.
Programmabhängige Domänen ermöglichen Variablenwerte, die ihrerseits eigene Variablen
definieren. Angenommen, für einen deklarierten Typen d sei eine Variable d.identifier definiert, die seinen Namen beschreibt. Weiterhin sei f ein Feld mit einer Variablen f.declaredType.
Dann bezeichnet f.declaredType.identifier die Variable, die den Namen des deklarierten Typen
beschreibt. Abhängig davon, welchen Wert f.declaredType zugewiesen bekommt, ändert sich
entsprechend die mi els dieser Indirektion (engl. indirection) [SKP ] beschriebene identifierVariable.
In die dri e Kategorie möglicher Domänen fallen die offenen Domänen. Ein typischer Vertreter ist die Menge gültiger Zeichenke en, welche als Namen von Deklarationen dienen dürfen.
Auch wenn in der Theorie unendlich viele mögliche Werte existieren, lassen sich diese Mengen in der Praxis häufig auf endliche Wertebereiche reduzieren. Im Falle der Namen würde
man für ein konkretes Transformationsproblem die Domäne einschränken auf alle derzeit im
Programm verwendeten Namen nebst einiger weniger Elemente, welche im Programm bisher
unbenu te, frische Namen repräsentieren. Notwendige Bedingung hierbei ist dann natürlich,
dass die Menge vom Refaktorisierungswerkzeug neu eingebrachter Namen gering ist, was in
der Praxis zutrifft [MHPB ].
Generierung von Programmelementen und Variablen Es lässt sich leicht einsehen, dass
zu einem gegebenen Eingabeprogramm und einer Menge von Variablenarten V ∗ Vorschriften
(zum Beispiel in Form a ributierter Grammatiken [Knu ]) zur Berechnung gefunden werden
können, die aus beidem eine Menge von Programmelementen P ∈ P und Folgen der Variablen
V ∈ V und ihrer initialen Werte V init ∈ Vinit erzeugen:
vargenV ∗ : AST → P × V × Vinit
In der Praxis wird bei dieser Variablengenerierung zunächst der abstrakte Syntaxbaum nach allen Programmelementen abgesucht, woraus eine Menge P resultiert. Anschließend wird für
jedes Programmelement p ∈ P anhand oben beschriebener Abbildung applyingKinds geprüft,
welche v ∗ ∈ V ∗ existieren, um aus diesen zu jedem derart ermi elten Tupel (p, v ∗ ) eine Variable v zu V hinzuzufügen. Die initialen Werte der Variablen ergeben sich dann direkt aus dem
Quellcode.
3.2. Programmtransformationen als Bedingungserfüllungsproblem
Der vorhergehende Abschni hat erläutert, wie Programmtransformationen mi els Variablen
formal dargestellt werden können. Dieser Abschni beschreibt den eigentlichen Kern con-
. . Programmtransformationen als Bedingungserfüllungsproblem
straintbasierter Refaktorisierungen, nämlich, wie Programmtransformationen berechnet werden können, die die Intention einer bestimmten Refaktorisierungsanwendung umse en und
dabei Invarianten zur Beibehaltung des Programmverhaltens erhalten.
Formal lässt sich (nach [SP ]) eine solche Programmtransformation T wie folgt als HoareTripel [Hoa ]
{Pre} T {Post}
mit Vorbedingung Pre und Nachbedingung Post beschreiben. Bezeichne Int die Intention des
Refaktorisierungswerkzeugs und Inv die zu erhaltenden Invarianten. Während die Intention
des Refaktorisierungswerkzeugs (im allgemeinen Fall) nur nach der Transformation erfüllt
ist, sollen die Invarianten sowohl vor als auch nach der Transformation gelten [SP ]:
{Inv} T {Inv ∧ Int}
Intention eines Refaktorisierungswerkzeugs Wie jedes Werkzeug erfüllen insbesondere
Refaktorisierungswerkzeuge einen konkreten Zweck. Dieser ergibt sich einerseits aus der Art
des Werkzeugs, also dem Muster, nach dem refaktorisiert werden soll, und andererseits aus
mitunter möglichen Konfigurationen, typischerweise als Benu ereingaben. Mit einer gegebenen Menge von Variablen V kann im einfachsten Fall die Intention einer Refaktorisierung
direkt als eine Menge von Variablenzuweisungen
Int = {vi := valT,i | vi ∈ V }
ausgedrückt werden. Für ein Refaktorisierungswerkzeug zum Umbenennen von Methoden
ergibt sich beispielsweise aus der Art des Werkzeugs, dass einer identifier-Variable ein neuer
Wert zugewiesen werden soll. Die konkrete Variable sowie der zuzuweisende Wert ergeben
sich aus der Konfiguration, für die der Benu er eine konkrete Methode sowie einen neuen
Namen für diese auswählt. Die le tendlich durchzuführende Programmtransformation – also
gültige Refaktorisierung – muss mindestens der Intention genügen, also eine Obermenge der
vorgegebenen Variablenzuweisungen sein.
Mitunter ist die Intention eines Refaktorisierungswerkzeugs aber auch auf einer semantisch
höheren Abstraktionsebene gegeben, was an einem (in diesem Abschni durchgängigen) Beispiel erläutert werden soll. Es sei folgendes Programm gegeben:
101
102
103
104
package a;
class A {
void m() { /* Methode m1 */ }
}
In diesem soll im Rahmen einer A S
-Refaktorisierung [Fow ] in einem neuen Paket
b eine Subklasse erzeugt werden, welche die Methode m überschreibt. Ein naiver Ansa würde den Programmtext der Klasse A kopieren, den Paketnamen und den Klassennamen anpassen und gegebenenfalls aus der Superklasse überschriebene Methoden mit einer @OverrideAnnotation kennzeichnen. Für das obige Beispiel käme es nach diesem Vorgehen aber zu nicht
kompilierendem Code:
105
106
107
package b;
class B extends a.A {
@Override
. Constraintbasierte Refaktorisierungen
108
109
}
void :::
m() { /* Methode m2 */ }
In Zeile
kommt es zu einem Kompilierfehler, weil die Methode m2 entgegen der angegebenen @Override-Annotation keine Methode der Superklassen überschreibt. Um innerhalb
eines fremden Pakets überschrieben werden zu können, müsste sowohl die Methode m in A,
als auch in der Folge dann die Methode m in B protected-zugrei ar sein. Die Intention bei der
gezeigten Refaktorisierung geht also über einzelne, lokale Änderungen hinaus. Sie beschreibt
einen komplexeren semantischen Zusammenhang, nämlich, dass die Methode m2 die Methode m1 überschreiben muss.
Schäfer [Sch ] fasst solche semantischen Informationen unter dem Begriff Abhängigkeiten
zusammen:
“The dependency framework allows us to abstract away from many of the gory details of
name binding or control and data flow, which are handled transparently.”
Er unterscheidet verschiedene Arten der Abhängigkeit wie Bindungen (name binding dependencies), Überschreibungen (overriding dependencies) und Kontroll- und Datenflussbeziehungen (flow dependencies).
Abhängigkeiten lassen sich als (meist zweistelliges) Tupel von Programmelementen ausdrücken. Sei Method ⊂ P die Teilmenge der Programmelemente, die in einem Java-Programm
Methoden repräsentieren. Damit lassen sich Überschreibungsbeziehungen zwischen Methoden beschreiben als ein zweistelliges Tupel von Programmelementen:
overrides ⊆ Method × Method
Analog lassen sich auch Bindungen zwischen deklarierten Entitäten Entity ⊂ P und Referenzen Reference ⊂ P als eine zweistelliges Tupel von Programmelementen beschreiben:
binds ⊆ Reference × Entity
In dieser Darstellung bei [SKP ] erstmalig verwendet heißen die Mengen dieser Tupel Programmfakten oder – in Anlehnung an das gleichnamige Konzept der logischen Programmierung [BG ] – einfach Fakten (engl. facts). Die einzelnen Elemente einer Faktenmenge werden
– in gleicher Anlehnung – durch ein Tupel mit vorangestellter Faktenmenge beschrieben, so
zum Beispiel binds(r, d) ∈ binds.
Um zum obigen Beispiel der A S
-Refaktorisierung zurückzukehren, ließe sich die
Intention der Werkzeuganwendung als
Int = overrides(m2 , m1 )
formulieren. Somit ergibt sich folgendes Hoare-Tripel:
{Inv} T {Inv ∧ overrides(m2 , m1 )}
Erfüllung der Invarianten Es bleibt zu klären, was die Invarianten Inv eines Refaktorisierungswerkzeugs konkret sind. In erster Linie sollte ein Refaktorisierungswerkzeug ein gültiges
. . Programmtransformationen als Bedingungserfüllungsproblem
(engl. well-formed) Programm erzeugen, also eines, welches den syntaktischen und semantischen Regeln der Programmiersprache genügt. Für ein zuvor fehlerfrei kompilierbares Programm muss gelten, dass es auch nach angewendeter Programmtransformation keine Kompilierfehler aufweist. Anderenfalls sollte es zumindest die Abhängigkeiten beibehalten, die
vor der Programmtransformation fehlerfrei aufgelöst werden konnten.
Ebenso wie die Intention eines Refaktorisierungswerkzeugs können auch die Invarianten
mi els Abhängigkeiten ausgedrückt werden. Das Beibehalten von Beziehungen wie Bindung,
Überschreibung, Kontroll- und Datenfluss ist wesentlicher Bestandteil bedeutungserhaltender Programmtransformation. Für das laufende Beispiel sei angenommen, dass Überschreibungsbeziehungen die einzigen relevanten Abhängigkeiten seien (tatsächlich greifen noch eine Menge weiterer Abhängigkeiten, Kapitel widmet sich diesen ausführlich). Es ergibt sich
dann folgendes Hoare-Tripel
{overrides = O} T {overrides = O ∪ {overrides (m2 , m1 )}}
in welchem overrides-Abhängigkeiten als Vor- und Nachbedingung auftreten. Aus deklarativer Sicht bedeuten dabei die overrides-Abhängigkeiten O in Vor- und Nachbedingung dasselbe, was sich auch in der identischen Notation niederschlägt. Operational ergibt sich hingegen
ein gewichtiger Unterschied: Während O in der Vorbedingung als eine Abfrage an das Programm beziehungsweise die daraus resultierende Faktenmenge zu verstehen ist, so ist die
overrides-Abhängigkeit in der Nachbedingung als eine zu erfüllende Zusicherung zu verstehen [SP ].
Programmabfragen Eine konkrete Umse ung der sich aus der Vorbedingung ergebenden
Programmabfragen ist unkompliziert. Unter welchen Bedingungen ein Fakt jeweils vorliegt,
ergibt sich aus der Sprachspezifikation des Eingabeprogramms. Für Java und die oben genannten Fakten binds und overrides sind dies konkret die statischen Binde- ( § ) und Überschreibungsregeln ( § . . . ) der Sprache. Programmabfragen erfordern also lediglich eine
Anpassung der Funktion vargen aus Abschni . . . Neben dem Eingabeprogramm und den
Variablenarten sei nun ebenso die Menge F ∗ von Faktenarten gegeben, die innerhalb der Vorbedingung eines Hoare-Tripels auftreten können. Zusä lich erhält man dann als Ausgabe
eine Menge F ∈ F von Programmfakten.
factgenV ∗ , F ∗ : AST → P × V × Vinit × F
Ein Quadrupel (P, V, V init , F ) nennen wir Faktenbasis (engl. fact base), der Prozess des Berechnens der Faktenbasis heißt Faktengenerierung (engl. fact generation). Eine konkrete Anfrage an
eine Faktenbasis, ob eine Menge von Programmelementen existiert, die eine bestimmte Menge von Fakten erfüllt, heißt Programmabfrage (engl. query). Diesen Abfragen wird dabei stets
die Annahme zur Weltabgeschlossenheit (engl. closed world assumption) zugrunde gelegt. Ist
ein Fakt in der Faktenbasis nicht enthalten, besteht auch eine entsprechende Abhängigkeit im
Programm nicht.
Zurückkehrend zum laufenden Beispiel würde eine Faktenbasis des Eingabeprogramms für
F ∗ = {binds} mangels jeglicher Überschreibungen keine binds-Fakten beinhalten. Die Menge
O wäre also die leere Menge.
{overrides = ∅} T {overrides = {overrides (m2 , m1 )}}
. Constraintbasierte Refaktorisierungen
Würde man ein erweitertes Beispiel mit zusä lichen Methodenüberschreibungen betrachten,
würde innerhalb der Nachbedingung die Menge der zu erfüllenden Fakten ansteigen. Für das
laufende Beispiel soll ein einzelnes Fakt aber genügen.
Faktenerfüllung Die Zusicherung einer bestimmten Faktenmenge innerhalb der Nachbedingung des Hoare-Tripels ist sozusagen das Gegenstück zur Faktengenerierung. Zu einem
gegebenen Fakt müssen die Bedingungen zugesichert werden, unter welchen das Fakt erfüllt
ist. Da diese Bedingungen le ten Endes in einer Programmtransformation Manifestation finden sollen, sollte dies in Form von Variablenzuweisungen geschehen. Eine Überschreibung
der Methoden m1 und m2 lässt sich dabei wie folgt ausdrücken:⁷
overrides(m2 , m1 ) ⇔ m1 .identifier = m2 .identifier
∧ m1 .parametercount = m2 .parametercount
∧ ∀i, 1 ≤ i ≤ m1 .parametercount : m1 .parameteri .type = m2 .parameteri .type
∧ m2 .hos ype ∈ m1 .hos ype.supertypes
∧ (m1 .accessiblity ≥ protected
∨ (m1 .accessiblity = default ∧ m1 .hostpackage = m2 .hostpackage)
Die Überschreibung der Methoden m1 und m2 zuzusichern ist notwendige Bedingung, um
die Nachbedingung des obigen Hoare-Tripels zu erfüllen. Allerdings ist dies noch nicht hinreichend, denn es wird nicht nur verlangt, dass
overrides (m2 , m1 ) ∈ overrides
sondern als Resultat der Invariante auch, dass keine weiteren Überschreibungen gelten (da
im Ausgangsprogramm keine weiteren Überschreibungen vorhanden waren):
overrides = {overrides (m2 , m1 )}
Entsprechend muss (wegen der Annahme zur Weltabgeschlossenheit) für alle weiteren Paare von Methoden mi , mj , i ̸= j gelten, dass diese nicht überschreiben, also die entsprechend
negierte Version obiger Variablenzuweisungen gegeben sind. Für jede weitere Methode im
Programm – das kann zum Beispiel die von Object geerbte Methode finalize() sein – muss innerhalb der Nachbedingung des Hoare-Tripels ebenso die negierte Version obigen Ausdrucks
gelten. Geht man – neben m1 und m2 – von nur einer weiteren solchen Methode m3 aus, ergibt
dies vier weitere Konjunktoren – jeweils um zu verhindern, dass m1 oder m2 die Methode m3
überschreiben oder umgekehrt m1 oder m2 von m3 überschrieben wird. Insgesamt ergibt sich
das folgende Hoare-Tripel, wobei zwecks kürzerer Darstellung bei den le tgenannten Prüfungen auf unerwünschte Methodenüberschreibung nur der erste der vier Fälle aufgeführt
ist.
⁷ Darüber, dass sich seit Java die Situation aufgrund möglicher Typparameter in Methodensignaturen sowie
Autoboxing tatsächlich noch ein wenig verkompliziert hat, sei an dieser Stelle (und bis Kapitel ) noch hinweggesehen. Dass hingegen in Java Methodenüberschreibung unabhängig von den Rückgabetypen der Methoden
definiert ist, entspricht der Java-Sprachspezifikation ( § . . . ).
. . Programmtransformationen als Bedingungserfüllungsproblem
{ } T { m1 .identifier = m2 .identifier
∧ m1 .parametercount = m2 .parametercount
∧ ∀i, 1 ≤ i ≤ m1 .parametercount : m1 .parameteri .type = m2 .parameteri .type
∧ m2 .hos ype ∈ m1 .hos ype.supertypes
∧ (m1 .accessiblity ≥ protected
∨ (m1 .accessiblity = default ∧ m1 .hostpackage = m2 .hostpackage)
∧ ( m1 .identifier ̸= m3 .identifier
∨ m1 .parametercount ̸= m3 .parametercount
∨ ∃i, 1 ≤ i ≤ m1 .parametercount : m1 .parameteri .type ̸= m3 .parameteri .type
∨ m3 .hos ype ∈
/ m1 .hos ype.supertypes
∨ ¬(m1 .accessiblity ≥ protected
∨ (m1 .accessiblity = default ∧ m1 .hostpackage = m3 .hostpackage))
...
}
Obwohl augenscheinlich das Ziel – die Anweisungen für eine konkrete Programmtransformation – mit der Darstellung der Nachbedingung als Variablenzuweisungen zum Greifen
nahe ist, verbleibt dennoch ein weiter Weg. Die Nachbedingungen beinhalten nämlich noch
keineswegs konkrete Handlungsanweisungen, sondern nur – wie der Name schon aussagt –
dabei zu berücksichtigende Bedingungen.
Bedingungserfüllung Glücklicherweise sind Systeme von Bedingungen, wie sie sich aus
den Nachbedingungen der Programmtransformation dargestellt durch Variablenzuweisungen ergeben, gut untersucht. So ist ein Bedingungserfüllungsproblem (engl. constraint satisfaction
problem) [Tsa ] oder kurz Constraintsystem oder CSP definiert als ein Tripel
⟨Z, D, C⟩
Darin ist Z eine Menge von Constraintvariablen (engl. constraint variable), D eine Funktion, welche die Elemente aus Z auf ihre jeweilige Domäne abbildet und C eine Menge von Bedingungen
(engl. Constraint). Eine Lösung (oder auch Constraintlösung) eines Bedingungserfüllungsproblems ist definiert als eine Menge von Variablenzuweisungen, die alle Constraints gleichzeitig erfüllt [Tsa ]. Im Allgemeinen können Bedingungserfüllungsprobleme abhängig von den
gegebenen Domänen und Constraints beliebig viele Lösungen haben. Existiert keine Lösung,
nennt man ein Bedingungserfüllungsproblem inkonsistent oder auch unlösbar.
Die Nachbedingungen, wie sie sich aus den Hoare-Tripeln ergeben haben, lassen sich damit
formulieren als ein Bedingungserfüllungsproblem
CSP = ⟨V, DV ∗ , R⟩
. Constraintbasierte Refaktorisierungen
wobei V der oben definierten Menge der Variablen entspricht,⁸ DV ∗ einer Funktion, die jeder Variablenart v ∗ ∈ V ∗ jeweils ihre Domäne Dv∗ zuordnet (siehe Abschni . . ) und R
die oben beschriebenen und zu erfüllenden Nachbedingungen der Programmtransformation
beschreibt.
Bedingungserfüllungsprobleme, deren Variablen ausschließlich endliche Domänen haben,
werden endliche Bedingungserfüllungsprobleme (engl. finite constraint satisfaction problem) [Tsa ]
genannt. Wie in Abschni . . auf Seite ausgeführt, sind die Aufzählungsdomänen ebenso
wie die programmabhängigen Domänen endlich und die offenen Domänen lassen sich ohne
funktionale Einschränkungen durch endliche Domänen erse en. V ∗ enthält also ausschließlich endliche Domänen. Insbesondere ist das erhaltene Bedingungserfüllungsproblem CSP
damit endlich.
In der Literatur wird eine Vielzahl von Verfahren beschrieben, um zu einem endlichen Bedingungserfüllungsproblem Aussagen zur Inkonsistenz oder Menge der Lösungen zu machen [Kum ]. Wendet man einen Constraintlöser (engl. constraint solver) auf CSP an, beinhaltet seine Ausgabe auch gleichzeitig eine Lösung, wie eine der Intention und den Invarianten
des Refaktorisierungswerkzeugs genügende Programmtransformation durchzuführen ist. Im
Falle eines inkonsistenten Constraintsystems kann die Transformation wegen sich widersprechender Invarianten und Intentionen nicht durchgeführt werden. Im Falle einer Lösung des
Constraintsystems besteht die Ausgabe des Constraintlösers in der Zuweisung von Werten
an Variablen, die alle Bedingungen erfüllen. Mit anderen Worten erhält man eine Menge von
von Variablenzuweisungen, die sowohl Obermenge der Invarianten als auch Obermenge der
Intentionen des Refaktorisierungswerkzeugs ist, also beiden genügt.
Für das laufende Beispiel der A S
-Refaktorisierung der zu überschreibenden Methode m1 ist das Constraintsystem, das sich aus der Nachbedingung des Hoare-Tripels ergibt,
lösbar. Mit einem Constraintlöser (oder auch scharfem Hinsehen) erkennt man, dass es mehrere Möglichkeiten gibt, für eine Erfüllung aller Bedingungen zu sorgen. Während die ersten
drei Bedingungen – Namensgleichheit beider Methoden, übereinstimmende Parametertypen
und Subtyprelation – schon bei der initialen Belegung der Variablen erfüllt sind, lässt sich
die dri e Bedingung erfüllen, indem wahlweise die Zugrei arkeit von m1 auf protected oder
public angehoben wird oder die beiden Klassen A und B in dasselbe Paket verschoben werden.
3.3. Constraintregeln
Der vorhergehende Abschni hat beschrieben, wie aus der Intention und den zu erhaltenden Invarianten eines Refaktorisierungswerkzeugs eine Menge von Bedingungen aufgestellt
werden kann, um anhand dieser mit einem Constraintlöser eine geeignete Programmtransformation zu berechnen. Neben einigen ausgewählten Bedingungen, die aus der Intention
einer Werkzeuganwendung resultieren und direkt in Form von Variablenzuweisungen gegeben werden können, muss der Großteil der zu erzeugenden Constraints die Beibehaltung von
Abhängigkeiten, jede davon ausgedrückt als Fakt f ∈ F , sicherstellen. Um diese Constraints
zu erstellen, werden Abbildungsvorschriften benötigt, die gemäß der jeweiligen Sprachspezifikation besagen, wie ein konkretes Fakt f zu erfüllen ist oder wie die Erfüllung eines in der
Faktenmenge nicht enthaltenen Fakts zu verhindern ist. Ein Beispiel für das Resultat einer sol⁸ daher werden im Folgenden die Begriffe Constraintvariable und Variable synonym genu t
. . Constraintregeln
chen Abbildungsvorschrift ist schon im vorhergehenden Kapitel . gegeben, wo auf Seite
gezeigt ist, wie die Variablen zweier Methoden m1 und m2 derart einzuschränken sind, um
eine Überschreibung zwischen diesen zu erreichen.
Die Abbildungsvorschriften, die aus einer gegebenen Faktenbasis Bedingungen ableiten,
heißen Constraintgenerierungsregeln (engl. constraint generation rule), oder auch kurz Constraintregeln. Wie jede Regel besteht auch eine Constraintregel aus zwei Teilen, der Prämisse sowie der
Konklusion. Die Prämisse nennt die Bedingungen, unter denen ein Constraint zu generieren
ist und entspricht einer Programmabfrage an die Faktenbasis. Die Konklusion zeigt ein Muster, nach dessen Vorbild das Constraint zu generieren ist. Wie für Constraintvariablen lassen
sich in der Literatur ebenso für Constraintregeln verschiedene Notationen finden (vergleiche
zum Beispiel [TKB ], [ST ] und [SP a]). Diese Arbeit verwendet eine Schluss- beziehungsweise Inferenzregelnotation, bei welcher die Prämisse getrennt durch einen Balken über der
Konklusion steht:
Prämisse
Konklusion
Eine einfache Regel für Java besagt, dass bei überschreibenden Methoden diese gleiche Namen
haben müssen:
overrides(m2 , m1 )
( )
m1 .identifier = m2 .identifier
Innerhalb dieser Regel sind m2 und m1 Regelvariablen (das sind keine Constraintvariablen!),
welche genau die geordneten Paare von Programmelementen repräsentieren, die in overrides
enthalten sind.
Es wird deutlich, dass die gegebene Notation für Constraintregeln eine wichtige Eigenschaft
unterschlägt, die nur deswegen entfallen kann, weil sie allen Constraintregeln gemein ist. Die
verwendeten Regelvariablen einer jeden Constraintregel sind allquantifiziert, was bedeutet,
dass eine Regel für alle Regelvariablenbelegungen Anwendung finden soll, die die Prämisse
erfüllen. Aus der oben gegebenen Regel ( ) ergeben sich also für n Methodenüberschreibungen insgesamt n Constraints. Es liegt auf der Hand, dass mit steigender Anzahl der Bedingungen in der Prämisse die Anzahl der daraus zu generierenden Constraints maximal gleich
bleibt, in den meisten Fällen aber sinken wird.
Eine zweite implizite Eigenschaft der Constraintgenerierungsregeln ergibt sich aus den Definitionen von Constraintsystemen und -lösungen. Demnach wirken alle erstellten Constraints
konjunktiv zusammen, es müssen für eine Constraintlösung also alle Constraints gemeinsam
erfüllt sein. Ein Nebeneffekt der Konjunktivität von Constraints ist, dass Constraintregeln, deren Konklusionen konjunktiv sind, in einzelne Regeln getrennt werden können. Umgekehrt
können Regeln mit identischer Prämisse jeweils zusammengefasst werden. So treffen neben
obiger Regel ( ) noch weitere Constraintregeln für überschreibende Methoden zu. Eine davon besagt, dass bei überschreibenden Methoden die überschreibende Methode mindestens
dieselbe Zugrei arkeitsstufe haben muss wie die überschriebene ( § . . . ):⁹
overrides(m2 , m1 )
( )
m2 .accessibility ≥ m1 .accessibility
⁹ Dies ist ein erstes Beispiel für eine Regel (beziehungsweise Constraintmenge), die nicht ein bestimmtes Programmverhalten zusichert, sondern die Gültigkeit des Programms sicherstellt.
. Constraintbasierte Refaktorisierungen
Wegen der zu Regel ( ) identischen Prämisse lassen sich beide Regeln äquivalent zu einer
gemeinsamen Regel zusammenfassen:
overrides(m2 , m1 )
( )+( )
(m1 .identifier = m2 .identifier) ∧ (m2 .accessibility ≥ m1 .accessibility)
Umgekehrt können die aus dieser Regel ( ) + ( ) generierten Constraints, die der Konklusion
entsprechend Konjunktionen sind, wahlweise in ihrer Gänze als Constraints des Constraintsystems verstanden werden, oder in ihre einzelnen Konjunkte zerfallen, die dann als einzelne
Constraints implizit durch ein und verbunden werden.
Bedingte Constraints Es verbietet sich, in der Prämisse einer Constraintregel und damit
einhergehend in einer verwendeten Faktenabfrage Annahmen über den Wert von Constraintvariablen zu machen [SP a]. Täte man dies und würde sich der Wert einer solchen Variablen
bei der Suche nach einer gültigen Constraintlösung ändern, könnte sich dadurch für eine konkrete Variablenbelegung eine zunächst unerfüllte Prämisse einer Constraintregel erst noch erfüllen. Die Folge wäre, dass entweder nötige Constraints fehlen oder nachträglich generiert
werden müssten. Im umgekehrten Fall könnte sich durch einen geänderten Variablenwert die
Prämisse einer Constraintregel als nichterfüllt entpuppen, sodass die Constraintmenge womöglich zu groß und damit unnötig restriktiv wird.
Dies soll an folgender Constraintregel erläutert werden, die besagt, dass in Java ein Topleveltyp ausschließlich public oder default-Zugrei arkeit haben darf ( § . ):
isType(t) isToplevel(t)
( )
t.accessibility = public ∨ t.accessibility = default
Diese Regel ist nur korrekt, soweit es keine Variable gibt, über deren Wertänderung beispielsweise ein Membertyp zu einem Topleveltypen transformiert werden kann. Denn gäbe es einen
solchen Membertypen tm , würde für ihn kein entsprechendes Constraint angelegt werden.
Würde tm aber nun im Rahmen der Programmtransformation zu einem Topleveltypen werden, fehlte im Constraintsystem ein entsprechendes Constraint, welches ihm einen privateoder protected-Modifizierer verbietet, der für ihn als Membertypen zulässig gewesen wäre.
Die Programmtransformation könnte somit ungültig werden. Es ist genau so aber auch der
umgekehrte Fall vorstellbar, in dem obige Regel zu restriktiv wäre, soweit ein Topleveltyp tt
tatsächlich in einen Membertypen gewandelt wird. In diesem Fall wäre die Einschränkung
von tt auf public- und default-Zugrei arkeit unnötig.
Diesem Problem kann begegnet werden, wenn vor der Programmtransformation schon klar
ist, ob tm nach der Transformation ein Toplevel- oder Membertyp sein wird. Neben dem Fall,
dass ein Typ niemals seine Toplevel-Eigenschaft ändern darf, trifft dies ebenso zu, wenn die
Intention eines Refaktorisierungswerkzeugs gerade in der Transformation eines Member- zu
einem Topleveltypen liegt. In diesem Fall müsste bei der Abfrage isToplevel in der Prämisse der Regel nicht nur der aktuelle Programmzustand berücksichtigt werden, sondern ebenso
im Rahmen einer vorausschauenden (engl. foresight) [ST ] Anwendung berücksichtigt werden,
welche Änderungen sich aus der Intention ergeben. Entspricht die Intention der Umwandlung eines Membertypen tm in einen Topleveltypen, muss für diesen die Abfrage isToplevel
unabhängig vom aktuellen Programmzustand zu Wahr auswerten.
. . Constraintregeln
Eine vorausschauende Auswertung der Regelprämisse verbietet jedoch, den Constraintlöser bei der Constraintlösung eigenständig die Toplevel-Eigenschaft vergeben zu lassen. Denn
gäbe es wiederum diese Möglichkeit, könnte sich die Auswertung der Prämisse durch die
Constraintlösung ändern – und erneut wäre die Constraintmenge entweder unvollständig
oder zu restriktiv.
Eine in [SP a] entwickelte generische Lösung für dieses Dilemma besteht darin, die veränderlichen Regelbestandteile von der Prämisse in die Konklusion zu ziehen, wie im Folgenden
mit einer abgeänderten Regel ( *) gezeigt:
isType(t)
( *)
t.isToplevel → (t.accessibility = public ∨ t.accessibility = default)
Die Konklusion hat nun eine zusä liche Wächterbedingung erhalten, die der entfallenen Abfrage innerhalb der Prämisse entspricht. Das Constraint ist damit zu einem bedingten Constraint
(engl. conditional constraint) [SKP ] geworden. In ihm wird über eine zusä liche Constraintvariable ausgedrückt, ob t ein Topleveltyp ist. Entsprechend ihrer Belegung findet die Konklusion der zugrundeliegenden Regel ( ) Anwendung.
Tro der offensichtlichen Vorteile bedingter Constraints muss die gesteigerte Mächtigkeit
von Refaktorisierungswerkzeugen, die man durch Herabziehen und Veränderlichmachen von
Regelbestandteilen erhält, gleich doppelt teuer erkauft werden. Einerseits sorgt das Entfallen
von Bedingungen innerhalb der Prämisse dafür, dass die Regel häufiger angewendet werden kann und somit insgesamt mehr Constraints entstehen. Vergleicht man die Regeln ( )
und ( *), so steigt die Anzahl der hinzukommenden Constraints um die Anzahl der Typen
im Programm, welche keine Topleveltypen sind. Unter der Annahme, dass in üblichen JavaProgrammen jeder dri e Typ ein Membertyp ist, stiege die Anzahl der Constraints um %.
Andererseits steigt auch der Aufwand bei der Auswertung eines jeden – nun bedingten –
Constraints. Nicht nur, dass nun eine zusä liche Variable referenziert wird, die Abhängigkeiten zu weiteren Constraints schafft, die Implikation sorgt für weitere, durch den Constraintlöser zu berücksichtigende Konfigurierbarkeit, das Constraintsystem wird zu einem dynamischen Bedingungserfüllungsproblem (engl. dynamic constraint satisfaction problem) [MF ].
Problematisch ist dabei der zusä liche oder-Operator, welcher bei der Suche nach einer gültigen Variablenbelegung zusä lichen Berechnungsaufwand bedeutet [CC , GGM ]. Während konjunktive Teilbedingungen mi els effizient durchführbarer Domänenreduktionen gemeinsam in die Suche nach einer Constraintlösung einfließen können, erzeugen disjunktive
Teilbedingungen Constraintsysteme, bei denen typischerweise zeitaufwändiges Backtracking
zum Einsa kommen muss [CC , GGM ]. Wegen der hohen Kosten bedingter Constraints
scheint es also geboten, diese nach Möglichkeit zu vermeiden. Wo abzusehen ist, dass ein
Transformationswerkzeug bestimmte Transformationen unter keinen Umständen ausführt,
sollten davon abhängige Bedingungen als Faktenabfragen innerhalb der Prämisse stehen, sta
sie über Variablen als Wächterbedingungen in Konklusion zu formulieren.
Für die Regelschreiber bringt es naturgemäß Nachteile mit sich, wenn die Formulierung
der Constraintregeln von den möglichen Transformationen des Refaktorisierungswerkzeugs
abhängig gemacht wird. So geht insbesondere der wesentliche Vorteil des constraintbasierten
Ansa es verloren, einmalig einen universellen Sa Constraintregeln für eine Programmiersprache entwickeln zu können, der für beliebige Transformationswerkzeuge Einsa finden
. Constraintbasierte Refaktorisierungen
kann. Steimann und von Pilgrim [SP a] liefern als Lösung hierzu die Technik des Regelumschreibens (engl. rule rewriting). Nach ihren Ideen ist der Ausgangspunkt der Constraintgenerierung nicht nur das zu transformierende Programm und der Regelsa , sondern auch eine
Filterfunktion, welche die Menge der Variablen auf genau die Variablen einschränkt, deren
Werte durch das Werkzeug tatsächlich Änderungen erfahren können sollen [SP a]. Sind die
unveränderlichen Variablen bekannt, können von ihnen abhängige Bedingungen automatisch
in die Prämisse übertragen werden, schließlich sind diese Bedingungen unabhängig von der
konkreten Variablenbelegung [SP a]. Insgesamt genügt es damit, einheitlich für alle Werkzeuge nur noch Regeln für bedingte Constraints anzugeben. Das Hochziehen konstant auswertbarer Bedingungen kann automatisiert für jedes Werkzeug individuell geschehen.
Während dieser Prozess formal gesehen einem fallweisen Umschreiben der Regel entspricht
(und daher seinen Namen zurecht trägt), findet die von Steimann und von Pilgrim gelieferte
technische Umse ung [SP a] in Form einer konstanten Auswertung (engl. constant evaluation)
bedingter Boolescher Ausdrücke sta . Um dies erneut an einem Beispiel deutlich zu machen:
Wie oben argumentiert genügt es, die allgemeinere Regel ( *) zu formulieren, aus welcher für
einen Typen A zunächst noch das bedingte Constraint generiert wird:
A.isT oplevel → (A.accessibility = public ∨ A.accessibility = default)
Ist nun bekannt, dass die Variable A.isToplevel unveränderlich ist, kann ihr Wert entsprechend
erse t werden. Im Falle, dass A ein Topleveltyp ist, ergibt sich
true → (A.accessibility = public ∨ A.accessibility = default)
wobei nach den Gese en der Booleschen Logik die Wächterbedingung entfallen kann:
A.accessibility = public ∨ A.accessibility = default
Es entsteht also genau das Constraint, welches Resultat der Anwendung von Regel ( ) gewesen wäre. Falls der Typ A hingegen ein Membertyp ist (und bleibt), so ergibt sich aus Regel
( *) das Constraint
false ⇒ (A.accessibility = public ∨ A.accessibility = default)
welches nach den Gese en der Booleschen Logik zu
true
ausgewertet werden kann. Insgesamt kann das Constraint also entfallen (da es stets gelöst ist),
was ebenso genau dem Zustand entspricht, als wenn Regel ( ) Anwendung gefunden hä e.
Nach dieser wäre schon die Prämisse fehlgeschlagen und eine Erzeugung des Constraints
wäre somit ebenso verhindert worden.
Vergleicht man die Idee der vorausschauenden Constraintregeln [ST ] (die le tenendes
ebenso die teuren, bedingten Constraints erspart) mit dem Regelumschreiben, fällt auf, dass
beide Ansä e ihr Optimierungspotential aus derselben Quelle schöpfen, nämlich dem vor
Constraintgenerierung bekannten Wissen über unveränderliche Variablenwerte. Der Unterschied liegt hingegen in der höheren Flexibilität, die mit dem Verfahren des Regelumschreibens gegen höhere Laufzeiten erkauft werden kann. Während für vorausschauende Constraintregeln ganze Arten von Constraintvariablen (zum Beispiel t.isToplevel für alle t) als
. . Constraintregeln
während der Constraintlösung unveränderlich angenommen werden, können bei der dem
Regelumschreiben folgenden konstanten Auswertung auch nur Teilmengen hiervon zwecks
Optimierung als unveränderlich gekennzeichnet werden. Ersteres ist dabei weit weniger nü lich. So mag man einem Werkzeug, das Deklarationen verschiebt, ungern pauschal verbieten,
Zugrei arkeiten anzupassen. Hingegen kann man die Menge der anzupassenden Zugreifbarkeiten durchaus sinnvoll auf solche der verschobenen Deklarationen reduzieren, was in
der Praxis nur einem Bruchteil der Gesamtmenge der Zugrei arkeitsvariablen entsprechen
sollte. Umgekehrt ist zu beachten, dass auch für das Regelumschreiben die vollständige Constraintmenge erzeugt werden muss und die Vereinfachung der Constraints erst beim Constraintlösen der Laufzeit zugute kommt.
Annahmen über den Wert konkreter Constraintvariablen innerhalb der Regelprämisse führen zu fehlerhaften Constraintsystemen. Der umgekehrte Fall einer unveränderlichen Abfrage
innerhalb der Konklusion ist hingegen problemlos. Constraints können nicht nur Beziehungen zwischen Variablen beinhalten, sondern auch Variablen mit Konstanten in Relation se ten, wie in Regel ( ) anhand der Konstanten public und default geschehen. Auch können die
Abfragen, wie sie in der Prämisse der Regel erscheinen, Teil der Konklusion sein. Als Beispiel
sei die folgende Regel gegeben:
isMethod(m) isReference(r) binds(r, m)
( )
m.accessibility = public ∨ subType(r.hostType, m.hostType) ∨ r.hostPackage = m.hostPackage
Sie besagt, dass bei einer Methodenreferenz r, die an eine Methode m bindet, entweder m als
public-zugrei ar deklariert sein muss, die Referenz r in einem Subtypen des deklarierenden
Typen von m liegen muss, oder Referenz und Methode beide im gleichen Paket liegen müssen.
Die Abfrage der Subtyp-Eingenschaft erfolgt hierbei in Form einer Faktenabfrage, die entweder zu wahr oder falsch auswertet abhängig davon, ob ein entsprechendes Fakt in F enthalten
ist.
Regeln ohne Prämisse: Axiome Mit den Mi eln des Regelumschreibens scheint eine möglichst bedingungslose Prämisse Idealzustand zu sein, bis hin zu Constraintregeln der Form
programElement(p1 ) programElement(p2 )
...
...
( )
die ihre Prämissen so abstrakt formulieren, dass für die verwendeten Regelvariablen beliebige
Programmelemente eingese t werden können. Es gibt mehrere Gründe, warum solche Regeln
in der Praxis nur selten auftreten.
Soweit ohnehin klar ist, dass absehbar kein Bedarf an bestimmter Variabilität entsteht, verschlechtert die dafür notwendige Einführung zusä licher Constraintvariablen die Lesbarkeit
der Regeln unnötig. So könnte man in Regel ( *) die Prämisse tatsächlich hin zu
programElement(t)
reduzieren, soweit man eine Constraintvariable p.isType einführt:
. Constraintbasierte Refaktorisierungen
programElement(t)
( **)
t.isType → (t.isToplevel → (t.accessibility = public ∨ t.accessibility = default))
Da es allerdings abwegig ist, (bedeutungserhaltend) in Java zum Beispiel eine Methode in
einen Typen umwandeln zu wollen, sollte diese Verallgemeinerung entfallen.
Weiterhin kann es vorkommen, dass sich Bedingungen der Prämisse nicht sinnvoll durch
Variablen ausdrücken lassen, beispielsweise, wenn Bezug zum Ausgangszustand des Programms vor der Programmtransformation genommen werden soll. Abschni . nannte mit
binds und overrides bereits zwei Abhängigkeiten, die bei bedeutungserhaltenden Programmtransformationen erhalten werden müssen. Um mit Constraints sicherzustellen, dass für eine
konkrete Variablenbelegung diese Abhängigkeiten weiterhin gegeben sind, muss dem Regelschreiber die Möglichkeit gegeben sein, sich auf den Zustand vor der Programmtransformation zu beziehen. Im einfachsten Fall könnte dies geschehen, indem man sich in den Regeln auf
die initiale Wertebelegung einer Variablen bezieht. [STST ] nennt ein Beispiel, in welchem
eine Deklaration, die als Teil der API des Programms vor der Programmtransformation public
deklariert war, auch nach der Transformation noch in der Programm-API enthalten sein soll.
Dies ließe sich unter Bezugnahme auf den initialen Wert wie folgt in eine bedingte Constraintregel (ohne Vorbedingungen) gießen, soweit eine entsprechende Funktion init zur Verfügung
steht:
programElement(t)
( )
init(t.accessibility) = public → t.accessibility = public
Umgekehrt sind initiale Werte aber unabhängig von der Belegung der Constraintvariablen. Sie
hängen nur von der Belegung der Regelvariablen ab, wie sie zum Zeitpunkt der Constraintgenerierung bekannt ist. So lässt sich in obiger Regel ( ) der Ausdruck init(t.accessibility) schon in
dem Moment zu wahr oder falsch auswerten, in dem klar ist, mit welchem Programmelement
t im jeweilig aus der Regel generierten Constraint belegt ist. Da in der konkreten Regel die
Abfrage des initialen Zustandes der Variable eine Wächterbedingung ist, deren Nichterfüllung eine Generierung des gesamten Constraints unnötig macht, wäre eine Auswertung zum
Zeitpunkt der Constraintgenerierung sogar wünschenswert. Le tendlich bietet sich also eine
Implementierung mi els einer eigenen Programmabfrage public eher an:
public(t)
( )
t.accessibility = public
Für den Regelschreiber ergeben sich also Situationen, in denen es tro Mechanismen zur
Regelumschreibung oder konstanten Auswertung durchaus Sinn ergibt, Bedingungen in der
Prämisse (sta der Konklusion) zu belassen.
3.4. Constraintgenerierung
Wie sich aus einer Menge von Constraintregeln und einer Faktenbasis die resultierende Constraintmenge berechnen lässt, hat der vorhergehende Abschni . bereits erläutert. Für jede
Regel wird die Prämisse als Abfrage gegen die Faktenmenge F ausgeführt [SKP ]. Dann
wird für jede so erhaltene Erse ung der Regelvariablen durch konkrete Programmelemente,
die die Prämisse erfüllt, gemäß der Konklusion ein Constraint erstellt [SKP ]. Abgesehen
. . Constraintgenerierung
von der Tatsache, dass die Programmabfragen effizient ausgeführt werden müssen¹⁰, ist dieses
Verfahren weitgehend einfach umse bar. Leider führt es so aber nicht zum Erfolg, was an der
großen Anzahl von Constraints liegt, die sich schon für sehr kleine Programme ergeben können und schnell zu untragbaren Laufzeiten führen [ST ]. Die Technik des Regelumschreibens
liefert zwar schon einen Ansa , die Constraintmenge zu reduzieren, allerdings ist ihr Erfolg
stark abhängig von der Menge an Variablen, deren Werte als unveränderlich gekennzeichnet
werden können. Daher bedarf es geeigneter Mi el, die Menge generierter Constraints weiter zu reduzieren. Wie dies geschehen kann, soll an folgendem Beispielprogramm erläutert
werden:
110
111
112
113
package a;
public class A {
public void m() { /*...*/ }
}
114
115
116
117
118
package b;
public class B extends a.A {
public void m() { /*...*/ }
}
119
120
121
122
123
package c;
public class C {
{ new a.A().m();}
}
Wendet man die in Abschni . gezeigten Constraintregeln ( ), ( ) und ( ) auf dieses Beispiel an, so ergeben sich die vier in Tablelle . gezeigten Constraints. Was sich nun zunu e gemacht werden kann, ist, dass nicht für jedes Transformationsproblem tatsächlich die vollständige Constraintmenge benötigt wird [SKP ]. Das wird deutlich, sobald man das Constraintsystem als einen bipartiten Constraintgraphen betrachtet, in dem die Knotenmenge aus den
Constraintvariablen und Constraints besteht und eine Kante genau dann eine Constraintvariable mit einem Constraint verbindet, wenn ein Constraint eine Constraintvariable beschränkt.
Für die Constraints aus Tabelle . ist in Abbildung . eine graphische Repräsentation gegeben.
Wie man sieht, zerfällt der Graph in drei nicht verbundene Zusammenhangskomponenten.
Zur Lösung eines Transformationsproblems genügt es nun, nur die Zusammenhangskomponenten zu betrachten (und zu generieren), die genau die Variablen beinhalten, die zur Codierung der Intention des Transformationsproblems nötig sind [SKP ]. Anderenfalls – also
wenn die Anpassung einer Variablen einer anderen Zusammenhangskomponenten nötig wäre – müsste es ein Constraint geben, welches diese benötigte Anpassung ausdrückt, was im
Widerspruch dazu steht, dass es keinen Pfad zwischen den einzelnen Zusammenhangskomponenten gibt. Man kann sich bei der Constraintgenerierung also zunu e machen, dass für
die Lösung eines konkreten Transformationsproblems in Abhängigkeit von diesem jeweils
nur ein Teil an Constraints tatsächlich benötigt wird.
¹⁰ Gegebenenfalls können die aus dem abstrakten Syntaxbaum extrahierten Fakten in eine Datenbank übertragen
werden, um die Abfragezeiten zu minimieren [SKP ].
. Constraintbasierte Refaktorisierungen
Tabelle . .: Die vollständige Constraintmenge für das Programmbeispiel von Seite
riert gemäß den Constraintregeln ( ), ( ) und ( )
Nr.
Regel
( )
( )
( )
( )
, gene-
Constraint
b.B.m().accessibility ≥ a.A.m().accessibility
a.A.accessibility = public ∨ a.A.accessibility = default
b.B.accessibility = public ∨ b.B.accessibility = default
a.A.m().accessibility = public
∨ subType(new A().m().hostType, a.A.m().hostType)
∨ new A().m().hostPackage = a.A.m().hostPackage
Faktisch wird der Fall, dass ein Constraintgraph in mehrere Zusammenhangskomponenten
zerfällt, nur selten eintreten. Auch wenn die Idee nahe liegt, dass es insbesondere in größeren Softwareprojekten einzelne Programmteile gibt, die weitgehend voneinander unabhängig
sind und daher der Constraintgraph in einzelne, nicht verbundene Komponenten zerfällt, gibt
es dennoch Regeln, die Bedingungen für den Fall abprüfen, dass Programmteile beispielsweise zwischen diesen Komponenten verschoben werden.
Erst wenn man bestimmte solcher Transformationen ausschließt, lassen sich kleinere Zusammenhangskomponenten erreichen. In der Praxis erfolgt so ein Ausschluss von Programmtransformationen, indem eine Variable als unveränderlich¹¹ gekennzeichnet wird, also ihre Domäne auf ihren initialen Wert eingeschränkt wird. Nimmt man an, dass die Methode a.A.m()
in obigem Beispiel aus einer unveränderlichen Programmbibliothek stammt, kann der Knoten a.A.m().accessibility entfallen, da diese Variable im Constraint nun durch eine Konstante
– nämlich den initialen Wert von a.A.m().accessibility – erse t werden kann (die sogenannte
frühzeitige Auswertung (engl. early evaluation) [SP a]). Das Constraint aus Regel ( ) braucht
somit nicht mehr generiert zu werden. Ebenso entfällt die Generierung weiterer Constraints,
die sich aus durch Constraint referenzierte Variablen ergeben könnten. Würden also weitere Regeln existieren, die die Variable new A().m().hostPackage bedingen würden, könnte deren
Behandlung ebenso entfallen.
Steimann et al. [SKP , Figure ] und parallel dazu Müller [Mül ] haben einen auf dieser
Idee au auenden Algorithmus zur Erzeugung solch minimierter Constraintsysteme in Abhängigkeit bestimmter Intentionen entwickelt. Das Constraintne wird dabei beginnend mit
den die Intention codierenden Variablen aufgebaut. In Form einer Breitensuche wird dann
der Constraintgraph vervollständigt, indem für jede erzeugte Variable die Constraints angelegt werden, die diese Variable bedingen, und für jedes generierte Constraint die Variablen
angelegt werden, die dieses Constraint referenziert [SKP , Mül ]. Ist eine Variable als unveränderlich gekennzeichnet, werden von dieser ausgehend keine weiteren Constraints angelegt.
Im Allgemeinen sind zwei Möglichkeiten denkbar, warum eine Variable als unveränderlich zu kennzeichnen ist. Einerseits kann dies schon bei der Faktengenerierung unabhängig
¹¹ Sta des etwas widersprüchlich anmutenden Begriffs einer unveränderlichen Variable kann man ebenso auch
von einer Erse ung der Variable durch eine Konstante sprechen [SKP ]. Da – wie später klar wird – eine
solche Erse ung aber häufig nur in Abhängigkeit von einem konkreten Transformationsproblem geschieht,
wird mitunter auch bei diesen Konstanten weiterhin der Begriff Variable genu t.
. . Constraintlösung
.
a.A.m().accessibility
b.B.accessibility
b.B.m().accessibility
a.A.m().accessibility
new A().m().hostType
a.A.m().hostType
new A().m().hostPackage
a.A.m().hostPackage
Abbildung . .: Darstellung der Constraints aus Tabelle . als (bipartiter) Constraintgraph.
Die Variablen sind durch abgerundete Rechtecke und die Constraints durch
ihre eingekreisten Nummern gemäß Tabelle . illustriert.
vom jeweiligen Transformationsproblem geschehen, was typischerweise bei unveränderlichem Code wie Bibliotheksfunktionen der Fall ist. Umgekehrt ist es denkbar, für einzelne
Werkzeuge die Menge der erlaubten Änderungen durch explizite Angabe der nur erlaubten
Änderungen (engl. allowed changes) [SKP ] zu beschränken. So kann zum Beispiel für eine
R
-Refaktorisierung festgelegt werden, dass diese keine Zugrei arkeiten ändern darf,
was in der Folge alle accessibility-Variablen als unveränderlich kennzeichnet.
3.5. Constraintlösung
Wie in Abschni . gezeigt, kann bei der constraintbasierten Programmtransformation das
Berechnen der durchzuführenden Änderungen auf ein generisches Bedingungserfüllungsproblem reduziert werden. Auch wenn damit die Frage der Constraintlösung formal gelöst
erscheint, gilt es dennoch, gewisse technische Aspekte zu berücksichtigen.
Übersetzung in Integer-Constraints Im Mi elpunkt der meisten Forschungsbestrebungen
zu Constraintsystemen stehen solche Systeme und Lösungen, deren Variablen als Domänen
ausschließlich endliche Mengen von Integer-Werten haben [Tsa ], was sich auch in den Implementierungen frei verfügbarer Constraintlöser (wie zum Beispiel Cream [Nao ] oder Choco [JRL ]) widerspiegelt. Entsprechend müssen Abbildungen für die in Abschni . . genannten Domänenarten gefunden werden, die jedes Element einer Domäne einem IntegerWert zuordnen. Da es sich – wie in Abschni . . beschrieben – ausschließlich um endliche
Domänen handelt, genügt eine einfache Durchnummerierung der Domänenelemente.
Neben den Variablenwerten selbst sind aber ebenso noch Überse ungen der in den Constraints verwendeten Operatoren notwendig, die ein äquivalentes Verhalten der Integer-Constraints erreichen. Einfache Prüfungen, wie die auf Gleichheit und Ungleichheit von Variablenwerten, können in Integer-Constraints ganz analog angewendet werden. Relationen wie
≥ und ≤ zwischen Werten aus vollständig geordneten Aufzählungsdomänen gelten ebenso
. Constraintbasierte Refaktorisierungen
analog, soweit die Nummerierung der Elemente anhand ihrer Ordnung erfolgt. Schwieriger
wird es bei komplexeren Booleschen Ausdrücken, wie sie sich zum Beispiel aus den oben gezeigten Regeln ( **) und ( ) ergeben können. Bietet der verwendete Constraintlöser keine eigene Domäne für Boolesche Ausdrücke, müssen diese ebenso in Integer-Constraints transformiert werden. Soweit der Constraintlöser eine gewisse Grundmenge an Integer-Operationen
bietet (zum Beispiel min- und max-Funktionen und einen Absolutbetrag), können Boolesche
Operationen mit diesen simuliert werden [Nao ]. Indirektion (siehe dazu Abschni . . )
lässt sich auf die in vielen Constraintlösern verfügbaren Element-Constraints [vHSD ] abbilden [SKP ].
Auswahl einer Constraintlösung Ist ein Bedingungserfüllungsproblem lösbar, besteht für
dieses die Ausgabe eines Constraintlösers aus der Menge der Lösungen [Tsa ]. Zwar stellt –
soweit die Menge der Constraintregeln vollständig ist – jede Lösung des Constraintsystems
eine gültige Programmtransformation dar, die gleichzeitig der Intention der Werkzeuganwendung gerecht wird, dennoch ist einem Benu er zumeist nur an einer einzigen Lösung gelegen.
Welche Lösung dies genau ist, lässt sich nicht mit Sicherheit beantworten und hängt mitunter von den Vorlieben des Benu ers ab. Soll zum Beispiel eine Methode m in eine Subklasse
verschoben werden, wobei m die einzige Verwenderin einer weiteren privaten Methode n innerhalb derselben Klasse ist, so gibt es zwei Alternativen. Entweder die Methode n wird mit m
mit verschoben oder die Methode n wird durch ausreichende Anhebung ihrer Zugrei arkeit
dem Klasseninterface der umschließenden Klasse hinzugefügt und somit auch in der Subklasse verfügbar. Forschungen, wie hier eine sinnvolle Heuristik zur Entscheidungsfindung
aussehen kann, stehen aus.
In jedem Fall aber zu vermeiden sind unnötige Änderungen. Insbesondere wenn zwei Programmtransformationen zur Auswahl stehen, von denen eine die andere enthält, so ist aller
Voraussicht nach die Lösung mit der geringeren Anzahl von Änderungen zu bevorzugen. Stehen zwei Transformationen zur Auswahl, die der gleichen Menge von Variablen neue Werte
zuweisen, ist in der Regel diejenige zu wählen, deren Änderungen weniger gewichtig ausfallen. So ist eine Änderung der Zugrei arkeit von private zu default vermutlich eher zu bevorzugen als eine Änderung von private zu public, auch wenn beide Änderungen eine gültige
Lösung darstellen.
Die Reihenfolge, in welcher Constraintlöser Lösungen berichten, hängt üblicherweise von
internen Details ab und ist kein verlässlicher Indikator. Auch Constraintlöser, die ausgehend
von einem bestehenden, beinahe gelösten Constraintsystem – wie es bei constraintbasierten
Programmtransformationen durch die initialen Werte vorliegt – in einer Nachbarschaftssuche
(engl. neighborhood search) nach einer Lösung suchen (zum Beispiel [Nao ]), berichten nicht
notwendigerweise die Constraintlösungen mit minimaler Änderungsanzahl zuerst.
Eine Lösung hierfür besteht darin, neben den eigentlichen Constraints noch eine weitere Bedingung zuzulassen, über die konkrete Präferenzen ausgedrückt werden können [VKDL ].
Dieser Idee folgend schlagen Steimann et al. [SKP ] für constraintbasierte Refaktorisierungen sogenannte penalties vor, Strafwerte, die sich durch die (gegebenenfalls gewichtete) Summe der Änderungen ergeben und welche während der Constraintlösung minimiert werden
sollen. Problematisch sind die dabei entstehenden Laufzeiten, da zur Ermi lung einer minimalen Lösung im schlimmsten Fall der gesamte Suchraum abgelaufen werden muss [SKP ].
. . Refacola
Glücklicherweise hat sich das Problem, eine minimale Lösung finden zu müssen, in der Praxis als wenig relevant herausgestellt. Durch die in Abschi . beschriebenen Minimierungen
des Constraintsystems entfallen schon ein Großteil für die Constraintlösung nicht benötigter Variablen, somit auch die Möglichkeit, deren Belegung unnötigerweise zu ändern. Die
Verwendung einer Nachbarschaftssuche reduziert (neben den Laufzeiten) weiter das Risiko
unnötiger Änderungen.
Weitere Fälle unnötiger Änderungen können durch ein mehrstufiges Constraintlösen verhindert werden. So können bei einer P
U -Refaktorisierung beispielsweise zunächst anhand eingeschränkter erlaubter Änderungen [SKP ] weitere Änderungen neben der Intention ausgeschlossen werden. Erst wenn für dieses Constraintsystem keine Lösung gefunden
wird, werden zunächst Änderungen beispielsweise von Zugrei arkeiten und dann das Verschieben weiterer Deklarationen gesta et. Für die in dieser Arbeit entwickelten Werkzeuge
liefert dieses Vorgehen eine gute Balance zwischen Laufzeit und Menge unnötiger Änderungen, denn wie sich in der Evaluation in Abschni . zeigen wird, sind insbesondere für Refaktorisierungen mit wenigen erlaubten Änderungen die Laufzeiten sehr kurz.
Offen hingegen ist das Problem, wie mit einem nicht lösbaren Constraintsystem umgegangen wird. Es wäre wünschenswert, in diesem Fall dem Benu er eine Fehlermeldung zu präsentieren, aus welchen Gründen die angestrebte Programmtransformation abgelehnt werden
muss. Umgekehrt lässt sich im allgemeinen Fall für ein Constraintsystem nicht ein einzelnes
Constraint ausmachen, welches die Inkonsistenz verursacht. Häufig sind mehrere Constraints
gemeinschaftlich beteiligt, deren Erfüllung sich gegenseitig ausschließt. Eine Erklärungskomponente (zum Beispiel [Jun ]) könnte hier Abhilfe schaffen. Wie und ob diese einen Benu er
aber sinnvoll durch einen Constraintlöseprozess führt und ob Softwareentwickler sich in der
Praxis auf einen mehrschri igen Dialog einlassen würden, ist ein offenes Problem.
3.6. Refacola
Die Refacola (Refactoring Constriant Language) [SKP , SP a] ist eine domänenspezifische Sprache sowie ein Framework, welches ursprünglich zur Entwicklung constraintbasierter Refaktorisierungswerkzeuge gedacht war. Beginnend
wurde die Refacola hauptsächlich durch
Jens von Pilgrim implementiert, gewisse Anteile wurden im Rahmen der vorliegenden Arbeit
als Erweiterungen beigesteuert. Da für die Refacola bisher keine Dokumentation existiert, die
sämtliche Funktionen beschreibt, auf welchen die vorliegende Arbeit au aut, schließt dieses
Kapitel diese Lücke.
3.6.1. Sprachdefinitionen
Die Entwicklung eines Refaktorisierungswerkzeugs mit der Refacola erfordert die Erstellung
mindestens dreier Refacola-Dateien: Eine Sprachdefinition, eine Auflistung von Constraintregeln sowie eine Spezifikation der Refaktorisierung mit Angabe der Art der Intention und
erlaubten Änderungen.
Eine Refacola-Sprachdefinition beschreibt die Mengen P der Programmelemente, V ∗ der
Variablenarten mit ihren Domänen Dv∗ und F ∗ der Faktenarten. Aufgebaut ist eine Sprachdefinition nach dem Schema
124
language Java
. Constraintbasierte Refaktorisierungen
125
126
127
128
129
130
131
132
kinds
...
properties
...
domains
...
queries
...
In Zeile
ist nach dem Schlüsselwort language der Name der beschriebenen Sprache zu
nennen (hier Java), um auf diese aus den Constraintregeln heraus zugreifen zu können.
Kinds Nach dem Schlüsselwort kinds folgt eine Auflistung der Arten von Programmelementen (sogenannten kinds) sowie der ihnen zugehörigen Variablenarten (ihren sogenannten properties). Folgender Ausschni definiert drei Arten von Programmelementen, nämlich Typdeklarationen (TypeDeclaration), Methodendeklarationen (MethodDeclaration) und Methodenreferenzen (MethodReference):
133
134
135
136
kinds
TypeDeclaration <: ENTITY { identifier, accessibility }
MethodDeclaration <: ENTITY { identifier, accessibility, parameterTypes, hostType }
MethodReference <: ENTITY { identifier, argumentTypes }
Variablenarten für eine Art von Programmelement werden deklariert, indem der jeweiligen
Deklarationen eine Liste der entsprechenden Variablenarten v ∗ ∈ V ∗ in geschweiften Klammern angefügt wird. Die in Zeile
deklarierte Programmelementart MethodDeclaration
hat beispielsweise insgesamt vier Constraintvariablen, je eine für den Namen der Methode
(identifier), ihre Zugrei arkeit (accessibility), die Typen ihrer Methodenparameter (parameterTypes) sowie ihren umschließenden Typen (hostType).
Die Arten von Programmelementen sind durch eine Unterart-Relation (engl. subkind) geordnet, wobei der Operator <: die Unterartbeziehung spezifiziert.¹² An der Spi e der Hierarchie
stehen die beiden vordefinierten Arten ENTITY und REFERENCE.¹³ Der Haup weck von Unterarten in der Refacola ergibt sich aus den vielfach zur Verfügung gestellten Mechanismen, anhand von Programmelementarten aus der Menge von Programmelementen Teilmengen auszuwählen. So gibt es in Constraintregeln die Möglichkeit, Regelvariablen auf bestimmte Arten
zu begrenzen. Wenn also zum Beispiel Constraintregeln häufiger sowohl für Methoden als
auch Konstruktoren gelten sollen, kann es Sinn ergeben, beim Hinzufügen von Konstruktoren für diese eine gemeinsame Überart MethodOrConstructor zu erstellen, von welchem dann
ebenso auch Methodendeklarationen ableiten (die Refacola erlaubt Mehrfachvererbung).
Weiterhin findet parallel zur Unterart-Hierarchie auch eine Vererbung der für diese deklarierten Variablenarten sta . So sind für jede Programmelementart stets auch die von seiner
Überart definierten Variablenarten verfügbar. Eng im Zusammenhang damit steht die Möglichkeit, über das Schlüsselwort abstract solche Programmelemente zu kennzeichnen, die im
¹² Die eigentlich viel passenderen Begriffe Typ für Art und Subtyp für Unterart werden bewusst nicht verwendet,
um Doppeldeutigkeiten in Verbindung mit den durch Programmelemente repräsentierte (Java-)Typen und
deren Subtypen zu vermeiden.
¹³ Ob eine Programmelementart von REFERENCE oder ENTITY ableitet, ist in der Refacola semantisch unerheblich.
Die Unterscheidung kann lediglich der Dokumentation des Codes dienen.
. . Refacola
Eingabeprogramm keine direkte Entsprechung haben, sondern bei denen lediglich die Unterarten instanziiert werden. Entsprechend lassen sich die Zeilen
bis
äquivalent ausdrücken als:
137
138
139
140
abstract NamedEntity <: ENTITY { identifier }
TypeDeclaration <: NamedEntity { accessibility }
MethodDeclaration <: NamedEntity { accessibility, parameterTypes, hostType }
MethodReference <: NamedEntity { argumentTypes }
Nun definiert keines der ursprünglichen Programmelemente mehr eine Variable identifier, dafür erbt jedes einzelne die Variable identifier der neu hinzugekommenen Programmelementart
NamedEntity.
Properties Nach dem Schlüsselwort properties muss jeder in den kinds-Definitionen verwendeten Variablenart eine Domäne zugeordnet werden:
141
142
143
144
145
146
properties
identifier: Identifier
parameterTypes: Sequence(Types)
argumentTypes: Sequence(Types)
hostType: Types
accessibility: AccessModifiers
Die Refacola hat vordefinierte Domänen Identifier für beliebige Zeichenke en und Boolean für
Boolesche Werte, die anhand gleichnamiger Schlüsselworte verwendet werden können. (Geordnete) Listen von Variablen gleichen Typs (im Beispiel parameterTypes und argumentTypes)
werden als Sequenzen über ein entsprechendes Schlüsselwort unter Angabe der Domäne der
Listenelemente definiert. Alle weiteren Domänen müssen jeweils durch den Programmierer
vorgegeben werden, wobei unterschiedliche Variablenarten dieselben Domänen nu en dürfen. Zum Beispiel verwenden sowohl hostType als auch parameterTypes die Domäne Types,
wobei sie im le teren Fall als Elemen yp einer Sequenz zum Einsa kommt.
Domains Jede nicht-vordefinierte und im properties-Abschni verwendete Domäne muss
innerhalb des Abschni es domains definiert werden. Neben den vordefinierten Domänen erlaubt die Refacola zwei weitere Arten von Domänen: Aufzählungsdomänen und programmabhängige Domänen, wie aus dem laufenden Beispiel hervorgeht:
147
148
149
domains
AccessModifiers = {private, package, protected, public}
Types = [TypeDeclaration]
Ein Beispiel für eine Aufzählungsdomäne liefert AccessModifiers. Aufzählungsdomänen werden durch eine in geschweifte Klammern gefasste Auflistung aller in der Domäne enthaltenen
Elemente definiert. Die Refacola interpretiert Aufzählungsdomänen stets als vollständig geordnet und erlaubt dementsprechend innerhalb von Constraintregeln Größer- und KleinerVergleiche zwischen ihren Elementen. Programmabhängige Domänen werden durch die Angabe des Elemen yps in eckigen Klammern definiert. Die Domäne Types entspricht somit der
Menge aller Programmelemente vom Typ TypeDeclaration sowie allen Programmelementen,
deren Typ von TypeDeclaration ableitet.
. Constraintbasierte Refaktorisierungen
Facts Der le te Abschni einer Refacola-Sprachdefinition beschreibt die Menge F ∗ der Faktenarten. Jede Faktenart wird über den Namen des Fakts, gefolgt von einer in runde Klammern gefassten Liste der Arten, definiert, dessen Kreuzprodukt ein solches Fakt entspringt. In
folgendem Beispiel sind mehrere Faktenarten definiert, so ist ein binds-Fact zum Beispiel ein
geordnetes Paar bestehend aus einer MethodDeclaration und einer MethodReference:
150
151
152
153
queries
binds(r: MethodReference, d: MethodDeclaration)
overrides(m1: MethodDeclaration, m2: MethodDeclaration)
subType(t1: TypeDeclaration, t2: TypeDeclaration)
Neben den Arten innerhalb des Tupels ist jeder Tupelposition noch ein Name zugeordnet (im
Beispiel r, d, m1, …). Innerhalb der Refacola-Definitionen ist dieser Name irrelevant und kann
lediglich der zusä lichen Dokumentation dienen.
3.6.2. Constraintregeln
Als weiteres Element können mit der Refacola Constraintregeln ausgedrückt werden, welche
ausgehend von den in der Sprachdefinition festgelegten Fakten die dort ebenso definierten
Variablen in ihren Wertebereichen einschränken. Eine Datei mit Regeldefinitionen hat in der
Refacola folgenden Au au:
154
155
156
157
158
import "Java.language.refacola"
ruleset JavaRules
language Java
rules
...
Unter import sind (ggf. mit mehreren import-Anweisungen) die Dateien zu nennen, auf denen
die Regeldefinition au aut. Hier ist zumindest eine Datei mit einer Refacola-Sprachdefinition
zu nennen. Da die Refacola auch sprachübergreifende Refaktorisierungen unterstü t, können
auch mehrere Sprachen eingebunden werden. Unter language sind noch einmal die verwendeten Sprachen anhand ihrer Namen zu nennen (welche sich nicht notwendigerweise mit den
in den Imports verwendeten Dateinamen decken müssen). Hinter dem Schlüsselwort ruleset
ist dem folgenden Regelsa ein Name zu geben. Constraintregeln können in mehrere Regelsä e (und damit Refacola-Dateien) aufgeteilt werden, was sinnvoll ist, um Constraintregeln
für unterschiedliche Belange (Typen, Zugrei arkeiten, Namen, …) voneinander zu trennen.
Dem Schlüsselwort rules folgt eine Auflistung der eigentlichen Constraintregeln. Eine Constraintregel ist aus vier Elementen aufgebaut: Einem Regelnamen, der Deklaration der Regelvariablen, der Regelprämisse und der Regelkonklusion, wobei folgende Syntax Anwendung
findet.
159
160
161
162
163
164
165
166
<Regelname>
for all
<Deklaration der Regelvariablen>
do
if
<Prämisse>
then
<Konklusion>
. . Refacola
167
end
Der Regelname einer Regel kann frei gewählt werden, wobei (auch über die Grenzen eines
Regelsa es hinaus) jeder Name nur einmal vergeben werden darf. Als Beispiel soll im Folgenden die in Abschni . gezeigte Regel ( ) mi els Refacola beschrieben werden, welche
zusichert, dass bei Methodenüberschreibung die Methodennamen identisch sind.
Deklaration von Regelvariablen Jede innerhalb einer Constraintregel verwendete Regelvariable muss im for all-Teil der Regel durch Angabe eines Namens definiert werden. Weiterhin
ist die Angabe einer Programmelementart erforderlich, anhand derer die Refacola die Menge
der Programmelemente, die von der Regelvariablen angenommen werden kann, einschränkt.
Regel ( ) verwendet zwei Regelvariablen, m1 und m2 , welche jeweils als MethodDeclaration
deklariert sind:
168
169
170
for all
m1: Java.MethodDeclaration
m2: Java.MethodDeclaration
Die Prämisse Die Prämisse einer Regel gibt an, unter welchen Bedingungen ein Constraint
zu generieren ist. Wie in Abschni . beschrieben besteht eine Regelprämisse aus Faktenabfragen. Innerhalb der Refacola sind diese mit vorangestelltem Namen der Sprache zu qualifizieren (denn mehrere zusammen verwendete Sprachen könnten gleichnamige Fakten definieren). Im Falle von Regel ( ) verlangt die Prämisse eine Methodenüberschreibung zwischen
m1 und m2 :
171
Java.overrides(m2, m1)
Neben Faktenabfragen sind noch weitere Bedingungen innerhalb der Prämisse zulässig. So
können Variablen auf Gleichheit (=) und Ungleichheit (!=) getestet werden. Die einzelnen Bedingungen sind dabei jeweils durch Kommata zu trennen und werden durch die Refacola als
Konjunktion (verundet) ausgewertet. Andere logische Operatoren werden nicht unterstü t,
eine Disjunktion (Veroderung) zum Beispiel kann aber mi els mehrere Regeln mit identischer
Konklusion emuliert werden.
Regelvariablen, die innerhalb einer Constraintregel definiert werden, aber durch die Prämisse in ihren Wertebereichen nicht eingeschränkt werden, heißen ungebunden. Durch die Allquantifizierung der Regelvariablen (siehe Abschni . ) vervielfacht jede ungebundene Variable die Menge der zu generierenden Constraints um einen Faktor entsprechend der Mächtigkeit ihrer Domäne. Da beim Regelschreiben ein häufiger Fehler darin besteht, versehentlich
nicht mehr benötigte Variablen innerhalb der Regelvariablendeklaration zu belassen, werden
ungebundene Regelvariablen seitens der Refacola stets mit einer Fehlermeldung versehen. Ist
doch einmal eine ungebundene Variable erwünscht, so kann die Fehlermeldung mi els einer
einstelligen pseudo-Query all unterdrückt werden.
Wie in Abschni . auf Seite
erklärt, führt die Verwendung von Constraintvariablen
innerhalb einer Regelprämisse zu fehlerhaften Ergebnissen. Daher verbietet die Refacola eine
solche und gibt hierfür eine entsprechende Fehlermeldung aus.
. Constraintbasierte Refaktorisierungen
Die Konklusion Die Konklusion einer Regel enthält das Muster der zu generierenden Constraints, in welchem die Regelvariablen bei Anwendung der Regel durch konkrete Programmelemente erse t werden. Im einfachsten Falle werden zwei Constraintvariablen zueinander
in Bezug gese t, im Falle von Regel ( ) die Namen der beiden Methoden m1 und m2 :
172
m1.identifier = m2.identifier
Weitere zulässige Operatoren sind der Ungleichheitsoperator != sowie für Vergleiche zwischen Elementen von Aufzählungsdomänen die Vergleichsoperatoren >=, >, <= und <.
Um bedingte Constraints zu generieren, können über den Implikationsoperator −> entsprechende Wächterbedingungen vorangestellt werden. Boolesche Ausdrücke lassen sich dabei
mi els der Operatoren and und or kombinieren und mi els not negieren. Ebenso ist es möglich, in der Konklusion Faktenabfragen zu verwenden, die dann ebenso zu Booleschen Werten
evaluieren in Abhängigkeit davon, ob das entsprechende Fakt in der Faktenbasis enthalten ist.
Wie Abschni . ausgeführt hat, wirken alle Constraints eines Constraintsystems implizit konjunktiv zusammen, insbesondere lassen sich daher Constraintregeln mit identischer
Prämisse (und im Falle der Refacola auch identischer Regelvariablendeklaration) zusammenfassen oder aufteilen. Sollen Regeln derart zusammengefasst werden, so werden die Generierungsmuster innerhalb der Konklusion durch Kommata getrennt. Folgender Codeausschni
zeigt die Constraintregel ( )+( ) von Seite
formuliert in Refacola:
173
174
175
176
177
178
179
180
181
182
183
overriding
for all
m1: Java.MethodDeclaration
m2: Java.MethodDeclaration
do
if
Java.overrides(m1, m2)
then
m1.identifier = m2.identifier,
m2.accessibility >= m1.accessibility
end
Queries Die Refacola erlaubt Faktenabfragen sowohl innerhalb der Prämisse als auch der
Konklusion. In beiden Fällen können Faktenabfragen über zusä liche Suffixe modifiziert werden. Ein dem Faktennamen hinten angestelltes Sternchen macht die Abfrage reflexiv, lässt also
eine Faktenabfrage, bei welcher alle übergebenen Regelvariablen dasselbe Programmelement
referenzieren, immer zu true auswerten. Für das Fakt subType, welches ausdrückt, dass ein
Typ ein Subtyp eines anderen Typen ist, würde eine Abfrage subType*(t1,t2) nicht nur zu
wahr auswerten, wenn t1 Subtyp von t2 ist, sondern auch, wenn t1 und t2 identisch sind.
Weiterhin kann einem Faktennamen auch ein Ausrufezeichen hinten angestellt werden, um
die Faktenabfrage zu negieren. Während sich das damit erreichte Verhalten in der Regel mit
dem des ebenso verfügbaren not-Operators deckt, spielt der !-Suffix nur bei Verwendung von
Sequenzen eine eigene Rolle.
Sequenzen Als Datenstruktur für Mengen von Constraintvariablen bietet die Refacola lediglich Sequenzen, also geordnete Mengen von Constraintvariablen gleichen Typs. Die Refacola bietet weder die Möglichkeit, gezielt auf einzelne Listenelemente zuzugreifen, noch die
. . Refacola
Liste nach konkreten Eigenschaften wie zum Beispiel der Listenlänge zu befragen. Sta dessen werden die Sequenzen innerhalb von Regelprämissen verwendet wie Variablen, wobei für
jedes Element der Liste ein Teilconstraint generiert wird, in dem die vorkommende Sequenz
durch jeweils dieses Listenelement erse t wird. Die Teilconstraints werden dann konjunktiv verbunden. Angenommen, es wäre eine Programmelementart NullType definiert, die allein
den null-Typen in Java umfasst, dann würde folgende Constraintregel ausdrücken, dass kein
Methodenparameter mit dem Typen null deklariert werden darf:
184
185
186
187
188
189
190
191
192
193
parameterTypesNotNull
for all
m: Java.MethodDeclaration
null: Java.NullType
do
if
all(m), all(null)
then
m.parameterTypes != null
end
Wird innerhalb der Konklusion eine Sequenz nicht mit einer einzelnen Constraintvariablen,
sondern mit einer weiteren Sequenz in Relation gese t, so werden die Elemente der Sequenzen jeweils paarweise miteinander in Relation gese t und Konjunktionen in Anzahl entsprechend der Listenlängen generiert. Haben beide Sequenzen unterschiedliche Länge, evaluiert
der die beiden Sequenzen umfassende Ausdruck pauschal zu false. Folgende Regel besagt,
dass bei Methodenaufrufen die Argumen ypen identisch oder Subtypen der Parametertypen
sein müssen:
194
195
196
197
198
199
200
201
202
203
methodBinding
for all
r: Java.MethodReference
d: Java.MethodDeclaration
do
if
Java.binds(r, d)
then
Java.subType*(r.argumentTypes,d.parameterTypes)
end
Soweit die Listen der Argumente und Parameter gleiche Länge haben, werden entsprechend
der Listenlänge Constraints generiert, in denen paarweise gefordert wird, dass jeder Argumen yp jeweils Subtyp des zugehörigen Parametertypen ist. Haben die Listen unterschiedliche Längen, erzeugt die Regel für die jeweilige Regelvariablenbelegung nur den Ausdruck
false. Das ist auch sinnvoll, schließlich würde bei unterschiedlich langer Parameter- und Argumentliste der Methodenaufruf nicht mehr an die Methode binden.
Für manche Regeln kann es gewünscht sein, Abfragen für Sequenzen zu negieren, wobei
der not-Operator nicht immer zum gewünschten Ergebnis führt. So liefert eine Konklusion
204
not Java.subType(m1.parameterTypes,m2.parameterTypes)
. Constraintbasierte Refaktorisierungen
bei gleichlangen Parameterlisten für m1 und m2 ein Constraint
(
)
¬( subType m1 .parameterType1 , m2 .parameterType1
(
)
∧ subType m1 .parameterType2 , m2 .parameterType2
∧ . . .)
Ist hingegen eine Negation jeder einzelnen Konjunktion vorgesehen
(
)
¬subType m1 .parameterType1 , m2 .parameterType1
(
)
∧ ¬subType m1 .parameterType2 , m2 .parameterType2
∧ ¬...
kann innerhalb der Refacola einer Query ein Ausrufezeichen hinten angestellt werden:
205
Java.subType!(m1.parameterTypes,m2.parameterTypes)
Dabei ließe sich ein !-Suffix auch mit dem *-Suffix kombinieren.
Kommentare Die Refacola erlaubt Kommentare, wie sie auch in Java definiert sind. Möglich sind entweder Blockkommentare (/*... */) oder durch zwei Schrägstriche // eingeleitete
einzeilige Kommentare jeweils bis zum Zeilenende.
3.6.3. Refaktorisierungsdefinition
Den dri en benötigten Teil, um mit der Refacola ein Refaktorisierungswerkzeug zu implementieren, bildet die Definition des Werkzeugs selbst. Nachfolgend ist ein Beispiel gegeben:
206
207
import "Java.language.refacola"
import "Java.rules.refacola"
208
209
210
211
212
213
214
215
216
refactoring RenameMethod
languages Java
uses JavaRules
forced changes
identifier of Java.MethodDeclaration as NewName
allowed changes
identifier of Java.MethodDeclaration {initial, NewName @forced}
identifier of Java.MethodReference {initial, NewName @forced}
Wie schon bei den Definitionen des Regelsa es gesehen, sind zunächst die zu importierenden
Refacola-Dateien zu nennen, darunter mindestens eine Sprachdefinition und ein Regelsa .
Nach dem Schlüsselwort refactoring ist dem zu definierenden Werkzeug ein Name zu geben,
den Schlüsselwörtern languages und uses folgend jeweils die Menge von Sprachdefinitionen
und Regelsä en, die für das Refaktorisierungswerkzeug Anwendung finden sollen.
Nach dem Schlüsselwort forced changes ist zu benennen, durch welche Variablenarten die
Intention der Refaktorisierung ausgedrückt werden kann. Im gegebenen Beispiel – einem
R
M
– soll die Intention aus dem Ändern des identifier einer MethodDeclaration
bestehen. Soweit die Intention einer Werkzeuganwendung Zuweisungen an mehrere Variablen nötig macht, können in weiteren Zeilen weitere forced changes angegeben werden.
. . Refacola
Abbildung . .: Schematische Abbildung eines mit dem Refacola-Framework entwickelten
Refaktorisierungswerkzeugs
Dem Schlüsselwort allowed changes folgend sind die Arten der erlaubten Änderungen (siehe Abschni . ) der Werkzeuganwendungen zu nennen. Außer dem forced change wird das
aus dieser Spezifikation generierte Werkzeug nur solche Variablen ändern, deren Variablenarten in den allowed changes gelistet sind. Für das Beispiel des R
M
sollen sowohl Methodenreferenzen als auch weitere Methodendeklarationen variabel sein. Le teres
ist nötig, damit auch überschriebene und überschreibende Methoden mit umbenannt werden
können.
Neben der Möglichkeit, bei den erlaubten Änderungen Einschränkungen anhand von Variablenarten zu machen, erlaubt die Refacola ebenso auch konkret die von diesen Variablen angenommenen Wertebereiche einzuschränken. Dazu kann der Deklaration eines allowed change
in geschweiften Klammern eine Liste von bestimmten, erlaubten Wertebereichen nachgestellt
werden. Das Schlüsselwort initial gibt an, dass eine Variable v ihren initialen Wert init(v) annehmen darf. Über das Schlüsselwort @forced kann der Menge erlaubter Werte ein in den
forced changes verwendeter Wert hinzugefügt werden, der dort über das Schlüsselwort as mit
einem Namen versehen wurde. In obigem Beispiel dürfen also Methodenreferenzen und Methoden ihren Namen nur entweder beibehalten oder genau zu dem Wert ändern, der dem
neuen Namen der in der Intention genannten Methode entspricht.
3.6.4. Das Refacola-Framework
Die Entwicklung einer Sprachdefinition, eines Regelsa es und einer Refaktorisierungsdefinition, wie in den vorhergehenden drei Abschni en beschrieben, bilden für sich genommen
noch kein ausführbares Werkzeug. Erst durch Verarbeitung mi els des Refacola-Compilers
entsteht gemeinsam mit weiteren, von der Refacola zur Verfügung gestellten Bibliotheken ein
ausführbares Werkzeug. Abbildung . gibt eine Übersicht über die gesamte Architektur.
Sprachdefinitionen, Regelsä e und Refaktorisierungsdefinitionen werden durch den Refa-
. Constraintbasierte Refaktorisierungen
cola-Compiler eingelesen. Ausgabe sind dann aus Java-Code bestehende Komponenten: einerseits die sogenannte Sprach-API und andererseits die dann tatsächlich ausführbaren, constraintbasierten Werkzeuge. Die Sprach-API wird aus der Sprachdefinition und den Regelsätzen erzeugt und bildet die einzelnen Programmelemente, Variablenarten, Constraintregeln
und Faktenarten auf konkrete Java-Klassen ab. Die konkreten Werkzeuge werden dann aus
den Vorgaben der Refaktorisierungsdefinitionen generiert. Über entsprechende Methoden,
deren Signaturen von der Sprach-API Gebrauch machen, kann einem Werkzeug die Intention der Werkzeuganwendung mitgeteilt werden. Ausgabe eines Werkzeugs ist eine ebenso
anhand der Sprach-API beschriebene Menge von geänderten Variablenbelegungen (soweit
das Constraintsystem lösbar ist). Intern bedient sich das generierte Werkzeug verschiedener,
mit der Refacola ausgelieferter Bibliotheken (in Abbildung . als interne API bezeichnet), die
insbesondere den Algorithmus zur Constraintgenerierung implementieren, ebenso aber auch
abstraktes Verhalten von Elementen der Sprach-API vorgeben, welches somit nicht jedes Mal
aufs Neue zu generieren ist. Weiterhin bedienen sich die generierten Werkzeuge eines generischen Constraintlösers, in der aktuellen Version der Refacola ist dies der Constraintlöser
Choco . . [JRL ].
Da die Refacola sprachunabhängig ist, bietet sie von sich aus keine Komponente, die eine Überse ung der konkreten Syntax einer Programmiersprache in Programmelemente und
Fakten anbietet. Daher sind durch den Werkzeugentwickler sowohl eine Abfrage-Engine, die
die Auswertung von Faktenabfragen gegen den Code erlaubt, als auch eine Rückschreibekomponente, die entsprechend einer Menge von Variablenzuweisungen die entsprechenden Änderungen im Code durchführt, selbst zu entwickeln. Beide bauen dabei ganz wesentlich auf der
Sprach-API auf. Diese bietet ihrerseits zum Beispiel Fabrikklassen an, um neue Programmelemente zu erzeugen und ihre Variablen mit initialen Werten zu belegen. Über entsprechende
Methoden können Variablen eines Programmelementes ebenso als unveränderlich gekennzeichnet werden (zum Beispiel, falls es sich um eine importierte Bibliotheksdeklaration handelt). Weiterhin bietet die Sprach-API Funktionen, um beim Zurückschreiben für eine gegebene Constraintlösung im Vergleich zu initialen Variablenbelegungen geänderte Variablenwerte
zu erkennen.
Die folgenden vier Kapitel beschreiben diejenigen Komponenten näher, die über das Refacola-Framework hinausgehend zu implementieren waren, um eine Suite von constraintbasierten Refaktorisierungswerkzeugen für Java zu entwickeln. Kapitel geht auf die entwickelte
Sprachdefinition für Java ein, Kapitel und erläutern die erstellten Regelsä e. Kapitel
geht dann auf die einzelnen Refaktorisierungsdefinitionen sowie die Implementierung der
Abfrage-Engine und Rückschreibekomponente ein.
4. Eine Sprachdefinition für Java in Refacola
Dieses Kapitel beschreibt die Sprachdefinition für Java in Refacola. Gemeinsam mit den folgenden Kapiteln und liefert es eine formale Beschreibung dessen, wie sich Programmverhalten in Abhängigkeit von einer bestimmten Menge möglicher Codeänderungen – den
Domänen der Variablen – beibehalten lässt. Diese Sprachdefinition bildet im Zusammenwirken mit den Constraintregeln die gemeinsame Grundlage für die im Rahmen dieser Arbeit
entwickelten Refaktorisierungswerkzeuge und ist auch darüber hinaus für weitere Programmierwerkzeuge einse bar, deren mögliche Codeänderungen sich mit Hilfe der gegebenen
Variablen ausdrücken lassen (Abschni . wird dies im Rahmen eines Ausblicks für QuickFix-Werkzeuge behandeln).
Wie in Abschni . . beschrieben, definiert eine Refacola-Sprachdefinition die Mengen der
Programmelemente P , die ihnen zugeordneten Arten von Constraintvariablen V ∗ , ihre Domänen Dv∗ sowie die Faktenarten F ∗ .
4.1. Angebotene Variablen
Das Ziel dieser Arbeit ist, eine Sprachdefinition und einen zugehörigen Sa an Constraintregeln zu liefern, der Refaktorisierungswerkzeugen das korrekte Refaktorisieren von Deklarationen gesta et. Betrachtet man die in Katalogen wie dem von Fowler [Fow ] beschriebenen
oder in integrierten Entwicklungsumgebungen wie Eclipse [ECL ] und IntelliJ [IJI ] implementierten Refaktorisierungen, lässt sich aus diesen ableiten, für welche Arten von Codeänderungen – also Variablen mit zugehörigen Domänen – primär Bedarf besteht.
Generell lassen sich die durch die beschriebenen oder implementierten Werkzeuge ausgeführten Änderungen in zwei Kategorien aufteilen. Einerseits gibt es Änderungen, die sich
auf Deklarationsebene abspielen, andererseits gibt es Änderungen, die auf Anweisungs- oder
Ausdrucksebene sta finden. In erste Kategorie fallen Änderungen an Deklarationen wie Typen, Methoden und deren Parametern sowie Feldern und Variablen¹⁴. Änderungen, die diese
betreffen, umfassen typischerweise das Einfügen, Verschieben und Löschen dieser Deklarationen ebenso wie das Ändern bestimmter Eigenschaften, beispielsweise Namen. Änderungen auf Anweisungs- oder Ausdrucksebene umfassen hingegen Änderungen innerhalb von
Blöcken oder zwischen Blöcken, indem einzelne Ausdrücke oder Anweisungen hinzugefügt,
entfernt, geändert, zusammengefasst oder verschoben werden. Viele Werkzeuge führen sowohl Änderungen auf Deklarations- als auch auf Anweisungs- und Ausdrucksebene durch.
Eine M
T -Refaktorisierung beispielsweise führt auf Deklarationsebene zunächst eine
Verschiebung durch, bevor auf Ausdrucksebene gegebenenfalls Zugriffe auf den verschobenen Typen (neu) qualifiziert werden müssen.
¹⁴ Auch wenn die Deklaration einer Variable in Java streng genommen ein Ausdruck ist, zähle sie hier zu den
Deklarationen.
. Eine Sprachdefinition für Java in Refacola
Nur ein verschwindend geringer Anteil an Werkzeugen spielt sich ausschließlich auf Ausdrucks- und Anweisungsebene ab. Eine dieser seltenen Ausnahmen ist C
D
C
F
[Fow ], bei welchem eine Anweisung, welche in beiden Fällen
einer Fallunterscheidung enthalten ist, aus dieser herausgelöst wird.
Ausgangspunkt für die Wahl der Variablen in der in dieser Arbeit entwickelten Sprachdefinition sind daher zunächst Deklarationen und ihre typischerweise durch Refaktorisierungswerkzeuge geänderten Eigenschaften. Welche Variablen dann in Folge für weitere erlaubte
Änderungen auf Ausdrucks- und Anweisungsebene nötig werden, ergibt sich aus diesen. Die
folgenden Abschni e beschreiben die für Deklarationen typischerweise Änderungen unterliegenden Eigenschaften: Namen, Zugrei arkeiten, Orte und Typen.
Namen Variable Namen ermöglichen – wo nötig – die Bezeichner von Deklarationen wie
Klassen, Feldern und Methoden, aber auch den benannten Referenzen auf diese, anzupassen. Häufigstes Anwendungsbeispiel ist zweifelsohne eine R
-Refaktorisierung, bei der
eine Deklaration umbenannt wird und im Rahmen der Constraintlösung entschieden wird,
ob und welche Referenzen im Programm mit umbenannt werden müssen und dürfen. Aber
auch im Kontext anderer Refaktorisierungen können variable Namen nü lich sein. In Java
existiert die Regel, dass es pro Überse ungseinheit nur genau einen, als public-zugrei ar deklarierten Topleveltypen geben darf ( § . ). Für diesen Typen muss die umschließende
Überse ungseinheit gleich diesem primären Typen¹⁵ benannt sein. Ändert nun ein Werkzeug
die Zugrei arkeit eines nicht-primären Typen und macht diesen damit zu einem primären
Typen (zum Beispiel, damit dieser für ein C
D
T
verfügbar wird), kann dies
zu einem Kompilierfehler führen, wenn der Name der umschließenden Überse ungseinheit
dann nicht dem des primären Typen entspricht.
Weitere Fälle, in denen variable Namen von Bedeutung sind, ergeben sich aus Verschieberefaktorisierungen. Wird im Kontext einer P
U F
-Refaktorisierung beispielsweise ein
Feld in eine Superklasse gezogen, in welcher bereits ein gleichnamiges, privates Feld vorhanden ist, kann bei variablen Namen eines der beiden Felder durch den Constraintlöser gleich
mit umbenannt werden.¹⁶ Die Alternative wäre (wie es auch derzeit in Eclipse, NetBeans und
IntelliJ der Fall ist), dass das Refaktorisierungswerkzeug die Refaktorisierung mit einer entsprechenden Fehlermeldung abbricht. Dem Benu er bleibt dann nichts anderes übrig, als
selbst zunächst ein entsprechendes R
anzustoßen. Abgesehen davon, dass der Benu er
bei diesem Vorgehen unnötigerweise die Verschieberefaktorisierung zweimal anstoßen und
konfigurieren muss, kann es auch passieren, dass er nach der Umbenennung beim erneuten
Ausführen des P
U feststellt, dass nun die Refaktorisierung aus anderen Gründen scheitert (man nehme an, das zu verschiebende Feld hat noch einen Initializer, der ein Feld aus der
¹⁵ Die Java-Sprachspezifikation gibt diesem einzigen public-zugrei aren Topleveltypen einer Überse ungseinheit keinen speziellen Namen, zumal auch das Zielsystem gewisse Freiheiten hat, wie die Namen der Übersetzungseinheiten zu wählen sind ( § . ). Innerhalb dieser Arbeit wird daher die vom JDT [JDT ] verwendete,
durchaus treffende Bezeichnung primärer Typ (engl. primary type) übernommen.
¹⁶ Es ist ein offenes Problem, wie dieser Name zu wählen ist. Wie Abschni . . beschreibt, kann die auf endlich viele Werte beschränkte Domäne der Namen durch eine Anzahl weiterer, bisher nicht benu ter, frischer
Namen erweitert werden, was derzeit Stand der Implementierung ist. Diese künstlichen, frischen Namen werden aber die umzubenennende Variable kaum sinnvoll beschreiben. Es ist denkbar, in späteren Versionen in
einem einfachen Nachbearbeitungsschri den frischen Namen gegebenenfalls durch den Benu er anpassen
zu lassen oder auch während der Constraintlösung den Benu er interaktiv neue Namen wählen zu lassen.
. . Angebotene Variablen
220
217
public class A {
218
219
221
222
}
223
224
class B extends A {
void m() { this.n(); }
void n() { this.l(); }
void l() { /* ... */ }
}
Abbildung . .: Soll die Methode m() in Zeile
aus B in die Superklasse A verschoben werden, müssen die Methoden n und l ebenso verschoben werden, da sie ansonsten auf den Empfängern this nicht mehr verfügbar sind.
Ursprungsklasse referenziert). In diesem Fall geschah das vorher durchgeführte R
umsonst und muss gegebenenfalls wieder rückgängig gemacht werden. Weder NetBeans noch
IntelliJ prüfen derzeit bei einem Pull up Field noch weitere Bedingungen, nachdem klar ist,
dass die Refaktorisierung wegen eines Namenskonflikts in der Zielklasse fehlschlägt. Lediglich Eclipse bietet eine Liste aller abzusehenden Konflikte – wobei sich diese aber dennoch nur
auf die Situation vor einem R
beziehen können.
Zugreifbarkeiten Beinahe jedes Refaktorisierungswerkzeug hat Zugrei arkeiten von Deklarationen zu berücksichtigen. Insbesondere Verschieberefaktorisierungen, die Deklarationen und in ihnen enthaltene Referenzen zu anderen Deklarationen über Klassen- oder Paketgrenzen hinweg bewegen, machen häufig eine Anpassung von Zugrei arkeiten nötig.
Seltener können variable Zugrei arkeiten auch Rename-Refaktorisierungen ermöglichen.
So kann bei Umbenennung eines Feldes ein Zugriff auf dieses mehrdeutig (engl. ambigous)
werden, wenn an der Aufrufestelle gleichnamige Felder aus einer Superklasse und einem Superinterface geerbt werden. Indem das Feld der Superklasse zum Beispiel auf private gese t
wird, kann der Aufruf eindeutig gemacht werden.
Orte Wird durch ein Refaktorisierungswerkzeug der Ort, an dem eine Deklaration steht,
verändert, geschieht dies meist im Rahmen der Intention. Bei Verschieberefaktorisierungen
wie Pull Up oder Push Down wählt der Benu er bei der Konfiguration der Refaktorisierung
genau aus, welche Deklaration wohin verschoben werden soll. Ebenso sind Refaktorisierungswerkzeuge, die neue Deklarationen (zum Beispiel get- und set-Methoden) generieren, solche,
bei denen die Änderung eines Ortes Teil der Intention ist. Praktisch können solche Werkzeuge
mit Constraints derart implementiert werden, dass man die zu generierenden Methoden im
Rahmen einer Vorausschau als bereits an einem Ort außerhalb des Programms (sozusagen im
Nirvana) als existent annimmt und sie dann durch die Intention erst an ihren Bestimmungsort
verschiebt.
Dennoch kann es Sinn ergeben, Refaktorisierungswerkzeugen auch im Rahmen der Constraintlösung zuzubilligen, weitere Deklarationen eigenständig zu verschieben. Soll eine Methode in eine Superklasse verschoben werden, welche noch andere Instanzmethoden der Ursprungsklasse referenziert, müssen diese mit verschoben werden, wie das Beispiel in Abbildung . zeigt.
Besondere Variabilität bei den möglichen Folgeänderungen ergibt sich, wenn sowohl Orte
als auch Zugrei arkeiten variabel sind, hängt doch die Zugrei arkeit einer Deklaration nicht
. Eine Sprachdefinition für Java in Refacola
229
230
226
public class A {
227
228
231
class B extends A {
void m() { helper1(); helper2(); }
void n() { helper1(); helper2(); }
232
}
private static void helper1() {}
private static void helper2() {}
233
234
235
}
Abbildung . .: Sollen die Methoden m() und n() gemeinsam in die Superklasse A verschoben
werden, scheint es sinnvoll, ebenso die helper-Methoden mit zu verschieben.
nur vom Zugrei arkeitsmodifizierer, sondern insbesondere von den Orten der Deklaration
und ihren Referenzen ab. Somit lässt sich die Zugrei arkeit einer Deklaration nicht nur erreichen, indem die Zugrei arkeitsstufe erhöht wird, sondern gegebenenfalls auch, indem eine
Deklaration näher an ihre Referenzierungen gebracht wird. Abbildung . gibt hierfür ein Beispiel: Sollen beide Methoden n() und m() gemeinsam in ihre Superklasse A verschoben werden,
scheint es sinnvoll, ebenso die beiden Hilfsmethoden helper1 und helper2 mit zu verschieben, schließlich erspart dies sowohl die Anpassung der Zugrei arkeiten der Hilfsmethoden,
als auch die Qualifizierung ihrer Aufrufe. Zwar ermöglichen alle betrachteten Entwicklungsumgebungen – NetBeans, IntelliJ und Eclipse – dem Benu er mehr als eine Methode zum
Hochziehen auszuwählen, allerdings wird keine weitere Unterstü ung geboten, um zu beurteilen, ob ein Hochziehen weiterer, nicht zwangsläufig im Supertyp benötigter Methoden
sinnvoll ist. Diese Frage ist ohne genaue Codeanalyse nicht zu beantworten. Soll nämlich nur
eine der beiden Methoden n() oder m() verschoben werden, scheint ein Verschieben der beiden
Hilfsmethoden weniger sinnvoll. Diese müssen dann nämlich ohnehin in der Zugrei arkeit
angepasst werden und auch in mindestens einer der beiden Methoden m() oder n() müssen
die Aufrufe der Hilfsmethoden qualifiziert werden.
Soweit ein Constraintlöser verwendet wird, der die Anzahl der Werteänderungen von Variablen im Vergleich zu ihren initialen Wertebelegungen minimieren kann (siehe hierzu Abschni . ), wird dieser automatisch abwägen, ob es sich lohnt, Zugrei arkeiten anzupassen
oder Deklarationen zu verschieben. Ist ein Constraintlöser angebunden, welcher das Berechnen aller Constraintlösungen unterstü t, können dem Benu er ebenso die verschiedenen Alternativen zur Auswahl gegeben werden.
Typen Die vierte unterstü te Kategorie von Variablen sind Typen, wie sie in Java genu t
werden, um Felder, Methoden, Parameter und Variablen zwecks Typprüfung¹⁷ zu annotieren. Moderne IDEs bieten üblicherweise gleich mehrere Werkzeuge, die deklarierte Typen im
Rahmen der Intention verändern, so zum Beispiel die Refaktorisierungswerkzeuge C
M
S
und U S
W
P
[TKB ], mit denen die Typen von
Methodenparametern, Rückgabewerten und Variablen verändert werden können.
Auch können sich Typänderungen nur indirekt aus der Intention ergeben, so bei M
Refaktorisierungen. Wird eine Instanzmethode in eine andere Klasse verschoben, bedeutet
dies auch, dass sich gleichzeitig alle in ihr enthaltenen Anweisungen verschieben. Ist dabei
¹⁷ und intern noch vieler weiterer Optimierungen
. . Angebotene Variablen
236
237
238
239
240
241
242
243
244
class A {
private String m() {
String s = "abc";
return s;
}
String n() {
return m().toString();
}
}
(a)
⇒
class A {
Object Object m() {
Object s = "abc";
return s;
}
String n() {
return m().toString();
}
}
(b)
Abbildung . .: Soll in Zeile
der deklarierte Typ von s zu java.lang.Object geändert werden,
erfordert dies auch die Änderung des Rückgabetypen der umschließenden
Methode m().
eine this-Referenz enthalten, ändert diese damit im Rahmen der Intention ihren (durch den
Compiler inferierten) Typen durch die Verschiebung ebenso.
Weiterhin ist es bei vielen Refaktorisierungen nötig, als Folgeänderungen vorhandene deklarierte Typen zu erse en. In erster Linie betrifft dies die schon angesprochenen Typ-Refaktorisierungen C
M
S
und U S
W
P
, bei denen
mitunter weitere Typannotationen anzupassen sind. Abbildung . gibt ein Beispiel, für das
eine Anpassung des deklarierten Typen von s zu java.lang.Object ebenso eine Änderung des
deklarierten Typen von m() hin zu java.lang.Object erfordert.
Doch nicht nur Typrefaktorisierungen können von variablen Typen profitieren. Denkbar
sind ebenso Anpassungen von Zugrei arkeiten, die erst dadurch ermöglicht werden, dass
Typreferenzen erse t werden können. Abbildung . liefert ein Beispiel, in dem ein Membertyp überhaupt nur in seiner Zugrei arkeit auf private gese t werden kann, indem Typreferenzen auf diesen durch Referenzen auf seinen Supertypen erse t werden – eine Funktionalität, wie sie in den untersuchten Entwicklungsumgebungen Eclipse, IntelliJ und NetBeans
innerhalb eines einzelnen Werkzeugs bisher gar nicht angeboten wird.
Das Zusammenspiel von variablen Namen, Zugreifbarkeiten, Orten und Typen Abbildung . (a) zeigt ein umfangreiches Beispiel für eine Refaktorisierung, in welcher die Intention nur dann fehlerfrei erfüllt werden kann, wenn als Folgeänderungen sowohl Typ-, Namens-,
Zugrei arkeits- und Ortsänderungen möglich sind. Ausgangspunkt ist die Klasse CImpl, aus
welcher die Methode n() in die Superklasse A verschoben werden soll. Als erste Folgeänderung
ergibt sich die Notwendigkeit, ebenso die Methode nInternal() nach A zu verschieben, da sie
ansonsten von n() aus nicht mehr aufgerufen werden kann. Als Folge kommt es allerdings zu
einem Problem bei der Zugrei arkeit der Klasse C, welche innerhalb der Methode nInternal(),
die nun im Paket a liegt, nicht mehr referenziert werden kann. Variable Zugrei arkeiten ermöglichen, die Klasse C als public zu deklarieren, allerdings erfordert dies ebenso, auch die
umschließende Überse ungseinheit CImpl.java in C.java umzubenennen, was nur mit variablen
Namen möglich ist. Nachdem in Java aber nur ein ToplevelTyp pro Überse ungseinheit als
public deklariert sein darf, muss nun der Typ CImpl auf Pake ugrei arkeit zurückgestuft wer-
. Eine Sprachdefinition für Java in Refacola
245
246
247
248
abstract class A {
static class AImpl extends A {
public void m() { /* ... */ }
}
abstract class A {
private static class AImpl extends A {
public void m() { /* ... */ }
}
249
static AImpl create() {
return new AImpl();
}
250
251
252
static AImpl create() {
return new AImpl();
}
⇒
253
abstract void m();
254
255
abstract void m();
}
}
class B {
A.AImpl a = A.create();
}
class B {
A a = A.create();
}
256
257
258
259
(a)
(b)
Abbildung . .: Ist es einer C
A
-Refaktorisierung gesta et, auch deklarierte Typen zu ändern, muss eine Änderung der Zugrei arkeit von AImpl zu
private nicht abgelehnt werden. Vielmehr können außerhalb von A Referenzen auf AImpl durch Referenzen auf A erse t werden, wie hier in Zeile
geschehen.
den, was aber für einen Kompilierfehler beim Aufruf der Methode m() in Zeile
sorgt. In
Java ist es nicht nur nötig, dass eine aufgerufene Methode zugrei ar ist, ebenso muss auch ihr
jeweiliger Empfängertyp zugrei ar sein, was in diesem Fall CImpl ist. Abhilfe kann hier – variable Typen vorausgese t – eine Änderung des Rückgabetyps der Methode getC in Zeile
hin zu C schaffen. Die insgesamt nötige Programmtransformation zeigt Abbildung . (b).
Es ist davon auszugehen, dass ein wie in Abbildung . gezeigter Fall, der sowohl Folgeänderungen von Typen, Zugrei arkeiten, Orten und Namen nötig macht, in der Praxis nur
selten auftri . Wohl ist aber davon auszugehen, dass mindestens die paarweisen Kombinationen von Folgeänderungen unterschiedlicher Art Praxisrelevanz haben, wie sie auch schon
in den Abbildungen . , . und . gezeigt sind.
4.2. Taxonomie der Programmelemente
Abschni . . zeigte auf, wie Programmelemente mit der Refacola definiert werden können und wie ihnen Constraintvariablen anhand von Variablenarten (den properties) zugeordnet oder an sie vererbt werden können. Die in dieser Arbeit präsentierte und in Anhang A
vollständig wiedergegebene Sprachdefinition für Java in Refacola unterscheidet in ihrer Ableitungshierarchie auf oberster Ebene drei Arten von Programmelementen: Entity, Reference
und Expression. Der relevante Teil der Sprachdefinition findet sich als Auszug ebenso noch einmal in Abbildung . . Entity umfasst die Menge aller Deklarationen im Programm, Reference
die Menge aller Referenzen auf diese und Expression die Menge der Ausdrücke. Die Unterscheidung in Deklarationen und Referenzen folgt dem vom Compilerbau bekannten Ansa ,
. . Taxonomie der Programmelemente
260
261
// File "A.java"
package a;
// File "A.java"
package a;
public class A {
}
public class A {
public void n() { nInternal(); }
private void nInternal() { c.C c; /* ... */ }
}
// File "B.java"
package c;
// File "B.java"
package c;
public class B extends a.A {
public CImpl getC() {
return C.createC();
}
}
public class B extends a.A {
public C getC() {
return C.createC();
}
}
// File: "CImpl.java"
package c;
// File: " C.java "
package c;
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
abstract class C extends B {
public static CImpl createC() {
return new CImpl();
}
public void m() { /* ... */ }
}
⇒
public abstract class C extends B {
public static CImpl createC() {
return new CImpl();
}
public void m() { /* ... */ }
}
286
287
288
289
290
public class CImpl extends C {
public void n() { nInternal(); }
private void nInternal() { c.C c; /* ... */ }
}
class CImpl extends C {
// File: "Main.java"
package main;
// File: "Main.java"
package main;
public class Main {
public void main() {
new c.B().getC().m();
}
}
public class Main {
public void main() {
new c.B().getC().m();
}
}
}
291
292
293
294
295
296
297
298
299
(a)
(b)
Abbildung . .: Soll die Methode n() in Zeile
in die Superklasse A verschoben werden, umfassen die nötigen Folgeänderungen sowohl das Verschieben weiterer Deklarationen, das Anpassen von Zugrei arkeiten, das Ändern deklarierter Typen
und das Umbenennen einer Überse ungseinheit.
. Eine Sprachdefinition für Java in Refacola
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
language Java
kinds
abstract Entity <: ENTITY
abstract Reference <: REFERENCE
abstract Expression <: ENTITY { expressionType }
...
properties
...
domains
...
queries
binds(reference: NonTypeReference, entity: Entity)
receiver(reference: TypedReference, receiver: Expression)
isExpression(reference: TypedReference, expression: Expression)
...
Abbildung . .: Die Refacola-Sprachdefinition für Java baut auf drei abstrakten Arten von
Programmelementen auf: Deklarationen (Entity), Referenzen (Reference) und
Ausdrücken (Expression), die über die Bindungen (binds), Nachrichtenempfänger (receiver) und Identität von Referenzen und Ausdrücken (isExpression)
miteinander verbunden sind.
Definition-Use-Paare [ASU , S.
- ] zu bilden, welche sich in Kombination mit dem Fakt
binds ergeben, welches jeder Referenz die jeweils gebundene Deklaration zuweist.¹⁸
Die dri e Komponente – die Ausdrücke (engl. expression) ( § ) – sind solche Codebestandteile in Java, die auswertbar sind und einen (gegebenenfalls durch den Compiler zu inferierenden) Typ haben. Ausdrücke nehmen gegenüber den Deklarationen und Referenzen eine Sonderrolle ein, denn im Gegensa zu den Deklarationen bilden sie eine nicht-leere Schni menge mit den Referenzen. So ist beispielsweise eine Feldreferenz gleichzeitig eine Referenz und
ein Ausdruck. Umgekehrt bilden aber weder Referenzen eine Unterart der Ausdrücke, noch
Ausdrücke eine Unterart der Referenzen. So bilden zum Beispiel Literale ( § . ) in Java
Ausdrücke, die keine Referenzen sind. Umgekehrt bilden Referenzen auf Pakete – beispielsweise innerhalb von Paketdeklarationen ( § . ) – und Referenzen auf Typen – zum Beispiel
innerhalb eines Typ-Casts ( § . ) keine Ausdrücke. Auch unter den Referenzen auf Konstruktoren findet sich mit den this(...)- und super(...)-Aufrufen Referenzen, die keine Ausdrücke
sind ( § . . . ) und im Gegensa zu void-Methoden schon allein von den syntaktischen Regeln der Sprache her niemals anstelle eines Ausdrucks eingese t werden dürfen. Man kann
also weder Reference von Expression ableiten lassen, noch umgekehrt, weswegen beide parallel
existieren. Fällt ein Ausdruck mit einer Referenz zusammen, wird das jeweilige Codeelement
(konkret: der Knoten im abstrakten Syntaxbaum) auf zwei Refacola-Programmelemente abgebildet und diese Dualität durch ein Fakt isExpression angegeben. Über dieses können Constraintregeln dann zwischen Referenzen und den durch sie unmi elbar gebildeten Ausdrü¹⁸ Tatsächlich gilt das nicht für Typ-Referenzen. Deren Bindungen sind nicht als binds-Fakten, sondern als Constraintvariablen typeBinding der Unterart TypeReference modelliert, weswegen die Deklaration der Faktenart
binds die Art der Referenzen auf NonTypeReference einschränkt.
. . Fakten und Abfragen
cken hin und her navigieren, was insbesondere für die Typinferenz benötigt wird. So hängt
zum Beispiel der inferierte Typ einer Method Invocation Expression ( § . ) unmi elbar von
der Bindung der in ihr enthaltenen Methodenreferenz ab.
Innerhalb der drei Ableitungsbäume von Deklarationen, Referenzen und Ausdrücken ergibt
sich die Strukturierung zumeist direkt aus den von der Java-Sprachspezifikation vorgegebenen Unterscheidungen und Nomenklaturen. So unterscheidet die Java-Sprachspezifikation
bei Typen zum Beispiel zwischen Topleveltypen und geschachtelten Typen (engl. nested type). Le tere können wiederum Membertypen (engl. member type) oder innere Klassen (engl.
inner class) sein. Ganz analog hierzu sieht die entwickelte Sprachdefinition eigene Programmelementarten für Topleveltypen, geschachtelte Typen, Membertypen und innere Klassen vor,
wobei Membertypen und innere Klassen von geschachtelten Typen abgeleitet sind und Membertypen und Topleveltypen (über weitere Arten) von einer abstrakten Programmelementart
Type ableiten. Diese Analogie ist insbesondere hilfreich bei der Formulierung der Constraintregeln. Dort, wo die Java-Sprachspezifikation Bedingungen für beliebige Arten von Typen
vorgibt, kann bei Formulierung entsprechender Constraintregeln jeweils die abstrakte Programmelementart Type für die entsprechende Regelvariable verwendet werden. Gilt eine Bedingung nur für Membertypen, wird die entsprechende Unterart verwendet. Beim Erse en
der Regelvariablen durch konkrete Programmelemente zum Zeitpunkt der Constraintgenerierung wählt die Refacola dann entsprechend alle Typen oder nur Membertypen aus, je nach
Programmelementart der Regelvariablen.
Abbildung . zeigt die Deklarationen für Java-Typen innerhalb der Refacola-Sprachdefinition als Diagramm. Jedes der horizontal dreigeteilten Rechtecke repräsentiert darin eine Programmelementart. Im oberen Dri el steht jeweils ihr Name, im mi leren Dri el sind die für
sie definierten Constraintvariablen gelistet. Das untere Dri el listet die jeweils geerbten Constraintvariablen. Die Pfeile zwischen den Programmelementarten geben Ableitungsbeziehungen an. Programmelementarten, für die noch Ableitungsbeziehungen zu nicht von Type abgeleiteten Programmelementarten bestehen, sind farbig hinterlegt. Eine graphische Darstellung
der gesamten Sprachdefinition ist technisch möglich [Kof ], aber wegen des resultierenden
sehr großen und vor allem nicht-planaren Graphen mit vielen Linienkreuzungen nicht mehr
zielführend. So zeigt auch Abbildung . nur einen Ausschni , in welchem beispielsweise
die Programmelementart Class fehlt, von welcher noch einmal TopLevelClass, InnerClass und
StaticMemberClass ableiten. Auch wurden aus Pla gründen einige Variablen (zum Beispiel
enclosingTypes) ausgespart. Die vollständigen Definitionen ergeben sich aus dem in Anhang A
gegebenen Quellcode.
4.3. Fakten und Abfragen
Die für die Java-Sprachdefinition deklarierten Faktenarten ergeben sich direkt aus den zur
Formulierung der Constraintregeln nötigen Abfragen. Viele der implementierten Faktenarten
beschreiben einfache, sich unmi elbar aus einer Analyse der reinen Syntax eines Programms
ergebende Zusammenhänge. So beschreibt zum Beispiel das zweistellige Fakt ReferenceInEntity, dass eine bestimmte Referenz unmi elbar innerhalb einer bestimmten Deklaration –
meist einer Methode – enthalten ist. Andere Faktenarten beschreiben komplexere Zusammenhänge, die eine semantische Analyse nötig machen und bei denen für die Faktengenerie-
. Eine Sprachdefinition für Java in Refacola
Type
AccessibleEntity
· accessibility
NullType
AccessModifiableType
TlOwnedEntity
· tlOwner
ReferenceType
· accessibility
· hostPackage
ClassOrInterfaceType
NamedEntity
· identifier
· accessibility
TlOwnedType
· accessibility
· hostPackage
· tlOwner
ArrayableType
ArrayType
NamedType
· componentType
· elementType
· accessibility
· hostPackage
· tlOwner
· identifier
TypeVariable
CompilationUnitMember
NamedClassOrInterfaceType
· typeRoot
TopLevelEnum
·
·
·
·
·
TopLevelClass
·
·
·
·
·
accessibility
hostPackage
identifier
tlOwner
typeRoot
·
·
·
·
·
·
·
·
·
·
·
·
·
·
accessibility
hostPackage
identifier
tlOwner
typeRoot
java.lang.Object
...
PrimitiveType
· identifier
TopLevelInterface
TopLevelType
accessibility
hostPackage
identifier
tlOwner
typeRoot
accessibility
hostPackage
identifier
tlOwner
accessibility
hostPackage
identifier
tlOwner
typeRoot
PrimitiveVoid
· identifier
java.lang.Iterable
...
PrimitiveBoolean
· identifier
NestedType
HidableMember
· accessibility
· hostPackage
· tlOwner
LocalClass
·
·
·
·
·
·
·
·
InnerClass
AnonymousClass
· accessibility
· hostPackage
· tlOwner
·
·
·
·
·
· identifier
accessibility
hostPackage
identifier
owner
tlOwner
NonStaticMember
Class
·
·
·
·
·
PrimitiveShort
MemberType
· accessibility
· hostPackage
· tlOwner
accessibility
hostPackage
identifier
tlOwner
accessibility
identifier
owner
tlOwner
accessibility
hostPackage
identifier
owner
tlOwner
·
·
·
·
·
InstanceMember
StaticMemberType
accessibility
hostPackage
identifier
owner
tlOwner
·
·
·
·
·
accessibility
hostPackage
identifier
owner
tlOwner
StaticMember
·
·
·
·
accessibility
identifier
owner
tlOwner
Abbildung . .: Graphische, ausschni sweise Darstellung der Programmelementarten für Typen, wie sie sich aus der entwickelten Refacola-Sprachdefinition für Java
ergeben.
. . Unveränderliche Programmeigenschaften und funktionale Einschränkungen
rung zweckmäßigerweise ein Compiler befragt wird. Beispiele hierfür sind die zweistelligen
binds-Fakten, die Bindungen zwischen Referenzen und Deklarationen wiedergeben, sowie die
zweistelligen override-Fakten, die Paare überschreibender und überschriebener Methoden angeben.
Leider lassen sich in der derzeitigen Implementierung gewisse Redundanzen nicht vermeiden, so unterstü t die Syntax der Refacola keine Formulierung transitiver Abfragen. Diese
wären aber nü lich, zum Beispiel für overrides-Faktenabfragen. So unterscheidet die JavaSprachspezifikation bei den Vorausse ungen für Methodenüberschreibungen einen transitiven Fall (eine Methode m1 überschreibt eine Methode m2 , indem m1 eine Methode m3 überschreibt, die wiederum die Methode m2 überschreibt) und einen nicht-transitiven Fall, für
welche unterschiedliche Anforderungen bezüglich der Zugrei arkeiten gelten ( § . . . ).
Dies schlägt sich in der Implementierung zweier Faktenarten overrides und overridesDirectly
nieder, wobei sich allerdings die erstere direkt aus der transitiven Hülle der le teren ergibt.
4.4. Unveränderliche Programmeigenschaften und funktionale Einschränkungen
Mit steigender Anzahl der Freiheiten, die ein constraintbasiertes Werkzeug im Bezug auf mögliche Folgeänderungen neben der Intention hat, steigt die Komplexität gleich in doppelter
Hinsicht. Einerseits verlangen mehr Freiheitsgrade in Form von zusä lichen Variablenarten
zusä liche Constraintregeln, um die gesteigerte Anzahl möglicher Variablenbelegungen auf
genau solche einzuschränken, die das Programmverhalten nicht unerwünscht verändern. Andererseits erhöht sich durch eine gesteigerte Anzahl von Variablen die Laufzeit des Programmierwerkzeugs, da die Zeiten zum Constraintlösen naturgemäß mit der Anzahl zu belegender
Variablen steigen.
Um einen sinnvollen Kompromiss zwischen Funktionalität und sich ergebendem Aufwand
für die Regelerstellung sowie den sich ergebenden Laufzeiten zu finden, wurden in dieser
Arbeit Annahmen über unveränderliche Programmeigenschaften gemacht, die im Folgenden
genannt sind.
Subtypbeziehungen Ableitungshierarchien von Typen sind in der vorliegenden Sprachdefinition nicht als Variablen repräsentiert. Damit sind keine Werkzeuge umse bar, die während der Constraintlösung dynamisch Änderungen der Typhierarchie vornehmen. Die dadurch zu erwartenden Einschränkungen sind aber gering. Die Prüfung auf Subtypbeziehung
innerhalb diverser Constraintregeln finden derzeit über Faktenabfragen anhand der zweistelligen Faktenart sub sta . Über eine vorausschauende Anwendung [ST ] (siehe auch den Abschni über bedingte Constraints ab Seite ) sind also nach wie vor Werkzeuge umse bar,
bei denen Änderungen der Typhierarchie schon vor Werkzeuganwendung vollständig bekannt sind, wie es zum Beispiel für die Replace Inheritance With Delegation-Refaktorisierung
beschrie[Fow , KS ] der Fall ist. Im Katalog von Fowler [Fow ] findet sich unter den
benen Werkzeugen keines, welches eine nicht im Vorhinein benennbare Änderung der Typhierarchie mit sich bringt.
Ausführungsreihenfolgen von Anweisungen Ausführungsreihenfolgen von Anweisungen innerhalb von Blöcken sind derzeit in der Sprachdefinition nicht modelliert, auch exis-
. Eine Sprachdefinition für Java in Refacola
tieren keine Faktenabfragen für diese oder Constraintregeln, die bei (selbst vorausschauender Anwendung einer) Vertauschung von Anweisungen über eine Bedeutungserhaltung wachen könnten. Die einzige lokale Zuordnung von Anweisungen geschieht über die Faktenart ReferenceInEntity, dessen Fakten angeben, welche möglicherweise in Anweisungen enthaltenen Referenzen in welchen Deklarationen enthalten sind. Damit sind alle Arten von
Programmtransformationen ausgeschlossen, die Anweisungen zwischen Blöcken verschieben
oder innerhalb von Blöcken in ihrer Reihenfolge beeinflussen.
Von den
bei Fowler [Fow ] beschriebenen Werkzeugen betreffen nur zwei ausschließlich das Ändern von Anweisungen, nämlich I
A
und das in Abschni .
schon erwähnte C
D
C
F
. Weitere zwei Refaktorisierungen kommen mit R
E
C
E
und R
E
T
hinzu, wenn man davon ausgeht, dass es sich bei den hinzugefügten oder entfernten Ausnahmen um nicht in Methodensignaturen genannte Laufzeitfehler handelt. Jede der weiteren
genannten Refaktorisierungen nimmt hingegen Einfluss auf Deklarationen. Ihre korrekte
Behandlung ist also Vorausse ung beinahe aller Refaktorisierungen und bildet daher – wie
auch ihr Titel vorgibt – den Schwerpunkt der vorliegenden Arbeit.
Umgekehrt gibt es einen weiteren Anteil an Refaktorisierungen, die sowohl Deklarationen
in ihren Eigenschaften ändern, hinzufügen oder entfernen, als auch Anweisungen zwischen
Blöcken verschieben oder vertauschen. Ein prominentes Beispiel ist die E
M
Refaktorisierung [Fow ], für welche die vorliegende Arbeit zwar eine Lösung für das sichere Einfügen der neuen Methode bietet, nicht aber prüfen kann, ob durch das Einbringen des
zusä lichen Methodenaufrufs anderweitige Änderungen im Programmverhalten ermöglicht
werden.
Die Vermutung liegt nahe, dass sich solche Bedingungen auf Anweisungsebene ebenso constraintbasiert lösen lassen wie solche für Refaktorisierungen von Deklarationen. So beschreiben Nielson et al. [NHN ] bereits constraintbasierte Analysen von Kontroll- und Datenfluss,
wie sie beim Verschieben von Anweisungen relevant werden. Schäfer et al. beschreiben sogenannte synchronization dependencies [SDS+ ], die bei Refaktorisierung parallel laufendem
Java-Codes einzuhalten sind und die insbesondere bei Programmtransformationen auf Ausdrücken Berücksichtigung finden müssen [SDS+ ]. Es darf vermutet werden, dass diese Abhängigkeiten analog zu den in dieser Arbeit behandelten Abhängigkeiten wie Bindungen und
Überschreibungen ebenso constraintbasiert umgese t werden können.
Qualifizierungen und explizite Typumwandlungen Die vorliegende Sprachdefinition betrachtet keine Qualifizierer von Namen und Ausdrücken ( §§ . , . , . . ) und auch –
außer im Fall von Umbenennungen – keine Import-Deklarationen ( § . ). Bei der Modellierung des Eingabeprogramms als Constraintvariablen wird jede Referenz als unqualifiziert
modelliert, auch existieren keine Constraintregeln, die über ein nötiges Qualifizieren von Referenzen wachen. Sta dessen wird in einem von der Constraintlösung separierten Postprocessing eine Prüfung aller betroffenen Referenzen durchgeführt, bei der gegebenenfalls Qualifizierer hinzugefügt oder angepasst werden. Damit findet ein Verfahren Anwendung, welches
schon unter Beteiligung des Autors der vorliegenden Arbeit in [STST ] beschrieben wurde.
Für dieses Vorgehen gibt es zweierlei Gründe. Einerseits wurde in der Sprache Java das
Konzept der Qualifizierung derart entwickelt, dass unabhängig von Name und Ort und sons-
. . Unveränderliche Programmeigenschaften und funktionale Einschränkungen
tigen Eigenschaften einer Deklaration stets eine geeignete Möglichkeit zur Qualifizierung einer Referenz gefunden werden kann, womit das Finden von Qualifizierern unabhängig von
Umbenennungen, Verschiebungen und Änderungen der sonstigen Eigenschaften einer Deklaration erfolgen kann.¹⁹ Andererseits liegt es in der Natur des constraintbasierten Ansa es,
primär Eigenschaften von vorhandenen Programmelementen zu bedingen, aber keine Erzeugung neuer Variablen während der Constraintlösung zuzulassen. Somit wird das Einfügen
von Qualifizierern zum Problem, welches sich höchstens dadurch lösen ließe, schon implizit jede Referenz als qualifiziert anzunehmen und die tatsächliche Existenz des Qualifizierers
über eine Boolesche Variable zu steuern. Da Java aber gleich mehrere – teils von den Syntaxregeln her sogar unterschiedliche – Arten der Qualifikation für gleiche Arten von Referenzen anbietet, entstünden aus diesem Ansa viele bedingte – und damit teure – Constraints.
Abschni . . wird im Rahmen der Beschreibung der Constraintregeln zu Verscha ungen
(welche häufig ein Qualifizieren von Referenzen nötig machen) ein Beispiel zeigen und die
Problematik erneut aufgreifen.
Ganz ähnlich gelagert ist die Situation bei expliziten Typumwandlungen (engl. type cast)
( § . ). Der constraintbasierte Ansa ist nicht dafür ausgelegt, während der Constraintlösung neue Constraintvariablen einzufügen, sondern nur den bestehenden Constraintvariablen geeignete Werte zuzuweisen. Somit bietet sich von Haus aus keine Lösung an, nach
Bedarf neue, explizite Typumwandlungen hinzuzufügen. Analog zu den Qualifizierern wäre
es aber auch hier möglich, für jeden Ausdruck eines Programms eine explizite Typumwandlung anzunehmen. Ihr initialer Wert entspricht genau dem bisher vom Compiler inferierten
Typen des jeweiligen Ausdruckes. Beim Zurückschreiben der durch den Constraintlöser ermi elten Variablenwerte in den Code muss dann für jeden Variablenwert geprüft werden,
ob dieser vom initialen Wert abweicht. In einem solchen Fall ist eine explizite Typumwandlung in den Code einzufügen, in allen übrigen Fällen kann sie (weiterhin) entfallen. Neben
der Frage, ob die damit gewonnenen Freiheitsgrade beim Refaktorisieren das dadurch entstehende Mehr an Constraintvariablen, Constraints und le tendlich Rechenzeit rechtfertigen,
bleibt allerdings ebenso unklar, wie mit mehreren, direkt ineinander verschachtelten expliziten Typumwandlungen vorgegangen werden soll. Daher werden neu einzuführende, explizite Typumwandlungen in dieser Arbeit nicht weiter betrachtet. Im Programm bereits bestehende Typumwandlungen hingegen werden voll unterstü t, Abschni . . benennt die für
diese implementierten Constraintregeln.
Umwandlungen von Deklarationen Bisher ebenso nicht in der Sprachdefinition modelliert sind Transformationen von Deklarationen hin zu anderen Deklarationsarten. Die derzeitige Spezifikation bietet keine Möglichkeit, Arten von Programmelementen während der
Constraintgenerierung anzupassen, somit kann zum Beispiel keine lokale Variable zu einem
¹⁹ Eine einzige Ausnahme bildet eine Qualifizierung mithilfe eines Typnamens, für welche der verwendete Typ
vom Punkt der Referenzierung aus zugrei ar sein muss. In diesem – einigermaßen exotischen – Fall kann es
tatsächlich passieren, dass eine Refaktorisierung wegen fehlender Möglichkeit der Qualifizierung abgelehnt
werden muss, obwohl sie durchführbar gewesen wäre, wenn ein geeignetes Constraint zuvor die Zugrei arkeit des zur Qualifizierung notwendigen Typen gefordert hä e. [STST ] löst dieses Problem mi els einer
gegebenenfalls erneuten Generierung von Zugrei arkeitsconstraints, wenn bei der Qualifizierung von Referenzen wiederum neue Referenzen mit ihrerseits neuen Zugrei arkeitsbedingungen entstehen. Dasselbe
Verfahren kann auch für die in dieser Arbeit entwickelten Werkzeuge angewendet werden.
. Eine Sprachdefinition für Java in Refacola
Feld umgewandelt werden. Ein weiteres Beispiel liefern primitiv und nicht-primitiv typisierte Deklarationen, die ebenso durch unterschiedliche Arten von Programmelementen repräsentiert werden. So ist zum Beispiel ein C
D
T
von einem nicht-primitiven
Typen hin zu einem primitiven Typen derzeit nicht möglich.²⁰ Technisch wäre ein solches
Verhalten umse bar, indem man unterschiedliche Arten von Programmelementen zu einer
Art zusammenfasst und anhand einer Constraintvariablen den Constraintlöser entscheiden
lässt, von welcher Art die entsprechende Deklaration ist. Dies würde allerdings bedeuten,
dass bestimmte Constraintvariablen, die nur für einzelne der zusammengefassten Arten Sinn
ergeben, in den übrigen Fällen eine Art null-Wert erhalten müssten. Um null-Zugriffe zu verhindern, ist dann aber mit einer deutlich erhöhten Anzahl nur teuer auszuwertender bedingter
Constraints zu rechnen. Ob hierbei Kosten und Nu en in einem sinnvollen Verhältnis stehen,
bleibt zu erforschen.
Statische Aufrufe auf Instanzvariablen und Methodenparameter mit variabler Stelligkeit
Die Sprache Java bietet zwei Komfortfunktionen, die nicht zwangsläufig Berücksichtigung in
der Refacola-Sprachdefinition benötigen, da sie beide nur alternative Schreibweisen sind, die
ohnehin sofort durch den Compiler erse t werden. Das sind einerseits statische Aufrufe auf
Instanzvariablen und andererseits Methodenparameter mit variabler Stelligkeit (engl. variable
arity parameter).
Beim Aufruf statischer Methoden oder Felder besteht an und für sich keine Notwendigkeit, diese mit einer Instanz des deklarierenden Typen zu qualifizieren. Dennoch verbietet die
Sprache Java einen solchen Aufruf nicht ( § . . . ), sondern erse t die jeweilige Referenz
auf die Instanz implizit durch eine Referenz auf ihren deklarierten Typen. Gängige Compiler
geben zusä lich eine Warnung aus. Ganz ähnlich verhält es sich mit Methodenparametern
mit variabler Stelligkeit ( § . . ), welche durch den Java-Compiler intern direkt durch entsprechende, eindimensionale Array-Typen erse t werden ( § . . ). Somit erübrigt sich eine Berücksichtigung dieser beiden Kurzschreibweisen, da sie problemlos erse t und in einer
Rückschreibekomponente gegebenenfalls wieder eingefügt werden können.
²⁰ Seit Java aber mit Version das Autoboxing ( § . . ) unterstü t, lässt sich hier ein Workaround recht einfach
implementieren, indem zunächst constraintbasiert auf den passenden Wrapper-Typen hin refaktorisiert wird
und dieser anschließend durch den entsprechenden Primitivtypen erse t wird.
5. Constraintregeln für Java
Dieses und das folgende Kapitel widmen sich solchen Constraintregeln und Constraints,
welche die im vorhergehenden Kapitel eingeführten Variablen derart beschränken, dass
sie nur noch die für Refaktorisierungswerkzeuge so wichtigen bedeutungserhaltenden Programmtransformationen gesta en.
Es liegt in der Natur der Constraints, nicht aufeinander aufzubauen, sondern gemeinsam
und gleichberechtigt konjunktiv zusammenzuwirken. Aus diesem Grund fällt es schwer zu
entscheiden, wie für eine didaktische Aufarbeitung die Constraintregeln zu reihen sind. Ein
Hauptunterscheidungskriterium ist, ob ein Constraint anhand einer statischen Codeanalyse
oder einer Laufzeitanalyse generiert wird. Dieses Kapitel widmet sich zunächst der rein statischen Analyse, Kapitel geht auf Constraintregeln ein, die sich eine Laufzeitanalyse zunu e
machen. Weiterhin hat sich der Autor dieser Arbeit entschlossen, die Constraintregeln dieses Kapitels in fünf Schwerpunktkategorien zu gruppieren, von denen sich vier aus den im
vorherigen Kapitel eingeführten Variablenarten ergeben: Orte (Abschi . ), Zugrei arkeiten
(Abschi . ), Typen (Abschi . und . ) und Namen (Abschi . ). Ganz eindeutig ist diese
Gruppierung nicht, so hängt zum Beispiel die Berechnung der Zugrei arkeit einer Deklaration von dem Ort ihrer Deklaration ab und der Typ einer Deklaration geht le tendlich auf
den verwendeten Namen der ihr vorangestellten Typreferenz zurück. Regeln, bei denen sich
partout keine Tendenz zu einer der vier Kategorien ausmachen lässt, sind vor allem solche,
die verhindern, dass durch gemeinsames Zusammenwirken von Ort, Name, Zugrei arkeit
und Typ einer Deklaration unerwünschte Bindungen oder Überschreibungsbeziehungen neu
entstehen. Diese sind in einem eigenen Abschni . ausgegliedert. Für den Leser dieser Arbeit ergibt sich wegen der Unabhängigkeit der Constraintregeln voneinander die glückliche
Situation, sich die Constraintregeln nach eigenen Wünschen erschließen zu können. Die Abschni e . bis . bauen nur lose aufeinander auf und können entsprechend in beliebiger
Reihung gelesen werden.
Bevor es an die erste, in Refacola formulierte Constraintregel geht, ein weiterer Hinweis. Der
Autor dieser Arbeit hat es weder als hilfreich empfunden, innerhalb dieses und des kommenden Kapitels jede der implementierten Constraintregeln vollständig aufzulisten, noch bei den
Ausführungen auf den Abdruck von Constraintregeln vollkommen zu verzichten und lediglich auf Seiten- oder Zeilennummern innerhalb von Anhang B zu verweisen, welcher ohnehin
noch einmal den gesamten Sa der Constraintregeln auflistet. Vielmehr kommt eine Mischform zum Einsa , welche die besprochenen Constraintregeln – wo sinnvoll – entweder der
Verständlichkeit halber innerhalb dieses Kapitels abdruckt, oder – insbesondere bei starker
Ähnlichkeit zu vorhergehend erläuterten Regeln – auf Anhang B verweist. Referenzen auf im
Anhang zu findende Constraintregeln geschehen jeweils anhand des (eindeutigen) Regelnamens und der jeweiligen Seitennummer mit einem vorangestellten Listenseiten-Piktogramm,
)“.²¹ Darüber hinaus sind auch alle in diesem Kazum Beispiel „LocalClassAccessibility ( S.
²¹ Leser der digitalen Version dieser Arbeit dürfen sich über in diesen Referenzen verborgene Links freuen, mit
. Constraintregeln für Java
pitel abgedruckten Constraintregeln in Anhang B zu finden und dort in jedem Falle anhand
der dort geltenden alphabetischen Sortierung der Regeln nach Regelnamen einfach auffindbar.
Gewinnung von Constraintregeln Für die Gewinnung von Constraintregeln für Refaktorisierungswerkzeuge existiert kein gradliniges Verfahren, mit welchem sich systematisch
ein vollständiger Sa von Constraintregeln erschließen ließe. Grundlage der Gewinnung von
Constraintregeln für eine Programmiersprache ist die jeweilig zugrundeliegende Sprachspezifikation. Für die vorliegende Arbeit geht somit die Suche nach Constraintregeln von der
Java-Sprachpezifikation [GJSB ] aus. Ein systematisches Durcharbeiten führt direkt zu einer
Vielzahl benötigter Constraintregeln, so besagt zum Beispiel die Java-Sprachspezifikation
“It is a compile-time error for a private method to be declared abstract.” (
§ . . . )
Entsprechend wurde die Constraintregel AbstractMethodAccessibility ( S.
) implementiert,
welche dies eins zu eins umse t, indem abstrakten Methoden untersagt wird, einen privateModifizierer anzunehmen.
Dennoch führt ein systematisches, händisches Durcharbeiten der Sprachspezifikation (wie
es unter anderem im Rahmen dieser Arbeit erfolgte) nicht zwangsläufig zu einem vollständigen Regelsa . So geschieht es leicht, dass unter der Menge der in der Java-Sprachspezifikation
genannten Eigenschaften der Sprache bei Einzelnen verkannt wird, welche Auswirkungen sie
in Bezug auf Refaktorisierungen haben können. So weist zum Beispiel in Ergänzung zu den
gut ausgeführten Erklärungen zur Methodenüberschreibung ( § . . . ) ein paar Abschnitte später ( § . . . ) nur ein einzelner Sa innerhalb einer Fallunterscheidung mit unterschiedlichen Sonderfällen darauf hin, dass von einer Methodenüberschreibung auch in einem
solchen Sonderfall gesprochen wird, in dem eine Klasse Methoden mit identischer Signatur
sowohl aus einer Superklasse als auch einem Interface erbt. Die Folge ist, dass eine Reihe von
Constraintregeln, die für Methodenüberschreibung gelten, entsprechend für diesen Sonderfall ergänzt werden müssen.²²
Weiterhin gibt es auch indirekte Bedingungen, die der Sprachspezifikation nicht unmi elbar zu entnehmen sind, sondern die sich vielmehr aus den möglichen Programmtransformationen ergeben. Insbesondere betrifft dies einige technische Regeln, wie zum Beispiel Update). Diese besagt, dass beim Verschieben einer Deklaration auch alle in
ReferenceOwner ( S.
dieser vorkommenden Referenzen derselben Ortsveränderung unterworfen sind.
Aus diesen Gründen sind Tests wesentlicher Bestandteil bei der Entwicklung korrekter Regelsä e für constraintbasierte Refaktorisierungen, während derer die Constraintregelmenge laufend ergänzt und korrigiert wird. Dies können einerseits klassische Regressionstests
sein, bei denen jeweils für eine einzelne Refaktorisierung die constraintbasiert durchgeführte Programmtransformation mit einem vorgegebenen Erwartungswert verglichen wird. Andererseits können es aber auch automatisierte Tests sein, bei denen beispielsweise für ein
Eingabeprogramm zunächst alle Constraints generiert werden und anschließend jede durch
den Constraintlöser gefundene Programmtransformation gemäß ihrer Variablenbelegungen
denen sich direkt zur jeweiligen Regel navigieren lässt.
²² Konkret entstanden die Constraintregeln EagerInterfaceImplementationAccessibility (siehe S.
S.
), EagerInterfaceMethodNames ( S.
), EagerInterfaceImplementationParameterType ( S.
EagerInterfaceImplementationReturnType ( S.
).
, ebenso
) sowie
. . Orts-Constraints
in den Code zurückgeschrieben wird [Kre ]. Anhand automatisierter (zum Beispiel durch
den Compiler) oder händischer Inspektion kann die Gültigkeit der Refaktorisierungen und
damit der Constraintregeln für den Einzelfall untersucht werden. Kapitel liefert einen umfangreichen Überblick über die im Rahmen dieser Arbeit erfolgten abschließenden Tests, welche im Wesentlichen auf durchgeführten Refaktorisierungen auf real verwendeten, quelloffenen Programmen au auen und die Industrietauglichkeit der implementierten Refaktorisierungswerkzeuge nachweisen sollen. Während der laufenden Entwicklung der Constraintregeln bieten sich nach der Erfahrung des Autors zum Testen hingegen eher kleine Programmausschni e an, welche nach Möglichkeit die unterschiedlichen Funktionen und Eigenschaften der Sprache Java in hoher Dichte nu en. Den wertvollsten Hinweis, wie so ein Proband
unkompliziert zu gewinnen ist, verdankt der Autor dieser Arbeit den von ihm betreuten Studierenden. Im Rahmen eines Fachpraktikums galt es, ein Werkzeug für das automatisierte
Testen von Refaktorisierungswerkzeugen (im Stile von [TS ] und [EH ]) zu entwickeln
und in einem We bewerb unter den Studierenden für ein gegebenes Refaktorisierungswerkzeug die meisten Fehler für dieses zu finden. Große Erfolge konnte dabei eine Gruppe verzeichnen, indem sie ihr Werkzeug mit Code aus gelösten Programmieraufgaben zu der im
Bachelor-Studiengang obligatorischen Einführungsvorlesung zu objektorientierter Programmierung und Java fü erten. Dieser Code schöpfte naturgemäß die Mi el der Programmiersprache Java weitestgehend und in hoher Dichte aus.
5.1. Orts-Constraints
In Java lässt sich der Ort einer Deklaration anhand seiner umschließenden Deklarationen beschreiben. Teilweise ist dies in Java schon durch die voll qualifizierten Namen (engl. fully qualified
name) und kanonischen Namen (engl. canonical name) ( § . ) vorgegeben, mit denen eine Deklaration neben ihrem Namen auch anhand der Namen der umschließenden Deklarationen
identifiziert werden kann. Auf diesem Prinzip au auend werden variable Orte umgese t,
indem jede potentiell verschiebbare Deklaration eine Variable für ihre jeweils nächst äußere
Deklaration erhält.
Insgesamt werden drei verschiedene Arten von Orten unterstü t:
• Container für Klassen TypeRoot (entweder Überse ungseinheiten in Form von *.javaoder *.class-Dateien) haben eine Variable hostPackage für das jeweils umschließende Paket.
• Toplevel-Typen TopLevelType, (also nicht in andere Typen geschachtelte Typen) haben
eine Variable typeRoot, die die jeweils umschließende Überse ungseinheit angibt.
• Member, also Membertypen, Felder und Methoden haben eine Variable owner, die den
jeweils umschließenden Typen angibt. Dies kann gegebenenfalls auch ein Toplevel-Typ
sein.
Bei Referenzen wird jeweils danach unterschieden, ob sie innerhalb oder außerhalb eines Typen auftreten. Typreferenzen können dabei außerhalb eines Typen zum Beispiel in einem Import oder in einer extends-Klausel auftreten. In ersterem Fall erhalten sie eine Variable für das
umschließende Paket, im zweiten Falle eine Variable für die umschließende Klasse.
. Constraintregeln für Java
Das Law of Demeter in Refacola Obwohl jede Deklaration nach obiger Festlegung jeweils
nur eine Variable für die nächst umschließende Deklaration hat, lässt sich davon ausgehend
ebenso die Menge aller umschließenden Deklarationen ermi eln, indem die umschließenden
Deklarationen wiederum auf ihre umschließenden Deklarationen hin befragt werden. Hierbei kommt es aber zu einem technischen Problem: die deklarative Natur der Refacola erlaubt
nicht, in einer Schleife über alle umschließenden Deklarationen zu iterieren, bis beispielsweise das umschließende Paket gefunden ist. Auch bietet sie keine Möglichkeit, ähnlich wie in
Java per instanceof, nach der Programmelementart einer Regelvariablen zu fragen und gegebenenfalls einen Typ-Cast durchzuführen. Dies wäre beim Iterieren aber nötig, um zu prüfen,
ob ein owner ein Topleveltyp ist oder ein weiterer Membertyp, der zunächst noch weiterhin
nach seinem umschließenden Typen befragt werden muss. Doch selbst wenn le teres möglich wäre, mutet es auch nicht als sonderlich elegant an, dort, wo die Iterationstiefe bekannt
ist, lange Verke ungen von Indirektionen zu bilden. Soll in einer Constraintregel das Paket
einer Methode in einem Topleveltypen referenziert werden, wäre dies mit einem Aufruf der
Art
315
method.tlowner.typeroot.hostPackage
verbunden, was in objektorientierten Sprachen als ein klarer Verstoß gegen das Gese Demeters [LHR ] zu werten wäre. In Kurzform besagt dieses Gese , dass innerhalb einer Methode
nur solche Methoden aufgerufen werden sollen, die auf den dort unmi elbar verfügbaren Objekten definiert sind. Verke ete Aufrufe sind damit Tabu. Die Vorteile liegen auf der Hand,
so wirken sich beispielsweise Designänderungen wie geänderte Schni stellen jeweils nur innerhalb eines Typen und seinen unmi elbaren Verwendern, nicht darüber hinaus aus. Auf
die Refacola trifft dieses Gese ganz ähnlich zu. Würde man sich entschließen, eine TypeRoot
nicht mehr als variabel zu betrachten (sodass dann Topleveltypen direkt eine Variable fürs
umschließende Paket hä en), müsste jede Constraintregel angefasst werden, die das mögliche Paket einer Methode beschränken soll – obwohl hierfür die jeweiligen TypeRoots gar keine
Rolle spielen.
Die Lösung aus diesem Dilemma ist einfach: man gibt jeder Deklaration zusä liche Variablen nicht nur für die jeweils umschließende Deklaration, sondern auch für weitere relevante
umschließende Deklarationen. Der Preis dafür ist derselbe wie bei einer Einhaltung des Gese
Demeters in der Objektorientierung, nämlich die Einführung der zusä lichen Variablen. So
erhält ein Member zusä lich zur ohnehin benötigten Variable owner auch Variablen tlowner
und hostPackage, die jeweils den umschließenden Topleveltypen und das umschließende Paket benennen. Somit kann obiger Aufruf des Pakets einer Methode in Zeile
durch
316
method.hostPackage
erse t werden. Konsequenterweise muss dann aber auch dafür gesorgt werden, dass diese zusä lich eingefügten, sozusagen redundanten Constraintvariablen im Einklang schwingen. So
dürfen die in Zeile
und
angegebenen Variablen niemals unterschiedliche Werte annehmen. Am einfachsten wird dies mi els zusä licher Constraints erreicht. So schreibt die folgende Constraintregel MemberOrConstructorTlOwnerAndHostPackage vor, dass sich der tlOwner
und hostPackage jeweils aus den jeweiligen Variablenwerten des umschließenden owner ergeben.
317
MemberOrConstructorTlOwnerAndHostPackage
. . Orts-Constraints
318
319
320
321
322
323
324
325
326
for all
d: Java.MemberOrConstructor
do
if
all(d)
then
d.tlowner = d.owner.tlowner,
d.hostPackage = d.owner.hostPackage
end
Ganz analog sorgen die Regeln TopLevelTypeHostPackage ( S.
), UpdateReferenceHostPackage ( S.
), UpdateReferenceTLOwner ( S.
) und UpdateReferenceOwner ( S.
)
dafür, dass auch bei weiteren redundanten Orts-Variablen die jeweiligen Variablenwerte mit
den Orten der umschließenden Deklarationen übereinstimmen.
Über diese Regeln hinaus ergibt sich kein weiterer Bedarf für Constraintregeln, die (ausschließlich) Orte beschränken. Für sich genommen können Deklarationen in einem Programm
frei verschoben werden, Einschränkungen ergeben sich erst in Kombination mit weiteren Variablen. So müssen beim Verschieben von benannten Deklarationen sowohl Namenskonflikte
als auch Zugrei arkeiten berücksichtigt werden. Beides wird jeweils im Detail in den jeweilig
noch folgenden Unterabschni en behandelt.
Weiterhin sind auch keine Constraints nötig, die verhindern, dass eine Deklaration an einen
Ort verschoben wird, an dem dies von der Syntax der Sprache her nicht gesta et ist. So erlaubt Java zum Beispiel nicht, eine Methode außerhalb eines Typen zu deklarieren. Derartige
Fehler werden aber schon durch die Typisierung der Refacola und die sich daraus ergebenden
entsprechend eingeschränkten programmabhängigen Domänen vermieden. So ist die Domäne der Variable owner zum Beispiel schon auf [ClassOrInterfaceType] eingeschränkt. Es ist also
sichergestellt, dass eine Methode weder in einen Primitivtypen, noch direkt in eine TypeRoot
verschoben werden kann.
Orts-Variablen für umschließende Blöcke Im Rahmen dieser Arbeit nicht umgese t werden Variablen für umschließende Methoden, Initializer oder andere Blöcke. Diese variabel zu
halten würde bedeuten, dass ein Refaktorisierungswerkzeug eigenständig während der Constraintlösung entscheiden dürfte, einzelne Anweisungen von einer Methode in eine andere zu
bewegen. Für sich genommen klingt diese Idee verlockend, könnte damit ein Werkzeug doch
von sich aus entscheiden, zum Beispiel bei einer E
M
-Refaktorisierung weitere
Anweisungen zusä lich in die neue Methode zu verschieben, um gegebenenfalls die Anzahl
nötiger Methodenparameter für diese zu reduzieren. Umgekehrt sind beim Verschieben einer Anweisung über Blöcke hinweg zahlreiche Bedingungen zu berücksichtigen, um einen
im Programmverhalten unveränderten Kontroll- und Datenfluss zu gewährleisten. Imperativ wurden Kontroll- und Datenflussanalysen für Refaktorisierungen bereits durch Schäfer et
al. [SVEM ] umgese t. Hinweise darauf, wie die Behandlung von Kontroll- und Datenfluss
möglicherweise auch constraintbasiert umgese t werden kann, gab bereits Abschni . .
Ebenso von einer Analyse des Kontrollflusses abhängig ist die korrekte Behandlung finaler Felder in Java. Für diese gilt, dass diesen definitiv ein Wert zugewiesen werden muss
( § . . . ). Diese Bedingung kann verle t werden, wenn ein Feld verschoben wird und
es nicht innerhalb seiner Deklaration, sondern beispielsweise in einem Konstruktor oder In-
. Constraintregeln für Java
itializer ( § . ) mit seinem Wert belegt wird. Dann sind nämlich Kontrollflüsse möglich, die
das Feld referenzieren und gleichzeitig keine Initialisierung des Feldes beinhalten, weshalb
bereits der Compiler einen Fehler meldet. Um derartige Situationen auszuschließen zu können, werden in der derzeitigen Implementierung nicht initialisierte, finale Felder bereits bei
der Faktengenerierung als in ihrem Ort (owner) unveränderlich gekennzeichnet.
5.2. Zugreifbarkeits-Constraints
Die Sprache Java erlaubt Programmierern mi els Zugrei arkeiten den Zugriff auf Klassen oder
deren Member von anderen Klassen oder Paketen künstlich einzuschränken. Zugrei arkeit
ist eine statische Programmeigenschaft, die zum Zeitpunkt des Kompiliervorganges ausgewertet wird und die mi els der Zugrei arkeitsmodifizierer public, protected und private individuell für Klassen, Methoden, Konstruktoren und Felder festgelegt wird ( § . ). Eine
vierte Zugrei arkeitsstufe, die default-Zugrei arkeit, ergibt sich dadurch, dass keiner der übrigen Zugrei arkeitsmodifizierer verwendet wird ( § . . ). Die vier Zugrei arkeitsstufen
haben in Java folgende Bedeutung:
• Eine private-Deklaration ist nur innerhalb des Topleveltypen zugrei ar, in dem sie deklariert wurde.
• Eine default-zugrei are Deklaration ist innerhalb desselben Paketes zugrei ar.²³
• Eine protected-Deklaration ist
– innerhalb desselben Paketes zugrei ar und
– innerhalb der Subtypen der deklarierenden Klasse zugrei ar, wenn die Aufrufstelle für die Implementierung von Subklassen-Objekten verantwortlich ist.
• Eine public-Deklaration ist im gesamten Programm zugrei ar.
Während die Definitionen für private-, public- und default-Zugrei arkeiten noch recht eingängig sind, gestalten sich die Zugrei arkeitsregeln für paketübergreifende Zugriffe auf Member mit protected-Modifizierer einigermaßen unintuitiv, insbesondere bei der Frage, wie es
um die Verantwortlichkeit für die Implementierung eines Subtyp-Objekts steht. Folgendes Beispiel
erläutert die zu unterscheidenden Fälle anschaulich:
327
package a;
328
329
330
331
332
public class A {
protected A() {}
protected void m() {}
protected class X {}
333
protected static void n(){}
334
335
}
336
337
338
package b;
public class B extends a.A {
²³ Es existieren einzelne Ausnahmen, die im folgenden Abschni thematisiert werden.
. . Zugrei arkeits-Constraints
B(a.A a) {
super(); // zugreifbar
new a.A(); // nicht zugreifbar
:::::::::
new a.A() {}; // zugreifbar
}
339
340
341
342
343
344
void x(a.A a, B b) {
this.m(); // zugreifbar
super.m(); // zugreifbar
a.m(); // nicht zugreifbar
:::::
b.m(); // zugreifbar
345
346
347
348
349
350
X x; // zugreifbar
a.A.X y; // zugreifbar
B.X z; // zugreifbar
351
352
353
354
n(); // zugreifbar
355
}
356
357
}
Der Zugriff auf den protected-Konstruktor der Superklasse A in Zeile
wird über einen
super()-Aufruf gesta et, da dieser unmi elbar der Instanziierung eines Subtyp-Objekts dient.
Die Instanziierung über new A(); in der folgenden Zeile
schlägt hingegen fehl, da das hieraus erzeugte Objekt nicht in Kontext des Subtyps B steht (es ist für den Aufruf unerheblich, ob
B Subtyp von A ist). Befremdlich wirkt hingegen, dass der im Endeffekt nahezu ergebnisgleibei Angabe einer (leeren) anonymen Klasse wiederum erfolgreich ist.
che Aufruf in Zeile
Dieser steht zwar ebenso nicht in Kontext der Klasse B, dafür aber in Kontext der damit deklarierten anonymen Klasse, die dann ihrerseits den Konstruktor von A in Form eines erlaubten
) aufruft.
(impliziten) super()-Aufrufs (ähnlich wie in Zeile
Ähnlich ist das Verhalten für Methoden. Die Aufrufe in den Zeilen
und
sind zulässig,
da diese die Subtypbeziehung von A und B vorausse en. Der Aufruf von m auf einem Objekt
hingegen wird durch den Compiler nicht gesta et, da dieser nicht
vom Typ A in Zeile
die Subtypbeziehung von A und B vorausse t. Erst beim Aufruf auf einem Objekt vom Typ B
wie in Zeile
ist der Zugriff erlaubt. protected-Zugrei arkeit für Felder gehorcht denselben
Regeln wie die für Methoden, im obigen Beispiel könnte man die Methode m durch ein Feld
i erse en und würde dieselben Einschränkungen bezüglich der Zugrei arkeit erhalten.
Ohne weitere Einschränkungen hingegen kommen Zugriffe auf als protected deklarierte
Membertypen und static-Member aus. Es ist völlig unerheblich, wie auf den Typen X der Superklasse A in den Zeilen
bis
zugegriffen wird. Dieser ist zugrei ar und auch der
Zugriff auf die statische Methode in Zeile
gelingt.
Betrachtet man die Zugrei arkeiten in Java in ihrer Gänze, fällt auf, dass sich sowohl die
Zugrei arkeitsmodifizierer als auch die Zugrei arkeitsregeln in Java nur auf Klassen und
deren Member beziehen, nicht auf einzelne Objekte. So gilt zum Beispiel, dass bei Deklaration einer Methode die Zugrei arkeit dieser Methode für alle Objekte dieses Typs identisch ist.
Andererseits gilt auch, dass bei Zugriff auf eine solche Methode nicht unterschieden wird, welches konkrete Objekt der Empfänger des Aufrufs ist – entscheidend ist allein der deklarierte
Typ des Empfängers. Andere Programmiersprachen erlauben auch Zugrei arkeiten auf Ob-
. Constraintregeln für Java
jektebene, zum Beispiel Ruby, wo private Zugrei arkeit den Zugriff auf das jeweilige Objekt
einschränkt, also jegliche (nicht implizit mit this) qualifizierte Aufrufe unterbunden werden.
Ebenso ist es in Ruby möglich, eine Methode für ein einzelnes Objekt zu (re-)definieren, womit
Zugrei arkeiten auch auf Objektebene unterschieden werden können.
5.2.1. Zugreifbarkeit als Constraintvariable
Innerhalb der Refacola-Sprachdefinition für Java wird die Zugrei arkeit über eine einheitliche Variablenart accessibility der abstrakten Programmelementart AccessibleEntity modelliert.
Die Domäne der Zugrei arkeiten umfasst entsprechend der obigen Auflistung die vier Zugrei arkeitsstufen, wobei die default-Zugrei arkeit eine Spezialbehandlung erfährt, um zwei
Ausnahmen zu behandeln: Member von Interfaces ohne Zugrei arkeitsmodifizierer erhalten
während des Kompiliervorganges einen impliziten public-Modifizierer ( § . ) und Konstruktoren von Enums sind auch ohne einen Zugrei arkeitsmodifizierer stets private ( § . ).
Um Komplexität in den Constraintregeln zu reduzieren, werden diese impliziten Modifizierer
bei der Faktengenerierung ganz analog zum Verhalten des Java-Compilers hinzugefügt und
beim Zurückschreiben gegebenenfalls wieder entfernt. Um diesen Umstand deutlich zu machen, wird die um diese beiden Sonderfälle bereinigte default-Zugrei arkeit in der Refacola
als #package-Zugrei arkeit bezeichnet. Die übrigen Zugrei arkeitsstufen sind analog zu den
entsprechenden Schlüsselwörtern in Java #public, #protected und #private benannt.
Da sich der Zugrei arkeitsbereich einer Deklaration von private bis hin zu public jeweils
nur erweitert, kann die Domäne der Zugrei arkeiten vollständig geordnet werden:
#public > #protected > #package > #private
Von dem abstrakten AccessibleEntity mit seiner Eigenschaft accessibility leiten Programmelementunterarten für alle Member (also Felder, Methoden und Membertypen) sowie Konstruktoren und alle Referenztypen (also keine Primitivtypen) ab. Die Entscheidung, in der
Sprachdefinition alle Referenztypen mit einem Zugrei arkeitsmodifizierer zu versehen, ist
nicht ganz unbedenklich, zählen doch zu den Referenztypen auch anonyme und lokale Klassen, welche in Java keine Zugrei arkeiten aufweisen ( §§ . , . . ). In diesen beiden
Fällen dennoch einen Zugrei arkeitsmodifizierer einzuführen, erlaubt aber an anderen Stellen Vereinfachungen. So haben zum Beispiel Array-Typen eine Zugrei arkeit gleich ihrer
Elemen ypen ( § . . ), was durch die Regel ArrayTypeAccessibility ( S.
) umgese t
wird. Hä en lokale Klassen keine accessibility-Eigenschaft, müsste bei Array-Typen ebenso
zwischen solchen mit Zugrei arkeit und solchen ohne unterschieden werden. Diese Dualität
würde sich über weitere Regeln, die Array-Typen bedingen (gegebenenfalls auch nur mittels Überarten), auf weitere Programmelementarten übertragen, sodass sich die Menge der
Programmelementarten und Regeln unnötig erhöhen würde. Weiterhin sind in der Refacola
Unterarten für den Constraintlöser unveränderlich, sodass in der Folge auch unnötigerweise Programmtransformationen ausgeschlossen werden würden, die zum Beispiel im Rahmen
eines U S
W
P
die Bindung einer Typreferenz auf eine lokale Klasse
(ohne Zugrei arkeitsmodifizierer) durch eine Typbindung auf eine Toplevel-Klasse (mit Zugrei arkeitsmodifizierer) erse en.
Führt man hingegen die zusä lichen accessibility-Modifizierer für lokale und anonyme Klassen ein, sind die negativen Konsequenzen gering. Da in Java auf lokale und anonyme Klassen
. . Zugrei arkeits-Constraints
ohnehin nicht außerhalb des umschließenden Blocks zugegriffen werden kann, wird der Modifizierer durch keine Constraints bedingt und somit während der Constraintlösung unberührt bleiben. Lediglich bei der Faktengenerierung ist eine entsprechende Berücksichtigung
nötig, um einen zusä lichen Modifizierer einzufügen. Da der Java-Compiler intern für anonyme und lokale Klassen default-Zugrei arkeit vergibt (was sich leicht über Java-Reflection feststellen lässt), wird auch bei der Faktengenerierung der #package-Wert verwendet. Um zu verhindern, dass bei Auflistung aller möglichen Constraintlösungen eines Constraintsystems sich
dieser Wert ändert, sichern die beiden Regeln LocalClassAccessibility ( S.
) und AnonymousClassAccessibility ( S.
) zusä lich zu, dass dieser Wert unverändert bleibt.
5.2.2. Zugreifbarkeiten zur Zugriffskontolle
Der Haup weck der Zugrei arkeiten in Java ist eine Einschränkung der Orte, von denen
aus bestimmte Deklarationen referenziert werden können. Umgekehrt hat die Zugrei arkeit
damit Einfluss auf die Bindung von Referenzen, denn ob und an welche Deklaration eine
Referenz bindet, hängt davon ab, welche Deklarationen zugrei ar sind. Wie ein Zugrei arkeitsmodifizierer einer Deklaration zu wählen ist, um einer bestimmten Referenz Zugriff auf
die Deklaration zu gesta en, hängt dabei nicht nur von dem Ort der Referenz und Deklaration ab, sondern auch von der Art der referenzierten Deklaration. So wird, wie im Beispiel auf
Seite
gezeigt, die protected-Zugrei arkeit für Member-Typen anders definiert als für Methoden. Weiterhin schlägt sich in den zu definierenden Constraintregeln unmi elbar nieder,
dass wie in Abschni . beschrieben Typbindungen als variabel angesehen werden sollen,
während Methoden- und Feldbindungen durch den Constraintlöser unveränderlich bleiben.
Sowohl bei der Definition der Constraintregeln, als auch bei ihrer Erklärung in diesem Abschni werden die unterschiedlichen Arten von Deklarationen daher separat behandelt.
Typen Bei der Referenzierung eines Typen muss unterschieden werden, ob die Referenz innerhalb eines Typen oder außerhalb eines Typen erfolgt. Typreferenzen außerhalb von Typen
können in Java einerseits in import-Anweisungen ( § . ) und andererseits bei der Deklaration eines Topleveltypen bei der Angabe von Supertypen und Superinterfaces vorkommen. Für
Typreferenzen außerhalb von Typen gilt, dass sie nur Typen referenzieren dürfen, die mindestens default-zugrei ar sind ( §§ . . , . ). Insbesondere lassen sich private Membertypen
damit weder importieren noch als Supertypen eines Topleveltypen verwenden, selbst wenn
beide Typen in derselben Überse ungseinheit stehen. Bei Referenzierung von Typen in anderen Paketen ist darüber hinausgehend public-Zugrei arkeit nötig ( § . ). Folgende Regel
TypeAccessOutsideType deckt diese Bedingungen für extends- und implements-Klauseln ab:
358
359
360
361
362
363
364
365
366
367
TypeAccessOutsideType
for all
tr: Java.TypeReferenceInCU
type: Java.ClassOrInterfaceType
do
if
all(type), all(tr)
then
(tr.typeBinding = type)
−> (type.accessibility >= #package),
. Constraintregeln für Java
((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage))
−> (type.accessibility = #public)
368
369
370
end
Die Zugrei arkeit von Typen, die innerhalb von Import-Deklarationen referenziert werden,
wird über die analog formulierte Regel ImportAccessibility ( S.
) bedingt.
Steht eine Typreferenz innerhalb eines Typen und wird somit zum Beispiel bei der Deklaration einer Variablen oder eines Methodenparameters verwendet, ist zusä lich die protectedZugrei arkeit zu berücksichtigen. Die beinahe identische Regel TypeAccess ( S.
), die allerdings gemäß der Regelvariablen nur für Typreferenzen innerhalb von Typen Anwendung
findet, fordert daher von Typen aus fremden Paketen zunächst nur #protected-Zugrei arkeit.
Ob diese dann tatsächlich genügt, hängt davon ab, ob der referenzierte Typ in einem Supertypen deklariert ist. Dabei kann nicht nur der die Typreferenz unmi elbar umschließende Typ
der erbende sein, sondern auch alle weiteren die Typreferenz umschließenden Typen können diese Typreferenz erben und somit in der inneren Klasse zugrei ar sein. So gelingt in
folgendem Codebeispiel das Referenzieren der protected-Klasse InA in Zeile
wegen des
nicht geerbten Typen InA nicht. Die beiden Referenzen in den Zeilen
und
hingegen
sind zulässig, weil der Typ InA von A geerbt wird, wenn auch im le teren Fall nur durch die
umschließende Klasse C:
371
package a;
372
373
374
375
public class A {
protected class InA {}
}
376
377
378
379
380
381
382
383
384
385
386
package b;
class B {
a.A.InA
inA;
::::::
class C extends a.A {
a.A.InA inA;
class D {
a.A.InA inA;
}
}
}
Die folgende Regel MemberTypeAccess se t diese Bedingungen um. Dabei fragt die le te andBedingung die gesamte Sequenz enclosingTypes der umschließenden Typen ab und verlangt –
falls zu keinem von ihnen eine Subtyp-Beziehung besteht – in Ergänzung zur Regel TypeAccess
#public-Zugrei arkeit.
387
388
389
390
391
392
393
394
MemberTypeAccess
for all
tr: Java.TypeReferenceInType
referencingType: Java.TlOwnedType
type: Java.MemberType
do
if
all(type), all(tr), all(referencingType)
. . Zugrei arkeits-Constraints
395
then
(tr.typeBinding = type)
and (referencingType = tr.owner)
and (tr.hostPackage != type.hostPackage)
and Java.sub*!(referencingType.enclosingTypes, type.owner)
−> type.accessibility = #public
396
397
398
399
400
401
end
Konstruktoren Java kennt vier Möglichkeiten, einen Konstruktor aufzurufen. Das Beispiel
ab Seite
zeigte bereits drei der Möglichkeiten in den Zeilen
bis
, nämlich die Constructor Invocation ( § . . . ) mi els Schlüsselwort super, sowie die Class Instance Creation
Expression ( § . ) mit Schlüsselwort new, einmal als reguläre Instanziierung und einmal
als anonyme Klasse. Darüber hinausgehend kann eine Constructor Invocation auch über das
Schlüsselwort this einen Konstruktor derselben Klasse aufrufen ( § . . . ), woraus aber
bezüglich der Zugrei arkeit keine Einschränkungen folgen können, da Konstruktoren derselben Klasse immer zugrei ar sind ( § . . ).
Die Menge der Konstruktoraufrufe aller vier Arten sind unter einer gemeinsamen Überart
von Programmelementen ConstructorReference zusammengefasst. Für diese sichert die folgende Regel zu, dass referenzierte Konstruktoren auch zugrei ar sind:
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
ConstructorAccess
for all
r: Java.ConstructorReference
c: Java.Constructor
do
if
Java.binds(r, c)
then
(r.tlowner != c.tlowner)
−> (c.accessibility >= #package),
(r.hostPackage != c.hostPackage)
−> (c.accessibility >= #protected),
(r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner))
−> c.accessibility = #public
end
Wie aus dem Beispiel ab Seite
hervorgegangen, gelten bezüglich der protected-Zugreifbarkeit bei Konstruktoraufrufen mi els new von nicht-anonymen Klassen strengere Regeln.
Die nachfolgende Regel ClassInstanceCreationAccess fordert daher ergänzend zu ConstructorAccess, dass für nicht-anonyme Class Instance Creation Expressions über Paketgrenzen hinweg
der aufgerufene Konstruktor als public deklariert sein muss:
417
418
419
420
421
422
423
ClassInstanceCreationAccess
for all
r: Java.ClassInstanceCreation
c: Java.Constructor
do
if
Java.binds(r, c)
. Constraintregeln für Java
424
425
426
427
then
(r.hostPackage != c.hostPackage)
−> (c.accessibility >= #public)
end
Wie schon im Rahmen des obigen Beispiels dargelegt, gilt diese Einschränkung nicht für
anonyme Klassen, da hier mit der anonymen Class Instance Creation Expression gleichzeitig ein
Subtyp erzeugt wird, von dem aus der Zugriff wieder gesta et ist. Dafür gibt es für diese
eine neue Einschränkung, nämlich dass bei einem anonymen Konstruktoraufruf der jeweils
passendste Superkonstruktor mit gleicher Parameteranzahl zugrei ar sein muss ( § . . ),
welcher aus dem impliziten Konstruktor der anonymen Klasse ( § . . . ) implizit mit
aufgerufen wird. Die folgende Regel sichert diese Bedingung zu:
428
429
430
431
432
433
434
435
436
437
438
439
440
AnonymousClassConstructorAccessibility
for all
constructor: Java.Constructor
anonymousConstructor: Java.AnonymousClassConstructor
do
if
Java.isAnonymousSuperConstructor(constructor, anonymousConstructor)
then
(constructor.tlowner != anonymousConstructor.tlowner)
−> (constructor.accessibility >= #package),
(constructor.hostPackage != anonymousConstructor.hostPackage)
−> (constructor.accessibility >= #protected)
end
Es genügt, nur nach #private-, #package- und #protected-Zugrei arkeit zu unterscheiden. Da
der Superkonstruktor zwingend in einer Superklasse liegt, reicht #protected-Zugrei arkeit in
jedem Fall.
Eine le te aus Konstruktoraufrufen resultierende Eigenschaft zur Zugrei arkeit bedingt
nicht die referenzierten Konstruktoren, sondern ihre umschließenden Typen. So erfordert ein
Konstruktoraufruf in Java stets auch, dass die instanziierte Klasse zugrei ar ist ( § . . ).
In folgendem Codebeispiel ist von der Klasse B aus der Konstruktor A() zwar zugrei ar, nicht
jedoch der Typ A, weswegen der Aufruf in Zeile
fehlschlägt.
441
package a;
442
443
444
445
class A {
public A() {}
}
446
447
package b;
448
449
450
451
public class B {
Object o = ::::
new ::::
a.A();
}
Die folgende Regel fordert bei Konstruktoraufrufen zusä lich auch die Zugrei arkeit des
den Konstruktor umschließenden Typen.
452
ConstructorAccessRequiresTypeAccessibility
. . Zugrei arkeits-Constraints
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
for all
r: Java.ConstructorReference
c: Java.Constructor
type: Java.AccessModifiableType
do
if
Java.binds(r, c), Java.instantiates(c,type)
then
(r.tlowner != c.tlowner)
−> (type.accessibility >= #package),
(r.hostPackage != c.hostPackage)
−> (type.accessibility >= #protected),
(r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner))
−> type.accessibility = #public
end
Für die ganz ähnlich gelagerten Konstruktoraufrufe im Rahmen der Instanziierung anonymer
Klassen greift die Regel AnonymousClassRequiresSuperTypeAccessibility ( S.
).
Methoden und Felder Während bei Typen und Konstruktoren verschiedene Arten des Zugriffs unterschieden wurden (Typreferenzen innerhalb eines Typen oder außerhalb beziehungsweise Referenzen auf Konstruktoren als Superkonstruktoraufruf oder zur Objektinstanziierung) entfällt eine solche Unterscheidung beim Aufruf von Methoden und dem Zugriff auf
Felder, da diese prinzipiell nur innerhalb von Klassen und nur durch Angabe des Methodenoder Feldnamens referenziert werden.
Wie schon bei den Zugriffsregeln für Membertypen gesehen, kann auch bei Methodenaufrufen und Feldzugriffen die Prüfung auf ausreichende Zugrei arkeit in zwei Regeln aufgeteilt werden. Die Regel MethodOrFieldAccess1 ( S.
) prüft zunächst analog zu TypeAccess
( S.
), ob mindestens #package- oder #protected-Zugrei arkeit vonnöten ist. Ergänzend
) – analog zu MemberTypefordern Constraints aus der Regel MethodOrFieldAccess2 ( S.
Access (siehe S. , ebenso S.
) – dass #public-Zugrei arkeit gegeben sein muss, soweit das
referenzierte Member nicht aus einer Superklasse oder der Superklasse einer umschließenden
Klasse entspringt.
Ähnlich den Konstruktoren und durch die Regel ConstructorAccessRequiresTypeAccessibility
(siehe S. , ebenso S.
) sichergestellt, ist es auch bei Methodenaufrufen und Feldzugriffen so, dass der jeweilig deklarierende Typ des referenzierten Members zugrei ar sein
muss. Während bei Konstruktoren dies noch halbwegs nachvollziehbar ist – schließlich deckt
sich der Name des Typen mit dem Namen des Konstruktors, womit eine Konstruktorreferenz beinahe gleichzeitig als eine Art Typreferenz gesehen werden kann –, wirkt diese Einschränkung bei Methoden und Feldreferenzen wenig eingängig. In folgendem Beispiel verden Zugriff auf die Methode m. Zwar ist diese als
weigert der Java-Compiler in Zeile
public-Deklaration in B zugrei ar, nicht aber ihr nur default-zugrei arer umschließender Typ
A.
468
469
package a;
class A {public void m() {}}
470
471
public class AFactory {
. Constraintregeln für Java
public A createA() {
return new A();
}
472
473
474
475
}
476
477
478
479
480
package b;
class B {
{new a.AFactory().createA().m();}
::::::::::::::::::::::
}
Die Constraintregeln MethodOrFieldAccessRequiresTypeAccessibility1 ( S.
) und MethodOrFieldAccessRequiresTypeAccessibility2 ( S.
) sichern zu, dass bei Zugriff auf Methoden oder
Felder auch der jeweilig umschließende Typ zugrei ar ist. Analog zu den getrennten Zugrei arkeitsregeln bei Typreferenzen zu Topleveltypen und Membertypen behandelt hierbei
die erste Regel Topleveltypen, die zweite Regel Membertypen. Andere Programmiersprachen
kennen übrigens ähnliche Restriktionen, was die Zugrei arkeit von Klassen bei Zugriff auf
deren Member angeht, diese manifestieren sich dann aber mitunter an anderer Stelle. So verbietet C# generell, bei geschachtelten Deklarationen die Zugrei arkeit im Inneren höher zu
deklarieren als im Äußeren [HWG , § . . ], womit im obigen Beispiel schon die Deklaration
der public-Methode m innerhalb der default-Klasse C zu einem Kompilierfehler geführt hä e.
gesehen, gelten auch für Methodenaufrufe (und FeldzuWie schon im Beispiel ab Seite
griffe analog) besondere Regeln, was die protected-Zugrei arkeit angeht. Bei Aufruf einer
solchen Methode aus einem anderen Paket muss das Empfängerobjekt des Methodenaufrufs
oder Feldzugriffs mindestens vom Typ der den Methodenaufruf umschließenden Klasse sein.
Die folgende Regel nimmt diese Überprüfung vor und zwingt das referenzierte Member gegebenenfalls, public-Zugrei arkeit anzunehmen.
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
ProtectedMethodOrFieldAccess
for all
reference: Java.MethodOrFieldReference
declaration: Java.InstanceMember
receiver: Java.ClassOrInterfaceTypedExpression
do
if
Java.binds(reference,declaration),
Java.receiver(reference,receiver)
then
((reference.hostPackage != declaration.hostPackage)
and (not Java.sub*(receiver.expressionType,reference.owner))
and (not Java.isThisOrSuperExpression(receiver)))
−> declaration.accessibility = #public
end
5.2.3. Zugreifbarkeit und Vererbung
Die Zugrei arkeitsstufe einer Member-Deklaration hat nicht nur Einfluss darauf, ob diese
von einer bestimmten Referenz aus zugrei ar ist, sondern bestimmt auch, ob sie an ihre Subtypen vererbt wird. So besteht die Menge der Member einer Klasse neben den selbst deklarierten Membern aus einer Teilmenge der Member ihrer unmi elbaren Superklasse und ih-
. . Zugrei arkeits-Constraints
ren unmi elbaren Superinterfaces ( § . . ). Dabei werden zur Vererbung nur jene Member
ausgewählt, die mindestens protected-Zugrei arkeit haben oder paketweit zugrei ar und
im gleichen Paket deklariert sind ( § . ). private-Member werden nie vererbt. Insgesamt
kann sich damit die Situation ergeben, dass ein Member zwar von einer Aufrufestelle aus zugrei ar ist, aber nicht an das Objekt vererbt wurde, über das auf dieses zugegriffen werden
soll, wie das folgende Beispiel demonstriert:
496
497
498
499
package a;
public class A {
void m() {}
private void n() {}
500
class D extends A {
{
new A().n();
new D().::
n();
}
}
501
502
503
504
505
506
507
}
508
509
510
511
512
513
514
class C extends b.B {
{
new A().m();
new C().:::
m();
}
}
515
516
517
package b;
public class B extends a.A {}
Innerhalb der Klasse D ist die private Methode n aus A zugrei ar, da sie innerhalb desselben
wird daher gestatTopleveltypen deklariert ist. Der Zugriff auf diese Methode in Zeile
tet. Da private Methoden aber nicht vererbt werden, ist n kein Member von D, womit der
Zugriff auf diese Methode auf einer Instanz von D in der folgenden Zeile
fehlschlägt.
Tückisch hierbei ist, dass die Zugrei arkeitsregeln für private Member bei Vererbung und
Aufruf unterschiedliche sind. Während Vererbung bei privaten Membern ausgeschlossen ist,
ist ein Aufruf immerhin noch innerhalb desselben Topleveltypen gesta et.
Ein weiteres Beispiel liefert der Aufruf der Methode m aus A innerhalb von C. Diese paketweit zugrei are Methode ist in C zugrei ar, da sie ebenso wie B im Paket a deklariert ist.
Allerdings wird sie nicht an den Typen B außerhalb von a vererbt. Daher darf m in C zwar in
Zeile
auf einer Instanz von A aufgerufen werden, nicht aber in Zeile
auf einer Instanz
von C.
Die folgende Regel InheritedMemberAccessibility se t diese Bedingungen um.
518
519
520
521
522
523
InheritedMemberAccessibility
for all
reference: Java.MethodOrFieldReference
declaration: Java.Member
receiver: Java.ClassOrInterfaceTypedExpression
inheritingType: Java.ClassOrInterfaceType
. Constraintregeln für Java
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
do
if
Java.binds(reference,declaration),
Java.receiver(reference,receiver),
all(inheritingType)
then
((Java.sub(inheritingType, declaration.owner))
and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType))
and (inheritingType.tlowner != reference.tlowner))
−> (declaration.accessibility >= #package),
((Java.sub(inheritingType, declaration.owner))
and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType))
and (inheritingType.hostPackage != reference.hostPackage))
−> (declaration.accessibility >= #protected)
end
Während InheritedMemberAccessibility dem Problem begegnet, dass ein Member aufgerufen
wird, welches an seinen Empfänger nicht vererbt wurde, gibt es auch noch die gegenteilige Situation, dass ein Empfänger gleich mehrere Member gleichen Namens erbt – der Aufruf wird
mehrdeutig. In folgendem Beispiel erbt die Klasse B gleichnamige Member i und X sowohl
aus ihrer Superklasse A als auch ihrem Superinterface I.
539
540
541
542
class A {
int i = 0;
class X {}
}
543
544
545
546
547
interface I {
int i = 1;
class X{}
}
548
549
550
551
552
553
554
555
556
class B extends A implements I {
X: x;
A.X y;
I.X z;
int j = :i ;
int k = super.i;
int l = I.i;
}
Das Erben gleichnamiger Member führt für sich genommen noch zu keinem Fehler, erst beim
und
kommt es zum Fehler
(unqualifizierten) Aufruf dieser Member in den Zeilen
( § . . ). Wird ein Membertyp hingegen anhand seines umschließenden Typen qualifiziert oder ein geerbtes statisches Feld über seinen deklarierenden Typ oder ein nicht-statisches
Feld über super qualifiziert, gelingt der Zugriff. Zwar sind Constraintregeln denkbar, die in
diesen Fällen unqualifizierte und damit mehrdeutige Referenzen verhindern und damit gegebenenfalls die Refaktorisierung ablehnen, eher wünschenswert ist hingegen, durch Einfügen
geeigneter Qualifizierungen derartige Mehrdeutigkeiten aufzulösen. Abschni . hat ab Seite
bereits erläutert, dass dies innerhalb dieser Arbeit in einem nicht-constraintbasierten Nach-
. . Zugrei arkeits-Constraints
behandlungsschri erfolgt, entsprechende Constraintregeln sind also nicht zu implementieren.
5.2.4. Zugreifbarkeit bei Methodenüberschreibung und -verdeckung
Ganz ähnlich zu der Vererbung hat die Zugrei arkeit von Methoden ebenso Einfluss auf Methodenüberschreibung. So kann in Java eine Methode nur dann überschrieben werden, wenn
sie mindestens protected-zugrei ar ist oder paketweite Zugrei arkeit hat und innerhalb desselben Pakets definiert ist ( § . . . ). Obwohl dies auf den ersten Blick den Zugrei arkeitsregeln für Vererbung zu entsprechen scheint, trügt dieser Umstand in Wirklichkeit. Während
Vererbung nämlich in jeder Klasse entlang der Vererbungshierarchie die Zugrei arkeit der
jeweils geerbten Methode vorausse t, kann eine Methode auch dann überschrieben werden,
wenn sie innerhalb der Vererbungshierarchie zwischen überschriebener und überschreibender Methode nicht zugrei ar ist. Das führt zu der etwas absonderlichen Situation, dass eine
nicht geerbte Methode überschrieben werden kann:
557
558
559
560
package a;
public class A {
void m() {}
}
561
562
563
564
565
class C extends b.B {
@Override
void m() { super.m();
}
::
}
566
567
568
package b;
public class B extends a.A {}
In Zeile
kann innerhalb von C die Methode m aus A problemlos überschrieben werden.
Der Aufruf der super-Methode hingegen scheitert, weil diese durch den Umweg über B und
damit das fremde Paket b nicht geerbt wird.
Unabhängig davon, was von dieser Eigentümlichkeit in Java zu halten ist, sollte ein Refaktorisierungswerkzeug nicht ohne die bewusste Handhabe des Benu ers Überschreibungsbeziehungen au eben, indem es die Zugrei arkeit von überschriebenen Methoden reduziert.
Im folgenden Programm könnte die Überschreibungsbeziehung zwischen den beiden Methoden m in A und B aufgehoben werden, indem die überschriebene Methode m in A mit einem
private-Modifizierer versehen wird.
569
570
571
572
573
574
575
576
577
578
579
class A {
void m() { System.out.println("A"); }
public static void main(String[] args) {
A a = new A();
if( ... )
a = new B();
a.m();
}
}
class B extends A {
void m() { System.out.println("B"); }
. Constraintregeln für Java
580
}
Die folgende Regel sichert zu, dass durch Verringerung von Zugrei arkeit keine Überschreibungsbeziehungen während der Refaktorisierung gelöst werden.
581
582
583
584
585
586
587
588
589
590
591
592
OverridingAccessibility1
for all
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
(overriddenMethod.accessibility >= #protected)
or ((overriddenMethod.accessibility = #package)
and (overriddenMethod.hostPackage = overridingMethod.hostPackage))
end
Eine weitere Regel zur Zugrei arkeit überschreibender Methoden in Java besagt, dass eine
überschreibende Methode im Vergleich zur überschriebenen Methode mit mindestens gleicher Zugrei arkeit, nicht aber geringerer Zugrei arkeit deklariert werden darf ( § . . . ).
Die folgende Regel se t dies um:
593
594
595
596
597
598
599
600
601
602
OverridingAccessibility2
for all
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
overridingMethod.accessibility >= overriddenMethod.accessibility
end
Neben einer direkten Methodenüberschreibung gibt es ebenso den Fall, dass eine lediglich
geerbte Methode eine Interfacemethode überschreibt ( § . . . ) beziehungsweise implementiert. Folgender Code zeigt einen entsprechenden Fall.
603
604
605
606
607
608
609
interface I {
void m()
}
class A {
public void m() { }
}
class B extends A implements I { }
Die Methode m in Zeile
wird von der Klasse B geerbt und überschreibt dort die Methode
m aus Zeile
, ohne dass die deklarierenden Typen der beiden m-Methoden in einer Subtypbeziehung stehen. Für den Fall einer solchen – anschaulich gesprochen vorausschauenden –
Interfaceimplementierung, die in der Faktenbasis durch ein Fakt eagerInterfaceImplementation
modelliert wird, sorgt die folgende Regel EagerInterfaceImplementationAccessibility dafür, dass
die aus der Superklasse geerbte Methode im Vergleich zur Interfacemethode keine geringere
. . Zugrei arkeits-Constraints
Zugrei arkeit aufweist. Da Interfacemethoden stets als public deklariert sind, genügt es, nur
diesen Fall zu berücksichtigen.
610
611
612
613
614
615
616
617
618
619
620
EagerInterfaceImplementationAccessibility
for all
method: Java.InstanceMethod
interfaceMethod: Java.InstanceMethod
class: Java.Class
do
if
Java.eagerInterfaceImplementation(class, method, interfaceMethod)
then
method.accessibility = #public
end
Oben genannte Regel OverridingAccessibility2 zu überschreibenden Methoden hat ein Pendant bei sich verdeckenden Methoden ( § . . . ), also solchen statischen Methoden, die von ihrer Zugrei arkeit her in eine Subklasse vererbt werden, aber dort durch eine weitere statische
Methode mit gleicher Signatur erse t werden. Für diese gilt ebenso, dass die Zugrei arkeit
einer verdeckten Methode die Zugrei arkeit der verdeckenden Methode nicht übersteigen
darf, was über die Regel HidingMethodAccessibility sichergestellt wird:
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
HidingMethodAccessibility
for all
hidingMethod: Java.StaticMethod
hiddenMethod: Java.StaticMethod
do
if
all(hidingMethod), all(hiddenMethod)
then
((hidingMethod.identifier = hiddenMethod.identifier)
and Java.sub(hidingMethod.owner, hiddenMethod.owner)
and (hidingMethod.parameters.declaredParameterType
= hiddenMethod.parameters.declaredParameterType)
and ( ((hidingMethod.hostPackage = hiddenMethod.hostPackage)
and (hiddenMethod.accessibility = #package))
or (hiddenMethod.accessibility >= #protected))
−> (hidingMethod.accessibility >= hiddenMethod.accessibility))
end
5.2.5. Weitere Zugreifbarkeitsregeln
Neben den bereits genannten Constraintregeln zu Zugrei arkeiten, die sich aus Zugriffen,
Vererbung, Überschreibung und Verdeckung ergeben, hält die Java-Sprachpezifikation auch
noch eine Reihe weiterer, einfach zu prüfender Bedingungen bereit, die sich direkt aus den
jeweiligen, mit einem Zugrei arkeitsmodifizierer versehenden Deklarationen ergeben.
So ist bei der Zugrei arkeit von Typen zwischen Topleveltypen und geschachtelten Typen,
die innerhalb anderer Typen deklariert sind, zu unterscheiden. Im Gegensa zu geschachtelten Typen, die mit allen vier möglichen Zugrei arkeitsstufen versehen werden können, kom-
. Constraintregeln für Java
men für Topleveltypen nur zwei Zugrei arkeitsstufen in Frage: public und default (
was über die folgende Regel TopLevelTypeAccessibility zugesichert wird:
638
639
640
641
642
643
644
645
646
§ . ),
TopLevelTypeAccessibility
for all
tlt: Java.TopLevelType
do
if
all(tlt)
then
tlt.accessibility = #package or tlt.accessibility = #public
end
Array-Typen haben keine eigene Deklaration, also keinen Zugrei arkeitsmodifizierer. Dennoch haben sie eine Zugrei arkeit, welche der Zugrei arkeit ihres Basisdatentyps entspricht
( § . . ). Die Regel ArrayTypeAccessibility ( S.
) se t dies um.
Für abstrakte Methoden gilt, dass sie nicht gleichzeitig als private deklariert sein dürfen
( § . . . ). Die Regel AbstractMethodAccessibility ( S.
) sichert dies zu. Weiterhin gilt,
dass Interface-Member stets public deklariert sind ( § . . ). Die Regel InterfaceMember
( S.
) stellt dies sicher. Ganz analog definiert die Java-Sprachspezifikation, dass enumKonstruktoren stets private sein müssen ( § . . ) (gegebenenfalls auch implizit, falls kein
) siModifizierer angegeben ist), was durch die Regel EnumConstructorAccessibility ( S.
chergestellt wird. Dies gilt auch für default-Konstruktoren von Enums ( § . . ), wo hingegen für default-Konstruktoren für Klassen gilt, dass ihre Zugrei arkeit der der umschließenden Klasse entspricht, was durch die Regel DefaultConstructorAccessibility ( S.
) sichergestellt wird.
5.3. Typ-Constraints
Java ist eine streng typisierte (engl. strongly typed) Programmiersprache ( § ). Das bedeutet,
dass jeder auswertbare Ausdruck einen Typen hat, der schon beim Kompilieren bekannt ist
und die Menge der Operationen einschränkt, die auf ihm durchgeführt werden können. Insbesondere ist für jede Variable und Methode bei Deklaration ein entsprechender (Rückgabe-)Typ
anzugeben. Auf solchen Typannotationen au auend kann dann für beliebige Ausdrücke mittels Inferenzregeln durch den Java-Compiler ein passender Typ abgeleitet werden ( § . ).
In erster Linie dient das Typsystem von Java damit der Fehlervermeidung, indem schon beim
Kompilieren zum Beispiel sichergestellt wird, dass für jeden Methodenaufruf das referenzierte Empfängerobjekt eine entsprechende Methode implementiert. Eine weitere entscheidende
Rolle spielt das Typsystem in Java dadurch, dass es Einfluss auf das Bindeverhalten von Referenzen hat. So ist es in Java erlaubt, innerhalb einer Klasse mehrere Methoden gleichen Namens und mit gleicher Parameteranzahl zu deklarieren – die Methodenüberladung ( § . . ).
Über die Methode, an welche dann eine Methodenreferenz bindet, wird anhand der Typen
der Methodenparameter entschieden, die sich dann für die Methoden einer Klasse jeweils
mindestens in einem Parameter unterscheiden müssen.
Die Constraintregeln für Typen behandeln entsprechend diese drei Schwerpunkte. Einerseits gibt es Regeln, die entsprechend der Typinferenzregeln in Java zu jedem Ausdruck den
inferierten Typen bestimmen (dazu Abschni . . ). Weiterhin gibt es Regeln, die die vom
. . Typ-Constraints
Java-Compiler geprüften Bedingungen zur Fehlervermeidung nachbilden, insbesondere beim
Member-Zugriff (dazu Abschni . . ). Zule t gibt es noch Regeln, die Bindungsänderungen
verhindern (dazu Abschni . ).
Wie Kapitel beschrieben hat, haben Tip et al. bereits umfangreiche Arbeiten zu Typ-Constraints geleistet [TKB , STD , BTF , FTK+ , KETF , Tip , TFK+ ]. Die in den verschiedenen Arbeiten genannten Constraintregeln gleichen sich im Kern stark, auch wenn sie
teils unterschiedlich benannt sind. Tabelle . nu t daher für eine Gegenüberstellung der Constraintregeln von Tip mit denen vorliegender Arbeit den Überblicksartikel [TFK+ ]²⁴, der die
von Tip et al. gesammelten Typ-Constraintregeln weitgehend zusammenfasst. Viele der dort
angegebenen Constraintregeln gelten für die vorliegende Arbeit genau so und haben lediglich
eine Umformulierung in Refacola erfahren. Durch den unterschiedlichen Formalismus von
Tip et al. im Kontrast zur Refacola-Sprachdefinition in dieser Arbeit ist es nicht immer möglich, eine direkte Zuordnung der Constraintregeln von Tip et al. zu denen in dieser Arbeit zu
geben. So vereint zum Beispiel die Constraintregel ArgumentPassing der vorliegenden Arbeit
die Constraintregeln ( ) und ( ) aus [TFK+ ]. Umgekehrt erforderte die Regel ( ) von dort
eine Umse ung in zwei Regeln Assignment1 und Assignment2. Ebenso nennen Tip et al. Constraintregeln, zu denen in vorliegender Arbeit keine Entsprechung gegeben wird. Die Regel
( ) aus [TFK+ ] verbietet beispielsweise, dass ein verdeckendes Feld (hiding field) in einen
Supertypen des verdeckten Feldes verschoben wird. Die vorliegende Arbeit ist da weniger
restriktiv, denn wenn eine solche Verschieberefaktorisierung zum Beispiel mit einer Umbenennung eines der beiden Felder einhergeht, ist eine Änderung der Bindung – und damit des
Programmverhaltens – leicht zu verhindern. Doch auch wenn eine solche nicht erfolgt, kann
noch eine Qualifizierung des Feldes Abhilfe schaffen (dazu Abschni . ). Da Tip hingegen
nur Typen als Variablenwerte betrachtete, ist die Regel ( ) mit den daraus resultierenden
Einschränkungen in seinem Kontext korrekt. Umgekehrt sind mit vorliegender Arbeit auch
eine Reihe Typ-Constraintregeln hinzugekommen, die Tip et al. nicht berücksichtigen, weil
sie zum Beispiel Methodenüberladung bei ihren Betrachtungen gänzlich außen vor lassen
[TFK+ ].
5.3.1. Typen als Constraintvariable
Constraintvariablen für Typen finden in der Sprachdefinition für Java an drei Stellen Verwendung.
• Jede Referenz auf einen Typen (TypeReference) hat eine Variablenart typeBinding, die
Werte dieser Variablen geben jeweils den gebundenen Typen an. Welcher dies ist, ergibt
sich vor allem aus dem Namen einer solchen Typreferenz, kann und muss also mi els
Constraintregeln abgeleitet werden. Obwohl die Werte der entsprechenden Constraintvariablen damit formal gesehen redundant sind, rechtfertigt sich ihr Vorhandensein mit
wesentlichen Vereinfachungen bei Formulierung der Constraintregeln. In diesen können dann nämlich die Bindevorschriften für Typreferenzen und sich aus den Typen ergebende weitere Bedingungen getrennt voneinander behandelt werden. So konnte zum
Beispiel in der bereits erwähnten Constraintregel TypeAccessOutsideType (siehe S. ,
²⁴ siehe dort Abbildung
. Constraintregeln für Java
Tabelle . .: Gegenüberstellung der Constraintregeln aus [TFK+
den Arbeit
Bedingt
Refacola-Regel
Zuweisungskompatibilität
Typinferenz
Dereferenzierung
Parameterübergabe
Objektinstanziierung
Parametertypen überschriebener Methoden
Rückgabetypen überschriebener Methoden
Methodenüberschreibung
Hiding von Feldern
return-Typen
Casts
this
Implizite Empfänger
statischer Aufrufe
Object als Wurzeltyp
null-Typ
] mit denen der vorliegen-
Assignment1 (siehe S.
, ebenso S.
)
Assignment2 ( S.
)
RegularTypedReferenceExpressionTypes
( S.
)
MemberAccess (siehe S.
, ebenso S.
)
ArgumentPassing ( S.
)
ConstructorIncovationExpressionTypes
( S.
)
OverridingParameterType ( S.
)
[TFK+
( )
( ), ( )
( ), ( )
( ), ( )
( )
( )
OverridingReturnType (siehe S.
, ebenso
S.
)
OverridingMustComeFromSubtype ( S.
)
(
)
(
)
—
MethodReturn ( S.
TypeCasts ( S.
)
This ( S.
)
ThisType ( S.
)
(
(
(
(
—
)
)
), (
)
(
(
)
)
—
—
)
]
), (
)
. . Typ-Constraints
ebenso S.
) die Zugrei arkeit eines referenzierten Typen geprüft werden, ohne dass
auch schon weitere Bedingungen zu Namen zu prüfen wären (dazu dann Abschni . ).
• Alle Deklarationen, die in Java mit einem Typen annotiert werden (also Felder, Methoden, formale Parameter und lokale Variablen, auch solche, die zum Beispiel durch forSchleifen und catch−Blöcke deklariert werden) leiten von TypedEntity ab. Soweit der deklarierte Typ nicht einer Typvariablen entspricht, findet die Programmelementunterart
RegularTypedEntity Anwendung, welches eine Variable declaredType einführt. Mit einer
Typvariablen typisierte Deklarationen (TypeVariableTypedEntity) haben hingegen keine
Variable für ihren deklarierten Typen. Abschni . . beschreibt, wie dieser sta dessen
in Abhängigkeit vom jeweiligen Verwendungskontext inferiert werden muss.
• Als dri e Programmelementart der Refacola-Sprachdefinition für Java haben Ausdrücke
eine Variable expressionType für ihren jeweiligen Typen.
Für TypeReference und RegularTypedEntity umfasst die Domäne der Typvariablen jeweils
die Menge aller Typen im Programm. Das umfasst die durch Klassen und Interfaces deklarierten Typen, Primitivtypen und Array-Typen. Da die Sprache Java keine Beschränkung zur
maximalen Dimension eines Array gibt, ist le tere Menge nicht endlich.²⁵ Daher werden nur
solche Array-Typen der Domäne hinzugefügt, die tatsächlich im zu transformierenden Programm auftreten. Soll eine Refaktorisierung einen bisher nicht genu ten Array-Typen einführen, muss dieser zuvor der Domäne der Typen hinzugefügt werden.
Die Domänen der Typvariablen von Ausdrücken ergeben sich aus der Art des jeweiligen
Ausdrucks vor der Refaktorisierung. Bei mit Klassen oder Interfaces typisierten Ausdrücken
umfasst die Domäne der Typvariablen auch nur genau solche Typen, erlaubt also weder primitive, noch Array-Typen. Ebenso können Array- und primitiv typisierte Ausdrücke jeweils
nur weitere Array- und Primitivtypen annehmen. Das verbietet Refaktorisierungen, in denen
zum Beispiel für ein Array ohne Elemen ugriffe der entsprechende Array-Typ durch seinen
Supertypen java.lang.Object erse t wird. Umgekehrt erspart es Regeln, die dann sicherstellen
müssten, dass array access expressions ( § . ) oder .length-Abfragen auf nicht Array-Typen
durchgeführt werden. Diese ließen sich mit der Refacola zwar umse en, indem jeder Ausdruck über Boolesche Constraintvariablen anzeigt, ob es sich aktuell um einen primitiven,
Array- oder Klassen- beziehungsweise Interfacetyp handelt, deren korrekte Belegung über
weitere Regeln gesteuert wird,²⁶ allerdings scheinen Aufwand und Nu en in keinem sinnvollen Verhältnis zu stehen. Man kann davon ausgehen, dass bei bedeutungserhaltenden (!)
Programmtransformationen in der Hauptsache Sub- durch Supertypen (oder umgekehrt) zu
erse en sind, wie es auch typischerweise bei den Refaktorisierungen G
D
T
und U S
W
P
der Fall ist. Da (außer zwischen Array-Typen und
java.lang.Object) zwischen den drei genannten Typ-Arten keine Subtypbeziehungen bestehen,
sind die negativen Konsequenzen wohl zu verschmerzen.
²⁵ Lediglich die Java Virtual Machine Specification [LY ] gibt in § . . vor, dass ein Array maximal
dimensional sein darf, was aber dennoch zu einer unpraktikabel großen Domäne der Typen führen würde.
²⁶ Eine Formulierung innerhalb einer einzelnen Regel ist bisher mit der Refacola (noch) nicht möglich, erfordert
dies doch instanceof-Abfragen und Casts von Programmelementarten (Kind Casts), die bisher nicht unterstü t
sind.
. Constraintregeln für Java
5.3.2. Constraintregeln zur Typinferenz
Die Werte, die Typ-Constraintvariablen von Typreferenzen, typisierten Deklarationen und
Ausdrücken annehmen dürfen, sind nicht voneinander unabhängig. Vielmehr ergeben sie sich
innerhalb des Compilers aus den Binderegeln für Typreferenzen und Typinferenzregeln der
Sprache Java. So ergibt sich der Typ eines Feldes aus dem Typen, an dem die Typreferenz innerhalb seiner Deklaration bindet. Der Typ eines Ausdrucks, der ein solches Feld referenziert,
ergibt sich dann wiederum aus dem Typen des Feldes. Genau dieses Verhalten muss über
Typ-Constraintregeln nachgebildet werden. Der erste Schri ist dabei einfach umzuse en, so
sichert die folgende Regel TypedEntityTypes zu, dass der Typ eines Feldes, einer Methode oder
Variablen genau der entsprechenden Typreferenz in ihrer Deklaration entspricht:
647
648
649
650
651
652
653
654
655
656
TypedEntityTypes
for all
typeReference: Java.TypeReference
typedEntity: Java.RegularTypedEntity
do
if
Java.definesTypeOf(typeReference, typedEntity)
then
typeReference.typeBinding = typedEntity.declaredType
end
Von diesen Typen abhängig auch für jeden Ausdruck im Programm den korrekten Typ zu
inferieren ist komplizierter, muss hier doch nach der Art des jeweiligen Ausdrucks unterschieden werden.
Referenzen auf typisierte Deklarationen Im einfachsten Fall entspricht ein Ausdruck einer
Referenz auf eine typisierte Deklaration. In diesem Fall sichert die Constraintregel RegularTypedReferenceExpressionTypes ( S.
) zu, dass der Typ des Ausdrucks gleich dem Typ der
referenzierten Deklaration ist. Der Sonderfall einer class instance creation expression, also eines
Konstruktoraufrufs mit Schlüsselwort new zur Instanzerzeugung, dessen Typ sich aus der instanziierten Klasse ergibt, wird über die Regel ConstructorIncovationExpressionTypes ( S.
)
abgehandelt.
Typpropagation Bei einer Reihe weiterer Arten von Ausdrücken ergibt sich der Typ des
Ausdrucks jeweils aus einem weiteren, in diesem enthaltenen Ausdruck. Ganz offensichtlich
ist im Fall eines geklammerten Ausdrucks, welcher wiederum einen Ausdruck bildet und dessen Typ dem Typen des geklammerten Ausdrucks entspricht ( § . . ). Ähnlich sieht es
zum Beispiel beim Inkrement- und Dekrement-Operator aus ( §§ . . , . . ), bei dem
der Typ des Ausdrucks jeweils dem des in- oder dekrementierten Ausdrucks entspricht. Auf
diese Fälle wird schon bei der Faktengenerierung geprüft und für Ausdrücke, die in einem
derartigen Verhältnis stehen, ein Fakt typePropagation angelegt, anhand dessen über die Con) die Gleichheit der Typen beider Ausdrücke zugesichert
straintregel TypePropagation ( S.
wird.
. . Typ-Constraints
Der Typ von this Die Verwendung einer this-Expression innerhalb eines nicht-statischen
Kontextes ( § . . ) referenziert das Objekt, welches – bei Konstruktoraufrufen – in diesem
Kontext gerade erzeugt oder dem – bei Methodenaufrufen – gerade eine Nachricht gesendet
wird ( § . . ). Der vom Compiler inferierte Typ eines this-Ausdrucks entspricht daher der
unmi elbar umschließenden Klasse, was die Regel this sicherstellt:
657
658
659
660
661
662
663
664
665
666
This
for all
thisReference: Java.ThisReference
thisExpression: Java.Expression
do
if
Java.isExpression(thisReference,thisExpression)
then
thisReference.owner = thisExpression.expressionType
end
Für den Sonderfall geschachtelter Klassen, bei denen sich das this auf eine der umschließenden
Klassen bezieht und gegebenenfalls auch qualifiziert werden kann ( § . . ), kommt als
Programmelement sta einer ThisReference eine QualifiedThisReference zur Anwendung. Die
Constraintregel QualifiedThis ( S.
) behandelt diesen Fall analog.
Casts Bei einer class cast expression ( § . ) leitet sich der Typ des Ausdrucks nicht aus
Typen von Deklarationen ab, sondern aus einer explizit angegebenen Typreferenz. Java erlaubt es dem Programmierer also, mit Type Casts dem Compiler zuzusichern, dass ein von
einem Ausdruck referenziertes Objekt einen bestimmten Typen hat, ohne dass dieser dies
von sich aus bestätigen könnte. Damit wird die Typprüfung durch den Compiler umgangen und ein eventuell auftretender Kompilierfehler auf die Laufzeitebene (dann mit einer
ClassCastException) verlagert. Aus Sicht des damit von seiner Pflicht der Typprüfung entbundenen Compilers hat ein Cast-Ausdruck genau den Typ, der vom Programmierer vorgegeben
ist.
Innerhalb der Refacola-Sprachdefinition für Java besteht ein Cast-Ausdruck stets aus drei
Programmelementen, die über ein dreistelliges Fakt castExpression miteinander als solche gekennzeichnet sind. Im Einzelnen handelt es sich um den CastType, die Typreferenz, die den
Typen referenziert, dessen Typ dem Compiler zugesichert wird, sowie die innerExpression, der
Ausdruck, dessen Typ zugesichert wird, und die CastExpression, die dann den zugesicherten
Typen aufweist. Neben weiteren Bedingungen, die für Casts in Java gelten müssen ( § . ),
weist die Regel TypeCasts ( S.
) mit der ersten Bedingung ihrer Konklusion einer class cast
expressions jeweils genau den Typen zu, welchen der Cast vorgibt.
Array-Elementzugriff Weiterhin sind noch array access expressions zu berücksichtigen, die
Zugriffen auf einzelne Elemente eines Arrays entsprechen ( § . ). Hier entspricht der Typ
eines Elemen ugriffs dem Elemen ypen des Arrays. Die Regel ArrayAccessTypes ( S.
)
se t dies um, wobei Zugriffe über mehrere Array-Dimensionen hinweg, zum Beispiel
667
i = field[x][y]
. Constraintregeln für Java
bei der Faktengenerierung als zwei ineinander geschachtelte Ausdrücke behandelt werden
(also im Beispiel (field[x])[y]).
Der ?-Operator Die Sprache Java bietet die sogenannte conditional expression ( § . ), eine
Kurzschreibweise des if statement mi els eines Fragezeichenoperators. Wertet ein vor dem ?
stehender Ausdruck zu true aus, wird der dem Fragezeichen folgende Ausdruck ausgeführt.
Wertet er zu false aus, wird ein optionaler dri er und hinter einem Doppelpunkt anzugebender
Ausdruck ausgeführt. In folgendem Beispiel wird für einen Wert von temperature größer null
die Zeichenke e hot ausgegeben. Anderenfalls lautet die Ausgabe cold;
668
temperature > 0 ? System.out.print("hot") : System.out.print("cold");
Offensichtlich muss für conditional expressions ebenso wie bei if-Anweisungen gelten, dass der
vorderste der drei Ausdrücke ein Boolescher Ausdruck ist (dazu Abschni . . ). Darüber
hinaus haben conditional expressions allerdings selbst auch noch einen Typen, welcher sich
aus den Typen der beiden hinteren Ausdrücke ergibt. Im einfachsten Fall haben beide identische Typen, dann ist auch der Typ des gesamten Ausdrucks derselbe. Formuliert man obiges
Beispiel wie folgt um
669
System.out.print(temperature > 0 ? "hot" : "cold");
dann wird der Typ der conditional expression als String inferiert und kann somit als Parameter
der print-Methode übergeben werden. Unterscheiden sich die inferierten Typen der beiden
hinteren Ausdrücke, kommen erweiterte Inferenzregeln zum Einsa , die im Wesentlichen den
speziellsten gemeinsamen Supertypen beider Ausdrücke ermi eln ( § . ). Diese lassen
sich allerdings mit den Ausdrucksmöglichkeiten der Refacola derzeit (noch) nicht formulieren. Die Java-Sprachspezifikation sieht insbesondere bei den primitiven Typen verschiedene
Fallunterscheidungen vor ( § . ), die es erforderlich machen, für den expressionType einer Expression nach der Art des Programmelementes zu fragen – was im Moment seitens der
Refacola nicht unterstü t wird. Als Folge werden bei der Faktengenerierung die Typen von
Ausdrücken innerhalb von conditional expressions als unveränderlich gekennzeichnet. Damit
bleibt der mit dieser Arbeit gelieferte Regelsa korrekt, allerdings für diesen Spezialfall restriktiv.
5.3.3. Member-Zugriff und Zuweisungskompatibilität
Einer der Haup wecke eines Typsystems, wie Java es bietet, ergibt sich aus der damit möglichen Typprüfung beim Nachrichtenversand, also der Überprüfung, ob eine auf einem Empfänger aufgerufene Methode (beziehungsweise ein zugegriffenes Feld) auf diesem tatsächlich
definiert ist. Die folgende Regel MemberAccess sichert dies zu:
670
671
672
673
674
675
676
677
MemberAccess
for all
receiver: Java.Expression
reference: Java.TypedReference
declaration: Java.OwnedEntity
do
if
Java.receiver(reference, receiver),
. . Typ-Constraints
678
679
680
681
Java.binds(reference, declaration)
then
Java.sub*(receiver.expressionType,declaration.owner)
end
Eine Beurteilung, ob ein Empfänger eine bestimmte Nachricht versteht, kann natürlich nur
gelingen, wenn für den Empfänger verlässliche Typinformationen bekannt sind. Erlaubt eine
Programmiersprache Zuweisungen, muss daher auch bei jeder Zuweisung im gesamten Eingabeprogramm geprüft werden, ob der Typ der Variablen, der zugewiesen wird, mindestens
dem entspricht, der zugewiesen wird. Für Java bedeutet das im Wesentlichen, dass bei einer
Zuweisung der Ausdruck links des Zuweisungsoperators den gleichen Typ oder einen Supertyp des Ausdrucks rechts des Zuweisungsoperators haben muss ( § . ), was die folgende
Regel Assignment1 umse t:
682
683
684
685
686
687
688
689
690
691
Assignment1
for all
lhs: Java.Expression
rhs: Java.Expression
do
if
Java.assignment(lhs, rhs)
then
Java.sub*(rhs.expressionType,lhs.expressionType)
end
Darüber hinaus gibt es noch einige wenige Ausnahmen, in denen eine Zuweisung auch
dann gesta et ist, wenn die beteiligten Ausdrücke in keiner Vererbungsbeziehung zueinander stehen ( § . ). Diese ergeben sich aus automatischen Typkonvertierungen zwischen
Primitivtypen und ihren Wrapperklassen, den sogenannten boxing conversions ( §§ . . ,
. . ). So sind zum Beispiel folgende Zuweisungen in Java erlaubt, bei denen ein primitiver
Typ zu seinem Wrapperklassentyp und umgekehrt gewandelt wird:
692
693
694
695
int i = 0;
Integer j;
j = i;
i = j;
Automatische Typkonvertierungen sind in dem entwickelten Regelsa für Refacola nicht berücksichtigt. Technisch wäre dies machbar, würde aber zu einer großen Zahl bedingter Constraints führen, da bei jeder Zuweisung entsprechend unterschieden werden müsste, ob automatische Typkonvertierung Anwendung finden könnte. Sta dessen wird eine einfache technische Lösung umgese t, bei welcher intern Primitivtypen und ihre Wrapperklassen als gegenseitige Subtypen zueinander in der Faktenbasis definiert werden (es wird also zum Beispiel sowohl ein Fakt sub(int, Integer) als auch sub(Integer, int) angelegt. Problematisch kann
eine solch künstliche Subtypbeziehung für Constraintregeln werden, die aus anderen Gründen als der Prüfung von Zuweisungskompatibilität Subtypbeziehungen prüfen.
Bei Zugrei arkeitsregeln wird dies nicht zu Problemen führen, da Primitivtypen und ihre
Wrapperklassen (sowie deren Member) ohnehin stets unveränderlich zugrei ar sind. Einzig
, ebenso S.
) werden,
problematisch kann oben stehende Regel MemberAccess (siehe S.
würde sie doch in der Annahme, int sei ein Subtyp von Integer, Aufrufe der Art
. Constraintregeln für Java
696
697
int i = 0;
i.compareTo(1);
::::::::::::
gesta en, obwohl eine Methode compareTo für Primitivtypen nicht definiert ist. Da dieser Fehler aber stets und völlig unabhängig durch Einfügen eines type casts auf die jeweilige Wrapperklasse verhindert werden kann, lässt er sich gegebenenfalls beim Zurückschreiben in den
Code durch ein automatisches Postprocessing leicht korrigieren.
Java erlaubt Zuweisungen nicht nur zwischen zwei Ausdrücken, wie in obigem Beispiel in
den Zeilen
und
, sondern ebenso auch direkt bei der Deklaration einer Variablen wie
in Zeile
. Dies macht die Behandlung durch eine eigene Regel Assignment2 ( S.
) nötig,
die analog zur oben bereits beschriebenen Constraintregel Assignment1 (siehe S.
, ebenso
S.
) formuliert werden kann. Ebenso finden indirekt Zuweisungen bei Methodenaufrufen sta , wobei die Argumente des Methodenaufrufs den Methodenparametern zugewiesen
werden, sodass hier ebenso Zuweisungskompatibilität gelten muss. Umgekehrt müssen auch
die von einer Methode per return-Anweisung zurückgegebenen Ausdrücke zuweisungskompatibel zu dem return-Typ der Methode sein. Beides wird mi els der Regeln ArgumentPassing
( S.
) und MethodReturn ( S.
) umgese t.
5.3.4. Überschreibung
Aus Methodenüberschreibungen ( § . . . ) ergeben sich insgesamt mehrere, einfach umzuse ende Typ-Constraintregeln. Zuvorderst kann eine Methode eine andere Methode nur
dann überschreiben, wenn zwischen den deklarierenden Typen beider Methoden eine Subtypbeziehung besteht, was die Regel OverridingMustComeFromSubtype ( S.
) sicherstellt.
Weiterhin müssen bei überschreibenden Methoden die Parametertypen übereinstimmen, was
OverridingParameterType ( S.
) sicherstellt. Die Rückgabetypen von überschreibenden Methoden müssen je nach Java-Version entweder identisch sein (bis Java . ) oder in kovarianter
Subtypbeziehung stehen, sodass eine überschreibende Methode also höchstens einen Subtyp
des Rückgabetypen der überschriebenen Methode zurückgeben darf (ab Java ). Folgende Regel zeigt die Java . -Version:
698
699
700
701
702
703
704
705
706
707
708
OverridingReturnType
for all
overridingMethod: Java.RegularTypedInstanceMethod
overriddenMethod: Java.RegularTypedInstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
overridingMethod.declaredType
= overriddenMethod.declaredType
end
Für Java -Projekte muss die Konklusion durch
709
Java.sub*(overridingMethodType.typeBinding, overriddenMethodType.typeBinding)
erse t werden.
Die beiden le tgenannten Regeln zu Parameter- und Rückgabetypen müssen nicht nur für
direkte Methodenüberschreibung, sondern auch die vorausschauende Interfaceimplementie-
. . Typ-Constraints
rung ( § . . . ) gelten (siehe dazu Abschni
. . ). Dies wird durch die Regeln EagerInterfaceImplementationParameterType ( S.
) und EagerInterfaceImplementationReturnType
( S.
) umgese t.
5.3.5. Zugriff auf statische Member
Obige Regel MemberAccess greift ausnahmslos für Zugriffe auf nicht-statische Deklarationen.
Das Query receiver ordnet darin jeweils einer Referenz einen Ausdruck – den Nachrichtenempfänger – zu. Zwar erlaubt Java auch, statische Member auf Ausdrücken aufzurufen, allerdings wird diese – ohnehin nicht empfohlene – Praxis, wie in Abschni . bereits erläutert,
in vorliegender Arbeit nicht berücksichtigt.
Sta dessen sind Aufrufe statischer Member mit einem Typen zu qualifizieren. Bei unqualifizierten Aufrufen entspricht der Empfängertyp dem die Referenz umschließenden Typen,
wobei bei Membertypen auch deren umschließende Typen infrage kommen. Ebenso sind statische Imports zu berücksichtigen. Unabhängig davon, ob ein statischer Zugriff einen qualifizierenden Typen vorangestellt hat, wird bei der Faktengenerierung der Zugriff als qualifiziert
angenommen, also gegebenenfalls implizit hinzugefügt. Dass dann auf diesem Typen das aufgerufene Member tatsächlich deklariert ist, sichert die Constraintregel StaticMemberAccess
( S.
) analog zum oben genannten nicht-statischen Pendant MemberAccess (siehe S.
,
ebenso S.
) zu.
5.3.6. Weitere Regeln
Die Java-Grammatik sieht eine Reihe von Anweisungen vor, bei denen Bedingungen an den
Typ der enthaltenen Ausdrücke gestellt werden. So ergibt ein if statement nur Sinn, soweit der
dem if folgende Ausdruck einen Booleschen Typen hat ( § . ). Ähnliches gilt für array
access expressions, bei denen der in eckigen Klammern angegebene Ausdruck für die Position
ein int sein muss ( § . ). In beiden, wie auch vielen ähnlich gelagerten Fällen muss über
Constraints sichergestellt sein, dass der Typ des Ausdrucks unverändert bleibt. Eine mögliche
Umse ung wäre ein eigenes Fakt für jede Art von Anweisung sowie je eine Constraintregel
für jedes Fakt, welches dann den Typ des Ausdrucks innerhalb der Anweisung unveränderlich macht. Einfacher ist aber, schon zum Zeitpunkt der Faktengenerierung eine vollständige
Behandlung durchzuführen: die Refacola erlaubt, generierte Programmelemente als unveränderlich zu kennzeichnen – eine Funktion, die primär für Programmelemente aus unveränderlichem Code aus Programmbibliotheken gedacht ist (siehe Abschni . . ) – worüber auch
die sich direkt aus der Syntax ergebenden Typeinschränkungen behandelt werden.
Technisch bedingt sind die drei Regeln ArrayTypedExpressions ( S.
), ClassOrInterface) und PrimitiveTypedExpressions ( S.
). Manche ConstraintreTypedExpressions ( S.
geln finden nur Anwendung für bestimmte Arten von Ausdrücken. So bezieht sich zum Bei) nur auf ClassOrspiel die Regel InheritedMemberAccessibility (siehe S. , ebenso S.
InterfaceTypedExpression. Umgekehrt se t sie beim Abfragen des inferierten Typs eines solchen Ausdrucks auch voraus, dass hier nur ein ClassOrInterfaceType zurückgegeben werden
kann. Da die Refacola keine Kind Casts unterstü t, kann die von Expression geerbte Variablenart expressionType keine Anwendung finden, sta dessen bringt die Unterart ClassOrInterfaceTypedExpression ihre eigene Variablenart inferredClassOrInterfaceType mit. Die Regel ClassOr-
. Constraintregeln für Java
InterfaceTypedExpressions ( S.
) sichert entsprechend zu, dass für eine ClassOrInterfaceTypedExpression die Variablen expressionType und inferredClassOrInterfaceType stets gleich belegt sind. Analog behandeln die beiden übrigen Regeln Array- und primitive Typen.
Weiterhin Berücksichtigung in Bezug auf Typen müssen try und catch-Anweisungen finden
( § . ) . Wird innerhalb eines try-Blocks der Programmfluss durch eine Ausnahme (engl.
exception) abrupt beendet, so werden anschließend die zugehörigen catch-Anweisungen dahingehend untersucht, ob eine von ihnen einen zum Ausnahmetypen passenden Parameter
deklariert ( § . . ). Eine Änderung deklarierter Typen von catch-Parametern kann also
darüber entscheiden, welcher catch-Block ausgeführt wird und somit das Programmverhalten
im Ausnahmefall beliebig beeinflussen. Allerdings ist im Rahmen einer statischen Programmanalyse nicht entscheidbar, welche Ausnahmetypen tatsächlich auftreten können. Java liefert
mit den runtime exceptions ( § . . ) ein Mi el, auch nicht deklarierte Ausnahmen auszulösen, die dennoch durch catch-Anweisungen abgefangen werden können. Aus diesem Grund
kommt bei der Auswahl eines geeigneten catch-Blocks auch jeweils der Laufzei yp der Ausnahme zur Anwendung ( § . . ), welcher sich einer statischen Codeanalyse en ieht. Mit
den Mi eln einer Laufzeitanalyse, wie sie ab Kapitel für Java-Reflection beschrieben wird,
wäre dieses Problem lösbar, wird aber wegen seiner geringen Relevanz (im Vergleich zum
Aufwand einer Laufzeitanalyse) in dieser Arbeit nicht weiter behandelt. Sta dessen werden
Variablen, die deklarierte Typen von catch-Parametern beschreiben, als unveränderlich gekennzeichnet.
5.4. Typ-Constraints für generische Typen
Seit Version unterstü t die Sprache Java generische Typen ( §§ . . , . . ) und Typvariablen ( § . ) beziehungsweise Typparameter²⁷, um Typen zu parametrisieren ( § . ).
Generische Typen sorgen bei Refaktorisierungen für zusä liche Bedingungen, die berücksichtigt werden müssen, insbesondere, wenn deklarierte Typen selbst Gegenstand der Refaktorisierung sind.
D
T -Refaktorisierung, bei welAbbildung . zeigt ein Beispiel für eine C
chem sich die Änderung eines deklarierten Typen ebenso auf die Belegungen von Typvariablen eines generischen Typen auswirken muss. Soll der Typ der Variablen in in Zeile
zu Number geändert werden, muss ebenso die Belegung der Typvariablen T in Zeile
von
Double zu Number geändert werden. Als direkte Folge dessen sind ebenso die Belegung der
Typvariablen beim Konstruktoraufruf in Zeile
und die Typisierung von out in Zeile
von Double hin zu Number anzupassen.
Die vorliegende Implementierung bietet für die Transformation von Programmen mit generischen Typen Unterstü ung in eingeschränktem Umfang. Unterstü t werden in erster Linie
Änderungen bei Belegungen von Typvariablen ( § . ), wie im vorstehenden Beispiel gezeigt
wird. Dabei werden auch obere und untere Schranken von Wildcards (engl. bounded wildcards)
( § . . ) unterstü t, also Änderungen von beispielsweise NumberBox<? extends Double> zu
NumberBox<? extends Number>.
²⁷ Die Java-Sprachspezifikation unterscheidet bei den Begriffen Typparameter und Typvariable nur sehr subtil: „It
[the type parameter section of a class] defines one or more type variables that act as parameters“ ( § . . ). Innerhalb
dieser Arbeit werden beide Begriffe synonym gebraucht.
. . Typ-Constraints für generische Typen
710
711
712
class NumberBox<T extends Number> {
T contents;
}
class NumberBox<T extends Number> {
T contents;
}
class Main {
public static void main(String[] args) {
Double in = new Double(23);
NumberBox< Double> box
= new NumberBox< Double>();
booleanBox.contents = in;
Double out = booleanBox.contents;
}
}
class Main {
public static void main(String[] args) {
Number in = new Double(23);
NumberBox<Number > box
= new NumberBox<Number >();
booleanBox.contents = in;
Number out = booleanBox.contents;
}
}
713
714
715
716
717
718
719
720
721
722
(a)
⇒
(b)
Abbildung . .: Soll im Rahmen eines C
D
T
der Typ der Variablen in in Zeile
zu Number geändert werden, muss sich dies ebenso auf die Belegung der
Typvariablen von box und auf den Typen von out auswirken.
Nicht unterstü t werden Anpassungen von Schranken von Typvariablen (engl. bounded type variables) ( § . ). In obigem Beispiel würde eine Änderung des deklarierten Typen von
in zu Object fehlschlagen, weil dann ebenso box als NumberBox<Object> zu deklarieren wäre.
Dies widerspricht aber der in Zeile
angegebenen oberen Schranke Number für T. Diese
könnte entfallen, da von ihr innerhalb von NumberBox keine Verwendung gemacht wird (es
wird auf contents keine Methode aus Number aufgerufen). Allerdings sieht die derzeitige Implementierung noch keine Variablen für Beschränkungen von Typvariablen vor, weswegen
diese unveränderlich sind und die Transformation abgelehnt werden muss.
Als weitere Einschränkung werden keine Transformationen unterstü t, die neue Typvariablen hinzufügen, vorhandene entfernen oder bei Verwendung von Wildcards neue obere oder
untere Schranken hinzufügen oder entfernen (bestehende Schranken können aber angepasst
werden). Auch sind keine Transformationen möglich, die an Verwendungsstellen generischer
Typen das Hinzufügen oder Entfernen von Typisierungsinformationen nach sich ziehen müssen. Angenommen, es gäbe für obiges Beispiel aus Abbildung . zusä lich die folgenden
Typdeklarationen,
723
724
class SubNumberBox<T extends Number> extends NumberBox<T> { /* ... */ }
class DoubleBox extends NumberBox<Double> { /* ... */ }
dann wäre eine Änderung einer Deklaration NumberBox<Double> box zum spezielleren Typen SubNumberBox<Double> box unterstü t, nicht aber zum spezielleren Typen DoubleBox,
bei dessen Verwendung dann die Angabe eines Typen für die Typvariable T entfällt. Ebenso wird keine Unterstü ung geboten für die Refaktorisierung von Methoden, die generische
Methoden überschreiben und dabei Typvariablen mit konkreten Typen belegen, wie es zum
Beispiel bei der Klasse DoubleBox möglich wäre. Würde die Klasse NumberBox eine Methode
T m() deklarieren, könnte diese mit Signatur Double m() in DoubleBox überschrieben werden.
Beim Verschieben dieser Methode müsste dann gegebenenfalls wiederum eine Erse ung des
. Constraintregeln für Java
konkreten Typen durch die Typvariable sta finden.
Der Grund für diese Einschränkungen ergibt sich aus der Modellierung der Typvariablenbelegungen als eigenes Programmelement. Programmelemente sind in der Refacola selbst nicht
Teil der Constraints, sondern nur die ihnen zugeordneten Variablen. Insofern kann die Existenz oder Nichtexistenz eines Programmelements nicht bei der Constraintlösung beeinflusst
werden. Zwar ließe sich so ein Verhalten mi els Boolescher Variablen modellieren, die den
jeweiligen Programmelementen zugeordnet werden, dies würde aber zu einer deutlich vergrößerten Menge von Programmelementen führen (denn auch wo keine Typparameter gegeben sind, müssen als nicht-existent gekennzeichnete Programmelemente für diese eingefügt
werden). Das Mehr an Programmelementen führt dann wiederum zu einem deutlichen Anstieg bedingter, und damit teurer (siehe dazu Abschni . ) Constraints. Untersuchungen, ob
dieses Unterfangen lohnenswert ist, stehen aus.
Da für generisch typisierte Deklarationen andere Programmelemente Verwendung finden,
als für nicht generisch typisierte Programmelemente, sind ebenso Refaktorisierungen ausgeschlossen, die Typvariablen durch konkrete Typen erse en (oder umgekehrt). Mitunter
verbietet dies auch Programmtransformationen, die in erster Linie gar nicht auf Änderungen deklarierter Typen abzielen, sondern diese nur als Folgeänderungen mit sich bringen.
Ein Beispiel hierfür wäre das Verschieben einer überschriebenen Methode. Überschriebe zum
Beispiel eine Methode aus DoubleBox eine Methode aus NumberBox, welche einen ihrer Parameter mit T typisiert, so ist in der überschriebenen Methode dieser Parameter mit der konkreten Belegung Double zu typisieren. Soll die überschreibende Methode in eine andere Klasse
verschoben werden, könnte es notwendig werden, den konkreten Typen wieder durch eine
Typvariable zu erse en.
Insgesamt verhält sich in diesen Fällen die vorliegende Implementierung konservativ. Inkorrekte Transformationen, also solche, die außerhalb der Intention das Programmverhalten
ändern oder zu nicht kompilierendem Code führen, werden unterbunden. Darüber hinaus
gibt es aber Situationen, in welchen eine Transformation mit zusä lichen Änderungen wie
den oben beschriebenen Anpassungen von Typschranken oder der Erse ung von konkreten
Typen durch Typvariablen möglich wäre, aber dennoch unterbunden wird (sogenannte overly
strong conditions [SMG ]).
Tro der genannten Einschränkungen trägt die korrekte Behandlung generischer Typen
weiterhin ein hohes Maß Komplexität in sich, welches in den folgenden Unterkapiteln au ereitet wird.
5.4.1. Typinferenz generisch typisierter Ausdrücke
Werden Deklarationen innerhalb eines generischen Typen mit Typvariablen typisiert, folgt
daraus direkt, dass der durch den Compiler inferierte Typ für diese Deklarationen nicht mehr
an allen Verwendungsstellen identisch ist. Innerhalb der Deklaration eines generischen Typen
kann für eine Typvariable dieses Typen zunächst gar kein Typ – beziehungsweise lediglich
Object – angenommen werden,²⁸ soweit Typschranken keine anderen Annahmen erlauben
(dazu weiteres in Abschni . . ). Bei der Verwendung generischer Typen kann der inferierte Typ einer mit Typvariable typisierten Deklaration je nach Verwendungsstelle völlig unter²⁸ Der Fall, dass bei der Deklaration eines generischen Typen derselbe Typ wiederum verwendet wird, sei hier
ausgenommen.
. . Typ-Constraints für generische Typen
schiedlich lauten, je nach dem, wie die jeweilige Typvariable belegt wurde. Folgendes Beispiel
illustriert diesen Sachverhalt:
725
726
727
728
729
730
731
732
733
734
735
public class Box<T> {
T contents;
}
class Main {
public static void main(String[] args) {
Box<String> stringBox = ...
Box<Object> objectBox = ...
String s = stringBox.contents
Object o = objectBox.contents
}
}
Das Feld contents der Klasse Box ist durch die Typvariable T typisiert. Je nach Verwendungskontext dieser Variablen unterscheidet sich dann der konkrete Typ, der für die Typvariable eingese t wird. In obigem Beispiel ist der durch den Compiler inferierte Typ des Feldes
contents in Zeile
einmal String, während er in Zeile
Object ist. Beides folgt aus den unterschiedlichen Belegungen der Typvariablen T bei den Variablendeklarationen von stringBox
und objectBox in den Zeilen
und
.
Die wichtigste Folge bei einer Darstellung von mit Typvariablen typisierten Deklarationen
in Refacola ist, dass für solche Deklarationen kein einheitlicher, inferierter Typ angegeben
werden kann, eine entsprechende Variablenart für den deklarierten Typen also keinen Sinn ergibt. Entsprechend deklariert das verantwortliche Programmelement TypeVariableTypedEntity
im Gegensa zu seinem nicht-generischen Pendant RegularTypedEntity auch keine Variablenart declaredType. Dennoch muss aber ebenso wie bei den nicht generisch typisierten Deklarationen eine Typinferenz durchgeführt werden. So gilt beispielsweise auch für Zuweisungen
an generisch typisierte Felder weiterhin, dass Zuweisungskompatibilität zwischen den Typen bestehen muss. Im obigen Beispiel darf stringBox also nur ein String (oder ein Subtyp von
String, gäbe es diesen) als Inhalt gegeben werden, eine Zuweisung
736
stringBox.contents = new Object();
::::::::::::::::::::::::::::::
führt zu einem Kompilierfehler. Während für nicht-generisch typisierte Deklarationen sich
der Typ des Ausdrucks stringBox.contents anhand der Regel RegularTypedReferenceExpressionTypes ( S.
) direkt aus der Deklaration des Feldes contents abgeleitet hä e (und die Zuweisung über die Regel Assignment1 (siehe S.
, ebenso S.
) verhindert worden wäre),
muss bei generisch typisierten Deklarationen der jeweilige Kontext – also die jeweils geltende
Belegung der Typvariablen – berücksichtigt werden, wie sie im vorstehenden Beispiel in Zeile
vorgenommen wurde, indem bei der Deklaration der Variablen Box die Typvariable T
den Wert String zugewiesen bekommt.
In vorliegender Implementierung wird eine jede solche Zuweisung über ein Programmelement TypeParameterBinding repräsentiert. Im einfachen Fall, in dem keine Wildcards Verwendung finden (näheres hierzu in Abschni . . ), wird die Unterart SimpleTypeParameterBinding
verwendet, welche eine Variablenart boundType hat, deren Werte genau dem zugewiesenen
Typen entsprechen. Folgende Regel SimpleTypeParameterBinding sorgt für eine derartige Zusicherung, wobei das Fakt typeReferenceFor die Information liefert, dass die genannte Typre-
. Constraintregeln für Java
ferenz genau diejenige ist, welche innerhalb der spi en Klammern bei Belegung der Typvariablen auftri .
737
738
739
740
741
742
743
744
745
746
SimpleTypeParameterBinding
for all
typeReference: Java.TypeReference
typeParameterBinding: Java.SimpleTypeParameterBinding
do
if
Java.typeReferenceFor(typeReference, typeParameterBinding)
then
typeReference.typeBinding = typeParameterBinding.boundType
end
Über eine weitere Constraintregel muss dann sichergestellt werden, dass Ausdrücke, deren
Typ einer derart belegten Typvariablen entspricht, für ihren inferierten Typen ihrerseits denselben Wert annehmen. Während das dabei entstehende Constraint – Gleichheit zweier Typvariablen – relativ einfach formuliert ist, gestaltet sich die Formulierung der Prämisse einer
zugehörigen Constraintregel ungleich aufwändiger, da sich aus dem Code der Zusammenhang zwischen der Belegung einer Typvariablen und dem Typ eines Ausdrucks nur über eine
Ke e mehrerer dazwischenliegender Abhängigkeiten wie Bindungen und Empfängerbeziehungen auflösen lässt, wie aus der zugehörigen Regel InferredParameterizedTypeNoWildcard
ersichtlich ist:
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
InferredParameterizedTypeNoWildcard
for all
expression: Java.TypeVariableTypedExpression
reference: Java.TypedReference
declaration: Java.TypeVariableTypedEntity
receiverExpression: Java.Expression
receiverReference: Java.TypedReference
receiverDeclaration: Java.TypedEntity
receiverDeclarationType: Java.TypeReference
typeVariable: Java.TypeVariable
genericType: Java.Type
typeParameterBinding: Java.SimpleTypeParameterBinding
do
if
Java.isExpression(reference, expression),
Java.receiver(reference, receiverExpression),
Java.isExpression(receiverReference, receiverExpression),
Java.binds(receiverReference, receiverDeclaration),
Java.definesTypeOf(receiverDeclarationType, receiverDeclaration),
Java.binds(reference, declaration),
767
768
769
Java.typeOf(declaration, typeVariable),
Java.typeVariableOf(typeVariable, genericType),
770
771
772
773
Java.typeParameterBinding(typeVariable, typeParameterBinding),
Java.typeContextOf(receiverDeclaration, typeParameterBinding)
then
. . Typ-Constraints für generische Typen
Tabelle . .: Die generische Typen betreffenden Faktendeklarationen
Fakt
Parameter
Bedeutung
typeOf
TypeVariableTypedEntity
Weist jeder mit Typvariable typiTypeVariable
sierten Deklaration die Typvariable
zu, mit welcher sie typisiert wurde
typeVariableOf
TypeVariable
Weist jeder Typvariable den Typen
Type
zu, in dem die deklariert wurde
Gibt zu jeder Belegung einer TypvatypeReferenceFor
TypeReference
TypeParameterBinding
riablen die entsprechende Typreferenz an, mit welcher die Belegung
durchgeführt wurde
typeContextOf
TypedEntity
Gibt zu jeder Belegung einer TypvaTypeParameterBinding
riablen an, für welche Deklaration
die Belegung sta gefunden hat
typeParameterTypeVariable
Gibt zu jeder Belegung einer TypvaBinding
TypeParameterBinding
riablen an, welcher Typparameter
belegt wurde
typeVariableBound
TypeVariable
Gibt für jede Schranke einer TypvaTypeOrTypeVariableReference
riablen die entsprechende Typreferenz dieser Schranke an
wildcardBound
WildcardTypeParameterBinding
Ordnet Wildcards ihre jeweiligen
TypeOrTypeVariableReference
oberen oder unteren Schranken zu
typeParameterTypeParameterBinding
Gibt Paarungen zusammengehöriAssignment
TypeParameterBinding
ger Typparameterbelegungen bei
Zuweisung von Ausdrücken generischen Typs an
expression.expressionType = typeParameterBinding.boundType
774
775
end
Tabelle . listet die für generische Typen hinzukommenden und teilweise in der Regel InferredParameterizedTypeNoWildcard verwendeten Fakten mit entsprechenden Erläuterungen
auf. Abbildung . zeigt einen an obiges Box<T>-Beispiel angelehnten Quellcodeausschni
und die für diesen generierten Fakten und Programmelemente, wie sie für die Regel InferredParameterizedTypeNoWildcard relevant sind. Das bei dieser Faktenlage generierte Constraint
besagt dann nur noch, dass der Ausdruck stringBox.contents denselben Typ wie die Typparameterbelegung der Variablendeklaration Box<String> contents haben muss.
Während das Beispiel aus Abbildung . nur eine einzige Typparameterbelegung, nämlich die von T auf String zeigt, können in komplexeren Programmen noch weitere Belegungen
hinzukommen. Einerseits kann ein Typ mehr als einen Parameter haben, andererseits können auch die Typparameter eines Typen mehr als nur einmal belegt werden, wie in obigem
Beispiel in Zeile
und
gezeigt. Die Lösung hierfür ist, für jede Variable und jede Belegung ein weiteres Programmelement TypeParameterBinding mitsamt typeContextOf- und typeParameterBinding-Fakten zu erzeugen. Für jedes dieser Programmelemente findet die Regel
. Constraintregeln für Java
genericType : Type
typeVariableOf
class Box<T> {
typeVariable : TypeVariable
typeParameterBinding
typeParameterBinding:
SimpleTypeParameterBinding
typeOf
T contents;
typeContextOf
declaration : TypeVariableTypedEntity
receiverDeclaration : TypedEntity
Box<String> stringBox;
definesTypeOf
receiverDeclarationType : TypeReference
binds
binds
expression: TypeVariableTypedExpression
receiver
String s = stringBox.contents;
receiverExpression :
Expression
isExpression
isExpression
reference :
TypedReference
}
receiverReference :
TypedReference
Abbildung . .: Programmbeispiel einer generischen Klasse Box<T> und ihrer Verwendung.
In grau sind (ausschni sweise) die sich aus einzelnen Programmteilen ergebenden Programmelemente gekennzeichnet. Grüne Linien und Beschriftungen zeigen, wie diese anhand von Fakten in Beziehung stehen. Für das
gegebene Beispiel würde die Constraintregel InferredParameterizedTypeNoWildcard ein Constraint generieren, welches den Wert der Constraintvariable boundType von typeParameterBinding zur Übereinstimmung mit dem
Wert des expressionType von expression zwingt, also für den Ausdruck
stringBox.contents denselben Typen inferiert, wie er bei der Deklaration der
stringBox als Typparameter angegeben wurde.
. . Typ-Constraints für generische Typen
InferredParameterizedTypeNoWildcard dann ein weiteres Mal Anwendung.
5.4.2. Beschränkt parametrische Typen
In Java können für Typparameter (obere) Schranken angeben werden ( § . ), was einem
beschränkt parametrischen Polymorphismus (engl. bounded polymorphism) [CW ] entspricht. Die
Belegung der Typvariablen eines generischen Typen ist dann nur noch möglich, soweit die
für die Variablen eingese ten Typen mindestens den angegebenen Schranken genügen. In
folgendem Beispiel darf der Typparameter T der Klasse RunnableBox nur noch mit Typen belegt werden, die java.lang.Runnable implementieren.
776
777
778
779
780
class RunnableBox<T extends Runnable>{
T contents;
RunnableBox<:::::
String> stringBox;
RunnableBox<Thread> threadBox;
}
Der Versuch, in Zeile
, den Typparameter T mit String zu belegen, schlägt fehl, während
die Belegung mit Thread als Subtyp von runnable in Zeile
erfolgreich ist.
Die Angabe der Existenz einer oberen Schranke wird in der Refacola-Umse ung mi els eines Fakts typeVariableBound umgese t, welches direkt von der Typvariablen auf die Typreferenz der oberen Schranke verweist. Mi els folgender, gleichnamiger Regel TypeVariableBound
wird sichergestellt, dass Typvariablen nur mit Subtypen der jeweiligen oberen Schranken belegt werden.
781
782
783
784
785
786
787
788
789
790
791
792
TypeVariableBound
for all
typeVariable: Java.TypeVariable
typeVariableBound: Java.TypeReference
typeParameterBinding: Java.SimpleTypeParameterBinding
do
if
Java.typeVariableBound(typeVariable,typeVariableBound),
Java.typeParameterBinding(typeVariable, typeParameterBinding)
then
Java.sub*(typeParameterBinding.boundType, typeVariableBound.typeBinding)
end
Ein wenig bekanntes, aber dennoch in der Sprache Java enthaltenes Feature ist die Angabe
mehrerer oberer Schranken für eine Typvariable. So lassen sich mi els des &-Trennzeichens
weitere obere Schranken angeben, wie folgendes Beispiel zeigt:
793
794
795
class RunnableReadableBox<T extends Runnable & Readable> {
T contents;
}
Der Typparameter T darf für den Typen RunnableReadableBox nur noch mit solchen Typen belegt werden, die sowohl Runnable als auch Readable implementieren, es entsteht ein sogenannter intersection type ( § . ). In diesem Fall wird für jede weitere obere Schranke ein weiteres
typeVariableBound-Fakt erzeugt, somit auch ein weiteres Constraint nach der TypeVariableBound-Regel, welches mögliche Belegungen der Typvariablen T dahingehend einschränkt,
auch alle weiteren Schranken berücksichtigen zu müssen.
. Constraintregeln für Java
Die obige Constraintregel TypeVariableBound hat gezeigt, dass durch den Einsa beschränkt
parametrischer Typen im Programm zusä liche Bedingungen einzuhalten sind, die der Nü lichkeit eines solch beschränkten Typen im Wege stehen können. Daher ist es logisch, dass um
den Einsa von Typschranken zu rechtfertigen, sich umgekehrt auch ein Nu en aus diesen
ergeben muss. Während Typvariablen ohne Typschranken Vorteile in erster Linie außerhalb
des generischen Typen, also bei den Verwendern, erzeugen, die für die Belegungen der Typvariablen verantwortlich sind, tri der Vorteil der beschränkten Typvariablen innerhalb des
generischen Typen auf. Hier können dann nämlich konkrete Annahmen über die Typen hinter den Typvariablen gemacht werden. Für das Beispiel der RunnableReadableBox aus Zeile
kann dann nämlich angenommen werden, dass durch das mit der Typvariablen T typisierte
Feld contents sowohl die Methoden aus Runnable als auch Readable implementiert sind, was
folgende Aufrufe möglich macht:
796
797
contents.run(); // declared in java.lang.Runnable
contents.read(null); // declared in java.lang.Readable
Auch wenn Typparameterschranken in der Refacola als Typreferenzen (TypeReference) mit
Variablen für ihre jeweilige Bindung modelliert sind, so wird eine Änderung dieser Bindung
und damit der Typschranken nicht unterstü t.
Der Grund hierfür besteht in der immens großen Menge potentieller Typen, die sich für
mit Typvariablen typisierte Ausdrücke (im obigen Beispiel contents) ergeben kann. Durch
das mögliche Vorkommen beliebig vieler, mit & konkatenierter oberer Schranken wächst die
Menge sich möglicherweise ergebender intersection types exponentiell (nämlich als Potenzmenge) mit den im Programm definierten Typen. Abgesehen davon, dass die Refacola derzeit
außer den in Abschni . . beschriebenen programmabhängigen Domänen keine weiteren
Möglichkeiten bietet, Domänen in Abhängigkeit von der Faktenbasis zu definieren, lässt sich
vermuten, dass ein Verfahren wie bei der Refacola, welches die Domänen vorab vollständig
berechnet, bei exponentiellem Wachstum der Domänengröße zu keinen befriedigenden Laufzeiten führen kann.
Da die Schranken für Typvariablen unveränderlich sind, entfällt auch die Notwendigkeit,
über Constraints Typen für Ausdrücke zu inferieren, die sich aus Typschranken ergeben würden. Auch diese können dann nur unverändert bleiben. Soweit eine Typvariable keiner Beschränkung unterliegt, kann ihr Typ ohnehin nur als Object angenommen werden. In folgendem Beispiel eines Typen mit unbeschränkter Typvariable T ergibt sich in Zeile
für den
Ausdruck contents als einzig inferierbarer Typ Object.
798
799
800
801
802
803
public class Box<T> {
T contents;
Object m() {
return contents;
}
}
Ist eine obere Typschranke wie im Beispiel der Klasse RunnableBox auf Seite
gegeben,
kann der inferierte Typ genau der oberen Schranke gleichgese t werden. Gibt es mehrere
, so ist
Schranken zur Auswahl, wie im obigen Beispiel der RunnableReadableBox auf Seite
eigentlich der sich aus allen Typschranken ergebende intersection type zu inferieren ( § . ).
Um aber der erwähnten Problematik zu entgehen, diese künstlichen Typen zu berechnen und
. . Typ-Constraints für generische Typen
in die Domäne der inferierten Typen einzupflegen, genügt es, auch nur jeweils den Typen als
inferierten Typen anzunehmen, der im jeweiligen Kontext benötigt wird. Für den Ausdruck
contents in Zeile
wäre dies Runnable, für den gleichlautenden Ausdruck in Zeile
wäre
dies Readable.
Weiterhin entfällt durch nicht-variable Typschranken die Notwendigkeit, bei der Angabe
dieser Schranken auf Typkonformität zu achten, die sich aus der Schachtelung von generischen Typen ergeben kann. In folgendem Beispiel muss der Typparameter S von StorageRack
mit einer oberen Schranke von (mindestens) Runnable versehen sein, da S wiederum Einsa
in der oberen Schranke von R findet, welches der Typvariablen T von RunnableBox zugewiesen
wird, für welches ebenso die Typschranke von Runnable gilt.
804
805
806
class RunnableBox<T extends Runnable> {
T contents;
}
807
808
809
810
public class StorageRack<S extends Runnable, R extends RunnableBox<S>> {
/* ... */
}
5.4.3. Zuweisungen an mit generischen Typen typisierte Ausdrücke
Für Ausdrücke, deren Typ einer generischen Klasse entspricht, gelten bei Zuweisungen dieselben Typregeln wie bei Ausdrücken nicht generischen Typs.²⁹ Auch weiterhin muss bei einer
Zuweisung der inferierte Typ des Ausdrucks linker Hand derselbe oder ein Supertyp dessen
sein, was sich nach Typinferenz für den Ausdruck rechter Hand ergibt.
Sind die inferierten Typen darüber hinaus auch noch mit Typvariablen versehen, so genügt
für deren Belegung keine Subtypbeziehung. Subtypbeziehungen zwischen generischen Typen
gelten bei belegten Typvariablen nur, wenn diese tatsächlich übereinstimmend belegt sind
die Zuweisung von einer
( § . . ). In folgendem Beispiel gelingt also weder in Zeile
Box<Object> an eine Box<String>, noch der umgekehrte Fall in Zeile
.
811
812
public class Box<T> {
T contents;
813
void m() {
Box<String> stringBox = ...;
Box<Object> objectBox = ...;
stringBox = objectBox;
::::::::
objectBox = ::::::::
stringBox;
}
814
815
816
817
818
819
820
}
An dieser Stelle zeigt sich ein deutlicher Unterschied zwischen generischen Typen und ArrayTypen. Bei le teren ist eine Zuweisung zwischen Arrays mit unterschiedlichen Elemen ypen
gesta et, solange diese in einer Subtypbeziehung stehen ( § . . ). Allerdings kann dies
²⁹ Mit Zuweisungen sind in diesem Abschni nicht nur die klassischen Zuweisungen durch den =-Operator gemeint, sondern ebenso auch die sich aus Parameterübergaben und Methoden-return ergebenden indirekten
Zuweisungen.
. Constraintregeln für Java
zu Laufzeitfehlern führen, wenn auf einem mit einem Super-Elemen yp deklarierten Alias
Elemente eingefügt werden, die nicht mehr dem Typen des Ursprungs-Arrays entsprechen.
Solche Fehler werden dann nicht mehr durch den Compiler erkannt, sondern zur Laufzeit
durch eine ArrayStoreException deutlich. Folgender Codeausschni zeigt in Zeile
eine solche Zuweisung, die in der folgenden Zeile
einen Laufzeitfehler provoziert, da ein Long
nicht in ein Integer-Array eingefügt werden kann.
821
822
Number[] numbers = new Integer[] { new Integer(1) };
numbers[0] = new Long(23);
Beim Entwurf der generischen Typen in Java war es ein Ziel, genau solche Laufzeitfehler zu
verhindern, weswegen Java Zuweisungskompatibilität nur noch bei übereinstimmend belegten Typvariablen vorsieht. In Ergänzung zu den Regeln Assignment1 (siehe S.
, ebenso
) und Assignment2 ( S.
), die auch für Array-Typen greifen, werden die sich zusä S.
lich aus den Belegungen der Typvariablen ergebenden Einschränkungen über die folgende
Regel TypeParameterAssignment zugesichert. Das Fakt typeParameterAssignment kennzeichnet dabei die entsprechenden Paare von Typvariablenbelegungen, deren Gleichheit über die
gleichnamige Regel sichergestellt wird.
823
824
825
826
827
828
829
830
831
832
TypeParameterAssignment
for all
lhs: Java.SimpleTypeParameterBinding
rhs: Java.SimpleTypeParameterBinding
do
if
Java.typeParameterAssignment(lhs,rhs)
then
lhs.boundType = rhs.boundType
end
Auch wenn die vorstehende Regel relativ einfach wirkt, wohnt ihr ein wenig mehr Komplexität inne, als sie vermuten lässt. Diese Komplexität ergibt sich aus dem schon angesprochenen
Fakt typeParameterAssignment. Dieses verweist nämlich nicht direkt auf die Ausdrücke linker
und rechter Hand der Zuweisung, sondern schon direkt an die Stellen, an denen die jeweiligen Typparameter belegt werden. Einerseits ist zu beachten, dass dabei auch schon ineinander
geschachtelte generische Typen berücksichtigt werden, also solche generischen Typen, deren
Typvariablen wiederum mit generischen Typen belegt sind. Ebenso werden aber auch Fälle abgedeckt, bei denen Typvariablen eines Supertypen im Laufe der Vererbungshierarchie
innerhalb der Deklarationen der Subtypen belegt wurden.
In folgendem Beispiel würde für die Zuweisung der neu erzeugten StringBox an die Variable myBox in Zeile
ein Fakt typeParameterAssignment erzeugt werden, welches linksseitig
auf die Belegung der Typvariablen myBox mit String in Zeile
referenziert, rechtsseitig auf
für alle Instanzen von
die Belegung der von Box geerbten Typvariablen T, die in Zeile
StringBox mit String belegt wird.
833
834
835
public class Box<T> {
T contents;
}
836
837
class StringBox extends Box<String> {
. . Typ-Constraints für generische Typen
void m() {
Box<String> myBox = ...;
myBox = new StringBox();
}
838
839
840
841
842
}
5.4.4. Wildcards
Die im vorangegangenen Abschni behandelte Beschränkung, in Java nur solche generisch
typisierten Ausdrücke einander zuweisen zu können, die identische Belegungen ihrer Typvariablen aufweisen, erhöht zwar die Typsicherheit, bringt für den Programmierer aber empfindliche Einschränkungen, was den Umgang mit generischen Typen anbelangt. Insbesondere
für Programmteile, die mit generischen Typen umgehen müssen, aber innerhalb derer die generisch typisierten Deklarationen des generischen Typen keine Verwendung finden, sind die
Einschränkungen unnötig. Um diesem Anwendungsfall gerecht zu werden, unterstü t Java
mi els des ?-Operators sogenannte Wildcards ( § . . ). Eine Wildcard entspricht der Belegung einer Typvariablen mit einem beliebigen Typen. Entsprechend ist ein mit Wildcards
belegter generischer Typ auch immer Supertyp jeder weiteren Parametrisierung dieses Typen.
Folgendes Beispiel zeigt eine Wildcard-Belegung eines Typparameters T in Zeile
sowie eine gültige Zuweisung eines Ausdrucks vom Typ Box<String> an einen Ausdruck vom Typ
Box<Object>.
843
844
public class Box<T> {
T contents;
845
public static void main(String[] args) {
Box<String> stringBox = null;
Box<?> wildCardBox = null;
wildCardBox = stringBox;
}
846
847
848
849
850
851
}
Der Preis für damit erhaltene Zuweisungskompatibilität besteht in nur beschränkten Möglichkeiten, auf mit Wildcards typisierte Member zugreifen zu können. Zuweisungen³⁰ an eine
solche Variable sind – außer einer Zuweisung an null – generell untersagt, da der Typ einer
Wildcard über die sogenannten capture conversions in Java ( § . . ) nur zum null-Type gewandelt werden kann. Wird lesend auf eine solche Variable zugegriffen, kann für sie lediglich
der Typ Object inferiert werden. In folgender Ergänzung obigen Beispiels führen beide Zuweisungen zu einem Compilerfehler – einmal in Zeile
wegen des Verbots der Zuweisung
, da für den rechtsseitigen Ausdruck sich der Typ
an Wildcard-Typen und einmal in Zeile
Object ergibt, der einem String nicht zugewiesen werden darf.
852
853
wildCardBox.contents = ::::
new:::::::
String();
String theContents = wildCardBox.contents;
::::::::::::::::::
³⁰ Auch hier sind weiterhin nicht nur die Zuweisungen durch den =-Operator gemeint, sondern auch die impliziten bei Parameterübergaben und return-Anweisungen auftretenden.
. Constraintregeln für Java
Obere und untere Schranken für Wildcards Wie das Beispiel der ArrayStoreException auf
Seite
gezeigt hat, war das bei generischen Typen zu vermeidende Problem die Möglichkeit, über ein Alias eines Arrays Supertypen des Elemen ypen des Arrays in dieses einzufügen. Erweitert man das Beispiel wie folgt, sieht man, dass eine Entnahme von Elementen wie
in Zeile
wiederum typsicher ist, während lediglich das Einfügen in Zeile
zu einem
Laufzeitfehler führt.
854
855
856
Number[] numbers = new Integer[] { new Integer(1) };
Number myNumber = numbers[0];
numbers[0] = new Long(23); // ArrayStoreException
Der Wunsch, weiterhin Elemente bei mit Wildcards belegten generischen Typen typsicher entnehmen³¹ zu können, ohne gleichzeitig auf die Typsicherheit beim Einfügen von Elementen
zu verzichten, führte zur Einführung von oberen Schranken für Wildcards in Java ( § . . ).
Eine solche Schranke gibt an, dass die mit der beschränkten Wildcard typisierten Elemente
mindestens diesem Typ entsprechen. Somit lässt sich obiges Array-Beispiel analog mit beschränkten Wildcards umse en, wobei nun aber der Laufzeitfehler aus Zeile
schon durch
den Compiler in Zeile
erkannt wird.
857
858
859
Box<? extends Number> numbers = new Box<Integer>();
Number myNumber = numbers.contents;
numbers.contents = ::::
new::::::::
Long(23);
Die eigentlich entscheidende Typprüfung findet dabei schon in Zeile
sta , in welcher geprüft wird, ob die Typvariable rechter Hand der Zuweisung (Integer) tatsächlich einem Supertyp der oberen Schranke der Wildcard linker Hand (Number) entspricht.
Den umgekehrten Fall zu oberen Schranken, nämlich untere Schranken, unterstü t Java
ebenso ( § . . ). Bei diesen gilt für die Zuweisung an Wildcard-Typen aber die genau umgekehrte Bedingung, nämlich dass bei der Zuweisung an einen Wildcard-Typen die Typvariable rechter Hand der Zuweisung einem Subtyp der oberen Schranke der Wildcard linker
Hand entspricht. Dann ist umgekehrt auch das typsichere Einfügen von Elementen möglich,
die mindestens dieser Schranke entsprechen, während entnommene Elemente nur noch vom
Typ Object sind, wie folgendes Beispiel zeigt:
860
861
862
863
Box<? super Number> numbers = new Box<Object>();
numbers.contents = new Long(23);
Number myNumber = numbers.contents;
:::::::::::::::
Object anObject = numbers.contents;
Bei den oberen Schranken von Wildcards ergibt sich ein interessanter Spezialfall, wenn diese
als Object angegeben wird. Auch dann ist weiterhin ein Einfügen von Elementen nicht möglich, bei der Entnahme kann nur noch der Typ Object angenommen werden. Das Verhalten
entspricht damit exakt dem einer Wildcard ohne jede Schranke. Aus diesem Grund werden in
³¹ Es ist nicht ganz korrekt, bei generischen Typen von der Entnahme oder dem Einfügen von Elementen zu sprechen, implementiert doch keineswegs jeder generische Typ zwingend einen Container. Dennoch sind Container ein häufiger Anwendungsfall für generische Typen und auch das in diesem Kapitel allgegenwärtige
Beispiel einer verschiedentlich Typ-parametrisierten Klasse Box folgt diesem Muster. Korrekterweise ist sta
vom Einfügen eines Elements besser von einer Zuweisung an eine mit der jeweiligen Typvariablen typisierten
Variable zu sprechen, beim Entnehmen eines Elements von der Referenzierung dieser Variablen. Bei Methoden
gilt entsprechendes für die Argumentübergabe an mit Typvariablen typisierte Methodenparameter.
. . Typ-Constraints für generische Typen
der vorliegenden Implementierung Wildcards ohne Schranken gar nicht erst explizit berücksichtigt. Vielmehr wird beim Einlesen des Programmcodes für diese schon implizit eine obere
Schranke Object hinzugefügt.
Constraintregeln für Wildcards Belegungen von Typvariablen mit Wildcards werden in
vorliegender Implementierung mi els einer Variablenart WildcardTypeParameterBinding, einer abstrakten, von TypeParameterBinding abgeleiteten Unterart, beschrieben. Abhängig davon, ob eine Wildcard mit einer oberen oder unteren Schranke versehen ist, kommen die
Unterarten UpperBoundTypeParameterBinding oder LowerBoundTypeParameterBinding zur Anwendung. Mit der jeweils angegebenen Typschranke (einer TypeOrTypeVariableReference) sind
diese dann über ein Fakt wildcardBound verbunden.
Die bisher in den vorangegangenen Abschni en genannten Constraintregeln zur Typinferenz generisch typisierter Ausdrücke und Zuweisung zwischen generischen Typen finden bei
Wildcards keinerlei Anwendung, da sich diese nur auf mit konkreten Typen belegte Typvariablen (SimpleTypeParameterBinding) beziehen, nicht aber auf WildcardTypeParameterBinding.
Dieses Verhalten ist erwünscht, da für Wildcards ohnehin abweichende Regeln gelten müssen.
Die Typinferenz von Wildcard-typisierten Ausdrücken gestaltet sich noch weitgehend analog zur Regel InferredParameterizedTypeNoWildcard (siehe S.
, ebenso S.
). Die ähnlich
) wählt dabei lediglich sta digestaltete Regel InferredParameterizedTypeForWildcard ( S.
rekt den die Typvariable belegenden Typen die jeweils obere beziehungsweise untere Schranke der Wildcard aus.
Schreibende Zugriffe auf mit beschränkten Wildcards typisierte Member benötigen keine
Behandlung. Zwar darf diesen – wie oben ausgeführt – nur null zugewiesen werden, allerdings
sind derzeit weder Constraintvariablen vorgesehen, die obere oder untere Schranken entfernen oder Wildcards durch konkrete Belegungen ändern, noch solche, die ein null-Ausdruck in
irgendeiner Weise beeinflussen. Daher kann sich ein Fehler hieraus während der Constraintlösung gar nicht erst ergeben. Für lesende Zugriffe bei nach unten beschränkten Wildcards
) den Typ des Ausdrucks rechter
schränkt die Regel UpperBoundWildcardAssignments ( S.
Hand auf Object ein, was Zuweisungen an nicht Object-typisierte Variablen unterbindet.
Zule t müssen noch analog zur Regel TypeParameterAssignment die Zuweisungen zwischen Ausdrücken beschränkt werden, die generischen Typs sind. Tabelle . listet die möglichen Fälle von Zuweisungen generischer Typen mit und ohne Wildcards – le tere mit und
ohne Schranken – auf und gibt dabei nötige Bedingungen für die Belegungen der Typvariablen an, wie sie sich aus den Regeln der type conversion ( § . ) und insbesondere der capture
conversion ( § . . ) für Java ergeben. So ergibt sich aus der Tabelle zum Beispiel für die Zuweisung in Zeile
aus obigem Beispiel, dass für die linksseitige Belegung der Typvariable
mit der nach oben beschränkten Wildcard <? extends Number> und rechtsseitigen Belegung
<Integer>, dass der rechts eingese te Typ (Integer) ein Subtyp von der linksseitigen Schranke
(Number) sein muss. Für die Zuweisung aus Zeile
ergibt sich bei linksseitiger und nach
unten beschränkter Wildcard <? super Number> und rechtsseitiger Belegung mit <Object> die
Bedingung, dass die Schranke links (Number) Subtyp der Belegung rechts (Object) sein muss.
Nicht alle Bedingungen der Tabelle . bedürfen (noch) der Umse ung mit Constraintregeln. Der Fall ohne Wildcards auf beiden Seiten ist schon mit der Regel TypeParameterAssignment aus Abschni . . abgehandelt. Die drei Fälle, in denen Zuweisungen unter kei-
. Constraintregeln für Java
Tabelle . .: Einzuhaltende Bedingungen an die Belegungen von Typvariablen bei einer Zuweisung x=y; zwischen Ausdrücken generischen Typs.
Belegung der
i-ten Typvariable
rechtsseitig
<B>
<? extends B>
<? super B>
Belegung der i-ten Typvariable linksseitig
<A> <? extends A>
<? super A>
A=B
sub*(B,A)
sub*(A,B)
false
sub*(B,A)
false
false
A = Object
sub*(A,B)
nen Umständen möglich sind (false), bedürfen ebenso keiner Berücksichtigung. Wandlungen von mit Wildcards belegten Typvariablen zu mit regulären Typen belegten Typvariablen
(und umgekehrt) werden derzeit nicht unterstü t. Da es keine Constrainvariablen hierfür
gibt, kann während der Constraintlösung auch keine Situation provoziert werden, die zu
einem solchen Fehler führen kann. Besteht der Fehler schon vor der Transformation, ist er
mit der derzeitigen Implementierung nicht behebbar. Ein fehlschlagendes Constraint würde
aber auch nicht helfen, da dieses weitere, mitunter an anderer Stelle sinnvolle Transformationen blockieren würde. Die übrigen fünf Fälle aus Tabelle . werden durch die Constraintregeln TypeParameterAssignmentWildcards1 ( S.
) bis TypeParameterAssignmentWildcards5
( S.
) abgehandelt.
5.4.5. Raw-Types
Um vorhandenen Code, welcher vor Java entwickelt wurde, gemeinsam mit neueren Programmteilen betreiben zu können, die generische Typen verwenden, erlaubt Java die Nu ung
sogeannter raw types ( § . ). Ein raw type ergibt sich durch die Benu ung eines generischen Typen ohne Angabe von Typvariablen. Vom Punkt der Referenzierung aus scheint es
also, als ob der verwendete Typ keine Typvariablen deklarieren würde. Der Unterschied zu
den ähnlich anmutenden – weil auch ohne konkret belegte Typvariablen daherkommenden –
Wildcard-Typen ergibt sich dabei aus signifikant gelockerten Typregeln. So sind bei der Zuweisungskompatibilität von generischen Typen die Belegungen der Typvariablen irrelevant,
sobald ein raw type beteiligt ist. Auch ist lesender und schreibender Zugriff auf die mit Typvariablen typisierten Member eines raw types stets möglich, wobei die Typvariablen stets als
Object angenommen werden. Als Folge kann bei Verwendung von raw types die Typsicherheit durch den Compiler nicht mehr zugesichert werden, weswegen die Verwendung von raw
types auch Warnungen durch den Compiler auslöst und nur als Notlösung betrachtet werden
sollte, die lediglich die Verwendung älteren Codes ermöglichen soll.
In der vorliegenden Implementierung erfahren raw types keine gesonderte Behandlung.
Vielmehr werden sie schon dadurch korrekt behandelt, dass bei ihrer Verwendung aufgrund
der fehlenden Typisierungsinformationen gar nicht erst TypeParameterBinding-Programmelemente angelegt werden, somit auch die vorstehenden Constraintregeln zu generischen Typen keine Anwendung finden und keine zusä lichen Einschränkungen bringen, was den
Eigenschaften der raw types entspricht. Da auch bei raw types weiterhin gilt, dass derzeit
keine Programmtransformationen vorgesehen sind, die zusä liche Typinformationen einfü-
. . Namens-Constraints
gen, werden keine Refaktorisierungswerkzeuge unterstü t, die die Erse ung von raw types
zugunsten parametrisierter Typen zur Folge haben.
5.5. Namens-Constraints
Namen werden in Java genu t, um auf deklarierte Elemente im Programm wie Pakete, Typen,
Member, Parameter und lokale Variablen Bezug nehmen zu können ( § ). Entsprechend
tauchen Namen an zwei Stellen im Programm auf, einerseits bei Deklarationen, andererseits
bei Referenzen. Folgerichtig deklariert die Refacola-Sprachdefinition für Java Subtypen von
Entity und Reference namens NamedEntity und NamedReference, die jeweils eine Variablenart
identifier deklarieren. Es leiten dann genau die Deklarationen und Referenzen von NamedEntity und NamedReference ab, die einen Namen in sich tragen, und erben somit die passenden
identifier-Variablen.
5.5.1. Bindung anhand von Namen
Die offensichtlichste, sich aus Namen ergebende Bedingung ist, dass bei Bindung einer benannten Referenz an eine benannte Deklaration die verwendeten Namen beider übereinstimmen müssen. Die folgende Regel ReferenceIdentifier se t dies für Bindungen an lokale Variablen, formale Parameter, Felder und Methoden um:
864
865
866
867
868
869
870
871
872
873
ReferenceIdentifier
for all
reference: Java.NamedTypedReference
entity: Java.NamedEntity
do
if
Java.binds(reference, entity)
then
reference.identifier = entity.identifier
end
Dasselbe muss ebenso für Referenzen zu Typen gelten. Die Regel TypeReferenceIdentifier se t
dies analog unter Berücksichtigung variabler Typbindungen um:
874
875
876
877
878
879
880
881
882
883
884
885
TypeReferenceIdentifier
for all
type: Java.NamedType
ref: Java.TypeReference
do
if
all(type),
all(ref)
then
(type = ref.typeBinding)
−> (type.identifier = ref.identifier)
end
. Constraintregeln für Java
5.5.2. Eindeutigkeit von Namen
Bindungen anhand von Namen sind nur möglich, soweit die verwendeten Namen in ihrem
jeweiligen Kontext eindeutig sind. Die bereits erwähnte Regel AmbigousFieldAccess ( S.
)
ist eine solche, die verhindert, dass eine Referenz mehrdeutig wird. Darüber hinaus gibt die
Java-Sprachspezifikation Regeln vor, die uneindeutige Namen schon im Vorhinein verhindern sollen. So dürfen zum Beispiel niemals zwei gleich benannte Typen ineinander geschachtelt sein ( §§ . , . ), was über die folgende Regel UniqueMemberTypeNames1 umgese t
wird:
886
887
888
889
890
891
892
893
894
895
UniqueMemberTypeNames1
for all
t: Java.MemberType
do
if
all(t)
then
t.identifier != t.enclosingNamedTypes.identifier,
t.identifier != t.tlowner.identifier
end
Zu beachten bei dieser Regel ist, dass das für MemberType definierte enclosingNamedTypes eine Sequenz ist (siehe Abschni . . ), welche jeweils alle umschließenden Typen eines Membertypen beinhaltet. Diese Regel generiert also für jeden umschließenden Typen ein einzelnes
Constraint, was nur unter der Bedingung korrekte Ergebnisse liefert, dass die Menge der umschließenden Typen eines Membertypen nicht variabel ist (eine Annahme, die in Abschni .
begründet wurde). Sollen hingegen Membertypen ihre umschließenden Typen ändern können, würde dies Mengen-Constraints [Sto ] erfordern, deren Formulierung und Lösung das
Refacola-Framework derzeit nicht unterstü t.
Weiterhin gibt es mehrere Bedingungen, die gleich benannte Deklarationen unmi elbar
innerhalb einer sie gemeinsam umschließenden Deklaration verbieten. Innerhalb eines Typen dürfen nicht zwei Typen oder Felder gleichen Namens deklariert sein ( § . ), ebenso wie nicht mehrere lokale Variablen innerhalb einer Methode gleichen Namen tragen dürfen oder mit den Namen von etwaigen Methodenparametern in Konflikt stehen dürfen. Die
Regeln UniqueMemberTypeNames2 ( S.
), UniqueFieldIdentifier ( S.
) sowie UniqueLocalVariableNames1 ( S.
), UniqueLocalVariableNames2 ( S.
) und UniqueLocalVariableNames3 ( S.
) decken diese Fälle ab.
Da Methoden in Java nicht nur anhand ihres Namens, sondern auch anhand ihrer Signatur unterschieden werden können, sind mehrere Methoden gleichen Namens innerhalb einer
Klasse gesta et und auch ein häufig anzutreffendes Muster – das sogenannte Überladen von
Methoden ( § . . ). Allerdings müssen ihre Signaturen dann unterschiedlich sein ( § . ),
was die folgende Regel UniqueMethodIdentifier sicherstellt.
896
897
898
899
900
901
UniqueMethodIdentifier
for all
method1: Java.RegularTypedMethod
method2: Java.RegularTypedMethod
do
if
. . Namens-Constraints
902
903
904
905
906
907
908
method1 != method2
then
(method1.owner = method2.owner)
and (method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType)
−> (method1.identifier != method2.identifier)
end
Zu guter Le t gilt, dass innerhalb eines Pakets nicht mehrere Toplevel-Typen gleichen Namens deklariert sein dürfen ( § . ), was die Regel UniqueTopLevelTypeIdentifier ( S.
)
sicherstellt.
5.5.3. Namen überschreibender Methoden
Methodenüberschreibung ist in Java nur zwischen Methoden mit identischen Namen möglich
( § . . . ). Die Regel OverridingMethodNames ( S.
) kann recht einfach sicherstellen, dass
beim Umbenennen einer Methode ebenso überschreibende oder überschriebene Methoden
mit umbenannt werden.
Darüber hinaus kann die Situation bestehen, dass auch ohne direkte Methodenüberschreibung eine Klassenmethode eine Interfacemethode implementiert, wie bereits im Zusammenhang mit Zugrei arkeiten in einem Beispiel auf Seite
gezeigt wurde. Die Regel EagerInterfaceMethodNames ( S.
) sorgt entsprechend dafür, dass auch bei vorausschauenden
Überschreibungen die Methodennamen weiterhin übereinstimmen.
5.5.4. Weitere Namensregeln
Eine Besonderheit bei Java ist – und das scheint auf den ersten Blick den gerade zuvor erläuterten Regelungen, Ambiguitäten vermeiden zu wollen, zuwiderzulaufen – dass Konstruktoren
den gleichen Namen wie die jeweils umschließende Klasse tragen müssen ( § . ). Faktisch
ist dies natürlich kein Problem, da Konstruktoraufrufe schon rein syntaktisch anhand der stets
übergebenen (wenn auch gegebenenfalls leeren) Parameterliste von Typreferenzen zu unterscheiden sind. Mi els des Fakts instantiates, welches jedem Konstruktor den durch diesen
instanziierten Typen zuordnet, lässt sich diese Bedingung in Refacola einfach formulieren:
909
910
911
912
913
914
915
916
917
918
ConstructorNames
for all
type: Java.NamedType
constructor: Java.Constructor
do
if
Java.instantiates(constructor, type)
then
type.identifier = constructor.identifier
end
Weitere Bedingungen aus Namen ergeben sich bei TopLevelTypen, welche – wenn sie public
deklariert sind – den Namen ihrer umschließenden Überse ungseinheit (TypeRoot) tragen
müssen ( § . ). Da hierbei etwaige Dateinamenserweiterungen ohnehin auszusparen sind
und diese auch je nach Zielsystem unterschiedlich sein können (bei Java-Quellcode .java oder
. Constraintregeln für Java
auch .jav), wird der Name einer TypeRoot intern stets ohne Dateinamenserweiterung dargestellt. Dieser muss bei der Faktengenerierung abhängig vom Zielsystem korrekt entfernt beziehungsweise beim Zurückschreiben korrekt wieder hinzugefügt werden. Daher kann in der
folgenden Regel TopLevelTypeNames, die bereits ganz ähnlich bei [Wag ] zu finden ist, tatsächlich Gleichheit der beiden identifier gefordert werden:
919
920
921
922
923
924
925
926
927
928
929
TopLevelTypeNames
for all
tlt: Java.TopLevelType
tr: Java.TypeRoot
do
if
all(tlt), all(tr)
then
(tlt.typeRoot = tr and tlt.accessibility = #public)
−> (tlt.identifier = tr.identifier)
end
Weiterhin gilt für Überse ungseinheiten, die eine Paketdeklaration enthalten (also nicht
im sogenannten default-package liegen), dass sich der dort angegebene Name mit dem des
umschließenden Pakets decken muss. Die Regel PackageDeclarationNames ( S.
) sichert
dies zu.
5.5.5. Ungültige Namen
Die Java-Sprachspezifikation sieht nicht nur Einschränkungen bei der Verwendung von Namen, sondern auch bezüglich der Namen selbst vor. So darf ein Name weder Leerräume noch
Klammern enthalten. Auch Schlüsselwörter der Sprache sind tabu, weiterhin muss das erste
Zeichen eines Bezeichners stets aus einer bestimmten Teilmenge der Unicode-Zeichen stammen ( § . ).
Es wäre möglich, diese Bedingungen mi els Constraints zu formulieren, allerdings unterstü t die Refacola keine String-Operationen, wie sie hierfür nötig wären. In Anbetracht der
Tatsache, dass die Gültigkeit von Namen bei Refaktorisierungen aber auch vollkommen isoliert durch eine Vorabprüfung aller Namen der Identifier-Domäne sta finden kann,³² ist dies
keine Einschränkung. So können bei einer R
-Refaktorisierung die neu zu vergebenen
Namen bei der Eingabe mi els regulärer Ausdrücke zunächst auf Gültigkeit geprüft (und gegebenenfalls zurückgewiesen) werden, bevor sie in die Domäne der Identifier eingefügt werden. Die vorgestellte Implementierung der R
-Refaktorisierung (siehe Abschni . . )
macht von einer solchen Lösung Gebrauch.
5.5.6. Verdunkeln und Verschatten
Verdunkeln (Obscuring) Java kennt Situationen, in denen von der Syntax her ein Name
im Programm sowohl eine Referenz zu einem Paket als auch zu einem Typen oder einer Variablen (also zum Beispiel Feld, Methodenparameter oder lokale Variable) darstellen kann.
Finden sich mehrere passende Deklarationen unterschiedlicher Art, gibt der Bindealgorithmus in Java Variablen den Vorzug vor Typen und Typen den Vorzug vor Paketen – etwaig
³² Es sei daran erinnert, dass es sich bei den Namen nur um eine endliche Domäne handelt, siehe Abschni
. . .
. . Namens-Constraints
930
931
932
933
934
935
936
package a;
public class A {
static void m(Object a) {
a.A X;
A.m(new Object());
}
}
(a)
̸⇒
package a;
public class a {
static void m(Object a) {
a.a X;
:::
a .::::::
m(new::::::::
Object());
}
}
(b)
Abbildung . .: Wird im linken Programm (a) die Klasse A mitsamt der Referenzen auf diese
in a umbenannt, kommt es aufgrund von Verdunkelung zu Kompilierfehlern
in den Zeilen
und
(b).
gleich genannte Typ- und Paketdeklarationen werden verdunkelt (engl. Obscuring) ( § . . ).
Abbildung . gibt ein Beispiel. Wird in dem Programm auf der linken Seite (a) die Klasse
A mitsamt beider Referenzen auf diese in a umbenannt, kommt es bei beiden Referenzen zu
Fehlern wegen verdunkelnder Deklarationen. In Zeile
wird die Paketreferenz a dann als
Referenz auf die Klasse a uminterpretiert, die wiederum keine Memberklasse namens a besi t, in Zeile
wird die Typreferenz a zu einer Referenz auf den Methodenparameter a
umgedeutet, auf welchem als Object typisiert keine Methode m aufgerufen werden kann.
In dieser Arbeit wird keine Lösung für Verdunkelung von Paket- oder Typnamen gegeben. Zwar ist es denkbar, mi els Constraintregeln versehentliches Verdunkeln von Deklarationen zu verhindern, allerdings steht der Aufwand in keinem Verhältnis zum Nu en, da
Verdunkelung in der Praxis belanglos ist. Die Java-Sprachspezifikation gibt eine Reihe von
Namenskonventionen vor ( § . ), bei deren Einhaltung Verdunkelung auszuschließen ist.
So haben zum Beispiel Paket- und Variablennamen mit Kleinbuchstaben und Typnamen mit
Großbuchstaben zu beginnen, was Verdunkelung von oder durch Typen ausschließt. Berücksichtigt man weiterhin die Regelungen zur Benennung von Paketen – mit Punkten separiert
und zuvorderst mit einer dem Domain Name System entlehnten Top-Level-Domain beginnend
– und Variablen, die nicht einer solchen Top-Level-Domain entsprechen sollen, sind auch hier
keine Konflikte zu befürchten.
Verschattung (Shadowing) Vom Verdunkeln zu unterscheiden ist in Java das Verscha en
(engl. shadowing) ( § . . ). Hierbei wird innerhalb des Geltungsbereichs (engl. scope) einer Deklaration eine weitere Deklaration mit gleichem Namen eingeführt, sodass sich beide
Geltungsbereiche überlagern. Innerhalb dieses Schni es von Geltungsbereichen kann unqualifiziert nur noch auf das jeweils zule t deklarierte Element zugegriffen werden. Verscha ung
findet (im Gegensa zur Verdunkelung) in Java nur zwischen Deklarationen gleicher Art sta ,
im Einzelnen sind dies Typen, Methoden und Variablen, die sich jeweils gegenseitig untereinander verscha en können ( § . . ). Abbildung . (a) zeigt ein Beispiel, in welchem das
Umbenennen eines formalen Parameters (hier von j zu i) die Verscha ung eines Feldes durch
diesen bewirkt.
Es lassen sich Constraintregeln angeben, die ein versehentliches Verscha en verhindern,
wenn es dadurch zu einer Bindungsänderung bei unqualifizierten Referenzen kommt. Die
. Constraintregeln für Java
937
938
939
940
941
942
class A {
int i;
public A(int j ){
i = j;
}
}
⇒
class A {
int i;
public A(int i ){
this.i = i;
}
}
(a)
(b)
Abbildung . .: Wird im linken Programm (a) der formale Parameter j in i umbenannt, führt
dies zu einer Verscha ung des Feldes i in Zeile
. Erst durch Einfügen des
Qualifizierers this kann j bedeutungserhaltend in i umbenannt werden (b).
folgende Regel, in welcher das Fakt isImplicit wiedergibt, ob eine thisReference nur implizit
durch den Compiler eingefügt wurde oder explizit im Programm vorgegeben ist, zeigt exemplarisch, wie für unqualifizierte Feldreferenzen ein Verscha en des referenzierten Feldes
durch formale Parameter wie in Abbildung . (a) verhindert werden könnte.
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
ShadowingByFormalParameter
for all
fieldReference: Java.FieldReference
receiver: Java.ClassOrInterfaceTypedExpression
thisReference: Java.ThisReference
formalParameter: Java.FormalParameter
method: Java.Method
do
if
Java.receiver(fieldReference, receiver),
Java.isExpression(thisReference, receiver),
Java.referenceInEntity(fieldReference, method),
Java.isImplicit(thisReference),
Java.formalParameterOfMethod(formalParameter, method)
then
fieldReference.identifier != formalParameter.identifier
end
Analog lassen sich auch Verscha ungen durch Typen und Methoden verhindern, wobei –
um auch geschachtelte Typen und Blöcke zu berücksichtigen – zusä liche Faktenabfragen
und Bedingungen nötig werden. Auch wenn derartige Regeln Änderungen des Programmverhaltens im Fall von Verscha ungen lösen können, sind sie dennoch unbefriedigend, da sie
übermäßig starke Einschränkungen [SMG ] mit sich bringen. Ansta im Beispiel aus Abbildung . (a) ein Umbenennen von j in i zu verhindern, wäre eine zu bevorzugende Lösung,
mit this zu qualifizieren, wie in Abbildung . (b)
sta dessen den Zugriff auf i in Zeile
gezeigt.
Im Rahmen der Constraintlösung über das Einfügen oder Auslassen von Qualifizierern zu
entscheiden ist problembehaftet. Das Einfügen eines Qualifizierers macht nämlich das Einfügen neuer Programmelemente – in obigem Beispiel einer this-Referenz beziehungsweise eines
this-Ausdrucks – nötig. Die Menge der Programmelemente ist aber nach Abschluss der Fak-
. . Namens-Constraints
tengenerierung unveränderlich. Der Umfang der möglichen Refaktorisierungen ergibt sich
lediglich aus den Constraintvariablen, die den initial generierten Programmelementen über
die Funktion applyingKinds (siehe Abschni . . ) zugeordnet sind.
Eine allgemeine Lösung für dieses Problem könnte darin bestehen, intern jeden Memberzugriff mit einem Qualifizierer zu versehen und über eine Boolesche Constraintvariable zu
steuern, ob dieser Qualifizierer tatsächlich im Quellcode ausgeschrieben ist oder ausgelassen
werden kann. Alle Constraints, die den Qualifizierer bedingen, müssten dann als bedingte
Constraints formuliert sein, in Abhängigkeit davon, ob der Qualifizierer tatsächlich Anwendung findet. Doch auch diese Lösung führt zu weiteren Problemen. So kann es zu derselben Referenz unterschiedliche Arten von Qualifizierern geben, unter denen eine Auswahl zu
bis
insgesamt neun Mögtreffen ist. Folgendes Programmstück zeigt in den Zeilen
lichkeiten, einen Zugriff auf das Feld i so zu qualifizieren, dass nicht an den gleichnamigen,
verscha enden Methodenparameter des Konstruktors von B gebunden wird.
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
package a;
class A {
int i;
class B extends A {
public B(int i){
this.i = i;
super.i = i;
A.this.i = i;
a.A.this.i = i;
B.this.i = i;
a.A.B.this.i = i;
B.super.i = i;
A.B.super.i = i;
a.A.B.super.i = i;
}
}
}
Wie zu ersehen ist, können einerseits this- und super-Qualifizierer verwendet werden, gleichzeitig diese aber wiederum noch selbst qualifiziert werden ( § . . ) wobei auch der qualifizierende Typ wie zum Beispiel in Zeile
qualifiziert sein kann ( § . . . ). Um das korrekte Einfügen von Qualifizierern während der Constraintlösung zu steuern, wären neben einer
Booleschen Variablen, ob überhaupt ein Qualifizierer gegeben ist, ebenso noch Constraintvariablen nötig, die zwischen this- und super-Qualifizierer unterscheiden, und angeben, ob und
wie diese qualifiziert sind und ob und inwieweit deren Qualifizierer erneut qualifiziert ist.
Die sich daraus ergebende Vielzahl bedingter Constraints und die zunehmende Komplexität
der Constraintregeln stellen eine Behandlung von Qualifizierern durch den Constraintlöser
generell in Frage.
Schäfer et al. [SVEM , Sch ] stellen mit dem Konzept der locked bindings ein Konzept vor,
bei welchem nach einer Refaktorisierung für jede Referenz einzeln geprüft wird, ob die sich
aus der Intention oder übrigen Abhängigkeiten (siehe Abschni . ) ergebenden Bindungen
erhalten wurden oder gegebenenfalls zu qualifizieren sind. Dass dieses Verfahren im Rahmen eines Postprocessing für eine constraintbasierte Refaktorisierung erfolgreich eingese t
werden kann, wurde in [STST ] gezeigt. Aus diesen Gründen werden innerhalb dieser Ar-
. Constraintregeln für Java
beit keine Constraintregeln angegeben, die versehentliches Verscha en verhindern, sondern
sta dessen die Namensqualifizierung im Nachgang durchgeführt (siehe Abschni . ).
5.6. Unerwünschtes Überschreiben, Verdecken und Überladen
Ein Großteil der Regeln, die sich aus den vorhergehenden Abschni en zu Zugrei arkeits-,
Namens-, aber auch Typ-Constraints ergeben haben, betrafen das Beibehalten der statischen
und dynamischen Bindungen (wobei sich le teres in Java in den Überschreibungsbeziehungen widerspiegelt). Neben dem Umstand, dass durch Änderung von Zugrei arkeiten, Namen oder deklarierten Typen eine Referenz nicht mehr an ihre ursprüngliche Deklaration
gebunden werden kann, gibt es auch das gegenteilige Problem, dass neue, unerwünschte Bindungen hinzukommen.
Unerwünschtes Überschreiben und Verdecken Wenn durch eine Refaktorisierung eine
neue Methodenüberschreibung in das Programm eingebracht wird, kann es zu Änderungen dynamischer Bindungen kommen. In Abschni
. . ist ein Beispiel gegeben, in welchem durch Änderung einer Methodenzugrei arkeit eine Überschreibungsbeziehung aufgehoben wurde und sich das Programmverhalten in Folge dessen änderte. Abhilfe schuf die
, ebenso S.
). Analog wurden weitere Regeln
Regel OverridingAccessibility1 (siehe S.
gezeigt, die für Beibehaltung von bestehenden Methodenüberschreibungen sorgen, nämlich
OverridingMethodNames ( S.
) für übereinstimmende Methodennamen und OverridingParameterType ( S.
) und OverridingMustComeFromSubtype ( S.
) für passende Parametertypen und Deklarationsorte bei überschreibenden Methoden.
Soll nun verhindert werden, dass neue Methodenüberschreibungen in das zu transformierende Programm eingebracht werden, genügt es, jeweils eine der Bedingungen aus diesen
Regeln fehlschlagen zu lassen, was folgende Regel AccidentalOverriding umse t:
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
AccidentalOverriding
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
do
if
subMethod != superMethod
then
Java.overrides(subMethod, superMethod)
or (not Java.sub(subMethod.owner, superMethod.owner))
or (superMethod.identifier != subMethod.identifier)
or (not (subMethod.parameters.declaredParameterType
= superMethod.parameters.declaredParameterType))
or (superMethod.accessibility = #private)
or ((superMethod.accessibility = #package)
and (superMethod.hostPackage != subMethod.hostPackage))
end
Diese Regel besagt, dass für jedes Paar von Instanzmethoden
• zuvor eine Überschreibungsbeziehung bestanden haben muss
. . Unerwünschtes Überschreiben, Verdecken und Überladen
• oder die deklarierenden Methoden nicht in einer Subtypbeziehung stehen dürfen
• oder beide Methoden unterschiedliche Namen tragen müssen
• oder sich an mindestens einer Stelle in den Parameterlisten beider Methoden sich der
Parametertyp unterscheiden muss
• oder die Methode des Supertyps als private deklariert sein muss, was Methodenüberschreibung ausschließt,
• oder die Methode aus dem Supertyp im Subtypen nicht zugrei ar ist, indem sie defaultzugrei ar und in einem anderen Paket deklariert ist.
Es ist zu beachten, dass man weitere Bedingungen, die für überschreibende Methoden zutreffen müssen, an dieser Stelle aussparen kann. So muss für überschreibende Methoden zum
Beispiel gelten, dass ihre Rückgabetypen (je nach Java-Version) gleich sind oder in kovarianter Subtypbeziehung stehen, was durch die bereits oben beschriebene Regel OverridingReturnType (siehe S.
, ebenso S.
) sichergestellt wird. Für die Regel AccidentalOverriding ist
dies aber unerheblich. Entweder, die Rückgabetypen beider Methoden stehen tatsächlich in
geeigneter Beziehung, dann kann es wegen der hinzukommenden Methodenüberschreibung
zur Laufzeit zu Verhaltensänderungen kommen (was zu verhindern ist), oder die Rückgabetypen sind für eine Methodenüberschreibung geeignet, was zu einem Kompilierfehler führt
(der ebenso zu unterbinden ist).
Neben einer unbeabsichtigten Methodenüberschreibung zwischen Instanzmethoden sind
ebenso Paarungen zwischen Instanzmethoden und statischen Methoden zu berücksichtigen,
bei denen entweder eine Instanzmethode versucht, eine statische Methode zu überschreiben,
oder umgekehrt. Beides ist in Java nicht erlaubt ( § . . . ), wobei es sich im zweiten Fall
streng genommen auch nicht um eine unerlaubte Überschreibung, sondern unerlaubtes Verdecken ( § . . . ) handelt. Analog zur Regel AccidentalOverriding wird dies durch die Re) und StaticMethodHidesInstanceMethod
geln InstanceMethodOverridingStaticMethod ( S.
( S.
) unterbunden.
Diese insgesamt drei Regeln für unbeabsichtigtes Überschreiben zwischen Paarungen von
Instanz- und statischen Klassenmethoden waren nötig, um den Fall zweier Klassenmethoden aussparen zu können, bei dem es nicht zu Überschreibung, sondern Verdeckung kommt.
Sich erst nach der Refaktorisierung neuerdings verdeckende Methoden sind unproblematischer als sich neuerdings überschreibende Methoden, da Bindungen an statische Methoden
in Java durch den Compiler ausgewertet werden und somit sich erst aus den Methodenreferenzen Bedingungen ergeben können, die gegen ein Verdecken zweier Methoden sprechen
(siehe dazu weiter unten). Was aber Berücksichtigung finden muss, ist, dass in Java eine als
final deklarierte Methode nicht verdeckt werden darf ( § . . . ), was die folgende Regel
AccidentalHidingFinalMethod sicherstellt:
994
995
996
997
998
999
AccidentalHidingFinalMethod
for all
method1: Java.Method
method2: Java.Method
do
if
. Constraintregeln für Java
1011
1012
1013
1014
public class A {
void m(String s) { /* ... */ }
void n (Object o) { /* ... */ }
}
1015
1016
1017
1018
̸⇒
public class A {
void m(String s) { /* ... */ }
void m (Object o) { /* ... */ }
}
class B {
{new A().n (new String());}
}
class B {
{new A().m(new String());}
}
(a)
(b)
Abbildung . .: Wird im linken Programm (a) die Methode n mitsamt ihrer Referenz in Klasse
B in m umbenannt, kommt es bei dieser zu einer Bindungsänderung an die
dann überladene Methode m(String) in A (b).
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
Java.finalMethod(method2), all(method1)
then
(method1.identifier != method2.identifier)
or (not Java.sub(method1.owner, method2.owner))
or (not(method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType))
or ((method1.tlowner != method2.tlowner)
and (method2.accessibility = #private))
or ((method1.hostPackage != method2.hostPackage)
and (method2.accessibility <= #package))
end
Unerwünschtes Überladen Eng im Zusammenhang mit dem Problem des unerwünschten
Überschreibens und Verdeckens finaler Methoden steht das unerwünschte Überladen. Während beim unerwünschten Überschreiben und Verdecken aber die Constraintregeln unabhängig von einem konkreten Methodenaufruf zu formulieren waren, ist ein Überladen von Methoden nur dann als kritisch einzustufen, wenn tatsächlich eine Methodenreferenz vorliegt,
für die sich eine Bindungsänderung an eine überladene Methode ergibt.
Abbildung . zeigt ein Beispiel für eine solche Bindungsänderung. Soll die Methode n mit
Parametertypen Object in m umbenannt werden, führt dies innerhalb der Klasse A zunächst
nicht zu Problemen. Erst bei Betrachtung der Klasse B mit ihrer Referenz auf die Methode n
(welche mi els ReferenceIdentifier (siehe S.
, ebenso S.
) ebenso in m umbenannt wird)
wird klar, dass dieser Methodenaufruf in Folge der Transformation an die Methode m(String)
sta die umbenannte Methode m(Object) binden wird.
Es gibt mehrere Möglichkeiten, Umbindungen bei Methodenüberladung zu verhindern. So
könnte in dem Beispiel aus Abbildung . entweder
• die Methode m(String) auf private gese t werden, damit sie für Aufrufe aus B nicht mehr
zugrei ar ist
• oder die Methode m(String) umbenannt werden
. . Unerwünschtes Überschreiben, Verdecken und Überladen
• oder der Typ des Methodenparameters s derart geändert werden, dass er nicht mehr
identisch mit (oder Subtyp des) Argumen ypen des Aufrufs von n in B ist
• oder mi els eines sicheren Upcast auf Object der Typ des Arguments des Aufrufs von n
in B derart geändert werden, dass er nicht mehr mit dem von m(String) identisch ist.³³
Die folgende Regel AccidentalInstanceMethodOverloadingInstanceMethod se t dies für den
allgemeinen Fall einer Instanzmethode und einer mit ihr im We bewerb stehenden und sie
überladenden weiteren Instanzmethode um.
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
AccidentalInstanceMethodOverloadingInstanceMethod
for all
method: Java.InstanceMethod
reference: Java.MethodReference
receiver: Java.ClassOrInterfaceTypedExpression
competingMethod: Java.InstanceMethod
do
if
Java.binds(reference, method),
Java.receiver(reference, receiver),
competingMethod != method
then
(method.identifier != competingMethod.identifier)
or (not Java.sub*(receiver.expressionType,competingMethod.owner))
or (Java.sub*(method.parameters.declaredParameterType,
competingMethod.parameters.declaredParameterType))
or (not Java.sub*(reference.arguments.expressionType,
competingMethod.parameters.declaredParameterType))
or ((reference.tlowner != competingMethod.tlowner)
and (competingMethod.accessibility = #private))
or ((reference.hostPackage != competingMethod.hostPackage)
and (competingMethod.accessibility <= #package))
or ((reference.hostPackage != competingMethod.hostPackage)
and (not Java.sub*(receiver.expressionType, reference.owner))
and (competingMethod.accessibility <= #protected))
end
Daneben gibt es noch drei weitere Regeln
• AccidentalInstanceMethodOverloadingStaticMethod,
• AccidentalStaticMethodOverloadingInstanceMethod und
• AccidentalStaticMethodOverloadingStaticMethod
die analog eine Umbindung von Methodenaufrufen bei überladenen statischen Methoden sowie Paaren von statischen und Instanzmethoden verhindern. Der Grund für die vier separaten Regeln für die vier möglichen Kombinationen von Klassen- und Instanzmethoden re³³ eine Bedingung, die sich zwar derzeit in Constraints formulieren lässt, aber – da die Existenz von Type Casts
im Rahmen dieser Arbeit nicht in Form von Variablen modelliert wurde (siehe Abschni . ) – nur geprüft
und nicht durch die Constraintlösung ermi elt werden kann
. Constraintregeln für Java
sultiert aus den unterschiedlichen Arten von Methodenempfängern bei statischen und nichtstatischen Methoden (einmal Klassen, einmal Objekte) sowie den unterschiedlichen Zugreifbarkeitsregeln für statische und nicht-statische Methoden. Bei le teren greift zusä lich eine
weitere Einschränkung bei protected-Zugrei arkeit, wie in Abschni . . anhand der Regel
ProtectedMethodOrFieldAccess bereits ausgeführt.
5.7. Unveränderliche Bibliotheksfunktionen
Neben den in den vorhergehenden Abschni en genannten Einschränkungen möglicher Refaktorisierungen durch Constraintregeln gibt es darüber hinaus eine weitere Art der Einschränkung, die durch die Refacola implizit umgese t wird. Nicht sämtlicher bei der Constraintgenerierung zu berücksichtigender Code kann tatsächlich durch ein Refaktorisierungswerkzeug bearbeitet werden. Das betrifft insbesondere Code, der aus Bibliotheken stammt
und nur als Bytecode vorhanden ist, aber auch manche Programmelemente, die nur implizit
durch den Compiler oder die Laufzeitumgebung vorgegeben sind, wie das size-A ribut von
Arrays ( § . ) oder die values()- und valueOf()-Methoden, die in Java jede Enum-Klasse
( § . ) implizit deklariert.
Wie Abschni . . erläutert hat, kann bei der Faktengenerierung ein Statusindikator gese t
werden, der angibt, ob die dem Programmelement anhängenden Variablen in ihren Werten
unveränderlich sein sollen. Dies geschieht insbesondere für Bibliothekscode, aber auch solche impliziten Programmelemente, die unveränderlich sind. Dabei wird nicht jede implizite
Deklaration automatisch unveränderlich gese t. Default-Konstruktoren sind beispielsweise
veränderlich, damit ihr Name und ihre Zugrei arkeit entsprechend des Namens und der Zugrei arkeit des umschließenden Typen mit verändert werden können.
5.8. Offene Probleme
Eine Schwäche der Refacola ist, dass sie innerhalb der Constraintregeln keine Verwendung eines Existenzquantors gesta et. Auch wurde bisher nicht erforscht, wie sich die Formulierung
solcher Constraints auf die Laufzeiten zur Constraintlösung auswirken würde.
Ein einfaches Beispiel für eine Situation, in der ein Existenzquantor erforderlich ist, ergibt
sich für eine Constraintregel, die besagen soll, dass zu jedem privaten Member mindestens
eine an dessen Deklaration bindende Referenz im Programm vorhanden sein muss.³⁴ Erdenkt
man Schlüsselwörter exists und with zur Refacola geeignet hinzu, könnte eine entsprechende
Constraintregel wie folgt lauten:
1045
1046
1047
1048
1049
1050
1051
1052
PrivateMemberMustBeReferenced
for all
member: Java.Member
do
if
all(member)
then
member.accessibility != #private
³⁴ Tatsächlich lässt sich zum Beispiel der Eclipse-Compiler so einstellen, dass er in diesem Fall einen Kompilierfehler meldet.
. . Offene Probleme
1056
1057
1058
interface I { int i = 1; }
class A implements I { private int j ;}
class B extends A { int x = i; }
⇒
(a)
interface I { int i = 1; }
class A implements I { private int i ;}
class B extends A { int x = :i ; }
(b)
Abbildung . .: Wird das Feld j in i umbenannt, verdeckt es für den Zugriff aus Klasse B anschließend das gleichnamige Feld aus I, obwohl ersteres in B nicht zugrei ar
ist. Die Folge ist ein Kompilierfehler.
or exists reference: Java.Reference with binds(reference, member)
1053
1054
end
Die Konklusion besagt in dieser Regel, dass für jedes Member gelten muss, dass dieses nicht
als private deklariert sein darf, oder eine Java.Reference (namens reference) existieren muss,
für die gilt, dass sie an das Member bindet. Die Formulierung einer äquivalenten Regel nur
mit den bisherigen Mi eln der Refacola ist nicht möglich. Schließlich müsste dann die Regelvariable reference im for all-Teil der Regel formuliert werden, um überhaupt ein binds-Fakt
in der Regel angeben zu können. Das würde aber – wegen der impliziten Allquantifizierung
über alle Regelvariablen – ausschließen, dass diese Regel generiert werden könnte, wenn im
Programm zum Beispiel keine einzige Referenz existieren würde. Existiert dann noch zusä lich ein privates Member, entsteht aber genau die Situation, die es ursprünglich zu verhindern
galt.
Es existieren weitere Einzelfälle, in denen es hilfreich wäre, Constraintregeln mit Existenzquantor zu formulieren. Ein Anwendungsfall, den auch schon Müller [Mül ] beschreibt, ist
das Verdecken von Feldern. Java erlaubt Subklassen, Felder zu deklarieren, deren Name bereits bei Feldern der Superklasse Verwendung findet ( §§ . . . , . . . ). Für die Subklasse
und alle ihre Subtypen gilt dann, dass unqualifizierte Zugriffe stets an das Feld der Subklasse
binden, das gleichnamige Feld der Superklasse wird verdeckt. Bemerkenswert ist, dass dabei auch ein nicht-zugrei ares Feld ein zugrei ares Feld verdecken kann. Abbildung .
zeigt ein Beispiel, in dem nach einer Rename-Refaktorisierung ein solcher Fall eintri , wodurch das Programm anschließend nicht mehr kompiliert. Das konkrete Problem ist hierbei,
dass nach der Refaktorisierung auf dem Vererbungspfad von I nach B nun das umbenannte,
gleichnamige und nicht zugrei are Feld i liegt. Freilich verhindert hä e dieses Problem werden können, wenn noch ein weiterer Vererbungspfad von I nach B existiert hä e, auf dem kein
nicht-zugrei ares, verdeckendes Feld gelegen hä e. Das wäre insbesondere der Fall gewesen,
wenn die Klasse zuvor B (mit völlig identischem Verhalten) als
1055
class B extends A implements I { int x = i; }
deklariert worden wäre. Es genügt nämlich im Falle nicht zugrei arer, verdeckender Felder,
dass innerhalb der Vererbungshierarchie von dem deklarierenden Typ bis zur referenzierenden Klasse mindestens ein Pfad existiert, auf dem kein solches Feld liegt.
Für die vorliegende Arbeit ist dieses Problem unerheblich. Durch Qualifizierung kann auch
auf verdeckte Felder der Superklasse zugegriffen werden. Auf Felder der unmi elbaren Superklasse kann dies anhand des Schlüsselwortes super geschehen, weiter oben in der Vererbungshierarchie hilft eine mit einem cast versehene Qualifikation auf this. Für das Beispiel aus
. Constraintregeln für Java
Abbildung . wäre eine passende Qualifizierung ((I)this).i. Ein Verdecken von Feldern kann
und sollte daher nur durch Qualifizierungen und den in Abschni . beschriebenen Nachbearbeitungsschri erfolgen, sta durch Constraints verhindert zu werden. Eine Formulierung
von Constraintregeln zu verdeckenden Feldern ist daher nicht nötig.
Ein Fall, der sich hingegen tatsächlich auf die implementierten Werkzeuge auswirkt, betrifft
das Verschieben abstrakter Methoden. Für diese gilt, dass erbende, nicht-abstrakte Klassen eine Implementierung bereitstellen oder erben müssen ( § . . . ). Wird nun eine abstrakte
Methode in eine Superklasse verschoben, muss gelten, dass alle damit neu hinzukommenden, die Methode erbenden nicht-abstrakten Klassen diese Methode ebenso implementieren
oder eine Implementierung erben müssen. Für jede solche Klasse muss also gelten, dass mindestens eine nicht-abstrakte Methode deklariert oder geerbt wird, welche die neu geerbte abstrakte Methode implementiert. Auch für diese Existenz-Bedingung stellt die Refacola derzeit
keine Ausdrucksmöglichkeiten bereit. Faktisch kann damit ein P
U M
für abstrakte
Methoden (derzeit) nicht unterstü t werden.
6. Berücksichtigung reflektiver Zugriffe
Java bietet mit der Reflection-API [REF ] einen Mechanismus, zur Laufzeit die Struktur des
ausgeführten Programms zu analysieren sowie den Ablauf eines Programms in Abhängigkeit
von einer solchen Analyse zu gestalten [McC ]. So ist es möglich, eine Referenz auf eine
Klasse zu erzeugen, welche dann – als Objekt repräsentiert – nach ihren Eigenschaften befragt
oder über Methodenaufrufe instanziiert werden kann. Derart instanziierte Objekte können
mi els der Reflection-API wiederum nach ihren Feldern und Methoden befragt werden. Es
ergibt sich somit innerhalb der Reflection-API eine Parallelwelt zu den regulären Mi eln der
Programmiersprache Java, in welcher ebenso Objekte instanziiert, referenziert und in ihrem
Zustand verändert werden können.
Dieses Kapitel stellt einen Sa von Constraintregeln für Refaktorisierungswerkzeuge vor,
der solche Zugriffe über die Reflection-API berücksichtigt. Ein großes Problem ergibt sich
daraus, dass sich Zugriffe über die Reflection-API einer statischen Programmanalyse en iehen. Zwar ist im Rahmen einer statischen Programmanalyse ersichtlich, dass über Reflection
zum Beispiel ein Methodenaufruf erfolgt³⁵, allerdings bleibt der verborgen, welche Methode
auf einem Objekt welcher Klasse aufgerufen wurde. Aus diesem Grund se en die in diesem
Kapitel beschriebenen Constraintregeln eine Laufzeitanalyse des zu refaktorisierenden Programms voraus, welche beispielsweise während der Ausführung von Unit-Tests durchgeführt
werden kann. Während dieser Programmausführung werden Aufrufe der Reflection-API in
Form von geeigneten Fakten protokolliert. Bei anschließenden Refaktorisierungen können
dann auf Basis dieser Faktenmenge mi els der in diesem Kapitel beschriebenen ReflectionConstraintregeln auch solche Bedingungen geprüft (und eingehalten) werden, die sich aus
reflektivem Programmverhalten ergeben.
Die Inhalte dieses Kapitel bauen im Wesentlichen auf einer vorangegangenen Publikation –
[TB ] – auf. Der Autor vorliegender Arbeit war in dieser verantwortlich für die Entwicklung
der Constraintregeln, wie sie in diesem Kapitel beschrieben werden, während der zweite Autor im Wesentlichen die Herausforderungen der Laufzeitanalyse gestemmt hat. Entsprechend
sind die dort und hier genannten Constraintregeln Werk des Autors. Das im Folgenden nur
dem besseren Verständnis halber auszugsweise erläuterte Werkzeug zum Sammeln der Laufzeitinformation – T
F
– hingegen ist nicht als Teil der vorliegenden Arbeit zu werten.
6.1. Die Reflection-API
Die von der Programmiersprache Java angebotene Reflection-API findet in der Praxis rege
Verwendung. Bodden et al. [EAJM , BSS+ ] berichten über
von ihnen analysierte JavaProjekte (unter anderem die vollständige DaCapo-Benchmark-Suite [BGH+ ]), von denen
alle Gebrauch von der Reflection-API machten. Dabei schwankte die Zahl reflektiver Aufru³⁵ der Fall, dass ein Aufruf an die Reflection-API mi els Reflection erfolgt, sei hier einmal ausgenommen
. Berücksichtigung reflektiver Zugriffe
fe von
(beim avrora-Projekt) bis zu
(beim tradebeans-Projekt). Anwendung findet die
Reflection-API häufig dort, wo Programmteile voneinander zu entkoppeln sind. Ein typischer
Anwendungsfall ist ein Framework, welches Code von Klienten aufrufen, aber gleichzeitig
unabhängig von diesen implementiert sein soll. Anhand von Konfigurationsdateien kann dem
Framework mitgeteilt werden, welche Java-Klassen konkret zu verwenden sind. Ihre Instanziierung erfolgt über die Reflection-API. Ein Beispiel hierfür ist das JUnit-Framework, bei welchem allerdings Angaben über auszuführende Testklassen in der Regel per Kommandozeilenparameter sta Konfigurationsdateien erfolgen. Weiterhin findet Reflection dort Anwendung,
wo Objekte unabhängig von der durch sie instanziierten Klasse inspiziert werden sollen. Anwendungsbeispiele hierfür sind Prozessmonitore, die Auskunft über von bestimmten Programmteilen verwendete Objektgeflechte geben sollen oder auch Persistierungsframeworks,
die ihnen übergebene Objektgeflechte in eine geeignete Textdarstellung oder Datenbank übertragen. Schon anhand dieser Beispiele ist zu erkennen, dass Aufrufe an die Reflection-API
nicht nur im Anwendercode³⁶ zu finden sind, sondern auch aus (als jar-Dateien eingebundenen) Bibliotheken stammen können. Diese referenzieren dann über Reflection wiederum den
Anwendercode.
Ein großes Problem beim Umgang mit der Reflection-API resultiert aus der teilweise mangelhaften Dokumentation. Während sich eine Beschreibung der wesentlichen Grundfunktionen in jedem besseren Java-Handbuch findet und auch vom Entwickler Sun (beziehungsweise
Oracle) verschiedene Anleitungen und auch JavaDoc-Kommentare innerhalb der eigentlichen
API bereitgestellt wurden, mangelt es an einer offiziellen Dokumentation. So lassen sich viele Detailfragen erst anhand händischer Tests klären, was im Rahmen dieser Arbeit mehrfach
nötig wurde.
Class-Objekte Den Einstieg in die Reflection-API bildet die Klasse java.lang.Class. Instanzen dieser Klasse repräsentieren Java-Klassen. Zum Erzeugen einer solchen Instanz sieht Java
zwei unterschiedliche Wege vor. Einerseits kann eine Instanz von Class entweder über ein
sogenanntes class literal ( § . . ) gewonnen werden, indem einer Typreferenz ein .class
nachgestellt wird. Andererseits erlaubt die statische Fabrikmethode Class.forName(...) die Erzeugung von Class-Objekten, indem ihr der voll qualifizierte Name der gewünschten Klasse
übergeben wird. Ergänzend zum kurzen Einführungsbeispiel aus Abbildung . in Kapitel
zeigt Abbildung . ein erweitertes Beispiel eines reflektiven Programms. In diesem wird in
Zeile
über den Aufruf Class.forName("a.C") eine neue Instanz eines Class-Objekts erzeugt,
welches die Klasse C repräsentiert. Denselben Effekt hä e ein Aufruf a.C.class gehabt, beide
liefern eine Referenz auf dasselbe Objekte zurück.
Analyse von Programmstukturen Ist erst einmal ein Class-Objekt gewonnen, kann dieses
über verschiedene get-Methoden nach seinen Eigenschaften befragt werden. Neben Abfragen
zum Beispiel zum Klassennamen, der Zugrei arkeit oder dem umschließenden Paket ist es
insbesondere möglich, eine Klasse nach den durch sie deklarierten Feldern und implementierten Methoden zu befragen, welche ebenso wie Klassen durch eigene Objekte repräsentiert
ein Field-Objekt angefordert, welches
werden. So wird im gegebenen Beispiel in Zeile
das von C implementierte Feld f repräsentiert. Ebenso wäre es möglich, über die Methode
³⁶ also dem als Quellcode vorliegenden und potentiell zu refaktorisierenden Projekt
. . Die Reflection-API
1059
package a;
1060
1061
1062
1063
1064
1065
1066
public class C {
private String s;
public String toString() {
return s;
}
}
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
public class Reflection {
public static void main(String[] args) throws Exception {
Class c = Class.forName("a.C");
java.lang.reflect.Field f = c.getDeclaredField("s");
Object myInstance = c.newInstance();
f.setAccessible(true);
f.set(myInstance, "abc");
System.out.println(myInstance);
}
}
Abbildung . .: Beispiel für ein reflektives Programm. Zunächst wird eine Instanz der Klasse
C erzeugt, dann folgt ein schreibender Zugriff auf das private Feld f.
getDeclaredFields die Menge aller von einer Klasse deklarierten Felder zu erfragen. Sind auch
die von den Superklassen geerbten Felder gefragt, so liefert die Methode getFields diese zusä lich zurück. Ganz analog existieren auch Methoden zur Abfrage von Methoden und Konstruktoren.
Instanziierung, Methoden- und Feldzugriffe Ist bekannt, welcher Klasse ein Objekt angehört und welche Programmelemente es deklariert oder erbt, können diese auch aktiv genu t werden. Im Fall von Konstruktoren können analog zur Class Instance Creation Expression ( § . ) – also der Objektinstanziierung mi els new-Operator – über die parameterlose Methode newInstance von Class neue Instanzen einer Klasse angelegt werden, wie es im
Beispiel in Zeile
geschieht. Soll ein nicht-parameterloser Konstruktor aufgerufen werden, ist die Methode newInstance der Constructor-Klasse des jeweils gewünschten Konstruktors zu verwenden. Da sich einer statischen Analyse offensichtlich en ieht, ob die Typen
der übergebenen Argumente denen des deklarierten Konstruktors entsprechen oder im Falle
von Class.newInstance überhaupt ein parameterloser Konstruktor zur jeweiligen Klasse existiert, müssen sich etwaig ergebende Fehler zur Laufzeit abgefangen werden, was mi els einer
java.lang.InstantiationException geschieht. Aufrufe von Methoden gestalten sich ganz ähnlich
zu Konstruktoraufrufen, nur dass sta dessen die Methode invoke der Klasse Method verwendet wird, der neben den Argumenten noch das jeweilige Empfängerobjekt übergeben wird.
Für den lesenden und schreibenden Zugriff auf Felder existieren get- und set-Methoden innerhalb der Klasse Field.³⁷
³⁷ Da die Reflection-API auf Zeiten vor dem mit Version eingeführten Autoboxing ( § . . ) zurückgeht,
existieren sowohl separate get- und set-Methoden für alle primitiven Typen, als auch mit Object typisierte
. Berücksichtigung reflektiver Zugriffe
Class.forName;
Class.getDeclaredField;
Class.newInstance;
Field.set*;
a.C;
<a.C: java.lang.String s>;
a.C;
<a.C: java.lang.String s>;
a.Reflection.main;
a.Reflection.main;
a.Reflection.main;
a.Reflection.main;
5;
6;
7;
9;
;
;
;
isAccessible=true;
Abbildung . .: Ein durch das TamiFlex-Werkzeug erstellte Reflection-Log für eine Ausführung des Programms aus Abbildung . .
Bemerkenswert ist, dass für reflektive Zugriffe – ebenso wie bei regulären Referenzen auf
Felder, Methoden, Konstruktoren und Klassen – die Zugrei arkeiten der jeweiligen Deklarationen zu berücksichtigen sind. Allerdings gibt es drei gewichtige Unterschiede: Erstens
unterscheiden sich die Zugrei arkeitsregeln merklich (Details führt Abschni . in diesem
Kapitel aus). Zweitens lässt sich der Zugrei arkeitsschu mi els der Methode setAccessible
umgehen, die von Feldern, Methoden und Konstruktoren implementiert wird und deren Verwendung im obigen Beispiel in Zeile
den Zugriff auf das Feld f in Zeile
überhaupt
erst möglich macht. Dri ens ist es auch bei der Zugrei arkeit – gleich der der oben schon
erwähnten Typprüfung – nicht möglich, mit einer statischen Codeanalyse zu entscheiden, ob
eine Deklaration zugrei ar ist. Aus diesem Grunde findet auch die Prüfung auf Zugrei arkeit zur Laufzeit sta , wobei im Fehlerfall eine java.lang.IllegalAccessException geworfen wird.
Analyse reflektiver Programmzugriffe Eine statische Analyse von reflektiven Zugriffen,
welche Aufschluss darüber gibt, mit welchen konkreten Parameterwerten welche Methode
der Reflection-API innerhalb welchen Ausdrucks im Programm aufgerufen wird, ist schwierig. Im gegebenen Beispiel aus Abbildung . lässt sich noch problemlos nachverfolgen, welche Zugriffe im Einzelnen erfolgen. Im Wesentlichen liegt dies an den verwendeten StringLiteralen, die eine eindeutige Zuordnung erlauben. In der Praxis werden übergebene Namen
aber zumeist erst zur Laufzeit berechnet, häufig durch Extraktion aus Konfigurationsdateien
[BSS+ , SR ]. Selbst wenn ein Analysewerkzeug auf diese Zugriff hä e, müsste es immer
noch die Programmlogik durchschauen, nach welcher die Datei eingelesen und die entsprechenden Teilstrings extrahiert werden. Eine solche Analyse von Programmlogik zur Ermi lung von String-Werten ist zwar in Einzelfällen möglich [CMS ] und konnte auch schon zur
statischen Auswertung reflektiver Ausdrücke genu t werden [LWL ], im allgemeinen Fall
bleibt die Frage aber unentscheidbar. Man denke nur an den Fall, dass im obigen Programm
der String a.C durch args[0] erse t wird und die Aufrufe des Programms von einer entfernten
Stelle erfolgen, die dem Analysewerkzeug unbekannt bleibt.
Für den Umgang mit reflektiven Zugriffen wird in dieser Arbeit daher eine dynamische
Analyse verwendet, für welche auf das TamiFlex-Werkzeug [BSS+ ] zurückgegriffen wurde.
Dieses protokolliert zur Laufzeit – zum Beispiel bei der Ausführung von Tests – sämtliche vom
Programm durchgeführten Aufrufe an die Reflection-API mitsamt der jeweils übergebenen
Argumente. Intern bedient es sich dabei der durch die java.lang.instrument-API ermöglichten
sogenannten Java-Agenten. Ein solcher Agent kann bei Programmausführung der virtuellen
get- und set-Methoden. In der mit vorliegender Arbeit gelieferten Implementierung werden diese aber alle
identisch behandelt, weswegen im weiteren Text auf die unterschiedlichen Methoden nicht näher eingegangen
wird.
. . Refaktorisierung reflektiven Codes
Maschine übergeben werden und beim Eintreffen bestimmter Ereignisse – im Falle von TamiFlex dem Aufruf einer Methode der Reflection-API – eigenen Code ausführen. Abbildung .
zeigt eine Logausgabe für eine Ausführung des Programmbeispieles aus Abbildung . . Die
erste Spalte gibt jeweils die Art des Aufrufs an die Reflection-API an. Die zweite Spalte zeigt
die übergebenen Parameter an, welche im Fall von Class.getDeclaredField; und Field.set*; auch die
verwendeten Parametertypen beinhalten. Die folgenden beiden Spalten geben die Quelle des
Aufrufs an, für das Beispiel jeweils die main-Methode in der Klasse Reflection mit Zeilennummer³⁸. Die le te Spalte gibt jeweils an, ob im Falle einer Referenz auf ein Feld, eine Methode
oder einen Konstruktor die isAccessible-Eigenschaft des Programmelements auf true gese t
wurde. Im gegebenen Beispiel ist dies ab Zeile
der Fall.
6.2. Refaktorisierung reflektiven Codes
Es lässt sich leicht einsehen, dass bei der Refaktorisierung von Programmen, die von der
Reflection-API Gebrauch machen, ganz ähnliche Bedingungen einzuhalten sind, wie sie sich
auch für nicht reflektive Zugriffe ergeben. So verhält sich im Beispiel aus Abbildung . die
Übergabe des Strings a.C wie eine reguläre Typreferenz. Wird entweder C in ein anderes Paket verschoben oder das Paket a beziehungsweise die Klasse C umbenannt, wird die Referenz
ungültig. Nur im Gegensa zu einer regulären Typreferenz gestaltet sich das Anpassen einer solchen reflektiven Referenz schwierig bis unmöglich. Zwar ergibt sich für das Beispiel aus
Abbildung . erneut eine noch recht einfache Situation, denn hier kann bei einem entsprechenden R
oder M
T
der String innerhalb des Aufrufs Class.forName("a.c") angepasst werden. Im allgemeinen Fall muss aber wiederum damit gerechnet werden, dass der
jeweils übergebene String erst zur Laufzeit berechnet wird. Eine weitere Schwierigkeit ergibt
sich aus der Tatsache, dass eine einzelne reflektive Anweisung durchaus mehrfach ausgeführt
werden kann, zum Beispiel, wenn in einer Schleife über mehrere zu instanziierende Klassen
iteriert wird. Somit können sich aus einem einzelnen Ausdruck gleich mehrere reflektive Referenzen ergeben. Gilt eine Umbenennung nur in einem dieser Fälle, wäre eine zusä liche
Fallunterscheidung einzufügen, was aber nicht unbedingt der Intention des Benu ers entsprechen muss. Zu guter Le t muss berücksichtigt werden, dass – wie in den einführenden
Beispielen zur Reflection-API erwähnt – reflektive Aufrufe auch aus eingebundenen Bibliotheken stammen können, die sich einer direkten Bearbeitung en iehen und keine Änderungen
ihres Quellcodes zulassen. Im allgemeinen Fall wird eine Refaktorisierung wegen fehlschlagender Zugriffe aus reflektiven Ausdrücken also eher abgelehnt werden müssen, als dass für
diese eine geeignete Quelltex ransformation zu finden wäre. Abschni . wird dennoch einen Ausblick geben, wie auch reflektive Anweisungen während einer Refaktorisierung entsprechende Quelltex ransformationen erfahren können.
Bisherige Ansätze Als bisher einziges Refaktorisierungswerkzeug für reflektive Programme kann der Refactoring Browser [RBJ ] für Smalltalk gelten. Smalltalk ist eine nicht typisierte, objektorientierte Programmiersprache, die umfangreiche [FJ ] und viel genu te
[CRTR ] Reflection-Möglichkeiten bietet. Durch das fehlende Typsystem ergeben sich in
³⁸ Wegen der fortlaufenden Zeilennummerierung aller Codebeispiele in dieser Arbeit ergeben sich naturgemäß
Abweichungen.
. Berücksichtigung reflektiver Zugriffe
Smalltalk schon für reguläre Methodenaufrufe³⁹ dieselben Probleme beim Refaktorisieren wie
in Java bei Nu ung der Reflection-API. So ist wegen der fehlenden Typisierung allein anhand
des Quellcodes keineswegs ersichtlich, ob eine bestimmte Methode vom Empfänger implementiert wird. Daher können Fehler auch nur zur Laufzeit behandelt werden, was in Smalltalk
anhand einer zurückgegebenen doesNotUnderstand:-Nachricht passiert.
Entsprechend nu te der Refactoring Browser ebenso eine Laufzeitanalyse [RBJ ], welche
allerdings einige Besonderheiten aufwies, die sich damit erklären lassen, dass die Sprache
Smalltalk dem Programmierer nicht den typischen Zyklus des Programmieren, Kompilieren
und Ausführen vorschreibt. Sta dessen können in Smalltalk Programme entwickelt werden,
indem zur Laufzeit das aktuell ausgeführte und ebenso in Smalltalk geschriebene Laufzeitsystem inklusive seines Klassenbrowsers nach und nach angepasst und in seinem Quellcode
verändert wird, bis es die gewünschten Funktionalitäten aufweist. Der Refactoring Browser
greift diesen Mechanismus auf, indem für eine Refaktorisierung die offensichtlich nötigen Änderungen sofort im Code ausgeführt werden, die sich ergebenden Folgeänderungen aber erst
bei erkanntem Bedarf erfolgen. Soll beispielsweise eine Methode umbenannt werden, schreibt
der Refactoring Browser den Code der Methodendeklaration unmi elbar um. Ergibt sich später zur Laufzeit – etwa bei der Ausführung von Tests – ein Methodenaufruf unter Verwendung
des alten Namens, wird dies innerhalb einer Laufzeitanalyse erkannt und erst dann der entsprechende Methodenaufruf umgeschrieben. Somit wird sichergestellt, dass für die ausgeführten Testfälle das Programmverhalten unverändert bleibt.
Während ein solches Verfahren für Smalltalk gute Dienste leistete, scheint es nicht sinnvoll,
eine Adaption dessen für Java vorzunehmen. Zwar mag es technisch möglich sein, zur Laufzeit ein Java-Programm gegebenenfalls anzuhalten und mi els eines inkrementellen Compilers den Bytecode und Quellcode simultan zu verändern, soweit eine Laufzeitanalyse dies für
nötig befindet. Allerdings lässt sich dieses Vorgehen nicht mit dem üblichen Bild der JavaWelt vereinen, in dem ein Programmierer nicht davon ausgehen muss, dass sich sein Code
zur Laufzeit selbstständig verändert. Wünschenswert ist viel mehr ein Refaktorisierungswerkzeug, welches den üblichen Konventionen entspricht, dem Programmierer alle nötigen Quellcodeänderungen auf einmal in einem Dialog zu präsentieren und nach Bestätigung geschlossen in den Code zu übertragen.
Aus diesem Grunde findet bei den in dieser Arbeit vorgestellten Refaktorisierungswerkzeugen die Laufzeitanalyse vor der Refaktorisierung sta . Der Benu ter muss hierzu zunächst
unmi elbar vor einer Refaktorisierung bestehende Testsuiten anstoßen, deren Ausführung
vom TamiFlex-Werkzeug überwacht wird. Die so gesammelten Informationen über reflektive Zugriffe werden in eine Faktenmenge umgewandelt, anhand derer aus Constraintregeln
die Constraints generiert werden können, die zur Einhaltung aller zur Beibehaltung des Programmverhaltens erforderlichen Bedingungen nötig sind.
6.3. Programmelemente und Fakten
Um auch reflektive Referenzen mi els Refacola-Constraintregeln zu bedingen, müssen diese analog zu den regulären Java-Referenzen mi els Programmelementen repräsentiert werden. Die verschiedenen Arten von Programmelementen ergeben sich für die in dieser Ar³⁹ Häufig wird im Smalltalk-Umfeld auch von Nachrichtenversand sta Methodenaufruf gesprochen.
. . Programmelemente und Fakten
beit vorgestellte Implementierung dabei direkt aus der Reflection-API. So existiert für jeden
zu berücksichtigenden Methodenaufruf der Reflection-API eine entsprechende Programmelementart, wobei jede von diesen von der abstrakten Art ReflectiveReference ableitet. So werden
zum Beispiel Aufrufe eines Class.forName(...) durch eine Programmelementart ClassForName
repräsentiert. Insgesamt existieren
solcher nicht-abstrakten Unterarten, die Methoden der
Reflection-API repräsentieren, wobei mitunter verschiedene Methoden der Reflection-API zu
einer Programmelementart zusammengefasst sind. So bietet zum Beispiel die Reflection-API
gleich zwei Wege, eine Klasse zu instanziieren. Für ein Class-Objekt c sind die beiden Aufrufe
c.newInstance() und c.getConstructor().newInstance() äquivalent.⁴⁰ Beide werden daher in einer
Programmelementart ConstructorNewInstance zusammengefasst. Weiterhin werden beispielsweise die vielen get- und set-Methoden, mit denen für Field-Objekte deren Feldwert gelesen
und geschrieben werden kann, in einer Programmelementart FieldGetSet zusammengefasst.
Eine vollständige Auflistung aller Programmelementarten ist den entsprechenden Deklarationen in Anhang A zu entnehmen.
Neben den
nicht-abstrakten Programmelementarten für Zugriffe auf die Reflection-API
existieren noch mehrere Programmelementarten, die genu t werden, um entweder Aufrufe
an die Reflection-API mi els einer gemeinsamen Überart zu gruppieren, oder auch gezielt an
eine solche Gruppe eine Constraintvariable zu vererben. So erbt zunächst jede der
Arten
reflektiver Referenzen von ReflectiveReference die beiden Constraintvariablen owner und hostPackage, welche die umschließende Klasse respektive das umschließende Paket angeben. Im
Gegensa zu regulären Referenzen ist eine Constraintvariable für den umschließenden Topleveltypen (tlowner) entbehrlich. Für reguläre Java-Referenzen diente diese zur Bestimmung
von Zugrei arkeiten. Für reflektive Zugriffe werden Zugrei arkeiten allerdings zur Laufzeit
bestimmt, zu dieser sind alle Klassen allerdings schon in jeweils eigene class-Dateien kompiliert, für die keine Schachtelungsbeziehung mehr gegeben ist, welche also auch nicht für die
Bestimmung von Zugrei arkeiten zurate gezogen werden kann. Ebenso über Vererbung werden verschiedene Namens-Variablen identifier an Programmelementarten vererbt, wo diese
bei Aufrufen der Reflection-API übergeben oder zurückgegeben werden. So erbt beispielsweise die Programmelementart ClassForName sowohl von ReflectiveTypeNameReference als auch
ReflectivePackageNameReference jeweils eine Namensvariable für den übergebenen Klassenund Paketnamen. Die Programmelementart MethodGetName, die einen Aufruf der Methode getName() von Method repräsentiert, erbt hingegen eine Namensvariable von ReflectiveMethodNameReference, die den nach Abarbeitung der Methode zurückgegebenen Methodennamen anzeigt.
An Faktenarten ergeben sich bei der Behandlung von Reflection nur die beiden zweistelligen
Prädikate reflectiveBinding und reflectiveReceiver. Beide nehmen eine ähnliche Rolle wie die bereits bekannten Faktenarten binds und receiver ein. Ein reflectiveBinding-Fakt repräsentiert die
Bindung einer reflektiven Referenz an eine Deklaration. So wird für das Beispiel aus Abbildung . in Zeile
das den Aufruf repräsentierende ClassGetDeclaredField-Programmelement per reflectiveBinding-Fakt an die Deklaration des Feldes s gebunden. Die EmpfängerBeziehung kommt zusä lich bei solchen Aufrufen an die Reflection-API ins Spiel, bei denen
⁴⁰ Technisch ist das nicht ganz korrekt. Im Verhalten unterscheiden sich beide newInstance-Methoden insofern,
als dass bei einer auftretenden Ausnahme während des Konstruktoraufrufs die Klasse Constructor diese in eine
InvocationTargetException kapselt, während die Klasse Class die Ausnahme direkt wirft. Für die für Refaktorisierungen zu prüfenden Bedingungen ist dies aber unerheblich.
. Berücksichtigung reflektiver Zugriffe
Zugriffe auf eine konkrete Objektinstanz sta finden, wie es im Beispiel in Zeile
der Fall
ist. Hier bindet der Aufruf an das Feld s, zusä lich ist der Empfänger hier ein Class-Objekt,
welches eine C-Klasse repräsentiert, was über ein entsprechendes reflectiveReceiver-Fakt ausgedrückt wird und später bei der Berechnung von Zugrei arkeiten Relevanz haben wird.
Generierung der Programmelemente und Fakten Die Generierung der Programmelemente und Fakten für reflektive Zugriffe gestaltet sich im Wesentlichen gradlinig. Aufgebaut wird
sowohl auf der bereits erzeugten regulären Faktenbasis, welche insbesondere bereits die Programmelemente für die mi els der Reflection-API referenzierbaren Deklarationen enthält, als
auch auf der entsprechenden Log-Ausgabe des TamiFlex-Werkzeugs. Le teres wird linear
abgearbeitet, wobei für jeden Eintrag die entsprechenden Programmelemente und Fakten generiert werden. Zu beachten ist dabei, dass keine Zuordnung eins zu eins von Quellcodeanweisungen zu Programmelementen entsteht, sondern von Ausführungen der Anweisungen zu Programmelementen. Findet ein Aufruf an die Reflection-API innerhalb einer Schleife
oder mehrfach aufgerufenen Methode sta und wird dieser entsprechend mehrfach (gegebenenfalls mit unterschiedlichen Parametern) ausgeführt, wird für jeden solchen Aufruf eine entsprechende Kombination aus Programmelement, reflectiveBinding- und gegebenenfalls
reflectiveReceiver-Fakt erzeugt.
Trickreich vom technischen Standpunkt her gestaltet sich bei der Faktengenerierung die
Abbildung von zur Laufzeit erkannten Deklarationen und Referenzen auf die entsprechenden Anweisungen im Quellcode. So erhalten anonyme und lokale Klassen beim Kompilieren einen Dateinamen mit Hilfe fortlaufender Nummerierung, anhand dessen wieder auf die
ursprüngliche Deklaration geschlossen werden muss. Komplizierter und auch kaum mehr
eindeutig möglich ist hingegen eine Zuordnung von Referenzen. Zwar bieten Java-Compiler
die Option, den Anweisungen im kompilierten Bytecode die jeweils entsprechenden Zeilennummern des Java-Quellcodes hinzuzufügen (insbesondere Debugger profitieren hiervon),
allerdings verliert diese Information an Wert, wenn mehrere Anweisungen in einer Zeile stehen. Da in aller Regel in Java jeweils nur eine Anweisung bestehend aus wenigen Ausdrücken
pro Zeile verwendet wird (was sich auch automatisiert vorm Kompilieren erzwingen ließe),
macht die vorliegende Implementierung die Annahme, dass dies stets der Fall ist.
6.4. Constraintregeln
Die Constraintregeln für Reflection lassen sich ähnlich den in Kapitel vorgestellten gliedern.
Erneut kann eine Trennung nach den Bereichen Namen, Zugrei arkeiten, Orte, Typen und Unerwünschte Bindungsänderungen vorgenommen werden, wobei auch hier die Constraintregeln
nicht aufeinander au auen, sondern in ihrer Gesamtheit zusammenwirken. Erneut ist es also
dem Leser überlassen, in welcher Reihenfolge er sich die folgenden fünf Teilabschni e zu den
oben genannten Bereichen erschließt.
Namen Ebenso wie bei regulären Referenzen im Quellcode gilt gleichermaßen für Aufrufe
über die Reflection-API, dass bei Bindungen anhand von Namen diese an Aufrufstelle und Deklaration übereinstimmen müssen. Für reguläre Referenzen wurde dies über die Constraintregel ReferenceIdentifier (siehe S.
, ebenso S.
) zugesichert. Bei reflektiven Referenzen
. . Constraintregeln
geschieht dies analog über vier verschiedene Constraintregeln, die jeweils die Übereinstimmung von Paket-, Klassen-, Methoden- und Feldnamen fordern. Für Typnamen lautet die
Regel wie folgt.
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
ReflectiveTypeNamesEquality
for all
r: Java.ReflectiveTypeNameReference
d: Java.NamedType
do
if
Java.reflectiveBinding(r, d)
then
r.identifier = d.identifier
end
Die weiteren drei Regel ReflectiveFieldNamesEquality ( S.
), ReflectiveMethodNamesEquality
( S.
) und ReflectivePackageNamesEquality ( S.
) sind analog formuliert. Die Aufteilung
in mehrere Regeln ist nötig, da einzelne reflektive Referenzen durchaus mehrere Deklarationen referenzieren können. So referenziert ein Class.forName() wegen des übergebenen qualifizierten Namens sowohl ein Paket als auch einen Typen. Entsprechend erbt (wie oben bereits
beschrieben) ClassForName sowohl vom Programmelement ReflectiveTypeNameReference als
auch vom Programmelement ReflectivePackageNameReference je eine identifier-Variable und
zieht ebenso die Erzeugung zweier reflectiveBinding-Fakten nach sich. Die beiden Constraintregeln ReflectivePackageNamesEquality und ReflectiveTypeNamesEquality stellen dann die Übereinstimmung von Typ- und Paketnamen sicher.
Als eine weitere Bedingung aus einem Aufruf von Class.forName() ergibt sich, dass wegen
des qualifizierten übergebenen Typnamens sich der Typ nicht mehr aus seinem ursprünglichen Paket wegbewegen darf, wie es beispielsweise bei einem Move Type geschehen könnte.
Die folgende Regel ReflectiveClassForNameStayInPackage verhindert dies.
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
ReflectiveClassForNameStayInPackage
for all
r: Java.ClassForName
d: Java.NamedClassOrInterfaceType
p: Java.Package
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveBinding(r, p)
then
d.hostPackage = p
end
Zugreifbarkeiten Ebenso wie für reguläre Zugriffe auf die Member eines Typen in Java Regeln zur Zugrei arkeit greifen, etabliert auch die Reflection-API eine Menge von Zugrei arkeitsregeln. Für Refaktorisierungen ist die korrekte Einhaltung von Zugrei arkeitsbedingungen insofern relevant, als dass bei dem reflektiven Zugriffsversuch auf eine nicht zugrei are
Deklaration eine java.lang.IllegalAccessException ausgelöst wird, welche das Programmverhalten in unvorhergesehener Art beeinflussen kann. Im Einzelnen betroffen sind hiervon die Me-
. Berücksichtigung reflektiver Zugriffe
thoden newInstance von Class und Constructor, die Methode invoke der Klasse Method sowie
die diversen get- und set-Methoden der Klasse Field. Es werden also Konstruktor- und Methodenaufrufe sowie Feldzugriffe beschränkt. Typreferenzen – wie beispielsweise innerhalb
eines Class.forName(...) erfahren keine Einschränkungen durch Zugrei arkeiten, obwohl Typen mit Zugrei arkeitsmodifizierern ausgesta et sind. Dennoch werden diese indirekt bei
Zugriffen auf Member einer Klasse relevant (dazu weiter unten).
Neben der Tatsache, dass Typreferenzen keiner Zugrei arkeitsbeschränkung unterliegen,
unterscheiden sich die Zugrei arkeitsregeln für reflektive Zugriffe auch in weiteren Punkten von den regulären Zugrei arkeitsregeln. Einige davon ergeben sich aus technischen Restriktionen, andere hingegen scheinen von den Autoren der Reflection-API beliebig festgelegt
worden zu sein. Da es keine offizielle Dokumentation der Zugrei arkeitsregeln für JavaReflection gibt (die vorliegende Arbeit stü t sich im Wesentlichen auf spärliche JavadocKommentare sowie exzessives Testen), sind die Motivationen für diese Festlegungen unklar.
Technisch bedingt ist, dass die Zugrei arkeitsregeln für reflektive Zugriffe nicht zwischen
Topleveltypen und Membertypen unterscheiden. Während Java regulär den Zugriff auf private Deklarationen innerhalb desselben Typen auch von Membertypen aus gesta et, verweigert
die Reflection-API solche Zugriffe. So wird im folgenden Beispiel in Zeile
der Zugriff auf
das private Feld der umschließenden Klasse gesta et (es kommt zu keinem Kompilierfehler),
der Zugriff in der folgenden Zeile
hingegen führt zu einer IllegalAccessException.
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
public class C {
private String s = "abc";
static class MemberClass {
public static void main(String[] args) throws Exception {
C c = new C();
System.out.println(c.s);
System.out.println(C.class.getDeclaredField("s").get(c));
}
}
}
Ursächlich ist hierfür, dass zur Laufzeit durch den Compiler alle Typen (auch lokale und anonyme) in jeweils eigene Class-Dateien kompiliert werden. Eine etwaige Schachtelung ist für
die Laufzeitumgebung nicht mehr unmi elbar ersichtlich und wird offensichtlich deswegen
nicht mehr als Kriterium herangezogen. Während also auf private Deklarationen reflektiv nur
noch unmi elbar innerhalb desselben Typen zugegriffen werden kann, ist innerhalb desselben Pakets default-Zugrei arkeit hingegen stets ausreichend, was exakt den regulären (nicht
reflektiven) Zugrei arkeitsbedingungen entspricht. Unterschiede ergeben sich wieder bei der
protected-Zugrei arkeit, welche durch die Reflection-API wiederum gar keine Berücksichtigung erfährt und stets als default-Zugrei arkeit interpretiert wird. Für reflektive Zugriffe über
Paketgrenzen hinweg ist also zwingend public-Zugrei arkeit erforderlich.
Ähnlichkeiten zu den regulären Zugrei arkeitsregeln lässt wiederum eine Einschränkung
zur Zugrei arkeit deklarierender Typen beim Zugriff auf seine Member erkennen. So fordern
alle oben genannten Methoden zum Zugriff auf Konstruktoren und deren Member, dass auch
der jeweilige Empfängertyp zugrei ar ist. Ein Typ gilt dabei als reflektiv zugrei ar, wenn er
die reflektive Anweisung unmi elbar umschließt oder als default- oder protected-zugrei ar
und innerhalb desselben Pakets deklariert ist oder als public deklariert ist. In folgendem Bei-
. . Constraintregeln
spiel kommt es also in Zeile
zu einer IllegalAccessException, da zwar das Feld s zugrei ar
ist, aber nicht der umschließende Typ B.
1110
1111
1112
1113
1114
1115
1116
1117
package a;
public class A {
public static void main(String[] args) throws Exception {
Class bClass = Class.forName("b.B");
Object o = bClass.newInstance();
System.out.println(bClass.getDeclaredField("s").get(o));
}
}
1118
1119
1120
1121
1122
package b;
class B {
public String s = "abc";
}
Leider lassen sich auch diese Bedingungen zu Typ-Zugrei arkeiten keiner dem Autor bekannten, offiziellen Dokumentation entnehmen, sondern waren nur anhand von ausführlichen, manuellen Tests zu erschließen. Erschwerend kam dabei hinzu, dass nicht einmal die in
den IllegalAccessException-Objekten übergebenen Fehlermeldungen sachdienlich waren. Für
das vorstehende Beispiel liest sich diese
1123
Class a.A can not access a member of class b.B with modifiers "public"
wobei der dort genannte public-Modifizierer derjenige des Feldes s ist, nicht der eigentlich
relevante der Klasse B. Entsprechend wenig verwundert, dass häufig zu beobachten ist, wie
ratlose Benu er mit dieser Fehlermeldung in den üblichen Java-Hilfeforen anbranden.
Eine weitere Besonderheit im Umgang mit Zugrei arkeiten und Reflection ergibt sich daraus, dass es die Reflection-API erlaubt, Zugrei arkeiten zu umgehen. So können Constructor-,
Method- und Field-Objekte über die Methode setAccessible jeweils angewiesen werden, den
Zugrei arkeitsschu zu ignorieren.⁴¹ Eine solche Anweisung zur Umgehung von Zugreifbarkeiten gilt dabei allerdings weder für eine konkrete Deklaration einer Klasse, noch eine
Deklaration eines Objekts, sondern jeweils für ein konkretes Constructor-, Method- oder FieldObjekt. Alle anhand dieses Objekts durchgeführten Zugriffe sind dann von den Zugrei arkeitsregeln befreit. Das schließt jeweils auch die Bedingungen an Zugrei arkeiten von deklarierenden Typen ein. Die folgende Constraintregel ReflectiveAccessibility se t diese Zugreifbarkeitsbedingungen um.
1124
1125
1126
1127
1128
1129
1130
1131
ReflectiveAccessibility
for all
r: Java.ReflectiveAccess
d: Java.MemberOrConstructor
rt: Java.ClassOrInterfaceType
do
if
Java.reflectiveBinding(r, d),
⁴¹ Freilich kann dies verhindert werden, indem beim Start der virtuellen Maschine ein sogenannter und geeignet
konfigurierter SecurityManager übergeben wird. Dessen Existenz ist aber von konkreten Refaktorisierungen
unabhängig und wird hier entsprechend nicht weiter thematisiert.
. Berücksichtigung reflektiver Zugriffe
1132
1133
1134
1135
Java.reflectiveReceiver(r, rt)
then
r.accessEnabled = #public
or(
1136
1137
1138
(((((r.owner != d.owner) −> (d.accessibility > #private))
and ((r.hostPackage != d.hostPackage) −> (d.accessibility = #public)))
1139
1140
1141
1142
and ((rt != d.owner) −> (rt.accessibility > #private)))
and ((rt.hostPackage != d.hostPackage) −> (rt.accessibility = #public))))
end
Die Constraintvariable accessEnabled gibt dabei an, ob für das jeweils verwendete ConstructorMethod- oder Field-Objekt die Umgehung der Zugrei arkeitsprüfung aktiviert wurde. Ihr initialer Wert ergibt sich bei der Faktengenerierung direkt aus dem von TamiFlex ausgegebenen
Log, wie im Beispiel in Abbildung . in der le ten Spalte zu sehen.
Weitere Bedingungen zu Zugrei arkeiten ergeben sich aus den drei Methoden getField,
getMethod und getConstructor der Klasse Class. Diese Methoden können genu t werden, eine von einer Klasse deklarierte oder geerbte Deklaration (Felder, Methoden oder Konstruktoren, wobei le tgenannte allerdings in Java nicht vererbt werden) zu erfragen. Im Gegensa
zu ihren Pendants getDeclaredField, getDeclaredMethod und getDeclaredConstructor, die keine
geerbten Deklarationen zurückgeben, geben die drei zuerst genannten Methoden nur solche
Deklarationen zurück, die public deklariert sind. Ansonsten kommt es zu einer Ausnahme,
zum Beispiel einer NoSuchFieldException im Falle eines getField. Es leuchtet ein, dass deshalb
eine über eine solche Methode zurückgegebene Deklaration ihre Zugrei arkeit nicht verändern darf, was die folgende Constraintregel ReflectiveClassGetDeclarationStayPublic zusichert:
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
ReflectiveClassGetDeclarationStayPublic
for all
r: Java.ReflectiveMemberReference
d: Java.MemberOrConstructor
do
if
Java.reflectiveBinding(r, d)
then
d.accessibility = # public
end
In dieser Hinsicht identisch verhalten sich die drei Methoden getFields, getMethods und
getConstructors, die jeweils alle deklarierten und geerbten public-Deklarationen einer Klasse
zurückgeben. Für sie formulieren die Constraintregeln ReflectiveClassGetMethodsStayPublic
( S.
), ReflectiveClassGetConstructorsStayPublic ( S.
) und ReflectiveClassGetFieldsSet) diese Bedingungen analog, dann aber für jeweils alle public-Deklarationen
StayPublic ( S.
der jeweiligen Art innerhalb derselben Klasse.
Eine le te Bedingung für Zugrei arkeiten ergibt sich aus verschiedenen Methoden, die
jeweils direkt oder indirekt den Zugrei arkeitsmodifizierer einer Deklaration zurückgeben.
Das gilt insbesondere für die Methode getModifiers(), die von den Klassen Field-, Method-,
. . Constraintregeln
Constructor- und Class implementiert wird, aber auch für weitere Methoden derselben Klassen, wie toGenericString. Dass sich bei Vorliegen einer solchen Abfrage die Zugrei arkeit der
jeweiligen Deklaration nicht ändert, wird durch die folgende Constraintregel ReflectiveFieldGetModifiersKeepAccessibility sichergestellt.
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
ReflectiveFieldGetModifiersKeepAccessibility
for all
r: Java.ReflectiveAccessModifierReference
d: Java.MemberOrConstructor
do
if
Java.reflectiveBinding(r, d)
then
d.accessibility = r.returnedAccessModifier
end
Die dort verwendete Programmelementart ReflectiveAccessModifierReference ist Überart aller
reflektiven Zugriffe, die Informationen über Zugrei arkeiten zurückgeben.
Orte Bereits im vorangegangenen Abschni wurden mit den Methoden getDeclaredField,
getDeclaredMethod und getDeclaredConstructor der Klasse Class Beispiele für reflektive Zugriffe genannt, die Annahmen über den Ort einer Deklaration machen. Ist bekannt, dass ein
Feld vor einer Refaktorisierung über einen Aufruf von Class.getDeclaredField zurückgegeben
wurde und wird dieses Feld im Rahmen einer Refaktorisierung verschoben, muss hinterher derselbe Aufruf zwangsläufig in einer NoSuchFieldException resultieren. Die beiden Constraintregeln ReflectiveDeclaredMemberReferenceStayThere ( S.
) und ReflectiveDeclaredMemberSetReferenceStayThere ( S.
) stellen jeweils sicher, dass es in diesen Fällen zu keiner Verschiebung der Deklarationen kommt. Le tere der beiden Regeln behandelt die Varianten, die Mengen von Deklarationen zurückgeben.
Ein sich dazu ergebendes gegenteiliges Problem kann daraus resultieren, dass bei einer Abfrage von zum Beispiel getDeclaredFields nach einer Refaktorisierung sich die zurückgegebene Menge von Deklarationen ändert, nicht weil Deklarationen wegverschoben wurden, sondern weil neue Deklarationen hinzugekommen sind. Die beiden Constraintregeln ReflectiveClassGetDeclaredFieldsNoNewFields ( S.
) und ReflectiveClassGetDeclaredMethodsNoNewMethods ( S.
) nehmen sich jeweils dieser Fälle für die Methoden getDeclaredFields und
getDeclaredMethods an. Eine Berücksichtigung von Konstruktoren ist hierbei unnötig, da ein
Verschieben von Konstruktoren zwischen Klassen weder Bestandteil üblicher Refaktorisierungen ist, noch im Rahmen der vorliegenden Arbeit unterstü t wird.
Dieselben Bedingungen müssen weiterhin bei Aufrufen der Methoden getFields, getMethods
und getConstructors eingehalten werden. Dabei gibt es aber einerseits die Einschränkung,
dass nur public-Deklarationen betroffen sind, und andererseits eine Ausweitung auf Deklarationen der Superklassen, die bei diesen Methoden zusä lich berücksichtigt werden. Die
Constraintregeln ReflectiveClassGetFieldsNoNewFields ( S.
), ReflectiveClassGetMethodsNo) sowie ReflectiveClassGetConstructorsNoNewConstructors nehmen sich
NewMethods ( S.
dieser Fälle an. Hierbei sind wiederum Konstruktoren zu berücksichtigen, da sie durch Änderungen der Zugrei arkeit hin zu public zu der Menge der neu zurückgegebenen Deklarationen hinzukommen können.
. Berücksichtigung reflektiver Zugriffe
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
public class A {
public void m(String string) {
System.out.println(string);
}
public static void main
(String[] args) throws Exception {
String s = "abc";
Class a = A.class;
Class s = String.class;
Method m = a.getMethod("m", s);
m.invoke(new A(), s);
}
}
(a)
⇒
public class A {
public void m(String string) {
System.out.println(string);
}
public static void main
(String[] args) throws Exception {
Object s = "abc";
Class a = A.class;
Class s = String.class;
Method m = a.getMethod("m", s);
m.invoke(new A(), s);
}
}
(b)
Abbildung . .: Wird in Zeile
der deklarierte Typ von s zu Object geändert, bleibt das
Programmverhalten unverändert.
Eine le te Bedingung ergibt sich aus der Methode getDeclaringClass, welche von Field-,
Method- und Constructor-Objekten implementiert wird. Erfolgt der Aufruf einer solchen Methode für eine Deklaration, die während einer Refaktorisierung verschoben wird, ändert sich
zwangsläufig der zurückgegebene Wert. Die Regel ReflectiveDeclaringClassReferenceStayThere
( S.
) verhindert dies.
Typen Deklarierte Typen spielen im Zusammenhang mit der Reflection-API eine untergeordnete Rolle. Hauptsächlich relevant werden deklarierte Typen beim Zugriff auf Methoden und Konstruktoren einer Klasse. So erwarten für die Klasse class alle vier Methoden
getMethod, getDeclaredMethod, getConstructor und getDeclaredConstructor jeweils eine Angabe der deklarierten Parametertypen, um bei überladenen Methoden und Konstruktoren eine eindeutige Zuordnung zu erhalten. Die Constraintregeln ReflectiveParameterListMatching
( S.
) verhindert jeweils eine Abweichung der deklarierten Parametertypen im Falle eines
solchen Aufrufs.
Eine Berücksichtigung von Typen für Methodenaufrufe via Method.invoke (und analog Instanziierungen mi els Constructor.newInstance) ist hingegen nicht notwendig. Für diese sind
jeweils nur die Laufzei ypen der Objekte bei Ausführung des Programms relevant. Wie die
jeweiligen Referenzen auf die Objekte typisiert sind, ist unerheblich. Abbildung . zeigt ein
Beispiel einer G
D
T -Refaktorisierung, die zulässig ist. Ändert sich in
der Typ der Variablen s von String zu Object, kann diese dennoch per Reflection
Zeile
der Methode m(String s) übergeben werden. Erst wenn sich der Laufzei yp des durch s referenzierten Objekts zu Object ändern würde (indem der Initialisierungsausdruck von s beispielsweise durch new Object() erse t wird), kommt es zu einer IllegalArgumentException. Da
aber keine der in dieser Arbeit angesprochenen Refaktorisierungen Änderungen an Laufzeittypen vorsieht (Bindungen an Konstruktoren werden stets erhalten), ist dieser Fall nicht zu
berücksichtigen.
. . Constraintregeln
Unerwünschtes Verdecken Ähnlich wie es für reguläre Java-Zugriffe das Konzept des Verdecken (hiding) gibt, kann eine dem Verdecken ähnliche Situation für reflektive Ausdrücke auftreten. In folgendem Beispiel wird innerhalb der main-Methode die parameterlose Methode m
auf dem Typen C mi els getMethod abgefragt. Zurückgegeben wird die entsprechende, von
Super geerbte Methode.
1176
1177
1178
class Super {
public void m() { System.out.println("a"); };
}
1179
1180
1181
1182
class C extends Super {
public void n() { System.out.println("b"); };
}
1183
1184
1185
1186
1187
1188
1189
1190
1191
public class Reflection {
public static void main(String[] args) throws Exception {
Class c = C.class;
Method m = c.getMethod("m");
Object o = c.newInstance();
m.invoke(o);
}
}
Wird nun beispielsweise die Methode n in m umbenannt (oder eine andere parameterlose
Methode mit Namen m nach C verschoben), ändert sich die reflektive Bindung. Es wird beim
Aufruf von getMethod nicht mehr länger m aus Super zurückgegeben, sondern nun m aus C.
Die Constraintregel ReflectiveClassGetMethodNoHiding
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
ReflectiveClassGetMethodNoHiding
for all
r: Java.ClassGetMethod
d: Java.Method
rt: Java.NamedType
c: Java.Method
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveBinding(r, rt),
d != c
then (d.identifier != c.identifier)
or(c.accessibility < # public)
or(not Java.sub(c.owner, d.owner))
or(not Java.sub*(rt, c.owner)
or(d.parameters.declaredParameterType !=
c.parameters.declaredParameterType))
end
beugt entsprechenden Bindungsänderungen vor. Sie fordert nämlich, dass neben der referenzierten Deklaration jede weitere Methodendeklaration entweder einen abweichenden Namen,
abweichende Parametertypen oder zu geringe Zugrei arkeit haben muss oder nicht innerhalb der Typhierarchie zwischen referenzierter Deklaration und Empfängertyp deklariert ist.
. Berücksichtigung reflektiver Zugriffe
Analog dazu beugt die Constraintregel ReflectiveClassGetFieldNoHiding ( S.
) Bindungsänderungen bei Feldreferenzierungen vor, allerdings kann hier auf die Prüfung der Parametertypen verzichtet werden.
6.5. Zurückschreiben in reflektive Ausdrücke
Abschni . hat bereits angesprochen, dass nicht erfüllte Bedingungen aus den ReflectionConstraintregeln dieses Kapitels tendenziell eher ein Ablehnen der Refaktorisierung zur Folge
haben, als dass eine Erfüllung mi els des Constraintlösers möglich wäre. Die Ursache hierfür
ist, dass für viele der Variablen, die für reflektive Referenzen deklariert sind, keine entsprechende Änderung des Quellcodes gefunden werden kann. In der Folge müssen diese Variablen bei der Faktengenerierung als unveränderlich gekennzeichnet werden (dazu siehe Abschni . . ).
Betrachtet man die Unterarten von ReflectiveReference, so deklarieren oder erben diese insgesamt sechs verschiedene Variablenarten. Die Werte der direkt von ReflectiveReference geerbten Variablen owner und hostPackage ergeben sich aus dem jeweiligen Ort, der eine reflektive Referenz beheimatet. Er ist zwar variabel, allerdings nur in Ausnahmefällen auch Teil der
erlaubten Änderungen einer Refaktorisierung, sodass es unwahrscheinlich ist, dass der Constraintlöser durch zum Beispiel Verschieben einer Methode mit einem reflektiven Aufruf eine
Constraintlösung herbeiführen kann.⁴² Die übrigen Constraintvariablen identifier, returnedAccessModifier und parameters spiegeln weitere, übergebene oder empfangene Eigenschaften
von Deklarationen wider, die Constraintvariable accessEnabled repräsentiert eine Eigenschaft
des Objekts, über die der Zugriff erfolgte (zum Beispiel das Field-Objekt zu einem Feld). Diese Variablen in ihren Werten verändern zu können, kann helfen, eine Constraintlösung herbeizuführen. Ob die dazu nötigen Programmtransformationen aber den Vorstellungen des
Programmierers entsprechen, ist fraglich.
Im Fall der Constraintvariable accessEnabled kann ein geänderter Wert in den Quellcode
zurückgeschrieben werden, indem ein zusä licher Aufruf von setAccessible in das zu refaktorisierende Programm eingefügt wird. Soll zum Beispiel in folgendem Programm die Zugreifbarkeit von i in A hin zu private verringert werden, würde dies für den Aufruf in Zeile
eine IllegalAccessException auslösen.
1210
package a;
1211
1212
1213
1214
class A {
int i = 1;
}
1215
1216
1217
1218
1219
1220
1221
public class Reflection {
public static void main(String[] args) throws Exception {
Field field = Class.forName("a.A").getDeclaredField("i");
Object object = new A();
System.out.println(getVal(field, object));
}
⁴² Dass die Orts-Variablen eines reflektiven Aufrufs denen der jeweils umschließenden Deklaration entsprechen,
sichert die (bisher nicht erwähnte) Constraintregel ReflectiveReferenceSamePackage ( S.
) zu.
. . Zurückschreiben in reflektive Ausdrücke
1226
1227
1228
... <expression>.getInt(<parameter>);
⇒
Field <freshName> = <expression>;
<freshName>.setAccessible(true);
... <freshName>.getInt(<parameter>);
(a)
(b)
Abbildung . .: Allgemeines Muster, um beim Zurückschreiben geänderte Werte von
accessEnabled-Variablen abzubilden.
static int getVal(Field field, Object o) throws Exception {
return field.getInt(o);
}
1222
1223
1224
1225
}
Entsprechend würde aufgrund der Constraintregel ReflectiveAccessibility (siehe S.
, ebenso S.
) ein Constraint erstellt werden, welches für den Aufruf in Zeile
entweder
fordert, dass die Zugrei arkeit von i nicht private ist, oder, dass für field zuvor mi els der
setAccessible-Methode die Zugrei arkeitsmechanismen umgangen werden. Die erste Bedingung ist dabei nicht erfüllbar, sie widerspricht der Intention. Die zweite Bedingung ist erder Ausdruck field.setAccessible(true); eingefügt wird. Ein allfüllbar, indem vor Zeile
gemeines Muster hierfür ist in Abbildung . gegeben. Es ist aber zu beachten, dass als Nebenwirkung dessen dann für alle reflektiven Zugriffe, die über diesen Ausdruck erfolgen, die
Zugriffsregeln umgangen werden. Der Grund hierfür ist, dass ein einzelner Ausdruck mehrfach ausgeführt werden kann und somit mehrere reflektive Referenzen erzeugen kann. Würde
in obigem Beispiel also die Methode getVal auch noch von anderer Stelle aufgerufen werden,
würden die erweiterten Zugriffsrechte dann auch dort gelten.
Ähnlich kann man auch bei identifier-Variablen innerhalb von reflektiven Referenzen vorgehen. Abbildung . zeigt ein allgemeines Muster für eine Transformation, die angewendet
werden kann, wenn eine Klasse umbenannt werden soll, die innerhalb einer Class.forName(...)
referenziert wird. Hierbei tri aber das Problem, dass ein einzelner Ausdruck mehrfach ausgeführt werden kann, stärker zu Tage. Zwar wird bei der Transformation aus Abbildung .
noch auf den Namen geprüft, existieren aber zwei Klassen mit gleichem voll qualifizierten
Namen,⁴³ von denen nur eine umbenannt wird, gilt die Umformulierung des reflektiven Ausdrucks dennoch für beide. Doch auch wenn man davon ausgeht, dass eine solche Refaktorisierung bedeutungserhaltend ist, muss dennoch die Frage gestellt werden, ob sie tatsächlich
dem entspricht, was sich der Programmierer erhofft. Es darf vermutet werden, dass in einem
Großteil der Fälle dem Programmierer mehr mit einem Hinweis auf die jeweilig betroffene
reflektive Referenz geholfen ist, als mit den etwas befremdlich anmutenden Codetransformationen. Abbildung . hat bereits einen Kompromiss aufgezeigt, indem dort zusä lich ein
Kommentar eingefügt wurde, der den Programmierer direkt auf das Problem hinweist.
⁴³ Dieser Fall ist zwar exotisch, aber da Java das Hinzuladen von Klassen zur Laufzeit gesta et, kann er tatsächlich
eintreten.
. Berücksichtigung reflektiver Zugriffe
... Class.forName(<old name>)
// TODO: consider renaming of <old name>
// to <new name> in <expression>
if(<freshName>.equals("<old␣name>")
<freshName> = "<new␣name>";
... Class.forName(<freshName>)
(a)
(b)
1229
⇒
1230
1231
1232
1233
Abbildung . .: Allgemeines Muster, um beim Zurückschreiben geänderte Werte von
identifier-Variablen reflektiver Referenzen abzubilden.
6.6. Einschränkungen des vorgestellten Ansatzes
Die in diesem Kapitel beschriebene Laufzeitanalyse hilft gemeinsam mit den genannten Constraintregeln, während Refaktorisierungen ungewollte Änderungen des Programmverhaltens
zu erkennen und zu verhindern. Dennoch bringt der Ansa einige Einschränkungen mit sich,
die im Folgenden genannt werden sollen.
Als wesentliche Einschränkung ist zu nennen, dass die Laufzeitanalyse nur solche Aufrufe an die Reflection-API berücksichtigen kann, die während der Programmausführung vor
der Refaktorisierung tatsächlich aufgetreten sind. Wird die Laufzeitanalyse also beispielsweise während der Ausführung einer Testsuite durchgeführt, kann lediglich zugesichert werden, dass sich für diese Tests das Programmverhalten nicht ändert. Werden Aufrufe an die
Reflection-API durch die Testsuite nicht abgedeckt, werden hierfür keine Fakten angelegt und
es werden keine entsprechenden Constraints generiert.
Als ein weiteres Problem ist zu nennen, dass die aufgrund von Aufrufen an die ReflectionAPI erzeugten Constraints in Einzelfällen zu restriktiv sein können. Die Laufzeitananlyse erkennt nur, welche Aufrufe sta gefunden haben und welche Informationen über Programmelemente oder deren Zustand dabei über- oder zurückgegeben wurden. Es bleibt ihr verborgen, zu welchem Zweck die Daten jeweils verwendet werden. Ein prominentes Beispiel, für
welches hieraus unnötig scharfe Einschränkungen resultieren, ist das JUnit-Framework. Bis
zur dri en Version wurden als Tests auszuführende Methoden gekennzeichnet, indem ihre Namen mit dem Präfix test zu versehen waren. Wird in diesem Fall per Method.getName
der Methodenname abgefragt, erkennt der vorgestellte Ansa zwar, dass der Methodenname Einfluss auf das Programmverhalten (in diesem Falle die Menge ausgeführter Tests) hat
und lehnt eine Umbenennung ab. Gleichzeitig verkennt er aber, dass ein Umbenennen zum
Beispiel von testeDies zu testeDas die Testausführung nicht weiter beeinflusst. Dieses Phänomen betrifft insbesondere auch Funktionen, die nur Programminformationen abfragen, um
diese zum Beispiel in Logs zu schreiben. Nachdem eine Deklaration zum Beispiel umbenannt
wird, ist es dann vermutlich nicht Intention des Programmierers, dass hinterher noch derselbe, dann veraltete Name, in den Log-Ausgaben auftaucht. Handelt es sich wiederum nicht um
Logs, sondern um ein wieder einlesbares Datenformat, welches abwärtskompatibel zu halten
ist, ist genau gewünscht, weiterhin den alten Namen zu verwenden. Le tendlich ist es im Falle eines nicht erfüllbaren Constraintsystems somit wohl am sinnvollsten, dem Programmierer
lediglich die fehlschlagenden Reflection-Constraints zu präsentieren und ihn selbst entscheiden zu lassen, wie er mit diesen umgeht.
. . Einschränkungen des vorgestellten Ansa es
Eine eher exotische Einschränkung des vorgestellten Ansa es resultiert aus einer Schwäche
des TamiFlex-Werkzeugs. Dieses erkennt nur erfolgreiche Aufrufe an die Reflection-API. Wird
also beispielsweise über Method.invoke(...) eine Methode mit einer Menge von vom Typen
her nicht passender Parametertypen aufgerufen, sodass es zu einer IllegalArgumentException
kommt, findet dieser Aufruf in den TamiFlex-Ausgaben keine Berücksichtigung. Natürlich ist
es aber denkbar, dass durch eine Refaktorisierung (beispielsweise durch Änderung an den Parametertypen einer Methode) der vorher fehlgeschlagene Aufruf nun zum Erfolg führt. Sta
der Ausnahmebehandlung kommt es dann zum Methodenaufruf, was aller Vermutung nach
das Programmverhalten verändert. Dennoch werden solche Fälle durch den vorliegenden Ansa nicht abgedeckt.
7. Deklaration konkreter
Refaktorisierungswerkzeuge und
Integration in Eclipse
Die vorhergehenden Kapitel und haben sich ausführlich den Bedingungen gewidmet, die
einzuhalten sind, wenn Java-Programme bedeutungserhaltend transformiert – also refaktorisiert – werden sollen. Wirft man noch einmal einen Blick zurück auf das schematische Bild
eines Refaktorisierungswerkzeugs mit Refacola aus Abbildung . auf Seite , sind (gemeinsam mit der Sprachdefinition aus Kapitel ) in der Abbildung linksseitig die beiden oberen
Komponenten abgeschlossen.
Damit ist der Hauptbeitrag der vorliegenden Arbeit abgeschlossen. Dennoch sind – wie sich
aus oben genannter Abbildung ergibt – noch einige weitere Komponenten zu liefern, bis ein
fertiges Refaktorisierungswerkzeug geboren ist. Zunächst sind die Constraintregeln in ihrer
Formulierung unabhängig von einem konkreten Refaktorisierungswerkzeug. Erst mit Angabe von Refaktorisierungsdefinitionen (Abbildung . links unten) wird spezifiziert, was Intention und erlaubte Änderungen der fertigen Refaktorisierungswerkzeuge sein sollen. Weiterhin müssen Komponenten für die Faktenextraktion, das Zurückschreiben sowie die Benutzerschni stelle implementiert werden.
Dieses Kapitel beschreibt diese noch verbliebenen Komponenten.
7.1. Deklaration der Refaktorisierungswerkzeuge
Wie Kapitel beschrieben hat, sind die in der gegebenen Refacola-Sprachdefinition enthaltenen Variablen ausreichend, um einen Großteil solcher Refaktorisierungswerkzeuge zu implementieren, die Deklarationen und deren Eigenschaften in Java-Programmen verändern.
Insgesamt wurden im Rahmen dieser Arbeit fünf verschiedene Refaktorisierungen zur Implementierung ausgewählt, wobei mit der Auswahl eine gewisse Diversität erreicht werden
sollte. So entstanden zwei Verschieberefaktorisierungen – einmal auf Klassen- und einmal auf
Methodenebene – und je ein Werkzeug zur Änderung von Namen, deklarierten Typen und
Zugrei arkeiten.
Die einzelnen Werkzeuge bieten jeweils noch verschiedene Optionen, um die neben der Intention weiterhin erlaubten Änderungen zu steuern, sodass sich aus Refacola-Sicht insgesamt
verschiedene Refaktorisierungswerkzeuge ergeben. Der entsprechende Refacola-Code alangegeben.
ler Werkzeuge ist in Anhang C ab Seite
7.1.1. Move Compilation Unit
Die Absicht einer M
C
U -Refaktorisierung ist, eine Überse ungseinheit (also *.java-Datei) von einem Paket in ein anderes Paket zu verschieben, typischerweise um einen
. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse
oder mehrere Topleveltypen mitsamt enthaltenen Membern zwischen Paketen zu bewegen.
Die Intention (also der forced change) besteht im Ändern des hostPackage einer TypeRoot⁴⁴.
Als erlaubte Änderung muss es dem Constraintlöser gesta et sein, das hostPackage weiterer,
möglicherweise in der TypeRoot enthaltener Deklarationen zu ändern. Da beim Verschieben
einer Kompilierungseinheit in ein anderes Paket auch die jeweilige package declaration ( § . )
(das Schlüsselwort package gefolgt vom Paketnamen als erste Anweisung innerhalb einer
Überse ungseinheit) mit anzupassen ist, umfassen die erlaubten Änderungen ebenso einen
Wechsel des identifier von PackageDeclaration-Elementen.
Das Verschieben von Deklarationen über Paketgrenzen hinweg kann Einfluss auf Zugreifbarkeiten haben, wenn Deklarationen default- oder protected-Zugrei arkeit haben. Aus diesem Grund wurde für M
C
U
ein zweiter Modus implementiert, in dem
zusä lich Zugrei arkeitsmodifizierer angepasst werden dürfen. Im Folgenden wird – in Anlehnung an die Benennungen aus [SP a] – der Basismodus von M
C
U
ohne erlaubte Zugrei arkeitsänderungen mit dem Zusa NOC (no other changes) kenntlich
gemacht, der Modus mit erlaubten Änderungen von Zugrei arkeiten trägt den Zusa CA
(change accessibility).
7.1.2. Pull Up Member
P
U M
ist eine der klassischen, schon von Fowler [Fow ] beschriebenen Refaktorisierungen – dort allerdings noch einmal unterteilt nach P
U M
und P
U F
.
Ziel ist es, eine Instanzmethode oder ein nicht-statisches Feld in einen Supertypen zu verschieben. Soweit mehrere Subklassen dasselbe Feld oder dieselbe Methode implementieren, sieht
Fowler vor, dass durch das P
U diese Redundanz behoben wird, indem alle Implementierungen in den Subklassen entfernt werden. Da die bisher entwickelten Constraintregeln keine
Prüfung zulassen, ob verschiedene Implementierungen derselben Methode dasselbe Verhalten aufweisen, beschränkt sich das in dieser Arbeit entwickelte Werkzeug auf den Fall des
P
U aus nur einer Klasse heraus.
U in der Änderung des Wertes einer owner-Variable
Der forced change besteht beim P
eines Feldes oder einer Methode. Die weiteren erlaubten Änderungen müssen tlowner- und
hostPackage-Eigenschaften von Membern und Referenzen einschließen, die in den zu verschiebenden Membern enthalten sein können. Weiterhin muss im Rahmen der erlaubten Änderungen auch zugelassen werden, Ausdrücke in ihrem inferierten Typen zu ändern, denn
wenn zum Beispiel innerhalb einer zu verschiebenden Methode eine this-Referenz liegt, erzwingen die Constraints eine Änderung ihres Typen auf die jeweilige Superklasse,⁴⁵ was für
sich allein genommen noch keine Ablehnung der Refaktorisierung begründen darf.
In der in Anhang C gezeigten Implementierung ist eine Besonderheit zu beobachten: Ansta
nur den Wechsel des owner der zu verschiebenden Methode als Intention zu formulieren und
⁴⁴ Die Java-Sprachdefinition unterscheidet nicht anhand unterschiedlicher Programmelemente zwischen als
quellcode vorliegender *.java und kompilierten und in das Programm eingebundenen *.class-Dateien. Da für
beide gleiche Bedingungen gelten, werden diese sta dessen einheitlich als TypeRoot behandelt. Technisch ist
es also bei dem beschriebenen Werkzeug auch möglich, als Intention die Verschiebung einer *.class-Datei anzugeben, faktisch führt dies aber nie zu erfüllbaren Constraints, da die hostPackage-Variable einer *.class-Datei
wie alle anderen Variablen von Programmelementen aus Bibliothekscode als unveränderlich markiert sind.
⁴⁵ wie die Regel This ( S.
) sicherstellt
. . Deklaration der Refaktorisierungswerkzeuge
den Wechsel des tlowner und des hostPackage im Rahmen der Constraintlösung über die entsprechenden Constraints sicherzustellen, sind dort ebenso besagte Wechsel des tlowner und
hostPackage Teil des forced change. Diese müssen also beim Initiieren der Refaktorisierung für
die zu verschiebende Methode im Vorhinein ermi elt und ebenso passend gese t werden.
Dieser Aufwand rechtfertigt sich dadurch, dass in der Folge bei Angabe der allowed changes
diese forced changes genu t werden können, um per @forced die Domänen der Constraintvariablen einschränken zu können (dazu Abschni . . ).
Sollen für eine Anwendung eines P
U M
nur die oben aufgezählten erlaubten Änderungen gelten, sei dieser noch recht restiktive Modus erneut mit dem Suffix NOC (no other
changes) bezeichnet. Darüber hinaus werden in vorliegender Implementierung noch zwei weitere, auch miteinander kombinierbare Modi angeboten. Da auch beim P
U M
Verschiebungen über Paketgrenzen hinweg sta finden können, wurde ebenso eine mit Suffix CA
(change accessibility) bezeichnete Variante implementiert, die auch Änderungen von Zugreifbarkeiten ermöglicht.
Weiterhin können P
U -Refaktorisierungen daran scheitern, dass eine weitere, in einer
hochzuschiebenden Deklaration referenzierte Deklaration mit verschoben werden muss. Abbildung . auf Seite
zeigte bereits ein Beispiel hierfür. Dieser Modus sei mit Suffix CL
(change location) gekennzeichnet. Der Fall, dass sowohl weitere Deklarationen verschoben als
auch Zugrei arkeiten geändert werden dürfen, ist durch den Modus CACL (change accessibility and change location) repräsentiert.
7.1.3. Rename
R
ist der Klassiker unter den Refaktorisierungen und entsprechende Werkzeuge sind
die häufigst ausgeführten Refaktorisierungswerkzeuge überhaupt [MHPB ]. Die vorliegende Implementierung erlaubt ein Umbenennen von Feldern, Typen, Methoden und Konstruktoren. Implementierungen eines R
für Parameter und lokale Variablen mitsamt dafür
nötiger Regeln wurden auf den Ergebnissen dieser Arbeit au auend durch Spengler [Spe ]
geliefert und werden in vorliegender Arbeit daher nicht weiter untersucht. Die Intention eines
R
ist die Umbenennung einer benannten Deklaration. In der Folge sind als erlaubte Änderungen nicht nur Umbenennungen von Referenzen zu gesta en, sondern auch Umbenennungen weiterer Deklarationen. So erzwingt das Umbenennen eines Typen auch das Umbenennen seiner Konstruktoren und – im Falle von primären Topleveltypen – der umschließenden Überse ungseinheit. Bei Methoden sind ebenso überschriebene und überschreibende
Methoden mit umzubenennen. Nicht zu den erlaubten Änderungen zählt hingegen das Ändern der Bezeichner von Feldern. Der einzige Fall, in welchem das Umbenennen eines Feldes
als erlaubte Änderung dienlich wäre, wäre eine Namenskollision, bei der der neue Name eines Feldes mit dem eines weiteren Feldes in Konflikt stände (zum Beispiel, wenn beide in
derselben Klasse deklariert sind). In diesem Fall müsste der Benu er interaktiv einen neuen
auf Seite ).
Namen vergeben (siehe dazu auch Fußnote
Weitere erlaubte Änderungen, die noch zusä liche Modi rechtfertigen würden, ergeben
sich nicht. Deswegen wurde nur ein Modus – im Folgenden CI (Change Identifier) genannt –
implementiert.
. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse
7.1.4. Generalize / Specialize Declared Type
Beim G
beziehungsweise S
D
T
besteht die Intention der Refaktorisierung aus einer Änderung des Typen einer Deklaration entweder hin zu einem Sub- oder
Supertypen des ursprünglich verwendeten Typen. In vorliegender Implementierung wurde
sich dabei auf Typen von Feldern und Rückgabetypen von Methoden beschränkt. Das Ändern des Typen einer solchen Deklaration wird als forced change mi els Umbelegung des
typeBinding der TypeReference innerhalb der jeweiligen Deklaration umgese t. Als erlaubte Änderungen sind dem Constraintlöser in jedem Fall die Änderung des identifier solcher
Typreferenzen zu gesta en, da ansonsten das jeweilige Constraintsystem nicht lösbar wäre.
Weiterhin ist es Ausdrücken zu gesta en, ihren jeweiligen inferierten Typen expressionType
beziehungsweise inferredClassOrInterfaceType zu ändern. Damit wird bewirkt, dass zum Beispiel bei Typänderung eines Feldes die Refaktorisierung nicht allein dadurch zurückgewiesen wird, dass auch Referenzen auf dieses Feld ihren inferierten Typen ändern müssen und
es nicht können.
Es ist zu erwarten, dass viele Typ-Refaktorisierungen abgelehnt werden, wenn sich nur der
Typ einer einzelnen Deklaration ändern darf. Mitunter ist auch weiteren Deklarationen eine
Änderung ihres deklarierten Typen zu gesta en. Abbildung . auf Seite zeigte hierfür ein
Beispiel. Aus diesem Grunde wurde ein zusä licher Modus implementiert, der au auend
auf oben beschriebener Variante – sie sei im Nachfolgenden ebenso als NOC bezeichnet – noch
die Änderung weiterer deklarierter Typen von Feldern, Methoden, Parametern und Variablen
zulässt. Dieser Modus wird entsprechend mit dem Suffix CT (change type) gekennzeichnet.
Zwar verschiebt ein G
beziehungsweise S
D
T
keine Deklaration über Paketgrenzen, ändert aber Bindungen von Typreferenzen, sodass Bedarf bestehen
kann, zuvor an einer bestimmten Stelle nicht zugrei are Typen dort zugrei ar zu machen.
Um Änderungen von Typzugrei arkeiten möglich zu machen, wurde analog zu den oben beschriebenen Werkzeugen ein entsprechender Modus CA (change accessibility) implementiert.
Für sowohl weitere Typ-Änderungen als auch Änderungen von Zugrei arkeiten wurde ein
CACT-Modus (change accessibility and change type) implementiert.
7.1.5. Change Accessibility
Die Absicht einer C
A
-Refaktorisierung ist die Änderung der Zugrei arkeit
einer Deklaration und damit die allgemeinere Formulierung des schon bei Fowler [Fow ] beschriebenen H
M
. Im Gegensa zu diesem macht C
A
weder Vorgaben zur Art der Deklaration, noch dazu, ob die Zugrei arkeit erweitert oder beschränkt
werden soll. Mit einer Ausnahme ergibt sich beim C
A
kein Vorteil daraus, neben der Intention noch weitere Änderungen von Zugrei arkeiten zuzulassen. Lediglich beim Überschreiben oder Hiding von Methoden kann das Ändern von Zugrei arkeiten
weiterer Methoden zusä liche Refaktorisierungen ermöglichen, wie bereits das Beispiel aus
Abbildung . im ersten Kapitel dieser Arbeit zeigte. Daher wurde neben dem NOC-Modus
für alle Arten von Deklarationen, die einen Zugrei arkeitsmodifizierer haben, ein CA-Modus
speziell für C
A
auf Methoden angeboten.
. . Deklaration der Refaktorisierungswerkzeuge
7.1.6. Validierung der Benutzereingaben
Zu jedem der im vorhergehenden Abschni . beschriebenen Refaktorisierungswerkzeuge
wurde ferner eine zusä liche Komponente implementiert, die auf herkömmliche, imperative
Art und Weise einige wesentliche Vorbedingungen prüft. Dies umfasst in der Hauptsache
ganz offensichtlich nötige Prüfungen der Benu ereingaben auf Plausibilität, so zum Beispiel,
ob überhaupt alle nötigen Parameter entsprechend gese t sind. Weiterhin wird geprüft, ob die
vom Benu er gegebenen Parameter zu den von den Refacola-Refaktorisierungen erwarteten
Eingaben passen, so zum Beispiel, ob eine für ein P
U ausgewählte Methode nicht als static
deklariert ist oder ob für ein C
D
T
der ausgewählte Typ kein primitiver ist
(dazu Näheres in Abschni . ).
Außerdem umfasst die zusä liche Prüfung von Vorbedingungen auch solche, die aus verschiedenen in dieser Arbeit jeweils zuvor genannten Gründen nicht constraintbasiert erfolgen.
Beispiele dafür sind der Test auf Gültigkeit von durch den Benu er beim R
vorgegebenen Namen (dazu näher in Abschni . . ) oder der Test, ob ein zu verschiebendes, finales
Feld auch initialisiert wurde (dazu Abschni . ).
7.1.7. Auswahl einer Constraintlösung
Abschni . hat beschrieben, dass eine jede Constraintlösung für ein gegebenes Refaktorisierungsproblem (einen korrekten Regelsa vorausgese t) zwar eine korrekte Lösung für dieses
darstellt, allerdings nicht immer den Wünschen des Benu ers entsprechen muss. Insbesondere sind Änderungen von Variablenwerten zu vermeiden, die nicht aktiv zur Lösung des
Constraintsystems beitragen. So kann es zum Beispiel Variablenbelegungen geben, bei denen
die Erse ung des Variablenwerts durch den initialen Wert der Variablen weiterhin zu einem
gelösten Constraintsystem führt. Wie in Abschni . beschrieben, ist die Berechnung einer
minimalen Lösung häufig wegen der sich ergebenden Laufzeiten ausgeschlossen.
Bei händischen Tests hat sich sehr häufig als störend gezeigt, dass insbesondere bei Modi
mit erweiterten erlaubten Änderungen (CA, CL, CT, CACL, CACT) diese durch den Constraintlöser auch genu t wurden, obwohl jeweils ein restriktiverer Modus genügt hä e. Es
wurden also zum Beispiel bei einem G
D
T
im CT-Modus weitere Typen mit angepasst, obwohl dies gar nicht nötig gewesen wäre. Daher nu t die vorliegende
Implementierung das in Abschni . bereits vorgeschlagene Verfahren, bei einer Werkzeuganwendung mit erweiterten erlaubten Änderungen zunächst die jeweils restriktiveren Modi
beginnend bei NOC durchzuprobieren, um bei Vorliegen einer Lösung für diesen die Anzahlen unnötiger Änderungen zu minimieren. Im praktischen Umgang mit den Werkzeugen hat
sich gezeigt (und in der Evaluation in Kapitel . bestätigt), dass insbesondere die restriktiven
Modi mit nur sehr geringen Laufzeiten auskommen, weswegen es durch das gegebenenfalls
mehrfache Constraintlösen hier nicht zu spürbaren Einbußen kommt.
Als weiteren störenden Faktor im praktischen Umgang mit den Werkzeugen hat sich das
unnötige Anpassen von Zugrei arkeiten erwiesen. So ist insbesondere bei Anwendungen,
die tatsächlich eine Änderung von Zugrei arkeiten erfordern und somit tatsächlich von den
erlaubten Änderungen profitieren, zu beobachten, dass es zu unnötig vielen Quellcodeänderungen kommt. So fiel bei händischen Tests zum Beispiel auf, dass das Verringern der Zugreifbarkeit einer Methode gleichzeitig bedingte, dass weitere Methoden derselben Klasse oder ih-
. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse
rer Superklassen in ihrer Zugrei arkeit verringert wurden, obwohl dies für eine Constraintlösung nicht notwendig war. Insbesondere bei Deklarationen, die bewusst (zum Beispiel beim
Au au eines Frameworks) höhere Zugrei arkeit haben als eigentlich nötig, ist dieses Verhalten unerwünscht.
Als pragmatische Lösung wird in der vorliegenden Implementierung vor der Refaktorisierung in der Faktenbasis die Zugrei arkeit von public-Deklarationen als unveränderlich
gekennzeichnet. Auch wenn dies in der Praxis zum gewünschten Verhalten der Werkzeuge
führt, ist dieses Vorgehen keineswegs generisch. Umgekehrt liegt nahe, dass sich zu vielen Refaktorisierungswerkzeugen Heuristiken finden lassen, mit denen sich nicht erwünschte (aber
dennoch bedeutungserhaltende) Teiländerungen ausschließen lassen und welche sich unterbinden lassen, indem Constraintvariablen gezielt als unveränderlich gekennzeichnet werden.
Sollte beispielsweise der Fall auftreten, dass für die Implementierung eines Change Declared Type-Werkzeugs unnötig viele Deklarationen in ihrem Typen angepasst werden, kann über
einen Zuweisungsgraphen [TR ] die Menge genau solcher Deklarationen gefunden werden,
die tatsächlich über Zuweisungen von der Ursprungsdeklaration erreicht werden kann. Alle
weiteren Deklarationen können dann als unveränderlich markiert werden.
7.1.8. Benutzerschnittstelle
Für die in dieser Arbeit implementierten Werkzeuge wurde eine Benu erschni stelle implementiert, um einen testweisen Gebrauch innerhalb der Eclipse-Pla form möglich zu machen.
Da der Hauptbeitrag dieser Arbeit – die Entwicklung der Constraintregeln für besagte Refaktorisierungswerkzeuge – unabhängig von einer speziellen Entwicklungsumgebung ist, wurde
die Benu erschni stelle bewusst nur prototypisch implementiert.
Die einzelnen Werkzeuge lassen sich über Kontextmenüeinträge der outline in Eclipse aufrufen, die sich dann jeweils öffnenden Dialoge fragen nötige Eingabeparameter sowie Modi ab.
Die Eingabedialoge sind dabei auf ein Minimum reduziert. Sollen auch reflektive Zugriffe berücksichtigt werden, ist zuvor eine mi els des separat zu beziehenden TamiFlex [BSS+ ] generierte Log-Datei in einem bestimmten Verzeichnis zu hinterlegen. Auch gestaltet sich mitunter die Auswahl der Parameter in den Dialogen aufwändig. So werden bei einem G
/S
D
T
derzeit alle wählbaren Typen in einer einfachen drop-down-Liste
angezeigt. Abgesehen von der damit einhergehenden mühsamen Auswahl benötigt allein die
Berechnung der Typen und damit die Anzeige des Dialogs einige Sekunden. Eine geeignete
Implementierung würde hier die bereits in anderen Dialogen von Eclipse vorhandenen Möglichkeiten nu en, bequem und effizient Deklarationen auszuwählen. Im Falle der Auswahl
eines Typen wäre dies entweder eine inkrementell berechnete Baumansicht der Typhierarchie
oder ein Textfeld mit automatischer Vervollständigung, in welches ein Typname einzugeben
ist.
Soweit der Benu er eine Vorschau der durchzuführenden Änderungen wünscht, wird ihm
diese angezeigt. Da die für die Implementierung verwendete Rückschreibekomponente von
Frenkel [Fre ] entsprechende Schni stellen bedient, konnten für die Vorschaudialoge die bereits vom Eclipse-Framework angebotenen Funktionen genu t werden. Weiterhin wird dank
derselben Schni stellen die ebenso verfügbare Option, Refaktorisierungen rückgängig zu machen, auch vom Eclipse-Framework übernommen. Da die Entwicklung einer Erklärungskomponente noch aussteht (dazu in Abschni . ), brechen Werkzeuganwendungen bei einem
. . Faktengenerierung
nicht lösbaren Constraintsystem derzeit mit einem entsprechenden Hinweisfenster, aber ohne
nähere Angabe zum nicht erfüllbaren Constraintsystem, ab. Abbildung . zeigt verschiedene
Bildschirmfotos der implementierten Benu erschni stelle.
7.2. Faktengenerierung
Wie bereits in Abschni . dargestellt, ist die Grundlage der Constraintgenerierung die Faktenbasis, gegen welche die Prämissen der Constraintregeln ausgewertet werden und somit
durch Erse ung der Regelvariablen innerhalb der Konklusion die Constraints entstehen. Damit eine solche Faktenbasis gemäß der jeweiligen Sprachdefinition aufgebaut werden kann,
generiert die Refacola eine Sprach-API (siehe auch Abbildung . ), in welcher jede Art von
Programmelement und jede Faktenart durch eine Java-Klasse repräsentiert ist. Diese Klassen
besi en wiederum Methoden, um die initialen Werte der ihnen zugehörigen Constraintvariablen respektive die von den Fakten referenzierten Programmelemente anzugeben.
Im derzeitigen Stadium der Entwicklung sieht das Refacola-Framework lediglich eine Abfrage-Engine vor, die stets den Au au einer vollständigen Faktenbasis fordert, also die Generierung aller Programmelemente und Fakten eines Programms unabhängig von der Art des
Eingabeprogramms und Refaktorisierungsproblems. Der Au au einer Faktenbasis, in welcher Fakten und Programmelemente nur dann erstellt werden, wenn sie tatsächlich durch eine
entsprechende Abfrage angefordert werden, wird hingegen nicht unterstü t. Somit reduziert
sich die Faktengenerierung auf das Erstellen der abstrakten Syntaxbäume, auf ein Traversieren des Eingabeprogramms und das Berichten stets aller Programmelemente mit den initialen
Werten ihrer Variablen sowie der Fakten.
Die vorliegende Implementierung baut auf den JDT [JDT ] auf, welche zu gegebenen JavaProjekten in Eclipse auf Anforderung bereits Syntaxbäume der Quellcodedateien bereitstellen und in denen viele Abhängigkeiten – insbesondere Bindungen – bereits aufgelöst und
somit navigierbar sind. Der abstrakte Syntaxbaum ist somit angereichert zu einem abstrakten
Semantikgraphen (engl. abstract semantic graph) [DRW ]. Ebenso wie die zu traversierenden
Semantikgraphen Zyklen enthalten können, kann es auch in der zu erstellenden Faktenbasis
zu Zyklen kommen. So ist ein Eingabeprogramm denkbar, in welchem zwei Klassen A und
B jeweils Methoden B m() und A m() mit Rückgabetypen der jeweils anderen Klasse beinhalten. Um die initialen Werte für die Typen der Methoden zu se en, muss zunächst die jeweils
andere Klasse als Programmelement verfügbar sein. Um derartige Konflikte zu lösen, wurde – ähnlich zum Compilerbau, wo moderne Sprachen mit ihren Vorwärtsreferenzen ebenso
nicht mit einem Single-Pass-Compiler [ASU ] auskommen – auch bei der Faktengenerierung
auf ein mehrstufiges Verfahren gese t. In diesem sind verschiedene Faktenlieferanten in einer
Ke e hintereinander geschaltet. Jeder Faktenlieferant ergänzt jeweils die bis dahin erstellte
Faktenbasis um neue Programmelemente, initiale Werte von Variablen oder Fakten. So werden in einem ersten Durchlauf lediglich Programmelemente für Pakete, Klassen und Interfaces, Methoden und Felder erstellt. Im folgenden Durchlauf werden dann die initialen Werte
der ihnen zugehörigen Variablen gese t, wobei nun angenommen werden kann, dass für deren Constraintvariablen die nötigen Programmelemente bereits vorliegen. Unabhängig von
den Vorteilen bei der Handhabung von Zyklen hat die Aufteilung der Faktengenerierung in
einzelne Faktenlieferanten auch den Vorteil, dass die Faktengenerierung insgesamt einfacher
. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse
Abbildung . .: Bildschirmfotos des implementierten User Interface. Links oben das Kontextmenü einer Methode in Eclipse, die neu angebotenen Werkzeuge sind durch
Icons roter Refacola-Dosen gekennzeichnet. Rechts oben ist die Eingabemaske eines P
U M
mit Auswahl des CACL-Modus gezeigt, unten die
Vorschau der berechneten Änderungen mit nebenstehendem Originalcode.
Im Einzelnen ist zu sehen, dass das Hochziehen der Methode calculate() ebenso ein Hochziehen der Methode loadData() bewirkt. Ebenso wird bei le terer
Methode der private-Modifizierer entfernt, um weiterhin die Zugrei arkeit
innerhalb des Konstruktors von Sub zu gewährleisten.
. . Namensqualifizierung
zu warten und erweitern ist, da sich die Komplexität innerhalb der einzelnen Faktenlieferanten deutlich reduziert. So kann zum Beispiel die Faktengenerierung für sub-Fakten mit dem
Au au der Subtyphierarchie gekapselt und unabhängig von anderen Komponenten implementiert werden. Ebenso können auch unkompliziert einzelne Faktenlieferanten deaktiviert
werden, so zum Beispiel der Faktenlieferant für Programmelemente und Fakten, die sich aus
reflektiven Aufrufen ergeben, wenn der Benu er bei Ausführung des Werkzeugs keine dynamische Codeanalyse wünscht.
7.3. Namensqualifizierung
Abschni . hat beschrieben, dass in der derzeitigen Refacola-Sprachdefinition der Sprache
Java ebenso wie in den Constraintregeln keine Qualifizierungen von Referenzen berücksichtigt werden und dass dies wegen der vielen sich daraus ergebenden bedingten und damit teuren Constraints auch nicht sinnvoll ist. Sta dessen kommt in vorliegender Arbeit zur Qualifizierung von Referenzen der Ansa von Schäfer et al. zur Anwendung, wie er bereits ausführlich in Abschni . beschrieben wurde und unter Mitwirkung des Autors vorliegender Arbeit
mit dem constraintbasierten Ansa kombiniert wurde [STST ]. Da le tgenannte Arbeit zur
Vereinigung von constraintbasiertem Ansa und locked bindings allerdings auf dem JastAddJCompiler [EH ] au aute und die Implementierung somit in aspektorientiertem JastAddCode gegeben war, war eine direkte Wiederverwendung des dortigen Codes im Zusammenspiel mit den JDT [JDT ] nicht möglich. Sta dessen wurden im Rahmen dieser Arbeit jene
Teile des locking und unlocking von Bindungen erneut implementiert, die für die Berechnung
von Qualifizierern zuständig waren.Dabei wurde auf eine Prüfung vieler bei Schäfer behandelter Einzelfälle verzichtet, was teilweise Einschränkungen für den Benu er der sich ergebenden Werkzeuge mit sich bringt. So wird zum Beispiel beim Verschieben einer Deklaration
nicht im Einzelnen geprüft, ob in ihr enthaltene Typreferenzen am neuen Ort einen entsprechenden Typ-Import benötigen, dieser schon vorhanden ist oder gegebenenfalls gar ein Typ
gleichen Namens schon importiert ist. Sta dessen wird jede verschobene Typreferenz pauschal voll qualifiziert, um ohne tiefere Programmanalyse Konflikte in jedem Fall ausschließen
zu können. Ebenso bewirkt ein Verschieben von Typen, dass alle Referenzen auf diesen Typ
voll qualifiziert werden, unabhängig davon, ob eine Qualifizierung tatsächlich nötig ist. Auch
sind die durchgeführten Prüfungen auf Verscha ung von Feldern, welche ebenso Qualifizierungen nötig machen, nur genau auf die oben beschriebenen Refaktorisierungswerkzeuge hin
beschränkt. All diese Einschränkungen sind für einen Produktiveinsa der Werkzeuge und
des Frameworks hinderlich, stellen aber keine prinzipiellen Probleme des constraintbasierten
Ansa es dar, da [STST ] bereits aufgezeigt hat, dass und wie ein effektives Postprocessing
zur Namensqualifizierung implementierbar ist. Eine im Erscheinen befindliche Arbeit von Nicolai [Nic ] nimmt sich bereits einer solchen Komponente an, was die hier nur prototypische
Implementierung rechtfertigt.
7.4. Rückschreibekomponente
Wie in Abschni . beschrieben, wird beim constraintbasierten Ansa nicht direkt eine Transformation des Syntaxgraphen berechnet, sondern die Änderungen in abstrakterer Form als Ei-
. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse
genschaften von Programmelementen beschrieben. Eine Rücküberse ung in konkrete Änderungen im abstrakten Syntaxbaum (oder auch direkt im Quelltext) ist anschließend in einem
von der Constraintlösung unabhängigen Schri mi els einer Rückschreibekomponente durchzuführen. Eine solche Rückschreibekomponente, implementiert von Frenkel [Fre ], findet in
dieser Arbeit Verwendung.
Ebenso wie der Faktengenerator baut auch die Rückschreibekomponente auf den JDT auf.
Zur Eingabe steht ein bereitgestelltes Interface bereit [Fre ], über welches die Constraintlösung berichtet werden kann. Als Ausgabe erhält man sogenannte Rewrite-Objekte, welche
Transformationen der Syntaxbäume repräsentieren und direkt an die Eclipse-IDE zwecks Darstellung in einer Vorschau oder Anwendung auf den Quellcode übergeben werden können.
Dabei behandelt die Rückschreibekomponente von sich aus schon verschiedene technische
Details, die innerhalb der Constraintregeln somit ausgespart werden konnten. So werden bei
der Faktengenerierung explizit gemachte – aber im Quellcode nur implizit vorhandene – Modifizierer beim Zurückschreiben korrekt ausgelassen und auch Felddeklarationen, die mehrere Felder innerhalb einer Anweisung deklarieren, korrekt in einzelne Anweisungen aufgetrennt, soweit sich Änderungen nur auf eines der Felder beziehen [Fre ].
8. Evaluation
Dieses Kapitel soll Antworten auf die Fragen liefern, ob der in dieser Arbeit entwickelte Sa
von Constraintregeln korrekt und mit annehmbarer Laufzeit arbeitet und ob die in Kapitel
angeführten Vorteile constraintbasierter Refaktorisierungen – insbesondere das einfache Ermöglichen nötiger Folgeänderungen – in der Praxis tatsächlich Relevanz haben.
Im Wesentlichen baut die Evaluation dabei auf einem systematischen und automatisierten Anwenden der in Kapitel beschriebenen Refaktorisierungswerkzeuge auf Open-SourceProgrammen auf, wobei für jede Anwendung gemessen wurde, ob eine Constraintlösung
vorlag, nach welcher Ausführungszeit diese gefunden wurde, wie viele Änderungen diese
im Code auslöst und ob das Programm nach der Transformation fehlerfrei kompilierte. In
Stichproben wurden auch den Open-Source-Programmen beiliegende Testsuiten ausgeführt.
Zum Test der in Kapitel beschriebenen Constraintregeln wurden insgesamt mehr als
.
Refaktorisierungen angestoßen, von denen in ca. % eine Constraintlösung gefunden und
damit eine passende Quelltex ransformation angeboten werden konnte. In weiteren .
Tests wurden Refaktorisierungen angestoßen, um Aussagen über die in Kapitel beschriebenen Constraintregeln zu reflektivem Programmverhalten zu gewinnen. Weitere Messungen,
insbesondere eine umfassende Testsuite mit über .
handgeschriebenen JUnit-Testfällen
(davon knapp
aus [Spe ]), von denen jeder eine einzelne Refaktorisierung von der Faktengenerierung bis zum Zurückschreiben ausführt, ergänzen die Evaluation.
8.1. Forschungsfragen
Die Vorgehensweise der Evaluation orientiert sich am Goal-Question-Metric-Ansa von Basil
et al. [BCR ], wobei bereits der oben stehende erste Absa dieses Kapitels die Ziele der Evaluation (goal) beschreibt. Aus diesem Ziel lassen sich die folgenden Forschungsfragen (question) ableiten:
Frage 1: Arbeiten die implementierten Constraintregeln und die daraus entwickelten
Werkzeuge korrekt? Für die entwickelten Refaktorisierungswerkzeuge muss gelten, dass
sie gültigen Quellcode erzeugen, also solchen, der nach der Transformation kompiliert, und
dass dessen Programmverhalten unverändert bleibt. Weiterhin muss gelten, dass die Werkzeuge die Benu erwünsche tatsächlich umse en, also beispielsweise nicht einfach gar keine
Änderung ausgeführt wird. Weiterhin sind von den Werkzeugen Eigenschaften zu erfüllen,
die für jedes Programm gelten sollten: sie sollten keine ungültigen Zustände annehmen (in Java also insbesondere keine nicht abgefangenen Ausnahmen auslösen) und regulär terminieren
(engl. proper termination) [Dij ].
Frage 2: Unterliegen die constraintbasierten Werkzeuge übermäßig starken Einschränkungen? Übermäßig starke Einschränkungen [SMG ] liegen vor, wenn eine Codetransfor-
. Evaluation
mation (gegebenenfalls mit weiteren, erlaubten Codetransformationen) bedeutungserhaltend
möglich gewesen wäre, aber dennoch durch das Werkzeug abgelehnt wurde. Ein Refaktorisierungswerkzeug, das nur in Einzelfällen von erfüllten Vorbedingungen ausgeht und Transformationen anbietet, kann zwar nicht direkt als inkorrekt bezeichnet werden, allerdings ist
sein praktischer Nu en dennoch begrenzt.
Frage 3: Bringen zusätzliche, erlaubte Änderungen in der Praxis einen Mehrwert? Ein
Vorteil constraintbasierter Refaktorisierungswerkzeuge liegt darin, ohne nennenswerten Zusa aufwand bei der Implementierung weitere, erlaubte Änderungen zuzulassen. So kann das
Verschieben von Deklarationen das Ändern von Zugrei arkeiten bedingen, was wiederum
das Ändern weiterer Zugrei arkeiten bedingt. Es soll festgestellt werden, ob und wie häufig
diese Fälle in der Praxis überhaupt auftreten.
Frage 4: Sind die sich ergebenden Laufzeiten praxistauglich? Ein Refaktorisierungswerkzeug mit (zu) langen Berechnungszeiten wird faktisch nicht zum Einsa kommen, zumal
es derzeit ohnehin schon mit durch den Programmierer händisch durchgeführten Refaktorisierungen konkurrieren muss [MHPB , VCN+ ]. Ab welchen Laufzeiten ein Werkzeug
konkret an Tauglichkeit einbüßt, lässt sich nicht genau definieren und hängt sowohl vom Benu er als auch von der konkreten Art des Refaktorisierungsproblems ab. Generell lässt sich
aber annehmen, dass die Berechnungszeiten den unteren einstelligen Sekundenbereich nicht
übersteigen sollten.
Frage 5: Bringt die Behandlung der sich aus der Reflection-API ergebenden Bedingungen einen Mehrwert? Kapitel hat eine Reihe von Bedingungen aufgezeigt, die sich bei
der Refaktorisierung von reflektivem Java-Code ergeben und wie diese Bedingungen durch
constraintbasierte Refaktorisierungswerkzeuge Berücksichtigung finden können. Es soll festgestellt werden, ob und in wie weit Notwendigkeit für eine Berücksichtigung dieser Bedingungen besteht.
8.2. Korrektheitsbeweise
Insbesondere die erste Forschungsfrage – die nach der Korrektheit der implementierten Constraintregeln und daraus resultierender Werkzeuge – schürt den Wunsch nach einem formalen
Korrektheitsbeweis, aber auch bei der Frage nach zu starken Restriktionen wäre ein solcher
wünschenswert. Leider ist so ein Beweis nicht vollständig durchführbar – und damit im Fall
von Refaktorisierungswerkzeugen nahezu wertlos.
Ein Korrektheitsbeweis für Java-Refaktorisierungswerkzeuge müsste als Eingabe die JavaSprachspezifikation haben. Diese liegt aber nur in Form mehrerer hundert Seiten weitgehend
textueller Beschreibung vor [GJSB ]. Der erste Schri eines Beweises wäre also, die Sprachspezifikation in eine geeignete formale Notation zu überführen, anhand derer ein systematischer Abgleich der implementierten Constraintregeln mit der Sprachspezifikation erfolgen
könnte. Da jedoch die vorliegenden Constraintregeln genau denselben Zweck haben, Teile
der Java-Sprachspezifikation zu formalisieren, ist unklar, wie dann die Korrektheit der im
. . Korrektheitsbeweise
Rahmen des Beweises nötigen Formalisierung der Sprachspezifikation nachgewiesen werden
soll.
Schaut man sich in der Literatur Korrektheitsbeweise für Formalisierungen (von Teilen) der
Java-Sprachspezifikation an, haben sie stets mit demselben Problem zu kämpfen. Niemals ist
vollständig gesichert, dass alle in der Sprachspezifikation genannten Bedingungen berücksichtigt wurden. Abschni . nannte bereits Arbeiten zur Formalisierung von Java, die im
Wesentlichen von Nipkow und Oheimb [NO ] stammen und die starke Parallelen zu den
von Tip et al. [TKB ] genannten Constraintregeln zu Typ-Refaktorisierungen zeigen. Dabei strengten beide formale Korrektheitsbeweise an, wenn auch das Beweisziel jeweils unterschiedlich war.
Bei Nipkow et al. ist das Beweisziel, die Korrektheit des Typsystems von Java zu zeigen.
Genauer haben sie bewiesen, dass für ein beliebiges, gültiges (also kompilierendes) Java-Programm mit einem typsicheren Programmzustand (ein Programmzustand, in dem jede Variable tatsächlich ein Objekt referenziert, das mindestens dem Typen entspricht, mit dem die
jeweilige Variable annotiert ist) auch das Ausführen einer beliebigen, vom Compiler als typsicher erkannten Anweisung zu einem typsicheren Zustand führt. Dazu wurden sowohl die
Typregeln der Sprache, als auch die Bedeutung der weiteren in Java zur Verfügung stehenden
Anweisungen von der natürlichen Sprache der Sprachspezifikation in eine formale Darstellung übertragen, die die weiteren Beweisschri e ermöglicht hat.
Tro der durch diesen Beweis gewonnenen Aussagen zur Korrektheit des Java-Typsystems
bleibt eine mögliche Fehlerquelle, die der Beweis nicht vollständig schließen kann. Es bleibt
unklar, ob eine der sich aus dem natürlichsprachlichen Teil der Java-Sprachspezifikation ergebenden Bedingung übersehen wurde. Ein solches Versehen würde nämlich im weiteren Beweis nicht zwangsläufig auffallen. Nimmt man an, dass zum Beispiel eine Typregel zu streng
übernommen wurde, ergibt der Beweis erst recht, dass Java typsicher ist. Wurde hingegen
eine Typregel zu schwach übernommen (oder übersehen), kann es sein, dass das Typsystem
von Java bereits im Vorhinein rigoroser als nötig in seinen Typregeln war.
Während unklar bleibt, ob dies auch für allgemeine Arbeiten zur Formalisierung der JavaSprachspezifikation gilt, ist nach Erfahrung des Autors vorliegender Arbeit das Übersehen
oder falsche Übertragen von sich aus der Sprachspezifikation ergebenden Bedingungen die
häufigste Fehlerquelle für Refaktorisierungswerkzeuge.⁴⁶ Dies gilt für seine eigenen Arbeiten, aber auch die anderer Entwickler. Ein Beispiel kann hier das durch Tip et al. [TKB ]
constraintbasiert implementierte E
I
liefern, welches innerhalb der JDT von
Eclipse [JDT ] verfügbar ist. Während die von Tip et al. angegebenen Constraintregeln reguläre Methodenüberschreibungen ( § . . . ) korrekt berücksichtigen, wurde offensichtlich die zu Beginn von Kapitel thematisierte und in der Sprachspezifikation nur versteckt
erwähnte Möglichkeit der Überschreibung von Methoden, die nicht aus einer Superklasse
stammen ( § . . . ), übersehen. Als Folge erlaubt das resultierende Werkzeug ungültige
Programmtransformationen, Abbildung . zeigt ein Beispiel, welches sich in Eclipse einfach
nachstellen lässt. Die ebenso von Tip et al. [TKB ] gegebenen Korrektheitsbeweise für dieses
Werkzeug, die den Nachweis erbringen sollen, dass die refaktorisierten Programme stets typkorrekt sind, erweisen sich, wie anhand dieses Beispiels zu erkennen ist, als unzureichend. Sie
⁴⁶ Das bei Erstellung jeden Programms unvermeidliche Auftreten von Flüchtigkeitsfehlern, die sich zügig mi els
einfacher Regressionstests aufdecken lassen, sei hier einmal außer Acht gelassen.
. Evaluation
1234
1235
1236
1237
1238
class A {
public void m(A a) {
a.m(a);
}
}
1239
1240
1241
1242
interface I {
void m(A a);
}
⇒
class A implements J {
public void m(A a) {
a.m(a);
}
}
interface I {
void m( A a);
}
1243
interface J { }
1244
1245
1246
class B extends A implements I {}
class B: extends A implements I {}
(a)
(b)
Abbildung . .: Das von Tip et al. [TKB ] implementierte E
I
verkennt Methodenüberschreibungen geerbter Interfacemethoden durch Superklassen. In
der Folge erlaubt es für das Programm (a) das Einführen und Verwenden des
Interfaces J innerhalb der Methodensignatur von m in I. Die dadurch verloren
gegangene Methodenüberschreibung resultiert im Ausgabeprogramm (b) in
einem Kompilierfehler für die Klasse B.
bauen darauf auf, dass alle sich aus der Sprachspezifikation ergebenden Bedingungen vollständig erfasst wurden – eine Vorausse ung, von der wegen der Komplexität und dem geringen Formalisierungsgrad der Java-Sprachspezifikation nicht ausgegangen werden darf.
8.3. Tests
Neben den oben genannten Problemen formaler Beweise ist gerade bei jüngeren Arbeiten zu
Refaktorisierungswerkzeugen (und auch Programmierwerkzeugen allgemein) zu beobachten, dass sta formaler Beweise insbesondere Tests zur Evaluation herangezogen werden (dazu auch Abschni . ). So ist allein das Testen von Refaktorisierungswerkzeugen selbst Thema einiger jüngerer Arbeiten [DDGM , TS , EH , SMG , SGM , GBO+ ] und es kann
auf eine Vielzahl von Publikationen geblickt werden, die zur Evaluation eines Programmierwerkzeugs primär auf die Möglichkeiten des Testens se en [Ste , BFS , KS , ST , Ikk ,
FMM+ , RH , SKP , SSA+ , STST , RHS ].
Auch diese Arbeit baut ihre Evaluation auf Tests auf, wobei unterschiedliche Ansä e verfolgt wurden. Einerseits wurden Regressionstests durchgeführt, die als Eingabe jeweils kleine,
speziell hierfür handgeschriebene Eingabeprogramme ha en. Sie liefern insbesondere Antworten auf die ersten beiden Forschungsfragen. Andererseits erfolgten Tests, bei denen die
Eingabe aus realen Open-Source-Programmen besteht, auf denen die implementierten Refaktorisierungswerkzeuge systematisch angewendet wurden. Sie liefern Antworten auf die erste,
dri e, vierte und fünfte Forschungsfrage.
. . Tests
Regressionstests Parallel zur Implementierung der in Kapitel vorgestellten Refaktorisierungswerkzeuge erfolgte auch der Au au einer Suite von Testfällen. Jeder dieser Testfälle
führt genau eine Refaktorisierung durch, indem zunächst ein neues Projekt mit im Testfall
vorgegebenen Quellcodedateien erstellt, hierauf eine Refaktorisierung angestoßen und das
Ergebnis der Refaktorisierung mit einem Erwartungswert vergleichen wird. Soweit ein gelöstes Constraintsystem erwartet wird, werden ebenso die durchgeführten Quelltex ransformationen mit im Testfall vorgegebenen Erwartungswerten verglichen. Die Eingabeprogramme
bestehen jeweils aus typischerweise nur zwei bis vier Deklarationen, gegebenenfalls mit darin enthaltenen Referenzen auf diese. Die Tests wurden jeweils so formuliert, dass nach Möglichkeit Rückschlüsse auf die Korrektheit einzelner Constraintregeln zu erlangen waren, die
dann im jeweiligen Testfall entscheidenden Einfluss auf das Ergebnis haben sollten. Da aber
typischerweise mehrere Constraintregeln gemeinsam Einfluss auf die Constraintmenge und
damit Constraintlösung haben, waren viele Regeln auch nur kombiniert mit anderen Regeln
zu testen.
Insgesamt wurden
Testfälle zusammengetragen. Von diesen wurden mit leichten Anpassungen aufgrund unterschiedlicher Schni stellen
Testfälle aus der ebenso vom Autor
stammenden Implementierung zu [ST ] übernommen. Weitere
Testfälle konnten aus der
eigenen Implementierung zu [STST ] übernommen werden. Mit insgesamt
Testfällen
stammt ein Großteil der genu ten Testfälle aus der Arbeit von Spengler [Spe ] und testet
insbesondere Namensregeln. Die übrigen
Testfälle waren bisher unveröffentlicht.
In der Hauptsache beschränken sich die vorliegenden Tests auf C
A
-, R und G
/S
D
T -Refaktorisierungen, da sich mit diesen wegen der jeweils geringeren Anzahl erlaubter Änderungen einzelne Constraintregeln gezielter
testen lassen. Nur neun beziehungsweise acht Tests wenden ein M
C
U
beziehungsweise P
U an.
Die Probanden Insbesondere die Fragen , und zielen auf im Alltag eines Softwareentwicklers zu erwartende Ergebnisse ab, weswegen sich eine Evaluation au auend auf Transformationen realer Softwareprojekte unterschiedlicher Größe (den Probanden) anbietet. Tabelle . listet die zur Anwendung gekommenen und frei verfügbaren Projekte mit einigen ihrer
Kenndaten auf. Die Spalte JDK gibt Aufschluss über die von den Projekten benötigte JavaVersion. Alle drei . -Projekte machen von den in dieser Version neu hinzugekommenen generischen Typen Gebrauch. Die Spalten Pakete, Typen und Member geben die Anzahl der Pakete, Topleveltypen und Member der einzelnen Projekte an, die nach der Faktengenerierung
innerhalb der Menge P enthalten sind. Neben solchen Programmelementen, die im Quellcode
vorliegen, sind hier auch Elemente aus den durch die jeweiligen Probanden referenzierten Bibliotheken (jar-Dateien) enthalten, soweit sie zur Erstellung der Faktenbasis notwendig sind.
Insbesondere sind alle solche Typen (und deren nicht-private Member) aus Bibliotheken in
der Faktenbasis enthalten, die Supertypen eines im Programmcode vorliegenden Typen sind.
Da die Mengen der Typen und Member nur wenig Rückschlüsse auf die tatsächliche Menge
und Komplexität der Anweisungen eines Programms erlaubt, ist zusä lich noch die Anzahl
der Ausdrücke (expressions) im Code angegeben, wobei auch bei ineinander geschachtelten
Ausdrücken jeder innere Ausdruck gezählt wurde. Weiterhin gibt Tabelle . Auskunft über
die Gesam ahl der Programmelemente p ∈ P (P.-Elem) und Fakten f ∈ F , wie sie sich bei
. Evaluation
der Faktengenerierung ergibt.
Eine Schwachstelle des RTT-Testens ist, dass es nur Aussagen über Refaktorisierungen innerhalb der jeweilig genu ten Probanden erlaubt. Gleichzeitig ist aber denkbar, dass ein bestimmtes Muster oder Programmkonstrukt einer Programmiersprache durch die Entwickler
der Probanden nicht oder nur selten genu t wurde. Die einzige Möglichkeit, diesem Problem
gegenzusteuern, besteht in der Wahl einer möglichst breiten Basis von Probanden. So wurde
bei den in Tabelle . genannten Probanden Wert auf eine gewisse Diversität gelegt. Viele Probanden stammen aus unterschiedlichen Entwicklerkreisen. JUnit ist als Proband zwar doppelt vorhanden, allerdings ergibt sich hier eine deutliche Unterscheidung aus dem Versionssprung von Java . zu . , in welchem sich allein durch die Einführung von Annotationstypen, Enums und generischer Typen eine deutliche Änderung des Codes und der verwendeten
Programmiermuster ergeben hat.
Tabelle . .: Die für die Evaluation der Constraintregeln aus Kapitel zum Einsa gekommenen Testprojekte.
Projekt
Joda Convert .
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
JDK
.
.
.
.
.
.
.
.
Pakete
Typen
Member
.
.
.
Ausdrücke
.
.
.
.
.
.
.
P.-Elem.
.
.
.
.
.
.
.
.
Fakten
.
.
.
.
.
.
.
.
Bevor die Testprojekte Verwendung fanden, wurden sie in zweierlei Hinsicht modifiziert,
um den in Abschni . beschriebenen Einschränkungen gerecht zu werden. Einerseits wurden statische Aufrufe auf Instanzvariablen erse t, indem für jeden solchen Aufruf der jeweilige Empfänger durch den vom Compiler inferierten Typen erse t wurde. Andererseits wurden variable Parameterlisten durch Array-Typen erse t. Beide Arten der Änderung sind bedeutungserhaltend und entsprechen genau den Schri en, die durch den Java-Compiler bei
Erzeugung des Bytecodes ohnehin vorgenommen werden und wie sie in einer weiterentwickelten Version der Werkzeuge durch ein entsprechendes Pre- und Postprocessing bei Faktengenerierung beziehungsweise vor dem Zurückschreiben automatisch angewendet und rückgängig gemacht werden könnten (dazu ebenso Abschni . ).
Eine Laufzeitanalyse zum Beispiel anhand der Ausführung von JUnit-Tests für die in Tabelle . genannten Probanden fand nicht sta . Entsprechend liefern alle für diese Probanden
erhobenen Daten keine Aussage über die in Kapitel beschriebenen Constraintregeln für Zugriffe auf die Reflection-API. Sta dessen wird eine Evaluation dieser Regeln auf einer separaten Probandenmenge in Abschni . beschrieben. Die separate Evaluation beider Regelmengen bringt dabei durchaus Vorteile. So kann sich eine fehlende Reflection-Regel nicht durch
einen Kompilierfehler nach der Refaktorisierung äußern, denn diese Regeln sollen allein das
Programmverhalten bedingen. Allerdings ist es dennoch denkbar, dass eine (korrekte oder
unkorrekte) Reflection-Regel genau eine solche Refaktorisierung zufälligerweise verhindert.
Somit ist bei einem separaten Testen beider Regelsä e insgesamt von einer besseren Testab-
. . Refactoring Tool Testing der implementierten Werkzeuge
deckung auszugehen.
8.4. Refactoring Tool Testing der implementierten Werkzeuge
Einen wesentlichen Teil der Evaluation macht das RTT-Testen (siehe Abschni . ) der implementierten Refaktorisierungswerkzeuge aus. In den durchgeführten Testläufen kamen dabei
vier verschiedene Orakel zum Einsa . Ein erstes Orakel prüft auf ungültige Programmzustände, insbesondere aufgetretene und nicht abgefangene Ausnahmen. Soweit für eine Werkzeuganwendung eine Constraintlösung gefunden werden kann – also eine Programmtransformation angeboten wird – prüft ein zweites Orakel, ob diese Programmtransformation tatsächlich eine ist, sich also das Ausgabeprogramm vom Eingabeprogramm unterscheidet. Ist
dem so, prüft ein dri es Orakel, ob dieses Programm kompiliert. Ein viertes Orakel versucht
das erhaltene Ausgabeprogramm auf Bedeutungserhaltung zu testen, indem es die den OpenSource-Programmen beiliegenden Testsuiten ausführt. Wegen der sich durch das vierte Orakel ergebenden sehr hohen Laufzeiten wurde dieses nur stichprobenhaft für einzelne Projekte
in Betrieb genommen.
Refaktorisierungs-Adapter Das Testen der vorliegenden fünf Refaktorisierungswerkzeuge
machte die Implementierung fünf verschiedener Adapter notwendig, die im Folgenden beschrieben sind.
• M
C
U
wurde auf jede Überse ungseinheit der Probanden angewendet, wobei einmal in ein (gegebenenfalls neu erzeugtes) leeres Paket und einmal in ein
zufällig⁴⁷ ausgewähltes, vorhandenes und nicht-leeres Paket verschoben wurde, soweit
der Proband mehr als ein nicht-leeres Paket aufwies. Da das implementierte M
C U -Werkzeug die Option bietet, wahlweise Zugrei arkeiten von Typen und
Membern mit anzupassen, ergaben sich somit pro Überse ungseinheit bis zu vier Anwendungen.
• P
U M
wurde für jede Methode und jedes Feld ausgeführt, deren deklarierender Typ eine im Quellcode vorliegende Superklasse hat. Durch die zwei miteinander
kombinierbaren Optionen, Zugrei arkeiten anzupassen und weitere Deklarationen mit
zu verschieben, ergaben sich für jede solche Deklaration stets vier Anwendungsstellen.
• R
wurde auf jede Typ-, Methoden- und Felddeklaration einmal angewendet, wobei jeweils ein frischer, im Programm zuvor nicht gebrauchter Name gewählt wurde.
• G
/S
D
T
wurde auf jedes Feld und jede Methode im Programm angewendet. Als neu zu wählender Typ der jeweiligen Deklaration wurde zufällig aus den Sub- und Supertypen des ursprünglichen Typen der Deklaration gewählt.
Insgesamt wurden aus dieser Typhierarchie vier Typen pro Anwendungsstelle gewählt.
Stand dabei der Typ java.lang.Object mit zur Auswahl (indem er nicht schon ursprünglicher Typ gewesen ist), wurde sta dessen dieser zuzüglich drei weiterer zufälliger Typen
⁴⁷ Um die Tests tro des Zufalls dennoch reproduzierbar zu gestalten, wurde bei allen Experimenten auf Pseudozufall mit festem Startwert (engl. seed) zurückgegriffen.
. Evaluation
ausgewählt. Da in Java jeder Typ von java.lang.Object erbt, schien ein Favorisieren dieses Typen sinnvoll, um an möglichst vielen Anwendungsstellen erwünschtes oder unterwünschtes Subtyping zu provozieren. Durch die zwei miteinander kombinierbaren
Optionen, wahlweise Zugrei arkeiten von Typen und deklarierte Typen weiterer Programmelemente mit anzupassen, ergaben sich maximal sechzehn Anwendungsstellen
pro Deklaration, je nach verfügbaren Sub- und Supertypen aber häufig weniger.
• C
A
wurde auf jede Typdeklaration (mit Ausnahme anonymer und
lokaler Klassen), Methodendeklaration sowie Felddeklaration angewendet. Dabei ergaben sich für jede Deklaration insgesamt drei Anwendungsstellen, indem der (gegebenenfalls auch nur implizit) vorhandene Zugrei arkeitsmodifizierer durch jeden der
drei weiteren in Java verfügbaren Zugrei arkeitsmodifizierer erse t wird. Für Methodendeklarationen ergaben sich darüber hinaus noch drei weitere Anwendungsstellen,
da hier zusä lich die Option geboten wird, weitere Methodendeklarationen in ihrer Zugrei arkeit mit anzupassen.
Um die Messergebnisse nicht zu verfälschen – insbesondere was Laufzeiten angeht – nutzen sämtliche Refaktorisierungs-Adapter die in Abschni . . beschriebenen Routinen, um
solche Refaktorisierungen auszufiltern, die wegen nicht durch Constraints geprüften Bedingungen scheitern, sondern schon zuvor abgebrochen wurden. Somit sind in den sich aus dem
Refactoring Tool Testing ergebenden Messungen nur noch solche Anwendungen enthalten,
bei denen tatsächlich Constraints generiert und zu lösen versucht wurden.
Ergebnisse des Refactoring Tool Testing Tabelle . schlüsselt die Menge der sich ergebenden Anwendungsstellen und erfolgreich durchführbarer Refaktorisierungen nach Probanden auf. Die Spalte total zeigt die Menge der von den oben beschriebenen RefaktorisierungsAdaptern gefundenen Anwendungsstellen für das jeweilige Werkzeug. Die Spalte erfolgreich
zeigt sowohl in absoluter Zahl als auch mi els Prozentsa an, wie häufig im jeweiligen Fall
eine Constraintlösung gefunden werden konnte, also wie häufig die Refaktorisierung durchgeführt werden konnte und Änderungen in den Code geschrieben wurden. Insgesamt wurden
.
Refaktorisierungen angestoßen (Summe total), von denen .
(Summe erfolgreich), also rund , % durchführbar waren und für welche eine Programmtransformation
des Quellcode durchgeführt wurde.
Die ersten beiden Orakel vermochten keine Fehler zu erkennen: Bei keiner der Anwendungen eines Refaktorisierungswerkzeugs kam es zu einer nicht-abgefangenen Ausnahme, die
an das Eclipse-Framework weitergereicht werden musste; weiterhin wurde auch für jede Anwendung festgestellt, dass mindestens eine Änderung im Quellcode vorgenommen wurde.
Das Compiler-Orakel hingegen vermochte Fehler zu erkennen, insgesamt traten
Kompilierfehler auf. Obwohl dies insgesamt nicht einmal , % aller Anwendungsfälle ausmacht,
wurden die entsprechenden Fehler dennoch von Hand analysiert. In der Hauptsache betroffen
war das C
T -Werkzeug, angewendet auf die Projekte Junit . (
Fehler)
und Jakarta Commons Collections . (
Fehler). Es zeigte sich, dass nicht die Constraintregeln für die Fehler zu verantworten sind, sondern mangelnde oder fehlerhafte Vorabprüfungen, insbesondere, was den Umgang mit generischen Typen anbelangt. Abschni . hat bereits ausgeführt, dass innerhalb dieser Arbeit nur bedingt Unterstü ung für generische Typen
Move C. Unit
total
erfolgreich
, %
, %
, %
, %
, %
, %
, %
, %
.
.
, %
Pull Up Member
total
erfolgreich
, %
, %
, %
, %
, %
.
, %
.
.
, %
.
, %
.
.
, %
.
.
.
.
total
Rename
erfolgreich
, %
, %
, %
, %
, %
.
, %
.
, %
.
, %
.
, %
Gen./Spec. Decl. Type
total
erfolgreich
, %
, %
, %
.
, %
.
, %
.
, %
.
, %
.
.
, %
.
.
, %
Projekt
Erlaubte Änderungen
Joda Convert .
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
total
Move C. Unit
NOC
CA
NOC
Pull Up Member
CA
CL CACL
Gen./Spec. Decl. Type
NOC CA
CT CACT
Change Acc.
total
erfolgreich
, %
.
, %
.
.
, %
.
.
, %
.
.
, %
.
.
, %
.
.
, %
.
.
, %
.
.
, %
Change Meth. Acc.
NOC
CA
Tabelle . .: Erfolgreiche Refaktorisierungen aufgeschlüsselt nach erlaubten Änderungen.
Projekt
Joda Convert .
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
total
Tabelle . .: Gesamtüberblick über alle durchgeführten Refaktorisierungen.
. . Refactoring Tool Testing der implementierten Werkzeuge
. Evaluation
geboten wird. So werden beispielsweise keine Wandlungen deklarierter Typen von generischen Typen hin zu nicht-generischen Typen und umgekehrt unterstü t. Entsprechend müssen solche Refaktorisierungsgesuche im Vorhinein abgelehnt werden (siehe Abschni . . ),
dennoch waren hier die Vorabprüfungen teils zu schwach implementiert. Beispielsweise wurde für Jakarta Commons Collections . die Änderung des Rückgabetypen Map.Entry<K, V> einer Methode hin zu Object nicht unterbunden. Ein Blick in die Implementierung zeigte, dass in
der Vorabprüfung Membertypen nicht geeignet berücksichtigt wurden. Ebenso zu schwache
Prüfungen erfolgten bei Methoden, die in ihrer Methodensignatur Typvariablen aus überschriebenen Methoden durch konkrete Typen erse ten. Auch dieser Fall wird nicht unterstü t (siehe Abschni . ), es fand aber dennoch keine ausreichende Vorabprüfung sta .
Weitere Kompilierfehler gingen auf die R
-Refaktorisierung zurück, wobei sich die
Kompilierfehler erneut allein auf die beiden Projekte Jakarta Commons Collections . ( Fehler) und Junit . ( Fehler) verteilten. Sechs weitere Fehler zeigten sich beim P
U auf
dem Junit . -Projekt. Hier zeigte sich die im Nachgang sta findende und nur prototypisch
implementierte Namensqualifizierung für die Fehler verantwortlich. Teils kam es zu semantischen Fehlern, wenn beispielsweise versucht wurde, eine Typvariable zu qualifizieren. Ebenso wurden aber auch Qualifizierungen teils nicht passend aktualisiert, was insbesondere beim
Umgang mit statischen und on-demand-Importen ( § . ) zu beobachten war. Erste händische
Tests mit den jüngst durch Nicolai [Nic ] entwickelten, umfangreicheren Komponenten zur
Namensqualifizierung zeigten, dass dort bereits ein Großteil der in dieser Arbeit gefundenen
Fehler behoben werden konnte.
Das vierte Orakel, welches nach jeder Werkzeuganwendung die für den Probanden vorhandenen Testsuiten ausführt, wurde ausschließlich für den JUnit . . -Probanden aktiviert, was
in etwa % der Anwendungsfälle entspricht. Es lag die Vermutung nahe, dass insbesondere
beim JUnit-Projekt die zuverlässigsten Tests vorhanden waren. Hier konnten zwar in sehr geringem Umfang einzelne Fehler aufgrund von fehlschlagenden Testfällen gefunden werden,
eine händische Inspektion dieser ergab aber, dass die Ursache sich aus der Verwendung der
Reflection-API durch JUnit und seine Testfälle ergab. Es handelte sich um Fehler, die durch
die in Kapitel vorgestellten Regeln hä en verhindert werden können, aber wegen der in
Abschni . vorgebrachten Gründe nicht Teil dieses Testlaufs waren.
Wegen der unterschiedlichen Struktur der einzelnen Probanden ergibt sich bei den Erfolgsquoten der Werkzeuge eine gewisse Varianz. Auffällig in Tabelle . ist zum Beispiel das
R
-Werkzeug, bei dem für sechs Projekte eine Erfolgsquote von über % zu erreichen
war, hingegen beim org.apache.commons.io . -Projekt die Erfolgsquote unter % und bei Jakarta Commons Collections . gar unter % lag. Ursache hierfür ist, dass beide Projekte in
großem Umfang von Klassen der Java-Standardbibliothek ableiten und deren Methoden überschreiben, was ein Umbenennen dieser Methoden unmöglich macht.⁴⁸ Beim org.apache.commons.io . -Projekt sind dies im Wesentlichen Stream-Klassen aus dem java.io-Paket, bei den
Jakarta Commons Collections . die Collection-Interfaces aus dem java.util-Paket, die neu implementiert werden.
⁴⁸ Sichergestellt wird dies durch die Regel OverridingMethodNames ( S.
Eigenschaft der Programmelemente aus Bibliotheken.
) in Verbindung mit der read-only-
. . Refactoring Tool Testing der implementierten Werkzeuge
Beantwortung von Frage 1 Die erhaltenen Daten legen den Schluss nahe, dass die Constraintregeln korrekt arbeiten. Es sind keine ungültigen Programmzustände aufgetreten und
die wenigen, nicht kompilierenden Ausgaben der Werkzeuge lassen sich auf Fehler in Komponenten zurückführen, die nicht Teil der Constraintgenerierung oder -lösung sind. Auch konnten durch das Testsuite-Orakel keine Anwendungsfälle gefunden werden, die auf Fehler in
den in Kapitel entwickelten Constraintregeln schließen lassen.
Fraglich ist, ob die Probanden und die durch den Refactoring Tool Tester darauf gefundenen Anwendungsstellen für die Refaktorisierungen repräsentativ für die im Alltag der Softwareentwicklung anfallenden Refaktorisierungsarbeiten sind. So handelt es sich bei den Probanden um Projekte in gereiftem Stadium. In der Praxis mag es aber insbesondere vermehrt
zu Refaktorisierungen kommen, während das zu refaktorisierende Programm noch in einem
unfertigeren Zustand ist. So ist es denkbar, dass während der Softwareentwicklung ein G D
T
eine größere Rolle spielt, als es derzeit das Refactoring Tool Testing
abbilden kann, wenn typischerweise zum Abschluss von Programmiertätigkeiten durch den
Entwickler noch einmal viele Typen generalisiert werden würden. Diese Anwendungen wären dann nämlich schon getätigt und könnten nicht erneut durch den RTT reproduziert werden.
Weiterhin kann nicht zugesichert werden, dass die von den Refaktorisierungs-Adaptern
gelieferten, weiteren Parameter der Refaktorisierung dem entsprechen, was im Alltag der
Softwareentwicklung typischerweise der Fall ist. Bei M
C
U
kann noch davon ausgegangen werden, dass bei den geringen Mengen von Paketen in den Probanden und
durch die zufällige Wahl eines solchen auch hinreichend viele praxisnahe Fälle abgedeckt werden. Kritisch hingegen ist die Wahl eines neuen Namens beim R
. Für die Wahl dessen
gibt es nahezu beliebig viele Möglichkeiten – möchte man nur gezielt auf Namenskonflikte
hin testen, verbleiben immer noch sehr viele zu testende Fälle. Wegen der Wahl bisher ungenu ter Namen in oben beschriebenem Refaktorisierungs-Adapter kam es innerhalb der RTTTests höchstens beim Testen der übrigen Werkzeuge in Ausnahmefällen⁴⁹ zu Namenskonflikten. Eine mögliche Alternative wäre gewesen, den Adapter fürs R
sta dessen derart zu
implementieren, einen zufällig gewählten und bereits im Programm genu ten Namen zu verwenden. Tatsächlich wurde auch mit einem solche Adapter getestet, doch die Anzahl der zusä lich abgelehnten Refaktorisierungen war verschwindend. Mit derart wenigen Einzelfällen
kann nicht behauptet werden, die Behandlung von Namenskonflikten ausreichend getestet
zu haben. Eine mögliche Abhilfe wäre, innerhalb des Refaktorisierungs-Adapters eine tiefer
gehende Programmanalyse durchzuführen, um Namenskonflikte gezielter herbeizuführen.
Doch je ausgefeilter ein solcher Adapter wäre, nach um so spezielleren Mustern würde er in
den Probanden suchen müssen, bis schließlich die Frage gestellt müsste, wie hoch die Trefferquote dieser Spezialfälle in den Probanden überhaupt noch wäre. Die Alternative hierzu
sind klassische Regressionstests, die jeden derart überlegten Spezialfall testen, ohne dass dieser zunächst in einem Probanden gesucht und gefunden werden muss. Da sich insbesondere
beim R
im RTT-Testen wegen der Vielzahl verfügbarer Namen keine nennenswerte Abdeckung aller theoretisch möglichen Werkzeuganwendungen erreichen lässt, lag der Schwerpunkt der oben bereits beschriebenen Suite von Testfällen eben bei diesem Werkzeug.
⁴⁹ zum Beispiel dem P
U eines Feldes in eine Superklasse, die ein gleichnamiges Feld deklariert
. Evaluation
8.5. Übermäßig starke Einschränkungen
Die Beantwortung der zweiten Forschungsfrage gestaltet sich schwierig, erfordert sie doch eigentlich händischen Einblick in jedes nicht erfüllte Refaktorisierungsproblem, ob nicht doch
eine Möglichkeit bestanden hä e, die Refaktorisierung durchzuführen. Eine solche manuelle
Prüfung verbietet sich aber alleine schon wegen der schieren Menge durchgeführter Anwendungen.
Klassische Werkzeuge, wie sie Opdyke [Opd ] beschreibt, die in weitgehend unabhängigen Schri en zunächst Vorbedingungen prüfen und erst anschließend Änderungen berechnen, ließen sich hingehend Frage testen, indem auch bei nicht erfüllten Vorbedingungen
tro dem die Änderungen berechnet und in den Code geschrieben werden. Schlägt das Kompilieren für das so berechnete Programm fehl, ist dies ein Indiz für korrekt berechnete Vorbedingungen. Ist das Ausgabeprogramm kompilierbar, muss dennoch manuell inspiziert werden, ob sich das Programmverhalten geändert hat. Es ist aber zu erwarten, dass hier nur noch
ein Bruchteil aller Fälle geprüft werden muss, da man davon ausgehen kann, dass ein Großteil
der Problemfälle bereits durch den Compiler erkannt wird. Für constraintbasierte Werkzeuge
ist dieser Ansa hingegen nicht durchführbar, da die Prüfung von Vorbedingungen und das
Berechnen der Änderungen in Form der Constraintlösung unmi elbar miteinander verwoben sind. Soweit die Constraints nicht lösbar sind, gibt es auch keine Angabe über einen Sa
nötiger Änderungen.
Soares et al. [SMG ] schlagen vor, zu restriktive Werkzeuganwendungen zu erkennen, indem unterschiedliche Werkzeuge mit identischen Parameterbelegungen auf demselben Programm angewendet und die Ergebnisse verglichen werden. Dazu ist allerdings ein weiteres Werkzeug mit ähnlichem Umfang nötig. Mit den Refaktorisierungswerkzeugen der JDT
[JDT ] stehen zwar Werkzeuge für die gleiche Pla form zur Verfügung, allerdings unterscheiden sich die Ein- und Ausgabeparameter. So bietet M
C
U
in Eclipse
ebenso wie C
D
T
kein Anpassen von Zugrei arkeiten und P
U M keine Option, zusä lich nötige Verschiebungen in Folge direkt mi uberechnen.⁵⁰ Bei der
Ausgabe hingegen unterscheidet Eclipse nicht nur zwischen gültiger und ungültiger Transformation, sondern nu t zusä lich auch noch die Option, Warnungen anzuzeigen. Diese Warnungen weisen auf nur möglicherweise geändertes Programmverhalten hin und erschweren
so einen automatischen Vergleich der Werkzeuge.
Um unnötig restriktiven Werkzeuganwendungen auf die Spur zu kommen, kann man sich
bei constraintbasierten Werkzeugen aber eines anderen Testverfahrens bedienen. So resultieren bei constraintbasierten Werkzeugen übermäßig starke Einschränkungen aus zu restriktiven Constraintregeln. Kreis [Kre ] liefert hierzu ein Werkzeug, um solche Regeln aufzuspüren: Ansta wie in Abschni . beschrieben, den minimalen Sa von Constraints für ein
gegebenes Transformationsproblem zu berechnen, wurde einmalig für jedes in Tabelle . genannte Projekt pauschal jede Regel auf jede mögliche Anwendungsstelle angewendet. Erse t
man anschließend in jedem Constraint die Variablen durch ihre initialen Werte, müsste ein jedes solchermaßen erzeugtes Constraint gelöst sein. Anderenfalls wäre die zugrundeliegende
Constraintregel zu restriktiv, denn das Ausgangsprogramm ist wohlgeformt (also kompilierbar) und trivialer Weise in seinem Programmverhalten unverändert.
⁵⁰ Sta dessen gibt es hier ein vorgelagertes Werkzeug, das eine eigene Analyse durchführt.
. . Übermäßig starke Einschränkungen
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
generiere Faktenbasis (P, V, init, F )
setze alle Programmelemente p ∈ P auf read−only
für alle Variablen v ∈ V
für alle Constraintregeln r ∈ R
erzeuge alle Constraints C aus r, die v bedingen können
für alle c ∈ C
probiere c konstant zu lösen
wenn c = FALSE
Fehlermeldung
wenn c nicht konstant lösbar
Fehlermeldung
Abbildung . .: Algorithmus zum systematischen Generieren aller Constraints.
Tabelle . .: Anzahlen erfüllter und nicht erfüllter Constraints bei vollständiger Generierung
gemäß dem Algorithmus aus Abbildung . .
Projekt
Joda Convert .
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
.
.
.
.
.
.
.
.
.
.
.
total
.
.
.
.
.
.
.
.
nicht erfüllt
.
.
.
nicht erfüllt in %
,
,
,
,
,
,
,
,
Um nicht nur die Constraintregeln zu testen, sondern insbesondere auch die Komponenten
des Refacola-Frameworks, welche aus diesen Regeln die Constraints generieren, wurde der
in Abbildung . gezeigte Algorithmus verwendet. Der Vorteil dieses Verfahrens ist, dass die
Schri e aus den Zeilen
und
beide dieselben sind, die auch während der Erzeugung
eines minimierten Constraintsystems mi els Breitensuche auf dem Constraintgraphen und
konstanter Auswertung durch die Refacola durchgeführt werden (siehe dazu Abschni . ).
Als Nachteil kann empfunden werden, dass eine große Menge Constraints mehrfach erzeugt
wird, nämlich für jede eingehende Kante im Constraintgraphen einmal. Umgekehrt wird damit erreicht, dass jede theoretisch denkbare Möglichkeit, das entsprechende Constraint zu
generieren, mindestens einmal getestet wird. Tabelle . zeigt die Ergebnisse bei Ausführung
des Algorithmus aus Abbildung . auf den Probanden. Die Spalte total nennt die Gesamtanzahl der generierten Constraints. Die Spalten nicht erfüllt und nicht erfüllt in % geben zu jedem
Projekt die Anzahl der nicht erfüllten Constraints an.
Obwohl der Fehleranteil mit ca. ,
% (org.htmlparser . ) bis ca. ,
% (Joda Convert . )
verschwindend gering ist, wurden auch hier stichprobenhaft und von Hand die Fehlerquellen
untersucht. Als Ergebnis konnten mehrere Implementierungsfehler innerhalb der Faktengenerierung (siehe Abschni . ) gefunden werden. So wurde offenbar, dass für Member der
Wrapper-Klassen von Primitivtypen (Integer, Boolean, ...) als initialer Werte für owner und
tlowner gleiche, aber nicht identische Programmelemente verwendet wurden. In der Folge
. Evaluation
schlagen mehrfach für deren Member Constraints nach der Regel MemberOrConstructorTlOwnerAndHostPackage (siehe S. , ebenso S.
) fehl. Zum Beispiel ist dann nämlich das
Constraint
1258
java.lang.Integer.intValue().owner.tlowner = java.lang.Integer.intValue().tlowner
nicht mehr erfüllt. Auf tatsächlich ausgeführte Refaktorisierungen wird sich dieser Fehler allerdings nicht auswirken können. Da die Wrapperklassen der Primitivtypen Bibliothekscode
sind und daher alle ihre Variablen schon bei der Faktengenerierung als unveränderlich markiert werden, wird das oben genannte Constraint so regulär nach den in Abschni . beschriebenen Optimierungen niemals generiert werden, schließlich sind sowohl seine linke als
auch seine rechte Seite unveränderlich.
Auch bei weiteren anhand der Tests gefundenen Fehlern, die auf Implementierungsfehler
bei der Faktengenerierung hindeuten, konnte davon ausgegangen werden, dass diese maximal in sehr speziellen Einzelfällen Einfluss auf das Ergebnis der Refaktorisierung haben.
Gemeinsam mit der im Vergleich zu den Zahlen der insgesamt generierten Constraints sehr
geringen Anzahl der fehlschlagenden Constraints darf davon ausgegangen werden, dass diese die erstellten Refaktorisierungswerkzeuge nicht übermäßig stark einschränken.
Neben zu restriktiven Constraintregeln gibt es eine weitere mögliche Quelle übermäßig starker Einschränkungen. Eine davon bildet das Fehlen geeigneter Variablen, um nötige Folgeänderungen zu erlauben. Auch ist denkbar, dass geeignete Variablen zwar vorhanden, aber nicht
in den erlaubten Änderungen entsprechender Werkzeuge enthalten sind. Für ein R
wird derzeit zum Beispiel die Umbenennung eines Feldes abgelehnt, wenn ein weiteres Feld
mit dem gewünschten Namen bereits in derselben Klasse vorhanden ist. Die Alternative wäre, beide Felder umzubenennen. Da die Namen weiterer Felder aber nicht in den erlaubten
Änderungen des implementierten R
-Werkzeugs enthalten sind, kommt es nicht zu einer solchen Constraintlösung. Es ist schwer zu beurteilen und entspricht le tlich auch dem
Geschmack des Anwenders, welche Änderungen erlaubt sein sollen, also ob die Menge der
jeweils erlaubten Änderungen für die angebotenen Werkzeuge ausreicht. Der folgende Abschni . wird näher auf die Auswirkungen erlaubter Änderungen eingehen. Nach den ermi elten Testergebnissen für das vollständige Generieren aller Constraints lässt sich aber zumindest vermuten, dass innerhalb der Constraintregeln keine unnötig scharfen Bedingungen
zu finden sind.
8.6. Auswirkungen erlaubter Änderungen
Ein Vorteil constraintbasierter Programmtransformationen ist, dass durch die deklarative Formulierung der einzuhaltenden Bedingungen während der Programmtransformation Folgeänderungen sehr einfach Berücksichtigung finden können. Dies spiegelt sich in den verschiedenen von den implementierten Werkzeugen angebotenen Modi wider, die neben der eigentlichen Intention noch unterschiedliche Arten von Folgeänderungen zulassen.
Tabelle . schlüsselt die in Tabelle . in der Spalte erfolgreich gezeigten Werkzeuganwendungen noch einmal nach den jeweils gewählten Modi der Werkzeuge auf. So entsprechen
zum Beispiel die sich aus Tabelle . ergebenden vier beziehungsweise fünf Anwendungen
von M
C
U
im NOC- beziehungsweise CA-Modus auf Joda Convert . genau den neun in Tabelle . genannten erfolgreichen M
C
U -Anwendungen
. . Auswirkungen erlaubter Änderungen
für diesen Probanden. Da für das R
-Werkzeug nur ein einziger Modus implementiert
wurde, ist dieser nicht noch einmal in Tabelle . gelistet. Für die C
A
Refaktorisierung steht wie in Abschni . . begründet ein CA-Modus nur für Methoden zur
Verfügung, weswegen sich die in Tabelle . genannten Zahlen für dieses Werkzeug auch nur
auf Zugrei arkeitsänderungen von Methoden beziehen.
Vergleicht man die Anzahlen erfolgreicher Refaktorisierungen, variieren die Erfolgsquoten je nach Werkzeug. Für M
C
U
macht sich beim CA-Modus, in welchem
zusä lich Zugrei arkeitsmodifizierer angepasst werden dürfen, eine deutliche Steigerung
von .
auf .
Anwendungsfälle bemerkbar, was einer Steigerung um % gegenüber
der Anzahl erfolgreicher Refaktorisierungen ohne weitere erlaubte Änderungen (NOC) entspricht. Noch extremere Steigerungsraten sind bei P
U M
zu verzeichnen. Durch die
Möglichkeit, zusä lich Zugrei arkeiten anzupassen (CA), stieg die Erfolgsquote von
Anwendungen auf
, was einem Anstieg um % entspricht. Die Möglichkeit, weitere Deklarationen zu verschieben (CL), sorgte für eine Steigerung auf
Anwendungen beziehungsweise einen Anstieg um circa %. Waren sowohl Änderungen von Zugrei arkeiten als auch das
Verschieben weiterer Deklarationen gesta et (CACL), ergab sich gar eine Steigerung auf
erfolgreiche Anwendungen, was einer Steigerung um circa % entspricht. Beim G
/S
D
T
gab es ähnlich signifikante Steigerungsraten, wenn man neben
der Ursprungsdeklaration auch weiteren Deklarationen erlaubte, ihren deklarierten Typen zu
ändern (CT). Hier konnte eine Steigerung von
auf
, also um circa % gemessen werden. Beinahe nu los gestaltete sich dagegen die Option, auch Typen in ihrer Zugrei arkeit
mit anpassen zu können – die wenigen Einzelfälle, in denen zusä liche Lösungen gefunden
werden konnten, liegen deutlich unter einem Prozentpunkt. Eine händische Untersuchung
diverser Anwendungsstellen in den Probanden offenbarte, dass ein Großteil der enthaltenen
Typen ohnehin öffentlich zugrei ar und die Möglichkeit einer zusä lichen Anpassung ihrer
Zugrei arkeiten somit unnötig sind. Messbar hingegen sind Erfolge bei der Durchführung
des C
M
A
. Hier steigerte sich die Erfolgsquote durch Zulassen zusä licher Zugrei arkeitsänderungen (CA) von .
auf .
Anwendungen, was immerhin
noch circa , % entspricht.
Die aus Tabelle . entnehmbaren Daten haben gezeigt, dass zusä lich erlaubte Änderungen die Anzahl möglicher Refaktorisierungen je nach Werkzeug in mäßigem bis deutlichem
Umfang steigern können. Aber auch hier muss die Frage gestellt werden, ob die durchgeführten Refaktorisierungen dem entsprechen, was in der Praxis an Werkzeuganwendungen
zu erwarten ist. So lässt es sich annehmen, dass mit einer ausreichenden Pale e erlaubter
Änderungen beinahe jede Refaktorisierung durchführbar ist,⁵¹ somit also mit steigender Anzahl erlaubter Änderungen stets die Erfolgsquote nach oben getrieben werden kann. Umgekehrt muss damit gerechnet werden, dass mit steigender Anzahl von Änderungen die Werkzeuganwendungen immer praxisferner werden. So gibt zum Beispiel Fowler in seinem Handbuch [Fow ] vor, wie sogenannte Big Refactorings [Fow , Chapter ] in einzelne Teilrefaktorisierungen zerlegt werden können, um das Verständnis für den Prozess zu bewahren. Es
lässt sich vermuten, dass tro vorhandener Werkzeugunterstü ung Programmierer nur solche Änderungen automatisiert durchführen lassen, die sie selbst noch auf einmal überblicken
⁵¹ selbst eine programmweit viel genu te Interface-Methode lässt sich auf Pake ugrei arkeit herabse en, indem das Interface zu einer abstrakten Klasse gewandelt wird und alle implementierenden und referenzierenden Klassen in dasselbe Paket verschoben werden...
. Evaluation
und nachvollziehen können und anderenfalls selbst eine Zerlegung in kleinere Teilrefaktorisierungen vornehmen.
Um fes ustellen, wie komplex die einzelnen Programmtransformationen ausgefallen sind,
wurde während der RTT-Durchläufe auf den Probanden die Anzahl der jeweiligen Quellcodeänderungen protokolliert. Abbildung . fasst sie graphisch in Form von Häufigkeitsverteilungen zusammen. Die fünf gezeigten Balkendiagramme zeigen die erfolgreichen Refaktorisierungen der fünf implementierten Werkzeuge jeweils in ihrem Modus mit der maximalen
Zahl erlaubter Änderungen. Für M
C
U
wird somit beispielsweise im Balkendiagramm oben links die entsprechende CA-Spalte aus Tabelle . noch einmal genauer
aufgeschlüsselt. Jede Säule eines Balkendiagramms repräsentiert eine Menge von Refaktorisierungen mit gleicher Anzahl von Änderungen einer bestimmten Variablenart. Oberhalb der
Säule ist die Menge der jeweiligen Refaktorisierungen angegeben, unterhalb die Anzahl der
Änderungen. Die Art der Änderungen ergibt sich aus der Balkenfarbe. Im Falle des M
C
U
gab es somit .
Anwendungsstellen, an denen keine Zugrei arkeiten
angepasst werden mussten (eine Information, die sich auch schon aus Tabelle . entnehmen
ließ). In Fällen wurde ein Zugrei arkeitsmodifizierer angepasst, in Fällen waren es derer zwei und so weiter. Die in den Diagrammen gezeigten Änderungen beinhalten sowohl
die Änderungen, die sich direkt aus der Intention der Werkzeuganwendung ergeben, als auch
Änderungen, die im Rahmen der Constraintlösung zusä lich ermi elt wurden. So ist es zu
erklären, dass im Fall des P
U M
sich stets der Ort mindestens einer Deklaration geändert hat. Gleiches gilt für Namen bei R
und Zugrei arkeiten bei C
A
. Um eine sinnvolle graphische Darstellung zu erlauben, skalieren die Balken nicht linear,
sondern logarithmisch zur Basis
mit der Anzahl durch sie repräsentierter Werkzeuganwendungen. Um dennoch leere von einelementigen Mengen unterscheiden zu können, ist bei
le teren tro fehlendem Balken stets immer noch eine „ “ mit angegeben.
Wie aus den Diagrammen in Abbildung . hervorgeht, bewegten sich für die meisten Werkzeuganwendungen die Anzahl jeweils durchgeführter Änderungen pro Variablenart im Bereich bis zu einem Du end, bei G
D
T
auch leicht darüber hinausgehend
mit einigen, wenigen Ausreißern. Insgesamt handelt es sich damit um plausibel wirkende Anwendungen von Refaktorisierungen, die sich auch noch bequem durch den Werkzeugbenutzer in einem entsprechenden Vorschaufenster nachvollziehen lassen. Lediglich beim R
kommt es zu einer wesentlich größeren Anzahl von Folgeänderungen. Zu beachten ist, dass
hier das entsprechende Diagramm nur einen Ausschni zeigt. Tatsächlich gibt es noch weitere einzelne Ausreißer, deren obere Grenzen bei einer Menge von
Änderungen erreicht
ist. Dies liegt in der Natur der Sache des R
, bei dem sich die Anzahl nötiger Folgeänderungen direkt aus der Anzahl der Referenzierungen ergibt. Den Spi enreiter mit
Anwendungen bildet hier eine Umbenennung der Klasse (oder ebenso des Konstruktors der
Klasse) CharacterReference innerhalb des org.htmlparser . -Projekts, welche zur Implementierung von Singleton-Objekten für diverse Sonderzeichen hunderte Male referenziert wurde.
Aber selbst in diesem Extremfall kann davon ausgegangen werden, dass durch die sich beim
R
relativ einleuchtend gestaltenden Änderungen in Form von Umbenennungen die Gesamtrefaktorisierung durch den Benu er nachvollziehbar bleibt und auch tatsächlich derart
ausgeführt werden würde, somit also als praxisrelevant zu deuten ist.
Die dri e gestellte Forschungsfrage lässt sich somit ebenso positiv beantworten. Zwar unterscheiden sich die Häufigkeiten, in denen weitere Folgeänderungen nötig waren. Für alle
. . Auswirkungen erlaubter Änderungen
MOVE COMPILATION UNIT (CA)
PULL UP MEMBER (CACL)
1100
Zugreifbarkeiten
Zugreifbarkeiten
Orte
516
420
141
65
34
26
29
24
12
6 6
8
6
4
2
2
7
3
2
67
5
1
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
0
1
SPECIALIZE / GENERALIZE DECLARED TYPE
(CTCA)
1162
678
Zugreifbarkeiten
2
4
4
5
6
7
8
9
10
1
11
12
Zugreifbarkeiten
8339
Typen
6
5
2
8
3
55
47
13
8
3
2
3
1
158 115
3
0
4
CHANGE METHOD ACCESSIBILITY (CA)
143 161
78
26
13
5
22
10
8
4
8
4
8
2 2 2
10 12 14 16 18 20 22 24 26 28 30 32
0
1
2
3
4
5
6
7
8
9
10
11
12
RENAME (CI)
1055
Namen
243
116
44
35
8
2
126
1
120
114
111
108
105
99
1
129
2
1
102
1
84
1
81
66
63
60
57
54
51
48
45
42
39
36
33
30
27
24
21
18
15
9
12
6
3
0
1
117
2 2
1
3
96
2
1
7
5
4
93
4
3
123
7
4
90
8
3
78
8
75
12
87
26
13
72
57
46
69
86
Abbildung . .: Häufigkeitsverteilungen durchgeführter Änderungen bei Anwendung der
fünf implementierten Refaktorisierungswerkzeuge. Jeder Balken repräsentiert eine Menge von Refaktorisierungen mit gleicher Anzahl Änderungen
einer Variablenart. Unterhalb der Balken ist jeweils die Anzahl dieser Änderungen angegeben. Oberhalb der Balken ist die Kardinalität der Mengen
angegeben. Für die R
-Refaktorisierung ist nur ein Ausschni gezeigt,
hier treten bis zu einer Menge von
Änderungen noch weitere einzelne
Ausreißer auf.
. Evaluation
Werkzeuge konnten aber Fälle gefunden werden, in denen zusä liche Änderungen die Menge
durchführbarer Refaktorisierungen vermehrt haben. Weiterhin lässt die Anzahl zusä licher
Folgeänderungen vermuten, dass die Refaktorisierungen dem entsprechen, was ein Benutzer tatsächlich mi els Werkzeugunterstü ung für ihn nachvollziehbar an Refaktorisierungen
durchführen würde.
8.7. Laufzeiten
Die vierte Forschungsfrage betrifft die zu erwartenden Laufzeiten einer Werkzeuganwendung, weswegen im Rahmen der RTT-Durchläufe ebenso auch die zugehörigen Laufzeiten
erfasst wurden. Alle erhobenen Zeitmessungen wurden auf einem Intel i
QM Quad-Core
Mobilprozessor mit , GHz Taktfrequenz durchgeführt. Der JVM standen maximal
GB
Speicher zur Verfügung.
Die gemessenen Zeiten sind insgesamt mit Vorsicht zu betrachten. Einerseits macht es einem
die Sprache Java mit ihrer Laufzeitumgebung durch nichtdeterministische Mechanismen wie
Garbage Collection [BCM ] oder weitere Optimierungen [GVG ] ohnehin schwer, zuverlässig reproduzierbare Laufzeitmessungen durchzuführen [GBE ], andererseits bringt auch
Eclipse mit seinen JDT [JDT ] eigene Caches und Optimierungen mit, die je nach übrigen
Umständen zu unterschiedlichen Laufzeiten führen. Insbesondere bei der Faktengenerierung
lässt sich beobachten, dass sich bei mehrfach wiederholter Ausführung die Laufzeiten beinahe
halbieren können. Es ist zu vermuten, dass insbesondere bei Anforderung abstrakter Syntaxbäume und den in ihnen gespeicherten Bindungen sowie bei Abfragen von Deklarationen
in jar-Dateien vom JDT teilweise Werte aus Caches zurückgeliefert werden. Ebenso machten
sich bei wiederholter Nu ung der Rückschreibekomponente – die ebenso auf die Berechnung
von Syntaxbäumen angewiesen ist – Geschwindigkeitssteigerungen bemerkbar. Dem entgegen stehen Einbußen durch die sogenannte local history, in welcher Eclipse erfolgte Quellcodeänderungen nachverfolgt und welche wiederum dazu neigt, mit steigender Zahl erfolgter Refaktorisierungen das System zu bremsen. Unglücklicherweise bietet Eclipse auch keine Möglichkeit, die local history abzuschalten. Offensichtlich wurde hier der Anwendungsfall abertausender, reihenweise hintereinander ausgeführter Refaktorisierungen ohne zwischenzeitliche
Beendigung der Pla form (bei welcher die local history gesäubert wird) nicht berücksichtigt.
Tro obiger Vorbehalte listet Tabelle . die erhobenen Daten auf. Die einzelnen Spalten
geben die durchschni lichen Laufzeiten der verschiedenen Refaktorisierungsanwendungen
aufgeschlüsselt nach den verwendeten Modi in Sekunden an. Da in der vorliegenden Implementierung die Faktengenerierung unabhängig vom jeweiligen Transformationsproblem geschieht und somit für jede Anwendung auf demselben Probanden einheitlich ist, bot es sich bei
den ohnehin lang laufenden Tests an, die Faktengenerierung nur einmalig auszuführen. Die
Zeiten zur Faktengenerierung sind entsprechend in Tabelle . nicht enthalten, werden aber
in einem folgenden Abschni noch einmal separat behandelt. Somit beschreiben die gemessenen Zeiten für jede Anwendung das Zeitintervall ab Bereitstehen der Faktenbasis bis zu dem
Moment, in dem die Rückschreibekomponente die Quellcodeänderungen in Form konkreter
String-Erse ungen in den Quellcode zurückgeschrieben oder der Constraintlöser vermeldet
hat, dass das Constraintsystem unlösbar war. Tabelle . zeigt jeweils die Durchschni szeiten
aller Werkzeuganwendungen pro Projekt und Werkzeug. Die le te Zeile zeigt die Durch-
. . Laufzeiten
schni szeiten über alle Refaktorisierungen eines Werkzeugs, wobei hierbei zu beachten ist,
dass die größeren Probanden mit ihren entsprechend höheren Anzahlen angestoßener Refaktorisierungen (siehe Tabelle . ) auch entsprechend stärker in die Mi elwertberechnung
eingehen. So konnte ein M
C
U
beispielsweise auf Joda Convert . nur
mal angestoßen werden, auf Jakarta Commons Collections . hingegen
mal. Entsprechend
stärker geht hier die lange durchschni liche Laufzeit von , Sekunden für le teres Projekt
gegenüber der kurzen Laufzeit von nur , Sekunden für ersteres Projekt in den Mi elwert
ein. Wie aus Tabelle . zu erkennen ist, fallen starke Schwankungen nicht nur zwischen den
einzelnen Werkzeugen, sondern insbesondere auch zwischen ihren unterschiedlichen Modi
auf. In ihren jeweils restriktivsten Modi – also bei R
CI und bei den übrigen Werkzeugen NOC – machen die meisten Werkzeuge eine gute Figur. Die Zeiten liegen jeweils unterhalb einer Sekunde, lediglich M
C
U
benötigt im Schni
Sekunden, was
noch annehmbar scheint, allerdings mit Blick auf die durchschni liche Laufzeit von knapp
Sekunden für das Jakarta Commons Collections . -Projekt als kaum zumutbar einzustufen
ist. Auch lassen die Zahlen nicht erwarten, dass für größere Projekte die Laufzeiten linear mit
der Projektgröße skalieren, da insbesondere bei den umfangreichen Projekten die Laufzeiten
sprunghaft ansteigen. Ähnlich sprunghafte Anstiege der Laufzeiten sind darüber hinaus noch
einmal zu vermelden, wenn man die einfachen Modi ohne weitere Änderungen mit denen
vergleicht, die zusä liche Änderungen zulassen.
Betreibt man Ursachenforschung für die langen Laufzeiten, fällt auf, dass die angegebenen
Durchschni szeiten ein verzerrtes Bild auf die tatsächliche Situation geben. Tatsächlich ist
ein Großteil der Refaktorisierungsanwendungen binnen kürzester Zeit abgeschlossen. Dem
gegenüber steht aber ein geringer Anteil von Refaktorisierungen mit aberwi ig langen Laufzeiten. Betrachtet man nur einmal das mit den längsten Laufzeiten aus Tabelle . hervorgehende P
U M
im CACL-Modus, so war dort im Höchstfall eine Laufzeit von über
einer Stunde zu messen. Dem gegenüber stehen viele Anwendungen mit sehr kurzen Laufzeiten. Abbildung . zeigt ein Histogramm der ersten
Sekunden aller Anwendungen dieses
Werkzeugs. Nach bereits , Sekunden waren über
% und nach , Sekunden über
%
aller Werkzeuganwendungen abgeschlossen. Bei den übrigen Modi sind ähnliche Effekte zu
beobachten. Tabelle . zeigt analog zu Tabelle . noch einmal die durchschni lichen Laufzeiten aller Werkzeuge, wobei allerdings die in den Laufzeiten am längsten dauernden % aller
Werkzeuganwendungen ignoriert wurden. Somit ergibt sich ein ganz anderes Bild, nämlich
eines, in dem sich die Laufzeiten im mi leren, einstelligen Sekundenbereich einpendeln, was
wiederum vertretbar erscheint.
Um zu ergründen, wie in den wenigen, kritisch lang andauernden Fällen die Laufzeiten zustande kommen, wurden diese Fälle näher untersucht. Als entscheidender Flaschenhals hat
sich dabei die Constraintgenerierung innerhalb des Refacola-Frameworks erwiesen. Bei stichprobenhaften Untersuchungen der mehrminütig laufenden Werkzeuganwendungen hat sich
gezeigt, dass beinahe die vollständige Laufzeit für die Belegung der Regelvariablen verwendet wird. In diesem Schri wird für alle Programmelemente geprüft, wie sich mit diesen die
im for all-Teil der Constraintregel definierten Regelvariablen belegen lassen, sodass die Prämisse im if-Teil der Regel erfüllt ist (Schri 06 im Algorithmus aus Figure in [SKP ]) und
gleichzeitig eine bestimmte Constraintvariable eines Programmelements bedingt wird.
Es leuchtet ein, dass sich je nach Menge und Art der Regelvariablen und Faktenabfragen
in der Prämisse unterschiedliche Laufzeiten ergeben. Bemerkenswert ist, dass der nahezu ge-
. Evaluation
Move C. Unit
NOC
CA
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
NOC
,
,
,
,
,
,
,
,
,
Pull Up Member
CA
CL
CACL
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
Rename
CI
,
,
,
,
,
,
,
,
,
Gen./Spec. Decl. Type
NOC CA
CT
CACT
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
Change Acc.
NOC
CA
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
Tabelle . .: Durchschni liche Laufzeiten der Refaktorisierungsanwendungen aus Tabelle . in Sekunden. Gemessen wurde für jede
Refaktorisierungsanwendung jeweils die Zeit ab Bereitstehen der Faktenbasis bis zur Durchführung der Quellcodeänderungen beziehungsweise der Fehlermeldung, dass das Constraintsystem nicht lösbar ist.
Projekt
Joda Convert .
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
Durchschni
Move C. Unit
NOC
CA
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
NOC
,
,
,
,
,
,
,
,
,
Pull Up Member
CA
CL
CACL
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
Rename
CI
,
,
,
,
,
,
,
,
,
Gen./Spec. Decl. Type
NOC CA
CT
CACT
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
Change Acc.
NOC
CA
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
Tabelle . .: Durchschni liche Laufzeiten der Refaktorisierungsanwendungen aus Tabelle . in Sekunden analog zu Tabelle . , wobei
die pro Werkzeug und Modus jeweils langsamsten % aller Anwendungen ignoriert wurden.
Projekt
Joda Convert .
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
total
. . Laufzeiten
PULL UP MEMBER (CACT)
JAKARTA COMMONS COLLECTIONS
Häufigkeit im Laufzeitintervall
Häufigkeit kumuliert in %
100%
250
90%
80%
200
Häufigkeit
70%
150
60%
50%
100
40%
30%
50
20%
10%
0
0%
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Laufzeit in s
Abbildung . .: Histogramm der Laufzeiten aller Anwendungen von P
U M
im
CACL-Modus auf Jakarta Commons Collections . . Die grünen Balken repräsentieren Mengen von Refaktorisierungsanwendungen, deren Laufzeiten im
gleichen Sekundenintervall liegen. Die viole e Kurve zeigt die kumulierten
Häufigkeiten als Prozentwerte. Während das Histogramm als Balken nur die
Anwendungen zeigt, die innerhalb von
Sekunden terminierten, beziehen
sich die Prozentwerte auf alle Anwendungen.
samte Berechnungsaufwand auf nur sehr wenige Regeln zurückgeht, nämlich solche, die mehrere ungebundene – also in der Prämisse nicht mit Programmabfragen bedingten – Regelvariablen haben. Solche ungebundenen Variablen lassen sich häufig nicht vermeiden. So ist es ja
gerade der Zweck vieler Regeln, eine große Menge von Variablen anderer Programmelemente
zu bedingen. Soll zum Beispiel eine für Methodenüberschreibung relevante Eigenschaft einer
, ebenso S.
) ja
Methode geändert werden, soll die Regel AccidentalOverriding (siehe S.
gerade für jede weitere Methode im Programm prüfen, ob diese nun nicht versehentlich eine
neue Überschreibungsbeziehung au aut. Zwar kann man hierbei geschickt weitere Annahmen über die Menge der zu prüfenden Methoden machen, das Refacola-Framework macht
hiervon aber (bisher) keinen Gebrauch. So könnte bei der Belegung der Regelvariablen schon
Wissen über die erlaubten Änderungen oder anderweitig unveränderlich gemachten Constraintvariablen genu t werden. Im Falle oben genannter Regel AccidentalOverriding kann
beispielsweise im Falle eines R
M
darauf verzichtet werden, alle weiteren Methoden des Programms zu betrachten. Es genügen jeweils die in den Sub- und Superklassen.
Diese Information ergibt sich allerdings erst aus der Konklusion (dem then-Teil), welche aber
bei Belegung der Regelvariablen nicht berücksichtigt wird, sodass zunächst unabhängig von
dieser alle möglichen Belegungen der Regelvariablen gesucht und in den Speicher geschrieben werden. Weitere Optimierungen des Refacola-Frameworks dahingehend wären für die
Zukunft also erstrebenswert.
Weiterhin könnte man aber auch mit geeigneten Erweiterungen der Refacola-Syntax einen
Teil der all-Queries vermeiden. Hilfreich wäre hierbei ein neuer Operator, der es in Constraint-
. Evaluation
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
MemberTypeAccess
for all
tr: Java.TypeReferenceInType
referencingType: Java.TlOwnedType
do
if
all(tr), all(referencingType)
then
assume tr.typeBinding instanceof Java.MemberType
and (referencingType = tr.owner)
and (tr.hostPackage != type.hostPackage)
and Java.sub*!(referencingType.enclosingTypes, type.owner)
−> type.accessibility = #public
end
Abbildung . .: Alternative Implementierung der Regel MemberTypeAccess, bei welcher mittels einer hypothetischen Refacola-Erweiterung um ein assume-instanceofKonstrukt die Regelvariable type eingespart werden konnte.
regeln gesta et, einen Variablenwert auf seinen Typ zu befragen. So hat zum Beispiel die Regel MemberTypeAccess (siehe S. , ebenso S.
) eine Regelvariable type, die theoretisch
durch eine Indirektion tr.typeBinding erse t werden könnte, was einen Verzicht der zusä lichen all-Query sowie der Bedingung (tr.typeBinding = type) erlaubte. Der Grund, warum die
zusä liche Variable type existiert, ist, dass die Indirektion tr.typeBinding zum Typ Type ausgewertet wird, auf welchem die in der Constraintregel benötigten Variablenarten wie owner
nicht verfügbar war. Umgekehrt kann tr.typeBinding auch nicht so typisiert werden, dass hierauf ein owner zur Verfügung steht, denn im Falle einer Referenz zu einer lokalen Klasse gäbe
es keinen unmi elbar umschließenden Typen (sondern nur einen Block) und die Regel wäre
nicht anzuwenden. Die Lösung wäre eine Syntax, die in der Konklusion einer Regel eine Typprüfung erlaubt und das jeweilige Constraint erfüllt sein lässt, wenn ein durch Indirektion
beschriebenes Programmelement nicht vom entsprechenden Typen ist. Abbildung . zeigt
eine mögliche Umse ung mi els neuer Schlüsselworte assume und instanceof.
Unabhängig davon lässt sich vermuten, dass für die Faktenabfragen und Berechnung von
Regelvariablenbelegungen im Refacola-Framework noch weitere Optimierungen verfügbar
sind, die über das Vorhalten der Fakten in einfachen, assoziativen Datenfeldern (zum Beispiel java.util.HashMap) hinausgehen. So wäre zu überlegen, ob intern für die Faktenabfragen leistungsfähigere Komponenten zum Einsa kommen, ein erster Schri wäre ein Einsa
des JTransformer-Frameworks [SRK ], welches eine Darstellung abstrakter Syntaxbäume als
Prolog-Fakten ermöglicht und Abfragen aus Java-Code zum Beispiel über das leistungsfähige
SWI-Prolog [WSTL ] gesta et. Dies sind aber le tendlich alles Optimierungen des RefacolaFrameworks. Der in dieser Arbeit entwickelte Sa an Constraintregeln ist von diesen Fragen
unabhängig.
Laufzeiten der Faktengenerierung Die RTT-Durchläufe haben in den Laufzeiten nicht den
Zeitanteil berücksichtigt, der für die Faktengenerierung benötigt wird. In diesem Prozess sind
. . Evaluation der Constraintregeln für reflektive Zugriffe
wie in Abschni . beschrieben aus den Quellcodedateien abstrakte Syntaxbäume zu generieren, Bindungen zu berechnen und in mehreren Durchläufen der Bäume die Programmelemente und Fakten zu erzeugen. Abbildung . listet tabellarisch die Zeiten auf, die zum
Einlesen der verwendeten Testprojekte benötigt wurden. Rechnet man diese Zeiten direkt zu
den Laufzeiten der einzelnen Refaktorisierungsanwendungen hinzu, erscheinen sie lang, insbesondere bei den beiden größten Projekten mit über Sekunden Laufzeit.
Tabelle . .: Laufzeiten der Faktengenerierung
Projekt
Joda Convert
Jester . b
org.apache.commons.codec .
JUnit . .
org.apache.commons.io .
Junit .
org.htmlparser .
Jakarta Commons Collections .
P.-Elem
.
.
.
.
.
.
.
.
Zeit (s)
,
,
,
,
,
,
,
,
P.-Elem pro Sekunde
,
,
,
,
,
,
,
,
Umgekehrt ergibt sich aber gerade bei der Faktengenerierung noch großes Optimierungspotential. Auch wenn das Refacola-Framework Derartiges noch nicht unterstü t, ist es denkbar, die Fakten nicht vollständig vorab zu generieren, sondern nur im Rahmen einer konkret
anstehenden Query gezielt die entsprechenden Syntaxbäume zu durchlaufen, wie es Müller [Mül ] schon erfolgreich und mit erfreulichen Laufzeiten gezeigt hat. Entscheidend hierbei ist auch, was für Sichten auf den Code die jeweils genu te Bibliothek zum Au au der
Syntaxbäume bereitstellt. So bietet zum Beispiel der JastAddJ-Compiler [EH ], auf welchem
Schäfer [Sch ] seine Werkzeugsammlung au aute (im Gegensa zu den JDT [JDT ]) Rückreferenzen für Bindungen an, sodass hier in konstanter Zeit zu einer Deklaration die Menge
der bei vorherigen Compilerdurchläufen gefundenen Referenzen auf diese abgefragt werden
können, ohne jede potentiell referenzierende Klasse eigens durchsuchen zu müssen.
Möchte man an einer Generierung aller Fakten für ein Projekt festhalten, bietet sich eine
inkrementelle Faktengenerierung an. Fanden zwischen zwei Werkzeuganwendungen nur innerhalb einer Klasse Änderungen sta , müssen auch nur für diese Klasse neue Fakten generiert werden. Allerdings erkauft man sich hier die bessere Laufzeit mit höherem Speicherverbrauch. Die vollständige Faktenbasis für das größte der betrachteten Projekte Jakarta Commons
Collections . belegt ca.
MB, was ungefähr dem entspricht, was Eclipse in Standardeinstellung maximal an Speicherbelegung zulässt. Umgekehrt werden Arbeitspla rechner heutzutage mit mehreren Gigabyte Hauptspeicher bestückt, sodass hier kaum Probleme zu erwarten sind. Es ist somit davon auszugehen, dass die in der prototypischen Faktengenerierung
auftretenden Laufzeiten in einem für den Praxiseinsa entwickelten Tool kaum negativ in
Erscheinung treten.
8.8. Evaluation der Constraintregeln für reflektive Zugriffe
Die Evaluation der Constraintregeln zur Behandlung reflektiver Zugriffe, die in Kapitel beschrieben wurden, erfolgte bereits im Rahmen einer vorangegangenen Publikation des Au-
. Evaluation
tors [TB ]. Der Vollständigkeit halber und insbesondere zur Beantwortung von Forschungsfrage werden die dort erhobenen Daten an dieser Stelle erneut vorgestellt.
Die für die Evaluation der Reflection-Regeln genu ten Refaktorisierungswerkzeuge weisen
Abweichungen zu den in Kapitel beschriebenen auf. So wurden zur Constraintgenerierung
allein die in Kapitel beschriebenen Reflection-Regeln herangezogen. Für die Berechnung
weiterer Vorbedingungen und Programmtransformationen wurden hingegen die Implementierungen von Refaktorisierungswerkzeugen der JDT [JDT ] genu t. Beim Ausführen einer
Refaktorisierung wurde demnach zunächst das JDT-Werkzeug angestoßen. Erst wenn dieses
die Refaktorisierung als durchführbar betrachtet und konkrete Codetransformationen berechnet hat, schaltet sich auf Basis dieser Codetransformationen zusä lich der constraintbasierte
Teil hinzu, in welchem dann lediglich die Bedingungen geprüft werden, die sich aus Zugriffen auf die Reflection-API ergeben. Nach diesem Prinzip wurden insgesamt sechs Werkzeuge
der JDT erweitert: vier Varianten der in den JDT vorhandenen R
-Werkzeuge (für Pakete, Typen, Felder sowie Methoden und Konstruktoren), das M
C
U - sowie
das C
M
/C
A
-Werkzeug. Insgesamt ist die Wahl bezüglich
weiterer erlaubter Änderungen (den Modi) bei den Werkzeugen der JDT eingeschränkter als
bei den in Kapitel beschriebenen Werkzeugen aus vorliegender Implementierung. Das M
C
U
der JDT bietet nur die Wahl, zusä lich Referenzen anzupassen, was dem
NOC-Modus des in dieser Arbeit entwickelten Werkzeugs entspricht. Das C
M
A
passt von sich aus auch Zugrei arkeiten weiterer Methoden an, was dem CAModus des in dieser Arbeit vorgestellten Werkzeugs entspricht. Die R
-Werkzeuge der
JDT bieten ebenso wie die R
-Werkzeuge aus dieser Arbeit nur einen CI-Modus.
RTT-Tests Analog zu den in Abschni . vorgestellten Tests der übrigen Constraintregeln
fußt auch die Evaluation der Reflection-Regeln im Wesentlichen auf RTT-Tests. Insgesamt kamen drei Testprojekte zur Anwendung. Neben dem Jakarta Commons Collections . -Projekt,
welches auch schon bei den oben genannten RTT-Tests zum Einsa kam, kamen mit Joda Convert . und The Play Framework . . zwei weitere Projekte hinzu, für die im Vorhinein bekannt
war, dass sie Gebrauch von der Reflection-API machen. Tabelle . listet ganz analog zu Tabelle . wesentliche Daten zu den drei Projekten auf, wobei sich für das Jakarta Commons Collections . -Projekt Abweichungen gegenüber Tabelle . daraus ergeben, dass nun zusä lich
auch zur Verfügung stehende JUnit-Testfälle der Projekte mit betrachtet (und refaktorisiert)
wurden. Als Ergänzung findet sich in Tabelle . eine zusä liche Spalte Refl. Aufrufe, welche
die Anzahl der in der Laufzeitanalyse gefundenen Verwendungen der Reflection-API nennt.
Es ist zu beachten, dass die Anzahl dieser Zugriffe nicht unbedingt mit der Anzahl der Ausdrücke korrelieren muss. Unabhängig davon, dass die Reflection-API in den einzelnen Projekten unterschiedlich starke Nu ung erfahren kann, können einerseits solche Zugriffe auch aus
(bei der Anzahl der Anweisungen nicht berücksichtigtem) Bibliothekscode stammen, andererseits kann auch durch Schleifen ein einzelner Ausdruck zur Laufzeit zu mehreren reflektiven
Aufrufen führen.
Tabelle . zeigt die Ergebnisse der RTT-Tests. Die Spalten total geben dabei für jedes der
drei Projekte an, wie häufig eine Refaktorisierung auf dem jeweiligen Projekt insgesamt angestoßen werden konnte. Zur Auswahl der Anwendungsstellen für die Refaktorisierungswerkzeuge kamen in den entsprechenden Refaktorisierungs-Adaptern ähnliche Algorithmen zur
. . Evaluation der Constraintregeln für reflektive Zugriffe
Anwendung, wie sie auch in Abschni . für die bereits oben beschriebenen RTT-Tests gewählt wurden.
Im Falle der R
-Refaktorisierungen wurde für jede Deklaration jeweils ein frischer Name gewählt. Beim C
A
wurde jede Methode und jeder Konstruktor jeweils
mit private-Zugrei arkeit versehen, soweit er nicht schon vorher als private deklariert war. In
diesem Fall wurde seine Zugrei arkeit auf public angehoben. Für M
C
U
wurde jeweils ein beliebiges existentes Paket zum Ziel gewählt. Wie man an Tabelle . erkennt, ergeben sich zumeist weniger Anwendungsstellen für Refaktorisierungen, als es entsprechende Deklarationen gibt. Ursächlich hierfür ist, dass Deklarationen aus Bibliotheken
von vornherein vom Refaktorisieren ausgeschlossen wurden.
Beantwortung von Forschungsfrage 5 Forschungsfrage zielt darauf ab zu ergründen,
ob die Laufzeitanalyse und die zusä lichen Reflection-Constraints einen Mehrwert ergeben.
Um dem auf den Grund zu gehen, wurde ein vergleichender Versuchsau au gewählt, in welchem zunächst die Generierung der Reflection-Constraints unterdrückt wurde, also allein die
Werkzeuge der JDT zum Einsa kamen. In Tabelle . listen die als ohne bezeichneten Spalten für diesen Durchgang zu jedem Refaktorisierungswerkzeug und Projekt auf, in wie vielen
Fällen eine Refaktorisierung während der Überprüfung der Vorbedingungen durch die JDT
abgelehnt wurde. So wurden beispielsweise in der Summe für alle Projekte von
R
F
-Refaktorisierungen
abgelehnt. Beim Umbenennen von Methoden hingegen lag die
Ablehnungsquote mit
aus
deutlich höher, was im Wesentlichen darin begründet
ist, dass bei Methoden häufig der Fall eintri , dass diese Methoden aus Bibliotheken überschreiben, was ein Umbenennen verbietet. Ein Umbenennen der Pakte konnte in jedem Fall
durchgeführt werden. Hier sind in der Praxis auch keine Bedingungen zu erwarten, die dies
verhindern sollten (vom sehr exotischen Verdunkeln ( § . . ) einmal abgesehen).
In einem zweiten Durchlauf wurde jede der vorher ausgeführten Refaktorisierungen erneut
angestoßen. Nun wurde allerdings die zusä liche Generierung der Reflection-Constraints anhand einer vorher sta gefundenen Laufzeitanalyse zugeschaltet. Die als mit überschriebenen
Spalten in Tabelle . listen die Anzahl der in diesem zweiten Durchgang abgelehnten Refaktorisierungen auf. Da sich durch die zusä lich zur Anwendung kommenden Constraintregeln
die geprüften Bedingungen nur verschärfen konnten, sank in keinem der Fälle die Anzahl
abgelehnter Refaktorisierungen. Bemerkenswert hingegen ist, dass in der Summe für jedes
der erweiterten Refaktorisierungswerkzeuge die Anzahl der abgelehnten Refaktorisierungen
stieg. Von den insgesamt .
Refaktorisierungen wurden nun .
Refaktorisierungen
zusä lich abgelehnt. Das bedeutet, dass in jedem dieser Anwendungsfälle für die den Projekten beiliegenden Testsuiten mindestens ein Zugriff auf die Reflection-API sta gefunden
hat, welcher nun einen veränderten Wert zurückgibt. Ob sich so ein geänderter Wert und somit geändertes Programmverhalten tatsächlich bis zum endgültigen Testergebnis propagiert,
bleibt dabei allerdings unbekannt. Eine Ausführung der JUnit-Tests nach jeder Refaktorisierung hat wegen der sich ergebenen langen Laufzeiten nicht sta gefunden. In Stichproben hingegen konnten Fälle gefunden werden, in denen neuerdings abgelehnte Refaktorisierungen
ein Fehlschlagen einzelner Testfälle tatsächlich verhinderten. Forschungsfrage kann damit
also positiv beantwortet werden. Für die betrachteten Probanden konnte tatsächlich ein signifikanter Anteil an Refaktorisierungen verhindert werden, der ansonsten zu abweichendem
. Evaluation
JDK
.
.
.
Pakete
Typen
.
Member
.
.
Ausdrücke
.
.
.
Tabelle . .: Die für die Evaluation der Reflection-Constraintregeln aus Kapitel
Projekt
The Play Framework . .
Joda Convert .
Jakarta Commons Collections .
zum Einsa
P.-Elem.
.
.
.
Refl. Aufrufe
.
.
.
gekommenen Testprojekte.
Fakten
.
.
.
Rename Meth./Const. (CI)
total ohne
mit
zurück
Ch. Meth./Const. Acc. (CA)
total ohne
mit zurück
total
Rename Type (CI)
ohne
mit zurück
total
Move Compilation Unit (NOC)
total ohne
mit
zurück
Rename Field (CI)
ohne mit zurück
Rename Package (CI)
total ohne mit zurück
Tabelle . .: Ergebnisse der RTT-Tests der Reflection-Constraintregeln. Die Spalte total gibt für jedes Refaktorisierungswerkzeug die
Menge aller angestoßener Refaktorisierungen an. Die folgenden drei Spalten zeigen die Anzahl erfolgreicher Refaktorisierungen, jeweils ohne Einsa der Reflection-Constraintregeln, mit Einsa der Reflection-Constraintregeln sowie mit
Einsa der Reflection-Constraintregeln und zusä lichem Zurückschreiben in reflektive Referenzen.
Projekt
The Play Framework . .
Joda Convert .
Jakarta Commons Collections .
total
Projekt
The Play Framework . .
Joda Convert .
Jakarta Commons Collections .
total
. . Evaluation der Constraintregeln für reflektive Zugriffe
Programmverhalten hä e führen können.
Gewinn durch zusätzliches Zurückschreiben in reflektive Ausdrücke Die zusä lichen Bedingungen, die sich aus Zugriffen auf die Reflection-API ergeben, schränken notwendigerweise die Anzahl der durchführbaren Refaktorisierungen ein. Abschni . hat einen Ausblick
geliefert, wie in Einzelfällen dennoch mi els Umschreiben von Aufrufen an die ReflectionAPI Refaktorisierungen durchgeführt werden können. Mit einem dri en Durchlauf der RTTTests sollte geprüft werden, ob mi els dieser Technik in der Praxis Vorteile errungen werden
können. Dazu wurden beide in Abschni . gezeigten Umschreibemuster implementiert. In
Tabelle . ist in den mit zurück überschriebenen Spalten jeweils die Anzahl an Refaktorisierungen gelistet, die auch mit den erweiterten Funktionen der Refaktorisierungswerkzeuge abgelehnt wurden. Für R
M
/C
und R
F
gab es insgesamt
Fälle (Differenz aus mit und zurück), in denen die zusä lichen Constraintregeln zur Berücksichtigung von Reflection die Refaktorisierung unterbunden hä en, aber diese dennoch dank
dem Zurückschreiben in reflektive Aufrufe bedeutungserhaltend durchgeführt werden konnte. Im Fall der übrigen Werkzeuge konnte keine Verbesserung erreicht werden. Eine Hauptursache, warum das Zurückschreiben in reflektive Aufrufe nur derart geringe Erfolgsquoten hatte, lag darin begründet, dass Aufrufe an die Reflection-API häufig aus Bibliotheken stammten,
für die sich eine Modifikation verbot. Es ist anzunehmen, dass sich auch für den praktischen
Einsa eine ähnliche Situation ergeben wird. Reflection bietet in Java die einzige Möglichkeit,
durch ein bereits kompiliertes Framework Code der Verwender (erstmalig) aufzurufen. Es ist
anzunehmen, dass ein Großteil der Anwendungen von Java-Reflection daher innerhalb von
Programmbibliotheken sta findet, die sich einer möglichen Änderung en iehen.
9. Weitere constraintbasierte Werkzeuge
Die in dieser Arbeit entwickelten Constraintregeln wurden aus der Motivation heraus zusammengetragen, Refaktorisierungswerkzeuge zu entwickeln. Ihr Anwendungsbereich ist aber
keineswegs auf Refaktorisierungen beschränkt. Durch die deklarative Formulierung der Regeln bieten sie eine abstrakte Beschreibung, unter welchen Transformationen ein gegebenes
Programm wohlgeformt (also kompilierbar) und bedeutungserhaltend bleibt, womit sich der
Nu erkreis der Regeln auf solche Werkzeuge erweitern lässt, die Programmtransformationen
durchführen und Eigenschaften zu Wohlgeformtheit und / oder Beibehaltung des Programmverhaltens zusichern sollen.
Dieses Kapitel zeigt im Rahmen eines Ausblicks zwei weitere Nu ungsformen der entwickelten Constraintregeln. Einerseits werden Quick Fixes beleuchtet und andererseits eine Anwendungsmöglichkeit beim Mutationstesten aufgezeigt.
9.1. Quick Fixes
Quick Fixes sind Werkzeuge, die für nicht kompilierende Programme Programmtransformationen vorschlagen, durch die Kompilierfehler behoben werden. Die Berechnung von Quick
Fixes kann dabei als Bedingungserfüllungsproblem formuliert werden [SU ]. Generiert man
für ein nicht kompilierbares Programm gemäß einem vollständigen und korrekten Regelsa
Constraints, so können aus den Kompilierfehlern fehlschlagende Constraints resultieren. So
würde zum Beispiel eine ungültigerweise als protected deklarierte Interface-Methode ein Constraint aus der Regel InterfaceMember ( S.
) verle en. Durch das Suchen nach Lösungen
für das Constraintsystem und Rückschreiben der geänderten Variablenwerte in den Code erhält man Programmtransformationen, die die Wohlgeformtheit des Programms wiederherstellen.
9.1.1. Ein prototypischer Quick Fix zum Anpassen von Zugreifbarkeiten
Im Rahmen dieser Arbeit wurde au auend auf den gegebenen Constraintregeln für Java ein
Werkzeug implementiert, welches als erster exemplarischer Prototyp bei Zugriffen auf nicht
zugrei are Deklarationen entsprechende Quick-Fixes anbietet. Intern arbeitet das Werkzeug
ähnlich zu einer C
A
-Refaktorisierung. Zunächst wird das nicht kompilierbare Programm eingelesen und in eine Faktenbasis überführt, in welcher anhand der Fehlermeldung des Compilers die nicht zugrei are Deklaration ermi elt wird. Anschließend
werden von dieser ausgehend gemäß den Constraintregeln Constraints generiert und eine
Constraintlösung gesucht, welche einem dann insbesondere kompilierenden Programm entsprechen muss. Die Angabe einer Intention ist an und für sich nicht nötig. Im Fall einer Refaktorisierung gibt sie die eigentlich durchzuführende Änderung an. Ohne sie könnte der Constraintlöser die initiale Belegung der Constraintvariablen (also das unveränderte Programm)
. Weitere constraintbasierte Werkzeuge
als gültige Lösung ausgeben, schließlich müssen für ein kompilierendes Programm initial alle
Constraints erfüllt sein, da es einerseits keine Kompilierfehler hat und andererseits per Definition im Programmverhalten unverändert ist. Für Quick-Fixes hingegen erübrigt sich die Angabe einer Intention, denn schon die den Kompilierfehler auslösende Deklaration muss mindestens ein initial nicht erfülltes Constraint bedingen, welches dann der Constraintlöser zu erfüllen versuchen wird. Allerdings verbietet die derzeitige Version des Refacola-Frameworks
(noch), keine Intention anzugeben. Der hier präsentierte Prototyp wählt als Intentionen daher
konkrete Änderungen der Zugrei arkeit der nicht zugrei aren Deklaration, wobei nacheinander alle für die Deklaration in Frage kommenden Zugrei arkeiten durchprobiert werden.
Sobald eine Constraintlösung gefunden wird, erhält der Benu er den entsprechenden Vorschlag zur Programmtransformation.
Ebenso wie die implementierten Refaktorisierungswerkzeuge wurde auch das Quick-FixWerkzeug in die Benu erschni stelle der JDT integriert, wobei derzeit sowohl die von Eclipse
als auch vom constraintbasierten Werkzeug gegebenen Vorschläge parallel angezeigt werden.
Abbildung . zeigt verschiedene Screenshots. Der jeweils obere Vorschlag kommt von den
JDT, der untere Vorschlag vom constraintbasierten Prototypen. Neben der Tatsache, dass le terer einige Fehler der Eclipse-Werkzeuge behebt, ist insbesondere der Fall aus Abbildung .
(iv) interessant. Für diesen kann keine Constraintlösung gefunden werden, ohne dass das
übrige Programm sein Programmverhalten ändert.
9.1.2. Probleme bei der Implementierung beliebiger Quick Fixes
Auch wenn das im vorhergehenden Abschni vorgestellte Werkzeug mit verblüffend wenig
Aufwand auf Basis der in dieser Arbeit entwickelten und implementierten Constraintregeln
erstaunlich ausgereift wirkt, lassen sich die Ergebnisse nicht ohne Weiteres auf andere Werkzeuge übertragen. Dieser Abschni nennt einige der sich ergebenden Probleme.
Zunächst ist der Ansa auf solche Kompilierfehler beschränkt, die sich bei der semantischen Prüfung des Compilers ergeben. Meldet der Compiler bereits Fehler beim Parsen des
Programms (indem es nicht der Grammatik der Sprache entspricht), wird schon die Abstraktion hin zu einer Faktenbasis scheitern. Für die Entwicklung der Refaktorisierungswerkzeuge
war es von Vorteil, durch die Abstraktion konkreter Syntaxknoten hin zu Belegungen von
Variablenwerten (siehe Abschni . . ) von vorneherein Syntaxprobleme ausblenden zu können. Umgekehrt schließt dies bei Quick Fixes die Behandlung von Syntaxfehlern aus.
Für das oben vorgestellte Quick-Fix-Werkzeug wurde angenommen, dass bekannt ist, an
welche (nicht zugrei are) Deklaration eine Referenz binden sollte. Das muss aber keineswegs immer der Fall sein. So ist es denkbar, dass der Aufruf einer Methode an möglicherweise zwei verschiedene überladene, nicht zugrei are Methoden mit gleicher Parameterzahl
der Superklasse binden könnte. In diesem Fall muss sich bei der Faktengenerierung vorausschauend für eine der beiden Deklarationen entschieden werden, denn für eine von beiden
muss ein binds-Fakt von der fehlschlagenden Referenz an die jeweilige Deklaration angelegt
werden. Anderenfalls käme es gar nicht erst zur Erzeugung des Constraints, welches die Zugrei arkeit einer der Deklarationen von der Aufrufstelle aus fordern würde. Im Fall der zwei
Methoden ist dies noch handhabbar, indem einfach beide Fälle nacheinander behandelt und
im Falle einer Constraintlösung dem Benu er präsentiert werden. Es sind aber auch Fälle
denkbar, in denen sich weit mehr Möglichkeiten ergeben. Kann eine Methodenreferenz nicht
. . Quick Fixes
(i)
(ii)
(iii)
(iv)
Abbildung . .: Vier Bildschirmfotos des implementierten Quick-Fix-Werkzeugs zum Anpassen von Zugrei arkeiten für Eclipse. Für jede gezeigte Codestelle werden
jeweils zwei Quick-Fixes angeboten. Der jeweils obere, mit einem grünen
Pfeil markierte Vorschlag wird durch die in Eclipse enthaltenen JDT [JDT ]
berechnet, der jeweils untere, mit dem Icon einer roten Refacola-Dose markierte Vorschlag entstammt dem in dieser Arbeit beschriebenen Quick-FixWerkzeug. In den Fällen (i) bis (iii) kommt das JDT-Werkzeug zu fehlerhaften Vorschlägen, deren Anwendung den Code weiterhin nicht kompilieren lässt, während das constraintbasierte Werkzeug korrekte Vorschläge
unterbreitet. Im Fall (iv) behebt das JDT-Werkzeug den vorhandenen Kompilierfehler, sorgt aber durch seine Änderung der Zugrei arkeit von m in
B gleichzeitig für geändertes Programmverhalten in der Zeile oberhalb des
Kompilierfehlers.
. Weitere constraintbasierte Werkzeuge
aufgelöst werden, weil keine Methode gleichen Namens auf dem Empfängerobjekt vorhanden
ist, kommen potentiell alle weiteren Methoden auf diesem in Frage. Da dies aber schnell einige hundert sein können, scheidet ein Constraintlösen für jeden dieser hundert Fälle einerseits
schon wegen der Laufzeit aus, andererseits aber auch, weil der Benu er typischerweise nur
eine begrenzte Zahl an Vorschlägen sinnvoll erfassen kann. Wünschenswert hingegen wäre
schon eine vorausschauende Vorabprüfung, ob es Methoden gibt, die sich im Namen nur geringfügig von dem bei der Methodenreferenz gewählten Bezeichner unterscheiden. Für diese
könnte dann im Einzelnen anhand von Constraints geprüft werden, ob sich durch die Umbindung der Referenz an diese ein gültiges Programm ergibt, in dem auch Zugrei arkeit,
Parametertypen und alle weiteren Bedingungen an einen Methodenaufruf erfüllt sind.
Das Problem, auf eine vorausschauende Vorabprüfung angewiesen zu sein, gilt nicht für alle
Arten von Quick Fixes. Bei den obigen Beispielen zu Zugrei arkeiten und ebenso zum unbekannten Namen innerhalb der Methodenreferenz war die vorausschauende Vorabprüfung
nötig, damit überhaupt Fakten generiert werden konnten, die die fehlschlagenden Constraints
erzeugen. In den Beispielen waren es jeweils binds-Fakten, die in die Faktenbasis einzutragen
waren, obwohl technisch gesehen wegen der zu geringen Zugrei arkeit oder dem falschen
Namen gar keine Bindung vorlag.
Anders sieht dies hingegen bei Eigenschaften aus, die sich allein aus Belegungen von Constraintvariablen ergeben. Dies können zum Beispiel die Bindungen von Typreferenzen sein,
die in der vorliegenden Arbeit anhand von Constraintvariablen typeBinding sta eines bindsFakts repräsentiert sind. Schlägt eine solche Typreferenz beispielsweise fehl, weil kein Typ
passenden Namens gefunden wird, kann der initiale Wert der Variablen typeBinding der Typreferenz zunächst mit einem null-Wert belegt werden. Über eine zusä liche Constraintregel
könnte dann gefordert werden, dass keine Variable mit einem solchen null-Wert belegt ist. Besonders elegant kommt dann zum Tragen, dass sich die in den oben gegebenen Beispielen erwähnte Notwendigkeit erübrigt, mehrere Constraintsysteme lösen zu müssen. Vielmehr stellt
jede Lösung des Constraintsystems nun einen gültigen Vorschlag dar, der dem Benu er unterbreitet werden kann. Für das Beispiel einer Typreferenz, zu der kein Typ dieses Namens
existiert, bestünden die Constraintlösungen vermutlich in Umbenennungen der Typreferenz
oder Umbenennen von Typen hin zum in der Typreferenz verwendeten Namen.
Allerdings bleibt dann als le tes Problem noch, dass zumindest das in der vorliegenden
Arbeit genu te Refacola-Framework zwingend die Angabe einer Intention vorausse t, da
auf ihr die Implementierung des Algorithmus zur Constraintgenerierung au aut. Dies ist
allerdings nur ein technisches Problem.
9.2. Constraintbasiertes Mutationstesten
Mutationstesten ist eine Methodik, um die Qualität von Testsuiten zu beurteilen. Zunächst erstmalig durch eine Studienarbeit Liptons Anfang der
er Jahre publiziert [Mat ] hat sich
aus der Anfangsidee ein ganzes Forschungsgebiet mit gegenwärtig über
Publikationen
entwickelt [JH ][Jia ]. Indem absichtlich kleine Änderungen (sogenannte Mutationen) in ein
zu testendes Programm eingestreut werden, können Rückschlüsse auf die Qualität der Testsuiten gezogen werden. Schlagen die Tests nach den Änderungen fehl, ist dies ein Indikator
für gute Testabdeckung.
. . Constraintbasiertes Mutationstesten
Das Hauptproblem beim Mutationstesten liegt in der Frage, wie geeignete Anwendungsstellen für Mutationen zu finden sind. Während es erstrebenswert ist, dass ein Verfahren zur
Mutantengenerierung in möglichst vielen Fällen Codeänderungen durchführt, die das Programmverhalten tatsächlich ändern, wird ein naives Verfahren, das nur auf syntaktischer Ebene Änderungen, aber keine semantisch tiefer gehende Programmanalyse durchführt, entweder das Programmverhalten häufig unverändert lassen oder nicht kompilierbaren Code erzeugen. In ersterem Fall, einem sogenannten redundanten Mutanten, wird kein Testfall je die Chance haben, den Mutanten zu entdecken (zu töten im Jargon des Mutationstestens), in le terem
Fall lassen sich die zu testenden Testsuiten nicht einmal ausführen. Eines der Hauptprobleme
beim Mutationstesten besteht somit darin, wie Codetransformationen gefunden werden können, die einerseits wohlgeformten Code erzeugen, und andererseits das Programmverhalten
mit hoher Wahrscheinlichkeit ändern.
Diesem Problem kann ebenso constraintbasiert begegnet werden [ST , Bär ]. So lassen
sich die für constraintbasierte Refaktorisierungswerkzeuge verwendeten Constraintregeln in
zwei Kategorien aufteilen. Zum einen gibt es solche Regeln, die die Wohlgeformtheit des Ausgabeprogramms sicherstellen. Ein Beispiel hierfür ist die Regel InterfaceMember ( S.
),
deren Verle ung stets zu einem Kompilierfehler führt. Zum anderen gibt es aber auch solche Regeln, deren Verle ung nicht durch einen Kompilierfehler offensichtlich wird. Ein Beispiel hierfür gibt die Regel AccidentalOverriding (siehe S.
, ebenso S.
). Eine Verle ung
dieser Regel bedeutet, dass eine zuvor nicht überschriebene Methode nach der Programmtransformation von einer anderen Methode überschrieben wird, was heimtückisch subtile
Änderungen im Programmverhalten aufgrund sich ändernder, dynamischer Methodenbindungen nach sich ziehen kann. In dieselbe Kategorie fällt die Regel AccidentalInstanceMethod, ebenso S.
), deren fehlschlagende Constraints
OverloadingInstanceMethod (siehe S.
geänderte statische Methodenbindungen bedeuten.
Mit einer solchen Unterteilung in Wohlgeformtheitsregeln und Programmverhaltensregeln
lassen sich constraintbasiert Mutanten generieren, indem für das Eingabeprogramm gemäß
aller Constraintregeln sämtliche Constraints generiert werden. In diesem, zunächst erfüllten,
Constraintsystem wird genau eines der Constraints, welches aus einer ProgrammverhaltensRegel entspringt, negiert. Anschließend wird für das nun ungelöste Constraintsystem eine
Lösung gesucht. Existiert eine solche Lösung, ist sie mit großer Wahrscheinlichkeit ein tauglicher Mutant. Einerseits erfüllt sie weiterhin alle Wohlgeformtheits-Constraints, ist somit kompilierbar. Andererseits existiert ein Programmverhaltens-Constraint, welches fehlgeschlagen
ist, und somit existiert eine Änderung statischer oder dynamischer Bindung im Programm,
welche – bei unterschiedlichem Verhalten der vorher und hinterher referenzierten Deklarationen – das Programmverhalten zumindest lokal (als sogenannter weak mutant [How ]) ändern
wird.
Die Arbeit [ST ] zeigt erfolgreich, dass constraintbasiert qualitativ hochwertige Mutanten
erzeugt werden können. Sie baut dabei auf einer früheren Version [ST ] der in der vorliegenden Arbeit noch einmal überarbeiteten Zugrei arkeitsconstraints auf. Während in der damaligen Version die Constraintregeln noch als Java-Code und noch nicht in Refacola vorlagen,
hat Grothoff [Gro ] mi lerweile gezeigt, wie über zusä liche Annotationen der Constraintregeln in Refacola Wohlgeformtheits-Regeln entsprechend annotiert werden können und über
ein von ihm entwickeltes Werkzeug constraintbasierte Mutationstests durchgeführt werden
können.
10. Zusammenfassung
Korrekte Refaktorisierungswerkzeuge zu entwickeln ist schwer. Die vielen Funktionen und
Ausdrucksmöglichkeiten moderner Programmiersprachen wie Java mögen es Programmierern heute einfacher machen, Code zu entwickeln. Gleichzeitig erschweren sie Werkzeugentwicklern die Implementierung von korrekten Refaktorisierungswerkzeugen. Die enge Verflechtung vieler Konzepte und Eigenschaften der Sprache Java zwingt sogar scheinbar einfache Refaktorisierungswerkzeuge dazu, komplexe Quellcodeanalysen durchzuführen. Schon
das unverdächtig anmutende Einfügen eines public-Modifizierers kann Einfluss auf Methodenbindungen im Programm nehmen und es somit in seinem Verhalten verändern. Lässt ein
Refaktorisierungswerkzeug so eine Codeänderung ohne entsprechende Prüfungen zu, sind
zeitraubende und somit kostenintensive Fehlersuchen seitens des Programmierers die Folge.
Erschwert wird die Situation dadurch, dass Refaktorisierungen typischerweise Folgeänderungen erfordern. So kann eine Verschieberefaktorisierung ebenso eine Änderung von Zugrei arkeiten verlangen, muss also zusä lich die entsprechenden Prüfungen hierfür durchführen. Gleichzeitig werden in heutigen Entwicklungsumgebungen Refaktorisierungswerkzeuge typischerweise weitgehend unabhängig voneinander implementiert. Die Folge ist, dass
sie das in ihnen implementierte Wissen über zu prüfende Bedingungen kaum teilen. Nicht
nur mehrfacher Entwicklungsaufwand ist die Folge, sondern auch, dass einzelne Werkzeuge
Fehler aufweisen, die in anderen Werkzeugen derselben Entwicklungsumgebung vermieden
wurden. Bisher gänzlich bei Refaktorisierungswerkzeugen für Java unberücksichtigt geblieben sind die Reflection-Funktionen der Sprache. Da bisherige Werkzeuge stets allein auf eine
statische Codeanalyse se ten, konnten Bedingungen aus reflektiven Aufrufen nur in Spezialfällen Berücksichtigung finden.
Die vorliegende Arbeit löst diese Probleme und stellt darüber hinaus noch einen Werkzeugkasten zur Verfügung, auf welchem auch andere Programmierwerkzeuge als nur solche für
Refaktorisierungen au auen können. Im Kern dieses Ansa es stehen die constraintbasierten
Refaktorisierungen, wie sie erstmalig von einer Arbeitsgruppe um Tip im Jahre
beschrieben wurden [TKB ]. Bei diesen werden die durch das Refaktorisierungswerkzeug möglichen Quelltex ransformationen als Variablen beschrieben. Die für eine bedeutungserhaltende Codetransformation einzuhaltenden Bedingungen werden als Constraintregeln formuliert.
Aus diesen generierte Bedingungen können gemeinsam mit den Variablen einem Constraintlöser übergeben werden. Kann dieser eine gültige Variablenbelegung finden, beinhaltet diese
in Form von Variablenbelegungen direkt die Information, welche Änderungen im Code für
eine alle Bedingungen erfüllende Refaktorisierung durchzuführen sind. Doch während diese ersten Werkzeuge von Tip et al. lediglich deklarierte Typen zu refaktorisieren vermochten
und damit ebenso wie bisherige Refaktorisierungswerkzeuge isoliert in der Eclipse-IDE zu
finden sind, ohne ihr Wissen mit anderen Werkzeugen teilen zu können, baut die vorliegende
Arbeit die enthaltenen Grundideen zu einem universell nu baren Framework für Refaktorisierungswerkzeuge aus.
. Zusammenfassung
Den Schwerpunkt legt die vorliegende Arbeit auf Refaktorisierung von Deklarationen in
Java. Um deren Grundeigenschaften – Namen, Deklarationsorte, deklarierte Typen und Zugrei arkeiten – bei Refaktorisierungen bedeutungserhaltend ändern zu können, wurde aus
der Java-Sprachspezifikation ein umfangreicher Sa von Constraintregeln abgeleitet. Dieser
Sa von Constraintregeln formuliert alle Bedingungen, die einzuhalten sind, um einzeln oder
auch beliebig kombiniert die oben genannten Eigenschaften von Deklarationen bedeutungserhaltend ändern zu können. Um zusä lich auch Bedingungen zu berücksichtigen, die sich
aus Verwendung der Reflection-API in Java ergeben, bietet das implementierte Framework
ebenso die Möglichkeit, vor der Refaktorisierung eine Laufzeitanalyse – zum Beispiel bei
Ausführung einer Testsuite – durchzuführen. Während dieser werden reflektive Aufrufe protokolliert, sodass aus diesem Protokoll weitere einzuhaltende Bedingungen abgeleitet werden können. Auf dieser Basis wurden fünf Refaktorisierungswerkzeuge implementiert: M
C
U ,P
U ,R
,G
/S
D
T
und C
A
.
Die Korrektheit von Refaktorisierungswerkzeugen zu beweisen ist schwierig. Bisherige Arbeiten anderer Autoren haben einzelne Eigenschaften ihrer Werkzeuge nachweisen können,
lassen aber die Frage offen, ob in diesen Beweisen auch der natürlichsprachliche Teil der JavaSprachspezifikation vollständig und korrekt erfasst wurde. Daher kam in vorliegender Arbeit
die etablierte Methode des Testens durch systematisches Anwenden der Refaktorisierungswerkzeuge auf quelloffenen Java-Projekten zum Einsa . Insgesamt wurden mehr als
.
Refaktorisierungen erfolgreich durchgeführt, was – wenn man von Refaktorisierungen am
Tag ausgeht – dem entspricht, was ein Programmierer in vierzig Jahren Berufstätigkeit an
Refaktorisierungen ausführt. Die dabei ermi elten Fehler – die Fehlerquote lag deutlich unterhalb eines Prozent – ließen sich auf Implementierungsfehler zurückführen, die unabhängig
von den entwickelten Constraintregeln sind. Gleichzeitig lagen die Laufzeiten für den Großteil der Anwendungsfälle im Bereich weniger Sekunden. Darüber hinaus zeigt die vorliegende
Arbeit noch Möglichkeiten zur weiteren Optimierung der Laufzeiten auf.
Da die entwickelten Constraintregeln für Java unabhängig von konkreten Refaktorisierungen formuliert sind, bietet sich ihre Nu ung auch über den Anwendungsbereich von Refaktorisierungswerkzeugen hinaus an. Im Rahmen eines Ausblicks wurden zwei mögliche Anwendungen aufgezeigt, einerseits das constraintbasierte Mutationstesten und andererseits die
Implementierung von Quick-Fix-Werkzeugen. In le terem Anwendungsfall konnte au auend auf den entwickelten Constraintregeln mit minimalem Aufwand ein Quick-Fix-Werkzeug
zum Anpassen von Zugrei arkeiten entwickelt werden, welches gegenüber seinem der Funktion nach identischen Pendant innerhalb der Entwicklungsumgebung Eclipse gleich eine Reihe von Fehlern behebt.
A. Java-Sprachdefinition
Dieser Anhang zeigt die Java-Sprachdefinition, wie sie in Kapitel beschrieben wird. In den
Kommentaren genannte Paragraphen beziehen sich auf solche der Java-Sprachspezifikation
[GJSB ], ihnen nachfolgende Texte sind aus dieser wörtlich übernommen und nicht als Werk
des Autors vorliegender Arbeit zu betrachten.
language Java
kinds
abstract Entity <: ENTITY
abstract NamedEntity <: Entity { identifier }
abstract PackagedEntity <: Entity { hostPackage }
abstract TlOwnedEntity <: PackagedEntity { tlowner }
abstract CompilationUnitMember <: Entity { typeRoot }
abstract AccessibleEntity <: Entity { accessibility }
abstract OwnedEntity <: Entity { owner }
abstract TypedEntity <: Entity
abstract RegularTypedEntity <: TypedEntity { declaredType }
abstract TypeVariableTypedEntity <: TypedEntity
/*
* Packages and Compilation Units
*/
Package <: NamedEntity
TypeRoot <: NamedEntity, PackagedEntity
PackageDeclaration <: NamedEntity
ImportDeclaration <: NamedEntity, PackagedReference { typeBinding }
/*
* Types
*
* § 4.1
* There are two kinds of types in the Java programming language: primitive types (§4.2) and
* reference types (§4.3).
*/
abstract Type <: Entity
abstract Interface <: ClassOrInterfaceType
abstract Class <: ClassOrInterfaceType
abstract Enumeration <: Class
abstract ArrayableType <: Type
abstract NamedType <: ArrayableType, NamedEntity
abstract AccessModifiableType <: AccessibleEntity, Type
/*
* The Null Type
A. Java-Sprachdefinition
*
* § 4.1
* There is also a special null type, the type of the expression
* null, which has no name. Because the null type has no name, it is
* impossible to declare a variable of the null type or to cast to
* the null type. The null reference is the only possible value of
* an expression of null type. The null reference can always be
* cast to any reference type.
*/
NullType <: Type
/*
* Reference Types
*
* § 4.3
* There are three kinds of reference types: class types (§8), interface types (§9), and array
* types (§10)
*
* N.B.
* ReferenceType inherits the property accessibility from AccessModifiableType. This is
* questionable, since anonymous and local types are reference types, but must not have an
* access modifier. Basically, this decision was made to allow cut downs on the reference side,
* where otherwise e.g., field references must have been divided in AccessibleTypeTypedField−
* References and NonAccessibleTypeTypedFieldReferences to correctly handle implicit type
* accesses. Instead, an additional constraint rule now ensures that local and anonymous types
* always have default accessibility.
*/
abstract ReferenceType <: AccessModifiableType
/*
* Primitive Types
*
* § 4.2
* A primitive type is predefined by the Java programming language and named by its reserved
* keyword
*/
abstract PrimitiveType <: NamedType
PrimitiveBoolean <: PrimitiveType
PrimitiveByte <: PrimitiveType
PrimitiveShort <: PrimitiveType
PrimitiveInt <: PrimitiveType
PrimitiveLong <: PrimitiveType
PrimitiveChar <: PrimitiveType
PrimitiveFloat <: PrimitiveType
PrimitiveDouble <: PrimitiveType
PrimitiveVoid <: PrimitiveType
/*
* Class or Interface Type
*
* § 4.3
* A class or interface type consists of a type declaration specifier
*/
abstract TlOwnedType <: ReferenceType, TlOwnedEntity {enclosingTypes, enclosingNamedTypes}
abstract ClassOrInterfaceType <: TlOwnedType
abstract NamedClassOrInterfaceType <: ClassOrInterfaceType, NamedType
/*
* Top Level Types
*
* § 7.6
* A top level type declaration declares a top level class type (§8) or a top level interface
* type (§9)
*
*§8
* A top level class is a class that is not a nested class.
*/
abstract TopLevelType <: CompilationUnitMember, NamedClassOrInterfaceType
TopLevelClass <: TopLevelType, Class
TopLevelInterface <: TopLevelType, Interface
TopLevelEnum <: TopLevelClass, Enumeration
/*
* Nested Types
*
*§8
* A nested class is any class whose declaration occurs within the body of another class or
* interface.
*
*§9
* A nested interface is any interface whose declaration occurs within the body of another
* class or interface.
*/
abstract NestedType <: ClassOrInterfaceType
/*
* Member Types
*
* § 8.5
* A member class is a class whose declaration is directly enclosed in another class or
* interface declaration. Similarly, a member interface is an interface whose declaration is
* directly enclosed in another class or interface declaration.
*/
abstract MemberType <: NestedType, HideableMember, NamedClassOrInterfaceType
/*
* Static Member Types
*
* § 8.5.2
* The static keyword may modify the declaration of a member type C within the body of a
* non−inner class T. Its effect is to declare that C is not an inner class.
*/
abstract StaticMemberType <: MemberType, StaticMember
StaticMemberClass <: StaticMemberType, Class
StaticMemberInterface <: StaticMemberType, Interface
StaticMemberEnum <: StaticMemberClass, Enumeration
/*
* Inner Class
*
A. Java-Sprachdefinition
* § 8.1.3
* An inner class is a nested class that is not explicitly or implicitly declared static. Inner
* classes include local (§14.3), anonymous (§15.9.5) and non−static member classes (§8.5).
*/
abstract InnerClass <: NestedType, Class
/*
* Inner Classes in Non Static Context
*
* § 8.1.3
* A statement or expression occurs in a static context if and only if the innermost method,
* constructor, instance initializer, static initializer, field initializer, or explicit
* constructor invocation statement enclosing the statement or expression is a static method, a
* static initializer, the variable initializer of a static variable, or an explicit
* constructor invocation statement (§8.8.7).
*/
abstract InnerClassInNonStaticContext <: InnerClass
/*
* Anonymous Classes
*
* § 15.9.5
* An anonymous class declaration is automatically derived from a class instance creation
* expression by the compiler. An anonymous class is always an inner class (§8.1.3); it is
* never static (§8.1.1, §8.5.2).
*/
abstract AnonymousClass <: InnerClass
AnonymousClassInNonStaticContext <: AnonymousClass, InnerClassInNonStaticContext
AnonymousClassInStaticContext <: AnonymousClass
/*
* Non Static Member Class
*
* § 8.5.2
* The static keyword may modify the declaration of a member type C within the body of a
* non−inner class T. Its effect is to declare that C is not an inner class.
*/
NonStaticMemberClass <: MemberType, InstanceMember, InnerClassInNonStaticContext
/**
* Local Classes
*
* § 14.3
* A local class is a nested class (§8) that is not a member of any class and that has a name.
* All local classes are inner classes (§8.1.3). Every local class declaration statement is
* immediately contained by a block.
*/
abstract LocalClass <: InnerClass, NamedClassOrInterfaceType
LocalClassInNonStaticContext <: LocalClass, InnerClassInNonStaticContext
LocalClassInStaticContext <: LocalClass
/*
* Array Types
*
* § 10
* An array object contains a number of variables. (...) These variables are called the
* components of the array.
*
* The component type of an array may itself be an array type. The components of such an array
* may contain references to subarrays. If, starting from any array type, one considers its
* component type, and then (if that is also an array type) the component type of that type,
* and so on, eventually one must reach a component type that is not an array type; this is
* called the element type of the original array, and the components at this level of the data
* structure are called the elements of the original array.
*/
ArrayType <: ArrayableType, NamedType { componentType, elementType }
TlOwnedArrayType <: TlOwnedType, ArrayType
MemberArrayType <: TlOwnedArrayType, MemberType
JavaLangObjectType <: TopLevelClass
JavaLangStringType <: TopLevelClass
JavaLangIterableType <: TopLevelInterface
JavaLangThrowableType <: TopLevelClass
/*
* Type Variables
*
* § 4.4
*
* A type variable is an unqualified identifier. Type variables are introduced by generic class
* declarations (§8.1.2) generic interface declarations (§9.1.2) generic method declarations
* (§8.4.4) and by generic constructor declarations (§8.8.4).
*/
TypeVariable <: NamedType
abstract TypeParameterBinding <: Entity
SimpleTypeParameterBinding <: TypeParameterBinding { boundType }
abstract WildcardTypeParameterBinding <: TypeParameterBinding
UpperBoundWildcardTypeParameterBinding <: WildcardTypeParameterBinding
LowerBoundWildcardTypeParameterBinding <: WildcardTypeParameterBinding
abstract MemberOrConstructor <: TlOwnedEntity, AccessibleEntity, OwnedEntity, NamedEntity
abstract MethodOrConstructor <: MemberOrConstructor { parameters }
/*
* Members
*
* § 6.4.3
*
* The members of a class type (§8.2) are classes (§8.5, §9.5), interfaces (§8.5, §9.5), fields
* (§8.3, §9.3, §10.7), and methods (§8.4, §9.4). Members are either declared in the type, or
* inherited because they are accessible members of a superclass or superinterface which are
* neither private nor hidden nor overridden (§8.4.8).
*
* § 9.2
*
* The members of an interface are:
* − Those members declared in the interface.
* − Those members inherited from direct superinterfaces.
* − If an interface has no direct superinterfaces, then the interface implicitly
* declares a public abstract member method m with signature s, return type r, and
A. Java-Sprachdefinition
* throws clause t corresponding to each public instance method m with signature s,
* return type r, and throws clause t declared in Object, unless a method with the
* same signature, same return type, and a compatible throws clause is explicitly
* declared by the interface. It is a compile−time error if the interface explicitly
* declares such a method m in the case where m is declared to be final in Object.
*/
abstract Member <: MemberOrConstructor
abstract InstanceMember <: Member
abstract HideableMember <: Member
abstract StaticMember <: HideableMember
/*
* § 8.3 Field Declarations
*/
abstract Field <: HideableMember, TypedEntity
abstract InstanceField <: Field, InstanceMember
abstract RegularTypedField <: Field, RegularTypedEntity
TypeVariableTypedInstanceField <: InstanceField, TypeVariableTypedEntity
RegularTypedInstanceField <: InstanceField, RegularTypedField
StaticField <: RegularTypedField, StaticMember
/*
* Enum Constants
*
* § 16.5
*
* an enum constant is essentially a static final field (§8.3.1.1, §8.3.1.2)
* that is initialized with a class instance creation expression
*
*/
EnumConstant <: StaticField
/*
* § 8.4 Method Declarations
*/
abstract Method <: MethodOrConstructor, Member, TypedEntity
abstract InstanceMethod <: Method, InstanceMember
abstract RegularTypedMethod <: Method, RegularTypedEntity
TypeVariableTypedInstanceMethod <: InstanceMethod, TypeVariableTypedEntity
RegularTypedInstanceMethod <: InstanceMethod, RegularTypedMethod
StaticMethod <: RegularTypedMethod, StaticMember
/*
* § 8.6 Instance Initializers
*
* An instance initializer declared in a class is executed when an instance
* of the class is created (§15.9), as specified in §8.8.7.1.
*
*
* § 8.7 Static Initializers
*
* Any static initializers declared in a class are executed when the class is
* initialized and, together with any field initializers (§8.3.2) for class
* variables, may be used to initialize the class variables of the class (§12.4).
*
*/
abstract Initializer <: OwnedEntity
InstanceInitializer <: Initializer
StaticInitializer <: Initializer
/*
* Local Variables and Formal Parameters (also from catch clauses)
*/
abstract LocalVariableOrParameter <: NamedEntity
abstract LocalVariable <: TypedEntity, LocalVariableOrParameter {enclosingLocalVariables}
RegularTypedLocalVariable <: LocalVariable, RegularTypedEntity
TypeVariableTypedLocalVariable <: LocalVariable, TypeVariableTypedEntity
abstract FormalParameter <: LocalVariableOrParameter, TypedEntity { declaredParameterType }
RegularTypedFormalParameter <: FormalParameter, RegularTypedEntity
TypeVariableTypedFormalParameter <: FormalParameter, TypeVariableTypedEntity
/*
* Constructors
*
* § 8.8
* A constructor is used in the creation of an object that is an instance of a class.
* Constructors are invoked by class instance creation expressions (§15.9), by the
* conversions and concatenations caused by the string concatenation operator + (§15.18.1),
* and by explicit constructor invocations from other constructors (§8.8.7).
*
* Constructor declarations are not members. They are never inherited and therefore are
* not subject to hiding or overriding.
*
* § 8.8.9
* If a class contains no constructor declarations, then a default constructor that takes
* no parameters is automatically provided.
*/
abstract Constructor <: MethodOrConstructor
DeclaredConstructor <: Constructor
DefaultConstructor <: Constructor
AnonymousClassConstructor <: Constructor
abstract Reference <: REFERENCE
abstract NamedReference <: Reference { identifier }
abstract PackagedReference <: Reference { hostPackage }
abstract ReferenceInType <: PackagedReference { owner, tlowner }
abstract TypeOrTypeVariableReference <: NamedReference
TypeVariableReference <: TypeOrTypeVariableReference
abstract TypeReference <: TypeOrTypeVariableReference, PackagedReference { typeBinding }
TypeReferenceInCU <: TypeReference
TypeReferenceInType <: TypeReference, ReferenceInType
ImplicitThisTypeReference <: TypeReferenceInType
abstract NonTypeReference <: ReferenceInType
abstract TypedReference <: NonTypeReference
A. Java-Sprachdefinition
abstract ThisOrSuperReference <: TypedReference
ThisReference <: ThisOrSuperReference
SuperReference <: ThisOrSuperReference
QualifiedThisReference <: ThisReference { thisQualifier }
abstract NamedTypedReference <: NamedReference, TypedReference
abstract ReferenceWithArguments <: NonTypeReference { arguments }
abstract MethodOrFieldReference <: NamedTypedReference
VariableOrParameterReference <: NamedTypedReference
MethodReference <: MethodOrFieldReference, ReferenceWithArguments
FieldReference <: MethodOrFieldReference
// Constructor accesses
abstract ConstructorReference <: ReferenceWithArguments
ClassInstanceCreation <: NamedTypedReference, ConstructorReference
ThisConstructorInvocation <: ConstructorReference
SuperConstructorInvocation <: ConstructorReference
ImplicitSuperConstructorInvocation <: SuperConstructorInvocation
abstract Expression <: ENTITY { expressionType }
NullExpression <: Expression
ArrayTypedExpression <: Expression { inferredArrayType }
ClassOrInterfaceTypedExpression <: Expression { inferredClassOrInterfaceType }
PrimitiveTypedExpression <: Expression { inferredPrimitiveType }
TypeVariableTypedExpression <: Expression
ClassExpression <: ClassOrInterfaceTypedExpression
/* Expressions consisting of accesses to the pseudo array field 'length' */
ArrayLengthAccessExpression <: PrimitiveTypedExpression
abstract ClassCastExpression <: Expression
ArrayTypedCastExpression <: ClassCastExpression, ArrayTypedExpression
ClassOrInterfaceCastTypedExpression <: ClassCastExpression, ClassOrInterfaceTypedExpression
PrimitiveTypedCastExpression <: ClassCastExpression, PrimitiveTypedExpression
TypeVariableTypedCastExpression <: ClassCastExpression, TypeVariableTypedExpression
/* R E F L E C T I O N */
abstract ReflectiveReference <: REFERENCE {owner, hostPackage }
abstract ReflectiveFieldNameReference <: ReflectiveReference { identifier }
abstract ReflectiveMethodNameReference <: ReflectiveReference { identifier }
abstract ReflectiveTypeNameReference <: ReflectiveReference { identifier }
abstract ReflectivePackageNameReference <: ReflectiveReference { identifier }
abstract ReflectiveAccessModifierReference <: ReflectiveReference { returnedAccessModifier }
abstract ReflectiveAccess <: ReflectiveReference { accessEnabled }
abstract ReflectiveParameterTypeListReference <: ReflectiveReference { parameters }
abstract ReflectiveMemberReference <: ReflectiveReference
abstract ReflectiveMemberSetReference <: ReflectiveReference
abstract ReflectiveDeclaredMemberReference <: ReflectiveReference
abstract ReflectiveDeclaredMemberSetReference <: ReflectiveReference
abstract ReflectiveDeclaringClassReference <: ReflectiveReference
// Class#forName(String className)
ClassForName <: ReflectiveTypeNameReference, ReflectivePackageNameReference {}
// Class#getDeclaredField(String name)
ClassGetDeclaredField <: ReflectiveFieldNameReference, ReflectiveDeclaredMemberReference {}
// Class#getDeclaredFields()
ClassGetDeclaredFields <: ReflectiveDeclaredMemberSetReference {}
// Class#getDeclaredMethod(String, Class<?>...)
ClassGetDeclaredMethod <: ReflectiveMethodNameReference,
ReflectiveDeclaredMemberReference,
ReflectiveParameterTypeListReference {}
// Class#getDeclaredMethods()
ClassGetDeclaredMethods <: ReflectiveDeclaredMemberSetReference {}
// Class#getDeclaredConstructor(String, Class<?>...)
ClassGetDeclaredConstructor <: ReflectiveMethodNameReference,
ReflectiveDeclaredMemberReference,
ReflectiveParameterTypeListReference {}
// Class#getDeclaredConstructors()
ClassGetDeclaredConstructors <: ReflectiveDeclaredMemberSetReference {}
// Class#getField(String name)
ClassGetField <: ReflectiveFieldNameReference, ReflectiveMemberReference {}
// Class#getFields()
ClassGetFields <: ReflectiveMemberSetReference {}
// Class#getMethod(String, Class<?>...)
ClassGetMethod <: ReflectiveMethodNameReference,
ReflectiveMemberReference,
ReflectiveParameterTypeListReference {}
// Class#getMethods()
ClassGetMethods <: ReflectiveMemberSetReference { }
// Class#getConstructor(String, Class<?>...)
ClassGetConstructor <: ReflectiveTypeNameReference,
ReflectiveMemberReference,
ReflectiveParameterTypeListReference {}
// Class#getConstructors()
ClassGetConstructors <: ReflectiveMemberSetReference { }
// Field#get(Object obj)
// Field#getBoolean(Object obj)
// Field#getByte(Object obj)
// Field#getChar(Object obj)
// Field#getDouble(Object obj)
A. Java-Sprachdefinition
// Field#getFloat(Object obj)
// Field#getInt(Object obj)
// Field#getLong(Object obj)
// Field#getShort(Object obj)
// Field#set(Object obj, Object value)
// Field#setBoolean(Object obj, boolean z)
// Field#setByte(Object obj, byte b)
// Field#setChar(Object obj, char c)
// Field#setDouble(Object obj, double d)
// Field#setFloat(Object obj, float f)
// Field#setInt(Object obj, int i)
// Field#setLong(Object obj, long l)
// Field#setShort(Object obj, short s)
FieldGetSet <: ReflectiveAccess { }
// Field#getDeclaringClass()
FieldGetDeclaringClass <: ReflectiveDeclaringClassReference { }
// Field#getModifiers()
FieldGetModifiers <: ReflectiveAccessModifierReference { }
// Field#getName
FieldGetName <: ReflectiveFieldNameReference { }
// Field#toGenericString()
FieldToGenericString <: ReflectiveFieldNameReference,
ReflectiveTypeNameReference,
ReflectivePackageNameReference,
ReflectiveAccessModifierReference,
ReflectiveDeclaringClassReference { }
// Method#getDeclaringClass()
MethodGetDeclaringClass <: ReflectiveReference {}
// Method#getModifiers()
MethodGetModifiers <: ReflectiveAccessModifierReference {}
// Method#getName()
MethodGetName <: ReflectiveMethodNameReference {}
// Method#invoke(Object, Object...)
MethodInvoke <: ReflectiveAccess {}
// Method#toGenericString
MethodToGenericString <: ReflectiveMethodNameReference,
ReflectiveTypeNameReference,
ReflectivePackageNameReference,
ReflectiveAccessModifierReference,
ReflectiveDeclaringClassReference { }
// Constructor#newInstance
ConstructorNewInstance <: ReflectiveAccess
// Constructor#toGenericString
ConstructorToGenericString <: ReflectiveTypeNameReference,
ReflectivePackageNameReference,
ReflectiveAccessModifierReference,
ReflectiveDeclaringClassReference { }
// Constructor#getName
ConstructorGetName <: ReflectiveTypeNameReference
// Constructor#getDeclaringClass
ConstructorGetDeclaringClass <: ReflectiveDeclaringClassReference
// Constructor#getModifiers()
ConstructorGetModifiers <: ReflectiveAccessModifierReference {}
properties
identifier: Identifier
accessibility: AccessModifierDomain
owner: ClassOrInterfaceTypes
tlowner: TopLevelTypes
hostPackage: Packages
typeRoot: TypeRoots
declaredType: Types
declaredParameterType: Types
typeBinding: Types
expressionType: Types
boundType: Types
thisQualifier: Types
inferredTypeVariableBinding: Types
inferredPrimitiveType: PrimitiveTypes
inferredClassOrInterfaceType: ClassOrInterfaceTypes
inferredArrayType: ArrayTypes
componentType : ArrayableTypes
elementType : NamedTypes
enclosingTypes : Sequence(Types) // INCLUDING the type itself!
enclosingNamedTypes : Sequence(NamedTypes) // NOT INCLUDING the type itself
enclosingLocalVariables : Sequence(LocalVariables)//NOT INCLUDING the localVariable itself!
isUpperBound : Boolean
parameters: Sequence(FormalParameters)
arguments: Sequence(Expressions)
/* R E F L E C T I O N */
returnedAccessModifier: AccessModifierDomain
accessEnabled : Boolean
domains
AccessModifierDomain = {private, package, protected, public}
ClassOrInterfaceTypes = [ ClassOrInterfaceType ]
TopLevelTypes = [ TopLevelType ]
Packages = [ Package ]
TypeRoots = [ TypeRoot ]
Types = [ Type ]
A. Java-Sprachdefinition
PrimitiveTypes = [ PrimitiveType ]
ArrayTypes = [ ArrayType ]
ArrayableTypes = [ ArrayableType ]
NamedTypes = [ NamedType ]
FormalParameters = [ FormalParameter ]
Expressions = [ Expression ]
LocalVariables = [ LocalVariable ]
queries
binds(reference: NonTypeReference, entity: Entity)
receiver(reference: TypedReference, receiver: Expression)
staticReceiver(reference: TypedReference, receiver: TypeReferenceInType)
sub(subtype : Type, supertype: Type)
isExpression(reference: TypedReference, expression: Expression)
castExpression(castType: TypeOrTypeVariableReference,
innerExpression: Expression,
castExpression: ClassCastExpression)
instantiates(C: Constructor, T: Type)
assignment(lhs: Expression, rhs : Expression)
abstractMethod(abstractMethod: InstanceMethod)
mainMethod(mainMethod: StaticMethod)
finalMethod(finalMethod : Method)
// fact expressing that a certain constructor is an implicit constructor inside an anonymous
// class implicitly invoking it's super constructor
isAnonymousSuperConstructor(superTypeConstructor: Constructor,
anonymousClassConstructor: AnonymousClassConstructor)
isAnonymousClass(anonymousClass: AnonymousClass, superClass: ClassOrInterfaceType)
initialAssignment(lhs: RegularTypedEntity, rhs: Expression)
returnStatement(expression: Expression, method: Method)
argumentPassing(expression: Expression, formalParameter: FormalParameter)
// e.g. a++ will cause typePropagation(a, a++)
typePropagation(fromExpression: Expression, toExpression: Expression)
arrayAccess(arrayAccessExpression: Expression, array: ArrayType)
overrides(overridingMethod : InstanceMethod, overriddenMethod: InstanceMethod)
// overridingMethod overrides overriddenMethod directly i.e., there is no other overriding
// method between them within the type hierarchy
overridesDirectly(overridingMethod : InstanceMethod, overriddenMethod: InstanceMethod)
// The method helps to implement an interface by providing an override equivalent signature
// to a method inherited by the class from one of its implementing interfaces.
// If there are two eager interface implementing methods overriding each other, only the
// overriding one is considered.
eagerInterfaceImplementation(clazz: Class,
typeMethod: InstanceMethod,
interfaceMethod: InstanceMethod)
referenceInEntity(r: Reference, p: Entity)
initialHostType(t: ClassOrInterfaceType, m: Java.Member)
// e.g. new String[]{ "abc", x } for which two facts are generated:
// − arrayInitializer(new String[]{ "abc", x } , "abc")
// − arrayInitializer(new String[]{ "abc", x } , x)
arrayInitializer(arrayTypedExpression: ArrayTypedExpression, expression: Expression)
memberOrConstructorOfType(memberOrConstructor: MemberOrConstructor, type: Type)
publicMemberOrConstructorOfType(memberOrConstructor: MemberOrConstructor, type: Type)
isThisOrSuperExpression(expression: Expression)
packageDeclarationInTypeRoot(packageDeclaration: PackageDeclaration, typeRoot: TypeRoot)
importDeclarationInTypeRoot(importDeclaration: ImportDeclaration, type: NamedType)
// type references inside of method, field, variable and parameter declarations
definesTypeOf(typeReference: TypeOrTypeVariableReference, typedEntity: TypedEntity)
thisOrSuperQualifier(typeReference: TypeOrTypeVariableReference,
qualifiedThisOrSuper: ThisOrSuperReference)
declaresLocalVariableOrParameter(method: MethodOrConstructor,
localVariableOrParameter: LocalVariableOrParameter)
hasEnclosingVariables(localVariable : LocalVariable)
booleanExpression(booleanExpression: Expression)
iterableExpression(iterableExpression: Expression)
throwStatement(throwExpression: Expression)
/* G E N E R I C S */
typeOf(variableOrMember: TypeVariableTypedEntity, typeVariable: TypeVariable)
typeVariableOf(typeVariable: TypeVariable, genericClass: Type)
typeReferenceFor(typeReference: TypeReference, typeParameterBinding: TypeParameterBinding)
typeContextOf(parameterizedTypeEntity: TypedEntity, typeParameterBinding: TypeParameterBinding)
typeParameterBinding(typeVariable: TypeVariable, typeParameterBinding: TypeParameterBinding)
typeVariableBound(typeVariable: TypeVariable, bound: TypeOrTypeVariableReference)
wildcardBound(wildcardParameterBinding: WildcardTypeParameterBinding,
bound: TypeOrTypeVariableReference)
typeParameterAssignment(lhs: TypeParameterBinding, rhs: TypeParameterBinding)
/* R E F L E C T I O N */
reflectiveBinding(reflectiveReference: ReflectiveReference, namedEntity: NamedEntity)
reflectiveReceiver(reflectiveAccess: ReflectiveAccess,
classOrInterfaceType: ClassOrInterfaceType)
B. Constraintregeln für Java
Dieser Anhang zeigt die Constraintregeln für Java, wie sie in den Kapiteln und beschrieben werden. In den Kommentaren genannte Paragraphen beziehen sich auf solche der JavaSprachspezifikation [GJSB ], ihnen nachfolgende Texte sind aus dieser wörtlich übernommen und nicht als Werk des Autors vorliegender Arbeit zu betrachten.
B. Constraintregeln für Java
/*
* § 8.4.3.1
*
* It is a compile−time error for a private method to be declared abstract.
*/
AbstractMethodAccessibility
for all
method: Java.InstanceMethod
do
if
Java.abstractMethod(method)
then
method.accessibility != #private
end
/*
* 8.4.3.3 Final Methods
*
* A method can be declared final to prevent subclasses from overriding or
* hiding it. It is a compile−time error to attempt to override or hide a
* final method.
*/
AccidentalHidingFinalMethod
for all
method1: Java.Method
method2: Java.Method
do
if
Java.finalMethod(method2), all(method1)
then
(method1.identifier != method2.identifier)
or (not Java.sub(method1.owner, method2.owner))
or (not(method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType))
or ((method1.tlowner != method2.tlowner)
and (method2.accessibility = #private))
or ((method1.hostPackage != method2.hostPackage)
and (method2.accessibility <= #package))
end
/*
* Former Ovr rule from [ST09]
*/
AccidentalInstanceMethodOverloadingInstanceMethod
for all
method: Java.InstanceMethod
reference: Java.MethodReference
receiver: Java.ClassOrInterfaceTypedExpression
competingMethod: Java.InstanceMethod
do
if
Java.binds(reference, method),
Java.receiver(reference, receiver),
competingMethod != method
then
(method.identifier != competingMethod.identifier)
or (not Java.sub*(receiver.expressionType,competingMethod.owner))
or (Java.sub*(method.parameters.declaredParameterType,
competingMethod.parameters.declaredParameterType))
or (not Java.sub*(reference.arguments.expressionType,
competingMethod.parameters.declaredParameterType))
or ((reference.tlowner != competingMethod.tlowner)
and (competingMethod.accessibility = #private))
or ((reference.hostPackage != competingMethod.hostPackage)
and (competingMethod.accessibility <= #package))
or ((reference.hostPackage != competingMethod.hostPackage)
and (not Java.sub*(receiver.expressionType, reference.owner))
and (competingMethod.accessibility <= #protected))
end
/*
* Former Ovr rule from [ST09]
*/
AccidentalInstanceMethodOverloadingStaticMethod
for all
method: Java.StaticMethod
reference: Java.MethodReference
receiver: Java.TypeReferenceInType
competingMethod: Java.InstanceMethod
do
if
Java.binds(reference, method),
Java.staticReceiver(reference, receiver),
all(competingMethod)
then
(method.identifier != competingMethod.identifier)
or (not Java.sub*(receiver.typeBinding,competingMethod.owner))
or (Java.sub*(method.parameters.declaredParameterType,
competingMethod.parameters.declaredParameterType))
or (not Java.sub*(reference.arguments.expressionType,
competingMethod.parameters.declaredParameterType))
or ((reference.tlowner != competingMethod.tlowner)
and (competingMethod.accessibility = #private))
or ((reference.hostPackage != competingMethod.hostPackage)
and (competingMethod.accessibility <= #package))
or ((reference.hostPackage != competingMethod.hostPackage)
and (not Java.sub*(reference.owner, competingMethod.owner))
and (competingMethod.accessibility <= #protected))
or ((reference.hostPackage != competingMethod.hostPackage)
and (not Java.sub*(receiver.typeBinding, reference.owner))
and (competingMethod.accessibility <= #protected))
end
/*
* Former Dyn−2 rule from [ST09]
*/
AccidentalOverriding
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
B. Constraintregeln für Java
do
if
subMethod != superMethod
then
Java.overrides(subMethod, superMethod)
or (not Java.sub(subMethod.owner, superMethod.owner))
or (superMethod.identifier != subMethod.identifier)
or (not (subMethod.parameters.declaredParameterType
= superMethod.parameters.declaredParameterType))
or (superMethod.accessibility = #private)
or ((superMethod.accessibility = #package)
and (superMethod.hostPackage != subMethod.hostPackage))
end
/*
* Former Ovr rule from [ST09]
*/
AccidentalStaticMethodOverloadingInstanceMethod
for all
method: Java.InstanceMethod
reference: Java.MethodReference
receiver: Java.ClassOrInterfaceTypedExpression
competingMethod: Java.StaticMethod
do
if
Java.binds(reference, method),
Java.receiver(reference, receiver),
all(competingMethod)
then
(method.identifier != competingMethod.identifier)
or (not Java.sub*(receiver.expressionType,competingMethod.owner))
or (Java.sub*(method.parameters.declaredParameterType,
competingMethod.parameters.declaredParameterType))
or (not Java.sub*(reference.arguments.expressionType,
competingMethod.parameters.declaredParameterType))
or ((reference.tlowner != competingMethod.tlowner)
and (competingMethod.accessibility = #private))
or ((reference.hostPackage != competingMethod.hostPackage)
and (competingMethod.accessibility <= #package))
or ((reference.hostPackage != competingMethod.hostPackage)
and (not Java.sub*(reference.owner, competingMethod.owner))
and (competingMethod.accessibility <= #protected))
end
/*
* Former Ovr rule from [ST09]
*/
AccidentalStaticMethodOverloadingStaticMethod
for all
method: Java.StaticMethod
reference: Java.MethodReference
receiver: Java.TypeReferenceInType
competingMethod: Java.StaticMethod
do
if
Java.binds(reference, method),
Java.staticReceiver(reference, receiver),
all(competingMethod)
then
(method.identifier != competingMethod.identifier)
or (not Java.sub*(receiver.typeBinding,competingMethod.owner))
or (Java.sub*(method.parameters.declaredParameterType,
competingMethod.parameters.declaredParameterType))
or (not Java.sub*(reference.arguments.expressionType,
competingMethod.parameters.declaredParameterType))
or ((reference.tlowner != competingMethod.tlowner)
and (competingMethod.accessibility = #private))
or ((reference.hostPackage != competingMethod.hostPackage)
and (competingMethod.accessibility <= #package))
or ((reference.hostPackage != competingMethod.hostPackage)
and (not Java.sub*(reference.owner, competingMethod.owner))
and (competingMethod.accessibility <= #protected))
end
/*
* Accesses to static fields in interfaces might become ambiguous in case
* of competing fields, which must then be either
*
* (1) not declared in a super type of the static field access receiver type
* (2) declared using a different identifier
* (3) hidden
* (4) inaccessible
*
* (Former Inh−2 rule)
*/
AmbigousFieldAccess
for all
field: Java.StaticField
fieldReference: Java.FieldReference
receiver: Java.TypeReferenceInType
competingField: Java.Field
do
if
Java.binds(fieldReference, field),
Java.staticReceiver(fieldReference, receiver),
all(competingField)
then
((field = competingField)
or (not Java.sub(receiver.typeBinding, competingField.owner))
or (Java.sub(field.owner, competingField.owner))
or ((competingField.identifier != field.identifier)
or (((competingField.tlowner != field.tlowner)
and (competingField.accessibility = #private))
or ((competingField.hostPackage != field.hostPackage)
and (competingField.accessibility <= #package)))))
end
/*
* For convenience, all reference types inherit from AccessModifiableType.
* Since local and anonymous classes have no access modifier,
B. Constraintregeln für Java
* their accessibility is fixed to default accessibility to avoid vain
* constraint solutions.
*/
AnonymousClassAccessibility
for all
anonymousClass: Java.AnonymousClass
do
if
all(anonymousClass)
then
anonymousClass.accessibility = #package
end
/*
* § 15.9.3
*
* [...] a compile−time method matching error results if there is no unique
* most−specific constructor that is [...] accessible
*/
AnonymousClassConstructorAccessibility
for all
constructor: Java.Constructor
anonymousConstructor: Java.AnonymousClassConstructor
do
if
Java.isAnonymousSuperConstructor(constructor, anonymousConstructor)
then
(constructor.tlowner != anonymousConstructor.tlowner)
−> (constructor.accessibility >= #package),
(constructor.hostPackage != anonymousConstructor.hostPackage)
−> (constructor.accessibility >= #protected)
end
/*
* § 15.9.1
*
* [...] let T be the ClassOrInterfaceType after the new token. It is a
* compile−time error if the class or interface named by T is not accessible
* (§6.6)
*/
AnonymousClassRequiresSuperTypeAccessibility
for all
anonymousClass: Java.AnonymousClass
superClass: Java.ClassOrInterfaceType
do
if
Java.isAnonymousClass(anonymousClass, superClass)
then
(anonymousClass.tlowner != superClass.tlowner)
−> (superClass.accessibility >= #package),
(anonymousClass.hostPackage != superClass.hostPackage)
−> (superClass.accessibility >= #protected)
end
/*
* Rule (5) and (8) from [TFK+11]
*/
ArgumentPassing
for all
expression: Java.Expression
formalParameter: Java.RegularTypedFormalParameter
do
if
Java.argumentPassing(expression, formalParameter)
then
Java.sub*(expression.expressionType,
formalParameter.declaredType)
end
/*
* Types of array access expression
*/
ArrayAccessTypes
for all
arrayAccessExpression: Java.Expression
arrayType: Java.ArrayType
do
if
Java.arrayAccess(arrayAccessExpression, arrayType)
then
arrayAccessExpression.expressionType = arrayType.componentType
end
/*
* Types of array initializers
*/
ArrayInitializer
for all
arrayTypedExpression: Java.ArrayTypedExpression
expression: Java.Expression
do
if
Java.arrayInitializer(arrayTypedExpression, expression)
then
expression.expressionType = arrayTypedExpression.inferredArrayType.componentType
end
/*
* § 6.6.1
*
* An array type is accessible if and only if its element type is accessible.
*/
ArrayTypeAccessibility
for all
arrayType: Java.TlOwnedArrayType
elementType: Java.AccessModifiableType
do
if
all(arrayType), all(elementType)
then
B. Constraintregeln für Java
(arrayType.elementType = elementType)
−> (arrayType.accessibility = elementType.accessibility)
end
/*
* just a technical rule: the expression type of an expression is equal to its inferred type
*/
ArrayTypedExpressions
for all
arrayTypedExpression: Java.ArrayTypedExpression
do
if
all(arrayTypedExpression)
then
arrayTypedExpression.expressionType
= arrayTypedExpression.inferredArrayType
end
/*
* Rule (1) from [TFK+11]
*/
Assignment1
for all
lhs: Java.Expression
rhs: Java.Expression
do
if
Java.assignment(lhs, rhs)
then
Java.sub*(rhs.expressionType,lhs.expressionType)
end
/*
* Rule (1) from [TFK+11]
*/
Assignment2
for all
lhs: Java.RegularTypedEntity
rhs: Java.Expression
do
if
Java.initialAssignment(lhs, rhs)
then
Java.sub*(rhs.expressionType,lhs.declaredType)
end
/*
* Class instance creation expressions from other packages require the
* accessed constructor to be public, even when the access comes from a
* sub−type
*
* See § 6.6.2.2
*/
ClassInstanceCreationAccess
for all
r: Java.ClassInstanceCreation
c: Java.Constructor
do
if
Java.binds(r, c)
then
(r.hostPackage != c.hostPackage)
−> (c.accessibility >= #public)
end
/*
* just a technical rule: the expression type of an expression is equal to its inferred type
*/
ClassOrInterfaceTypedExpressions
for all
classOrInterfaceTypedExpression: Java.ClassOrInterfaceTypedExpression
do
if
all(classOrInterfaceTypedExpression)
then
classOrInterfaceTypedExpression.expressionType
= classOrInterfaceTypedExpression.inferredClassOrInterfaceType
end
/*
* When accessing a constructor (via this(), super() or a class instance
* creation expression), it must be accessible from the point of reference
*
* See § 6.6.2.2
*/
ConstructorAccess
for all
r: Java.ConstructorReference
c: Java.Constructor
do
if
Java.binds(r, c)
then
(r.tlowner != c.tlowner)
−> (c.accessibility >= #package),
(r.hostPackage != c.hostPackage)
−> (c.accessibility >= #protected),
(r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner))
−> c.accessibility = #public
end
/*
* When accessing a constructor, the instantiated type must be accessible
*
* See § 15.9.1
*/
ConstructorAccessRequiresTypeAccessibility
for all
r: Java.ConstructorReference
c: Java.Constructor
B. Constraintregeln für Java
type: Java.AccessModifiableType
do
if
Java.binds(r, c), Java.instantiates(c,type)
then
(r.tlowner != c.tlowner)
−> (type.accessibility >= #package),
(r.hostPackage != c.hostPackage)
−> (type.accessibility >= #protected),
(r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner))
−> type.accessibility = #public
end
/*
* The type of a Class instance creation expression is the
* type declaring the invoked constructor.
*
* Rule (7) from [TFK+11]
*/
ConstructorIncovationExpressionTypes
for all
reference: Java.TypedReference
constructor: Java.Constructor
expression: Java.Expression
type: Java.Type
do
if
Java.isExpression(reference, expression),
Java.binds(reference, constructor),
Java.instantiates(constructor,type)
then
expression.expressionType = type
end
/*
* § 8.8
*
* The SimpleTypeName in the ConstructorDeclarator must be the simple
* name of the class that contains the constructor declaration; otherwise
* a compile−time error occurs.
*/
ConstructorNames
for all
type: Java.NamedType
constructor: Java.Constructor
do
if
Java.instantiates(constructor, type)
then
type.identifier = constructor.identifier
end
/*
* § 8.8.9
*
* In an enum type (§8.9), the default constructor is implicitly private.
* Otherwise, if the class is declared public, then the default constructor
* is implicitly given the access modifier public (§6.6); if the class is
* declared protected, then the default constructor is implicitly given the
* access modifier protected (§6.6); if the class is declared private, then
* the default constructor is implicitly given the access modifier private
* (§6.6); otherwise, the default constructor has the default access implied
* by no access modifier.
*/
DefaultConstructorAccessibility
for all
c: Java.DefaultConstructor
t: Java.Class
do
if
Java.instantiates(c,t)
then
t.accessibility = c.accessibility
end
/*
* A method implementing an interface method in a subclass must be public accessible
*/
EagerInterfaceImplementationAccessibility
for all
method: Java.InstanceMethod
interfaceMethod: Java.InstanceMethod
class: Java.Class
do
if
Java.eagerInterfaceImplementation(class, method, interfaceMethod)
then
method.accessibility = #public
end
/*
* Similar to rule (9) from [TFK+11] but for eager interface implementation
*/
EagerInterfaceImplementationParameterType
for all
clazz: Java.Class
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.eagerInterfaceImplementation(clazz, overridingMethod, overriddenMethod)
then
overridingMethod.parameters.declaredParameterType
= overriddenMethod.parameters.declaredParameterType
end
/*
* § 8.4.8.3 in case of eager interface implementation
*/
EagerInterfaceImplementationReturnType
B. Constraintregeln für Java
for all
clazz: Java.Class
overridingMethod: Java.RegularTypedInstanceMethod
overriddenMethod: Java.RegularTypedInstanceMethod
do
if
Java.eagerInterfaceImplementation(clazz, overridingMethod, overriddenMethod)
then
overridingMethod.declaredType = overriddenMethod.declaredType
end
/*
* § 8.4.8.4 Inheriting Methods with Override−Equivalent Signatures
*
* It is possible for a class to inherit multiple methods with override−equivalent
* (§8.4.2) signatures. [...] the method that is not abstract is considered to
* override, and therefore to implement, all the other methods on behalf of the
* class that inherits it.
*/
EagerInterfaceMethodNames
for all
type: Java.Class
typeMethod: Java.InstanceMethod
interfaceMethod: Java.InstanceMethod
do
if
Java.eagerInterfaceImplementation(type, typeMethod, interfaceMethod)
then
typeMethod.identifier = interfaceMethod.identifier
end
/*
* § 8.8.3
*
* If no access modifier is specified for the constructor of an enum type,
* the constructor is private. It is a compile−time error if the constructor
* of an enum type (§8.9) is declared public or protected.
*/
EnumConstructorAccessibility
for all
enumeration: Java.Enumeration
constructor: Java.Constructor
do
if
Java.instantiates(constructor,enumeration)
then
constructor.accessibility = #private
end
/*
* § 8.4.8.3
*
* The access modifier (§6.6) of an overriding or hiding method must provide at
* least as much access as the overridden or hidden method, or a compile−time
* error occurs.
*/
HidingMethodAccessibility
for all
hidingMethod: Java.StaticMethod
hiddenMethod: Java.StaticMethod
do
if
all(hidingMethod), all(hiddenMethod)
then
((hidingMethod.identifier = hiddenMethod.identifier)
and Java.sub(hidingMethod.owner, hiddenMethod.owner)
and (hidingMethod.parameters.declaredParameterType
= hiddenMethod.parameters.declaredParameterType)
and ( ((hidingMethod.hostPackage = hiddenMethod.hostPackage)
and (hiddenMethod.accessibility = #package))
or (hiddenMethod.accessibility >= #protected))
−> (hidingMethod.accessibility >= hiddenMethod.accessibility))
end
/*
* § 8.4.8.3
*
* If a method declaration d1 with return type R1 overrides
* or hides the declaration of another method d2 with return
* type R2, then d1 must be return−type substitutable for d2,
* or a compile−time error occurs.
*/
HidingMethodReturnType
for all
hidingMethod: Java.StaticMethod
hiddenMethod: Java.StaticMethod
do
if
all(hidingMethod), all(hiddenMethod)
then
((hidingMethod.identifier = hiddenMethod.identifier)
and Java.sub(hidingMethod.owner, hiddenMethod.owner)
and (hidingMethod.parameters.declaredParameterType
= hiddenMethod.parameters.declaredParameterType)
and ( ((hidingMethod.hostPackage = hiddenMethod.hostPackage)
and (hiddenMethod.accessibility = #package))
or (hiddenMethod.accessibility >= #protected))
−> (hidingMethod.declaredType = hiddenMethod.declaredType))
end
/*
* § 7.5.1:
*
* A single−type−import declaration imports a single type by giving its canonical name [...]
* The named type must be accessible (§6.6) or a compile−time error occurs.
*/
ImportAccessibility
for all
tr: Java.ImportDeclaration
type: Java.TlOwnedType
B. Constraintregeln für Java
do
if
all(type), all(tr)
then
(tr.typeBinding = type)
−> (type.accessibility >= #package),
((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage))
−> (type.accessibility = #public)
end
InferredParameterizedTypeForWildcard
for all
expression: Java.TypeVariableTypedExpression
reference: Java.TypedReference
declaration: Java.TypeVariableTypedEntity
receiverExpression: Java.Expression
receiverReference: Java.TypedReference
receiverDeclaration: Java.TypedEntity
receiverDeclarationType: Java.TypeReference
typeVariable: Java.TypeVariable
genericType: Java.Type
typeParameterBinding: Java.WildcardTypeParameterBinding
wildcardBound: Java.TypeReference
do
if
Java.isExpression(reference, expression),
Java.receiver(reference, receiverExpression),
Java.isExpression(receiverReference, receiverExpression),
Java.binds(receiverReference, receiverDeclaration),
Java.definesTypeOf(receiverDeclarationType, receiverDeclaration),
Java.binds(reference, declaration),
Java.typeOf(declaration, typeVariable),
Java.typeVariableOf(typeVariable, genericType),
Java.typeParameterBinding(typeVariable, typeParameterBinding),
Java.typeContextOf(receiverDeclaration, typeParameterBinding),
Java.wildcardBound(typeParameterBinding, wildcardBound)
then
expression.expressionType = wildcardBound.typeBinding
end
/*
* 01: class Box<T> { T contents; }
* 02: Box<String> stringBox;
* 03: String s = stringBox.contents;
*
* The inferred type of stringBox.contents must be String
*/
InferredParameterizedTypeNoWildcard
for all
expression: Java.TypeVariableTypedExpression /* 03: stringBox.contents */
reference: Java.TypedReference /* 03: contents */
declaration: Java.TypeVariableTypedEntity /* 01: T contents */
receiverExpression: Java.Expression /* 03: stringBox */
receiverReference: Java.TypedReference /* 03: stringBox */
receiverDeclaration: Java.TypedEntity /* 02: Box<String> stringBox*/
receiverDeclarationType: Java.TypeReference /* 02: Box<String> */
typeVariable: Java.TypeVariable /* 01: T */
genericType: Java.Type /* 01: class Box<T> {T contents;} */
typeParameterBinding: Java.SimpleTypeParameterBinding /* 02: <String> */
do
if
Java.isExpression(reference, expression),
Java.receiver(reference, receiverExpression),
Java.isExpression(receiverReference, receiverExpression),
Java.binds(receiverReference, receiverDeclaration),
Java.definesTypeOf(receiverDeclarationType, receiverDeclaration),
Java.binds(reference, declaration),
Java.typeOf(declaration, typeVariable),
Java.typeVariableOf(typeVariable, genericType),
Java.typeParameterBinding(typeVariable, typeParameterBinding),
Java.typeContextOf(receiverDeclaration, typeParameterBinding)
then
expression.expressionType = typeParameterBinding.boundType
end
/*
* See § 15.12.4.
*
* Accessing a member not only requires it to be accessible from the point of
* reference, but also to be accessible within the whole type hierarchy
* through which it gets accessed.
*
* (Former Inh−1 rule, but now checks the whole type hierarchy between
* declaration an receiver)
*/
InheritedMemberAccessibility
for all
reference: Java.MethodOrFieldReference
declaration: Java.Member
receiver: Java.ClassOrInterfaceTypedExpression
inheritingType: Java.ClassOrInterfaceType
do
if
Java.binds(reference,declaration),
Java.receiver(reference,receiver),
all(inheritingType)
then
((Java.sub(inheritingType, declaration.owner))
and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType))
and (inheritingType.tlowner != reference.tlowner))
−> (declaration.accessibility >= #package),
((Java.sub(inheritingType, declaration.owner))
and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType))
and (inheritingType.hostPackage != reference.hostPackage))
−> (declaration.accessibility >= #protected)
B. Constraintregeln für Java
end
/*
* § 8.4.8.1
*
* A compile−time error occurs if an instance method overrides a static method.
*/
InstanceMethodOverridingStaticMethod
for all
staticMethod: Java.StaticMethod
instanceMethod: Java.InstanceMethod
do
if
all(staticMethod), all(instanceMethod)
then
(staticMethod.identifier != instanceMethod.identifier)
or (not Java.sub(instanceMethod.owner, staticMethod.owner))
or (not(staticMethod.parameters.declaredParameterType
= instanceMethod.parameters.declaredParameterType))
or ((staticMethod.tlowner != instanceMethod.tlowner)
and (staticMethod.accessibility = #private))
or ((staticMethod.hostPackage != instanceMethod.hostPackage)
and (staticMethod.accessibility <= #package))
end
/*
*
* § 9.1.5
*
* All interface members are implicitly public. They are accessible outside
* the package where the interface is declared if the interface is also
* declared public or protected, in accordance with the rules of §6.6.
*/
InterfaceMember
for all
interface: Java.Interface
member: Java.Member
do
if
all(interface), all(member)
then
(member.owner = interface)
−> (member.accessibility = #public)
end
/*
* Iterable Expressions
*/
IterableExpressions
for all
iterableExpression: Java.Expression
iterable: Java.JavaLangIterableType
do
if
Java.iterableExpression(iterableExpression), all(iterable)
then
iterableExpression.expressionType = iterable
end
/*
* For convenience, all reference types inherit from AccessModifiableType.
* Since local and anonymous classes have no access modifier,
* their accessibility is fixed to default accessibility to avoid vain
* constraint solutions.
*/
LocalClassAccessibility
for all
localClass: Java.LocalClass
do
if
all(localClass)
then
localClass.accessibility = #package
end
/*
* Rule (3) and (6) from [TFK+11]
*/
MemberAccess
for all
receiver: Java.Expression
reference: Java.TypedReference
declaration: Java.OwnedEntity
do
if
Java.receiver(reference, receiver),
Java.binds(reference, declaration)
then
Java.sub*(receiver.expressionType,declaration.owner)
end
/*
* The top level type of a member or constructor is the top level type of the declaring class
* (owner).
*
* The host package of a member or constructor is the host package of the declaring class.
*/
MemberOrConstructorTlOwnerAndHostPackage
for all
d: Java.MemberOrConstructor
do
if
all(d)
then
d.tlowner = d.owner.tlowner,
d.hostPackage = d.owner.hostPackage
end
/*
* Accessibility when accessing member types
B. Constraintregeln für Java
*
* See § 6.6.1
*/
MemberTypeAccess
for all
tr: Java.TypeReferenceInType
referencingType: Java.TlOwnedType
type: Java.MemberType
do
if
all(type), all(tr), all(referencingType)
then
(tr.typeBinding = type)
and (referencingType = tr.owner)
and (tr.hostPackage != type.hostPackage)
and Java.sub*!(referencingType.enclosingTypes, type.owner)
−> type.accessibility = #public
end
/*
* Accessibility for method and field accesses
*
* See § 6.6.1
*/
MethodOrFieldAccess1
for all
r: Java.MethodOrFieldReference
d: Java.Member
do
if
Java.binds(r,d)
then
(r.tlowner != d.tlowner)
−> (d.accessibility >= #package),
(r.hostPackage != d.hostPackage)
−> (d.accessibility >= #protected)
end
/*
* Accessibility for method and field accesses
*
* See § 6.6.1
*/
MethodOrFieldAccess2
for all
r: Java.MethodOrFieldReference
d: Java.Member
referencingType: Java.TlOwnedType
do
if
Java.binds(r,d), all(referencingType)
then
( (referencingType = r.owner)
and (r.hostPackage != d.hostPackage)
and ( Java.sub*!(referencingType.enclosingTypes, d.owner))
)
−> d.accessibility = #public
end
/*
* Private accessibility is not sufficient, if the referencing
* type is not enclosed within (or equal to) the declaring type
*
* See § 6.6.1
*/
MethodOrFieldAccess3
for all
r: Java.MethodOrFieldReference
d: Java.Member
referencingType: Java.NestedType
do
if
Java.binds(r,d), all(referencingType)
then
( referencingType = r.owner
and referencingType.enclosingTypes != d.owner
and d.owner.enclosingTypes != referencingType
)
−> d.accessibility != #private
end
/*
* In case of a member access on an anonymous class declaration ( new A(){}.m() ) the owner of
* the method invocation is the enclosing type, but not the anonymous class. For this reason,
* in case of an anonymous class creation, the following rule is required
*/
MethodOrFieldAccess3AnonymousClass
for all
r: Java.MethodOrFieldReference
d: Java.Member
rec: Java.ClassOrInterfaceTypedExpression
classInstanceCreation: Java.ClassInstanceCreation
anonymousClassConstructor: Java.AnonymousClassConstructor
anonymousClass: Java.AnonymousClass
do
if
Java.binds(r,d),
Java.receiver(r,rec),
Java.isExpression(classInstanceCreation, rec),
Java.binds(classInstanceCreation,anonymousClassConstructor),
Java.instantiates(anonymousClassConstructor, anonymousClass)
then
( anonymousClass.enclosingTypes != d.owner
and d.owner.enclosingTypes != anonymousClass
)
−> d.accessibility != #private
end
/*
* See § 15.12.4.3
B. Constraintregeln für Java
*/
MethodOrFieldAccessRequiresTypeAccessibility1
for all
reference: Java.MethodOrFieldReference
member: Java.Member
memberOwner: Java.TopLevelType
do
if
Java.binds(reference,member), all(memberOwner)
then
( (memberOwner = member.owner)
and (reference.tlowner != memberOwner))
−> (memberOwner.accessibility >= #package),
((memberOwner = member.owner)
and (reference.hostPackage != memberOwner.hostPackage))
−> (memberOwner.accessibility >= #public)
end
/*
* See § 15.12.4.3
*/
MethodOrFieldAccessRequiresTypeAccessibility2
for all
reference: Java.MethodOrFieldReference
member: Java.Member
memberOwner: Java.MemberType
referenceOwner: Java.ClassOrInterfaceType
do
if
Java.binds(reference,member),
all(memberOwner),
all(referenceOwner)
then
( (memberOwner = member.owner)
and (reference.tlowner != memberOwner.tlowner))
−> (memberOwner.accessibility >= #package),
( (memberOwner = member.owner)
and (reference.hostPackage != memberOwner.hostPackage))
−> (memberOwner.accessibility >= #protected),
( (memberOwner = member.owner)
and (referenceOwner = reference.owner)
and (reference.hostPackage != memberOwner.hostPackage)
and (Java.sub!(referenceOwner.enclosingTypes, memberOwner.owner))
)
−> (memberOwner.accessibility = #public)
end
/*
* Rule (13) from [TFK+11]
*/
MethodReturn
for all
expression: Java.Expression
method: Java.Method
methodType: Java.TypeReference
do
if
Java.returnStatement(expression, method),
Java.definesTypeOf(methodType, method)
then
Java.sub*(expression.expressionType,methodType.typeBinding)
end
/*
* Currently we do not allow expressions to change from
* primitive to class or interface or array types or vice versa
*/
NoExpressionKindChange1
for all
primitiveTypedExpression: Java.PrimitiveTypedExpression
classOrInterfaceType: Java.ClassOrInterfaceType
do
if
all(primitiveTypedExpression),
all(classOrInterfaceType)
then
primitiveTypedExpression.expressionType != classOrInterfaceType
end
NoExpressionKindChange2
for all
primitiveTypedExpression: Java.PrimitiveTypedExpression
arrayType: Java.ArrayType
do
if
all(primitiveTypedExpression),
all(arrayType)
then
primitiveTypedExpression.expressionType != arrayType
end
NoExpressionKindChange3
for all
arrayTypedExpression: Java.ArrayTypedExpression
classOrInterfaceType: Java.ClassOrInterfaceType
do
if
all(arrayTypedExpression),
all(classOrInterfaceType)
then
arrayTypedExpression.expressionType != classOrInterfaceType
end
NoExpressionKindChange4
for all
arrayTypedExpression: Java.ArrayTypedExpression
primitiveType: Java.PrimitiveType
do
if
all(arrayTypedExpression),
B. Constraintregeln für Java
all(primitiveType)
then
arrayTypedExpression.expressionType != primitiveType
end
NoExpressionKindChange5
for all
classOrInterfaceTypedExpression: Java.ClassOrInterfaceCastTypedExpression
primitiveType: Java.PrimitiveType
do
if
all(classOrInterfaceTypedExpression),
all(primitiveType)
then
classOrInterfaceTypedExpression.expressionType != primitiveType
end
NoExpressionKindChange6
for all
classOrInterfaceTypedExpression: Java.ClassOrInterfaceCastTypedExpression
arrayType: Java.ArrayType
do
if
all(classOrInterfaceTypedExpression),
all(arrayType)
then
classOrInterfaceTypedExpression.expressionType != arrayType
end
/*
* § 8.4.8.1
*
* An instance method m1 declared in a class C overrides another instance method,
* m2, declared in class A iff all of the following are true:
*
* − (...)
* − Either
* − m2 is public, protected or declared with default
* access in the same package as C, or
* − m1 overrides a method m3, m3 distinct from m1,
* m3 distinct from m2, such that m3 overrides m2.
*/
OverridingAccessibility1
for all
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
(overriddenMethod.accessibility >= #protected)
or ((overriddenMethod.accessibility = #package)
and (overriddenMethod.hostPackage = overridingMethod.hostPackage))
end
/*
* § 8.4.8.3
*
* The access modifier (§6.6) of an overriding or hiding method must provide at
* least as much access as the overridden or hidden method, or a compile−time
* error occurs.
*/
OverridingAccessibility2
for all
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
overridingMethod.accessibility >= overriddenMethod.accessibility
end
/*
* 8.4.8.1 Overriding (by Instance Methods)
*
* An instance method m1 declared in a class C overrides another instance method, m2,
* declared in class A iff all of the following are true:
* − (...)
* − The signature of m1 is a subsignature (§8.4.2) of the signature of m2.
*
*
* 8.4.2 Method Signature
*
* Two methods have the same signature if they have the same name and (...)
*/
OverridingMethodNames
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
do
if
Java.overrides(subMethod, superMethod)
then
subMethod.identifier = superMethod.identifier
end
/*
* Rule (11) from [TFK+11]
*/
OverridingMustComeFromSubtype
for all
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
Java.sub(overridingMethod.owner, overriddenMethod.owner)
end
B. Constraintregeln für Java
/*
* Rule (9) from [TFK+11]
*/
OverridingParameterType
for all
overridingMethod: Java.InstanceMethod
overriddenMethod: Java.InstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
overridingMethod.parameters.declaredParameterType
= overriddenMethod.parameters.declaredParameterType
end
/*
* Rule (10) from [TFK+11]
*
* For Java 1.5 the conclusion must read:
* Java.sub*(overridingMethodType.typeBinding, overriddenMethodType.typeBinding)
*/
OverridingReturnType
for all
overridingMethod: Java.RegularTypedInstanceMethod
overriddenMethod: Java.RegularTypedInstanceMethod
do
if
Java.overridesDirectly(overridingMethod, overriddenMethod)
then
overridingMethod.declaredType
= overriddenMethod.declaredType
end
/*
* § 7.3 Compilation Units
*
* A compilation unit consists of three parts, each of which is optional:
*
* − A package declaration (§7.4), giving the fully qualified name (§6.7)
* of the package to which the compilation unit belongs. A compilation
* unit that has no package declaration is part of an unnamed package
* (§7.4.2).
* − ...
*/
PackageDeclarationNames
for all
packageDeclaration: Java.PackageDeclaration
typeRoot: Java.TypeRoot
do
if
Java.packageDeclarationInTypeRoot(packageDeclaration, typeRoot)
then
packageDeclaration.identifier = typeRoot.hostPackage.identifier
end
/*
* In case of nested types do not move references to method parameters
* outside the parameter declaring method:
*
* class A{}
*
* class B{
* void m(final int p) {
* class X extends A {
* void m() {int y = p;} // <− do not pull up m to A
*}
*}
*}
*
*/
ParameterReferencesFromInnerClassStayInPlace
for all
parameterReference: Java.VariableOrParameterReference
parameterReferenceHostMethod: Java.Method
parameter: Java.FormalParameter
parameterHostMethod: Java.Method
originalParameterReferenceHostType: Java.ClassOrInterfaceType
do
if
Java.binds(parameterReference, parameter),
Java.declaresLocalVariableOrParameter(parameterHostMethod, parameter),
Java.referenceInEntity(parameterReference,parameterReferenceHostMethod),
Java.initialHostType(originalParameterReferenceHostType, parameterReferenceHostMethod)
then
(parameterReferenceHostMethod != parameterHostMethod)
−> parameterReferenceHostMethod.owner = originalParameterReferenceHostType
end
ParameterTypes
for all
regularTypedFormalParameter: Java.RegularTypedFormalParameter
do
if
all(regularTypedFormalParameter)
then
regularTypedFormalParameter.declaredParameterType
= regularTypedFormalParameter.declaredType
end
/*
* just a technical rule: the expression type of an expression is equal to its inferred type
*/
PrimitiveTypedExpressions
for all
primitiveTypedExpression: Java.PrimitiveTypedExpression
do
if
all(primitiveTypedExpression)
then
primitiveTypedExpression.expressionType
B. Constraintregeln für Java
= primitiveTypedExpression.inferredPrimitiveType
end
/*
* § 6.6.2
*
* A protected member or constructor of an object may be accessed from outside
* the package in which it is declared only by code that is responsible for
* the implementation of that object.
*
* (Former Acc−2 rule)
*/
ProtectedMethodOrFieldAccess
for all
reference: Java.MethodOrFieldReference
declaration: Java.InstanceMember
receiver: Java.ClassOrInterfaceTypedExpression
do
if
Java.binds(reference,declaration),
Java.receiver(reference,receiver)
then
((reference.hostPackage != declaration.hostPackage)
and (not Java.sub*(receiver.expressionType,reference.owner))
and (not Java.isThisOrSuperExpression(receiver)))
−> declaration.accessibility = #public
end
QualifiedThis
for all
qualifiedThisReference: Java.QualifiedThisReference
thisExpression: Java.Expression
do
if
Java.isExpression(qualifiedThisReference,thisExpression)
then
qualifiedThisReference.thisQualifier = thisExpression.expressionType
end
/*
* The identifier of a named reference must match the name of the name of the referenced entity.
*/
ReferenceIdentifier
for all
reference: Java.NamedTypedReference
entity: Java.NamedEntity
do
if
Java.binds(reference, entity)
then
reference.identifier = entity.identifier
end
/*
* When accessing a member or constructor the accessed declaration must
* be reflective accessible.
*
* Please note, that accessibility for reflective invocations notably
* differs from the accessibility rules for regular member accesses as
* defined in the JLS. A member is reflective accessible if one of the
* following statements is true:
*
* − The accessed member has been set accessible with the setAccessible()−
* method
* − The accessed member is declared in the same type as the accessing
* statement (the JLS requires same top level types instead)
* − The accessed member is declared package accessible and in the same
* package as the accessing statement
* − The accessed member is declared as public
* (the JLS instead also considers protected accessibility)
*
* When accessing a member for which isAccessible() is not set to true,
* also its declaring type must be accessible.
*
* A type is reflective accessible if one of the following statements is
* true:
* − The type is the same as that in which the accessing statement resides
* − The type is declared package accessible and resides in the same
* package as the accessing statement
* − The type is declared as a public type
*
*/
ReflectiveAccessibility
for all
r: Java.ReflectiveAccess
d: Java.MemberOrConstructor
rt: Java.ClassOrInterfaceType
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveReceiver(r, rt)
then
r.accessEnabled = #public
or(
// accessibility of the member
(((((r.owner != d.owner) −> (d.accessibility > #private))
and ((r.hostPackage != d.hostPackage) −> (d.accessibility = #public)))
// accessibility of the declaring type
and ((rt != d.owner) −> (rt.accessibility > #private)))
and ((rt.hostPackage != d.hostPackage) −> (rt.accessibility = #public))))
end
/*
* When accessing a type with a qualified name, it must remain in the
* package used as qualification
*/
ReflectiveClassForNameStayInPackage
for all
r: Java.ClassForName
d: Java.NamedClassOrInterfaceType
B. Constraintregeln für Java
p: Java.Package
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveBinding(r, p)
then
d.hostPackage = p
end
ReflectiveClassGetConstructorsNoNewConstructors
for all
r: Java.ClassGetConstructors
t: Java.NamedType
competing: Java.Constructor
competingOldOwner: Java.Type
do
if
Java.reflectiveBinding(r, t),
Java.memberOrConstructorOfType(competing,competingOldOwner)
then
( competing.accessibility < # public)
or (Java.sub*(t, competingOldOwner))
or(not Java.sub*(t, competing.owner))
end
ReflectiveClassGetConstructorsStayPublic
for all
r: Java.ClassGetConstructors
t: Java.NamedType
d: Java.Constructor
do
if
Java.reflectiveBinding(r, t),
Java.publicMemberOrConstructorOfType(d,t)
then
d.accessibility = #public
end
/*
* Class#get...() returns a member declared public only; thus all
* members d referenced by a get...()−invocation r must be declared
* public.
*/
ReflectiveClassGetDeclarationStayPublic
for all
r: Java.ReflectiveMemberReference
d: Java.MemberOrConstructor
do
if
Java.reflectiveBinding(r, d)
then
d.accessibility = # public
end
/*
* If Class#getDeclared...s() gets invoked on a type t no other
* member of the same type must change its declared type to t.
*/
ReflectiveClassGetDeclaredFieldsNoNewFields
for all
r: Java.ClassGetDeclaredFields
t: Java.NamedType
c: Java.Field // the competing field
ct: Java.NamedType // the (original) declaring type of h
do
if
Java.reflectiveBinding(r, t),
Java.memberOrConstructorOfType(c, ct),
ct != t
then(c.accessibility < # private) or(c.owner != t)
end
ReflectiveClassGetDeclaredMethodsNoNewMethods
for all
r: Java.ClassGetDeclaredMethods
t: Java.NamedType
c: Java.Method //the competing method
ct: Java.NamedType //the (original) declaring type of h
do
if
Java.reflectiveBinding(r, t),
Java.memberOrConstructorOfType(c, ct),
ct != t
then
(c.accessibility < # private) or(c.owner != t)
end
/*
* If Class#get...(...) gets invoked on a subtype rt of the type declaring the
* referenced member declaration d, there must not be any other public member
* c with the same name moving in between.
*/
ReflectiveClassGetFieldNoHiding
for all
r: Java.ClassGetField // the reference to a field
d: Java.Field // the referenced field
rt: Java.NamedType // the receiver type
c: Java.Field // the competing field
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveBinding(r, rt),
d != c
then
(d.identifier != c.identifier)
or(c.accessibility < # public)
or(not Java.sub(c.owner, d.owner))
or(not Java.sub*(rt,c.owner))
end
B. Constraintregeln für Java
/*
* If Class#get...s() is invoked on a type t there must not be any other
* competing public declaration of the same kind moving to t or a super
* type of t.
*/
ReflectiveClassGetFieldsNoNewFields
for all
r: Java.ClassGetFields
t: Java.NamedType
competing: Java.Field
competingOldOwner: Java.Type
do
if
Java.reflectiveBinding(r, t),
Java.memberOrConstructorOfType(competing,competingOldOwner)
then
( competing.accessibility < # public)
or (Java.sub*(t, competingOldOwner))
or(not Java.sub*(t, competing.owner))
end
ReflectiveClassGetFieldsSetStayPublic
for all
r: Java.ClassGetFields
t: Java.NamedType
d: Java.Field
do
if
Java.reflectiveBinding(r, t),
Java.publicMemberOrConstructorOfType(d,t)
then
d.accessibility = #public
end
ReflectiveClassGetMethodNoHiding
for all
r: Java.ClassGetMethod // the reference to a method
d: Java.Method // the referenced method
rt: Java.NamedType // the receiver type
c: Java.Method // the competing method
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveBinding(r, rt),
d != c
then (d.identifier != c.identifier)
or(c.accessibility < # public)
or(not Java.sub(c.owner, d.owner))
or(not Java.sub*(rt, c.owner)
or(d.parameters.declaredParameterType !=
c.parameters.declaredParameterType))
end
ReflectiveClassGetMethodsNoNewMethods
for all
r: Java.ClassGetMethods
t: Java.NamedType
competing: Java.Method
competingOldOwner: Java.Type
do
if
Java.reflectiveBinding(r, t),
Java.memberOrConstructorOfType(competing,competingOldOwner)
then
( competing.accessibility < # public) // not accessible
or (Java.sub*(t, competingOldOwner)) // or returned before
or(not Java.sub*(t, competing.owner)) // or not returned after
end
/*
* Class#get...s() returns public declarations, only; thus
* all d returned by a get...s()−invocation r must be
* declared as public.
*
* Note that three different rules are required in order to keep e.g., in
* case of a Class.getMethods only the methods public.
*/
ReflectiveClassGetMethodsStayPublic
for all
r: Java.ClassGetMethods
t: Java.NamedType
d: Java.Method
do
if
Java.reflectiveBinding(r, t),
Java.publicMemberOrConstructorOfType(d,t)
then
d.accessibility = #public
end
/*
* In case of a Class#getDeclared...(...) the referenced declaration d
* must not change it's declaring type t.
*/
ReflectiveDeclaredMemberReferenceStayThere
for all
r: Java.ReflectiveDeclaredMemberReference
d: Java.MemberOrConstructor
t: Java.Type
do
if
Java.reflectiveBinding(r, d),
Java.memberOrConstructorOfType(d, t)
then
d.owner = t
end
/*
* In case of a Class#getDeclared...s() none of the declared members d
* in this type t must change its location.
B. Constraintregeln für Java
*/
ReflectiveDeclaredMemberSetReferenceStayThere
for all
r: Java.ReflectiveDeclaredMemberSetReference
d: Java.MemberOrConstructor
t: Java.NamedType
do
if
Java.reflectiveBinding(r, t),
Java.memberOrConstructorOfType(d, t)
then
d.owner = t
end
/*
* If the method getDeclaringClass() gets invoked on a declaration d,
* d must not change its declaring type t.
*/
ReflectiveDeclaringClassReferenceStayThere
for all
r: Java.ReflectiveDeclaringClassReference
d: Java.MemberOrConstructor
t: Java.NamedType
do
if
Java.reflectiveBinding(r, d),
Java.reflectiveBinding(r, t)
then
d.owner = t
end
/**
* An invocation of d.getModifiers() requires the member
* declaration d to keep its original access modifier.
*/
ReflectiveFieldGetModifiersKeepAccessibility
for all
r: Java.ReflectiveAccessModifierReference
d: Java.MemberOrConstructor
do
if
Java.reflectiveBinding(r, d)
then
d.accessibility = r.returnedAccessModifier
end
/*
* If there exists a reflective reference r to the name of a field
* declaration d the name used in r must remain equal to the name of d.
*/
ReflectiveFieldNamesEquality
for all
r: Java.ReflectiveFieldNameReference
d: Java.Field
do
if
Java.reflectiveBinding(r, d)
then
r.identifier = d.identifier
end
ReflectiveMethodNamesEquality
for all
r: Java.ReflectiveMethodNameReference
d: Java.Method
do
if
Java.reflectiveBinding(r, d)
then
r.identifier = d.identifier
end
ReflectivePackageNamesEquality
for all
r: Java.ReflectivePackageNameReference
d: Java.Package
do
if
Java.reflectiveBinding(r, d)
then
r.identifier = d.identifier
end
ReflectiveParameterListMatching
for all
r: Java.ReflectiveParameterTypeListReference
d: Java.MethodOrConstructor
do
if
Java.reflectiveBinding(r,d)
then
r.parameters.declaredParameterType = d.parameters.declaredParameterType
end
/*
* A quite technical rule: the host package of a reference is the host
* package of the enclosing type.
*/
ReflectiveReferenceSamePackage
for all
R: Java.ReflectiveReference
do
if
all(R)
then
R.hostPackage = R.owner.hostPackage
end
ReflectiveTypeNamesEquality
for all
B. Constraintregeln für Java
r: Java.ReflectiveTypeNameReference
d: Java.NamedType
do
if
Java.reflectiveBinding(r, d)
then
r.identifier = d.identifier
end
/*
* For expressions consisting of a field−, method− or variable reference
* the type of the expression is the declared type of the referenced
* field, method or variable.
*
* In case of field references: Rule (2) from [TFK+11]
* In case of method invocations: Rule (4) from [TFK+11]
*
*/
RegularTypedReferenceExpressionTypes
for all
typedReference: Java.TypedReference
expression: Java.Expression
typedEntity: Java.RegularTypedEntity
do
if
Java.isExpression(typedReference, expression),
Java.binds(typedReference, typedEntity)
then
expression.expressionType = typedEntity.declaredType
end
SimpleTypeParameterBinding
for all
typeReference: Java.TypeReference
typeParameterBinding: Java.SimpleTypeParameterBinding
do
if
Java.typeReferenceFor(typeReference, typeParameterBinding)
then
typeReference.typeBinding = typeParameterBinding.boundType
end
/*
* Similar to rule (3) and (6) from [TFK+11], but for static accesses
*/
StaticMemberAccess
for all
reference: Java.TypedReference
receiver: Java.TypeReferenceInType
declaration: Java.OwnedEntity
do
if
Java.staticReceiver(reference, receiver),
Java.binds(reference, declaration)
then
Java.sub*(receiver.typeBinding,declaration.owner)
end
/*
* § 8.4.8.2
*
* A compile−time error occurs if a static method hides an instance method.
*/
StaticMethodHidesInstanceMethod
for all
instanceMethod: Java.InstanceMethod
staticMethod: Java.StaticMethod
do
if
all(instanceMethod), all(staticMethod)
then
(instanceMethod.identifier != staticMethod.identifier)
or (not Java.sub(staticMethod.owner, instanceMethod.owner))
or (not(instanceMethod.parameters.declaredParameterType
= staticMethod.parameters.declaredParameterType))
or ((instanceMethod.tlowner != staticMethod.tlowner)
and (instanceMethod.accessibility = #private))
or ((instanceMethod.hostPackage != staticMethod.hostPackage)
and (instanceMethod.accessibility <= #package))
end
/*
* Rule (19) from [TFK+11]
*/
This
for all
thisReference: Java.ThisReference
thisExpression: Java.Expression
do
if
Java.isExpression(thisReference,thisExpression)
then
thisReference.owner = thisExpression.expressionType
end
/*
* When accessing a static member unqualified, the receiver is the current type.
*/
ThisType
for all
thisTypeReference: Java.ImplicitThisTypeReference
do
if
all(thisTypeReference)
then
thisTypeReference.typeBinding = thisTypeReference.owner
end
/*
* Thrown objects must be throwable
B. Constraintregeln für Java
*/
ThrowStatements
for all
throwsExpression: Java.Expression
throwable: Java.JavaLangThrowableType
do
if
Java.throwStatement(throwsExpression), all(throwable)
then
Java.sub*(throwsExpression.expressionType, throwable)
end
/*
* § 6.7
*
* By default, the top level types declared in a package are accessible only within the
* compilation units of that package, but a type may be declared to be public to grant access to
* the type from code in other packages (§6.6, §8.1.1, §9.1.1).
*/
TopLevelTypeAccessibility
for all
tlt: Java.TopLevelType
do
if
all(tlt)
then
tlt.accessibility = #package or tlt.accessibility = #public
end
/*
* The host package of a top level type can be inferred from the host package of it's
* enclosing type root.
*
* The tlOwner of a top level type is the top level type itself (in order to end the recursion).
*/
TopLevelTypeHostPackage
for all
t: Java.TopLevelType
do
if
all(t)
then
t.typeRoot.hostPackage = t.hostPackage,
t.tlowner = t
end
/*
* § 7.6
*
* When packages are stored in a file system (§7.2.1), the host system may
* choose to enforce the restriction that it is a compile−time error if a
* type is not found in a file under a name composed of the type name plus
* an extension (such as .java or .jav) if either of the following is true:
*
* − The type is referred to by code in other compilation units of the
* package in which the type is declared.
* − The type is declared public (and therefore is potentially accessible
* from code in other packages).
*
* This restriction implies that there must be at most one such type per
* compilation unit.
*/
TopLevelTypeNames
for all
tlt: Java.TopLevelType
tr: Java.TypeRoot
do
if
all(tlt), all(tr)
then
(tlt.typeRoot = tr and tlt.accessibility = #public)
−> (tlt.identifier = tr.identifier)
end
/*
* Accessibility for types accesses
*
* See § 6.6.1
*/
TypeAccess
for all
tr: Java.TypeReferenceInType
type: Java.TlOwnedType
do
if
all(type), all(tr)
then
((tr.typeBinding = type) and (tr.tlowner != type.tlowner))
−> (type.accessibility >= #package),
((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage))
−> (type.accessibility >= #protected)
end
/*
* Accessibility of super classes and super interfaces
*
* See §§ 8.1.4, 8.1.5, and 9.1.3
*/
TypeAccessOutsideType
for all
tr: Java.TypeReferenceInCU
type: Java.ClassOrInterfaceType
do
if
all(type), all(tr)
then
(tr.typeBinding = type)
−> (type.accessibility >= #package),
((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage))
−> (type.accessibility = #public)
B. Constraintregeln für Java
end
/*
* Rule (14−16) from [TFK+11]
*/
TypeCasts
for all
castType: Java.TypeReference
innerExpression: Java.Expression
castExpression: Java.ClassCastExpression
do
if
Java.castExpression(castType, innerExpression, castExpression)
then
castExpression.expressionType = castType.typeBinding,
Java.sub*(castType.typeBinding, innerExpression.expressionType)
or Java.sub*(innerExpression.expressionType, castType.typeBinding)
end
TypeParameterAssignment
for all
lhs: Java.SimpleTypeParameterBinding
rhs: Java.SimpleTypeParameterBinding
do
if
Java.typeParameterAssignment(lhs,rhs)
then
lhs.boundType = rhs.boundType
end
TypeParameterAssignmentWildcards1
for all
lhs: Java.UpperBoundWildcardTypeParameterBinding
rhs: Java.SimpleTypeParameterBinding
lhsBound: Java.TypeReference
do
if
Java.typeParameterAssignment(lhs,rhs),
Java.wildcardBound(lhs,lhsBound)
then
Java.sub*(rhs.boundType,lhsBound.typeBinding)
end
TypeParameterAssignmentWildcards2
for all
lhs: Java.LowerBoundWildcardTypeParameterBinding
rhs: Java.SimpleTypeParameterBinding
lhsBound: Java.TypeReference
do
if
Java.typeParameterAssignment(lhs,rhs),
Java.wildcardBound(lhs,lhsBound)
then
Java.sub*(lhsBound.typeBinding,rhs.boundType)
end
TypeParameterAssignmentWildcards3
for all
lhs: Java.UpperBoundWildcardTypeParameterBinding
rhs: Java.UpperBoundWildcardTypeParameterBinding
lhsBound: Java.TypeReference
rhsBound: Java.TypeReference
do
if
Java.typeParameterAssignment(lhs,rhs),
Java.wildcardBound(lhs,lhsBound),
Java.wildcardBound(rhs,rhsBound)
then
Java.sub*(rhsBound.typeBinding,lhsBound.typeBinding)
end
TypeParameterAssignmentWildcards4
for all
lhs: Java.LowerBoundWildcardTypeParameterBinding
rhs: Java.LowerBoundWildcardTypeParameterBinding
lhsBound: Java.TypeReference
rhsBound: Java.TypeReference
do
if
Java.typeParameterAssignment(lhs,rhs),
Java.wildcardBound(lhs,lhsBound),
Java.wildcardBound(rhs,rhsBound)
then
Java.sub*(lhsBound.typeBinding,rhsBound.typeBinding)
end
TypeParameterAssignmentWildcards5
for all
lhs: Java.UpperBoundWildcardTypeParameterBinding
rhs: Java.LowerBoundWildcardTypeParameterBinding
lhsBound: Java.TypeReference
object: Java.JavaLangObjectType
do
if
Java.typeParameterAssignment(lhs,rhs),
Java.wildcardBound(lhs,lhsBound),
all(object)
then
lhsBound.typeBinding = object
end
/*
* Propagation of types from nested to enclosing expressions
*/
TypePropagation
for all
expression1: Java.Expression
expression2: Java.Expression
do
if
Java.typePropagation(expression1,expression2)
B. Constraintregeln für Java
then
expression1.expressionType = expression2.expressionType
end
/*
* The identifier of a type reference must match the name of the referenced type.
*/
TypeReferenceIdentifier
for all
type: Java.NamedType
ref: Java.TypeReference
do
if
all(type),
all(ref)
then
(type = ref.typeBinding)
−> (type.identifier = ref.identifier)
end
TypeVariableBound
for all
typeVariable: Java.TypeVariable
typeVariableBound: Java.TypeReference
typeParameterBinding: Java.SimpleTypeParameterBinding
do
if
Java.typeVariableBound(typeVariable,typeVariableBound),
Java.typeParameterBinding(typeVariable, typeParameterBinding)
then
Java.sub*(typeParameterBinding.boundType, typeVariableBound.typeBinding)
end
/*
* The type of a typed entity (method, field, variable, parameter,...)
* depends on the type binding of the type reference inside the typed
* entity's declaration.
*/
TypedEntityTypes
for all
typeReference: Java.TypeReference
typedEntity: Java.RegularTypedEntity
do
if
Java.definesTypeOf(typeReference, typedEntity)
then
typeReference.typeBinding = typedEntity.declaredType
end
/*
* § 8.3 Field Declarations
*
* It is a compile−time error for the body of a class declaration to
* declare two fields with the same name. Methods, types, and fields
* may have the same name, since they are used in different contexts
* and are disambiguated by different lookup procedures (§6.5).
*/
UniqueFieldIdentifier
for all
field1: Java.Field
field2: Java.Field
do
if
field1 != field2
then
field1.owner = field2.owner
−>(field1.identifier != field2.identifier)
end
/*
* § 8.4.1:
*
* If two formal parameters of the same method or constructor are declared to have the same name
* (that is, their declarations mention the same Identifier), then a compile−time error occurs.
*
*/
UniqueLocalVariableNames1
for all
parameter1: Java.FormalParameter
parameter2: Java.FormalParameter
methodOrConstructor: Java.MethodOrConstructor
do
if
Java.declaresLocalVariableOrParameter(methodOrConstructor, parameter1),
Java.declaresLocalVariableOrParameter(methodOrConstructor, parameter2),
parameter1 != parameter2
then
parameter1.identifier != parameter2.identifier
end
UniqueLocalVariableNames2
for all
parameter: Java.FormalParameter
localVariable: Java.LocalVariable
methodOrConstructor: Java.MethodOrConstructor
do
if
Java.declaresLocalVariableOrParameter(methodOrConstructor, parameter),
Java.declaresLocalVariableOrParameter(methodOrConstructor, localVariable)
then
(parameter.identifier != localVariable.identifier)
end
UniqueLocalVariableNames3
for all
localVariable: Java.LocalVariable
do
if
Java.hasEnclosingVariables(localVariable)
then
B. Constraintregeln für Java
localVariable.identifier != localVariable.enclosingLocalVariables.identifier
end
/*
* § 8.1 Class Declaration
*
* A compile−time error occurs if a class has the same simple name as any
* of its enclosing classes or interfaces
*
* § 9.1 Interface Declarations
*
* A compile−time error occurs if an interface has the same simple name
* as any of its enclosing classes or interfaces
*
*/
UniqueMemberTypeNames1
for all
t: Java.MemberType
do
if
all(t)
then
t.identifier != t.enclosingNamedTypes.identifier,
t.identifier != t.tlowner.identifier
end
/*
* The property enclosingNamedTypes does not include the type itself so we need
* this rule in addition to UniqueMemberTypeNames1.
*/
UniqueMemberTypeNames2
for all
memberType1: Java.MemberType
memberType2: Java.MemberType
do
if
memberType1 != memberType2
then
(memberType1.owner = memberType2.owner)
−> (memberType1.identifier != memberType2.identifier)
end
/*
* § 8.4 Method Declarations
*
* It is a compile−time error for the body of a class to declare as
* members two methods with override−equivalent signatures (§8.4.2)
* (name, number of parameters, and types of any parameters). Methods
* and fields may have the same name, since they are used in different
* contexts and are disambiguated by different lookup procedures (§6.5).
*/
UniqueMethodIdentifier
for all
method1: Java.RegularTypedMethod
method2: Java.RegularTypedMethod
do
if
method1 != method2
then
(method1.owner = method2.owner)
and (method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType)
−> (method1.identifier != method2.identifier)
end
/*
* § 7.6
*
* A compile−time error occurs if the name of a top level type appears as
* the name of any other top level class or interface type declared in
* the same package (§7.6).
*/
UniqueTopLevelTypeIdentifier
for all
tlt1: Java.TopLevelType
tlt2: Java.TopLevelType
do
if
tlt1 != tlt2
then
(tlt1.hostPackage = tlt2.hostPackage)
−> (tlt1.identifier != tlt2.identifier)
end
/*
* References in Methods, Constructors or field initializers have the same host package,
* owner and tlowner as their enclosing declarations
*/
UpdateReferenceHostPackage
for all
r: Java.PackagedReference
d: Java.PackagedEntity
do
if
Java.referenceInEntity(r, d)
then
r.hostPackage = d.hostPackage
end
UpdateReferenceOwner
for all
r: Java.ReferenceInType
d: Java.OwnedEntity
do
if
Java.referenceInEntity(r, d)
then
r.owner = d.owner
end
B. Constraintregeln für Java
UpdateReferenceOwner2
for all
r: Java.TypeReferenceInCU
d: Java.PackagedEntity
do
if
Java.referenceInEntity(r, d)
then
r.hostPackage = d.hostPackage
end
UpdateReferenceTLOwner
for all
r: Java.ReferenceInType
d: Java.TlOwnedEntity
do
if
Java.referenceInEntity(r, d)
then
r.tlowner = d.tlowner
end
UpperBoundWildcardAssignments
for all
lhs: Java.TypeVariableTypedExpression
rhs: Java.Expression
reference: Java.TypedReference
receiverExpression: Java.Expression
receiverReference: Java.TypedReference
receiverDeclaration: Java.TypedEntity
typeParameterBinding: Java.WildcardTypeParameterBinding
object: Java.JavaLangObjectType
do
if
Java.assignment(lhs,rhs),
Java.isExpression(reference,rhs),
Java.receiver(reference,receiverExpression),
Java.isExpression(receiverReference,receiverExpression),
Java.binds(receiverReference,receiverDeclaration),
Java.typeContextOf(receiverDeclaration,typeParameterBinding),
all(object)
then
rhs.expressionType = object
end
C. Refaktorisierungsdefinitionen
Move Compilation Unit
refactoring MoveCompilationUnit
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
hostPackage of Java.TypeRoot as HostPackage
allowed changes
identifier of Java.PackageDeclaration
hostPackage of Java.TlOwnedType
hostPackage of Java.MemberOrConstructor
hostPackage of Java.ReferenceInType
hostPackage of Java.TypeReferenceInCU
refactoring MoveCompilationUnitCA
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
hostPackage of Java.TypeRoot as HostPackage
allowed changes
identifier of Java.PackageDeclaration
hostPackage of Java.TlOwnedType
hostPackage of Java.MemberOrConstructor
hostPackage of Java.ReferenceInType
hostPackage of Java.TypeReferenceInCU
accessibility of Java.AccessibleEntity
Pull Up Member
refactoring PullUpMember
languages Java
uses Accessibility, Types, Locations, Names, Reflection
forced changes
owner of Java.Member as HostType
tlowner of Java.Member as ToplevelType
hostPackage of Java.Member as HostPackage
allowed changes
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced}
expressionType of Java.Expression {initial, HostType @ forced}
hostPackage of Java.PackagedReference {initial, HostPackage @ forced}
owner of Java.ReferenceInType {initial, HostType @ forced}
tlowner of Java.ReferenceInType {initial, ToplevelType @ forced}
C. Refaktorisierungsdefinitionen
refactoring PullUpMemberCA
languages Java
uses Accessibility, Types, Locations, Names, Reflection
forced changes
owner of Java.Member as HostType
tlowner of Java.Member as ToplevelType
hostPackage of Java.Member as HostPackage
allowed changes
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced}
expressionType of Java.Expression {initial, HostType @ forced}
hostPackage of Java.PackagedReference {initial, HostPackage @ forced}
owner of Java.ReferenceInType {initial, HostType @ forced}
tlowner of Java.ReferenceInType {initial, ToplevelType @ forced}
accessibility of Java.AccessibleEntity
refactoring PullUpMemberCL
languages Java
uses Accessibility, Types, Locations, Names, Reflection
forced changes
owner of Java.Member as HostType
tlowner of Java.Member as ToplevelType
hostPackage of Java.Member as HostPackage
allowed changes
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced}
expressionType of Java.Expression {initial, HostType @ forced}
hostPackage of Java.PackagedReference {initial, HostPackage @ forced}
owner of Java.ReferenceInType {initial, HostType @ forced}
tlowner of Java.ReferenceInType {initial, ToplevelType @ forced}
owner of Java.Member {initial, HostType @ forced}
tlowner of Java.Member {initial, ToplevelType @ forced}
hostPackage of Java.Member {initial, HostPackage @ forced}
refactoring PullUpMemberCACL
languages Java
uses Accessibility, Types, Locations, Names, Reflection
forced changes
owner of Java.Member as HostType
tlowner of Java.Member as ToplevelType
hostPackage of Java.Member as HostPackage
allowed changes
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced}
expressionType of Java.Expression {initial, HostType @ forced}
hostPackage of Java.PackagedReference {initial, HostPackage @ forced}
owner of Java.ReferenceInType {initial, HostType @ forced}
tlowner of Java.ReferenceInType {initial, ToplevelType @ forced}
accessibility of Java.AccessibleEntity
owner of Java.Member {initial, HostType @ forced}
tlowner of Java.Member {initial, ToplevelType @ forced}
hostPackage of Java.Member {initial, HostPackage @ forced}
Rename
refactoring Rename
Generalize / Specialize Declared Type
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
identifier of Java.NamedEntity as NewName
allowed changes
identifier of Java.NamedReference {initial, NewName @ forced}
identifier of Java.Constructor {initial, NewName @ forced}
identifier of Java.Method {initial, NewName @ forced}
identifier of Java.ImportDeclaration {initial, NewName @ forced}
identifier of Java.FormalParameter {initial, NewName @ forced}
identifier of Java.MemberType {initial, NewName @ forced}
identifier of Java.TypeRoot {initial, NewName @ forced}
identifier of Java.TopLevelType {initial, NewName @ forced}
Generalize / Specialize Declared Type
refactoring ChangeDeclaredType
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
typeBinding of Java.TypeReference as NewType
allowed changes
identifier of Java.TypeReference
declaredType of Java.RegularTypedEntity {initial, NewType @ forced}
expressionType of Java.Expression {initial, NewType @ forced}
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced}
refactoring ChangeDeclaredTypeCA
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
typeBinding of Java.TypeReference as NewType
allowed changes
identifier of Java.TypeReference
declaredType of Java.RegularTypedEntity {initial, NewType @ forced}
expressionType of Java.Expression {initial, NewType @ forced}
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced}
accessibility of Java.AccessModifiableType
accessibility of Java.DefaultConstructor
refactoring ChangeDeclaredTypeCT
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
typeBinding of Java.TypeReference as NewType
allowed changes
identifier of Java.TypeReference
declaredType of Java.RegularTypedEntity {initial, NewType @ forced}
declaredParameterType of Java.FormalParameter {initial, NewType @ forced}
expressionType of Java.Expression {initial, NewType @ forced}
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced}
typeBinding of Java.TypeReference {initial, NewType @ forced}
C. Refaktorisierungsdefinitionen
refactoring ChangeDeclaredTypeCACT
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
typeBinding of Java.TypeReference as NewType
allowed changes
identifier of Java.TypeReference
declaredType of Java.RegularTypedEntity {initial, NewType @ forced}
declaredParameterType of Java.FormalParameter {initial, NewType @ forced}
expressionType of Java.Expression {initial, NewType @ forced}
inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced}
typeBinding of Java.TypeReference {initial, NewType @ forced}
accessibility of Java.AccessModifiableType
accessibility of Java.DefaultConstructor
Change Accessibility
refactoring ChangeAccessibility
languages Java
uses Accessibility, Types, Locations, Names, Reflection, Reflection
forced changes
accessibility of Java.AccessibleEntity as Accessibility
allowed changes
accessibility of Java.DefaultConstructor
accessibility of Java.AnonymousClassConstructor
accessibility of Java.TlOwnedArrayType
refactoring ChangeMethodAccessibility
languages Java
uses Accessibility, Types, Locations, Names, Reflection
forced changes
accessibility of Java.Method as Accessibility
allowed changes
accessibility of Java.Method
accessibility of Java.DefaultConstructor
accessibility of Java.AnonymousClassConstructor
accessibility of Java.TlOwnedArrayType
Literaturverzeichnis
[ASU
]
Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. Compilers, principles, techniques,
and tools. Addison-Wesley Pub. Co., Reading and Mass,
.
[BA
]
Timothy A. Budd and Dana Angluin. Two notions of correctness and their relation
to testing. Acta Informatica (Springer), ( ): – ,
.
[Bär
]
Robert Bär. Mutantengenerierung durch Typ-Constraints. Bachelorarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/
ps/bachelorarbeit-baer.pdf,
.
[BCM
]
Stephen M. Blackburn, Perry Cheng, and Kathryn S. McKinley. Myths and realities: the performance impact of garbage collection. ACM SIGMETRICS Performance Evaluation Review, ( ): – ,
.
[BCR
]
Victor Basili, Gianluigi Caldiera, and Dieter H. Rombach. The goal question
metric approach. In Encyclopedia of Software Engineering, pages
– . Wiley,
.
[Bec ]
Kent Beck. Extreme programming eXplained: Embrace change. XP series. AddisonWesley, Reading and MA,
.
[BFS ]
Markus Bach, Florian Forster, and Friedrich Steimann. Declared Type Generalization Checker: An Eclipse plug-in for systematic programming with more general
types. In Proceedings of the ᵗ International Conference on Fundamental Approaches
to Software Engineering. FASE’ , pages
– . Springer,
.
[BG
]
Chi a Baral and Michael Gelfond. Logic Programming and Knowledge Representation. Journal of Logic Programming, : – ,
.
[BG
]
Joshua Bloch and Neal Gafter. Java puzzlers: Traps, pitfalls, and corner cases.
Addison-Wesley, Upper Saddle River and NJ,
.
[BGH+
] Stephen M. Blackburn, Robin Garner, Chris Hoffmann, Asjad M. Khang, Kathryn S. McKinley, Rotem Ben ur, Amer Diwan, Daniel Feinberg, Daniel Frampton, Samuel Z. Guyer, Martin Hirzel, Antony Hosking, Maria Jump, Han Lee,
J. Eliot B. Moss, Aashish Phansalkar, Darko Stefanović, Thomas VanDrunen,
Daniel von Dincklage, and Ben Wiedermann. The DaCapo Benchmarks: Java
Benchmarking Development and Analysis. In Proceedings of the
ᵗ Conference
on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ ,
pages
– . ACM,
.
Literaturverzeichnis
[BGS
]
[BSS+
]
[BTF
[CC
Philipp Bouillon, Eric Großkinsky, and Friedrich Steimann. Controlling Accessibility in Agile Projects with the Access Modifier Modifier. In Proceedings of the
ᵗ International Conference on Technology of Object-Oriented Languages and Systems
(TOOLS EUROPE’ ), pages – . Springer,
.
Eric Bodden, Andreas Sewe, Jan Sinschek, Hela Oueslati, and Mira Mezini.
Taming reflection: Aiding static analysis in the presence of reflection and custom
class loaders. In Proceedings of the ᵈ International Conference on Software Engineering. ICSE ’ , ICSE ’ , pages
– , New York and NY and USA,
. ACM.
]
I ai Balaban, Frank Tip, and Robert M. Fuhrer. Refactoring Support for Class
Library Migration. In Proceedings of the ᵗ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages
– . ACM,
.
]
[CMS
Björn Carlson and Mats Carlsson. Compiling and Executing Disjunctions of Finite Domain Constraints. In Proceedings of the ᵗ International Conference on Logic
Programming. ICLP’ , pages
– . MIT Press,
.
]
[CRTR
[CW
Aske Simon Christensen, Anders Møller, and Michael I. Schwar bach. Precise
Analysis of String Expressions. In Proceedings of the ᵗ International Conference on
Static Analysis. SAS ’ , pages – . Springer-Verlag,
.
]
]
Oscar Callaú, Romain Robbes, Éric Tanter, and David Röthlisberger. How Developers Use the Dynamic Features of Programming Languages: The Case of Smalltalk. In Proceedings of the ᵗ Working Conference on Mining Software Repositories.
MSR ’ , pages – . ACM,
.
Luca Cardelli and Peter Wegner. On understanding types, data abstraction, and
polymorphism. ACM Computing Surveys, ( ): – ,
.
[DDGM ] Bre Daniel, Danny Dig, Kely Garcia, and Darko Marinov. Automated testing
of refactoring engines. In Proceedings of the ᵗ joint meeting of the European Software Engineering Conference and the ACM SIGSOFT Symposium on the Foundations
of Software Engineering. ESEC/FSE ’ , pages
– . ACM,
.
[DE
]
Sophia Drossopoulou and Susan Eisenbach. Java is type safe — Probably. In
Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP
’ , pages
– ,
.
[Dij
]
Edsger Wybe Dijkstra. A discipline of programming. Prentice-Hall series in automatic computation. Prentice-Hall, Englewood Cliffs and N.J,
.
[DKTE
]
Alan Donovan, Adam Kiežun, Ma hew S. Tschan , and Michael D. Ernst. Converting java programs to use generic libraries. In Proceedings of the ᵗ Conference
on Object-oriented programing, systems, languages, and applications. OOPSLA’ , pages – , New York and NY and USA,
. ACM.
Literaturverzeichnis
[DKWB
] Martin Drozdz, Derrick G. Kourie, Bruce W. Watson, and Andrew Boake. Refactoring Tools and Complementary Techniques. In Proceedings of the ᵗ ACS/IEEE
International Conference on Computer Systems and Applications (AICCSA ’ ), pages
–
,
.
[DRW ]
Premkumar T. Devanbu, David S. Rosenblum, and Alexander L. Wolf. Generating
testing and analysis tools with Aria. ACM Transactions on Software Engineering and
Methodology (TOSEM), ( ): – ,
.
[EAJM
Eric Bodden, Andreas Sewe, Jan Sinschek, and Mira Mezini. Taming Reflection:
Static Analysis in the Presence of Reflection and Custom Class Loaders. Technical
Report TUD-CS, CASED,
.
]
[Eck ]
Bruce Eckel. Thinking in Java. Prentice Hall, edition,
[ECL
The Eclipse IDE, Juno Release . . : http://www.eclipse.org/,
]
.
.
[EH ]
Torbjörn Ekman and Görel Hedin. The JastAdd Extensible Java Compiler. In Proceedings of the
ᵈ Conference on Object-Oriented Programming, systems, languages,
and applications. OOPSLA’ , pages – , New York and NY and USA,
. ACM.
[EH ]
Osama El Hosami.
Implementierung eines Eclipse-Plugins zum automatisierten Testen von Refaktorisierungswerkzeugen.
Masterarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/ps/
.
masterarbeit-el-hosami.pdf,
[FF
]
Ira R. Forman and Nate Forman. Java Reflection In Action. Manning and Pearson
Education, Greenwich and Conn and London,
.
[FJ
]
B. Foote and Ralph E. Johnson. Reflective Facilities in Smalltalk- . In Proceedings of the ᵗ Conference on Object-Oriented Programming, systems, languages, and
applications. OOPSLA’ , pages
– . ACM,
.
[FMM+
[Fow
[Fre
] Asger Feldthaus, Todd Millstein, Anders Møller, Max Schäfer, and Frank Tip.
Tool-supported Refactoring for JavaScript. In Proceedings of the ᵗ Conference on
Object-oriented programing, systems, languages, and applications. OOPSLA’ , pages
–
,
.
]
Martin Fowler. Refactoring: Improving the Design of Existing Code. Addison-Wesley,
.
]
[FTK+
Sarah Frenkel. Implementierung einer Zurückschreibekomponente für Constraintbasierte Refaktorisierungen. Bachelorarbeit, FernUniversität in Hagen, http://www.
fernuni-hagen.de/imperia/md/content/ps/bachelorarbeit-frenkel.pdf,
.
]
Robert M. Fuhrer, Frank Tip, Adam Kiezun, Julian Dolby, and Markus Keller.
Efficiently Refactoring Java Applications to Use Generic Libraries. In Proceedings
of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages
– . Springer,
.
Literaturverzeichnis
[FYBT
]
[GBE
Francisco Javier Pérez, Yania Crespo, Berthold Hoffmann, and Tom Mens. A case study to evaluate the suitability of graph transformation tools for program
refactoring. International Journal on Software Tools for Technology Transfer, ( ): – ,
.
]
Andy Georges, Dries Buytaert, and Lieven Eeckhout. Statistically Rigorous Java
Performance Evaluation. In Proceedings of the
ᵈ Conference on Object-Oriented
Programming, systems, languages, and applications. OOPSLA’ , pages – , New
York and NY and USA,
. ACM.
[GBO+
] Milos Gligoric, Farnaz Behrang, Jeffrey Overbey, Munawar Hafiz, and Darko Marinov. Systematic Testing of Refactoring Engines on Real Software Projects. In
Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP
’ , pages
– . Springer,
.
[GGM
]
Georg Go lob, Gianluigi Greco, and Toni Mancini. Conditional Constraint Satisfaction: Logical Foundations and Complexity. In Proceedings of the ᵗ International
Joint Conference on Artifical Intelligence. IJCAI’ , pages – , San Francisco,
.
Morgan Kaufmann Publishers Inc.
[GJSB
]
James Gosling, Bill Joy, Guy Steele, and Gilad Bracha. The Java™ Language Specification. Addison-Wesley, ᵈ edition,
.
[GR
]
Adele Goldberg and David Robson. Smalltalktation. Addison-Wesley,
.
[Gri
]
William G. Griswold. Program Restructuring as an Aid to Software Maintenance. PhD
thesis, University of Washington,
.
[Gro
]
Markus Grothoff. Mutation Testing mit Refacola. Bachelorarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/ps/
bachelorarbeit-grothoff.pdf,
.
[GVG
]
: The Language and its Implemen-
Dayong Gu, Clark Verbrugge, and Etienne M. Gagnon. Relative factors in performance analysis of Java virtual machines. In Proceedings of the ᵈ International
Conference on Virtual execution environments. VEE’ , pages
– . ACM,
.
[Hec
]
Reiko Heckel. Graph Transformation in a Nutshell. Electronic Notes in Theoretical
Computer Science,
( ): – ,
.
[Hoa
]
Charles A. R. Hoare. An Axiomatic Basis for Computer Programming. Communications of the ACM, ( ): – ,
.
[How
[HWG
]
William E. Howden. Weak Mutation Testing and Completeness of Test Sets. IEEE
Transactions on Software Engineering, ( ): – ,
.
]
Anders Hejlsberg, Sco Wiltamuth, and Peter Golde. C# Language Specification.
Addison-Wesley,
.
Literaturverzeichnis
[IJI
[Ikk
]
IntelliJ IDEA Community Edition
.
]
[JDT
. . : http://www.jetbrains.com/idea/,
Sergei Ikkert. Untersuchung der Eclipse-JDT-Refaktorisierungen mit Hilfe des Refactoring Tool Testers. Masterarbeit, FernUniversität in Hagen, http://www.
fernuni-hagen.de/imperia/md/content/ps/masterarbeit-ikkert.pdf,
.
The Eclipse Java Development Tools, Juno Release: http://www.eclipse.org/
jdt/,
.
]
[JH ]
Yue Jia and Mark Harman. An Analysis and Survey of the Development of Mutation Testing. IEEE Transactions on Software Engineering, ( ): – ,
.
[Jia ]
Yue Jia. Mutation Testing Repository – Publications on Mutation Testing: http:
//www.dcs.kcl.ac.uk/pg/jiayue/repository/,
.
[JRL
]
Narendra Jussien, Guillaume Rochart, and Xavier Lorca. The CHOCO constraint
programming solver. In Workshop on Open-Source Software for Integer and Constraint
Programming.OSSIP’ ,
.
[Jun
]
Ulrich Junker. QUICKXPLAIN: preferred explanations and relaxations for overconstrained problems. In Proceedings of the ᵗ national conference on Artifical intelligence. AAAI’ , pages
– ,
.
[KETF
]
Adam Kiezun, Michael D. Ernst, Frank Tip, and Robert M. Fuhrer. Refactoring
for Parameterizing Java Classes. In Proceedings of the ᵗ International Conference
on Software Engineering. ICSE ’ , pages
–
. IEEE Computer Society,
.
[KH ]
Guido Krüger and Heiko Hansen. Handbuch der Java-Programmierung. AddisonWesley, München, edition,
.
[KK
Günter Kniesel and Helge Koch. Static Composition of Refactorings. Science of
Computer Programming - Special issue on program transformation, ( – ): – ,
.
]
[KKP+
]
[KN ]
[Knu
David J. Kuck, Robert H. Kuhn, David A. Padua, Bruce Leasure, and Michael J.
Wolfe. Dependence Graphs and Compiler Optimizations. In Proceedings of the ᵗ
Symposium on Principles of Programming Languages. POPL ’ , pages
– ,
.
Gerwin Klein and Tobias Nipkow. A machine-checked model for a Java-like language, virtual machine, and compiler. Transactions on Programming Languages and
Systems (TOPLAS), ( ): – ,
.
]
[Kof ]
Donald E. Knuth. Semantics of context-free languages. Mathematical systems theory, ( ): – ,
.
Jürgen Kofler. Verbinden eines textuellen und eines graphischen Editors bei modellgetriebener Entwicklung am Beispiel der Sprache Refacola. Diplomarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/ps/arbeiten/kofler.shtml,
.
Literaturverzeichnis
[Kre
]
[KS
Marius Kreis. Systematisches Testen von Constraintregeln. Masterarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/
ps/masterarbeit-kreis.pdf,
.
]
Hannes Kegel and Friedrich Steimann. Systematically refactoring inheritance to
delegation in java. In Proceedings of the ᵗ International Conference on Software
Engineering. ICSE ’ , pages
– . ACM,
.
[Kum
]
Vipin Kumar. Algorithms for Constraint Satisfaction Problems: A Survey. AI
Magazine, ( ): – ,
.
[LHR
]
Karl J. Lieberherr, Ian M. Holland, and Arthur J. Riel. Object-oriented programming: an objective sense of style. In Proceedings of the ᵈ Conference on
Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages
– . ACM,
.
[LWL
]
Benjamin Livshits, John Whaley, and Monica S. Lam. Reflection Analysis for Java.
In Proceedings of the ᵈ Asian Conference on Programming Languages and Systems.
APLAS ’ , pages
– . Springer-Verlag,
.
[LY
]
Tim Lindholm and Frank Yellin. The Java Virtual Machine Specification. The Java
series. Addison-Wesley, Reading and MA, edition,
.
[Mai
]
André Mainka. Typconstraints für generische Typen in Refacola. Diplomarbeit, FernUniversität in Hagen,
.
[Mat
]
Aditya Mathur. Mutation Testing. In Encyclopedia of Software Engineering, volume , pages
– . Wiley,
.
[McC
]
Glen McCluskey. Using Java Reflection: http://www.oracle.com/technetwork/
.
articles/java/javareflection-1536171.html,
[MDJ
]
Tom Mens, Serge Demeyer, and Dirk Janssens. Formalising Behaviour Preserving Program Transformations. In Andrea Corradini, Hartmut Ehrig, Hans-Jörg
Kreowski, and Grzegorz Rozenberg, editors, Graph Transformation, volume
of Lecture Notes in Computer Science, pages
– . Springer Berlin Heidelberg,
.
[MF
]
Sanjay Mi al and Brian Falkenhainer. Dynamic Constraint Satisfaction Problems.
In Proceedings of the ᵗ National Conference on Artificial Intelligence. AAAI’ , pages
– ,
.
[MHPB
] Emerson Murphy-Hill, Chris Parnin, and Andrew P. Black. How we refactor,
and how we know it. In Proceedings of the
ᵗ International Conference on Software
Engineering. ICSE ’ , pages
– . IEEE,
.
[MT
]
Tom Mens and Tom Tourwé. A Survey of Software Refactoring. IEEE Transactions
on Software Engineering, ( ): –
,
.
Literaturverzeichnis
[MTR
]
Tom Mens, Gabriele Taen er, and Olga Runge. Analysing refactoring dependencies using graph transformation. Software & Systems Modeling, ( ): – ,
.
[Mül ]
Andreas Müller. Bytecode Analysis for Checking Java Access Modifiers. In Proceedings of the Work-in-Progress Session at the ᵗ International Conference on the Principles and Practice of Programming in Java (PPPJ’ ),
.
[Mül ]
Erland Müller. Ein Algorithmus zur systematischen Constrainterzeugung für die constraintbasierte Refaktorisierung. Diplomarbeit, FernUniversität in Hagen, http://
www.fernuni-hagen.de/imperia/md/content/ps/masterarbeit-mueller.pdf,
.
[MvEDJ ] Tom Mens, Niels van Eetvelde, Serge Demeyer, and Dirk Janssens. Formalizing
Refactorings with Graph Transformations. Journal of Software Maintenance and Evolution: Research and Practice, ( ): – ,
.
[Nao
]
Naoyuki Tamura. Cream: Class Library for Constraint Programming in Java:
http://bach.istc.kobe-u.ac.jp/cream/,
.
[NBE
]
NetBeans IDE . : http://netbeans.org/,
.
[NHN ]
Flemming Nielson, Chris Hankin, and Hanne Nielson. Principles of program analysis: With tables. Springer, Berlin [u.a.],
.
[Nic ]
Sven Nicolai. Postprocessing zur Qualifizierung von Namen: als Ergänzung des constraintbasierten Refaktorisierungsframeworks REFACOLA für die Programmiersprache
Java. Diplomarbeit, FernUniversität in Hagen,
.
[NO
]
Tobias Nipkow and David von Oheimb. Javalight is type-safe—definitely. In
Proceedings of the ᵗ Symposium on Principles of Programming Languages. POPL ’ ,
pages
– . ACM,
.
[Ohe
]
David von Oheimb. Analyzing Java in Isabelle/HOL: Formalization, Type Safety and
Hoare Logic. Dissertation, Technische Universität München,
.
[ON ]
David Oheimb and Tobias Nipkow. Machine-Checking the Java Specification:
Proving Type-Safety. In Jim Alves-Foss, editor, Formal Syntax and Semantics of
Java, volume
of Lecture Notes in Computer Science, pages
– . Springer
Berlin Heidelberg,
.
[Opd
]
William F. Opdyke. Refactoring object-oriented frameworks. Dissertation, University
of Illinois at Urbana-Champaign,
.
[Pal ]
Jens Palsberg. Closure Analysis in Constraint Form. Transactions on Programming
Languages and Systems (TOPLAS), ( ): – ,
.
[PS ]
Jens Palsberg and Michael I. Schwar bach. Object-oriented type systems. Wiley,
.
Literaturverzeichnis
[PUTS
]
Jens von Pilgrim, Bastian Ulke, Andreas Thies, and Friedrich Steimann. Model/Code Co-Refactoring: An MDE Approach. In Proceedings of the ᵗ International
Conference on Automated Software Engineering. ASE’ , pages
– . ACM,
.
[RBJ
]
Don Roberts, John Brant, and Ralph Johnson. A Refactoring Tool for Smalltalk.
Theory and Practice of Object Systems, ( ): – ,
.
[REF
]
The Reflection API: http://docs.oracle.com/javase/tutorial/reflect/
index.html,
.
[RH
]
Soroush Radpour and Laurie Hendren. Refactoring MATLAB. Technical Report
SABLE-TR- , Sable Research Group, School of Computer Science, McGill
University, Montréal and Québec and Canada,
.
[RHS
]
Soroush Radpour, Laurie Hendren, and Max Schäfer. Refactoring MATLAB. In
Compiler Construction, volume
of Lecture Notes in Computer Science, pages
– . Springer Berlin Heidelberg,
.
[Rob
]
Donald B. Roberts. Practical analysis for refactoring. Dissertation, University of
Illinois at Urbana-Champaign,
.
[Rok
]
Sosic Rok. The Many Faces of Introspection. Dissertation, University of Utah,
[Roz
]
Grzegorz Rozenberg. Handbook of graph grammars and computing by graph transformation. World Scientific, Singapore and New Jersey,
.
[Sch
]
Norbert Schirmer. Analysing the Java package-access concepts in Isabelle-HOL:
Research Articles. Concurrency and Computation: Practice & Experience - Formal
Techniques for Java-like Programs, ( ): – ,
.
[Sch
]
Max Schäfer. Specification, Implementation and Verification of Refactorings. Dissertation, Oxford University,
.
[SDS+
]
.
Max Schäfer, Julian Dolby, Manu Sridharan, Emina Torlak, and Frank Tip. Correct
refactoring of concurrent java code. In Proceedings of the ᵗ European Conference
on Object-Oriented Programming. ECOOP ’ , pages
– . Springer,
.
[SEM
]
Max Schäfer, Torbjörn Ekman, and Oege de Moor. Sound and Extensible Renaming for Java. In Proceedings of the
ᵈ Conference on Object-oriented Programming
Systems Languages and Applications (OOPSLA’ ), pages
– ,
.
[SGM
]
Gustavo Soares, Rohit Gheyi, and Tiago Massoni. Automated Behavioral Testing
of Refactoring Engines. IEEE Transactions on Software Engineering, ( ):
– ,
.
[SKP
]
Friedrich Steimann, Christian Kollee, and Jens von Pilgrim. A Refactoring Constraint Language and its Application to Eiffel. In Proceedings of the ᵗ European
Conference on Object-Oriented Programming. ECOOP ’ , pages
– . SpringerVerlag,
.
Literaturverzeichnis
[SM
]
Friedrich Steimann and Philip Mayer. Type Access Analysis: Towards Informed
Interface Design. Journal of Object Technology, ( ): – ,
.
[SM
]
Max Schäfer and Oege de Moor. Specifying and implementing refactorings. In
Proceedings of the ᵗ Conference on Object-oriented programing, systems, languages,
and applications. OOPSLA’ , pages
– . ACM,
.
[SMG ]
Gustavo Soares, Melina Mongiovi, and Rohit Gheyi. Identifying overly strong
conditions in refactoring implementations. In Proceedings of the ᵗ Conference on
Software Maintenance. ICSM ’ , pages
– . IEEE,
.
[SP
]
Friedrich Steimann and Jens von Pilgrim. Refactorings without names. Technical
Report (unpublished), FernUniversität in Hagen,
.
[SP
a]
Friedrich Steimann and Jens von Pilgrim. Constraint-Based Refactoring with
Foresight. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages
– . Springer,
.
[SP
b]
Friedrich Steimann and Jens von Pilgrim. Refactorings without names. In Proceedings of the ᵗ International Conference on Automated Software Engineering. ASE’ ,
pages
– . ACM,
.
[Spe ]
Hermine Spengler. Constraintbasiertes Refaktorisieren von Identifiern in Java. Bachelorarbeit, FernUniversität in Hagen,
.
[SR
Jason Sawin and Atanas Rountev. Improving Static Resolution of Dynamic Class
Loading in Java Using Dynamically Gathered Environment Information. Automated Software Engineering, ( ): – ,
.
]
[SRK ]
[SSA+
Daniel Speicher, Tobias Rho, and Günter Kniesel. JTransformer - Eine logikbasierte Infrastruktur zur Codeanalyse. Softwaretechnik-Trends, ( ),
.
]
Hesam Samimi, Max Schäfer, Shay Ar i, Todd Millstein, Frank Tip, and Laurie
Hendren. Automated Repair of HTML Generation Errors in PHP Applications
Using String Constraint Solving. In Proceedings of the ᵗ International Conference
on Software Engineering. ICSE ’ , pages
– ,
.
[ST
]
Friedrich Steimann and Andreas Thies. From Public to Private to Absent: Refactoring Java Programs under Constrained Accessibility. In Proceedings of the
ᵈ
European Conference on Object-Oriented Programming. ECOOP ’ , pages
– .
Springer,
.
[ST
]
Friedrich Steimann and Andreas Thies. From behaviour preservation to behaviour modification: Constraint-based mutant generation. In Proceedings of the
ᵈ
International Conference on Software Engineering. ICSE ’ , pages
– . ACM,
.
[STD ]
Bjorn de Su er, Frank Tip, and Julian Dolby. Customization of Java Library Classes using Type Constraints and Profile Information. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages
– ,
.
Literaturverzeichnis
[Ste
]
Friedrich Steimann. The Infer Type Refactoring and its Use for Interface-Based
Programming. Journal of Object Technology, ( ): – ,
.
[Ste
]
Friedrich Steimann. Constraint-Based Model Refactoring. In Proceedings of the
ᵗ International Conference on Model Driven Engineering Languages and Systems.
MODELS’ , pages
– . Springer,
.
[Ste
]
Friedrich Steimann. From well-formedness to meaning preservation: model refactoring for almost free. Software & Systems Modeling, to appear,
.
[Sto
]
Frieder Stolzenburg. Membership-Constraints and Complexity in Logic Programming with Sets. In Frontiers of Combining Systems. FroCoS ’ , volume , pages
– . Springer Netherlands,
.
[STST
[SU
]
Max Schäfer, Andreas Thies, Friedrich Steimann, and Frank Tip. A Comprehensive Approach to Naming and Accessibility in Refactoring Java Programs. IEEE
Transactions on Software Engineering, ( ):
–
,
.
]
Friedrich Steimann and Bastian Ulke. Generic Model Assist. In Proceedings of
the ᵗ International Conference on Model Driven Engineering Languages and Systems.
MODELS’ , volume to appear,
.
[SVEM
] Max Schäfer, Mathieu Verbaere, Torbjörn Ekman, and Oege de Moor. Stepping
Stones over the Refactoring Rubicon. In Proceedings of the ᵈ European Conference
on Object-Oriented Programming. ECOOP ’ , pages
– . Springer,
.
[TB
]
Andreas Thies and Eric Bodden. RefaFlex: Safer Refactorings for Reflective Java
Programs. In Proceedings of the
International Symposium on Software Testing
and Analysis, ISSTA
, pages – , New York and NY and USA,
. ACM.
[Tei
]
Warren Teitelman. Automated programmering: the programmer’s assistant. In
Proceedings of the December - ,
, fall joint computer conference, part II (AFIPS
’ ), pages
– , New York and NY and USA,
. ACM.
[TFK+
[Tip
]
]
[TKB
[TR
Frank Tip. Refactoring Using Type Constraints. In Hanne Riis Nielson and Gilberto Filé, editors, Proceedings of the ᵗ International Symposium on Static Analysis.
SAS’ , volume
of Lecture Notes in Computer Science, pages – . Springer,
.
]
]
Frank Tip, Robert M. Fuhrer, Adam Kieżun, Michael D. Ernst, I ai Balaban, and
Bjorn de Su er. Refactoring using type constraints. Transactions on Programming
Languages and Systems (TOPLAS), ( ): : – : ,
.
Frank Tip, Adam Kiezun, and Dirk Bäumer. Refactoring for generalization using
type constraints. In Proceedings of the ᵗ Conference on Object-oriented programing,
systems, languages, and applications. OOPSLA’ , pages – . ACM,
.
Andreas Thies and Christian Roth. Recommending Rename Refactorings. In Proceedings of the ᵈ International Workshop on Recommendation Systems for Software
Engineering (RSSE ’ ), pages – , New York and NY and USA,
. ACM.
Literaturverzeichnis
[TS
[Tsa
]
Andreas Thies and Friedrich Steimann. Systematic Testing of Refactoring Tools.
In Poster exhibition at the ᵗ Workshop on Automation of Software Test. AST’ ,
.
]
Edward Tsang. Foundations of Constraint Satisfaction. Academic Press,
.
[Ull ]
Christian Ullenboom. Java ist auch eine Insel: Das umfassende Handbuch. Galileo
Press, edition,
.
[VCN+
] Mohsen Vakilian, Nicholas Chen, Stas Negara, Balaji A. Rajkumar, Brian P. Bailey, and Ralph E. Johnson. Use, Disuse, and Misuse of Automated Refactorings.
In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ ,
pages
– ,
.
[vHSD
]
[VKDL
] Angi Voß, Werner Karbach, Uwe Drouven, and Darius Lorek. Competence Assessment in Configuration Tasks. In European Conference on Artificial Intelligence.
(ECAI’ ), pages
– ,
.
Pascal van Hentenryck, Helmut Simonis, and Mehmet Dincbas. Constraint
Satisfaction Using Constraint Logic Programming. Artificial Intelligence, ( ):
– ,
.
[Wag ]
Mirko Wagner. Sprachübergreifendes Java-XML-Refaktorisieren mit Refacola. Bachelorarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/
.
md/content/ps/bachelorarbeit-wagner.pdf,
[Wil ]
David S. Wile. Abstract syntax from concrete syntax. In Proceedings of the ᵗ
International Conference on Software Engineering. ICSE ’ , pages
– . ACM,
.
[WSTL ] Jan Wielemaker, Tom Schrijvers, Markus Triska, and Torbjörn Lager. SWI-Prolog.
Theory and Practice of Logic Programming, ( - ): – ,
.
Index
private,
protected,
public,
Abfrage-Engine,
Abhängigkeiten, , , ,
abstract semantic graph, siehe abstrakter Semantikgraph
abstract syntax tree, siehe abstrakter Syntaxbaum
abstrakter Semantikgraph,
abstrakter Syntaxbaum,
access modifier, siehe Zugrei arkeitsmodifizierer
allowed changes, siehe erlaubte Änderungen
ambigous, siehe Mehrdeutigkeit
angereicherter abstrakter Syntaxbaum, ,
Annahme zur Weltabgeschlossenheit,
Art,
AST, siehe abstrakter Syntaxbaum
Aufzählungsdomäne, ,
geordnete,
partiell geordnete,
ungeordnete,
Ausdruck,
Ausnahme,
Bedingung, ,
Bedingungserfüllungsproblem, ,
dynamisches,
endliches,
beschränkt parametrischer Polymorphismus,
bounded polymorphism, siehe beschränkt parametrischer Polymorphismus
bounded type variables, siehe Schranken von
Typvariablen
bounded wildcards, siehe Schranken von Wildcards
CA,
–
CACL,
CACT,
CI,
CL,
Class Instance Creation Expression, ,
class literal,
closed world assumption, siehe Annahme zur
Weltabgeschlossenheit
constant evaluation, siehe konstante Auswertung
Constraint
bedingtes,
Constraint, siehe Bedingung
constraint, siehe Bedingung
constraint generation rule, siehe Constraintregel
constraint rule, siehe Constraintregel
constraint satisfaction problem, siehe Bedingungserfüllungsproblem
constraint solver, siehe Constraintlöser
constraint variable, siehe Constraintvariable
Constraintlöser,
Constraintlösung,
Constraintregel, ,
Constraintsystem, siehe Bedingungserfüllungsproblem
Constraintvariable, , ,
Constructor Invocation,
CSP, siehe Bedingungserfüllungsproblem
CT,
Index
default-Zugrei arkeit,
dependencies, siehe Abhängigkeiten
domain, siehe Domäne
Domäne, ,
early evaluation, siehe frühzeitige Auswertung
Einschränkungen
Übermäßig starke,
,
enumeration domain, siehe Aufzählungsdomäne
Erklärungskomponente,
erlaubte Änderungen, , ,
exception, siehe Ausnahme
explizite Typumwandlung,
expression, siehe Ausdruck
Extreme Programming,
fact base, siehe Faktenbasis
fact generation, siehe Faktengenerierung
facts, siehe Fakten
Fakten, , ,
Faktenart,
Faktenbasis,
Faktengenerierung,
Faktenlieferant,
foresight, siehe Vorausschau
frühzeitige Auswertung,
Geltungsbereich,
geschachtelter Typ,
,
hiding, siehe Verdecken
indirection, siehe Indirektion
Indirektion,
Inferenzregeln,
initial value, siehe initialer Wert
initialer Wert,
Inkonsistenz,
inner class, siehe innere Klasse
innere Klasse,
interne API,
intersection type,
kind, siehe Art
Konklusion, ,
konstante Auswertung,
Leerraum, ,
locked bindings,
,
Lösung, siehe Constraintlösung
Mehrdeutigkeit, ,
member type, siehe Membertyp
Membertyp,
Methodenparameter mit variabler Stelligkeit,
Methodenüberladung,
Mutation,
Mutationstesten,
,
Nachbarschaftssuche,
Nachbedingung,
Nachrichtenversand,
Name
kanonischer,
voll qualifizierter,
neighborhood search, siehe Nachbarschaftssuche
nested type, siehe geschachtelter Typ
–
NOC,
Obscuring, siehe Verdunkelung
offene Domäne,
overloading, siehe Überladen
postcondition, siehe Nachbedingung
precondition, siehe Vorbedingung
primary type, siehe primärer Typ
primärer Typ,
program dependence graph, siehe Programmabhängigkeitsgraph
program element, siehe Programmelement
program-dependent domain, siehe programmabhängige Domäne
Programm
gültiges, ,
Programmabfrage, ,
programmabhängige Domäne, , ,
Programmabhängigkeitsgraph,
Programmanalyse
dynamische,
statische, ,
Programmelement, ,
Index
Programmfakten, siehe Fakten
Programmierung
introspektive,
Programmierwerkzeug,
Programmtransformation,
Programmtransformationen,
property, siehe Variablenart
Prämisse, ,
subkind, siehe Unterart
query, siehe Programmabfrage
quick fix, ,
Überladen,
Unterart,
raw types,
Refacola, , ,
refactoring, siehe Refaktorisierung
Refactoring Browser,
refactoring tool, siehe Refaktorisierungswerkzeug
Refactoring Tool Tester, ,
Refaktorisierung, ,
constraintbasierte,
Refaktorisierungs-Adapter,
Refaktorisierungswerkzeug,
reference construction, siehe Referenzkonstruktion
Referenzkonstruktion,
Reflection-API, ,
reflektive Referenz,
Regelsa ,
Regelumschreiben,
Regelvariable, ,
ungebundene,
rule rewriting, siehe Regelumschreiben
runtime exception,
Rückschreibekomponente, ,
Variable, ,
unveränderlich,
variable arity parameter, siehe Methodenparameter mit variabler Stelligkeit
Variablenart, ,
Variablengenerierung,
Verdecken, ,
,
Verdecken von Feldern,
Verdunkelung,
Verscha en, ,
visibility, siehe Sichtbarkeit
Vorausschau, , ,
Vorbedingung,
Schranken von Typvariablen,
Schranken von Wildcards,
scope, siehe Geltungsbereich
Seiteneffekt,
Sequenz, ,
shadowing, siehe Verscha en
Sichtbarkeit,
Sprach-API,
streng typisiert,
strongly typed, siehe streng typisiert
TamiFlex,
Typ
primär,
type cast, siehe explizite Typumwandlung
Typparameter,
Typvariable,
white spaces, siehe Leerraum
Zugrei arkeit, ,
Zugrei arkeitsmodifizierer,
Zugrei arkeitsstufe,
,
Lebenslauf
Berufliche Tätigkeiten
seit
/
/
Anwendungsentwickler
Novomind AG
-
/
Wintersemester
und Sommersemester
Wissenschaftlicher Mitarbeiter
FernUniversität in Hagen
Lehrgebiet Programmiersysteme
Studentische Hilfskraft
Universität Bonn
Institut für Informatik I und II
Ausbildung
seit
/
Promotionsstudium zum Dr. rer. nat.
/
Abschluss als Diplom-Informatiker
Universität Bonn
Nebenfach: Jura (Strafrecht)
/
–
/
/
Studium der Informatik
Universität Bonn
Erwerb der allgemeinen Hochschulreife
–
Besuch des Gymnasiums Blankenese
Freie und Hansestadt Hamburg
Wehrpflicht
/
–
/
Wehrersa dienst
Caritasverband Leverkusen
Häusliche Krankenpflege
Persönliche Angaben
Geburtsdatum
Geburtsort
Staatsangehörigkeit
. März
Freie und Hansestadt Hamburg
Deutsch
Familienstand
Verheiratet