© APSIS GmbH , Polling, 2008
Ergänzende Abschnitte zum Kapitel 1. Einführung
Die Gliederung der zweiten Phase kann vereinfacht folgendermaßen dargestellt werden:
Abb. 1.4: Prozess der klassischen Programmentwicklung
Die dargestellte Vorgehensweise ist charakteristisch für die meisten heute gängigen Programmiersprachen und ihre Übersetzer. Sie hat sich bewährt, wo die Programme für eine Art bestimmte Art von Rechnern (z.B. für PCs, Großrechner usw.) erzeugt und sie immer wieder am selben Rechner ausgeführt wurden. Wenn ein Programm an verschiedenen Rechnern ausgeführt werden soll, muss es für jeden Rechner gesondert übersetzt und gebunden werden.
In Java spielen Pakete die Rolle der Bibliotheken.
Heutige Java-Compiler benutzen keinen Binder; es ist jedoch vorstellbar, dass die nächsten Generationen ihn für die Steigerung ihrer Effektivität einsetzen werden. Deswegen untersuchen wir, wie er in anderen Programmiersprachen verwendet wird.
Das Ergebnis der Übersetzung heißt traditionell Objektmodul, oft auch Bindemodul oder Objektcode genannt. In Java ist dies eine Klassendatei, die den Bytecode enthält. Dies ist genau das Abbild des zu entwickelnden Programms auf der Zielsprache, wie die Übersetzungseinheit es auf der Programmiersprache darstellt.
Das Objektmodul (d.h. die Klassendatei) ist noch kein fertiges Programm, da es nur jene Teile enthält, die in der entsprechenden Übersetzungseinheit enthalten sind. Die meisten Programme bestehen aus mehreren Modulen, die möglicherweise in mehreren Übersetzungseinheiten formuliert worden sind. Darüber hinaus nehmen selbst die einfacheren Programme Dienste von anderen, vielleicht von mit dem Compiler oder mit diesem Lehrbuch gelieferten Modulen in Anspruch. Das heißt, Bausteine werden benötigt, die im Quellprogramm nur benutzt (wie es heißt, importiert ), aber nicht definiert worden sind. Die Texte dieser Module müssen – wenn nicht schon geschehen – ebenfalls übersetzt werden und als Objektmodul vorliegen, damit der Binder (Linker) sie zu einem lauffähigen (ausführbaren) Programm oder Zielprogramm zusammenbinden kann. Dieses Programm liegt in Maschinencode (evtl. in Bytecode) vor und wird fast immer in einer eigenen Datei, einer ausführbaren Datei abgelegt (z.B. in DOS eine exe-Datei). Sie kann bei Bedarf mit Hilfe des Laders in den Speicher des Rechners geladen und dann ausgeführt werden.
Weil Java-Entwicklungssysteme zurzeit keinen Binder benutzen, muss das Ergebnis jeder Übersetzung (die Klassendatei mit dem Bytecode) vom Interpreter einzeln eingelesen werden. Wenn dies über das Internet erfolgt, kostet es beträchtliche Wartezeit für die Übertragung. Dies ist ein Grund für die im Moment eingeschränkte Verwendbarkeit der im Internet benutzten Java-Applets .
In der Regel werden nicht alle Bestandteile eines lauffähigen Programms zusammengebunden, um seine Größe zu reduzieren. Einige seiner Teile werden nämlich nicht immer gebraucht; diese werden bei Bedarf zur Laufzeit nachgeladen. Dies kostet zwar Zeit, dafür wird Platz gespart. Die Erwägung zwischen den Vor- und Nachteilen ist nicht trivial.
Die Programmteile, die bei Bedarf nachgeladen werden, können entweder als Objektmodule einzeln vorliegen, oder können in dynamischen Ladebibliotheken (dll, dynamic linking and loading) gespeichert werden. Diese werden von einem Binder – ähnlich wie eine ausführbare Programmdatei – hergestellt: Objektmodule werden zu einem „dll“ zusammengebunden.
In Java werden beide Modelle benutzt. Die Klassendateien liegen entweder einzeln vor und werden bei Bedarf zur Laufzeit nachgeladen oder sie können zu Archiven (sog. jar-Dateien) mit Hilfe eines Werkzeugs (wie das Programm jar) zusammengebunden werden. Der Vorteil dieser Lösung liegt darin, dass die nachzuladenden Klassendateien nicht einzeln übers Internet angefordert werden müssen, sondern die gesamte jar-Datei übertragen wird. Andererseits werden auf diese Weise auch Klassen in der jar-Datei übertragen, die gar nicht benötigt (und nachgeladen) werden.
Die ersten drei Werkzeuge (der Editor, der Übersetzer und der Binder) reichen im Prinzip für die Produktion von Programmen auf einer höheren Programmiersprache. Viele Compiler arbeiten aber auch noch mit einer Bibliotheksverwaltung (library manager) zusammen. Dies bedeutet, dass nicht jedes Objektmodul unabhängig voneinander abgelegt wird, sondern alle Objektmodule zusammengefasst in der Bibliothek vorliegen. Der Binder liest sie von da heraus und erzeugt das ausführbare Programm. Dieses kann auch in der Bibliothek abgelegt werden. In der Bibliothek können auch noch weitere Informationen über die einzelnen Übersetzungseinheiten vorliegen, die eventuell für die Übersetzung und fürs Binden notwendig sind.
In Java spielt die eingeschränkte Rolle des Bibliothekars das schon erwähnte Programm jar, mit dessen Hilfe Klassendateien zu Archiven zusammengefasst werden.
Es ist möglich, dass gleichzeitig mehrere Bibliotheken benutzt werden, insbesondere wenn vorgefertigte Programmbausteine in Anspruch genommen werden. So kann ein Softwarehaus seine käuflichen Bausteine in Form von Bibliotheken zur Verfügung stellen; diese können mit der privaten Bibliothek des Benutzers und der Standardbibliothek des Compilers zusammen benutzt werden. Sie enthält viele, am häufigsten benutzte Bausteine; bei Java wird dies durch die Sprachbeschreibung vorgeschrieben. Dem Compiler und dem Binder muss dabei mitgeteilt werden, wo sich diese Bibliotheken befinden. Wie dies geschieht (z.B. ob durch ein Hilfsprogramm), muss aus der Bedienungsanleitung der Entwicklungsumgebung entnommen werden. Bei javac besteht dies z.B. aus dem Setzen der Umgebungsvariable CLASSPATH. (S. auch die Bedienungsanleitung zur Lehrbuchbibliothek).
Abb: Klassische Werkzeuge der Programmentwicklung
Der Ausführer ist ein abstraktes Gebilde, das genau das tut, was im Programm für es bestimmt wurde. Der Ausführer kann der Prozessor eines Computers sein, der das vom Compiler übersetzte Programm ausführt, oder ein Student mit Papier und einem Stift, der Schritt für Schritt genau das nachvollzieht, was im Programm steht.
Manche Compilersysteme erstellen immer ein selbstständig lauffähiges Programm. Oft sind diese aber sehr groß, da sie alle Funktionalitäten enthalten. Um ihre Größe zu reduzieren, wird manchmal nicht alles in das Programm eingebunden, sondern es wird vorausgesetzt, dass beim Ausführen des Programms das Laufzeitsystem vorhanden ist. Dies ist häufig der Fall z.B. bei Fehlerbehandlung: Kann ein Programm nicht ordnungsgemäß zu Ende laufen, werden Fehlerbehandlungsroutinen nachgeladen, um die Art des Fehlers dem Anwender des Programms zu melden und um evtl. weitere Anweisungen abzufragen. Das Betriebssystem kann aus der Sicht des Programmierers als ein Teil des Laufzeitsystems betrachtet werden, da es auch bei der Ausführung des Programms verschiedene Dienste übernimmt.
Oft ist die Grenze zwischen dem Programm und dem Laufzeitsystem nicht eindeutig zu ziehen: Manchmal werden Teile des Laufzeitsystems in die Programmdatei eingebunden; manchmal befinden sie sich in dynamischen Ladebibliotheken.
In Java übernimmt der Interpreter die Rolle des Laufzeitsystems. Die Rolle der Nachladebibliotheken spielen die Archive.
Bei der Ausführung eines Programms wird der Rechner angewiesen, die im Programm enthaltenen einzelnen Anweisungen auszuführen. Dies ist das eigentliche Ziel des Programmierens: dem Rechner zu sagen, was er zu tun hat.
Es ist die Erfahrung jedes Programmierers, dass bestimmte Aufgaben immer wieder mühsam erledigt werden müssen. Um diese Tätigkeit zu optimieren, werden oft Programmgeneratoren zur Verfügung gestellt, die bestimmte Arten von Programmen (meistens grafisch gesteuert) erzeugen. Hierzu gehören z.B. die Oberflächengeneratoren , die Menüs, Masken, Schaltflächen usw. produzieren. Aus einem grafisch dargestellten Datenmodell kann ein Datenbank-Generator Programmteile erzeugen, die die Datenhaltung übernehmen. Ein Testtreibergenerator erzeugt leistungsfähige Testtreiber für einzelne Module aus ihrer Schnittstellenbeschreibung.
Im Zusammenhang mit der sog. verteilten Programmierung (wenn Programmteile in einem Netzwerk verteilt ablaufen) kommen Generatoren auch zum Einsatz, die zu einem Programmmodul die erforderlichen Hilfsspezifikationen und Programmteile erzeugen.
Das Ergebnis eines Generators ist eine Textdatei, die ein Programm in der Quellsprache enthält. Es muss normalerweise nicht gelesen und verstanden, nur übersetzt werden. Manchmal – selten – sind doch Modifikationen notwendig, wenn der Generator nicht flexibel genug ist und die Wünsche des Programmierers nicht in jeder Hinsicht befriedigen kann.
Die Firma Sun stellt eine einfache Entwicklungsumgebung sdk (Software Development Kit) frei aus dem Internet zur Verfügung. Sie beinhaltet eine Reihe von Werkzeugen, die aus der Kommandozeile heraus einzeln aufgerufen werden können. Die wichtigsten von ihnen sind:
javac |
Übersetzer |
java |
Interpreter |
appletviewer |
Interpreter |
javadoc |
Dokumentation |
jar |
Archivierung (Bibliothekar) |
jdb |
Spurverfolgung (Debugger) |
javap |
Rückübersetzer |
javah |
Verbindung zu anderen Sprachen (C/C++) |
Tab.: sdk-Werkzeuge
Eine Programmiersprache ist ähnlich aufgebaut wie eine natürliche Sprache, nur viel einfacher. Es gibt einen festen Wortschatz: Diese heißen reservierte Wörteroder Schlüsselwörter (eine unglückliche Übersetzung des englischen Fachbegriffs key word), die meistens aus dem Englischen stammen und sinngemäß zu benutzen sind: z.B. class oder while. In diesem Buch werden sie fett gedruckt. Für neue Begriffe kann man nach bestimmten Regeln (s. Kapitel 1.5.3.) neue Wörter, Bezeichner oder benutzerdefinierte Namen (z.B. objekt, GRUEN oder Telefonbuch) erfinden. Reservierte Wörter dürfen nicht als Bezeichner benutzt werden.
Alle Wörter des Programms werden aus bestimmten Zeichen zusammengesetzt; in natürlichen Sprachen sind dies die Laute oder in Schriftform die Buchstaben; bei Programmiersprachen (bei denen die Texte immer in schriftlicher Form vorliegen) kommen zu den (kleinen und großen) Buchstaben die zehn Ziffern und ein Satz von Sonderzeichen dazu, z.B. das Pluszeichen + oder die Unterstreichung _.
Die Programmiersprache Java ist von einer Arbeitsgruppe der Firma Sun detailliert und verbindlich definiert worden. Diese Definition wurde in einem Dokument, The Java Language Specification, (Die Spezifikation der Sprache Java, s. [JLS] im Literaturverzeichnis) veröffentlicht. Es steht im Internet frei zur Verfügung. Dieses Handbuch zu lesen, ist unerlässlich zum Programmieren in Java.
Im Gegensatz zu vielen Programmiersprachen (wie Pascal oder Ada ) sind in Java Klein- und Großbuchstaben nicht austauschbar (ähnlich wie in C und C++), d.h. der Compiler unterscheidet zwischen Objekt und objekt. In den verschiedenen Sprachen haben sich Konventionen eingebürgert, manchmal als Empfehlung der Sprachentwickler, wie Klein- und Großschreibung benutzt werden sollen. In Java werden üblicherweise Klassennamen mit großem (z.B. Applet), Paket- und Methodennamen mit kleinem Anfangsbuchstaben geschrieben (z.B. applet oder start), Konstanten durchgehend groß (z.B. GRUEN). Eine Silbentrennung am Zeilenende, wie etwa Telefonbuch, ist verboten. Das Zeilenende wird als Trennzeichen angesehen, genauso wie ein Leerzeichen (blank), ein Tabulatorzeichen und einige andere, die reservierte Wörter und benutzerdefinierte Bezeichner abschließen.
Der Autoren empfiehlt, in Programmen, die nur im deutschsprachigen Raum gelesen werden, für selbst definierte Bezeichner deutsche Wörter zu benutzen. Dadurch ist auch eine Unterscheidung zu den aus importierten Bibliotheken stammenden Namen möglich.
Wie aus den Zeichen und reservierten Wörtern sinnvolle Sätze (d.h. Programme) gebildet werden können, wird durch die Syntax der Sprache geregelt. Die Syntax besteht aus einem Satz von syntaktischen Regeln, meistens formal in einer Metasprache festgelegt. Für die deutsche Sprache gibt es beispielsweise die syntaktischen Regeln, dass das Prädikat im Aussagesatz an der zweiten Stelle steht, oder dass die Adjektive vor dem Substantiv stehen müssen. In Java ist eine vergleichbare Regel, dass jede Klasse mit dem reservierten Wort class eingeleitet werden muss oder dass Anweisungen mit einem Strichpunkt ; (Semikolon) abgeschlossen werden müssen.
Die Metasprache, in der die Syntax formal beschrieben wird, ist – im Vergleich zur Sprache selbst – relativ einfach. Sie besteht aus Metazeichen, syntaktischen Begriffen (nichtterminale Symbole oder Zwischensymbole) und Endsymbolen (terminale Symbole), d.h. den Elementen der Programmiersprache. Ein Beispiel für syntaktische Begriffe ist Ziffer, während 1, 3 und 0 Endsymbole sind; diese kommen in der Zielsprache (hier: Java) als Sprachelemente vor.
Für jeden syntaktischen Begriff gibt es eine Regel, die beschreibt, welche Symbolfolgen für diesen Begriff eingesetzt werden können. Für den Begriff Ziffer sieht z.B. die Regel folgendermaßen aus:
Ziffer ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Hier sind das Zeichen | und die Zeichenfolge ::= die Metazeichen. | bezeichnet die Alternative und wird als „oder“ gelesen. Das Metazeichen ::= heißt Definition und kann gelesen werden wie „wird definiert als“ oder „wird ersetzt durch“.
Eine syntaktische Ableitung besteht daraus, dass ausgehend von einem ursprünglichen Begriff alle Begriffe durch je eine ihren Regeln entsprechende Symbolfolge ersetzt werden, bis man keine syntaktischen Begriffe mehr in der Folge hat. Das Ergebnis besteht also nur aus Endsymbolen und stellt einen syntaktisch gültigen Programmausschnitt dar, der dem ursprünglichen Begriff entspricht.
Im Regelsatz für die Bildung von einfachen Bezeichnern (in „Pascal-Stil“) werden die weiteren Metazeichen { und } verwendet, sie definieren die Wiederholung; der Inhalt zwischen den zwei geschweiften Klammern kann beliebig oft wiederholt werden (null-mal, einmal, zweimal usw.):
Bezeichner ::= Buchstabe { Buchstabe_oder_Ziffer }
Buchstabe_oder_Ziffer ::= Buchstabe | Ziffer
Buchstabe ::= Kleinbuchstabe | Großbuchstabe
Kleinbuchstabe ::= a | b | c | d | e | f | g | h | i | j | k |
l | m | n | o | p | q | r | s | t | u | v | w | x | y | z
Großbuchstabe ::= A | B | C | D | E | F | G | H | I | J | K |
L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z
Ziffer ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Hier sind Bezeichner, Buchstabe_oder_Ziffer, Buchstabe, Kleinbuchstabe, Großbuchstabe und Ziffer sechs syntaktische Begriffe, die zweimal 26 Buchstaben und zehn Ziffern die Endsymbole. Wir werden diese in syntaktischen Regeln fett drucken, um sie von den Zwischensymbolen zu unterscheiden.
Die erste Regel besagt, dass ein Bezeichner mit einem Buchstabe anfangen muss, worauf eine beliebige Anzahl von Buchstabe_oder_Ziffer folgen kann (möglicherweise keines). Nach der zweiten Regel ist Buchstabe_oder_Ziffer entweder ein Buchstabe oder eine Ziffer; die nächste Regel definiert, dass Buchstabe ein Kleinbuchstabe oder ein Großbuchstabe sein kann. Was ein Kleinbuchstabe, ein Großbuchstabe und eine Ziffer ist, beschreiben die letzten drei Regeln.
Bezeichner der Sprache Java sind ähnlich wie in Pascal, als Buchstabe können jedoch auch deutsche Umlaute, russische, griechische oder alle im Zeichensatz Unicode als Buchstabe definierte Zeichen benutzt werden. Dies ist eine Kodierungsmethode für Texte, die primär im Internet benutzt wird (s. [Unicode] im Literaturverzeichnis).
Wir können etwas komplexere Bezeichner „in Ada-Stil“ bauen. Hier lautet die erste Regel:
Bezeichner ::= Buchstabe { [ _ ] Buchstabe_oder_Ziffer }
Die übrigen Regeln sind wie für Bezeichner „in Pascal-Stil“. Hier kommen auch die Metazeichen [ und ] vor; diese bedeuten die Option: Den Inhalt kann man wählen oder weglassen. Ein Bezeichner in „Ada-Stil“ fängt also auch mit einem Buchstabe an, worauf eine beliebige Anzahl von Buchstabe, Ziffer und Unterstreichungen folgen kann. Zwei Unterstreichungen können allerdings nicht nebeneinander stehen, und das letzte Zeichen eines Bezeichner kann auch keine Unterstreichung sein.
In der Sprachdefinition [JLS] befindet sich die Syntaxbeschreibung von Java in einer ähnlichen Notation. Sie heißt nach ihren Entwicklern Backus-Naur-Form oder BNF-Syntax. Es gibt auch viele andere Wege, eine Syntax zu beschreiben, durch die eine Sprache genauer als in der BN-Form definiert werden kann.
Es gibt nämlich Quellprogramme, die der Syntax genau entsprechen, und trotzdem fehlerhaft sind. Zu den Regeln einer Sprache gehören nämlich auch noch die Kontextbedingungen, d.h. die Gesetze, die den Zusammenhang zwischen den einzelnen Sprachkonstruktionen beschreiben. Beispielsweise, wenn der an sich gültige Bezeichner an einer Stelle benutzt wurde, muss er an einer anderen Stelle vereinbart worden sein. Obwohl es möglich ist, die Kontextbedingungen auch formal zu definieren (dies gehört zur wissenschaftlichen Definition einer Programmiersprache), sind für den Alltagsbenutzer (und auch für manche Fachleute) solche Syntaxbeschreibungen unbrauchbar kompliziert. Deswegen werden diese zusätzlichen Regeln normalerweise in einer natürlichen Sprache (verbal) beschrieben.
Neben der Syntax gehört zur Beschreibung einer Sprache die Semantik, die Bedeutung der einzelnen Sprachkonstruktionen. Sie definiert, wie der Ausführer handeln muss, wenn er ein Sprachelement im Programm entdeckt: Was geschieht, wenn man einen Bezeichner definiert? Was wird eine bestimmte Anweisung im laufenden Programm bewirken? Das Verständnis der Semantik ist eine Voraussetzung dafür, dass man sinnvolle Programme schreibt, die genau das tun, was der Programmierer sich im Voraus vorgestellt hat.
Die Semantik einiger Programmelemente ist von der Sprache her gegeben: Die reservierten Wörter wie for oder protected können nur mit einer bestimmten Bedeutung (wie „Zählschleife“ oder „geschützt“) benutzt werden. Andere Programmelemente, insbesondere die Bezeichner, werden vom Programmierer mit einer Bedeutung versehen.
In diesem Sinne unterscheiden wir zwischen Vereinbarung (oder Deklaration) und Definition eines Programmelements (z.B. eines Bezeichners), wenn auch diese beiden oft zusammenfallen. Durch die Vereinbarung wird nur der Name (z.B. einer Methode) bekannt gegeben, während durch die Definition ihm ein Sinn gegeben wird (z.B. werden die durch sie auszuführenden Aktionen aufgelistet).
Alle diese Werkzeuge werden mit den modernen Compilern im Rahmen einer Entwicklungsumgebung ausgeliefert, die diese Werkzeuge in ein menügesteuertes Programm integrieren. Neben der bequemen Bedienung ist deren großer Vorteil, dass die vom Compiler erkannten Fehlerstellen gleich lokalisiert und im Editor korrigiert werden können. Bessere Entwicklungsumgebungen schließen weitere Werkzeuge wie Projektverwaltung (für die Handhabung der Zusammenhänge zwischen den Bausteinen) usw. ein.
© APSIS GmbH , Polling, 2008