Implementierung objektorientierter Programmiersprachen

Werbung
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
Zugehörige Unterlagen
Herunterladen