Implementierung objektorientierter Programmiersprachen Eine Mitschrift der Vorlesung aus dem Wintersemester 2003/2004 Gehalten von PD Dr. Wolfgang Goerigk1 Institut für Informatik und Praktische Mathematik Christian-Albrechts-Universität zu Kiel Olshausenstr. 40, D-24098 Kiel angefertigt von Herrn cand. inform. Eike Schulz Kiel, den 24.7.2004 1 Telefon: +49-431-880-7274, Fax: -7613, Email: [email protected] 1 Vorwort Die vorliegende Vorlesungsmitschrift entstammt einer vierstündigen Vorlesung mit zweistündigen Übungen, die ich im Wintersemester 2003/04 an der Universität Kiel zum Thema Implementierung objektorientierter Programmiersprachen gehalten habe, einer Vorlesung f ür Studierende der Informatik im Hauptstudium. Am Beispiel der weit verbreiteten objektorientierten Sprache Java wurden (sequentielle) objektorientierte Programme zunächst als klassische imperative Programme mit zeigerreferenzierten dynamischen Datentypen eingeführt. Auf dieser Basis lassen sich die Konzepte der Objektorientierung wie Vererbung, Nachrichtenaustausch (message passing), Kapselung usw. untersuchen und erklären. Hier bildet zunächst die statische Semantik einen deutlichen Schwerpunkt. Typen, Typ-Subtyp-Beziehungen, statischer Typ und dynamischer Typ (Klasse) eines Objektes, der Vererbungsgraph sowie die Durchführung der Vererbung (Finalisieren der Klassen) zur Bestimmung der Komponenten der Klassen sind die zentralen Begriffe. Überdecken (statische Bindung) bei Instanzvariablen und Überschreiben (dynamische Bindung, late binding) bei Methoden sind dabei für Java kennzeichnend. Die dynamische Semantik läßt sich dann wie die klassischer imperativer Programme als Zustandstransformation verstehen. Zur Implementierung von objektorientierten Sprachen, zum Beispiel von Java, werden häufig virtuelle Maschinen benutzt, zum Beispiel die Java Virtual Machine (JVM). Dabei werden Programme zunächst in virtuellen Maschinencode transformiert (kompiliert), der dann durch eine Implementierung der virtuellen Maschine (einen Interpreter) ausgeführt wird. Die JVM, ihr Maschinencode und das Binärformat (classfile format), auch Bytecode-Verifikation zur Überprüfung der Gutartigkeit“ von JVM-Code, und die Kompilation von Java-Programmen in den Code der ” JVM bildeten den Schwerpunkt des zweiten Teils der Vorlesung und auch der Übungen, in denen schließlich ein Übersetzer eines Ausschnitts von Java in konkreten und ausführbaren JVM-Code entwickelt und in Java implementiert wurde. Aus Sicherheitssicht adäquat lässt JVM-basierte interpretative Ausführung von Programmen doch hinsichtlich der Effizienz Wünsche offen. Der dritte Teil der Vorlesung behandelte deshalb alternative Implementierungstechniken, zunächst die inkrementelle Kompilation (JIT, just-intime-Kompilation) des virtuellen Maschinencodes in den Maschinencode der ausf ührenden Plattform. Der Übergang des Stack-Maschinencodes der JVM in den durch Datenflussanalysen stark optimierbaren Register-Code heutiger Plattformen verspricht gehörigen Laufzeitgewinn. Eine weitere Alternative ist die Implementierung durch Übersetzung der Java-Quellprogramme in höhere imperative Sprachen an (z.B. nach C oder auch nach Pascal oder Modula 2). Die vorliegende Vorlesungsmitschrift, die für mich einen ersten Schritt zur Ausarbeitung eines ausführlichen Skriptes zur Vorlesung darstellt, entstammt bis auf dieses Vorwort allein der Feder von Herrn cand. inform. Eike Schulz. Für sein eigenständiges Engagement, für die viele mühevolle Arbeit und für sein Einverständnis, diese Mitschrift anderen, auch mir selbst, zur Verfügung zu stellen, möchte ich mich an dieser Stelle sehr herzlich bedanken. Wolfgang Goerigk 2 Inhaltsverzeichnis 1 Einführung 1.1 Objektorientierte Sprachen . . . . . . . . . . . . . . . 1.2 Propagierte Vorzüge . . . . . . . . . . . . . . . . . . . 1.3 Aus den Java-Werbetexten . . . . . . . . . . . . . . . 1.4 Grundideen und Begriffliches, Implementierungsideen . 1.4.1 Objekte . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Klassen . . . . . . . . . . . . . . . . . . . . . . 1.4.3 Vererbung . . . . . . . . . . . . . . . . . . . . . 1.4.4 Implementierungsideen für Methodenaufrufe . . 1.4.5 Zusammenfassendes . . . . . . . . . . . . . . . 2 Java als Programmiersprache 2.1 Der imperative Kern . . . . . . . . . . . . . . . . . . . 2.1.1 Datentypen . . . . . . . . . . . . . . . . . . . . 2.1.2 Kontrollstrukturen . . . . . . . . . . . . . . . . 2.2 Implementierungsbemerkungen zum imperativen Kern 2.2.1 Operatoren . . . . . . . . . . . . . . . . . . . . 2.2.2 Zuweisungen . . . . . . . . . . . . . . . . . . . 2.2.3 Variablendeklarationen . . . . . . . . . . . . . . 2.2.4 Blöcke . . . . . . . . . . . . . . . . . . . . . . . 2.2.5 Bedingte Anweisungen . . . . . . . . . . . . . . 2.2.6 Endliche Fallunterscheidung (switch) . . . . . 2.2.7 Schleifen . . . . . . . . . . . . . . . . . . . . . . 2.2.8 Funktionen und/oder Prozeduren . . . . . . . . 2.2.9 Zusammenfassendes und Beispiel . . . . . . . . 2.3 Zeigerreferenzierte Daten . . . . . . . . . . . . . . . . 2.3.1 Referenzdatentypen . . . . . . . . . . . . . . . 2.3.2 Arrays . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Strings . . . . . . . . . . . . . . . . . . . . . . . 2.3.4 Implementierung . . . . . . . . . . . . . . . . . 2.4 Objekte, Klassen, Methoden, Konstruktoren . . . . . . 2.4.1 Klassen und Objekte . . . . . . . . . . . . . . . 2.4.2 Überladen von Methoden . . . . . . . . . . . . 2.4.3 Signaturen . . . . . . . . . . . . . . . . . . . . 2.4.4 Statische Variablen . . . . . . . . . . . . . . . . 2.4.5 Statische und Instanzinitialisierer . . . . . . . . 2.4.6 Modifikatoren . . . . . . . . . . . . . . . . . . . 2.4.7 Ein Ausflug nach Modula-2 . . . . . . . . . . . 2.4.8 Zugriff auf Komponenten der Superklasse(n) . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 7 8 8 9 9 11 12 . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 15 16 19 19 20 21 21 21 22 23 25 28 29 29 30 31 31 32 32 32 32 33 33 34 34 34 4 INHALTSVERZEICHNIS 2.4.9 Typanpassung (Casting) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.10 Überschreiben von Methoden und Überdecken von Instanzvariablen . . . 3 Mini-Java Programme 3.1 Abstrakte Syntax . . . . . . . . . . . . . . . . . . . . 3.1.1 Definitionen . . . . . . . . . . . . . . . . . . . 3.2 Statisch semantische (kontextsensitive) Information . 3.2.1 Typkonvertierung (casting) . . . . . . . . . . 3.2.2 Resultattypänderungen . . . . . . . . . . . . 3.3 Durchführung der Vererbung . . . . . . . . . . . . . 3.4 Signaturen von Methoden (gemäß JVM) . . . . . . . 3.5 Typisierung von Ausdrücken . . . . . . . . . . . . . . 3.6 Wohlgeformtheit von Programmen . . . . . . . . . . 3.6.1 Definition . . . . . . . . . . . . . . . . . . . . 3.7 Wohlgeformtheit von Anweisungen und Ausdrücken 35 36 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 40 40 41 42 42 43 44 45 45 46 4 Eine virtuelle Mini-Java-Maschine 4.1 Struktur und Inhalt von Klassendaten . . . . . . . . . 4.1.1 Field-Deskriptoren . . . . . . . . . . . . . . . . 4.1.2 Methoden-Deskriptoren . . . . . . . . . . . . . 4.1.3 NameAndType-Deskriptoren . . . . . . . . . . 4.1.4 Long- und Double-Einträge . . . . . . . . . . . 4.1.5 Attribute . . . . . . . . . . . . . . . . . . . . . 4.2 Bytecode von Klassendateien . . . . . . . . . . . . . . 4.2.1 Ein Beispiel . . . . . . . . . . . . . . . . . . . . 4.2.2 Fehlende Konstantenpool-Einträge . . . . . . . 4.3 ACCESS-Flags . . . . . . . . . . . . . . . . . . . . . . 4.4 JVM-Konfiguration . . . . . . . . . . . . . . . . . . . . 4.5 Zusammenfassung des JVM-Instruktionssatzes . . . . 4.5.1 Maschinentypen vs. Java-Typen . . . . . . . . 4.5.2 Lade- und Speicherinstruktionen . . . . . . . . 4.5.3 Arithmetische Operationen . . . . . . . . . . . 4.5.4 Typkonversionen . . . . . . . . . . . . . . . . . 4.5.5 Objekterzeugung und -manipulation . . . . . . 4.5.6 Stack-Manipulation . . . . . . . . . . . . . . . 4.5.7 Sprungbefehle . . . . . . . . . . . . . . . . . . . 4.5.8 Methodenaufrufe . . . . . . . . . . . . . . . . . 4.6 Beispiele von Operationscode-Beschreibungen . . . . . 4.7 Wohlgeformtheit von Klassendateien . . . . . . . . . . 4.7.1 Statische Bedingungen an Codefelder . . . . . . 4.7.2 Strukturelle Bedingungen . . . . . . . . . . . . 4.8 Verifikation von Klassendateien (Bytecode Verifier) . . 4.8.1 Motivation . . . . . . . . . . . . . . . . . . . . 4.8.2 Bytecode-Verifikation zur Ladezeit (+Laufzeit) 4.8.3 4 Phasen” der Bytecode-Verifikation . . . . . ” 4.9 Übersetzung von Mini-Java nach JVM-Code . . . . . . 4.9.1 Übersetzung von Klassen . . . . . . . . . . . . 4.9.2 Übersetzung von Methodenrümpfen . . . . . . 4.9.3 Übersetzung von Ausdrücken . . . . . . . . . . 4.10 Übersetzungsspezifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 50 51 51 51 51 52 52 54 55 55 56 56 56 57 57 57 58 58 58 58 60 60 60 60 60 61 61 62 62 63 68 70 INHALTSVERZEICHNIS 5 4.10.1 Übersetzungsspezifikation (Definition) . . . . . . . . . . 4.10.2 Übersetzungsspezifikation (Mini-Java → JVM, Auszüge) 4.11 Just-in-Time-Kompilation . . . . . . . . . . . . . . . . . . . . . 4.11.1 Typische Architektur eines JIT-Compilers . . . . . . . . 4.11.2 Adaptive Kompilation . . . . . . . . . . . . . . . . . . . 5 Übersetzung in höheren Quellcode 5.1 Grundlegende Ideen . . . . . . . . . . . . . . . . . 5.1.1 Zusammenfassendes . . . . . . . . . . . . . 5.2 Übersetzung von Mini-Java nach Modula-2 . . . . 5.2.1 Erzeugen der Klassenobjekte und Instanzen 5.2.2 Methoden . . . . . . . . . . . . . . . . . . . 5.2.3 Abschließende Bemerkungen . . . . . . . . A Hilfreiche Internetadressen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 72 74 75 76 . . . . . . 77 77 79 83 84 85 85 87 6 INHALTSVERZEICHNIS Kapitel 1 Einführung 1.1 Objektorientierte Sprachen • Simula 67 (seit 1967, O.J.Dahl et al.) [strenges Typkonzept] • Smalltalk (seit 1971; seit 1980 Goldberg/Robsen: Smalltalk 80) [Smalltalk hat kein Typkonzept ( schwach getypt”), Smalltalk 80 ist streng getypt] ” • C++ (seit etwa 1986, Stroustrup) • Common Lisp Object System ( CLOS”, seit 1988, Bobrow et al.) ” • Java (seit 1990/91, Gosling/Joy) Sun Microsystems, Oak”, Green Project, interaktives ” Fernsehen; Durchbruch in der Anwendungsprogrammierung etwa 1995 (Sunworld 95) → HotJava (Web-Browser mit integrierten Java-Applets) 1.2 Propagierte Vorzüge • Realitätsnahe und durchgängige softwaretechnische Modellierung von Entitäten und Beziehungen in einem Anwendungsbereich (Brockhaus: Objekt ist Gegenstand des Erkennens, ” Denkens, Handelns” ↔ im Gegensatz zu Subjekt) • Klassen und Objekte in Analyse, Design und Implementierung • Realitätsnähe durch direkte Abstraktion von den Objekten des Denkens ( was gehört alles ” zu einer Vorlesung? . . .”) • Man erhofft sich: ◦ erhöhte Zuverlässigkeit (Korrektheit und Robustheit) ◦ Flexibilität und Wiederverwendbarkeit (Wiederbenutzen, Anpassen, Adaptieren, . . .) O.Madsen (1990): Die Grundphilosophie der objektorientierten Programmierung ist, daß Programme so weit wie möglich den Teil der Realität wiederspiegeln, den sie bearbeiten. 1.3 Aus den Java-Werbetexten • Java ist objektorientierte Programmiersprache im Stile von C++, vereinfacht und ergänzt um die Unterstützung verteilter Resourcen in Netzen; 7 8 KAPITEL 1. EINFÜHRUNG • eignet sich für verteilte parallele Anwendungen in heterogenen Rechnernetzen (Internet, WWW), in denen verschiedene Plattformen lose gekoppelt kommunizieren; • übersetzter Java-Code ist architekturneutral (Bytecode): Java-Programm - Bytecode (einer abstrakten, virtuellen Maschine) I Übersetzer Kompakt und gut” über Netze zu kommunizieren ” Wird zur Laufzeit interpretiert (Bytecode-Interpreter, JVM) Effizient → Just-in-Time Kompilation (JIT) [Bei Verwendung eines Just-In-Time Compilers wird vor der Ausführung einer Methode der Bytecode in den nativen Code der Zielplattform compiliert und dann als native Methode ausgeführt.] • umfangreiche Bibliothek u.a. zur Programmierung von TCP/IP-Protokollen (ftp, http, . . .) • Nutzung von Resourcen im Netz (URL’s, uniform resource locator) • ausgefeilte Sicherheitsmechanismen ◦ automatische Speicherverwaltung (verhindert Programmierfehler) ◦ Typsicherheit (Operationen zur Laufzeit nur auf korrektem Typ und unter Einhaltung von Zugriffsrechten) ◦ Security-Manager (kontrolliert sicherheitskritische Operationen) ◦ Bytecode-Verifier (überprüft Sicherheitseigenschaften) 1.4 1.4.1 Grundideen und Begriffliches, Implementierungsideen Objekte • haben Identität und Eigenschaften; • haben einen Zustand (Eigenschaften können sich ändern); • werden i.a. dynamisch erzeugt, haben eine Lebensdauer, verlieren aber ihre Identität nicht; • können sich nicht gleichzeitig an zwei verschiedenen Orten aufhalten; • jedoch kann von verschiedenen Stellen aus auf sie verwiesen werden (Objekte unterscheiden sich von üblichen mathematischen Objekten wie Zahlen, Mengen, Relationen, Funktionen, ...). Implementierungsidee: Zeigerreferenzierte Verbunde (Records): 1.4. GRUNDIDEEN UND BEGRIFFLICHES, IMPLEMENTIERUNGSIDEEN Auto: Farbe: - Objekt1: Hersteller Typ Farbe #Räder #Zylinder 9 Auto Daimler 200D 4 4 - Name R G B Farbe Schwarz 0 0 0 Wichtig: Objekte sind Daten, keine Variablen. Objekte sind Instanzen von Klassen, d.h. Daten eines durch die zugehörige Klassendefinition festgelegten Typs. Beispiel: Das Datum #Räder eines Objektes meinAuto vom Typ Auto kann durch Zuweisungen verändert werden: meinAuto.#Räder = 3 ( meinAuto^.#Räder := 3 Java-Notation Pascal-Notation ) (Nach der Durchführung hat #Räder den Wert 3). 1.4.2 Klassen • sind Datentypen; • sind konkret oder abstrakt; • definieren Schnittstellen (Interfaces) und Typen; • legen die Struktur (Signatur, relevante Eigenschaften, Merkmale) ihrer Objekte (Instanzen) fest. Merkmale: ◦ Komponenten des Objektzustandes (Instanzvariablen); ◦ Methoden, die durch Nachrichten aufgerufen werden können. 1.4.3 Vererbung • Eine Klasse A kann Subklasse einer (i.a. mehrerer) Klasse(n) B sein (Superklassen). Java-Notation: class A extends B { . . . } • Struktursicht: A erbt” alle Merkmale von B ” ◦ A ist strukturell feiner als B ◦ A ist Spezialisierung von B • Typsicht: A ist Subtyp von B ◦ jedes A ist auch ein B”, d.h. A ⊆ B” der Idee nach. ” ” ◦ Beispiele: Kreise sind spezielle Ellipsen”, Angestellte sind spezielle Personen”, . . . ” ” 10 KAPITEL 1. EINFÜHRUNG • Objekte haben evtl. mehrere Instanzvariablen mit gleichem Namen ( Überdecken von Variablen). • Zu jedem Methodennamen kann es verschiedene Methodendefinitionen geben ( Überschreiben von Methoden, Polymorphie) → late binding”. ” Beispiel: Kreis: real x , y // Mittelpunkt real r // Radius Ellipse: real x, y // Mittelpunkt real r1, r2 // Radien Idee: Koordinaten x, y und Radius r werden von Kreis an Ellipse vererbt (r1→r): Kreis Ellipse Also: A ∼ Ellipse, B ∼ Kreis → keine gute Idee! ( jede Ellipse ist ein Kreis?”) ” 2. Möglichkeit: Kreis: real x, y real r1, r2 // Mittelpunkt // r1 ist Radius Ellipse: real x , y real r1 , r2 // Mittelpunkt // Radien Kreis erbt Mittelpunkt-Koordinaten und Radien von Ellipse: Ellipse Kreis → Strukturell richtig, aber Kreise haben dann zwei Radien ( welches ist der Richtige?”) ” [Was man eigentlich braucht, ist eine Klasseninvariante: r1=r2 (nicht in Java möglich)] Methoden für Umfangsberechnung: Kreis: real umfang () { 2 · π · r1 } Ellipse: √ real umfang () { π · [ 32 · (r1 + r2) − r1 · r2] } 1.4. GRUNDIDEEN UND BEGRIFFLICHES, IMPLEMENTIERUNGSIDEEN 11 • Methodenaufrufe object.nachricht(a1, . . ., an ) 6 K Empfängerobjekt Methodenname Objekte unterschiedlichen Typs (Ellipse, Kreis) können auf eine Nachricht (umfang()) mit verschiedenen Methoden reagieren. Es hängt vom Typ des Empfängerobjektes (object) ab, welche Methode aufgerufen wird (Polymorphismus, Generizität). [Es reicht eigentlich eine Methode aus: nachricht(object, a1 , . . ., an ) → Notwendig hier: generischer1 Auswahlmechanismus, der prüft, um was für ein Objekt es sich handelt → anschließend Verzweigung zur richtigen Methode] 1.4.4 Implementierungsideen für Methodenaufrufe Late binding, d.h. nach der richtigen Methode wird zur Laufzeit gesucht” (Smalltalk 80) oder ” durch Nachschauen in der Klasse des Empfängerobjektes und weiter in den Superklassen gesucht. In JVM: Spezielle Instruktion invoke virtual 1. Suche nach der richtigen Methode m zu obj.m(a 1, . . ., an ) zur Laufzeit in Abhängigkeit von der Klasse von obj. → Induktion über Klassenobjekte und ihre Superklassenlisten → Methodencaching 2. Erzeugen generischer” Funktions- oder Prozeduraufrufe: ” obj.m(a1 , . . ., an ) ⇔ m(obj, a1 , . . ., an ) Und erzeuge procedure m (this, a1 , . . ., an ) { function typecase this A1 : M1 (this, a1 , . . ., an ) . . . Ak : Mk (this, a1 , . . ., an ) } wobei A1 , . . . , Ak alle möglichen Klassen des Empfängerobjektes sind, die eine Methodendefinition zu m haben, und M1 , . . ., Mk sind ohne Methoden. Vorteil: Effizienz. Nachteil: Nur für geschlossene” Programme, d.h. wenn alle Klassen bekannt sind. ” 1 generisch = vom Typ abhängig” ” 12 KAPITEL 1. EINFÜHRUNG 1.4.5 Zusammenfassendes Um ein objektorientiertes Programm zu verstehen, brauchen wir Informationen über die Merkmale der Objekte aller Klassen: • Namen und Typen von Instanzvariablen, • Namen und Signaturen von Methoden. Sind alle Klassen bekannt, dann sind diese Informationen alle statisch zu berechnen → Vererbungsgraph eines Programms. Beispiel (Vererbungsgraph): A x : int B C x : int y : real x : int y : String D x : int y: ? In Java ist der Vererbungsgraph immer ein Baum. Es gibt folglich für jede Klasse einen eindeutigen Pfad zur Wurzel. Eine multiple Vererbung wie für Klasse D gibt es in Java nicht. Der Graph mit dem Bereich unterhalb der gestrichelten Linie (erbt D das y von B oder C?) ist in Java somit nicht möglich. Beispiel (Vererbungskonflikte): Es sei A eine Klasse, die eine Variable x und zwei Methoden f() und m() enthält. f() gibt den Wert von x zurück, m() ruft f() auf. Klasse B enthalte eine Variable x und eine Funktion f(), die den Wert von x zurückgibt. B sei Subklasse von A: A int x=1 f(){x} m(){this.f} B int x=2 f(){x} Welchen Rückgabewert liefert der Befehl new A().m() ? Was liefert new B().m() ? 1.4. GRUNDIDEEN UND BEGRIFFLICHES, IMPLEMENTIERUNGSIDEEN 13 • new A().m() ,→ 1, weil in A die Methode m aufgerufen wird und diese den Rückgabewert von f aus A (siehe Pfeil) liefert. Die Bindung für Instanzvariablen ist statisch (static scoping). • new B().m() ,→ 2, weil m von A an B vererbt wird, in m jedoch nun die Funktion f der Klasse B aufgerufen wird (dynamische Bindung) und diese x aus B zurückgibt (siehe Pfeil). Die Bindung für Methodenidentifikatoren ist dynamisch (late binding). Angenommen, a sei eine Variable vom Typ A, und es erfolgt nach der Erzeugung einer Instanz für a der Aufruf a.m() . Welcher Wert wird von a.m() zurückgegeben? • A a; |{z} . . . ; a.m() ,→ 1 oder 2, weil a auch eine Referenz auf eine Instanz von B sein kann! (∗) Beispielsweise könnte für (∗) stehen: if ( das Wetter ist schön”) { a = new B(); } else { a = new A(); } ” 14 KAPITEL 1. EINFÜHRUNG Kapitel 2 Java als Programmiersprache 2.1 Der imperative Kern Datentypen, Kontrollstrukturen, Implementierung. 2.1.1 Datentypen • Einfache Datentypen: byte short int long boolean char float double a 8-bit 16-bit 32-bit 64-bit (true, false) 16-bit 32-bit 64-bit Vorzeichenbehaftet in Zweierkomplement (Beispiel short: Wertebereich −215 bis 215 − 1) Unicode (zur Darstellung internat. Zeichensätze) IEEEa → Standard für Gleitkommazahlen IEEE - Institute of Electrical and Electronics Engineers” ” • Operatoren: == != <, <=, >, >= +, -, *, / % &, | ! &&, ||, ^ ++, -- gleich ungleich kleiner(-gleich), größer(-gleich) (übliche) arithmetische Operationen modulo (bitweises) UND, ODER NICHT (boolsches) UND, ODER, XOR auf boolschen Operanden Autoinkrement und -dekrement (nur für ganzzahlige Variablen), z.B. x++, ++x, x--, --x • Variablendeklarationen: [<Sichtbarkeit>] [static] <Typ> <Id 1 >[=<Init1>], . . ., <Idn >[=<Initn>]; Beispiele: int i=10; public static String hello="Hello World!"; 15 16 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE int i=10, j, k, l=2+m; Im letzten Beispiel sind j und k vorbelegt mit einem Default-Wert (0). [auf Kosten der Effizienz] • Zuweisungen: <Variable> = <Ausdruck>; Beispiel: Zuweisungsoperator ? i = j++ -3; Der Ausdruck j++ -3 ist rechte Seite/RHS (right hand side) 6 Variable/Platz Zuweisungen sind auch Ausdrücke, Wert ist der Wert der rechten Seite. Beispiele: x = (y = 4);// Wert 4 x = (y++); // Wert von y vor Inkrement 2.1.2 Kontrollstrukturen • Sequentielle Komposition (Hintereinanderausführung): <St1 ><St2 > Sequenzen von Anweisungen (und Deklarationen) werden mit Klammern {} umschlossen. Beispiel: { int i = int j = i = 10; 2 * i; j; i + i; } Lokal in einem Block deklarierte Variablen gelten nur innerhalb des Blocks! • Bedingte Anweisungen: if (<Bedingung>) { <Then-Teil> } [else { <Else-Teil> }] Dabei ist <Bedingung> ein Ausdruck vom Typ boolean, die else-Anweisung ist optional. 2.1. DER IMPERATIVE KERN 17 Beispiel: if (x == 3) { y = x++; } else { y = ++x; } Achtung: Syntaktischer Fall: if (i == 0) if (j == 0) x = 1 else x = 2; Das else gehört jeweils zum innersten if. • Endliche Fallunterscheidung: switch (<Ausdruck>) { case c1 : <Anweisung1> . . . case cn : <Anweisungn> [default : <Anweisung>] } Dabei kann <Ausdruck> vom Typ int, byte, short oder char sein; c 1 , . . . , cn sind Konstanten des entsprechenden Typs. <Ausdruck> wird ausgewertet, dann der Reihe nach mit den c 1 , . . . , cn verglichen. Ist er gleich cj für ein erstes cj , dann werden ab <Anweisungj > alle folgenden Anweisungen ausgeführt. Speziell: Es gibt eine break-Anweisung, die die Ausführung beendet. Beispiel: int x = 2, j; switch (x) { case 1 : j = 10; case 2 : j = 20; case 3 : { j = 25; break; } case 4 : j = 0; default : j++; } • Schleifen: ◦ while (<Bedingung>) { <Anweisungen> } ◦ do { <Anweisungen> } while (<Bedingung>) ◦ for (<Initialisierungen>; [<Bedingung>]; <Abschlußanweisungen>) { <Anweisungen> } Dabei ist <Bedingung> stets vom Typ boolean. 18 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE Beispiel: for (i = 0; i < 10; i++) { x = x+1; } P berechnet n−1 = 45 + x i=0 i + x |{z} n=10 Bemerkung: Die for-Schleife ist eine spezielle while-Schleife. Dieses wird durch das folgende Flußdiagramm verdeutlicht: ? <Initialisierung> ? - <Bed> + ? <Anweisungen> ? <Abschlußanweisungen> ? Das Diagramm beschreibt die Anweisung <Initialisierung>; while (<Bed>) { <Anweisungen>; <Abschlußanweisungen>; } welche einer for-Schleife der o.g. Form entspricht. • Funktionen und Prozeduren: ◦ ◦ ◦ ◦ ◦ ◦ Statische Methoden; Prozedur, falls Resultattyp void ist; Aufrufe von Funktionen in Ausdrücken; Aufrufe von Prozeduren als Anweisungen; Funktionsaufrufe als Anweisungen ignorieren den Resultatwert; In Rümpfen gibt es die spezielle Anweisung return [<Ausdruck>] ◦ Syntaktisch sind Methodendefinitionen nur in Klassendefinitionen erlaubt. 2.2. IMPLEMENTIERUNGSBEMERKUNGEN ZUM IMPERATIVEN KERN 19 Beispiel: class Fakultaet { static int fac (int n) { if (n == 0) return 1; else return n*fac(n-1); } public static void main (String[] argv) { System.out.println(fac(6)); } } 2.2 Implementierungsbemerkungen zum imperativen Kern Bemerkungen zur Übersetzung von Operatoren, Zuweisungen, Variablendeklarationen, Blöcken, bedingten Anweisungen, endlichen Fallunterscheidungen, Schleifen, Funktionen und/oder Prozeduren. 2.2.1 Operatoren Operator (hier: 2-stellig) ? e1 op e2 M Teilausdrücke Übersetzung: 1) Berechne den Wert von e1 , und stelle das Resultat v1 an der Stelle h1 zur Verfügung (h1 ist Hilfszelle, z.B. in einem Register oder auf dem Laufzeitkeller). 2) Analog mit e2 , v2 in h2 . 3) Führe eine Codesequenz aus, die die Operation zu op auf die Inhalte von h 1 , h2 anwendet und das Resultat in h3 bereitstellt. 4) Gebe h1 und h2 wieder frei. Beispiel: Betrachte den Infix-Ausdruck 3 + (4 * 5) . In der Umgekehrten Polnischen Notation (Postfix-Notation, bekannte, feste Operatorstelligkeit) können Regeln wie Multiplikation vor Addition” entfallen, alle Operationen arbeiten mit den ” beiden oberen Elementen des Stack. Der Beispielausdruck heißt in UPN: 345*+ In einer Kellermaschine wird der Ausdruck wie folgt übersetzt: 1) 3 wird auf Stack geschrieben: 20 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE 3 .. . h1 .. . 0 2) 4 wird auf Stack geschrieben: 4 3 .. . h2 h1 .. . 0 3) 5 wird auf Stack geschrieben: 5 4 3 .. . h3 h2 h1 .. . 0 4) h2 = 4 ∗ 5 (also h2 ∗ h3 ) : 20 3 .. . h2 h1 .. . 0 5) h1 = h1 + h2 : 23 .. . h1 .. . 0 In Maschinencode: LDC 3; PUSH; LDC 4; PUSH; LDC 5; PUSH; MULT; ADD; ; 3 auf den Stack ; 4 auf den Stack ; 5 auf den Stack ; * ; + Die Reihenfolge der Übersetzung eines Ausdrucks wie oben ist dabei typisch für Maschinen, die mit einem Laufzeitkeller (run time stack) arbeiten. 2.2.2 Zuweisungen x = e 2.2. IMPLEMENTIERUNGSBEMERKUNGEN ZUM IMPERATIVEN KERN 21 Übersetzung: 1) Berechne den Wert v von e nach h1 . 2) Berechne die Adresse von x . 3) Überschreibe den mit x assoziierten Speicherplatz mit v . 4) Gebe h1 wieder frei. 2.2.3 Variablendeklarationen Es wird Speicherplatz der durch den Typ festliegenden Größe alloziert und mit den Variablen assoziiert (das passiert evtl. an anderer Stelle) → Adreßbuch, Deklarationstabelle” zur Über” setzungszeit. Beispiel: { int i, j; double x=10, y; int k=10; ... k ... i ... } Veranschaulichung der Speicherallokation: Anfangsadresse → (z.B. in einem Register) 0 1 2 i j 4 6 y k | 2.2.4 x {z 32−bit } Blöcke Übersetzung: 1) Alloziere Speicherplatz für die (lokalen) Variablen. 2) Führe die Anweisungen der Reihe nach aus. 3) Gebe die Speicherplätze für die lokalen Variablen wieder frei. 2.2.5 Bedingte Anweisungen if(<test>) {<then>} else {<else>} 22 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE Veranschaulichung als Flußdiagramm: ? - <test> + ? ? <then> <else> ? Das abstrakte Flußdiagramm läßt sich linearisiert wie folgt darstellen: .. . <test> jump if false <then> jump <else> Maschinencode ? ← conditional jump (branch) ← unbedingter Sprung (jump) .. . 2.2.6 Endliche Fallunterscheidung (switch) switch (e) { case c1 : S1 ; . . . case cn : Sn ; [default : Sdef ;] } Annahme (der Einfachheit halber): {c 1 , . . . , cn } ⊆ [cmin , cmax ] und cmax − cmin + 1 ist klein, d.h. es lohnt sich, eine Sprungtabelle zu erzeugen. Berechne eine Sprungtabelle tab der Länge cmax − cmin + 1 an der Adresse b (derart, daß tab[k] = M em[b + k]). tab[ci − cmin ] = Anfangsadresse der Übersetzung von Si tab[j −cmin ] = Anfangsadresse der Übersetzung von Sdef für j ∈ [cmin , cmax ]\{c1 , . . . , cn } (oder die Adresse direkt hinter dem switch, falls kein default-Fall vorkommt) Übersetzung: 1) Berechne den Wert v von e und v − cmin 2) Wenn 0 ≤ v − cmin ≤ cmax − cmin , dann springe indirekt über tab[v − cmin ] (zu Sj bzw. Sdef ), sonst direkt zum Code von Sdef 3) Für break springe ans Ende 2.2. IMPLEMENTIERUNGSBEMERKUNGEN ZUM IMPERATIVEN KERN 23 Veranschaulichung: .. . [Vorab Sprunganweisung, da in Tabelle i.a. keine sinnvollen Be←fehle stehen] .. . jmp b = tab[0] .. . .. . .. . Berechne v = Wert von e jmpclt v − cmin jmpclt cmax − v jmpi b + v − cmin code für s1 .. . code für sn code für sdef .. . .. . jmpclt: Springe, falls < 0 ←(jump conditional less than) ←jmpi: Springe indirekt Durch jmpclt v − cmin ” und jmpclt cmax − v” wird überprüft, ob v zu klein bzw. zu groß ” ” ist. 2.2.7 Schleifen a) while (<bed>) {<stm>} Veranschaulichung als Flußdiagramm: ? <bed> + - ? <stm> ? b) for (<Init>; [<bed>]; <Abschluß>) {<stm>} Ist äquivalent zu <Init>; while (<bed>) {<stm>; <Abschluß>} Veranschaulichung als Flußdiagramm: 24 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE ? <Init> ? <bed> + - ? <stm> ? <Abschluß> ? c) do {<stm>} while (<bed>) Veranschaulichung als Flußdiagramm: ? <stm> ? + <bed> - ? Es gilt: do {<stm>} while (<bed>) ∼ = <stm>; if (<bed>) do {<stm>} while (<bed>); else skip; ∼ = <stm>; if (<bed>) while (<bed>) {<stm>}; else skip; ∼ = <stm>; while (<bed>) {<stm>} Also ergibt sich das folgende, äquivalente Flußdiagramm: 2.2. IMPLEMENTIERUNGSBEMERKUNGEN ZUM IMPERATIVEN KERN 25 ? <stm> ? - <bed> + ? <stm> ? 2.2.8 Funktionen und/oder Prozeduren Statische Methoden werden wie folgt deklariert: Name Argument ? ? [Sichtbarkeit] static R p (A1 x1 , . . ., Ak xk ) {<body>} 6 Resultattyp 6 Argumenttyp 6 Rumpf erweiterter Prozedurrumpf Bemerkungen: • Falls R = void : Kein Resultat, Prozedur. Sonst: Funktion, deren Rumpf ein return enthalten muß. • Ist p Prozedur, dann sind Aufrufe p(a 1 , . . ., ak ) Anweisungen (wobei ai Ausdrücke vom Typ Ai seien). Ist p Funktion, dann sind Aufrufe p(a 1 , . . ., ak ) Ausdrücke. • Parameterübergabe: call-by-value Bedeutung: (Kopierregelsemantik) für Aufrufe Definition: Ein Variablenvorkommen der x i in <body> heißt frei, wenn es außerhalb jeden Bindungsbereiches lokaler Variablendeklarationen T xi = . . .” vorkommt. [Behandlung forma” ler Parameter wie lokale Variablen] Übersetzung: 1) Alloziere k neue Variablen x1 , ..., xk entsprechenden Typs. 2) Zur Ausführung von p(a1 , . . ., ak ) führe x1 = a1 ; . . .; xk = ak ; <body>’ aus, wobei <body>’ als modifizierter Prozedurrumpf aus <body> dadurch entsteht, daß 26 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE a) alle freien Vorkommen der xi durch xi ersetzt sind und b) alle Vorkommen globaler Variablen in <body> so gebunden 1 umbenannt sind, daß keine globalen Bindungs-Verfälschungen entstehen. Beispiel: Betrachte das folgende Codefragment (Pfeile deuten Bindungen an): int j = 1; . 6 . . static void p (int i) { . . . i + j . . . } . 6 . . { int j = 2; p(3) . . . } Nach den Schritten 1), 2a) und Kopierregelanwendung ergibt sich für den Block in der letzten Zeile: { int j = 2; { int i; i = 3; { . . . i + j . . . } } } 6 6 globale Bindungsverfälschung Problem: Das j-Vorkommen ist nun statt an das globale j an das lokal deklarierte j gebunden → Umbenennung von j, um Verfälschung zu vermeiden: int j’ = 1; . . . static void p (int i) { . . . i + j’ . . . } . . . { int j = 2; { int i; i = 3; { . . . i + j’ . . . } } } Bemerkung: • Im allgemeinen kann ein Variablenname in einem Rumpf <body> sowohl frei als auch gebunden vorkommen: { x = x + y; int x = 3; y = x + x . . . } 6 • Bei Referenzparameterübergabe sind die a1 , . . . , an Variablen, und sie selbst werden (statt der xi ) für die formalen Parameter eingesetzt: • Methoden können i.a. rekursiv und auch wechselseitig rekursiv sein. [Klassisches Beispiel: Funktionen even” und odd”] ” ” • Laufzeitmodell: Ein Keller (run time stack) von Aktivierungsrahmen für aufgerufene Prozedurinkarnationen . . . p(. . .) . . . → { . . . { . . . q(. . .) . . . } } → ... mit eigenen Speicherplätzen für Parameter und lokale Variablen. 1 gebunden: Systematisch angewandte und ihre definierenden Vorkommen gleich. 2.2. IMPLEMENTIERUNGSBEMERKUNGEN ZUM IMPERATIVEN KERN 27 Implementierung (Laufzeitkeller): .. . Hilfszellen Lokale Variablen Parameter xk .. . x1 ← fp DLD - ← tos stack-frame von g stack-frame von f .. 6 . Rückkehradressenkeller Im Rumpf von f befinde sich der Aufruf g(a 1 , . . ., ak ). Begriffe/Abkürzungen: ◦ top of stack (tos): Erste freie Zelle auf Laufzeitkeller; ◦ frame-pointer (fp): Beginn des aktuellen Aktivierungsrahmens; ◦ dynamic link of predecessor (DLD): Beginn des Stack-Frames der aufrufenden Prozedur. Übersetzung des Methodenrumpfes: 1) Formalen Parametern und lokalen Variablen werden eindeutige Relativadressen relativ zum Anfang des Stack-Frames zugeordnet. Hilfszellen oberhalb der lokalen Variablen kellerartig. 2) Rumpf wird als Anweisung ausgeführt. 3) Übersetzung von return e (wobei e Ausdruck): a) Berechne den Wert v von e (in h). b) Speichere v an dem Platz für den Rückgabewert (typischerweise an der Relativposition 0, unter Merken von DLD-Verweis). c) Rekonstruiere den alten frame-pointer-Inhalt (DLD). d) Rücksprung indirekt über den obersten Rückkehr-Adressen-Kellerinhalt. (Schritte c) und d) auch bei Prozedurende). Übersetzung von Methodenaufrufen: p(a1 , . . ., an ) 1) Schreibe Verweis auf den Beginn des aktiven Stackframes nach top of stack” (push des ” Inhalts des Framepointer-Registers auf den Keller). 2) Berechne nacheinander a1 , . . ., an , und speichere die Werte v1 , . . . , vn auf dem Laufzeitkeller an die Stellen tos + 1, . . . , tos + n . 28 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE 3) Reserviere Platz für die lokalen Variablen der aufgerufenen Methode p (Inkrement von dem tos-Register). 4) Merke die Rückkehradresse auf dem RKA-Keller. 5) Springe zum Anfang der aufgerufenen Methode (zum Code von p). (Für Schritte 4) und 5) benutze die jump to subroutine (JSR)-Instruktion). 2.2.9 Zusammenfassendes und Beispiel In einer Programmersprache wie Java müssen wir betrachten: • Basisdatentypen (Größe von Daten), • Konstanten, lokale Variablen, Parameter, • Ausdrücke, • Zuweisungen, • Sequentielle Komposition, • Konditional (if), • Schleifen (while, do, for), • endliche Fallunterscheidung (switch), • statische Methoden und -aufrufe, • Blöcke. Beispiel: Wir geben die Maschinencode-Folgen für die folgende Funktion an: static int fac (int n) { if (n == 0) { return 1 } else { return n*fac(n-1); } } • Für den Aufruf fac(10)”: ” PUSH fp PUSHC 10 fp = tos-1 JSR fac Veranschaulichung: 10 Inhalt fp .. . • Für den Rumpf: ← tos ← fp (zeigt auf tos-1) 2.3. ZEIGERREFERENZIERTE DATEN 29 PUSH 1 ; fp+2 PUSHC 0 ; fp+3 == ; fp+2 JMPCF ELSE ; (verbraucht das oberste Kellerelement) PUSHC 1 ; fp+2 JMP ENDE ELSE: PUSH 1 PUSH fp PUSH 1 PUSHC 1 fp = tos-1 JSR fac * ENDE: Merke DLD Kopiere Result ; fp = 0 fp = DLD RETURN 2.3 Zeigerreferenzierte Daten Programmeigenschaften: • Haldenspeicher (Heap), • dynamisch erzeugte Daten + Zeiger, • garbage collection. (Speicherbereinigung) [Freigabe von Speicherplatz dynamisch erzeugter Objekte, die nicht mehr benötigt werden, wird automatisch verwaltet → zusätzlicher Rechenaufwand; durch Maschinenarchitektur (insbes. großer Cache) kann ein Programm mit Garbage Collection jedoch sogar schneller sein als ein Programm ohne Garbage Collection (sofern ersteres im Gegensatz zum zweiten vollständig in den Cache passt)] Zwei Sorten zeigerreferenzierter Daten: • Arrays, • Objekte. [seit C; in Pascal durch Zeigerrecords, nicht in Fortran] Erzeugung mittels new-Anweisung (→ Dynamisches Erzeugen). 2.3.1 Referenzdatentypen • Klassen und Arraytypen (Referenz-Datentypen, also Objekte und zeigerreferenzierte Records); • Strings sind keine Arrays, sondern Objekte; • Java kennt keine Zeiger, benutzt sie aber intensiv implizit (Variablen eines Referenzdatentyps enthalten Zeiger auf das eigentliche Datum). ABER: Variablen sind keine Zeiger! [Variablen ≈ Programmkonzept, Zeiger → Datum; Arrays u.a. existieren unabhängig von Variablen] 30 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE 2.3.2 Arrays • werden per Referenz manipuliert; • werden dynamisch mit new erzeugt; • die Array-Daten (Felder) haben eine Größe, aber die Array-Variablen nicht; new int[10] erzeugt ein zehnelementiges Array mit Komponenten vom Typ int; new byte[256][16] erzeugt ein 256-elementiges Array von 16-elementigen byte-Arrays; new int[] { 2, 3, 5, 7, 11, 13, 17 } erzeugt ein siebenelementiges Array vom Typ int mit den ersten sieben Primzahlen; [dieses Array hat keinen Namen; Arrays haben keine Namen!] • Array-Variablen werden durch <name>[] bezeichnet, auch durch <name>[] . . . [] | {z } k−mal für ein k-dimensionales Array”; Beispiel: ” A[][] Deklarationen: Typ, gefolgt von Array-Namen: int A[][] mit Vorbelegung: int A[][] = new int[10][10]; Auch möglich: int A[][] = new int[10][]; (Array mit 10 Nullpointern) Beispiel: String-Array-Typ R Argumentvektor” ” public static void main (String[] argv) {} | {z } Parameterdeklaration 2.3. ZEIGERREFERENZIERTE DATEN 31 Array-Zugriffe: A[i] i ist ein Ausdruck mit Wert zwischen 0, . . . , n − 1, falls n die Länge des in A enthaltenen Arrays ist (Indexgrenzen werden geprüft). Der Ausdruck (new int[] { 1, 2, 3 })[2] liefert den Wert 2. 2.3.3 Strings Strings (Zeichenreihen, Konstanten der Form "Hugo") sind in Java Objekte. Die Klasse java.lang.String definiert eine Reihe von Methoden, z.B. length(), charAt(int), equals(String), . . . Die Methode equals(String) führt einen Zeichenreihenvergleich zwischen Strings S 1 , S2 durch. Beachte: S1 .equals(S2) 6≈ S1 == S2 Der Ausdruck S1 == S2 führt einen Zeigervergleich durch! 2.3.4 Implementierung Konstruktoren für • Arrays • Klassen (→ später genauer) Haldenspeicher (Heap): Der Haldenspeicher enthält die dynamisch erzeugten zeigerreferenzierten Daten. • Den Basistypen ist fest eine Größe ihrer Daten zugeordnet. • Damit liegt auch die Größe der dynamisch erzeugten Daten (Arrays und Objekte) fest. [Arrays müssen zur Laufzeit ihre Größe herumtragen, bei Objekten ist die Größe zur Übersetzungszeit bekannt] Veranschaulichung: .. . - Class a b c d .. . ← Basisadresse Relativadressen [Die Klasse Class enthalte die Daten a, b, c und d; ein Objekt, das eine Referenz auf Class besitzt, kennt die Basisadresse und kann somit über die Relativadressen auf die Daten von Class zugreifen] 32 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE 2.4 Objekte, Klassen, Methoden, Konstruktoren 2.4.1 Klassen und Objekte Datentypen, Daten, Klassen mit Vererbung (einfach). (Manchmal sieht man Klassen auch als Module (Module in Modula-2 eher Instanzen von Klassen)) → eher Packages als Module Beispiel: public class Circle { double x, y; double r; } Circle c; // Deklaration einer Variablen c vom Typ Circle c = new Circle(); // Erzeugung eines Objektes Die letzten beiden Zeilen können zu einer zusammengefasst werden: Circle c = new Circle(); Die Erzeugung des Objektes erfolgt durch Aufruf des Konstruktors (→ new Circle()). Dabei werden auch die Konstruktoren der Superklasse(n) ausgeführt. [Es macht Sinn, einen 0-stelligen Konstruktor explizit zu deklarieren] 2.4.2 Überladen von Methoden In Java sind zwei Methoden verschieden, falls sie • verschiedene Namen haben oder • in verschiedenen Klassen definiert sind oder • verschiedene Signaturen haben, d.h. sich in Anzahl und/oder Typen der Parameter voneinander unterscheiden (auch Reihenfolge). Beachte: Überladen ↔ Überschreiben, Überdecken (vgl. 2.4.10) 2.4.3 Signaturen Formale Terme, bestehend aus Methodennamen, Resultattyp und Parametertypen. Beispiele: int fac(int) Circle Circle() Circle Circle(double, double, double) 2.4. OBJEKTE, KLASSEN, METHODEN, KONSTRUKTOREN 33 Besonderheiten bei Konstruktordefinition: public Circle (double x, double y, double r) { this.x = x; this.y = y; this.r = r; } public Circle () { this(1.0, 1.0, 3.0);// ruft den passenden anderen Konstruktor mit // Namen Circle auf } 2.4.4 Statische Variablen Variablen, die pro Klasse, nicht pro Objekt deklariert werden → Klassenvariablen, Deklaration: static Typ Name . . . Beispiel: Ein klassisches Beispiel ist das Zählen der Instanzen einer Klasse. Man füge in die Klasse Circle eine statische Zählvariable ein: static int num circles; Zugriff ist Klassenname.Variablenname, also Circle.num circles; ferner werde der erste Konstruktor wie folgt erweitert: public Circle (double x, double y, double r) { this.x = x; this.y = y; this.r = r; num circles++; } Bei jeder Instanzerzeugung wird num circles um 1 erhöht und gibt somit die Anzahl der CircleInstanzen an. • Statische Variablen: Ersatz für globale Variablen. • Statische Methoden: Ersatz für globale Funktionen/Prozeduren. 2.4.5 Statische und Instanzinitialisierer [werden ausgeführt, sobald Klasse geladen ist (bei Programmstart)] class . . . { . . . } static {// statischer Initialisierer (ein Block, der beim Laden der . . . // Klasse ausgeführt wird) } Ohne static: Instanzinitialisierer, wird bei jeder Instanziierung/Initialisierung ausgef ührt. 34 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE 2.4.6 Modifikatoren • Sichtbarkeiten: überall sichtbar, wird vererbt; public protected innerhalb des eigenen Packages; in der eigenen und allen Subklassen, wird vererbt; <keiner> innerhalb des eigenen Packages; private nur in der eigenen Klasse, wird nicht vererbt. • final kein Überschreiben möglich → keine weiteren Subklassen • synchronized (hat Bedeutung im Zusammenhang mit Threads, light weight processes) • abstract 2.4.7 Ein Ausflug nach Modula-2 Das Circle-Beispiel von oben könnte in Modula-2 folgendermaßen aussehen: TYPE Circle = POINTER TO CircleType; CircleType = RECORD x, y, r : real END; PROCEDURE Circle’ (x, y, r: real) : Circle; VAR c : Circle; BEGIN NEW(c); c^.x := x; c^.y := y; c^.r := r; END; PROCEDURE umfang (this: Circle) : real; BEGIN RETURN (2 * 3.14159 * this^.r); END; BEGIN . . . x := umfang(Circle’(2.0, 3.0, 5.0)) . . . | {z } END in Java: (new Circle(2.0, 3.0, 5.0)).umfang() 2.4.8 Zugriff auf Komponenten der Superklasse(n) super kann überall dort auftreten, wo auch this auftreten kann. Beispiel: Gegeben seien eine Klasse B und ihre Superklasse A: 2.4. OBJEKTE, KLASSEN, METHODEN, KONSTRUKTOREN 35 A int x int f() 6 B int x int f() int g() { . . . this.f() . . . super.f() . . . this.x . . . super.x yY } Zugriff auf die Komponente der Superklasse der Klasse, in der der Zugriff auftaucht (nicht notwendigerweise der Klasse der Superklasse von this) Es sei eine Klasse C Subklasse von B, und es sei keine Funktion g in C definiert. Dann wird durch (new C()).g() die Methode g aus B aufgerufen. Der Ausdruck super f() in g (aus B) ruft dann f aus A auf, nicht f aus B! → Dynamische Bindung. [Ein typisches Beispiel für super-Anwendung ist das Schließen von Fenstern eines Programms] 2.4.9 Typanpassung (Casting) Ausdruck e von Typ A, schreibe (B)e |{z} vom Typ B Funktioniert immer, wenn B eine Superklasse von A ist ( uninteressanter Fall“). ” Beachte: • (B)e führt niemals zu einer Umkonstruktion des Wertes von e ; wenn e ein Objekt der Klasse C (C Subklasse von A) liefert, dann auch (B)e. • Führt andersherum zu einer Laufzeitprüfung, denn es ist statisch unentscheidbar, ob ein Ausdruck vom Typ A immer eine Instanz des Subtyps B bedeutet. Beispiel: (Zu Punkt zwei) ((String)(table.elementAt(i))).equals(name) Ist table eine Instanz von java.util.Vector, muß table nicht unbedingt nur Instanzen von String enthalten. 36 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE 2.4.10 Überschreiben von Methoden und Überdecken von Instanzvariablen Zusammenhang mit Vererbung: Idee: • Dynamische Bindung (late binding) für Methodenidentifikatoren. • Aber: Statische Bindung für Instanzvariablen. Beachte: Überschreiben, Überdecken ↔ Überladen (vgl. 2.4.2) Überschreiben von Methoden: Überschreibende Methoden spezialisieren das Verhalten von Subklasseninstanzen → Konsequenzen hinsichtlich der Bedeutung von Ausdr ücken. Beispiel: class A { int i = 1; int f() { return i; } } class B int int int int } extends A { i = 2; f() { return i; } g() { return super.f(); } h() { return ((A)this).f(); } class C extends B { int i = 3; int f() { return i; } } Wir betrachten einige Beispielausdrücke und geben an, welches Ergebnis sie liefern: C c = new C(); c.i ; 3 c.f() ; 3 ((A)c).i ; 1 (i ist gebunden an die Deklaration in der Klasse, die formaler Typ des Ausdrucks ist) ((A)c).f() ; 3 (es wird die Methode f der Klasse von c aufgerufen, dynamische Bindung) c.g() ; 1 c.h() ; 3 (!) B c = new B(); c.i ; 2 c.f() ; 2 ((A)c).i ; 1 ((A)c).f() ; 2 2.4. OBJEKTE, KLASSEN, METHODEN, KONSTRUKTOREN c.g() ; 1 c.h() ; 2 37 38 KAPITEL 2. JAVA ALS PROGRAMMIERSPRACHE Kapitel 3 Mini-Java Programme • Nur geschlossene Programme (alle Klassen sind bekannt). • Genau eine Klasse erhält eine statische Methode main. • Keine Sichtbarkeiten (alles wie public). 3.1 Abstrakte Syntax p ::= class1 ; . . .; classn class ::= class C (C1 ) { T1 x1 ; . . .; Tk xk ; [static] R1 m1 (T1,1 m1,1 ; . . .; T1,n1 m1,n1 ) s1 ; . . . [static] Rn mn (Tn,1 nn,1 ; . . .; Tn,nn mn,nn ) sn ; } s ::= x = e | e1 .x = e2 | e.m(e1 , . . ., en ) | s1 ;s2 | if (e) s1 else s2 | while (e) s | { T1 x1 ; . . .; Tk xk ; s } | skip | return e e ::= c | x | e.x | this | super | (T)e | e.m(e 1 , . . ., en ) | new C() | unop(e) | binop(e1, e2 ) Dabei sind • c Konstanten der Basisdatentypen • x, xi Variablen resp. Instanzvariablen • m, mi Methodennamen • T, Ti , R, Ri Typbezeichner, auch für Basistypen wie boolean, int, long, byte, . . . • C, Ci Klassennamen, die ebenfalls als Typbezeichner erlaubt sind Es gilt: • Jede Klasse hat genau eine Superklasse, notfalls Object, die jedes Programm in der Form 39 40 KAPITEL 3. MINI-JAVA PROGRAMME class Object () { } enthalte. • Keine Initialisierungsausdrücke für Instanzvariablen. Stattdessen könnten wir Initialisierungsmethoden wie folgt benutzen: Keine impliziten Aufrufe der Konstruktoren der Superklasse static A newA () { A obj = new A(); obj.x1 = e1 ; . . .; obj.xk = ek ; return obj; } 3.1.1 Definitionen • Classesp sei die Menge der im Programm p definierten Klassen. • C1 ∈ Classesp heißt direkte Superklasse von C (in Zeichen: C → p C1 ), falls C durch class C (C1 ) { . . . } in p definiert ist. • →p heißt Vererbungsrelation in p. • (Classesp , →p ) sei der Vererbungsgraph von p. • (Classesp , →p ) ist ein Baum (insbesondere azyklisch und geordnet) mit Wurzel Object. • C1 heißt Superklasse von C, falls Transitive Hülle C →+ p C1 . C heißt dann auch (direkte) Subklasse von C 1 . 3.2 Statisch semantische (kontextsensitive) Information (a) Klassen: Für jede Klasse C merken wir uns: • Superklasse C.super, • Liste C.vars der Instanzvariablen, • Liste C.methods der Methoden, • ein Flag C.finalized, das angibt, ob die Klasse bereits finalisiert ist (später). (b) Instanzvariablen: • Typ x.type (formaler Typ), • Klasse x.class, in der x deklariert wurde, • Relativadresse x.reladdr von x in Objekten des Typs x.class 1 [Wichtig!]. 1 Funktioniert nicht bei multipler Vererbung! 3.2. STATISCH SEMANTISCHE (KONTEXTSENSITIVE) INFORMATION 41 (c) gewöhnliche Variablen: • Typ x.type, • Relativadresse x.address relativ zum Anfang des Stack-Frames der umfassenden Methode. (d) Methoden: • Signatur m.sig, speziell den Resultattyp m.type, • Klasse m.class, in der m deklariert ist, • Rumpf m.body . Zusätzlich benötigen wir für jeden Ausdruck e ebenfalls den (formalen) Typ e.type . Bemerkungen: • Jedes Objekt hat genau eine Klasse, aber evtl. mehrere Typen. • Strenge Typisierung: Der (formale) Typ eines Ausdrucks e muß aus dem Programmkontext und den Typen der Teilausdrücke (statisch) bestimmbar sein. Damit ist zur Laufzeit garantiert, daß das Resultat des Ausdrucks den Typ des Ausdrucks hat. (Beachte: Das Resultat kann weiterhin Instanz verschiedener Klassen sein!) 3.2.1 Typkonvertierung (casting) Eine Typkonvertierung eines Ausdrucks e zu Typ T findet statt durch: (T)e • Nach oben immer erlaubt. Beispiel: Gegeben seien zwei Klassen A und B. Es sei A Superklasse von B: A Der Ausdruck e sei vom Typ B. (A)e ist dann immer erlaubt. B • Nach unten ebenfalls erlaubt, aber es ist eine Laufzeittypüberprüfung nötig. Beispiel: Gegeben seien zwei Klassen A und B. Es sei A Superklasse von B: A int x; B int x; int m() {return this.x;} Dann ist eine Typkonvertierung wie in folgendem Fall erlaubt: 42 KAPITEL 3. MINI-JAVA PROGRAMME A obj = new B(); ((B) obj ).m(); |{z} | {z A } B Dabei findet zur Laufzeit eine Überprüfung statt, ob obj Instanz von B ist. → Statisch i.a. nicht entscheidbar. Beispiel hierfür: A obj; if ( das Wetter ist schön”) { ” obj = new A(); } else { obj = new B(); } Welchen Typ hat obj nach Durchlaufen der if-else-Anweisung? → Laufzeitüberprüfung! 3.2.2 Resultattypänderungen Beispiel: Gegeben seien zwei Klassen A und B. Es sei A Superklasse von B: A A m() {return this;} B B m() {return this;} → In Java nicht erlaubt! Der Resultattyp eines jeden Methodenaufrufs muß statisch (syntaktisch) eindeutig bestimmt sein, also: A A m() {return this;} B A m() {return this;} Methoden, die potentiell aufgerufen werden, müssen alle denselben Resultattyp haben. A obj; ((B)obj).m() ,→ liefert Objekt vom Typ A Formaler Typ von ((B)obj).m() ist hier A und nicht B. 3.3 Durchführung der Vererbung • Idee: Rekursives Durchlaufen des Vererbungsgraphen eines Programms p und Finalisieren aller Klassen (Zusammensammeln aller lokalen und vererbten Komponenten). • Resultat: Pro Klasse eine Menge von Methoden und eine geordnete Liste (evtl. gleichnamiger) Instanzvariablen. 3.4. SIGNATUREN VON METHODEN (GEMÄSS JVM) 43 • Verfahren: Betrachte eine Klasse in p, die deklariert sei durch class C (C’) { var1 ; . . .; vark ; meth1 ; . . .; methn ; } (1) Ist C’ = Object, dann ist C finalisiert und C.vars =df (var1 , . . . , vark ) C.methods =df (meth1 , . . . , methn ) (2) Ist C’ 6= Object und finalisiert mit Komponenten C’.vars = (var’1 , . . . ,var’k0 ) und C’.methods = (meth’1 , . . . ,meth’n0 ), dann wird C finalisiert durch C.vars =df (var1 , . . . , vark ,var’1 , . . . ,var’k0 ) C.methods =df (meth1 , . . . , methn ,meth’i1 , . . . ,meth’in0 ), wobei {meth’i1 , . . . ,meth’in0 } diejenigen Methoden aus C’.methods sind, deren Signatur sich von allen Methodensignaturen in (meth 1 , . . . , methn ) unterscheiden. (3) Andernfalls finalisiere C’ und fahre mit Schritt (2) fort. → Funktioniert immer, falls V Gp ein azyklischer (gerichteter) Graph ist. 3.4 Signaturen von Methoden (gemäß JVM) Syntax von Feldtypen: <base type> <object type> <array type> <field type> ::= ::= ::= ::= B | C | D | F | I | J | S | Z L<classname>; [<field type> <base type> | <object type> | <array type> Interpretation: • B→ 7 byte, I→ 7 int, C→ 7 char, J→ 7 long, D→ 7 double, S→ 7 short, F→ 7 float, Z→ 7 boolean • L<classname>; 7→ Klasse mit Namen <classname> • [<field type> 7→ <field type>[] Beispiele: • [I ; int[] • [[Ljava/lang/String; ; java.lang.String[][] Syntax von Methodentypen <method type> ::= (<field type>∗ )<field type> K Hier auch V (für void) möglich. 44 KAPITEL 3. MINI-JAVA PROGRAMME Beispiele: • (I)I • ([Ljava/lang/String;)V ; Typische Signatur der main-Methode eines Java-Programms: void main (String[]) { } Zwei Methodensignaturen sind verschieden, falls die ihnen zugeordneten Signaturzeichenreihen syntaktisch verschieden sind oder die Methoden sich im Namen voneinander unterscheiden. 3.5 Typisierung von Ausdrücken Ausdrücke kommen nur in Methodenrümpfen vor. Damit ist aus dem Kontext der Methode m und mit m.class auch die Klasse bekannt, in der ein Ausdruck auftritt. • Konstanten: Einer Konstanten c kann man ihren Typ type(c) ansehen. • Variablenzugriffe: Drei Fälle für eine Variable x: ◦ Es gibt einen kleinsten umfassenden Block mit Deklaration Tx :x Dann ist type(x) = Tx . ◦ Andernfalls: x ist formaler Parameter einer Methode m, und in der Parameterliste kommt Tx :x vor. Auch dann ist type(x) = Tx . ◦ Andernfalls: x ist Instanzvariable mit Deklaration T x :x in der Klasse m.class . Auch dann ist type(x) = Tx . [Es gilt dann Tx :x”∈ m.class.vars] ” Für e.x : Sei type(e) = C . Dann muß C Klasse in p sein und T x :x kommt in C.vars vor. Dann ist type(e.x) = Tx . • Schlüsselwörter this, super: Beim Vorkommen in einer Methode m gilt: ◦ type(this) = m.class ◦ type(super) = m.class.super • Typkonvertierung (Casting) Es gilt für einen Ausdruck e und einen Typ T type((T)e) = T, falls type(e) nach T konvertierbar ist. [Einschub: Ein Typ T1 heißt (typ)-kompatibel zu T2 (in Zeichen T1 < T2 ), falls (a) T1 = T2 oder 3.6. WOHLGEFORMTHEIT VON PROGRAMMEN 45 (b) T1 Subklasse von T2 ist. Ansonsten gilt: byte < short < int < long < float < double . Ein Typ T1 heißt nach T2 konvertierbar, falls (a) T1 Subklasse von T2 oder umgekehrt ist oder (b) T1 und T2 Basisdatentypen aus {char, byte, short, int, long, float, double} sind.] • Instanziierung: Für einen Klassentyp C gilt: type(new C()) = C • Methodenaufrufe: Wir betrachten Methodenaufrufe der Form e.m(e1 , ..., en ) mit Ausdrücken e, e1 , . . . , en und Methodenname m. Sei type(e) = C, und C ist ein Klassentyp. Dann bestimmen C und die Signatur von m eindeutig eine Methode m mit Resultattyp R (beachte: Alle potentiell aufgerufenen Methoden in Subklassen von C haben denselben Resultattyp). Dann ist type(e.m(e1, ..., en )) = R . Beachte: m in C hat dann zu type(e1 ), . . . , type(en) passende formale Parameter T1 x1 , . . . , T n xn . • Operatoraufrufe: Zu bestimmen sind type(unop(e1)) und type(binop(e1, e2 )). type(unop(e1)) und type(binop(e1, e2 )) sind durch type(e1 ), type(e2) und den Operator unop resp. binop bestimmt. 3.6 Wohlgeformtheit von Programmen Beachte: Anweisungen kommen wie Ausdrücke ausschließlich in Methodenrümpfen vor. 3.6.1 Definition • Ein Programm p = cl1 ; . . .; cln heißt wohlgeformt (engl. well-formed) oder statisch-semantisch korrekt oder übersetzbar, wenn (a) alle Klassen cl1 , . . ., cln wohlgeformt und paarweise verschieden bekannt sind, 46 KAPITEL 3. MINI-JAVA PROGRAMME (b) genau eine Klasse cli eine statische Methode main enthält, (c) mit cli = class C (C’) { . . . } entweder C’ = Object oder Name einer der Klassen cl1 , . . ., cli , cli+1 , . . ., cln ist und der durch →p gegebene (V Gp , →p ) ein azyklischer Graph ist. Ein Typ heißt in p deklariert, falls er entweder Basisdatentyp, Object oder eine in p vorkommende Klasse ist. • Eine Klasse(ndefinition) class C (C’) { T1 x1 ; ...; Tk xk ; meth1 ; ...; methn } heißt wohlgeformt, falls (a) alle Typbezeichner T1 , . . . , Tk in p deklariert sind, (b) die Variablen x1 , . . . , xk paarweise verschieden sind, (c) die Methoden meth1 , . . . , methn sich in Namen und/oder Signatur alle unterscheiden und (d) alle Methodenrümpfe si der Methoden Ri mi (Ti,1 xi,1 , ..., Ti,ni xi,ni ) si bezüglich p, C, mi und einer Umgebung (1 ≤ i ≤ n) % = [xi,1 ← Ti,1 , . . . , xi,ni ← Ti,ni ] wohlgeformt sind. 3.7 Wohlgeformtheit von Anweisungen und Ausdrücken Es ist definiert, wann ein Klassentyp eine Komponente (Variable oder Methode) hat. • Ein Ausdruck e ist wohlgeformt bzgl. dem Programm p der umgebenen Klasse C, der umgebenen Methode m und einer Variablenumgebung %, ◦ falls e wohlgeformt ist mit type(e) = T, T ∈ p deklariert ist, alle Teilausdrücke von e bzgl. p, C, m, % wohlgeformt sind und ◦ falls e = this oder e = super und m dabei keine statische Methode ist. Bemerkungen: (a) e.x wohlgetypt → type(e) hat eine Instanzvariable x . (b) e.m(e1 ,...,en ) wohlgetypt → type(e) hat eine Methode m mit entsprechender Signatur. • Eine Anweisung s heißt wohlgeformt bzgl. p, C, m, %, falls alle Teilausdrücke wohlgetypt sind und gilt: (a) s ist x = e . Dann ist type(e) < type(x) . (b) s ist e1 .x = e2 . Dann ist type(e2 ) < type(e1 .x) . (c) s ist e.m(e1 ,...,en). Dann reicht, daß s als Ausdruck wohlgetypt ist. (d) s ist if (e) s1 else s2 . Dann ist type(e) = boolean und s1 , s2 sind bzgl. p, C, m, % wohlgeformt. (e) s ist while(e) s1 . Analog zu (d). 3.7. WOHLGEFORMTHEIT VON ANWEISUNGEN UND AUSDR ÜCKEN 47 (f) s ist s1 ;s2 . Analog zu (d), (e). (g) s ist { T1 x1 ; ...; Tk xk ; s1 }. Dann sind T1 , ..., Tk in p deklariert, x1 , . . . , xk sind alle paarweise verschieden und s 1 ist wohlgeformt bzgl. p, C, m und % = [x1 ← T1 , . . . , xk ← Tk ]. [Anm: In Java sind echte Blöcke wie {int i; . . . {int i; . . . }} nicht erlaubt] Bemerkungen: (a) Wohlgeformte Programme heißen auch übersetzbar. (b) Um Wohlgeformtheit zu prüfen, müssen Klassen finalisiert sein (Wohlgetyptheit von e.x, e.m(e1 ,...,en )). (c) Die lokale Umgebung % können wir uns als Liste von Bindungen für Variablen vorstellen, die entsprechend der Blockstruktur des Programms verlängert und wieder verkürzt wird. Ihre maximale Länge innerhalb eines Methodenrumpfes ( |% max | ) gibt die Länge des Parameterund Variablenblocks im Stack-Frame der Methode an. (d) Ist eine Methode durch R m (T1 x1 , ..., Tk xk ) s deklariert, dann ist der Variablenbereich des Stack-Frames bei |% max | = j wie folgt aufgeteilt: j .. . .. . k+1 k .. . xk .. . 1 0 x1 this Lokale Variablen Parameter [Für die lokalen Variablen werden Relativadressen gespeichert. Für alle Variablen ist Speicherplatz reserviert, aber auf Variablen kann nicht von überall zugegriffen werden] Sinnvolles Vorgehen (bei statisch-semantischer Analyse): • Gleichzeitig zur Analyse auch die Relativadressenvergabe. • Statisch-semantische Analyse erzeugt aus dem Syntaxbaum einen gerichteten azyklischen Graphen (DAG), in dem jedes Variablenvorkommen durch einen Zeiger auf das zugehörige definierende Vorkommen repräsentiert ist. Beispiel: 48 KAPITEL 3. MINI-JAVA PROGRAMME class C C’ vars methods var method x 6 T m sig vars block stmt assign x this Im obigen Beispielgraph ist das x der Methode m an die einzige x-Deklaration im Graphen gebunden (Pfeil). Kapitel 4 Eine virtuelle Mini-Java-Maschine • Angelehnt an die JVM (Java Virtual Machine); • Implementierungsbasis für Java; • Konzepte ähnlich zu Pascal-Maschine mit ihrem P-Code ◦ Maschinenkonfiguration, Initial- und Terminalkonfiguration; ◦ Maschinenbefehle als Einzelschritt-Konfigurationstransformationen; ◦ Bedeutung eines Programms ergibt sich als iterativer Effekt des (geladenen) Programms auf die initiale Konfiguration. 4.1 Struktur und Inhalt von Klassendaten • Classfiles; • Objektcodeformat der JVM; • pro Klasse erzeugen wir eine Klassendatei (Binärdump der nun folgenden Struktur). Abkürzungen im folgenden: u2 2 Byte unsigned u4 4 Byte unsigned Klassendatei-Struktur: classfile { u4 magic; // = 0xCAFEBABE u2 minor version; u2 major version; u2 constant pool count; ← Länge des Konstantenpools cp info constant pool[constant pool count - 1]; u2 access flags; u2 this class; ← Verweis auf Konstantenpool-Eintrag u2 super class; ← Verweis auf Konstantenpool-Eintrag u2 interfaces count; u2 interfaces[interfaces count]; ← Verweise auf KP-Einträge u2 field count; 49 50 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE field info fields[field count]; ← Instanzvariablen u2 methods count; methods info methods[methods count]; ← Methoden u2 attributes count; attribute info attributes[attributes count]; } Dabei enthält attributes nur den source file-String. Jeder Eintrag in dem Konstantenpool beginnt mit einer Typkennzeichnung (tag, 1 Byte): CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT CONSTANT Class Fieldref Methodref Interface Methodref String Integer Float Long Double NameAndType Utf8 7 9 10 11 8 3 4 5 6 12 1 • Struktur einer Klassenreferenz: CONSTANT Class { u1 tag; // = 7 u2 name index; } Dabei ist name index eine Referenz in den Konstantenpool auf ein CONSTANT Utf8-Feld mit dem voll qualifizierten Namen der Klasse. • Feld-, Methoden- und Interface-Methoden-Referenzen haben alle die gleiche Struktur: { u1 tag; // = 9, 10, 11 u2 class index; u2 name and type index; } class index ist ein Verweis auf die zugehörige Klasse im Konstantenpool; name and type index ist ein Verweis auf einen CONSTANT NameAndType-Eintrag im Konstantenpool, der den Namen und den Typ (Signatur) der Komponente ausgibt. 4.1.1 Field-Deskriptoren Instanzvariablen werden beschrieben durch field info { u2 access flags; u2 name index; u2 descriptor index; u2 attributes count; attributes info attributes[attribute count]; } descriptor index zeigt auf einen gültigen field type (Utf8-String). 4.1. STRUKTUR UND INHALT VON KLASSENDATEN 51 Attribute Attribute werden beschrieben durch attribute info { u2 attribute name index; u4 attribute length; u1 info[attribute length]; } 4.1.2 Methoden-Deskriptoren Methoden werden beschrieben durch method info { u2 access flags; u2 name index; u2 descriptor index; u2 attributes count; attribute info attributes[attributes count]; } descriptor index ist ein Zeiger auf einen gültigen method type. 4.1.3 NameAndType-Deskriptoren CONSTANT NameAndType { u1 tag; // = 12 u2 name index; u2 descriptor index; } descriptor index ist ein Verweis auf field type oder method type (→ Utf8-String). 4.1.4 Long- und Double-Einträge • beschreiben 8-Byte-lange Konstanten; • verbrauchen 2 Konstantenpool-Einträge, der erste entspricht dem double- oder long-Wert, der zweite muß ein gültiger Eintrag sein, dessen Wert aber als ungültig (invalid) betrachtet wird. → Rückwirkung auf das Längenfeld für den Konstantenpool. 4.1.5 Attribute Felder können "ConstantValue"-Attribute haben, Methoden können "Code"- und "Exception"Attribute haben. Alle anderen Attribute dürfen von einer JVM-Implementierung ignoriert werden. Interessant hier: Das Code Attribute von method info-Einträgen: Code Attribute { u2 u4 u2 u2 attribute name index; attribute length; max stack; max locals; u4 code length; u1 code[code length]; 52 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE u2 u2 u2 u2 u2 exception table length; start pc; end pc; handles pc; catch type exception table[exception table length]; u2 attributes count; attribute info attributes[attributes count]; } attribute name index ist ein Verweis auf einen "Code"-Eintrag. Durch code length und code[code length] wird der (Byte-)Code repräsentiert. public class fac extends java.lang.Object { public static int fact (int n) { if (n != 0) return n*fact(n-1); else return 1; } } 4.2 4.2.1 Bytecode von Klassendateien Ein Beispiel Wir betrachten die folgende Bytecode-Datei im ACSII-Editor (mit Trennsymbolen | und . (hier nicht als Komma zu interpretieren!)): 00000000: 00000010: 00000020: 00000030: 00000040: 00000050: 00000060: 00000070: 00000080: 00000090: 000000a0: 000000b0: 000000c0: 000000d0: 000000e0: 000000f0: 00000100: 00000110: 00000120: ca fe 0a 00 00 12 49|01 65|01 65|01 0f.4c 01 00 73|01 03.66 00 04. 6e 67 00|00 00 00 ac 1a 00 00 00 01| 2a b7 01 00 ba be| 02 00 00 08| 00 06| 00 0d. 00 0a. 69 6e 0e.4c 00 0a. 61 63| 66 61 2f 4f 00|00 27 00 1a 04 00 06 00 0a 00 03 00 00 00 03 05|0a 01 00 3c 69 43 6f 45 78 65 4e 6f 63 53 6f 01 00 63 74| 62 6a 02|00 03 00 64 b8 00 01 00 00 b1 00 01|00 00 2d| 00 01 03.28 6e 69 6e 73 63 65 75 6d 61 6c 75 72 08.66 01 00 65 63 09 00 01|00 00 04 00 00 00 1d 00 00 01 00 00 14| 00 06| 29 56| 74 3e| 74 61 70 74 62 65 56 61 63 65 61 63 10.6a 74|00 12 00 00 00 68 ac| 00 03| 00 01 01|00 0f 00 07 00 0c 00 01 00 01 00 6e 74 69 6f 72 54 72 69 46 69 2e 6a 61 76 21|00 08 00 0f 1a 00 00 00 01 00 01| 0d 00 00 00 10|07 09 00 04|28 04.43 56 61 6e 73| 61 62 61 62 6c 65| 61 76 61 2f 01|00 01|00 9a 00 00 01| 00 09 00 00 00 00 02 00 Der Bytecode setzt sich wie folgt zusammen: • Java class-file (cafebabe) • Version 45.3 (min ver = 0003, max ver = 002d) • 19 Einträge im Konstantenpool (+1 ungültiger Verweis, 0014): [01] Class { 07 0010 } = Klasse fac 00 13| 07|0c 49 29 6f 64 6c 75 01 00 6c 65| 6c 65 01 00 61|01 6c 61 02|00 0a 00 05 04 00 0d 00 07 00 05| 06 00 11 .......-........ ................ .......()V...(I) I...<init>...Cod e...ConstantValu e...Exceptions.. .LineNumberTable ...LocalVariable s...SourceFile.. .fac...fac.java. ..fact...java/la ng/Object.!..... ................ ..’............. ....d...h....... ................ ................ *............... ............... 4.2. BYTECODE VON KLASSENDATEIEN [02] [03] [04] [05] [06] [07] [08] [09] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] 53 Class { 07 0013 } = Klasse java/lang/Object Method { 0a 0002 0005 } = Methode <init> von Object Method { 0a 0001 0006 } = Methode fact von fac NameAndType { 0c 0009 0007 } = void <init>() NameAndType { 0c 0012 0008 } = int fac (int) utf8 { 01 0003 ”()V” } utf8 { 01 0004 ”(I)I” } utf8 { 01 0006 ”<init>” } utf8 { 01 0004 ”Code” } utf8 { 01 000D ”ConstantValue” } utf8 { 01 000A ”Exceptions” } utf8 { 01 000F ”LineNumberTable” } utf8 { 01 000E ”LocalVariables” } utf8 { 01 000A ”SourceFile” } utf8 { 01 0003 ”fac” } utf8 { 01 0008 ”fac.java” } utf8 { 01 0004 ”fact” } utf8 { 01 0010 ”java/lang/Object” } • Accessflag 0021 = public • this-Klasse 0001 = Klasse fac • super-Klasse 0002 = Klasse java/lang/Object • interface count 0000 = Keine Schnittstellen • fields count 0000 = Keine Instanzvariablen • methods count 0002 = 2 Methoden [01] method info { 0009 0012 0008 0001 } 6 1 Attribut = Methode public static (= 0009) mit Namen fact (= 0012) und Signatur (I)I (= 0008) i.e.: public static int fact (int) [01] code attribute { 000a 00000027 0003 0001 } 6 Code” ” 6 Länge 6 I max stack Lokale Variablen 15×u1 z }| { . . . 0000000f . . . (. . .) . . . 0000 0001 6 Codelänge: 15 Zeichen 6 I keine Exceptions 1 Attribut [01] attribute info { 000d 00000006 0001 0000 0003 } = Attribut ”LineNumberTable” 54 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE [02] method info { 0001 0009 0007 0001 } = public void <init> () [01] code attribute { 000a 0000001d 0001 0001 } 5×u1 z }| { . . . 00000005 . . . (. . .) . . . 0000 0001 6 Codelänge: 5 Zeichen 6 I keine Exceptions 1 Attribut [01] attribute info { 000d 00000006 0001 0000 0003 } = Attribut ”LineNumberTable” • attribute count 0001 = 1 Attribut [01] attr info { 000f 00000002 0001 } = Attribut ”SourceFile” • Bytecode für <init>: [00] aload 0 [01] invoke special 0,3 [04] return • Bytecode für fact: [00] iload 0 n [01] ifne 0,5 n != 0 [04] iconst 1 1 [05] ireturn [06] iload 0 n [07] iload 0 n [08] iconst 1 1 [09] isub - [10] invoke static 0,4 fact [13] imul * [14] ireturn 4.2.2 Fehlende Konstantenpool-Einträge • CONSTANT utf8 info { u1 tag; (= 1) u2 length; u1 bytes[length]; } • CONSTANT String info { u1 tag; (= 8) u2 string index; (→ CONSTANT utf8 info) } • CONSTANT Integer info = CONSTANT Float info { u1 tag; (= 3, 4) u4 bytes; } 4.3. ACCESS-FLAGS 55 • CONSTANT Long info = CONSTANT Double info { u1 tag; (= 5, 6) u4 high bytes; u4 low bytes; } Verbraucht 2 Konstantenpool-Einträge. Steht ein Long oder Double an der Stelle i im Konstantenpool, dann ist der Eintrag an der Stelle i + 1 ungültig. 4.3 ACCESS-Flags • Klassen 0001 0010 0020 0200 0400 public final super interface abstract Anmerkung: Das super-Flag (ACC SUPER) hat Auswirkungen auf die Bedeutung von invoke special <index> . Eine Anweisung der Form super.m(...) erzeugt einen Methodenlookup beginnend mit der Superklasse der aktuellen Klasse. Falls super nicht gesetzt ist, hat invoke special ein Verhalten gemäß älterer JMV-Spezifikationen. • Fields (Instanzvariablen) 0001 0002 0004 0008 0010 0040 0080 public private protected static final volatile transient • Methoden 0001 public 0002 private 0004 protected 0008 static 0010 final 0020 synchronized 0100 native 0400 abstract 0800 strictfp 4.4 JVM-Konfiguration Laufzeitkomponenten (JVM-Spezifikation, Kapitel 3, 3.5): • Programmzähler (program counter, PC) • Laufzeitkeller, evtl. mehrere, pro Thread einen, hier aber nur einen (aufgeteilt in Stack Frames) 56 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE • Halde (heap), wird von allen Threads gemeinsam benutzt (Speicherallokation muß mit OutOfMemoryError sichtbar abbrechen, falls kein Speicher mehr zur Verfügung steht!) • Methodenbereich (method area), Code der Methoden, einmal, wird von allen Threads gemeinsam benutzt. • Laufzeit-Konstantenpool, für jede geladene Klasse separat. • Stack-Frames, auf dem Laufzeitkeller gespeicherte Daten, pro Methoden-Aufruf (invocation). ◦ Ein Bereich für lokale Variablen (Größe ist zur Übersetzungszeit bekannt (max localsInformation der aufgerufenen Methode)). ◦ Ein Bereich für Zwischenergebnisse von Berechnungen (Operandenkeller), Größe ist zur Übersetzungszeit bekannt (max stack-Information der aufgerufenen Methode). Long- und Double-Daten benutzen 2 Speicherzellen. ◦ Verweis auf die aktuelle Methode (n-info) im Laufzeitkonstantenpool der aktuellen Klasse (JVM erlaubt dynamisches Linken). 4.5 Zusammenfassung des JVM-Instruktionssatzes Klar: Bedeutung von Maschinenprogrammen (Methodenrümpfen in Bytecode) ist der iterative Effekt der einzelnen Bytecodes (Befehle) auf die Anfangskonfiguration. 4.5.1 Maschinentypen vs. Java-Typen Java boolean byte char short int float reference return Address long double I F A A L D JVM int int int o int Beachtung des Vorzeichens int float reference return Address gemäß IEEE Standard long double Maschinentypen werden auch als computational types” bezeichnet. ” 4.5.2 Lade- und Speicherinstruktionen • Lade lokale Variable auf den Operandenkeller iload <relAddr> iload 0 iload 1 iload <i> ist das gleiche wie iload <i>, iload 2 braucht aber 2 Byte Codelänge. iload 3 Analog: lload, fload, dload, aload. 4.5. ZUSAMMENFASSUNG DES JVM-INSTRUKTIONSSATZES 57 • Speichere ersten Eintrag des Operandenkellers in lokale Variable istore <relAddr> <type>store [0, 1, 2, 3] • Lade Konstanten bipush für byte bzw. short, i” für int ” sipush ldc, ldc w aconst null iconst m1, iconst 0, iconst 1, . . ., iconst 5 Analog für long, float und double . 4.5.3 Arithmetische Operationen Addition Subtraktion Multiplikation Division Remainder Negation (einstellig) Shift Bitweises ODER Bitweises AND Bitweises XOR Inkrement lokaler Variablen Vergleichsbefehle 4.5.4 iadd, ladd, fadd, dadd isub, . . . imul, . . . idiv, . . . irem, . . . ineg, . . . ishl, ishr, iushr, auch long ior, lor iand, land ixor, lxor iinc Typkonversionen Integer → Long, Long → Integer, . . . Beschreibung etwas kompliziert, weil bei den verkürzenden Konversionen auf das Bit genau spezifiziert ist, wie das Resultat aussieht. 4.5.5 Objekterzeugung und -manipulation • Instanziierung: new • (Erzeugung von Arrays: newarray, anewarray, multiarray) • Feldreferenzen: getfield, putfield, getstatic, putstatic • (Laden von Arraykomponenten: baload, caload, saload,. . . ) 6 byte 6 char 6 short • Speichern in Arraykomponenten: bastore, castore, sastore, . . . (d|l|i|f|a) • Länge eines Arrays: arraylength • Prüfen von Referenzeigenschaften: instanceof, checkcast 58 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE 4.5.6 Stack-Manipulation pop, pop2, dup, dup2, swap, . . . 4.5.7 Sprungbefehle • Bedingte Sprünge ifeq, iflt, ifle, ifgt, ifnull, ifge, ifnonnull, if icmpeq, if icmpne, . . . , lt, . . . , gt, . . . , le, . . . , ge, if acmpeq, if acmpne • Unbedingte Sprünge goto, goto w, jsr, jsr w, ret • Zusammengesetzte Sprünge tableswitch, lookupswitch 4.5.8 Methodenaufrufe • invokevirtual (Aufruf von Instanzmethoden) • invokestatic (Aufruf von statischen Methoden) • invokespecial (Aufruf von Klassen- und Instanzinitialisierern) • invokeinterface 4.6 Beispiele von Operationscode-Beschreibungen (a) iconst <i> Operation: pushc Integer-Konstante Formen: iconst m1 (2 = 0x2) iconst 0 (0x3) .. . iconst 5 (0x8) Operandenkeller: . . . ⇒ . . .,<i> (i wird auf den Keller gelegt) Beschreibung: Pushe die Integer-Konstante <i> (= -1, 0, 1, 2, 3, 4 oder 5) auf den Operandenkeller. Bemerkungen: Semantisch äquivalent zu bipush <i> 4.6. BEISPIELE VON OPERATIONSCODE-BESCHREIBUNGEN (b) iadd <i> Operation: Integer-Addition Formen: iadd (0x96) Operandenkeller: . . . , value1, value2 ⇒ . . . , result Beschreibung: Beide Operanden müssen vom Typ int sein (kann durch Bytecode-Verifikation sichergestellt werden). Die Operanden werden vom Keller gelöscht und das Resultat, die Summe von value1 und value2 wird auf den Keller gepusht. Das Resultat sind die niederwertigen 32 Bit der mathematischen Addition von value1 und value2 im 2er-Komplement. Im Fall eines Überlaufs kann das Vorzeichen falsch sein. Es wird kein Laufzeitfehler ausgelöst. (c) if acmp<cond> (mit <cond> ∈ {eq, ne}) Operation: Springe, falls Adressvergleich der Operation erfolgreich. Format if acmp <cond> branchbyte1 branchbyte2 Formen: if acmpeq (0xA5) if acmpne (0xA6) Operandenkeller: . . . , value1, value2 ⇒ . . . , Beschreibung: value1, value2 müssen vom Typ reference (Adressen) sein. Sie werden vom Operandenkeller gelöscht, und das Resultat des Vergleichs ist - eq ist erfolgreich ⇔ value1 = value2 - ne ist erfolgreich ⇔ value1 6= value2 Falls erfolgreich, wird aus branchbyte1 branchbyte2 ein 16-Bit-Offset gebildet und zu dem Befehl an der Stelle Anfang des if acmp-Befehls + Offset verzweigt. Andernfalls wird mit dem unmittelbar folgenden Befehl fortgesetzt. 59 60 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE 4.7 Wohlgeformtheit von Klassendateien • Keine überflüssigen Informationen. • Nur wohlgeformte und typrichtige Referenzen. • Statische und strukturelle Bedingungen an Code. Reihenfolge der Überprüfung: 1. Statische Bedingungen 2. Strukturelle Bedingungen 4.7.1 Statische Bedingungen an Codefelder (a) Kein Code-Feld ist leer (code length ist nie 0). (b) code length ist immer < 65536 = 216 . (c) Codeintegrität: • Die erste Instruktion steht in code[0] . • Für jede Instruktion in code[i] außer der letzten gilt, daß die folgende Instruktion in code[i+l] steht, l die Länge der aktuellen Instruktion. • Das letzte Byte der letzten Instruktion steht in code[code length-1] . Der Code-Bereich jeder Methode beginnt mit der ersten, endet mit der letzten Instruktion und enthält keine Lücken. (d) Statische Bedingungen an einzelne Instruktionen, z.B. lcd, lcd w: Der Operand muß ein gültiger Konstantenpool-Eintrag vom Typ CONSTANT Integer, CONSTANT float oder CONSTANT String sein. 4.7.2 Strukturelle Bedingungen Bedingungen um die dynamische Semantik, Typrichtigkeit, Vermeidung von Overflows und Underflows des Operandenstacks, . . . , die erst zur Ladezeit oder sogar erst zur Laufzeit sichergestellt werden (können). Beispiel: Es darf keine lokale Variable lesend zugegriffen werden, wenn sie nicht sicher vorher mit dem richtigen Typ beschrieben wurde → Datenflußanalyse. 4.8 4.8.1 Verifikation von Klassendateien (Bytecode Verifier) Motivation • class-Dateien werden über das Netz geladen, und man kann garantieren, daß sie mögliches Resultat eines regulären Compilers sind. • Versionsprobleme von Subklassen inzwischen geänderter Klassen, mehr oder weniger Instanzvariablen, geänderte Rechte, . . . → Die JVM selbst muß sicherstellen, daß alles regulär und konsistent zugeht! Also: 4.8. VERIFIKATION VON KLASSENDATEIEN (BYTECODE VERIFIER) 4.8.2 61 Bytecode-Verifikation zur Ladezeit (+Laufzeit) • Statische Bedingungen. • Vor allem aber ◦ keine Operanden-Stack-Over- oder Underflows; ◦ alle lokalen Variablenzugriffe sind gültig; ◦ alle JVM-Instruktionen greifen auf Argumente des richtigen Typs zu; ◦ referenzierte Instanzvariablen und Methoden existieren wirklich mit passenden Signaturen. Bytecode-Verifikation ist unabhängig von konkreten Compilern und auch von Java selbst. 4.8.3 4 Phasen” der Bytecode-Verifikation ” 2 beim Einlesen der Klassendatei, 1 beim Laden der Klasse, 1 beim Ausf ühren von Methoden. • 1. Phase: (nur) ein Gedankenmodell Parsen der Klassendatei und Feststellen, ob das classfile-Format stimmt ◦ von cafebabe und magic-number ◦ bis zur richtigen Länge und Vollständigkeit der Bereiche und Attribute. • 2. Phase: Überprüfen der strukturellen Eigenschaften, die unabhängig sind von den Code-Arrays in den Code-Attributen der Methoden: ◦ Es werden keine Subklassen von final-deklarierten Klassen gebildet. ◦ Jede Klasse (außer java/lang/Object) hat eine Superklasse. ◦ Der Konstantenpool ist statisch korrekt, d.h. zum Beispiel jede CONSTANT Class infoStruktur hat einen name index-Verweis auf eine gültige CONSTANT utf8 info-Struktur im Konstantenpool (KP), . . . ◦ Alle Feld- und Methodenreferenzen im KP haben gültige Namen, Konstanten, Transkriptoren, . . . • 3. Phase: Datenflußanalyse für die Code-Arrays der Code-Attribute der Methoden. Es wird überprüft, daß folgendes an jedem Programmpunkt und für jeden Pfad im Kontrollfluß, der dorthin führt, gilt: ◦ Der Operandenkeller enthält immer gleich viele Daten desselben Typs. ◦ Es gibt keinen Zugriff (lesend) auf eine lokale Variable, wenn nicht sicher ist, daß sie mit einem Wert des passenden Typs belegt ist. ◦ Methodenaufrufe (invoke-. . . -Befehle) geschehen nur mit passenden Argumenten (Anzahl und Typ). ◦ Alle Maschinenbefehle greifen höchstens auf passende Argumente auf dem OperandenStack oder passende lokale Variablen zu. 62 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE • 4. Phase: Überprüfung beim erstmaligen Ausführen des Codes von Methoden (gedanklich): ◦ Klassen, die referenziert werden, müssen geladen und auch verifiziert sein. Das geschieht gedanklich durch Varianten der JVM-Befehle, die folgendes tun: - Lade die Definition des referenzierten Typs, falls noch nicht geschehen. Prüfe, ob der aktuelle Typ den referenzierten Typ überhaupt referenzieren darf, die referenzierte Instanzvariable oder Methode existiert und passenden Typ (resp. Signatur) hat. ◦ Die Überprüfung externer Referenzen” sind alle von der Art, daß sie nur einmal ” durchgeführt werden müssen. Die 4. Phase kann auch Teil der 3. Phase sein. Die aufwendigste Phase ist die 3. Phase (Abschnitt 4.9 der JVM-Spezifikation). [F ür diese Vorlesung nicht ganz so wichtig] 4.9 Übersetzung von Mini-Java nach JVM-Code 4.9.1 Übersetzung von Klassen Wir übersetzen einzelne Klassen eines Mini-Java Programms p ::= cl1 ; ...; cln cli ::= class Ci (Si ) { } T1 x1 ; ...; Tk xk ; R1 m1 (A11 y11 , ..., A1n1 y1n1 ) { s1 } . . . Rl ml (Al1 yl1 , ..., Alnl ylnl ) { sl } Für jede der Klassen Ci erzeugen wir ein class-file Ci .class mit k Feldern, l Methoden und eine <init>-Methode mit Namen <init>” vom Typ ()V” und dem Code ” ” 6 Instanzinitialisierer des Konstruktors Ci (){;} aload 0; invokespecial <index>; (2 Bytes Index in den Konstantenpool) return;Y [eigentlich Konstruktorcode hier] Dabei ist <index> ein Zeiger auf einen CONSTANT Methodref-Eintrag der Form <init>”, ()V” ” ” der Klasse Si (Aufruf des Instanzinitialisierers der Superklasse S i ). Konkret erwarten wir an der Stelle <index> im Konstantenpool einen Eintrag CP[<index>] = CONSTANT Methodref { tag = 10; CP CONSTANT Class info class indexi −→ { tag = 7; CP "S "; name index −→ i } 4.9. ÜBERSETZUNG VON MINI-JAVA NACH JVM-CODE } 63 CP CONSTANT NameAndType name type index −→ { tag = 12; CP "<init>"; name index −→ CP "()V"; descriptor index −→ } Dabei muß Object java/lang/Object” heißen. ” • Instanzvariablen T1 x1 ; . . .; Tk xk Alle diese lokalen Instanzvariablen erhalten einen Field info-Eintrag der Form Field info { access flags = 00; CP "x "; name index −→ i CP "<type >"; descriptor index −→ i attribute index = 00; } wobei <typei > den Feldtyp Ti kodiert. • Methoden ◦ Für die m1 , . . ., ml und <init> erzeugen wir Method info-Einträge der Form Method info { access flags = 00; (bzw. 08 bei static Methoden) CP "m "; (bzw. ”<init>”) name index −→ i CP "<type >"; descriptor index −→ i (<typei > kodiert den Methodentyp (Ai1 , ..., Aini ) Ri ) attributes count = 01; CP "Code"; attributes info { attribute name index −→ attribute length = 4 Byte mit der Länge des Code-Attribut-Wertes; Code-Attribut { . . . } (ohne Exceptions und ohne weitere Attribute) } } Dabei enthält das Code-Attribut max stack und max locals in Abhängigkeit von dem Rumpf si der Methode mi und der Parameterliste (Ai1 yi1 , . . ., Aini yini ), und das Byte-Array code[codelength] ist die Übersetzung des Rumpfes si mit dem return, das dem Resultattyp Ri entspricht. Eine der Klassen enthält eine statische Methode main, die wir als Hauptprogramm des gesamten Programms auffassen. 4.9.2 Übersetzung von Methodenrümpfen s ::= x = e | e1 .x = e2 | e.m(e1 , ..., en ) | s1 ;s2 | if (e) s1 else s2 | while (e) s | { T1 x1 ; ...; Tk xk ; s } • Übersetzung einer Zuweisung x = e : 64 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE { Übersetzung von e } (liefert den Wert von e als oberstes Element auf dem Operandenstack) <type>store i; (Dabei ist i die Relativadresse von x im locals-Bereich der Methode, <type> = i, f, d, l oder a, je nach Typ von e; evtl. auch <type>store [0, 1, 2, 3, 4] für spezielle i ). • Übersetzung von Zuweisungen an Instanzvariablen e 1 .x = e2 : { Übersetzung von e1 } { Übersetzung von e2 } putfield <index>; wobei CP CONSTANT Fieldref <index> −→ { tag = 9; CP Klasse, in der x deklariert ist; class index −→ CP CONSTANT NameAndType name and type index −→ { tag = 12; CP "x"; name index −→ CP Typ von x”; descriptor index −→ ” } } erzeugt on-the-fly”. ” • Aufruf von Methoden: Ein Methodenaufruf der Form e.m(e1 , ..., en ) wird wie folgt übersetzt: { Übersetzung von e } { Übersetzung von e1 } .. . { Übersetzung von en } invoke virtual <index> Dabei ist <index> ein Verweis auf eine CONSTANT Methodref im Konstantenpool, die wir hier anlegen, falls sie nicht bereits existiert. Falls m eine Methode mit Resultattyp 6= void ist, erzeugen wir zusätzlich noch ein [pop] oder [pop2] (pop2, falls der Resultattyp von m double oder long ist). [Nach der Übersetzung sind die Werte der Ausdrücke also wie folgt angeordnet (Reihenfolge beachten!)] 4.9. ÜBERSETZUNG VON MINI-JAVA NACH JVM-CODE 65 .. . v(en ) .. . v(e1 ) v(e) .. . • Kontrollstrukturen: ◦ Das Konditional: if (e) s1 else s2 Im allgemeinen Fall ist e ein Ausdruck vom Typ boolean (true oder false) [auf der Maschine int 0 (false) und int 1 (true)] Übersetzung: {Übersetzung von e} ifeq <else> {Übersetzung von s1 } goto <ende> {Übersetzung von s2 } Springe, falls der oberste Kellereintrag (Wert von e) = 0 (false) ist. <else> = Länge des Codes für s1 in Byte + 1 + Länge von ifeq <else> + Länge von goto <ende> . <ende> = Länge des Codes für s2 + 1 + Länge von goto <ende> . Für längere Sprungdistanzen gibt es den Befehl goto w mit doppelt breitem Index: goto w b1 , b2 (Sprungdistanz = (b1 <<8)|b2 ) Statt ifeq <else> erzeugen wir ifneq <then> goto w <else> - Falls e die Form e1 <relator> e2 hat und type(e1 ), type(e2 ) int, short oder byte sind, wird nicht der boolesche Wert von e berechnet, sondern nach {Übersetzung von e1 } ein {Übersetzung von e2 } <branch> <else> erzeugt, mit: <branch> ist einer der bedingten Sprungbefehle if cmp<cond> , <cond> ∈ {eq, ne, lt, le, gt, ge} bzw. 66 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE if acmp<cond> , <cond> ∈ {eq, ne} , falls type(e 1 ), type(e2 ) Referenztypen, also Adressen sind. Speziell für den Vergleich mit 0 auch noch die Befehle if<cond> mit <cond> ∈ {eq, ne, lt, le, gt, ge} und für den Zeigervergleich mit null noch ifnonnull, ifnull . Für den Vergleich von double- und float-Werten gibt es Befehle <type>cmp[l|g] , die 1, 0 oder -1 auf den Stack liefern, je nachdem ob v 1 < v2 (1), v1 = v2 (0), v1 > v2 (-1) bzw. umgekehrt für g statt l. Wir können dann mit if<cond> springen oder nicht, je nach der Bedingung, die gelten soll. ◦ Die while-Schleife: while (e) s Übersetzung: {Übersetzung von e} ifeq <ende> {Übersetzung des Rumpfes s} goto <loop> - Achtung: Codelängen und Sprungdistanzen beeinflussen sich evtl. gegenseitig. [Beispiel: <loop> wird 3 Bytes groß, Gesamtlänge > Bytegröße, <ende> läuft über] Die Abhängigkeit lässt sich auflösen, da bei der Übersetzung die Codelängen von e und s bekannt sind. ◦ Sequentielle Komposition: s1 ;s2 Übersetzung: {Übersetzung von s1 } {Übersetzung von s2 } ◦ Blöcke: { T1 x1 ; ...; Tk xk ; s } Übersetzung: {Übersetzung von s} → Nur Auswirkungen auf die Übersetzungszeitumgebung (das Adreßbuch). Bemerkungen: • Branch-Befehle haben 2 Byte (16 bit)-Argumente, Offsets −2 15 bis 215 − 1. • goto w hat 4 Byte (32 bit)-Argumente. • Ein goto b1 , b2 ” ist 3 Byte lang. An der Stelle i im Code wird nach ” i + ((b1 <<8)|b2 ) verzweigt. 4.9. ÜBERSETZUNG VON MINI-JAVA NACH JVM-CODE 67 Beispiele: • goto 0, 0” ist eine Endlosschleife; ” • goto 0, 3” springt zum nächsten Befehl; ” • goto -1, -5” springt nach i-5 (wie goto -5); ” • if (e) s1 else s2 ” wird übersetzt durch ” {Übersetzung von e} ifeq <else> {Übersetzung von s1 } goto[ w] <ende> ← Nur, falls der letzte Befehl von s 1 kein return-Befehl ist. {Übersetzung von s2 } - mit <else> = |s1 ’| + 6(8) , <ende> = |s2 ’| + 3(5) . Nebenbemerkung: if (e) { . . . return } else { . . . return } . . . erzeugt eine Fehlermeldung, da der untere Teil unerreichbar ist. • while (e) s” wird übersetzt durch ” goto w <test> {Übersetzung von s} -{Übersetzung von e} ifne <loop> mit <test> = 3(5) + |s’| , <loop> = (|s’| + |e’|) . 68 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE Bedeutung von invoke virtual: (auch invoke static, aber keine obj ref) invoke virtual byte1 , byte2 (byte1 <<8)|byte2 ist gültiger Verweis in den Laufzeit-Konstantenpool auf eine Methodenreferenz. Operandenkeller: . . . , objref, arg1 , . . . , argn ⇒ . . . Löscht die n + 1 Argumente vom Operandenkeller, übergibt sie in den neu erzeugten Kellerrahmen in den Zellen locals[0], . . . , locals[n] und führt dann den Rumpf der Methode aus. Veranschaulichung: In meth1 stehe der Aufruf e.meth2 (e1 , ..., en ) . Operandenkeller von meth2 ↑ locals-Bereich von meth2 - 6 argn .. . arg1 objref .. . frame-pointer - 4.9.3 .. . Operandenkeller von meth1 locals-Bereich von meth1 Übersetzung von Ausdrücken e ::= c | x | e.x | this | super | (T)e | e.m(e 1 , . . ., en ) | unop(e) | binop(e1, e2 ) | new C() [ Prinzip”: Jeder Ausdruck legt seinen Wert auf die nächste freie Kellerstelle.] ” • Konstanten c: Beispiele: true false iconst 1 iconst 0 −1, 0, 1, . . . , 5 iconst m1, iconst 0, iconst 1, . . . , iconst 5 byte short bipush <byte> sipush <byte1 >, <byte2 > null aaconst null double float dconst 0, dconst 1 fconst 0, fconst 1 4.9. ÜBERSETZUNG VON MINI-JAVA NACH JVM-CODE 69 Ansonsten: ldc <byte1 >, <byte2 > , Argument 16-bit-Index in den Laufzeit-Konstantenpool bzw. ldc w <byte1 >, <byte2 >, <byte3 >, <byte4 > • Variablenzugriffe x: <type>load [0|1|2|3] <type>load <byte1 >, <byte2 > <type> ∈ {i, f, l, d, a} [byte, short wie int] • Instanzvariablenzugriffe e.x: {Übersetzung von e} getfield <index | {z }> 16−bit LCP Feldreferenz mit Klasse von x, Typ von x, Name von x • this, super: Übersetzung mittels aload 0 (push locals[0]) . Übersetzung von super.m(e1,...,en) : aload 0 {Übersetzung von e1 } .. . {Übersetzung von en } CP invoke special <index> −→ CONSTANT Methodref für m in einer der Superklassen der aktuellen Klasse • Typkonversionen (T)e: {Übersetzung von e} i2l, l2i, f2i, d2f, i2d, l2d, f2d, d2i, i2f, i2b, i2c, i2s | l2f | f2l | d2l Für den Fall, daß type(e) und T Klassen sind und T < type(e): {Übersetzung von e} CP checkout <index> −→ CONSTANT Classinfo für T erzeugt Laufzeitfehler, falls der Wert von e keine Instanz von T oder einer Subklasse von T ist (ClassCastError). Andernfalls passiert nichts. 70 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE checkout <index> Operandenkeller, . . . , objref ⇒ . . . , objref • Methodenaufrufe e.m(e1 ,...,en): { Übersetzung von e } { Übersetzung von e1 } .. . { Übersetzung von en } invoke virtual <index> Kein pop, pop2 am Ende! • Unäre und binäre Operatoranwendungen unop(e), binop(e 1 , e2 ): { Übersetzung von e bzw. e1 , e2 } Der zu unop (bzw. binop) gehörende JVM-Befehl (iadd) • Objekterzeugung new C(): Erzeugt Defaultwerte in den Instanzvariablen CP new <index> −→ CONSTANT Class info von C . Beispiel: int fac (int n) { if (n == 0) return 1; else return n* fac(n − 1) ; | {z } this.fac(n−1) } Übersetzung: iload 1 ifne 0, 5 iconst 1 ireturn -iload 1 aload 0 iload 1 iconst 1 isub invoke virtual <Index von fac> imul ireturn 4.10 Übersetzungsspezifikation • setzt Quellprogrammfragmente zu Zielprogrammfragmenten in Beziehung; • benötigt zusätzliche statisch-semantische und Übersetzungszeitinformation (Adreßbuch, Übersetzungszeitumgebung); 4.10. ÜBERSETZUNGSSPEZIFIKATION 71 • ist i.a. eine Relation, denn für ein Quellprogrammstück kann es mehrere richtige Übersetzungen geben (Optimierungen). → Auswahl geschieht durch das Übersetzungsprogramm z.B. nach Kostengesichtspunkten (Laufzeit- und Speichereffizienz). • System von induktiv definierten Relationen auf den syntaktischen Bereichen der Quellsprache (induktiv über den Aufbau von Programmen, Klassen, Anweisungen, Ausdrücken, . . .): ◦ Quellsprache SL (source language) ◦ Übersetzungsumgebung CEnv (compiletime-environment) ◦ Zielsprache T L (target language) 4.10.1 Übersetzungsspezifikation (Definition) TL (als Funktion: Cprog : (SL × CEnv) → T L) • Cprog ⊆ (SL × CEnv) × |{z} {z } | I Ausgabe Eingabe Die Zielsprache kann auch etwas komplexer sein, Classfile, Bytecode + Konstantenpool + . . . • Mit γ ∈ CEnv schreibt man Cprog [[p]]γ ⊇Def m [Es ist Cprog [[p]]γ ⊆ T L] Die durch m bezeichnete Menge von Zielprogrammfragmenten ist per Definitionen Teilmenge der Übersetzungen von p in γ . Beispiel: p s e (Zuweisungen mit Variablen und arithmetischen Ausdrücken) ::= var x0 , . . ., xk ; s ::= x = e | s1 ;s2 ::= c | x | e1 + e2 | e1 * e2 p ∈ P rog s ∈ Stmt e ∈ Expr x, xi ∈ V ar Einfacher Fall: Variablen stehen am Anfang des Laufzeitkellers; drei syntaktische Kategorien, folgerichtig definieren wir drei Übersetzungsrelationen, die sich aufeinander abstützen: • Cp ⊆ P rog × Code∗ [Übersetzung Programm → Maschinenprogramm] • ρ ∈ CEnv sind Übersetzungszeitumgebungen, und zwar endliche Abbildungen fin N V ar −→ 0 • Cp [[var x0 , . . . , xk ; s]] = Cp [[p]] ⊇Def Cs [[s]][x0 ← 0, . . . , xk ← k] {z } | ∈CEnv • Cs ⊆ (Stmt × CEnv) × Code∗ • Cs [[x = e]]ρ ⊇Def Ce [[e]]ρ; istore ρ(x) • Ce ⊆ (Expr × CEnv) × Code∗ • Ce [[c]]ρ ⊇Def iconst c 6 eigentlich {<iconst c>} 72 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE • Ce [[x]]ρ ⊇Def iload ρ(x) • Ce [[e1 + e2 ]]ρ ⊇Def Ce [[e1 ]]ρ; Ce [[e2 ]]ρ; iadd • Ce [[e1 ∗ e2 ]]ρ ⊇Def Ce [[e1 ]]ρ; Ce [[e2 ]]ρ; imul • Ce [[e1 + 0]]ρ ⊇Def Ce [[e1 ]]ρ • Ce [[e1 + 1]]ρ ⊇Def Ce [[e1 ]]ρ; iinc • Ce [[1 + e2 ]]ρ ⊇Def Ce [[e2 ]]ρ; iinc • Ce [[e1 ∗ 1]]ρ ⊇Def Ce [[e1 ]]ρ • Ce [[e1 ∗ 0]]ρ ⊇Def iconst 0 BURS-Codegeneratoren (bottom up rewrite system) werden mit derartigen Übersetzungsregeln spezifiziert, suchen kostenorientiert nach besten” Zielprogrammen, können Optimierungen an” wenden. Übungsaufgabe: Man spezifiziere Cp , Cs , Ce für den Fall, daß iload, istore relativ zum obersten Kellerelement (tos) adressieren. x+x 4.10.2 Ce [[e]]ρ k 6 aktuelle Anzahl benutzter Kellerelemente Übersetzungsspezifikation (Mini-Java → JVM, Auszüge) • Cprog ⊆ M iniJava × JV M Class∗ • Cprog [[cl1 ; . . . ; cln ]] ⊇Def Cclass [[cl1 ]]. . . Cclass [[cln ]] • Cclass ⊆ Class × JV M Class • Cclass [[class C(S) {T1 x1 ; . . . ; Tk xk ; m1 . . . mn }]] ⊇Def (cp, PUBLIC, class index(C, cp), class index(S, cp), [(PUBLIC, name index("x1", cp), descriptor index("T1", cp)) . . . (PUBLIC, name index("xk ", cp), descriptor index("Tk ", cp))] [Cmethod [[m1 ]]cp0 , Cmethod [[m2 ]]cp1 , . . . , Cmethod [[mn ]]cpn−1 , init method(C, S, cpn )]) wobei cp0 ein Konstantenpool ist, der die CONSTANT Class-Einträge für C und S und die CONSTANT utf8-Einträge der x1 ” . . . xk ” und der zu T1 ” . . . Tk ” gehörenden Feldtyp” ” ” ” namen enthält. cpi entsteht aus cpi−1 durch die Ergänzungen bei der Methodenübersetzung von mi (inklusive der <init>-Methode), und cp = cpn−1 . cp ist der Ergebnispool” nach Durchführung aller Methoden. ” • Cmethod ⊆ M ethod × CP ool × (Bytecode∗ × int × int × CP ool) 6 max stack 6 max locals • Cmethod [[R m (T1 y1 , . . . , Tk yk ) s]]cp ⊇Def Cstmt [[s]] max stack 0 k+1 cp ([this ← 0, y1 ← 1, . . . , yk ← k], k + 1) {z } | 6 I Übersetzungszeitumgebung max locals Konstantenpool I aktuelle Stackgröße 4.10. ÜBERSETZUNGSSPEZIFIKATION 73 • Cstmt ⊆ Stmt × int × int × CP ool × CEnv × (Bytecode ∗ × int × int × CP ool) , wobei CEnv ein Paar aus endlicher Abb. und verbrauchter Länge im locals-Bereich ist: CEnv = ((V ar → int) × int) Bemerkung: rechnen. Implementierungen von C method und Cstmt müssen auch noch Codelängen be- • Cstmt [[x = e]] ms ml cp ρ ⊇Def (mi <type>store i, ms0 , ml0 , cp0 ) , 6 6 max stack max locals • Cexpr [[e]] 0 ms cp ρ 3 (m, ms0 , cp0 ) wobei i = ρ ↓1 (x) 6 erste Komponente von ρ auf x angewandt <type> = i|l|f|d|a je nach formalem Typ von x und ρ . • Cstmt [[{T1 x1 ; . . . ; Tm xm ; s}]] ms ml cp ρ ⊇Def (M, ms0 , ml0 , cp0 ) , wobei (M, ms0 , ml0 , cp0 ) ∈ Cstmt [[s]] ms ml 00 cp (ρ ↓1 [x1 ← k + 1, . . . , xm ← k + m], k + m) mit k = ρ ↓2 (k ist die aktuelle Länge des locals[]-Bereich) und ml00 = max{ml, k + m}. • Cexpr ⊆ Expr × int × int × CP ool × CEnv × (Bytecode ∗ × int × CP ool) aktuelle Tiefe des Operandenkellers • Cexpr [[x]] k ms cp I maximale Tiefe des Operandenkellers 6 maximale Tiefe des Operandenkellers ρ ⊇Def (<type>load i, ms0 , cp) , wobei i = ρ ↓1 (x) <type> = i|l|f|d|a je nach Typ von x und ms0 = max{ms, k + 1} bzw. max{ms, k + 2}, falls <type> = l|d , k = ρ ↓2 . • Cexpr [[x]] k ms cp ρ ⊇Def (iload 0 , max{ms, k + 1}, cp) , falls x den Typ int hat und ρ ↓1 (x) = 0 auch für andere Typen und ρ ↓1 (x) ∈ {1, 2, 3}. 74 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE Beachte: Es werden Relationen induktiv definiert, die Schreibweise erinnert an einen funktionalen Stil. Die Relationen hängen von globalen Größen ab (ml, ms, cp, . . . ) und ändern diese auch: Das heißt, die Form ist zum Beispiel Cstmt [[s]]. . . cp . . . ⊇ . . . cp0 6 Das Übersetzungsresultat von Cclass [[cl]] ist eine abstrakte Datenstruktur (≈ (JVM-)Class aus der Übungsaufgabe 16). Codegenerierung erzeugt dann daraus ein Classfile. Das Übersetzungsprogramm, das solche Spezifikation implementiert, wird deterministisch sein, also eine Funktion realisieren. 4.11 Just-in-Time-Kompilation JIT: Effizienzverbesserung der JVM-Interpretation. Idee: Statt JVM-Code zu interpretieren, übersetzen wir ihn zunächst in nativen Code des Wirt-Prozessors, um ihn dann (hoffentlich häufig) auszuführen. • Betrifft die Implementierung der JVM, • nicht das Codeformat von Classfiles [z.B. nicht das native-Interface des Java-API]. • Typisch: ◦ Übersetzung von Methodenrümpfen beim Laden von Klassen ◦ oder beim ersten Ausführen ◦ oder nachdem durch Monitor-Komponenten der JVM festgestellt wurde, daß die Methode ausreichend häufig aufgerufen wird. • Trade-off zwischen Kompilations- und Ausführungszeit. [M.a.W.: Wenn eine Methode nur einmal ausgeführt wird, lohnt” sich die Überprüfung der Ausführungs-Anzahl nicht] ” • Möglichkeiten lokaler Optimierungen. • Gemischte Ausführung von kompiliertem und interpretiertem Code. → Komplett in die JVM-Implementierung integriert. Vorteile: Optimierungen: • Schnelle und effiziente Registerallokation. • Peephole-Optimierungen. • Klassische Optimierungsverfahren: ◦ Elimination redundanten Codes. ◦ Konstantenpropagierung und -faltung. ◦ Code-Motion (aus Schleifen heraus). Nachteile: Höhere Ladezeiten, gerade bei hochgradigen Optimierungen. Just-in-Time-Kompilation geschieht Methode für Methode (kleine Übersetzungseinheiten). → Optimierungen intra-prozedural (und nicht inter-prozedural). 4.11. JUST-IN-TIME-KOMPILATION 4.11.1 75 Typische Architektur eines JIT-Compilers Bytecode =⇒ Zwischencodeerzeugung Optimierungen Kontrollflußgraph aus Basisblök* ken abstrakter Maschinenbefehle (Basisblöcke = sequentielle Stückchen von Code mit nur einem Einund Ausgang) Register-Allokationen Codegenerierung =⇒ nativer Code des Rumpfes Beispiel: (aus dem LaTTe-JIT-Compiler, Seoul-Univ., Korea, http://latte.snu.ac.kr , IBM Watson Research Center) → Erzeugt zunächst aus dem Bytecode abstrakten Maschinencode einer unbeschränkten Registermaschine, angelehnt an die SPARC-RISC-Architektur (beliebig viele Register f ür lokale Variablen und Stackzellen für die relevanten Typen): • is[0] = is 0 ... • il[0] = il 0 ... Befehl für Befehl transformieren: • iadd → add[is[top-1]] • aload 0 ... Wir betrachten folgende Methode: int f (int x, int y, int z) { int val = ((x >= y) ? x : y) + z; f(val); } Maschinencode und Übersetzung in Basisblöcke: 76 KAPITEL 4. EINE VIRTUELLE MINI-JAVA-MASCHINE [Basisblöcke:] iload 1 iload 2 if cmplt 9 iload 1 goto 10 - iload 2 - iload 3 iadd istore 4 aload 0 iload 4 invoke virtual <index> ireturn move(il[1], is[0]) move(il[2], is[1]) comp (is[0], is[1]) R move(il[1], is[0]) R comp(il[1], is[2]) R move(il[2], il[1]) add(il[1], il[3], al[0]) <Übersetzung von invoke> move(al[0], il[0]) return • +Registerallokation • +Codegenerierung 4.11.2 Adaptive Kompilation • Zunächst schnelle und einfache JIT-Kompilation, zum Beispiel: ◦ Nur Registerallokation und ◦ Methodencaching. • Danach bei sog. heißen Methoden” (hot methods, hotspots): ” ◦ Eine optimierende Rekompilation, ◦ auch weitere klassische Optimierungen. move(il[3], is[1]) add(is[0], is[1], is[0]) move(is[0], il[4]) move(al[0], as[0]) move(il[4], is[1]) <Übersetzung von invoke> return Die Basisblöcke lassen sich insgesamt wie folgt vereinfachen: ? move(il[2], is[0]) Kapitel 5 Übersetzung in höheren Quellcode • Übersetzender Aspekt • Softwaretechnischer Effekt [Lesbarkeit, Sprache nicht unbedingt objektorientiert, . . . ] 5.1 Grundlegende Ideen • Objekte sind zeigerreferenzierte Records. • Klassen sind Zeigertypen auf Records. • Methodenaufrufe sind Aufrufe generischer Prozeduren und Funktionen: e.m(e1 , ..., en ) ,→ generic m(e, e1 , ..., en ) 6 this Beispiel: Wir betrachten die Java-Klasse class Circle { float x, y, r; float umfang () { return 2*Math.PI*r; } } ,→ In Modula-2: TYPE Circle = POINTER TO Circle Record; Circle Record = RECORD x, y, r : REAL END; PROCEDURE Circle umfang (this : Circle | {z }) : REAL; 1 BEGIN RETURN (2*3.14159*thisˆ.r | {z }); 3 END Umfang; Instanzerzeugung/Methodenaufruf in Java: new Circle().umfang(); {z } | {z } | 1 2 77 78 KAPITEL 5. ÜBERSETZUNG IN HÖHEREN QUELLCODE ,→ In Modula-2: BEGIN VAR c : Circle; NEW(c); cˆ.x := 0.0; cˆ.y := 0.0; cˆ.r := 0.0; | {z } 1 Circle umfang(c); {z } | 2 END Für C-Codegenerierung: Zeigerreferenzierte Strukturen • this->r • this*.r Komplikationen: 1 umfang() kann auch mit Instanzen von Subklassen von Circle aufgerufen werden. 2 Die Klasse des Inhalts von c ist nicht notwendigerweise Circle, und umfang() kann überschrieben sein, so daß eine andere Methodenfunktion aufgerufen werden muß. 3 Im allgemeinen haben Instanzen mehrere verschiedene Instanzvariablen gleichen Namens, aber evtl. verschiedenen Typs. Instanzvariablen sind in Java statisch gebunden. D.h. sie können gebunden umbenannt werden: r ,→ Circle r , this^.r ,→ this^.Circle r , analog auch für x, y. Damit ist 3 gelöst. • Wir benötigen alle Instanzvariablen (Finalisieren der Klassen). • Objekte bleiben wie in Java. Weitere Problemlösungen: • Zu dem Problem 1 : Alle Objekte sind zeigerreferenziert. Es gibt auch in Modula-2 den Typ ADDRESS. Jeder Zeigertyp in Modula-2 ist dazu kompatibel (Achtung: Das ist aus softwaretechnischer Sicht problematisch, d.h. fehleranfällig). PROCEDURE Circle umfang (this : ADDRESS) : REAL; BEGIN RETURN (2*3.14159*this^.Circle r); END Circle unfang; Diese Methodenfunktion kann damit auch für Subklasseninstanzen von Circle verwendet werden. Übersetzender Aspekt: Der Generierungsmechanismus stellt sicher, daß kein Unfug passieren kann. • Zu dem Problem 2 : Es geht um Methodenaufrufe, für e.m(e1 ,...,en ) ,→ generic m(e,e1 ,...,en ) 5.1. GRUNDLEGENDE IDEEN 79 Seien C0,1 , . . . , C0,n die Subklassen von Circle, die umfang() nicht überschreiben, d.h. für deren Instanzen Circle umfang verwendet wird. Weiterhin seien für Klassen Ci weitere Methoden Ci umfang definiert, und Ci,1 , . . . , Ci,ni seien die Klassen, für deren Instanzen die Methode Ci umfang verwendet wird. PROCEDURE generic umfang (this : ADDRESS) : REAL; BEGIN typecase (this) C0,1 , ..., C0,n : Circle umfang(this); C1,1 , ..., C1,n1 : C1 umfang(this); . . . Cn,1 , ..., Cn,nn: Cn umfang(this); END generic umfang; Methodenaufrufe wie folgt: e.umfang() ,→ generic umfang(e) 6 formaler Typ ist Circle Selektieren der richtigen Methode zur Laufzeit: e^.class^.umfang(e) 5.1.1 Zusammenfassendes 1 Methoden haben Empfänger unterschiedlichen Typs. → Lösung (brutal): ADDRESS als Objekttyp aller Zeigertypen. 3 Instanzvariablenzugriffe auf Records verschiedenen Typs. → Gelöst: Statische Bindung in Java, gebundene Umbenennung. 2 Aufrufe der richtigen Methodenprozedurauswahl zur Laufzeit in Abhängigkeit von der Klasse des Empfängers. → Klasse enthält für jede Methodensignatur die richtige Methodenprozedur. Erzeuge für e.m(e1 ,. . .,en ) ,→ e^.class^.m T1 . . . Tn T(e,e1 , . . .,en ) 6 6 Vermeidung doppelter Berechnung Beispiel: • Klassen werden in Zeigerrecords mit class- und Instanzvariablen-Komponenten überführt. • Für die Klasse gibt es einen Verbund mit den Methodenprozedurbindungen. TYPE Circle Class = POINTER TO RECORD super class : Object Class; umfang FLOAT : PROCEDURE (ADDRESS) : REAL; END; Circle = POINTER TO RECORD class : Circle Class; 80 KAPITEL 5. ÜBERSETZUNG IN HÖHEREN QUELLCODE Circle x, Circle y, Circle r : REAL; END; PROCEDURE Circle umfang FLOAT (this : ADDRESS) : REAL; BEGIN RETURN (2*3.14159*this^.Circle r); END Circle umfang FLOAT; Bei Umbenennung des formalen Parameters: PROCEDURE Circle umfang FLOAT ( obj : ADDRESS ) : REAL; VAR this : Circle; BEGIN this := obj; . . . Instanzerzeugung/Methodenaufruf in Java: float r = new Circle().umfang() Hauptprogramm: TYPE Object Class = POINTER TO RECORD super class : ADDRESS; END; VAR object class : Object Class; circle class : Circle Class; r : REAL; c : Circle; BEGIN NEW(object class); object class^.super class = NIL; NEW(circle class); circle class^.super class := object class; circle class^.umfang FLOAT := Circle umfang FLOAT; c := New Circle(); r := c^.class^.umfang FLOAT(c); END. In der letzten Zeile r := c^.class^.umfang FLOAT(c); tritt durch class^.umfang FLOAT(c) der Methodendispatch (Auswahl/Selection) auf. Weiteres Beispiel: (vgl. Übungsaufgabe, Klasse A hier um m()-Funktion erweitert) class A { B b; int i; A f() { return b; } int m() { return i; } 5.1. GRUNDLEGENDE IDEEN 81 } class B extends A { int i; A f() { return ((B)(super.f())).g(); } B g() { return this; } int h() { return ((A)this).f().i; } } Hauptprogramm: B b; b = new B(); b.b = b; b.i = 2; A a; a = b.f(); ,→ In Modula-2: TYPE A Class = TYPE A = POINTER TO RECORD class : A Class; A b : B; A i : INTEGER; END; TYPE B Class = TYPE B = POINTER TO RECORD super class : Object Class; f A : PROCEDURE (ADDRESS) : ADDRESS; END; POINTER TO RECORD super class : A Class; m int : PROCEDURE (ADDRESS) f A : PROCEDURE (ADDRESS) : g B : PROCEDURE (ADDRESS) : h int : PROCEDURE (ADDRESS) END; POINTER TO RECORD class : B Class; A b : ADDRESS; A i : INTEGER; B i : INTEGER; END; PROCEDURE A f A (this : ADDRESS) : ADDRESS; BEGIN RETURN (this^.A b) END A f A; PROCEDURE A m int (this : ADDRESS) : INTEGER; BEGIN RETURN (this^.A i) END A m int; PROCEDURE B g B (this : ADDRESS) : ADDRESS; : INTEGER; ADDRESS; ADDRESS; : INTEGER; 82 KAPITEL 5. ÜBERSETZUNG IN HÖHEREN QUELLCODE BEGIN RETURN (this) END B g B; PROCEDURE B f A (this : ADDRESS) : ADDRESS; BEGIN check cast(b class, A f A(this)); {z } | Übersetzung von (B)(super.f()) RETURN (A f A(this)^.class^.g B(this)) END B f A; PROCEDURE B h int (this : ADDRESS) : INTEGER; BEGIN RETURN (this^.class^.f A(this).A i) END B h int; Hauptprogramm: VAR object class : Object Class; a class : A Class; b class : B Class; b : B; a : A; BEGIN Initialisierungen f ür object class, a class, b class (insbesondere bˆ class.m int := A m int;) b := NewB(); b^.A b := b; b^.b i := 2; b^.class^.f A(b); END Konstruktoren (hier nur nullstellige) Erzeuge eine New - und eine Init -Prozedur: • New erzeugt das leere initialisierte Objekt und ruft Init auf. • Init initialisiert die Instanzvariablen (für Mini-Java nicht zu tun) und ruft Init der Superklasse auf. Beispiel: Vgl. Beispiel oben, für B: PROCEDURE New B () : B; VAR new b : B; BEGIN NEW(new b); new b^.class = b class; WITH new b^ DO BEGIN A b := nil; A i := 0; B i := 0 END; Init B(new b); RETURN(new b); 5.2. ÜBERSETZUNG VON MINI-JAVA NACH MODULA-2 83 END New B; PROCEDURE Init B (this : ADDRESS); BEGIN Init A(this) END Init B; 5.2 Übersetzung von Mini-Java nach Modula-2 Allgemeiner, unter dem übersetzendem Aspekt: • Klassen werden zu POINTER TO RECORD-Typen. ◦ Erzeugen von Klassenobjekten mit Superklasse und Methodenkomponenten m signature. ◦ Erzeugen von Instanzobjekten mit Klasse und allen Instanzvariablen. ◦ Erzeugen von Methodenprozeduren C m signature: PROCEDURE C m T1 ... Tk T (this : ADDRESS, ...) [: Resultat-Typ] ◦ Erzeugen von Konstruktor und Initialisierer. ◦ Erzeugen von Initialisierungscode für die Klassenobjekte: VAR c class : C Class; NEW(c class); c class^.superclass := ... c class^.m1 signature1 := C1 m1 signature1; wobei C1 die Klasse ist, in der m1 deklariert ist (C selbst oder eine Superklasse von C). • Methodenrümpfe werden entsprechende Modula-2 Anweisungen: ◦ ◦ ◦ ◦ ◦ ◦ ◦ ◦ new C() ,→ New C() e.x = e1 ,→ e^.C x := e1 e.m(e1 , ..., en ) ,→ e^.class^.m signature(e, e1 , . . ., en ) super.m(e1, ..., en ) ,→ C m signature(e, e1 , . . ., en ) , wobei C die Superklasse der Aufrufstelle ist, in der m mit entsprechender Signatur definiert ist. Klassen als Variablen- und Resultatstypen werden zu ADDRESS. if ( ) s1 else s2 ,→ IF ( ) THEN BEGIN s01 ELSE s02 END; while ( ) s ,→ WHILE ( ) DO BEGIN s END; Blöcke { T1 x1 ; ...; Tk xk ; s } ,→ VAR x1 :T01 ; ...; xk :T0k ; .. I . s0 deklariert in der zugehörigen Methodenprozedur • Ausdrücke werden zu entsprechenden Modula-2 Ausdrücken: ◦ this ,→ this ◦ super ,→ super ◦ super.m(e1, ..., en ) ,→ C m signature(this, e1 , ..., en ) analog zu den Anweisungen. ◦ (C)e ,→ VAR xe : typ(e); .. . xe := e; check cast(c class, xe ); ... xe 84 KAPITEL 5. ÜBERSETZUNG IN HÖHEREN QUELLCODE 5.2.1 Erzeugen der Klassenobjekte und Instanzen Nach Durchführung der Vererbung (Finalisieren der Klassen) werden benötigt: • Alle Instanzvariablen mit Name, Typ, Klasse. y • Alle Methoden mit Namen, Signatur, Klasse. 9 die Klasse, in der die Komponente deklariert ist 6 (T1 ,...,Tn)T • Alle lokalen Methoden mit Namen, Signatur, (Klasse), Rumpf. 6 (T1 ,...,Tn)T • Die Superklasse. Klassenobjekte und Initialisierungen Für class C extends S { T1 x1 ; . . .; Tk xk ; m1 ...mn } erzeuge TYPE C Class = POINTER TO RECORD super class:S Class; . . . methodi T1 ... Tj T : PROCEDURE (ADDRESS, T01 , ..., T0j ):T; . . . END mit den Typanpassungen für einfache Datentypen und C ,→ ADDRESS für Klassen. Es ist methodi der jeweilige Methodenname der i-ten Methode. Die Übersetzung erfolgt für alle Methoden der Klasse (vererbte und lokale). Initialisierung: VAR c class : C Class; . . . NEW(c class); WITH c class^ DO BEGIN super class := s class; . . . methodi T1 ... Tj T = Ci method T1 ... Tj T; END Dabei ist Ci der Klassenname der methodi definierenden Klasse. Instanzen Es seien Ti xi aus Klasse Ci alle Instanzvariablen der Klasse C (inklusive der lokalen T i xi , . . ., Tk xk ). Erzeuge 5.2. ÜBERSETZUNG VON MINI-JAVA NACH MODULA-2 TYPE C = 85 POINTER TO RECORD class : C Class; . . . Ci xi :Ti ’ END; mit Konstanten und Initialisierern PROCEDURE Init C (this : ADDRESS); BEGIN Init S(this) END Init C; PROCEDURE New C() : C; VAR new c : C; BEGIN NEW(new c) . . . new c^.Ci xi := defaultTi ; . . . Init C(new c); RETURN(new c) END New C; 5.2.2 Methoden Für jede lokale Methode mi mit Namen methodi, Signatur (T1 ,...,Tk )T und Rumpf si erzeuge PROCEDURE C methodi T1 ... Tk T (this : ADDRESS, y1 : T01 , ..., yk : T0k ) : T; <Evtl. Deklarationen> BEGIN s; ENDC C methodi T1 ... Tk T; Bei Umbenennung des formalen this-Parameters: PROCEDURE C methodi T1 ... Tk T ( obj : ADDRESS , y1 : T01 , ..., yk : T0k ) : T; VAR this : Ci ; BEGIN this := obj; . . . 5.2.3 Abschließende Bemerkungen Der Übersetzungsprozess erzeugt für ein System von Klassen • Typdefinitionen der Klassen und Klassenobjekte, • Methodenprozedurdefinitionen, • Konstruktoren und Initialisierer, • Initialisierungscode für die Klassenobjekte. → Es ergibt sich ein Modula-2 Modul. 86 KAPITEL 5. ÜBERSETZUNG IN HÖHEREN QUELLCODE Nutzung: • Ergänzung um ein Hauptprogramm, kompilieren, ausführen → geschlossene Programme; • als Klassenbibliothek, auch Subklassenbildung von Bibliotheksklassen. Anhang A Hilfreiche Internetadressen • http://java.sun.com/docs/books/ • http://developer.java.sun.com/developer/infodocs/ • http://www.informatik.uni-kiel.de/~wg/Lehre/Vorlesung-SS2002/Uebung.html 87