Sandini Bib
Programmieren lernen
Sandini Bib
Die Lernen-Reihe In der Lernen-Reihe des Addison-Wesley Verlages sind die folgenden Titel bereits erschienen bzw. in Vorbereitung: André Willms C-Programmierung lernen 432 Seiten, ISBN 3-8273-1405-4 André Willms C++-Programmierung lernen 408 Seiten, ISBN 3-8273-1342-2 Guido Lang, Andreas Bohne Delphi 6 lernen 432 Seiten, ISBN 3-8273-1776-2 Walter Herglotz HTML lernen 323 Seiten, ISBN 3-8273-1717-7 Judy Bishop Java lernen 636 Seiten, ISBN 3-8273-1794-0 R. Allen Wyke, Donald B. Thomas Perl 5 lernen 441 Seiten, ISBN 3-8273-1650-2 Michael Ebner SQL lernen 336 Seiten, ISBN 3-8273-1515-8 René Martin XML und VBA lernen 336 Seiten, ISBN 3-8273-1952-8 René Martin VBA mit Word 2002 lernen 393 Seiten, ISBN 3-8273-1897-1 René Martin VBA mit Office 2000 lernen 576 Seiten, ISBN 3-8273-1549-2 Dirk Abels Visual Basic 6 lernen 425 Seiten, ISBN 3-8273-1371-6 Patrizia Sabrina Prudenzi VBA mit Excel 2000 lernen 512 Seiten, ISBN 3-8273-1572-7
Sandini Bib
Jürgen Bayer
Programmieren lernen Anfangen, anwenden, verstehen
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Sandini Bib
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig auch eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material. 10 9 8 7 6 5 4 3 2 1 05 04 03 02 ISBN 3-8273-1951-X © 2002 by Addison Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Korrektorat: Lektorat: Herstellung: CD-Mastering: Satz: Druck und Verarbeitung: Printed in Germany
4
Barbara Thoben, Köln Simone Meißner, Fürstenfeldbruck Christiane Auf,
[email protected] Tobias Draxler,
[email protected] Ulrike Hempel,
[email protected] Gregor Kopietz,
[email protected] mediaService, Siegen Media Print, Paderborn
Sandini Bib
I
Inhaltsverzeichnis
le
n e rn
V
Vorwort
1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8
Einführung Zum Buch Der Umgang mit der Konsole des Betriebssystems Lizenzvereinbarungen Warum individuelle Programme? Der Weg zum Profi Wie sucht ein Programmierer Informationen? Zusammenfassung Fragen und Übungen
11 11 22 25 27 29 34 44 45
2 2.1 2.2 2.3
Erste Schritte Einige Begriffe zuvor Hello World in Delphi/Kylix Eine Delphi/Kylix-Konsolenanwendung zur Berechnung eines Nettobetrags Entwicklung einer einfachen Delphi/Kylix-Anwendung mit grafischer Oberfläche Grundlagen zum Umgang mit der Delphi/KylixEntwicklungsumgebung Hello World in Java Hello World in Java mit Sun ONE Studio 4 Grundlagen zum Umgang mit Sun ONE Studio 4 Entwickeln einer Anwendung mit grafischer Oberfläche mit Sun ONE Studio 4 Die Weitergabe einer Anwendung Zusammenfassung Fragen und Übungen
47 48 50
2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12
9
62 69 84 85 92 105 107 116 117 117
5
Sandini Bib
3 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 4 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9
Basiswissen Wie arbeitet ein Computer? Wie werden Programme und Daten gespeichert? Wie werden Programme geschrieben und für den Computer übersetzt? Übersicht über die aktuellen Software-Architekturen Übersicht über die aktuellen Programmiersprachen Algorithmen Zusammenfassung Fragen und Übungen
119 119 129
Grundlagen der Programmierung Variablen Grundlagen zu Datentypen Strukturen Elementare Anweisungen Verwenden der Bibliotheken einer Programmiersprache Einfaches Debuggen Abfangen von Ausnahmen Zusammenfassung Fragen und Übungen
181 182 187 212 213
5 5.1 5.2 5.3 5.4 5.5 5.6 5.7
Die Strukturierung eines Programms Strukturierte und unstrukturierte Programme Vergleichsausdrücke Grundlagen zu Schleifen und Verzweigungen Schleifen Verzweigungen Funktionen und Prozeduren Bibliotheken: Funktionen und Prozeduren in eigenen Modulen 5.8 Variablen in Modulen 5.9 Zusammenfassung 5.10 Fragen und Übungen
6
, Q K D OW V Y H U ] HL F K Q L V
141 150 160 166 178 179
231 245 250 256 257 259 260 262 273 276 288 297 316 328 336 337
Sandini Bib
6 6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 6.9 6.10 6.11 6.12 6.13 6.14 6.15
Objektorientierte Programmierung Was ist objektorientierte Programmierung? Welche Vorteile bietet die OOP? Einfache Klassen und deren Anwendung Grundsätze zum Entwurf von Klassen Eine kleine Übung Die Referenz self bzw. this Private und öffentliche Elemente einer Klasse Überladen von Methoden Initialisieren von Klassen: Konstruktoren Aufräumarbeiten: Destruktoren Datenkapselung Vererbungs-Grundlagen Weitere Möglichkeiten, die nicht besprochen werden Zusammenfassung Fragen und Übungen
339 340 347 356 370 373 378 382 386 389 394 396 404 406 407 407
7 7.1 7.2 7.3 7.4
Daten speichern Speichern im Arbeitsspeicher Verwalten von (Text-)Dateien Zusammenfassung Fragen und Übungen
409 409 423 428 429
8 8.1 8.2 8.3 8.4 8.5
Programmieren einer Beispielanwendung Einleitung Vorbereitungen Einlesen der Daten Test des ersten Entwurfs Programmierung der eigentlichen Funktionalität des Programms 8.6 Weitere interessante Features 8.7 Zusammenfassung
431 431 432 437 442
9 Daten in Datenbanken verwalten 9.1 Was ist eine Datenbank? 9.2 Datenbankdesign light: Erzeugen der Beispieldatenbank mit MySQL 9.3 Daten mit SQL bearbeiten 9.4 Daten in Java-Programmen bearbeiten 9.5 Zusammenfassung 9.6 Fragen und Übungen
453 454
443 450 451
460 467 472 483 484
,QKDOWVYHU]HLFKQLV
7
Sandini Bib
8
N
Nachwort
485
A A.1 A.2 A.3 A.4 A.5
Anhang Lösungen zu den Fragen und Übungen Glossar ASCII-Tabelle ISO-8859-1 Wichtige One Studio 4-Tastenkombinationen Wichtige Kylix/Delphi-Tastenkombinationen
487 487 498 509 511 512
S
Stichwortverzeichnis
515
, Q K D OW V Y H U ] HL F K Q L V
Sandini Bib
V
Vorwort
le
n e rn
Hallo lieber Leser1. Sie wollen programmieren lernen!? Dann halten Sie wohl das richtige Buch in Ihren Händen. Dieses Buch vermittelt die Grundlagen der Programmierung, zeigt daneben aber auch aktuelle Wege zur Lösung von Programmier-Problemen. Trotz des Ziels (das natürlich auch ein wenig Theorie erfordert), ist das Buch nicht allzu theoretisch. Schon von Anfang an können (und sollen) Sie eigene Programme entwickeln und ausführen. Das Buch erläutert in einer verständlichen Form alle wichtigen Handwerkszeuge eines Programmierers und geht dabei einen sehr modernen Weg. Sie lernen neben den absoluten Grundlagen z. B. auch, wie Sie heutzutage Programmier-Probleme effizient lösen, wie Sie Programme entwickeln, die in Fenstern dargestellt werden, wie Sie objektorientiert programmieren und wie Sie in einem Programm auf die in einer Datenbank gespeicherten Daten zugreifen. Wenn Sie dieses Buch gelesen und die einzelnen praktischen Themen nachvollzogen haben, kennen Sie zwar (natürlich) noch nicht alle Programmier-Techniken. Sie beherrschen aber die Grundlagen und sind in der Lage, mit ein wenig zusätzlicher Recherche Programme zur Lösung der allermeisten »normalen« Probleme zu schreiben. Wollen Sie z. B. für einen Freund oder eine Freundin ein Programm zur Verwaltung deren Musik-CD-Sammlung schreiben? Das wird nach dem Durcharbeiten dieses Buchs für Sie kein Problem darstellen. Programmieren ist ein weites Feld. Deshalb stand ich bei der Konzeption dieses Buchs vor der Entscheidung, entweder nur die Kern-Themen oder auch mehr am Rande angesiedelte Themen zu behandeln. Einige dieser Rand-Themen (wie z. B. spezielle Algorithmen2) wären sicherlich 1.
Obwohl ich selbst ein ausgesprochener Feminist bin, verwende ich im Buch ausschließlich die übliche männliche Form. Immer doppelgleisig zu schreiben (nach dem Muster »der/die Leser(in)« wäre einfach zu aufwändig und würde den Lesefluss behindern.
2.
Hier finden Sie bereits die erste Erläuterung eines Fachworts: Ein Algorithmus ist die Beschreibung der einzelnen, zur Lösung eines Problems notwendigen Schritte.
9
Sandini Bib
für einige Leser sehr interessant. Hätte ich diese aber im Buch behandelt, hätte ich die zentralen Themen reduzieren müssen. Und dagegen spricht mein Stil, einzelne Themen immer so zu beschreiben, dass Sie (der Leser) darin alle wichtigen Informationen finden und über (möglichst) alle potenziellen Probleme informiert werden. Also habe ich entschieden, die Rand-Themen nicht im Buch zu behandeln. Damit Sie sich aber trotzdem darüber informieren können, finden Sie auf der Buch-CD einige zusätzliche Artikel .
-
Ich beschreibe alle Programmierthemen im Buch anhand der Programmiersprachen Object Pascal (in Form von Delphi 6 und Kylix 2) und Java. Es mag vielleicht etwas ungewöhnlich sein, dass Sie in einem Anfänger-Buch gleich zwei Programmiersprachen lernen. Aber dieses Vorgehen entspricht dem Ziel dieses Buchs, sich nicht auf die Spezialitäten einer Programmiersprache zu beschränken, sondern Ihnen die generellen und gemeinsamen Grundlagen zu vermitteln. Daneben erhalten Sie auch einen recht guten Überblick über die unterschiedlichen Konzepte zweier Programmiersprachen. Zum Anderen sind Delphi, Kylix und Java sehr aktuelle Sprachen. Schließlich können Sie auch selbst entscheiden, welche dieser Sprachen Sie zum Nachvollziehen der Beispiele und zur Vertiefung des Erlernten verwenden. Ich danke meinem Lektor Tobias Draxler für die nette und verständnisvolle Unterstützung und vor allen Dingen auch für die sehr hilfreiche konstruktive Kritik. Seinen Ideen folgend beschreibe ich im Buch u. a. auch die Programmierung unter Linux. Meinem guten Freund Alistair Firth danke ich für die Begutachtung des Manuskripts und für die hilfreichen Anregungen. Ich denke, dass er nun programmieren kann und mir in unseren gemeinsamen Projekten deshalb in Zukunft viel Arbeit abnimmt, sodass ich (noch) öfter die Möglichkeit habe, in Urlaub zu fahren. Und ich danke auch Merve und Sefa, den Nachbarskindern, die mir gezeigt haben, wie viele Spiele auf meinem SuSE Linux installiert sind und die mich fast jeden Tag aus meinem Schreiballtag herausgerissen und mich immer wieder auf neue Ideen gebracht haben. Ich wünsche Ihnen viel Spaß beim Lesen und vor allen Dingen auch beim Programmieren. Falls Sie Fragen, Anregungen oder Kritik zum Buch haben, senden Sie mir einfach eine E-Mail. Jürgen Bayer
[email protected]
10
9RUZRUW
Sandini Bib
1
Einführung
Sie erfahren in dieser Einführung:
le
n e rn
• was in diesem Buch enthalten ist, • welche Zielgruppe dieses Buch anvisiert und welche Voraussetzungen Sie als Leser mitbringen sollten, • wie Sie mit der Konsole Ihres Betriebssystems umgehen, • welches Betriebssystem und welche Hardware Sie besitzen sollten, • warum überhaupt individuelle Programme geschrieben werden, wo es doch so viele fertige Programme gibt, • wie im Allgemeinen der Weg aussieht, der zum Programmier-Profi führt, • wie Sie effizient lernen und • wo und wie Sie beim Programmieren Informationen, Lösungen und Hilfestellungen suchen und finden. Dieses Kapitel schafft die Voraussetzungen dafür, dass Sie die weiteren Kapitel des Buchs erfolgreich durcharbeiten können. Ich vermittle hier noch kein Programmierwissen im eigentlichen Sinn.
1.1
Zum Buch
1.1.1 Der Inhalt dieses Buchs Dieses Buch hilft Ihnen dabei, Programmieren zu lernen. Es behandelt die grundlegenden Themen, die Sie kennen müssen, um erfolgreich in die Programmierung einzusteigen. Wenn Sie das Buch gelesen (und verstanden) haben, wird es für Sie kein Problem sein, die Features einer Programmiersprache zu erforschen und damit Programme zu entwickeln.
=XP %XFK
11
Sandini Bib
Die meisten Bücher zu einzelnen Programmiersprachen setzen voraus, dass Sie die Grundlagen der Programmierung beherrschen. Kaum ein Autor hat in seinem speziellen Buch Platz für grundlegende Erläuterungen wie beispielsweise die Beschreibung der Bedeutung von Variablen bei der Programmierung. Programmiereinsteiger müssen aber auch diese Grundlagen irgendwann einmal lernen. Das ist das Thema dieses Buchs. Ich lege dabei Wert auf das aus heutiger Sicht wichtige Wissen. Dazu gehören neben den „normalen“ Grundlagen (Grundwissen, Umgang mit Compilern, Interpretern und Entwicklungsumgebungen, Programmstrukturierung etc.) auch Informationen zu einem guten Programmierstil (den viele Programmierer leider nicht beherrschen). Eher theoretische Themen wie spezielle Algorithmen zur Lösung komplexer Probleme werden im Buch nicht behandelt. Als Programmiersprachen setze ich im Buch Java und Object Pascal ein. Sie können mit beiden Sprachen unter Windows oder unter Linux programmieren. Für Java verwenden Sie dabei für beide Betriebssysteme identische Werkzeuge. Als Java-Entwicklungsumgebung setze ich im Buch Sun ONE Studio 4 ein. Object Pascal-Programme entwickeln Sie unter Windows mit Delphi 6 oder unter Linux mit Kylix 2. Delphi und Kylix unterscheiden sich kaum. Das Buch wird deshalb also nicht unübersichtlich. Der Vorteil ist, dass es für die Arbeit mit dem Buch vollkommen unerheblich ist, ob Sie nun Windows oder Linux einsetzen (obwohl bei Linux schon einige Einschränkungen auf bestimmte Distributionen bestehen, wie ich es im CD-Artikel „Installation“ beschreibe).
Die Programmiersprachen
Zum Zeitpunkt der Drucklegung dieses Buchs hat Borland gerade die neuen Versionen von Kylix und Delphi angekündigt. Es kann also sein, dass Sie mit Delphi 7 oder Kylix 3 arbeiten. Für das Buch macht das aber keinen Unterschied. Lediglich die Menüs, die Fenster und die Dialoge dieser Entwicklungsumgebungen könnten dann etwas anders aufgebaut sein, als ich es im Buch beschreibe. In Kapitel 2 lernen Sie zunächst, wie Sie Java- und Object PascalProgramme grundsätzlich entwickeln. In diesem Kapitel schreiben Sie bereits Ihre ersten Programme, ohne allerdings Näheres zu dem zu erfahren, was Sie da machen. Dieses Kapitel zeigt, wie Programme heutzutage geschrieben werden und erläutert die zwei grundsätzlichen Programm-Arten am Beispiel. Außerdem lernen Sie hier, mit den Entwicklungsumgebungen für Object Pascal und Java umzugehen.
Der Aufbau des Buchs
12
(LQIKUXQJ
Sandini Bib
In Kapitel 3 gehe ich dann auf das zum Programmieren notwendige Basiswissen ein. Ich erläutere in diesem Kapitel zunächst die grundsätzliche Arbeitsweise des Computers (aus Programmierersicht) und zeige, wie Programme aus dessen Sicht aussehen. Danach erfahren Sie, wie Daten in Programmen und in Dateien gespeichert werden. Hier lernen Sie u. a., was Bits und Bytes sind und wie Textdaten so transformiert werden, dass diese in Form von Zahlen gespeichert werden können. Danach eige ich, wie Programme geschrieben und für den Computer übersetzt werden, beschreibe die verschiedenen Arten von Programmen und beleuchte die Unterschiede zwischen den einzelnen Programmiersprachen. Schließlich erfahren Sie noch, was ein Algorithmus ist und lernen, einen solchen zu entwerfen. Nachdem Sie wissen, wie Programme grundsätzlich arbeiten und entwickelt werden, beschreibe ich in Kapitel 4 die grundlegenden Techniken, die Sie beim Programmieren tagtäglich einsetzen. Sie lernen in diesem Kapitel, welche Programmtechniken Sie zur Umsetzung eines Algorithmus in einem Programm verwenden (wobei ich noch nicht auf Schleifen und Verzweigungen eingehe). Da Sie gerade am Anfang viele Fehler machen werden, erfahren Sie in diesem Kapitel gleich auch, wie Sie logische Fehler in Ihren Programmen auf eine möglichst einfache Weise finden und wie Sie Fehler, die durch ungültige Eingaben entstehen, behandeln. In Kapitel 5 lernen Sie dann, Ihre Programme zu strukturieren. Ich zeige zunächst, wie Sie einen Programmteil wiederholt oder bedingungsabhängig ausführen können. Dann lernen Sie, Programmteile, die Sie an mehreren Stellen wiederverwenden können, in Funktionen oder Prozeduren zu schreiben. Dabei gehe ich auch noch ein wenig auf die veraltete strukturierte Programmierung ein (nur damit Sie wissen, worum es sich dabei handelt). Das Kapitel 6 behandelt die Grundprinzipien der objektorientierten Programmierung (OOP). In diesem Kapitel beschreibe ich zunächst, was die objektorientierte von der klassischen strukturierten Programmierung unterscheidet und beleuchte die Vorteile der OOP. Danach lernen Sie die Grundprinzipien der OOP kennen und erstellen Ihre ersten eigenen Klassen. Ich lege in diesem Kapitel (wie auch in Kapitel 4 und 5) viel Wert auf die Grundlagen und darauf, dass Sie wissen, warum Sie objektorientiert programmieren sollten. Die meisten Programme verwalten Daten im Arbeitsspeicher und in Dateien oder Datenbanken. Deshalb zeige ich zunächst in Kapitel 7, wie Sie Daten effektiv im Programm verwalten und wie Sie einfache Textdateien lesen und schreiben. In diesem Kapitel erfahren Sie einiges über moderne Techniken der Speicherung von Daten im Arbeitsspeicher.
=XP %XFK
13
Sandini Bib
Um ein wenig Praxis in das Buch zu bringen, erstellen Sie in Kapitel 8 ein Programm, über das Sie Adressdaten in einer Textdatei verwalten und bearbeiten können. Das Beispielprogramm setzt viele der bis zu diesem Kapitel erlernten Techniken, wie z. B. die objektorientierte Programmierung, das Strukturieren von Programmen und das Abfangen von Fehlern ein, und soll Ihnen zeigen, wie diese Techniken in der Praxis verwendet werden. In Kapitel 9 zeige ich dann, wie Sie in Java-Programmen Daten in Datenbanken verwalten. Dabei gehe ich auf die Grundlagen von Datenbanken, auf die Standard-Datenbank-Abfragesprache SQL (Structured Query Language) und auf die Verwaltung von Datenbanken in einem Programm ein. Mit diesem Kapitel schließe ich das Buch ab.
1.1.2 Das Konzept Das Konzept des Buchs ist so angelegt, dass Ihnen die einzelnen Themen grundlegend erläutert werden. Sie werden keine vertieften, speziellen Informationen finden, aber immer das wichtige Grundlagenwissen. Da Programme und die zum Programmieren verwendeten Tools leider auch manchmal etwas komplex sind, beschreibe ich immer auch die möglichen Probleme und zeige den Lösungsweg (ohne allerdings zu tief darauf einzugehen). Die behandelten Programmierthemen verdeutliche ich mit vielen möglichst einfachen Beispielen. Die Beispiele finden Sie natürlich auch auf der Buch-CD. Um das Verständnis dieser Programme nicht unnötig zu erschweren, verwende ich in den Beispielen möglichst1 keine englischen, sondern deutsche Begriffe (obwohl ich selbst lieber englische Begriffe einsetze). Jedes Kapitel enthält am Ende eine Zusammenfassung, die Sie darüber informiert, was Sie nach der Lektüre dieses Kapitels (und nach dem damit verbundenen Ausprobieren an Ihrem Computer) wissen sollten. Alle Kapitel bis auf Kapitel 1 und 8 sind zudem mit einer Fragen-/Aufgabensammlung abgeschlossen. Hier können Sie Ihr neu erworbenes Wissen testen. Die Lösungen zu den Fragen und Aufgaben finden Sie im Anhang. In einem Programmierbuch kann ich natürlich nicht verhindern, Fachvokabular einzusetzen. Ich erläutere aber möglichst jeden neuen Begriff. Begriffe, die nicht direkt mit dem jeweiligen Thema in Verbindung
1.
14
Auch wenn ich mich noch so angestrengt habe: in einigen Fällen haben sich mit Sicherheit auch englische Begriffe in die Beispiele eingemogelt
-
(LQIKUXQJ
Sandini Bib
stehen, aber Ihnen bekannt sein sollten, erkläre ich (hoffentlich immer ) mit einer Fußnote. Außerdem finden Sie im Anhang ein Glossar, das die Fachbegriffe noch einmal näher erläutert.
-
1.1.3 Die Zielgruppe Die Zielgruppe dieses Buchs sind Leser, die mit Windows oder Linux sicher umgehen können, die aber noch nie selbst programmiert haben. Sie sollten also Programme bedienen, mit Dateien umgehen (speichern, öffnen, kopieren etc.) und Programme installieren können. Ich verzichte im Buch auf grundlegende Erläuterungen zu diesen Dingen um das eigentliche Thema möglichst umfangreich behandeln zu können. In welche Richtung Sie bei der Programmierung später gehen, ist im Moment unerheblich. Ob Sie Spieleprogrammierer werden wollen, in der Forschung mit künstlicher Intelligenz experimentieren oder datenbankgestützte Anwendungen für mittelständische Unternehmen entwickeln wollen, macht für dieses Buch keinen Unterschied. Alle Programmierer müssen die Grundlagen beherrschen.
1.1.4 Voraussetzungen Was Sie wissen sollten Da dieses Buch ein Grundlagenbuch ist, setze ich keine besonderen Kenntnisse voraus. Sie sollten lediglich recht sicher mit Windows bzw. Linux umgehen können, je nachdem, welches Betriebssystem Sie einsetzen (andere Betriebssysteme werden im Buch nicht berücksichtigt). Prinzipiell reicht es aus, wenn Sie Programme starten und installieren und mit der Eingabeaufforderung (Windows) bzw. der Shell (Linux) umgehen können. Für den Fall, dass Sie diese noch nicht kennen, beschreibe ich ab Seite 22 kurz, worum es sich dabei handelt und wie Sie diese starten und benutzen. Betriebssystem- und Hardware-Voraussetzungen Die Entwicklungsumgebungen bzw. Tools für die verwendeten Programmiersprachen setzen einiges an Ihrem System voraus, das ich im Folgenden beschreibe. In der Datei links.htm finden Sie Links zu Internetseiten, in denen die Systemvoraussetzungen vom jeweiligen Hersteller umfassend beschrieben werden.
=XP %XFK
15
Sandini Bib
Wenn Sie Windows einsetzen, benötigen Sie:
Windows-Voraussetzungen
• Windows 98, Me, NT 4, 2000 oder XP, • einen Prozessor mit mindestens 500 MHz oder • einen Prozessor mit mindestens 166 MHz, wenn Sie Sun ONE Studio 4 nicht installieren, • mindestens 256 MB Arbeitsspeicher oder • mindestens 32 MB Arbeitsspeicher, wenn Sie Sun ONE Studio 4 nicht installieren, • 75 bis 160 MB freien Festplattenplatz für Delphi, • 125 MB freien Festplattenplatz für Sun ONE Studio 4 oder 70 MB für das Java-SDK, • 160 MB freien Festplattenplatz für die optionale Java-Dokumentation. Unter Linux benötigen Sie ein System mit den folgenden Mindestdaten:
Linux-Voraussetzungen
• idealerweise Red Hat Linux ab Version 7.1 oder SuSE Linux ab Version 7.2, • einen Intel-kompatiblen Prozessor mit mindestens 500 MHz oder • einen Intel-kompatiblen Prozessor mit mindestens 200 MHz, wenn Sie Sun ONE Studio 4 nicht installieren, • mindestens 256 MB Arbeitsspeicher oder • mindestens 64 MB Arbeitsspeicher, wenn Sie Sun ONE Studio 4 nicht installieren, • 110 MB freien Festplattenplatz für Kylix, • 125 MB freien Festplattenplatz für Sun ONE Studio 4 oder 75 MB für das Java-SDK, • 145 MB freien Festplattenplatz für die optionale Java-Dokumentation. Falls Ihre Rechner die Anforderungen für Sun ONE Studio 4 nicht erfüllt, sollten Sie trotzdem versuchen, diese Entwicklungsumgebung zu installieren. Eventuell ist es nun auch einmal Zeit, den Rechner ein wenig aufzurüsten. Ohne Sun ONE Studio 4 können Sie zwar die meisten Java-Beispiele des Buchs nachvollziehen, nicht aber die Java-Programme, die mit einer grafischen Oberfläche arbeiten. Linux-Besonderheiten Kylix 2 unterstützt Red Hat Linux ab Version 7.1, SuSE ab Version 7.2 und Mandrake ab Version 8.0. Diese Distributionen dürften also keine Probleme verursachen. Die meisten Kylix-Features wurden ebenfalls unter ALT Linux Master 2.0 und Caldera Open Linux 3.1.1 getestet. Kylix dürfte in diesen Linux-Distributionen wohl einigermaßen gut laufen.
16
(LQIKUXQJ
Sandini Bib
Java unterstützt offiziell laut Sun (dem Hersteller) nur Red Hat Linux 6.2 und 7.1. Java 1.4 wurde allerdings auch in den englischen Versionen von TurboLinux 6.5, Caldera 3.1, Cobalt mit Kernel 3.2 und glibc und SuSE 7.1 getestet. Sun ONE Studio 4 wurde laut Sun lediglich unter Red Hat Linux 7.2 getestet, läuft aber auch auf meinem SuSE Linux 7.3. Unter anderen Distributionen, auf denen das Java-SDK installiert werden kann, sollte Sun ONE Studio 4 allerdings auch ausgeführt werden können, da diese Entwicklungsumgebung anscheinend2 komplett in Java programmiert wurde. Ich verwende für das Buch SuSE Linux 7.3 in der deutschen Version.
1.1.5 Die Programmiersprachen Ich habe lange nach den „richtigen“ Programmiersprachen für dieses Buch gesucht, was keine leichte Aufgabe war. Damit Sie verstehen, warum ich Object Pascal und Java als optimale Sprachen gefunden habe, zeige ich den Weg auf, der zu dieser Entscheidung geführt hat. Nebenbei erfahren Sie hier auch einiges über moderne Programmiersprachen und die Qual der Wahl, vor der Sie später bei der Entwicklung eigener Anwendungen stehen werden. Eine „exotische“ Sprache wie Eiffel oder Python wollte ich nicht verwenden, es sollte schon eine Sprache sein, die von möglichst vielen Programmierern eingesetzt wird (und eine, die ich kenne ). Außerdem sollte es sich dabei um eine „echte“ Programmiersprache handeln und nicht um eine Scriptsprache (wie JavaScript). Übrig blieben die Sprachen C++, Visual Basic 6, Object Pascal (in Form von Delphi und Kylix), Visual Basic. NET, C# und Java.
-
Die ersten Entscheidungen fielen recht schnell: C++ ist zu kompliziert und zu fehlerträchtig für den Beginn. Visual Basic 6 beherrscht wichtige Konzepte wie das der objektorientierten Programmierung nicht und muss zudem gekauft werden. Object Pascal, Visual Basic .NET, C# und Java sind sehr gut geeignet, die Grundlagen der Programmierung zu lernen. Jede dieser Sprachen wäre für dieses Buch gut geeignet. Ein weiteres Kriterium zur Auswahl einer Programmiersprache war die Möglichkeit, die notwendigen Entwicklungs-Tools auf allen gängigen Windows- und Linux-Betriebssystemen installieren zu können. Das für
2.
Wenigstens ist dies für die NetBeans-Entwicklungsumgebung der Fall, die die Basis von ONE ist.
=XP %XFK
17
Sandini Bib
Visual Basic .NET und C# notwendige .NET-Framework SDK lässt sich leider nur unter Windows NT 4, Windows 2000 und Windows XP ab der Version „Professional“ installieren. Windows 95 bis Me und Windows XP Home bleiben außen vor. Ein .NET-Framework für Linux ist zurzeit zwar (im Mono-Projekt; www.go-mono.com) in der Entwicklung, aber noch lange nicht fertiggestellt. C# wäre in meinen Augen die optimale Programmiersprache für dieses Buch. Ich kann aber von Lesern, die das Programmieren erst lernen wollen, nicht verlangen, eines der professionellen Windows-Betriebssysteme zu kaufen und zu installieren. Was wohl besonders dann gilt, wenn es sich beim Leser um einen Linux-Anhänger handelt .
-
Übrig blieben also Object Pascal und Java. Die Object Pascal-Entwicklungsumgebungen Delphi und Kylix bieten einen hohen, sehr modernen Komfort bei der Entwicklung von Programmen. Eigentlich würde Object Pascal für das Buch ausreichen. Um aber auch eine Sprache zu behandeln, die anderen Konzepten folgt als Object Pascal, und die zudem sehr viel Ähnlichkeit mit C++, C# und JavaScript hat, setze ich im Buch auch Java ein. So lernen Sie gleich zwei moderne und wichtige Programmiersprachen kennen. Delphi läuft unter Windows, Kylix ist eine nahezu identische Entwicklungsumgebung für Linux. Beide Varianten arbeiten mit der Programmiersprache Object Pascal und sind zueinander kompatibel. Borland bietet für Delphi und Kylix je eine kostenlose Edition an, die Delphi Personal Edition bzw. die Kylix Open Edition. Delphi kann unter Windows ab Version 98 ausgeführt werden. Kylix läuft ohne Probleme auf verschiedenen Linux-Distributionen und mit einigen Patches auch auf anderen LinuxSystemen (siehe im Artikel „Installation“ auf der Buch-CD).
Delphi und Kylix
Über die Delphi Personal Edition und die Kylix Open Edition können Sie problemlos einfache Anwendungen erzeugen. Die kommerziellen Editionen dieser Entwicklungsumgebungen besitzen zusätzliche Features zur Erzeugung von Internet- und verteilten Anwendungen, zusätzliche Komponenten, Features zum Datenbankzugriff und natürlich noch ein Menge andere Dinge. Für das Buch reichen die kostenlosen Editionen von Delphi und Kylix aber aus. Leider fehlt diesen der Datenbankzugriff, der in professionellen Programmen eine große Rolle spielt. Den Datenbankzugriff zeige ich deshalb im Buch am Beispiel von Java. Wenn Sie die Unterschiede zwischen den einzelnen Delphi- und KylixEditionen kennen lernen wollen, lesen Sie die Feature-Vergleiche auf den Seiten www.borland.com/delphi/pdf/del6_feamatrix.pdf und www.borland.com/kylix/pdf/kyl2_feamatrix.pdf.
18
(LQIKUXQJ
Sandini Bib
Die für Java notwendigen Programme und Tools können Sie unter den verschiedensten Betriebssystemen, also auch unter allen aktuellen Windows- und Linux-Versionen, installieren. Da Java prinzipiell eine kostenfreie Programmiersprache ist, enthält die Java-Installation alles, was Sie beim Programmieren (mindestens) benötigen. Eine Entwicklungsumgebung, von denen Sie im Internet zahlreiche finden, müssen Sie aber separat installieren. Hilfreiche Informationen dazu finden Sie auf der Website von Simon Peter: www.simonpeter.com/techie/java_ide.html. Für dieses Buch verwende ich Sun ONE Studio 4, die Entwicklungsumgebung von Sun.
Java
Sie sehen, wie viele Überlegungen in die Wahl der Programmiersprache einfließen. Das ist nicht nur beim Schreiben eines Buchs der Fall, sondern oft auch in professionellen Softwareprojekten. Für Sie als Programmierer bedeutet dies, dass Sie sich nicht nur auf eine Programmiersprache konzentrieren sollten. Das Buch geht deshalb auch den Weg, gleich zwei moderne Programmiersprachen mit unterschiedlichen Konzepten zu beschreiben.
1.1.6 Die Installation Die Installation der benötigen Programme beschreibe aus Platzgründen ich nicht im Buch, sondern im Artikel „Installation“, den Sie im Ordner Dokumente auf der Buch-CD finden. Sie können diesen Artikel auch im Internet unter der Adresse www.juergen-bayer.net/buecher/programmierenlernen/artikel/installation.html abrufen.
1.1.7 Die Buch-CD Auf der beiliegenden CD finden Sie alle Beispiele des Buchs, Installationsprogramme für die im Buch eingesetzten Programme, zusätzliche Dokumente und eine HTML-Datei links.htm, die alle im Buch angesprochenen Internetadressen als Link enthält. Im Ordner Tipps finden Sie zwei hilfreiche HTML-Dateien, die wichtige Tipps und Tricks zu Delphi, Kylix und Java enthalten und die so organisiert sind, dass Sie diese Tipps und Tricks sehr schnell finden.
1.1.8 Informationen im Internet Auf meiner Website finden Sie unter der Adresse www.juergen-bayer.net/ buecher/programmierenlernen alle Artikel, die auch auf der CD gespeichert sind (Artikel im Internet abzurufen ist in meinen Augen oft einfacher als eine CD einzulegen und zu durchsuchen), weitere Informationen zum Buch, ein aktuelles Dokument mit Links zu den im Buch angesproche-
=XP %XFK
19
Sandini Bib
nen Themen und gegebenenfalls ein Erratum, in dem ich eventuelle Fehler (die ja leider immer wieder vorkommen) korrigiere und Updates beschreibe.
1.1.9 Icons und typografische Konventionen Dieses Buch verwendet einige typografische Konventionen, die dem allgemeinen Standard entsprechen, und verschiedene Icons, die Ihnen die Orientierung erleichtern sollen. Syntaxbeschreibungen Syntaxbeschreibungen3 (die im Buch eher selten vorkommen, weil das Buch mehr auf Beispielen aufbaut) erläutern, wie Sie Ihren Quellcode schreiben müssen, damit dieser korrekt übersetzt werden kann. Sie sind in der Schriftart Courier gedruckt. Das folgende Beispiel zeigt die Syntaxbeschreibung einer If-Verzweigung in Object Pascal: if Vergleichsausdruck then begin Anweisungen end;
Kursive Wörter in diesen Beschreibungen sind Platzhalter für von Ihnen anzugebende Informationen. Im obigen Beispiel müssen Sie für Vergleichsausdruck z. B. einen Vergleich und für Anweisungen eine oder mehrere Anweisungen eintragen. Das folgende Beispiel demonstriert dies: if i = 1 then begin writeln('i ist gleich 1'); end;
Was Sie an diesen kursiven Wörtern tatsächlich angeben, bleibt natürlich Ihnen überlassen. Es muss lediglich in logischer und syntaktischer Hinsicht passen.
3.
20
Als „Syntax“ werden die Regeln bezeichnet, nach denen die Worte und Zeichen einer Sprache aneinander gereiht werden müssen, damit ein Compiler oder Interpreter, der das Programm übersetzt, diese versteht.
(LQIKUXQJ
Sandini Bib
In vielen Syntaxbeschreibungen außerhalb dieses Buchs werden einzelne Elemente in eckigen Klammern dargestellt. Diese Elemente sind optional, d. h. sie können angegeben werden, müssen aber nicht. Lassen Sie das entsprechende Element weg, so wird automatisch eine Voreinstellung verwendet. Die eckigen Klammern sind dann übrigens nur Teil der Syntaxbeschreibung und werden nicht im Programmcode angegeben. Beispiel-Listings Alle Programmierthemen werden an Hand von Beispielen erläutert und vertieft. Beispiel-Listings werden in der Schriftart Courier dargestellt: 01 02 03 04 05 06
write("Geben Sie eine Zahl ein: "); readln(zahl1); write("Geben Sie eine weitere Zahl ein: "); readln(zahl2); ergebnis = zahl1 + zahl2; writeln(ergebnis);
Die einzelnen Zeilen von kompletten Beispielen werden immer numeriert. Das erleichtert den Bezug auf Zeilen, wenn Beispiele im Buch erläutert werden. Die Zahlen dürfen Sie nicht mit eingeben, wenn Sie Beispiel-Listings nachvollziehen. Exkurse Für den Fall, dass ein Begriff, der eigentlich nicht direkt zum jeweiligen Thema gehört, doch kurz beschrieben werden soll, erfolgt diese Beschreibung in Form eines Exkurses. Ein Exkurs wird wie dieser Absatz dargestellt. Typografische Konventionen im Fließtext Im normalen Text werden sprachspezifische Schlüsselwörter in der Schriftart Courier dargestellt. Wörter, die Teile der Benutzerschnittstelle, wie z. B. Menübefehle und Schalter bezeichnen, werden so dargestellt: DATEI/ÖFFNEN. Der Schrägstrich trennt die einzelnen Ebenen von Menübefehlen. Das Beispiel bezeichnet z. B. den Befehl ÖFFNEN im DATEI-Menü. Internetadressen werden folgendermaßen gekennzeichnet: www.addison-wesley.de. Tastenkappen wie (F1) stehen für Tasten und Tastenkombinationen, die Sie betätigen können, um bestimmte Aktionen zu starten.
=XP %XFK
21
Sandini Bib
Die Symbole Zur Erleichterung der Orientierung verwendet dieses Buch verschiedene Symbole für Textabschnitte mit besonderer Bedeutung. Über dieses Symbol werden Hinweise und besondere Informationen gekennzeichnet, die Sie beachten sollten. Programmiersprachen und die beim Programmieren verwendeten Komponenten bieten häufig einige Fallen, in die Sie arglos tappen können. Spezielle „Achtung“-Hinweise, die mit diesem Icon gekennzeichnet sind, machen auf diese Fallen aufmerksam und bieten natürlich – sofern möglich – gleich auch eine Lösung. Dieses Symbol kennzeichnet allgemeine Tipps und Hinweise zur Lösung bestimmter Probleme, die im Buch nicht weiter behandelt werden. An diesem „Referenz“-Symbol finden Sie Hinweise auf Webseiten, CDDateien und Bücher mit weiteren Informationen. Das Übungs-Icon kennzeichnet Fragen und Aufgaben, über die Sie Ihr Wissen testen können. Diese Übungen finden Sie immer am Ende eines Kapitels. Programm-Beispiele, die in der Programmiersprache Java verfasst sind, erkennen Sie an diesem Symbol. Dieses Symbol steht für Beispiel-Programme in der Programmiersprache Object Pascal (in Form von Delphi bzw. Kylix).
1.2
Der Umgang mit der Konsole des Betriebssystems
Windows und Linux besitzen eine Konsole, in der Sie Befehle eingeben und Programme starten können. In Windows wird diese als „Eingabeaufforderung“ bezeichnet, Linux nennt die Konsole „Shell“. Diese Konsole kennt dazu einige Befehle wie beispielsweise dir zum Auflisten des Inhalts des aktuellen Ordners und copy zum Kopieren mit Dateien. Daneben erlaubt eine Konsole aber auch das eingeschränkte Programmieren in Batch-Dateien (Windows) bzw. Shell-Skripten (Linux). Mit Hilfe solcher Programme, die in einfachen Textdateien gespeichert sind, wer-
22
(LQIKUXQJ
Sandini Bib
den meist einfache, häufig wiederkehrende Aufgaben automatisiert. Unter Linux werden viele Programme beispielsweise über Shell-Skripte installiert. Die Eingabeaufforderung starten Sie unter Windows über das Startmenü im Ordner Programme/Zubehör.
Abbildung 1.1: Die Windows-Eingabeaufforderung
Linux besitzt gleich mehrere Shells, die unterschiedliche Befehle verstehen und früher (als es in UNIX noch keine grafische Oberfläche gab) für verschiedene Zwecke verwendet wurden. In der C-Shell können Sie Shell-Skripte beispielsweise in einer der Programmiersprache C ähnlichen Syntax schreiben. Heute wird wohl überwiegend die bash4-Shell eingesetzt. Wenn Ihr Linux nicht direkt mit einer Shell, sondern mit der KDE (der heutzutage standardmäßig benutzten grafischen Oberfläche) startet, öffnen Sie eine Shell entweder über das Programm-Menü (System/Kommandozeilen), den Ausführen-Dialog ((Alt) + (F2)), indem Sie bash eingeben und (¢) betätigen oder über das Muschel-Icon in der Kontrollleiste.
Linux Shells
Diese Informationen beziehen sich auf die KDE in der Version 2.2. In anderen Versionen oder bei einer angepassten Oberfläche kann der Start einer Shell auch auf andere Weise erfolgen.
4.
bash steht für „Bourne Again Shell“, ein Wortspiel, das sich auf die alte Bourne-Shell bezieht.
'HU 8PJDQJ PLW GHU .RQVROH GHV %HWULHEVV\VWHPV
23
Sandini Bib
Abbildung 1.2: Die Linux-bash-Shell in der KDE Aktueller Ordner
In der Konsole ist immer ein Ordner aktuell. In Windows bis XP ist dies standardmäßig der Stammordner der Betriebssystempartition, in Linux und Windows startet die Konsole in Ihrem Home-Ordner. Mit dem Befehl cd Ordner können Sie (in Windows und Linux) in einen anderen Ordner wechseln. In Windows können Sie durch die Eingabe des Laufwerk-Bezeichners (c:, d: etc.) zu einem anderen Laufwerk wechseln.
Befehle
In der Konsole können Sie Befehle eingeben. Shell-Befehle unterscheiden sich je nach Konsole und Betriebssystem. Viele grundsätzliche Befehle sind aber in Windows und Linux auch identisch. In beiden Betriebssystemen können Sie beispielsweise dir eingeben, um den Inhalt des aktuellen Ordners anzeigen zu lassen. Betätigen Sie (¢), um den Befehl auszuführen. Daneben können Sie auch Programme in der Konsole starten, indem Sie einfach deren Namen eingeben und (¢) betätigen. In Windows können Sie beispielsweise durch die Eingabe von notepad den standardmäßigen Texteditor starten, in Linux können Sie zum Start eines Texteditors emacs oder kedit eingeben (um nur zwei der Texteditoren von Linux zu nennen). Umgebungsvariablen: Die Path-Variable In Windows und Linux existieren einige so genannte Umgebungsvariablen. Umgebungsvariablen sind Daten, die systemweit gelten und die über einen Namen angesprochen werden können. Viele Anwendungen wie beispielsweise Compiler fragen bestimmte Umgebungsvariablen ab, die meist der Konfiguration dienen. Umgebungsvariablen können beliebig im System über Shell-Skripte, Batch-Dateien oder System-Konfigurations-Dialoge gesetzt werden.
24
(LQIKUXQJ
Sandini Bib
Eine wichtige Umgebungsvariable ist Path (Windows) bzw. PATH (Linux). Diese Variable verwaltet die Verzeichnispfade zu wichtigen Anwendungen. Wenn Sie in einer Konsole oder über den Ausführen-Dialog des Betriebssystems (START / AUSFÜHREN bei Windows, (Alt) + (F2) bei der Linux-KDE) ein Programm aufrufen, sucht das Betriebssystem zunächst im aktuellen Ordner nach diesem Programm. Wird das Programm dort nicht gefunden, durchsucht das Betriebssystem die einzelnen, in der Path-Variablen angegebenen Pfade. Deshalb können Sie unter Windows beispielsweise einfach notepad eingeben, um den Standard-Texteditor zu starten, und unter Linux gimp, um das Standard-Grafikprogramm zu starten.
Die Path-Variable verwaltet Pfade zu wichtigen Anwendungen
In Windows können Sie den Wert der Path-Variablen abfragen, indem Sie an der Konsole Path eingeben. Eine typische Einstellung auf einem einfachen Windows 2000-System wäre z. B.: C:\WINNT\system32;C:\WINNT;C:\WINNT\System32\Wbem;
Unter Linux fragen Sie PATH über echo $PATH ab. Eine (für SuSE Linux 7.3) typische Einstellung wäre: /usr/local/bin:/usr/bin:/usr/X11R6/bin:/bin:/usr/lib/java/bin: /usr/games/bin:/usr/games:/opt/gnome/bin:/opt/kde2/bin: /opt/kde/bin:.:/opt/gnome/bin
Die meisten oder alle Linux-Programme sind in einem der in der PATHVariablen enthaltenen Ordner installiert oder werden dort über einen Link (eine Datei, die auf den originalen Speicherort des Programms verweist) repräsentiert. Deshalb können Sie unter Linux normalerweise alle Programme direkt über deren Namen aufrufen. In Windows sind per Voreinstellung nur die Systemordner im Pfad enthalten. Hier können Sie also nur Systemprogramme direkt aufrufen.
1.3
Lizenzvereinbarungen
Wenn Sie Java, Delphi oder Kylix auf Ihrem System installieren, erwerben (bzw. besitzen) Sie damit (wenn Sie diese Programmiersprachen legal erworben haben) eine Lizenz zur Benutzung. Wenn Sie nun Programme entwickeln, sollten Sie die Lizenzvereinbarungen der Hersteller beachten. Im Verlauf der Installation von Delphi und Kylix werden Ihnen diese angezeigt. Sie finden die entsprechenden Dateien aber auch in den Installationsdateien. Sie sollten diese Vereinbarungen lesen (womit ich in Bezug auf eventuelle Rechtsstreitigkeiten entlastet bin ).
-
/L]HQ]YHUHLQEDUXQJHQ
25
Sandini Bib
Besonders wichtig sind die Lizenzvereinbarungen in Bezug auf den Verkauf von Programmen, die Sie mit Java, Delphi oder Kylix entwickelt haben. Das Wichtigste habe ich für Sie zusammengefasst. Java Java ist eine freie Programmiersprache. Sie können Java-Programme an andere Personen weitergeben und auch zu kommerziellen Zwecken einsetzen. Lediglich wenn Ihre Programme spezielle Komponenten verwenden, müssen Sie deren separate Lizenzbestimmungen beachten. Informationen zur Java-Lizenzierung finden Sie im Internet an der Adresse servlet.java.sun.com/help/legal_and_licensing. Delphi Personal Edition Programme, die Sie mit der Delphi Personal Edition entwickelt haben, dürfen Sie nicht zu kommerziellen Zwecken einsetzen oder auf irgendeine Art verkaufen (auch nicht tauschen). Sie dürfen diese Programme nur für eigene, persönliche Zwecke einsetzen oder an andere Personen ohne Gegenleistung weitergeben. Für kommerzielle Programme müssen Sie eine der professionellen Delphi-Editionen kaufen. Kylix Open Edition Bei der Kylix Open Edition sieht das Ganze etwas anders aus. Für persönliche Zwecke können Sie diese Edition genau wie Delphi einsetzen. Wenn Sie aber Programme an andere Personen weitergeben wollen, muss dies unter den Bedingungen der „GNU General Public License“ (GPL) geschehen.
General Public License
Der Kernpunkt dieser öffentlichen Lizenz ist, dass Entwickler, die unter der GPL-Lizenz entwickeln, immer den Quellcode ihrer Programme mit ausliefern müssen. Andere Programmierer dürfen diesen Quellcode verändern, daraus eigene Programme erzeugen und diese sogar verkaufen (was Sie natürlich auch dürfen). Da dabei aber immer laut den Lizenzbestimmungen ein Hinweis auf den Urheber des Programms und die vorgenommenen Veränderungen in das Programm integriert werden müssen, sind Ihre Urheberrechte geschützt. Ziel dieser Lizenz ist, Betriebssysteme und Programme zu schaffen, deren Quellen frei verfügbar und für jeden zugänglich und nutzbar sind. So können sich sehr viele Programmierer an der Weiterentwicklung von Programmen beteiligen. Linux, das auch unter GPL lizenziert ist, ist ein gutes Beispiel dafür. Jeder kann den Quellcode von Linux (der entsprechend der Lizenz der Distribution beiliegt) verändern und verbessern.
26
(LQIKUXQJ
Sandini Bib
Genau das machten und machen sehr viele Programmierer. Viele dieser Änderungen werden immer wieder in den Linux-Kernel oder in LinuxDistributionen aufgenommen. Weil sehr viele Entwickler an Verbesserungen und Neuentwicklungen beteiligt sind, entwickelt sich Linux damit zu einem immer perfekteren und gleichzeitig kostenfreien (oder bei den Distributionen wenigstens sehr preisgünstigen) Betriebssystem. Kylix-Open-Edition-Programme müssen, wie alle unter GPL lizenzierten Programme, einen Hinweis auf die GPL-Lizenz enthalten. Kylix erledigt das aber automatisch für Sie. Beim Start einer mit der Kylix Open Edition erzeugten Anwendung gibt diese einen Hinweis auf die Lizenz aus. Lesen Sie die offiziellen Informationen zur GPL-Lizenz, die Sie auf der Buch-CD oder an der Adresse www.gnu.org/copyleft/gpl.html finden, um Näheres zu erfahren. Wichtig ist diese Lizenz auch für die Weitergabe von Kylix-Bibliotheks-Komponenten, die Sie in Ihren Programmen nutzen. Einige Komponenten dürfen Sie gar nicht, einige eingeschränkt und andere nur unter der Mitlieferung des Quellcodes mit dem Programm weitergeben. Lesen Sie dazu die Lizenzbestimmungen, die Sie im Kylix-Ordner in der Datei license.txt finden.
1.4
Warum individuelle Programme?
Auf dem Software-Markt können Sie eine fast unüberschaubare Menge an Standardanwendungen für alle möglichen Problemlösungen kaufen oder kostenfrei erhalten. Und in vielen Fällen reichen diese Standardanwendungen auch vollkommen aus. Wenn Sie Texte verfassen wollen und keine Textverarbeitung besitzen, werden Sie wohl kaum eine selbst programmieren (wofür Sie auch als erfahrener Programmierer wohl einige Monate benötigen würden), sondern ein passendes Programm kaufen oder als Freeware aus dem Internet downloaden. Wollen Sie CDs brennen, werden Sie die dazu notwendige Brennsoftware ebenfalls eher kaufen oder downloaden, als selbst zu programmieren. Wenn Sie ein wenig (im Internet) suchen, finden Sie zum einen ganz normale Standardanwendungen wie Microsoft Word oder StarOffice. Sie finden aber auch eine Vielzahl an Programmen, die zur Lösung kleinerer Probleme entwickelt wurden und sehr spezielle Standardanwendungen. So gibt es z. B. Finanzbuchhaltungsprogramme, die speziell auf die Bedürfnisse von Handwerksbetrieben zugeschnitten sind. Und vielleicht auch solche für Ingenieure, die auf dem Planeten Magrathea5 ar5.
Magrathea: Planet, auf dem Hyperraum-Ingenieure vor langer, langer Zeit Materie durch weiße Löcher im All heranzogen, um sie in Traumplaneten zu verwandeln. Frei nach meiner fünfteiligen Lieblings-Trilogie.
27
Sandini Bib
beiten. Warum existiert also trotzdem Bedarf für individuell entwickelte Programme? Eine Antwort auf diese Frage ist: Weil nahezu jeder Mensch und jede Firma eine andere Vorgehensweise bei der Bearbeitung von Problemen hat und sich wünscht, dass ein Programm genau ihren Bedürfnissen entspricht. Standardanwendungen entsprechen oft nicht diesen Anforderungen. Das beginnt schon bei der Steuerung der Anwendung, die für den einen oder anderen zu kompliziert ist, und endet noch lange nicht bei dem Wunsch, bestimmte Funktionalitäten der Anwendung in einer anderen Art und Weise auszuführen, als es mit der Anwendung möglich ist. Und vielleicht fehlen sogar Funktionalitäten, die der Anwender bzw. die Firma dringend benötigt. Standardanwendungen lassen sich oft zwar in gewissen Grenzen an die Bedürfnisse der Benutzer anpassen, für viele Anwender und Firmen reicht diese Anpassung jedoch nicht aus. Firmen arbeiten sehr häufig mit einer ganz eigenen Geschäftslogik6, die sich häufig mit Standardanwendungen nicht exakt abbilden lässt. Es gibt zwar spezielle Anwendungen (wie die der Firma SAP7), mit denen sehr viele verschiedene Geschäftsmodelle abgebildet werden können. Aber auch diese Anwendungen zwingen eine Firma, ihren Geschäftsablauf wenigstens teilweise zu ändern. Daneben sind diese mächtigen Standardanwendungen oft auch sehr komplex, so dass deren Benutzung und Anpassung schwierig ist. Firmen bevorzugen, wie normale Benutzer auch, einfach anzuwendende Programme, die möglichst genau auf die Bedürfnisse der Anwender abgestimmt sind. Und diese müssen dann eben individuell programmiert werden.
Standardanwendungen sind oft ungeeignet
Aber auch die Anpassung von Standardsoftware ist ein Betätigungsfeld für Softwareentwickler. Mit Hilfe einer in die Anwendung integrierten Programmiersprache können Sie diese häufig an individuelle Bedürfnisse anpassen. Microsoft Word, Excel und Access erlauben beispielsweise eine recht umfangreiche Programmierung über so genannte Makros, die in der Programmiersprache Visual Basic geschrieben werden.
Anpassung von Standardanwendungen
28
6.
Die Logik, mit der Firmen ihre Geschäfte abwickeln, wird als Geschäftslogik bezeichnet. Jede Firma besitzt ihre eigene Geschäftslogik. Beispielsweise werden Aufträge nur per Fax entgegengenommen, Lieferungen nur per UPS oder per Post ausgesendet, Zahlungen auch in Teilzahlungen akzeptiert und regelmäßig die Rabatte von Kunden, die 3 Monate nicht bestellt haben, um 1 % gekürzt. Diese Geschäftslogik ist in der Praxis meist recht komplex und schwer zu verstehen.
7.
SAP ist der Hersteller einer Palette von umfangreichen Anwendungen zur Abwicklung aller Geschäftsprozesse, die in einem Unternehmen auftreten. Die bekannteste Anwendung dieser Firma ist SAP R/3, die Lösungen bietet für die Auftragsabwicklung, den Einund Verkauf, das Rechnungs- und das Personalwesen.
(LQIKUXQJ
Sandini Bib
In seltenen Fällen kommt es auch einmal vor, dass es eine spezielle Anwendung noch gar nicht gibt oder dass diese einfach zu teuer ist. Dann ist auch wieder der Programmierer gefragt.
Neuentwicklung
Natürlich werden Softwareentwickler auch bei den Firmen benötigt, die Standardsoftware entwickeln. Gerade weil Betriebssysteme und die dazugehörige Software sich immer schneller weiterentwickeln, braucht man immer auch Programmierer, die Standardsoftware an die neuen Betriebssysteme anpassen und weiterentwickeln. Sie sehen also: Für Softwareentwickler gibt es genug zu tun.
1.5
Der Weg zum Profi
Mit diesem Buch haben Sie den Weg zum Profi bereits eingeschlagen. Dieser Weg ist normalerweise nicht besonders schwierig, dafür aber nicht gerade kurz. Das Erreichen der einzelnen Meilensteine kostet etwas Zeit, weil Sie dazu jeweils eine Menge lernen müssen. Damit Sie diesen Weg kennen lernen und einschätzen können, zeige ich, welche Etappen Sie zurücklegen müssen. Damit Sie diesen Weg möglichst effizient gehen können und nicht durch vielleicht falsche Lernmethoden gebremst werden, erfahren Sie zuvor, wie Sie möglichst effizient lernen.
1.5.1 Einige Worte zum Lernen Das effiziente (und damit leicht fallende) Lernen ist eine wichtige Voraussetzung auf dem Weg zum Programmierer. Ich bin kein Profi in der Theorie des Lernens. Meine eigenen Lern-Erfahrungen, Erfahrungen in vielen Seminaren und das Studium verschiedener Literatur haben mir aber gezeigt, dass es unterschiedliche Lern-Typen gibt und wie der Mensch prinzipiell lernt. Wenn Sie Ihren Typ kennen, Ihr Lernen daran anpassen, und wenn Sie wissen, wie ein Mensch prinzipiell lernt, fällt es Ihnen wahrscheinlich leichter. Frederic Vester, ein bekannter Kybernetiker, der sich neben Umweltfragen auch mit dem menschlichen Denken beschäftigt (und der u. a. die bekannten Umwelt-Strategiespiele Ökolopoly und Ecopolicy entwickelt hat), beschreibt in seinem hervorragenden Buch „Denken, Lernen, Vergessen“, dass es grundsätzlich vier verschiedene Lern-Typen gibt. Der erste Typ lernt durch Kommunikation, benötigt also immer wieder Erklärungen einer anderen Person, die idealerweise seinem Lernmuster entsprechen. Verständnisprobleme werden durch Fragen, Argumente und Gegenargumente aus der Welt geschafft. Der zweite Typ lernt mehr durch seine Augen, also durch Beobachtung und Experimente. Der dritte Typ lernt im Wesentlichen durch Anfassen und Fühlen (was zuge-
'HU :HJ ]XP 3URIL
Lern-Typen
29
Sandini Bib
gebenermaßen beim Lernen der Programmierung etwas schwierig ist). Der letzte Lern-Typ lernt eher in Form abstrakter Formeln, also rein über seinen Intellekt. 18 Sekunden Zeit
Das Lernen selbst geschieht in drei Schritten: Wenn Sie etwas erfahren, lesen oder sehen, rotiert dieser Eindruck etwa 18 Sekunden lang in Ihrem Ultrakurzzeit-Gedächtnis. Nur wenn Sie dieses neue „Wissen“ innerhalb dieser 18 Sekunden bewusst noch einmal abrufen oder mit anderem, bereits gespeichertem Wissen assoziieren, wird das Wissen in das Kurzzeit-Gedächtnis übertragen. Rufen Sie das neue Wissen nicht ab, geht es unwiederbringlich verloren. Diese Art der Speicherung schützt uns davor, in unserem täglichen Leben zu viele Eindrücke zu speichern und nach kurzer Zeit einen „Informationsüberlauf“ zu erleiden.
Kurzzeit- und
Neues Wissen bleibt für etwa 20 Minuten im Kurzzeit-Gedächtnis gespeichert. Unter normalen Umständen wird dieses Wissen dann nach diesen 20 Minuten in das Langzeit-Gedächtnis übertragen. Wissen, das im Langzeit-Gedächtnis gespeichert wurde, bleibt dort für immer oder wenigstens für eine lange Zeit erhalten (wenn es auch manchmal etwas „vergraben“ ist). Lediglich altersbedingte oder krankhafte Umstände oder ein Schock (wie bei einem Unfall) können die Übertragung in das Langzeitgedächtnis verhindern. Bei vielen älteren Menschen ist eben diese Übertragung in das Langzeitgedächtnis gestört. Diese Menschen können sich oft sehr genau an frühere Dinge erinnern, aber nicht mehr an den vorigen Tag.
Langzeitgedächtnis
In vielen Fällen reicht eine einmalige Speicherung im Gedächtnis aber nicht dazu aus, eine Thematik zu beherrschen oder sich klar und deutlich daran zu erinnern. Häufig benötigt unser Gehirn die mehrfache Aufnahme des Wissens, quasi um das Muster, das das Wissen im Gehirn erzeugt hat, durch neue Eindrücke und Assoziationen zu verstärken. Sie können sich das Ganze so vorstellen, dass ein erstes Lernen, eine erste Erfahrung ein nur schwaches Muster in das Gedächtnis prägt. Das Erinnern fällt dann oft noch schwer. Erst wenn weiteres, zu diesem Muster passendes Wissen oder weitere Assoziationen hinzukommen, wird der Abdruck verstärkt und die Erinnerung immer deutlicher. Frederic Vester liefert in seinem Buch wissenschaftliche Nachweise und Erklärungsmuster für diese Theorie, die aber hier den Rahmen sprengen würden. Interessant ist, dass die Theorie des „Einprägens“ durch die alte Redewendung „sich etwas einprägen“ bestärkt wird. Für das Lernen der Programmierung bedeutet dies in meinen Augen, dass Sie Themen, die Sie noch nicht kennen, nicht einfach aufnehmen (lesen, hören) können, sondern zwischenzeitlich (idealerweise in 18Sekunden-Intervallen) immer wieder innehalten und über das neu Gelernte kurz nachdenken (sofern es für Sie wichtig ist). Versuchen Sie, das
Programmieren lernen
30
(LQIKUXQJ
Sandini Bib
neu Gelernte mit bereits vorhandenem Wissen zu assoziieren. Damit erzeugen Sie wohl die stärksten Wissensmuster im Gedächtnis. Nach dem grundsätzlichen Lernen einer neuen Thematik sollten Sie diese dann noch auf eine andere Weise vertiefen. Ich denke, für die meisten LernTypen ist das Nachvollziehen des gelernten Stoffs am eigenen Beispiel (also quasi im Experiment) ideal. Wenn Sie beispielsweise die Grundlagen der strukturierten Programmierung lernen, sollten Sie die im Buch erlernten Konzepte über eigene Programme noch einmal nachvollziehen. Die dabei notwendige enorme Gedächtnisleistung (Sie müssen ja alles selbst nachvollziehen) reicht häufig aus, das Erlernte dauerhaft und jederzeit abrufbar zu speichern. Das Wissensmuster wird später noch weiter verstärkt, wenn Sie das Erlernte in der Praxis anwenden (und dabei neue Erfahrungen sammeln). Für andere Lern-Typen ist es wahrscheinlich effizienter, den gelernten Stoff noch einmal an Hand anderer Quellen, beispielsweise des Internets, auf eine andere Art zu erfassen oder sich schwierige Themen von einer anderen Person erläutern zu lassen. Finden Sie heraus, welcher Lern-Typ Sie sind, und das Lernen wird Ihnen leicht fallen. Ich bin übrigens ein Lern-Typ, der mit Erklärungen anderer nicht viel anfangen kann (diese verwirren mich sehr schnell und versperren damit die weitere Informationsaufnahme), der sein Grundwissen in schriftlicher Form bezieht (dabei habe ich genug Zeit und Raum für eigene Gedanken) und der dieses Wissen in relativ kurzen Abständen direkt nach dem Lesen durch eigene Experimente so vertieft, dass es dauerhaft gespeichert bleibt. Häufig lese ich lediglich ein paar Sätze, um dann stundenlang zu experimentieren. Aber es funktioniert. Wie Sie sehen.
1.5.2 Die Etappen auf dem Weg zum Profi Grundlagen lernen Als wichtige Basis der Programmierung müssen Sie die Grundlagen lernen, weswegen Sie ja dieses Buch lesen. Ich habe im Laufe meiner Dozententätigkeit so einige Teilnehmer kennen gelernt, die zwar bereits jahrelang programmierten, die aber wichtige Grundlagen einfach nicht kannten. Natürlich können auch Sie so programmieren. Sie werden dabei aber so viele Fehler machen, dass die Zeit, die Sie für die Beseitigung dieser Fehler aufbringen müssen, die relativ kurze Zeit, die Sie zum Erlernen der Grundlagen benötigen, bei weitem überwiegt. Abgesehen davon werden Ihre Programme dann sehr unübersichtlich, kryptisch programmiert, unsauber und damit langsam und nur sehr schwer wartund erweiterbar. Aber Ihnen wird das ja nicht passieren, Sie lesen schließlich dieses Buch .
-
'HU :HJ ]XP 3URIL
31
Sandini Bib
Programmiersprache(n) lernen Wenn Sie die Grundlagen der Programmierung kennen (ich vermeide bewusst das Wort „beherrschen“, die Beherrschung der Programmierung kommt erst mit der Zeit in der Praxis), sollten Sie eine oder vielleicht auch zwei Programmiersprachen lernen. Nehmen Sie sich aber nicht allzu viel vor, meist stehen Sie vor einem ziemlich großen Berg neuen Wissens, das erst erlernt werden muss. Programmierer sollten aber schon mit mehr als einer Programmiersprache umgehen können, schon allein deshalb, weil verschiedene Sprachen ganz unterschiedliche Möglichkeiten bieten. Da Sie die Grundlagen der Programmierung ja bereits kennen, können Sie diese meist innerhalb weniger Stunden auf die neue Programmiersprache anwenden. Aufwändiger ist da schon die Erforschung der Funktions- oder Klassenbibliothek einer Programmiersprache, die in der Regel sehr viele Möglichkeiten bietet. Hierbei müssen Sie oft erst die zugrunde liegende Technologie verstehen, was manchmal nicht allzu einfach ist. Die Technik, mit der Sie in Java ein Windows-Fenster mit Steuerelementen erzeugen, ist beispielsweise deutlich anders als diejenige, die Sie in Delphi oder C# verwenden. Viele moderne Programmiersprachen besitzen in diesem Bereich aber auch sehr viele Gemeinsamkeiten. Delphi, Visual Basic, J# und C# unterscheiden sich beispielsweise prinzipiell nicht allzu sehr bei der Erstellung der Oberfläche einer Anwendung (über die Steuerelemente der Klassenbibliothek).
Klassenbibliothek
Sie sollten aber nie versuchen, alle Möglichkeiten der Funktions- oder Klassenbibliothek zu erlernen. Meist ist diese Bibliothek einfach zu mächtig. Sie sollten sich lediglich wichtige Grundlagen aneignen, wissen, was prinzipiell möglich ist und wo Sie erfahren, wie das eine oder andere Feature eingesetzt wird, wenn Sie dieses benötigen. Ab Seite 34 zeige ich, wie Sie nach solchen Informationen suchen. Ideal ist, wenn Sie sich mit einem Feature erst dann tiefer gehend beschäftigen, wenn Sie dieses bei der Programmierung einsetzen müssen. Auf diese Weise lernen Sie die Anwendung eines Features meist quasi nebenbei (weil es in der Praxis geschieht) und sehr dauerhaft. Außerdem vermeiden Sie so, dass Sie den Umgang mit Features lernen, die dann, wenn Sie diese zum ersten Mal in der Praxis einsetzen wollen, hoffnungslos veraltet sind. Technologien lernen Wenn Sie Ihre bevorzugten Programmiersprache(n) kennen, sollten Sie sich mit speziellen Technologien beschäftigen. Dazu gehören Datenbankprogramme, Internetprogramme, Mehrschichten-Anwendungen
32
(LQIKUXQJ
Sandini Bib
und die Verwendung spezieller Server wie des Microsoft Index Servers (der häufig auf Webservern eingesetzt wird, um die dort verwalteten Dokumente suchbar zu machen). Wichtig dabei ist, dass Sie einzelne, zurzeit für Sie noch nicht wichtige Technologien zunächst nur grundlegend kennen, also lediglich wissen, worum es sich dabei handelt. Ein wenig hilft dieses Buch dabei (Seite 150). Erst wenn Sie eine dieser Technologien anwenden müssen, sollten Sie diese in der gebotenen Tiefe erlernen. Ansonsten laufen Sie Gefahr zu viel Wissen anzusammeln (und darüber auch zu viel Zeit zu verbringen), das Sie eventuell nie anwenden werden. Ihr(e) Beziehungspartner(in) und/oder Ihre Freunde werden es Ihnen danken … Projektierung lernen Wenn Sie kommerziell Programme entwickeln (die Sie also an Kunden verkaufen), sollten Sie schließlich noch lernen, wie Sie Projekte abwickeln. Dazu gehören der Umgang mit Kunden, das Umsetzen von Kundenanforderungen, die Erstellung eines Pflichtenhefts (das im Prinzip auflistet, was alles Bestandteil des zu entwickelnden Programms ist), die Einschätzung der benötigten Zeit, das Erkennen potenzieller Fehlerquellen und das Denken in einem weiteren Rahmen als der Kunde (um weitere Möglichkeiten und potenzielle Fehler zu erkennen). Einige dieser Punkte behandle ich im Artikel „Programmentwurf“, den Sie auf der Buch-CD finden. Das meiste Wissen wird sich aber im Laufe der Zeit bei Ihren eigenen Projekten ansammeln, bis Sie schließlich SoftwareProjekte relativ sicher angehen und vor allen Dingen auch korrekt einschätzen können. Dabei schadet es auch nicht, einmal das eine oder andere Buch zur Projektierung zu lesen. Dabei sollten Sie aber berücksichtigen, dass einige dieser Bücher sehr theoretisch sind und das Vorgehen nach den darin vorgestellten Schemata für kleinere bis mittlere Projekte oft viel zu viel Aufwand verursacht. Viel Geld verdienen Schließlich sollten Sie natürlich auch die Früchte Ihrer Lern-Arbeit ernten und viel Geld mit Ihren Programmen verdienen. Programmierer sind schließlich Allround-Spezialisten mit sehr viel Wissen. Und das will bezahlt werden.
'HU :HJ ]XP 3URIL
33
Sandini Bib
1.6
Wie sucht ein Programmierer Informationen?
Ein Programmierer kann und sollte auch gar nicht alles von dem wissen, was seine Programmiersprachen beherrschen. Programmiersprachen und die beim Programmieren verwendeten Features und Tools sind einfach zu komplex. Wenn Sie ein Feature benötigen oder dessen Anwendung erläutert haben müssen, können Sie Ihre Fragen heutzutage wunderbar über verschiedene Medien klären, wobei das Internet wohl die größte Rolle spielt. Ich zeige Ihnen nun, wie Sie dabei vorgehen können. Die Informationssuche in Büchern (die wohl enorm wichtig ist ) lasse ich hier weg, Bücher kennen Sie ja. Dass ich die Informationssuche bereits an dieser Stelle beschreibe (wo Sie doch häufig noch gar nicht wissen, wonach Sie suchen), hat eine Bedeutung: So können Sie nämlich Fragen selbst klären, die bei der Lektüre dieses Buchs eventuell offen bleiben.
-
1.6.1 Die Dokumentation Jede Programmiersprache liefert normalerweise eine Dokumentation mit. Für Java haben Sie diese z. B. bei der Installation separat installiert. Delphi und Kylix installieren die Dokumentation automatisch. In der Dokumentation werden normalerweise die Syntax und die Features der Programmiersprache beschrieben, oft aber auch allgemeine Vorgehensweisen (wie beispielsweise Informationen zur Gestaltung der Oberfläche einer Anwendung). Meine Erfahrungen mit diversen Dokumentationen haben mir gezeigt, dass Sie meist mit den allgemeinen (Grundlagen-)Themen einer Dokumentation nicht viel anfangen können, weil diese zu komplex sind, zu viel Eigenwerbung beinhalten und zu unkritisch sind. Lesen Sie lieber ein Buch zur jeweiligen Programmiersprache, wenn Sie Grundlagenwissen benötigen. Das ist meist effizienter und macht auch mehr Spaß. Suchen in der Dokumentation Wichtig ist die Dokumentation aber für das Nachschlagen der Syntax der verwendeten Features und der Möglichkeiten der Programmiersprache. Wenn Sie das Feature, nach dem Sie suchen, bereits kennen, können Sie normalerweise direkt im Index der Dokumentation danach suchen. In Delphi öffnen Sie die Dokumentation dazu über den Befehl DELPHI HELP im HELP-Menü, in Kylix wählen Sie den Befehl KYLIX-HILFE im HILFE-Menü. Im Delphi-Hilfethemen-Fenster können Sie direkt zum Index wechseln und dort nach einem Bezeichner suchen. Abbildung 1.3 zeigt die Suche nach einer Hilfe zur Writeln-Prozedur.
34
(LQIKUXQJ
Sandini Bib
Abbildung 1.3: Suche im Index der Delphi-Hilfe
In Kylix müssen Sie im zuerst erscheinenden Hilfefenster zunächst auf HELP TOPICS klicken, um das Hilfethemen-Fenster anzuzeigen (das dann ähnlich aussieht wie das von Delphi). Für Java starten Sie die Dokumentation über die Datei index.html in dem Ordner, in dem Sie die Hilfe entpackt haben. Diese Startseite bietet Links zu allen verfügbaren Dokumentationen. Die für Programmierer wohl wichtigste Dokumentation, die „Java 2 ... API Specification“, die die Bibliothek dieser Sprache beschreibt, können Sie auch direkt über die Datei index.html im Ordner api öffnen. Leider können Sie die HTML-Version der Java-Dokumentationen nicht komfortabel durchsuchen. Sie müssen sich zum benötigten Thema durchklicken. Suchen können Sie online (im Internet) über die Adresse java.sun.com/j2se/1.4/search.html. Windows-Benutzer können aber an der Adresse www.confluent.fr/javadoc/jdk14e.html eine Windows-Hilfe-Version der Java-Dokumentation downloaden (immerhin 21 MB). Diese können Sie dann wie bei der Delphi-Hilfe über den Index durchsuchen. Kontextsensitive Hilfe in Delphi und Kylix In Delphi/Kylix können Sie auch einfach den Eingabecursor im Quellcode auf einen Bezeichner setzen und (F1) betätigen. Die Entwicklungs-
:LH VXFKW HLQ 3URJUDPPLHUHU ,QIRUPDWLRQHQ"
35
Sandini Bib
umgebung öffnet die Hilfe dann kontextsensitiv, also mit Informationen zum aktuellen Kontext (zum Bezeichner, auf dem der Cursor steht). Stellen Sie den Cursor z. B. auf Writeln, wird die Hilfe direkt mit der Beschreibung dieser Prozedur geöffnet. Die kontextsensitive Hilfe funktioniert auch auf Formularen (siehe Seite 71). Selektieren Sie beispielsweise eine Textbox und betätigen Sie (F1), zeigt die Hilfe die Beschreibung dieses Steuerelements an (Abbildung 1.4).
Abbildung 1.4: Delphis Hilfe zum Edit-Steuerelement
Wie Sie im Laufe der Zeit noch erkennen werden, sind gerade bei Steuerelementen deren Eigenschaften sehr wichtig. Über den Verweis PROPERTIES bzw. EIGENSCHAFTEN können Sie diese in einer Liste anzeigen lassen. Über einen Klick auf einen Eintrag in der Liste öffnen Sie dann eine Hilfe zur jeweiligen Eigenschaft. Wichtig sind daneben auch die Methoden und Ereignisse (Events), aber dazu erfahren Sie mehr in den folgenden Kapiteln.
1.6.2 Suchen im Internet Im Internet finden Sie Unmengen von Informationen zu allen möglichen Programmiersprachen. Sie müssen nur an den richtigen Stellen suchen.
36
(LQIKUXQJ
Sandini Bib
Suchen von Webseiten mit Google Wahrscheinlich kennen Sie die Suche nach Webseiten über eine der vielen Suchmaschinen bereits. Für den Fall, dass dies noch nicht der Fall ist, zeige ich hier kurz, wie Sie damit umgehen. Zum Suchen von Webseiten können Sie unter einer Vielzahl von Suchmaschinen wählen. Ich gehe hier allerdings nur auf Google (www.google.de) ein, eine der in meinen Augen besten Suchmaschinen. Auf der Google-Suchseite können Sie recht komplexe Suchbegriffe eingeben (die Ihnen über den Link SUCHTIPPS erläutert werden). In den meisten Fällen reicht eine einfache Suche nach einem oder mehreren Suchbegriffen jedoch aus. In Abbildung 1.5 suche ich nach einer Möglichkeit, in Java zu drucken.
Abbildung 1.5: Suche nach Webseiten über Google
Das Ergebnis dieser Suche ist schon viel versprechend (Abbildung 1.6).
:LH VXFKW HLQ 3URJUDPPLHUHU ,QIRUPDWLRQHQ"
37
Sandini Bib
Abbildung 1.6: Ergebnis einer Google-Suche nach „java drucken“
Da das Internet eine fast unendlich große Menge an Informationen bietet, finden Sie in vielen Fällen natürlich auch Informationen, mit denen Sie nichts anfangen können. Im Suchergebnis sind häufig auch Werbeseiten verschiedener Firmen zur finden (die in der Regel für unseren Zweck uninteressant sind). Wichtig ist, dass Sie möglichst viele eindeutige Suchbegriffe angeben. Da weitaus mehr Informationen in englischer als in deutscher Sprache zu finden sind, sollten Sie auf jeden Fall auch englische Begriffe ausprobieren. Meine Suche zum Drucken in Java führte auf jeden Fall recht schnell zum Erfolg (ich wusste vorher wirklich nicht, wie es geht, jetzt weiß ich es ). Der erste im Ergebnis (Abbildung 1.6) dargestellte Artikel erklärt das Ganze sehr gut. Sie haben natürlich nicht immer so viel Glück und finden die Lösung Ihres Problems auf Anhieb. Manchmal müssen Sie ein wenig im Ergebnis der Suche „surfen“, um die benötigten Informationen zu finden, und manchmal müssen Sie auch verschiedene Suchbegriffe ausprobieren.
-
Wenn Sie die Links der Google-Ergebnisseite (und auch alle anderen Links auf irgendwelchen Seiten) mit der rechten Maustaste anklicken, können Sie in den Standard-Browsern im Kontextmenü den Befehl IN NEUEM FENSTER ÖFFNEN (oder ähnlich) wählen, um die Webseite, die der jeweilige Link referenziert, in einem separaten Fenster zu öffnen. So bleibt die Suchergebnisseite erhalten, sodass Sie ohne Probleme auch andere Webseiten besuchen können.
38
(LQIKUXQJ
Sandini Bib
Newsgroups Newsgroups sind für Programmierer eine sehr wichtige Informationsquelle. Im Internet existieren sehr viele Newsgroups, die sich immer nur mit einem speziellen Thema beschäftigen. Die aktuellen Beiträge einer Newsgroup können Sie in einem normalen Mail-Programm ähnlich einer E-Mail lesen. Genauso einfach können Sie eine E-Mail an die Newsgroup senden, um selbst etwas zum Thema beizutragen. Die meisten ursprünglichen Beiträge einer Newsgroup sind allerdings Fragen. Auf diese Fragen antworten dann häufig irgendwelche netten Menschen und lösen damit in vielen Fällen die Probleme des Fragenden. Manchmal werden aber auch bestimmte Themen in einer Newsgroup diskutiert. Ein Beitrag löst dann häufig eine Kette von Antworten und neuen Beiträgen aus, die sich auf den ursprünglichen Beitrag oder auf eine der zugehörigen Antworten beziehen. Diese Kette wird als Thread (Faden) bezeichnet. Ich kann an dieser Stelle nicht auf die oft unterschiedliche Konfiguration eines Mail-Programms zur Darstellung der Beiträge einer Newsgroup eingehen. Mail-Programme sind mittlerweile Bestandteil jedes Browsers. In den meisten Fällen müssen Sie ein neues Konto einrichten, das auf einen Newsserver verweist. Dazu können Sie die NewsserverAdressen msnews.microsoft.com (eher Microsoft-orientierte Themen) und news.t-online.de (eher allgemeine Themen) einsetzen. Diese Newsserver verwalten teilweise dieselben Newsgroups (und gleichen sich dabei auf eine magische Weise irgendwie gegeneinander ab). Nun müssen Sie nur noch eine oder mehrere Newsgroups „abonnieren“, was aber lediglich bedeutet, dass Sie die Beiträge dieser Newsgroup in Ihrem Mail-Programm anzeigen wollen. Wie das geht, hängt wiederum von Ihrem Mail-Programm ab. In Microsoft Outlook Express wählen Sie dazu den Befehl NEWSGROUPS im EXTRAS-Menü.
Abbildung 1.7: Abonnieren von Newsgroups in Outlook Express
:LH VXFKW HLQ 3URJUDPPLHUHU ,QIRUPDWLRQHQ"
39
Sandini Bib
Falls Ihr Mail-Programm die Möglichkeit bietet, die angezeigten Newsgroups einzuschränken (wie ich es in Abbildung 1.7 gemacht habe), sollten Sie das auch nutzen. Etwas problematisch ist dabei, dass oft sehr viele Newsgroups existieren. Am Namen der Newsgroup können Sie aber meist erkennen, welches Thema die Newsgroup hat und in welcher Sprache dort gemailt wird. Im nächsten Abschnitt zeige ich, wie Sie nach Newsgroup-Beiträgen flexibel suchen. Im Suchergebnis sehen Sie dann auch die Namen der Newsgroups und können so recht einfach feststellen, welche Sie abonnieren sollten. Die Beiträge der abonnierten Newsgroups können Sie dann wie normale Mails lesen (Abbildung 1.8).
Abbildung 1.8: Anzeige der Beiträge einer Newsgroup in Outlook Express
Beachten Sie, dass ein Newsreader-Programm beim ersten Download von Beiträgen oft nur die aktuellsten vom Newsserver herunterlädt. Outlook Express lädt z. B. nur immer die letzten 300 Kopfzeilen der auf dem Newsserver gespeicherten Beiträge. Über spezielle Befehle (bei Outlook Express ist das der Befehl WEITERE 300 KOPFZEILEN ABRUFEN im EXTRAS-Menü) können Sie aber auch weitere, ältere Beiträge in die Liste aufnehmen. Wie Sie Abbildung 1.8 entnehmen können, antworten manchmal sehr viele Programmierer auf eine Frage (was jedoch nicht unbedingt immer der Fall ist, manchmal gibt es auch keine Antworten auf einen Beitrag). Sie können nun selbst eine Frage oder einen Beitrag in die Newsgroup mailen, indem Sie einfach eine neue E-Mail an die Adresse der News-
40
(LQIKUXQJ
Sandini Bib
group senden (die vom Mail-Programm normalerweise automatisch eingetragen wird). Wenn Sie einer Newsgroup eine Mail senden, beachten Sie die Regeln, die sich im Internet für die Newsgroup-Kommunikation entwickelt haben. Dazu gehört neben einem meist lockeren, sachlichen, aber freundlichen Umgangston, dass Sie Ihre Beiträge beziehungsweise Fragen immer nur an eine Newsgroup senden, die sich auch mit einem passenden Thema beschäftigt. Senden Sie z. B. keine Frage zum Drucken unter Java an eine Newsgroup, deren Thema Datenbanken sind. Falls Sie das trotzdem machen, erhalten Sie nur in den seltensten Fällen eine Antwort, und die ist manchmal ziemlich rüde. Lesen Sie einfach einige Beiträge der Newsgroup, um zu erfahren, auf welche Weise die Leute dort miteinander kommunizieren. Achten Sie auch darauf, dass Ihre Beiträge nicht zu lang werden, was besonders dann gilt, wenn es sich um eine Frage handelt. Nur wenige Leute haben Zeit und Lust, lange Fragen zu lesen, und Sie erhalten in diesem Fall häufig einfach keine Antwort. Suche nach Newsgroup-Beiträgen Das Durchgehen der Beiträge einer Newsgroup im Mail-Programm führt in den seltensten Fällen zur Lösung eines Problems. Bevor Sie jedoch eine Frage in die Newsgroup mailen, sollten Sie zunächst nach einem Beitrag suchen, der Ihr Problem vielleicht löst. Google archiviert alle Newsgroup-Beiträge der wichtigsten Newsgroups und ermöglicht über die Adresse groups.google.com/advanced_group_search sehr komfortabel in diesem Archiv zu suchen. Hier können Sie nach Stichworten suchen und dabei sogar die Suche auf bestimmte Themen, Autoren und Newsgroups einschränken. Abbildung 1.9 zeigt eine Suche nach der Möglichkeit einer Konsolen-Eingabe in Java-Programmen (was leider nicht allzu einfach ist).
:LH VXFKW HLQ 3URJUDPPLHUHU ,QIRUPDWLRQHQ"
41
Sandini Bib
Abbildung 1.9: Suche bei der Newsgroup-Archivsuche von Google
Da die weitaus meisten Newsgroup-Beiträge in englischer Sprache verfasst sind, sollten Sie möglichst auch immer mit englischen Begriffen suchen. Wie bei der normalen Google-Suche können Sie auch hier mehrere Suchbegriffe eingeben. Eine Besonderheit ist die Eingabe im Feld NEWSGROUP. Hier können Sie den kompletten oder auch nur einen teilweisen Namen von einer oder mehreren Newsgroups eingeben, um die Suche auf diese Newsgroups einzuschränken. Mehrere Namen trennen Sie einfach durch Semikola. Wenn Sie einen Namen nur teilweise eingeben wollen, verwenden Sie dazu wie in Abbildung 1.9 den Platzhalter „*“. Das Beispiel sucht in allen Newsgroups, die den Begriff „Java“ im Namen tragen (also in allen Java-Newsgroups). Das Ergebnis wird wieder in einer übersichtlichen Liste dargestellt (Abbildung 1.10).
42
(LQIKUXQJ
Sandini Bib
Abbildung 1.10: Ergebnis einer Google-Newsgroup-Suche nach einer Konsolen-Eingabe in Java-Programmen
Die Beiträge, denen ein „Re:“ vorangestellt ist, sind übrigens Antworten auf vorherige Beiträge. Das sind oft die für Sie interessantesten Beiträge, weil es sich meist um Antworten auf Fragen handelt. Wie bei der normalen Google-Suche müssen Sie auch hier oft ein wenig surfen, um zur Lösung Ihres Problems zu gelangen. Beachten Sie dabei, dass nicht unbedingt jeder, der eine Antwort auf eine Frage schreibt, auch eine gute Lösung bietet. Manchmal müssen Sie auch in anderen Beiträgen nachschauen, um eine möglichst optimale Lösung zu finden. Wenn Sie einen interessanten Beitrag geöffnet haben, empfiehlt sich eigentlich immer, den Verweis COMPLETE TREAD (X ARTICLES) oben im Dokument anzuklicken. Sie sehen dann den gesamten Thread („Faden“), also den ursprünglichen Beitrag und alle Antworten darauf. In diesen Beiträgen finden Sie häufig eine Lösung.
:LH VXFKW HLQ 3URJUDPPLHUHU ,QIRUPDWLRQHQ"
Der Thread
43
Sandini Bib
Abbildung 1.11: Beitrag einer Newsgroup inklusive Thread
1.7
Zusammenfassung
In diesem Kapitel haben Sie zunächst erfahren, was in diesem Buch behandelt wird, wie Sie mit der Konsole des Betriebssystems umgehen und wie Sie die im Buch verwendeten Programmiersprachen installieren. Daneben wissen Sie, warum Sie dieses Buch überhaupt lesen (also warum Firmen und Anwender immer wieder die Dienste von Programmierern in Anspruch nehmen oder selbst programmieren müssen). Dabei haben Sie den Unterschied zwischen der Erweiterung von Standardanwendungen und der Neuprogrammierung kennen gelernt. Sie wissen nun, wie der Weg zum Profi-Programmierer prinzipiell aussieht und welche Meilensteine Sie auf diesem Weg erreichen müssen. Dabei haben Sie gleich noch erfahren, wie Sie möglichst effizient lernen. Für den Fall, dass Sie ein Feature, das Sie benötigen, doch noch nicht kennen (was auch bei erfahrenen Programmierern tagtäglich vorkommt), wissen Sie nun auch, wie Sie erfolgreich in der Dokumentation Ihrer Programmiersprache und im Internet nach Informationen suchen. In diesem Zusammenhang können Sie nun auch Beiträge einer Newsgroup lesen und eigene Beiträge verfassen (wenn Sie ein wenig geübt haben ...).
44
(LQIKUXQJ
Sandini Bib
1.8
Fragen und Übungen
1. Warum besteht häufig der Bedarf, Standardanwendungen durch in-
tegrierte Programme zu erweitern? 2. Sie haben das Problem, eine Datenbank in Java ansprechen zu müs-
sen. Sie kennen Datenbanken, wissen aber nicht, welche Komponenten Sie einsetzen müssen und wie Sie diese programmieren. Wie finden Sie die benötigten Informationen?
45
Sandini Bib
Sandini Bib
2
Erste Schritte
Sie lernen in diesem Kapitel:
le
n e rn
• wie Sie mit Delphi oder Kylix eine Konsolenanwendung entwickeln, • wie Sie mit Delphi oder Kylix eine einfache Anwendung mit grafischer Oberfläche entwickeln, • wie Sie mit einem einfachen Editor ein Java-Konsolenprogramm schreiben, • wie Sie dieses Programm kompilieren (für den Computer übersetzen) und Kompilierfehler auswerten, • wie Sie ein Java-Programm direkt über den Java-Interpreter starten und • wie Sie Sun ONE Studio 4 einsetzen, um einfache Java-Anwendungen zu entwickeln. Dieses Kapitel soll Ihnen vermitteln, wie Programme prinzipiell geschrieben, übersetzt und ausgeführt werden, und den Umgang mit den Delphi/Kylix- und Java-Werkzeugen erläutern. Es zeigt dabei zwei unterschiedliche Wege. Unter Delphi, Kylix und Sun One Studio 4 entwickeln Sie einige Beispiel-Programme mit Hilfe der Entwicklungsumgebung. Entwicklungsumgebungen liefern einfach zu viele Features, als dass Sie diese heutzutage ignorieren könnten. Außerdem können Delphi- und Kylix-Programme prinzipiell gar nicht ohne die Entwicklungsumgebung erstellt werden. Damit Sie aber auch lernen, ein Programm in einem einfachen Editor zu schreiben und „von Hand“ zu kompilieren, beschreibe ich im Java-Abschnitt auch, wie Sie den JavaCompiler (der Ihren Quellcode in ausführbare Programme übersetzt) direkt nutzen. Sie entwerfen in diesem Kapitel jeweils zuerst eine einfache Konsolenanwendung. Diese Art Anwendung ist noch recht einfach zu verstehen. Dabei lernen Sie bereits den grundsätzlichen Umgang mit der Entwicklungsumgebung bzw. mit dem Compiler. Sie erfahren aber auch, wie Sie
47
Sandini Bib
einfache Anwendungen mit einer grafischen Oberfläche entwickeln. Dieses Thema ist bereits etwas komplexer und setzt einiges an Wissen voraus, das ich eigentlich erst in späteren Kapiteln vermittle. Sehen Sie dieses Kapitel als eine Art Workshop, der Ihnen zeigt, wie es geht, und wenden Sie das Gelernte in den späteren Kapiteln an, wobei Sie Ihr Wissen dann natürlich noch vertiefen.
2.1
Einige Begriffe zuvor
Bevor Sie beginnen zu programmieren, sollten Sie einige der in diesem Kapitel verwendeten Begriffe kennen, die ich im weiteren Verlauf nicht weiter erläutere: • Quellcode: Als Quellcode (auch: Quelltext) wird ein in Textform vorliegendes ursprüngliches Programm bezeichnet. Wenn Sie ein Programm entwickeln, schreiben Sie zunächst den Quellcode. Dieser wird dann später von einem Compiler oder Interpreter in ein ausführbares Programm übersetzt. • Compiler: Ein Compiler übersetzt einen Quellcode in ein ausführbares Programm und speichert dieses in eine Datei. Ein so übersetztes Programm können Sie meist direkt über das Betriebssystem ausführen. Bei einigen Sprachen wie Java werden vom Compiler übersetzte Programme aber auch über spezielle Anwendungen ausgeführt, wie Sie es noch ab Seite 91 näher erfahren. • Interpreter: Ein Interpreter übersetzt den Quellcode eines Programms, ähnlich wie ein Compiler. Ein Interpreter speichert das Ergebnis aber nicht in eine Datei, sondern führt den übersetzten Code gleich aus. Zur Ausführung von Programmen, die interpretiert werden, benötigen Sie bei einfachen Interpretern den Quellcode. Einige Programmiersprachen wie Java verwenden auch ein Zwischending: Ein Compiler übersetzt den Quellcode, allerdings nicht in ausführbaren Code, sondern in einen speziellen Code. Ein Interpreter übersetzt diesen Code dann in ausführbaren Programmcode. • Syntax: Die Regeln, die eine Programmiersprache zur Erstellung von Programmen bestimmt, werden als Syntax bezeichnet. Java legt beispielsweise fest, dass Anweisungen mit einem Semikolon abgeschlossen und dass mehrere zusammengehörige Anweisungen in geschweifte Klammern eingefügt werden müssen. Die Regeln der im Buch verwendeten Programmiersprachen lernen Sie hauptsächlich in Kapitel 4 und 5. Im vorliegenden Kapitel gehe ich nur grundlegend auf die Syntax von Java und Object Pascal ein. • Debuggen: Beim Programmieren macht jeder Programmierer Fehler. Viele Programme sind einfach zu komplex, als dass das Programm
48
Sandini Bib
beim ersten Versuch fehlerfrei laufen kann. Sie kennen wohl alle irgendwelche Windows- oder Linux-Programme, die Fehler aufweisen. Diese Fehler werden häufig erst in einer späteren Version des Programms beseitigt. Ziel eines jeden Programmierers ist es natürlich, schon in der ersten Version fehlerfreie Programme zu erzeugen. Deshalb müssen Fehler gesucht und beseitigt werden. Dieses Suchen von Fehlern in einem Programm wird als Debugging bezeichnet. Viele Progammiersprachen stellen dem Programmierer dazu spezielle Werkzeuge, die so genannten Debugger, zur Verfügung. Auf das Debugging gehe ich grundlegend in Kapitel 4 ein. Die Bezeichnung „Debugging“ hat eine Geschichte. Diese Geschichte kursiert in verschiedenen Varianten. Meine Variante basiert auf Informationen der US Navy (www.history.navy.mil/photos/pers-us/uspers-h/ g-hoppr.htm und www.hopper.navy.mil/grace/grace.htm): Grace Murray Hopper, damals Lieutenant und später Admiral der US Naval Reserve (USNR), war 1945 hauptsächlich an der Weiterentwicklung eines der ersten Computer, des Mark II, beteiligt. Bei der Suche nach einem Fehler im Programm (das damals noch über Schalttafeln „geschrieben“ wurde) fand sie eine tote Motte, die einige Anschlussstellen eines Relais elektrisch überbrückte und damit den Fehler auslöste. In das Arbeitsprotokoll schrieb sie, dass sie den ersten Fall eines „Bug“ gefunden hatte. Bug ist die englische Bezeichnung für Käfer. Die Beseitigung der Motte bezeichnete sie dann noch als „debugging“. Ein Photo dieses Protokolls finden Sie unter www.history.navy.mil/photos/images/ h96000/h96566k.jpg.
Grace M. Hopper
Grace M. Hopper war übrigens eine der Pionierinnen der Computerund Programmentwicklung. Sie entwickelte den ersten Compiler und war maßgeblich an der Entwicklung von COBOL beteiligt, der Programmiersprache, in der in den ersten Jahren des Computers die meisten Programme entwickelt wurden. Deshalb wird Grace M. Hopper auch oft für das Jahr-2000-Problem verantwortlich gemacht. Die COBOLEntwickler entschieden damals nämlich, dass zur Speicherung des Jahresteils eines Datums zwei Stellen ausreichen. Und das wurde wohl von vielen Entwicklern einfach übernommen. Das Ganze ist irgendwie witzig: Grace M. Hopper fand den ersten Bug und war gleichzeitig mitverantwortlich für den größten Bug der Geschichte.
49
Sandini Bib
2.2
Hello World in Delphi/Kylix
Das erste Programm, das Sie entwickeln, ist eine einfache Konsolenanwendung, die den Text „Hello World“ ausgibt. Die Ausgabe von Hello World ist bei Programmier-Anfängern ein alter Brauch. Sie sollten sich an diesen Brauch halten. Über die Begrüßung der Welt vertreiben Sie wahrscheinlich den Fehlerteufel aus Ihren Programmen, der ansonsten über Nacht sehr schwer zu findende Fehler in Ihre Programme einbaut. Starten Sie zunächst Delphi bzw. Kylix. Delphi finden Sie unter Windows im Startmenü. Unter Linux rufen Sie zum Start von Kylix das Shell-Skript startkylix auf. Bevor Sie beginnen zu programmieren, möchte ich Ihnen den folgenden Hinweis ans Herz legen: Wenn Sie ein Delphi- oder Kylix-Projekt oder eine Datei aus diesem Projekt an eine andere Stelle kopieren wollen (z. B. als Sicherheitskopie auf eine ZIP-Disk), verwenden Sie dazu nicht die Speichern unter-Funktion der Entwicklungsumgebung. Sie bringen damit Ihr Projekt durcheinander, weil die Entwicklungsumgebung dann den neuen Speicherort der Dateien als Basis für das Projekt verwendet. Kopieren Sie Dateien einfach über die entsprechenden Tools Ihres Betriebssystems. Die auf den folgenden Seiten immer wieder genannten Tastenkombinationen zum Umgang mit der Entwicklungsumgebung finden Sie im Anhang in einer übersichtlichen Tabelle.
2.2.1 Ein wichtiger Hinweis zu Kylix Wenn Sie unter Linux mit der KDE arbeiten, müssen Sie beachten, dass diese einige Tastenkombinationen mit besonderen Funktionen belegt. Über (Strg) (F1) bis (Strg) (F12) können Sie z. B. zu einem der Arbeitsbereiche wechseln. Leider handelt es sich dabei häufig um Tastenkombinationen, die auch in Kylix verwendet werden. Die KDE hat aber immer Vorrang. Wenn Sie z. B. in Kylix (Strg) (F9) betätigen um ein Programm zu kompilieren, wechselt die KDE stattdessen zum Arbeitsbereich 9. Dieses Verhalten ist schon sehr ärgerlich, weil ein vernünftiges Arbeiten mit Kylix ohne die von der KDE belegten Tastenkombinationen nicht möglich ist.
50
Sandini Bib
Sie können die Tastenzuordnungen der KDE aber auch anpassen. Rufen Sie dazu den Befehl TASTENZUORDNUNG im KDE-Menü EINSTELLUNGEN/ERSCHEINUNGSBILD auf. Entfernen Sie alle Tastenkombinationen, die Sie unter Kylix benötigen oder legen Sie diese auf andere Kombinationen um. Speichern Sie Ihre Einstellungen idealerweise als separates Schema, damit Sie später ohne Probleme zwischen dem KDE-Standard und Ihren speziellen Kylix-Einstellungen wechseln können. Die für Kylix wichtigen Tastenkombinationen finden Sie im Anhang. Im GNOME-Desktop trat das Problem übrigens nicht auf.
2.2.2 Erzeugen einer neuen Konsolenanwendung Delphi und Kylix starten mit einem neuen, leeren Projekt für eine Anwendung mit grafischer Oberfläche (Abbildung 2.1 und Abbildung 2.2)
Abbildung 2.1: Der Startbildschirm von Delphi
51
Sandini Bib
Abbildung 2.2: Der Startbildschirm von Kylix
An den Abbildungen erkennen Sie auch, dass Delphi und Kylix prinzipiell nur wenig unterscheidet. Um eine Konsolenanwendung zu programmieren, müssen Sie zunächst ein entsprechendes Projekt erzeugen. Wählen Sie bei Delphi dazu den Befehl NEW/OTHER im FILE-Menü. In Kylix (das ja in deutscher Sprache vorliegt) rufen Sie den Befehl NEU im DATEI-Menü auf. Im danach geöffneten Dialog wählen Sie den Eintrag CONSOLE APPLICATION bzw. KONSOLEN-ANWENDUNG (Abbildung 2.3).
Abbildung 2.3: Auswahl einer Konsolenanwendung als neues Projekt in Delphi
52
Sandini Bib
Lassen Sie sich nicht von der Vielzahl der Möglichkeiten verwirren. Delphi und Kylix sind (wie Java) umfangreiche Programmiersprachen mit sehr vielen Möglichkeiten. Gerade das macht diese Programmiersprachen so mächtig. Am Anfang können Sie aber nicht erfassen, was die einzelnen Einträge in diesem Dialog (und damit die Möglichkeiten der Programmentwicklung mit Delphi/Kylix) bedeuten. Später, wenn Sie die Grundlagen beherrschen und sich mit weiteren Themen beschäftigen, werden Sie auch die meisten der erweiterten Möglichkeiten verstehen. Alle kenne allerdings selbst ich nicht ... Nach dem Erzeugen einer neuen Konsolenanwendung sieht Delphi aus wie in Abbildung 2.4.
Abbildung 2.4: Delphi mit einer neuen Konsolenanwendung
Die Anweisung in den geschweiften Klammern zwischen begin und end ist ein Kommentar. Kommentare werden zu Dokumentationszwecken verwendet und gehören nicht zum eigentlichen Programm. Der Kommentar in der Delphi-Konsolenanwendung ist ein spezieller Kommentar, der automatisch in der „To Do“-Liste von Delphi erscheint (die aber leider in der Personal-Edition nicht enthalten ist). In Kylix sieht eine Konsolenanwendung ähnlich aus. Das Programm ist aber gegenüber Delphi ein wenig einfacher aufgebaut, weil die usesAnweisung und der Kommentar fehlen. Die uses-Anweisung bindet in Delphi die Bibliothek SysUtils ein (die auch unter Kylix zur Verfügung steht). Diese Bibliothek benötigen Sie in einfachen Anwendungen noch nicht.
53
Sandini Bib
2.2.3 Der Rahmen-Quellcode einer Konsolenanwendung Eine Konsolenanwendung besitzt in Delphi und Kylix einen festgelegten Rahmen: program Programmname; {$APPTYPE CONSOLE} begin end. program
Die erste Anweisung teilt dem Compiler mit, dass hier ein Programm entwickelt wird (und keine Bibliothek, die mit library eingeleitet wird) und definiert gleich auch den Programmnamen. Der Name des Programms muss mit dem Dateinamen identisch sein.1 Solange die Datei noch nicht gespeichert ist, heißt das Programm Project1 oder ähnlich. Wenn Sie die Datei speichern, wird der Programmname automatisch von Delphi bzw. Kylix angepasst.
{$APPTYPE
Die Anweisung {$APPTYPE CONSOLE} ist eine spezielle Compileranweisung. Über diese Anweisung erfährt der Compiler, dass das Programm eine Konsolenanwendung ist.
CONSOLE}
begin ... end
Das eigentliche Programm wird dann zwischen begin und end geschrieben. Diese Schlüsselwörter2 kennzeichnen in Object Pascal einen Programmblock. Ein Programmblock fasst mehrere Anweisungen zu einer Einheit zusammen. Der Programmblock der Konsolenanwendung steht für das gesamte Programm. Der Punkt hinter dem letzten end steht übrigens für das Ende der Programmdatei.
uses
Eine mit Delphi erzeugte Konsolenanwendung besitzt per Voreinstellung noch eine zusätzliche Anweisung: uses SysUtils;
54
1.
In einfachen Konsolenanwendungen kommt es noch nicht zu Fehlern, wenn der Programmname nicht dem Namen der Programmdatei entspricht. In Anwendungen, die mit einer Oberfläche arbeiten, assoziiert die Entwicklungsumgebung aber verschiedene Dateien mit dem Programm. Nennen Sie das Programm um, ohne den Programmdateinamen anzupassen, findet Delphi bzw. Kylix diese Dateien nicht mehr und meldet beim Kompilieren entsprechende Fehler.
2.
Als Schlüsselwort wird ein Wort bezeichnet, das in einer Programmiersprache eine festgelegt Bedeutung besitzt
Sandini Bib
Mit dieser Anweisung, die Sie auch in Kylix verwenden können (weil Kylix auf derselben Programmiersprache basiert), binden Sie die Bibliothek SysUtils in das Programm ein. Eine Bibliothek enthält vordefinierte Programmteile (Funktionen). Die SysUtils-Bibliothek ist ein Bestandteil von Delphi bzw. Kylix und enthält viele „Utilities“ (kleine, nützliche Funktionen). Durch das Einbinden einer Bibliothek können Sie diese in Ihren Programmen verwenden. Die Funktionen der SysUtils-Bibliothek werden eigentlich immer benötigt, weswegen diese Bibliothek in Delphi standardmäßig eingebunden wird. Auf Bibliotheken und deren Bedeutung komme ich noch in Kapitel 4 zurück. Für eine einfache Konsolenanwendung können Sie auf die Einbindung dieser Bibliothek verzichten, ein Einbinden bringt aber keine Nachteile.
2.2.4 Das Projekt Delphi und Kylix arbeiten mit Projekten. In einem Projekt werden alle Dateien verwaltet, die zu einem Programm gehören. Ein Projekt erleichtert zum einen das Wiederfinden der Programmdateien (über die Projektverwaltung, die Sie über das ANSICHT- bzw. VIEW-Menü öffnen können). Zum anderen erkennt die Entwicklungsumgebung über das Projekt, welche Dateien kompiliert werden müssen, wenn Sie das Programm erzeugen. Die Einstellungen eines Projekts werden in Delphi und Kylix in einigen Dateien gespeichert, die im Projektordner angelegt werden. Die Hauptdatei besitzt die Endung .dpr und enthält gleichzeitig den Start-Quellcode des Programms. Die Dateien mit der Endung .cfg und .dof (Delphi) bzw. .conf und .kof (Kylix) verwalten projektspezifische Daten wie spezielle Einstellungen des Compilers. Zum Öffnen eines Projekts benötigen Sie lediglich die .dpr-Datei (und natürlich die Quellcode-Dateien der Anwendung). Die anderen Dateien sollten Sie aber nicht löschen, damit Ihre speziellen Einstellungen nicht verloren gehen (im weiteren Verlauf dieses Buchs werden Sie einige wenige Compiler-Einstellungen ändern). Speichern des Projekts Bevor Sie beginnen zu programmieren, speichern Sie das Projekt idealerweise. Dabei sollten Sie einen wichtigen Grundsatz beachten:
55
Sandini Bib
Speichern Sie alle Dateien eines Projekt grundsätzlich in einem separaten Ordner. Idealerweise sollte ein Ordner nur die Dateien enthalten, die zu einem Projekt gehören. Damit erleichtern Sie sich zum einen das Wiederfinden und Kopieren eines Projekts und sorgen zum anderen dafür, dass Sie keine Probleme mit der Benennung der Dateien erhalten. Dieser Ratschlag kommt aus der Praxis: Viele Anfänger speichern in meinen Seminaren (trotz meines Tipps) alle Projekte in einem einzigen Ordner und haben dann nach einigen Projekten enorme Probleme, die Dateien eines Projekts wiederzufinden. Dabei sollten Sie wohl alle Projekte zusammenhängend speichern. Wenn Sie meinem Vorschlag folgen, erzeugen Sie dazu den Ordner C:\Projekte\Delphi (Windows) bzw. /projekte/kylix (Linux). Sie können die dazu notwendigen Ordner direkt im Speichern-Dialog erzeugen, den Sie in Delphi über den Befehl SPEICHERN im DATEI-Menü und in Kylix über den Befehl SAVE im FILE-Menü öffnen. Das zweite Icon von rechts in der Symbolleiste dieses Dialogs erlaubt das Anlegen neuer Ordner. Erzeugen Sie zudem einen Unterordner für Ihr erstes Projekt. Nennen Sie diesen Ordner vielleicht hello. In diesem Ordner speichern Sie die Projektdatei dann ab.
Projekte sollten in einem eigenen Ordner gespeichert werden
2.2.5 Das erste Delphi/Kylix-Programm Um nun das erste Delphi/Kylix-Programm zu erstellen, erweitern Sie das Grundgerüst folgendermaßen: 01 02 03 04 05 06 07 08 09
program hello; {$APPTYPE CONSOLE} uses SysUtils; begin writeln('Hello World'); end.
Sie verwenden in diesem Beispiel die writeln-Prozedur, die einen Text an der Konsole ausgibt. Eine Prozedur ist ein bereits vorhandenes Teilprogramm, das einen festgelegten Job ausführt (in diesem Fall die Ausgabe eines Textes an der Konsole). Die writeln-Prozedur gehört zu der Bibliothek von Delphi bzw. Kylix. Eine Bibliothek ist eine Sammlung von Prozeduren (und anderen Dingen), die Sie bei der Programmierung nutzen können.
56
Sandini Bib
Dieser Prozedur übergeben Sie einen Text, den Sie in Apostrophe einschließen müssen. Daran erkennt der Compiler, dass es sich um einen Text handelt. Die gesamte Anweisung wird mit einem Semikolon abgeschlossen, was dem Compiler mitteilt, dass die Anweisung zu Ende ist. Wundern Sie sich nicht, wenn Sie unter Windows arbeiten und Umlaute, die Sie in Ihren Konsolenanwendungen ausgeben, mit eigenartigen Zeichen dargestellt werden. Das liegt daran, dass die WindowsKonsole einen anderen Zeichensatz verwendet als der Editor, mit dem Sie das Programm schreiben. Dieses Problem müssen Sie zunächst in Kauf nehmen, weil dessen Lösung nicht trivial ist. Im Artikel „KonsoleIO“ auf der Buch-CD finden Sie eine Lösung. Delphi und Kylix unterscheiden Groß- und Kleinschreibung nicht (wie es aber bei Java der Fall ist). Deshalb ist es prinzipiell unwichtig, wie Sie die Anweisungen schreiben. Sie können also auch Writeln('Hello World');
schreiben. Für Delphi und Kylix macht das keinen Unterschied. Kompilieren des Programms Das Programm können Sie nun über das PROJECT- bzw. PROJEKT-Menü oder über (Strg) (F9) kompilieren. In der (Linux-)KDE ist (Strg) (F9) per Voreinstellung mit einem Wechsel zum Arbeitsbereich 9 belegt. Diese Tastenkombination funktioniert in diesem Fall unter Kylix nicht. Beachten Sie dazu meine Hinweise auf Seite 50. Wenn Delphi/Kylix beim Kompilieren keine Fehler meldet, wurde das Programm erzeugt. Meist meldet die Entwicklungsumgebung aber gleich mehrere Fehler, weil Sie (und ich) eben nicht fehlerfrei sind und schon einmal Programme schreiben, die der Compiler nicht versteht. Ein Fehler, den ich beispielsweise sehr häufig mache, ist, dass ich in Object Pascal Texte versehentlich in Anführungszeichen einschließe: Writeln("Hello World");
Solche Fehler werden als Syntaxfehler bezeichnet, weil Sie die Syntax des Programms betreffen. Syntaxfehler werden im Gegensatz zu Laufzeitfehlern, die erst dann auftreten, wenn das Programm ausgeführt wird, beim Kompilieren gemeldet. Die Entwicklungsumgebung meldet alle Syntaxfehler nach einem Kompilier-Versuch in dem Fenster, das den Programmcode enthält (Abbildung 2.5). Deshalb sind diese Fehler auch sehr einfach zu finden.
Syntaxfehler
57
Sandini Bib
Abbildung 2.5: Kylix meldet Fehler beim Kompilieren
Das Schöne daran ist, dass Sie einfach auf einen der Fehler doppelklicken können um die fehlerhafte Zeile rot zu markieren und den Eingabecursor auf die Fehlerstelle zu bewegen. Starten des Programms Wenn Sie das Programm fehlerfrei kompiliert haben, können Sie dieses ausführen. Prinzipiell ist dies immer auch innerhalb der Entwicklungsumgebung möglich, indem Sie (F9) betätigen. Leider sehen Sie dann zunächst nicht allzu viel. Das Programm wird, da es ein Konsolenprogramm ist, in der Konsole ausgeführt, die nur für die Abarbeitung des Programms einen kurzen Moment geöffnet wird. Ist das Programm beendet, wird auch die Konsole wieder geschlossen. Sie sehen nur auf sehr langsamen Computern überhaupt, dass etwas passiert. Sie können das erzeugte Programm auch in der Konsole direkt starten. Delphi und Kylix erzeugen ausführbare Dateien, die direkt vom Betriebssystem ausgeführt werden können (ohne Delphi bzw. Kylix!). Sie müssen also lediglich den Namen des Programms eingeben. Ein DelphiHello-World-Programm können Sie unter Windows beispielsweise so starten wie in Abbildung 2.6.
Abbildung 2.6: Start des Delphi-Hello-World-Programms in der Konsole unter Windows
58
Sandini Bib
Sinnvoll ist das aber eigentlich während der Entwicklung eines Programms nicht, wozu besitzen Sie schließlich eine Entwicklungsumgebung. Also müssen Sie einen kleinen Trick anwenden, damit das Programm anhält, wenn es in der Entwicklungsumgebung gestartet wurde: 01 02 03 04 05 06 07 08 09 10
program hello; {$APPTYPE CONSOLE} uses SysUtils; begin writeln('Hello World'); readln; end.
Die readln- Prozedur (Read Line) ist eigentlich dazu gedacht, Eingaben entgegenzunehmen. Da diese Prozedur aber darauf wartet, dass der Anwender (¢) betätigt, hält das Programm an der Stelle dieser Anweisung an. Nun können Sie das Programm auch in Delphi direkt starten. In Kylix passiert allerdings auch jetzt noch nicht allzu viel.
2.2.6 Die Kylix Open Edition-Besonderheiten Um Konsolenanwendungen in Kylix ausführen zu können, müssen Sie eine Einstellung des Projekts ändern. Wählen Sie dazu den Befehl PARAMETER im START-Menü. Im Startparameter-Dialog schalten Sie die Option STARTER-ANWENDUNG VERWENDEN ein (Abbildung 2.7).
59
Sandini Bib
Abbildung 2.7: Einstellung der Startparameter den Kylix-Projekts für den Start einer Konsolenanwendung
Damit legen Sie fest, dass Ihr Programm in der bash-Shell gestartet wird. Diese Shell wird dabei so geöffnet, dass Kylix in der Lage ist, das Programm zu kontrollieren (was zur Fehlerbeseitigung beim Debugging notwendig ist). Ändern Sie den im Texteingabefeld angegebenen Aufruf der Shell nur, wenn Sie genau wissen, was Sie tun. Nachdem Sie die neue Einstellung mit OK bestätigt haben, startet das Programm auch unter Kylix in einer Konsole, wenn Sie (F9) betätigen. Diese Einstellung müssen Sie übrigens für jedes neue Kylix-Konsolenanwendungs-Projekt erneut vornehmen. Wie Sie ja bereits wissen, ist die Kylix Open Edition speziell für die Entwicklung von Programmen vorgesehen, die unter der GNU General Public License (GPL) vertrieben werden. Weil jede GPL-Anwendung einen Hinweis auf die Lizenz enthalten muss, gibt eine Anwendung, die Sie mit der Kylix Open Edition erzeugen, einen entsprechenden Hinweis aus (Abbildung 2.8).
GPL-Hinweise
60
Sandini Bib
Abbildung 2.8: Das Hello-World-Programm, gestartet unter Kylix
Dieser Hinweis ist (leider) notwendig und muss im Programm enthalten bleiben. Eine andere Besonderheit von Kylix ist, dass Sie das bash-Debug-Fenster, in dem die Anwendung ausgeführt wird, nicht ohne weiteres vergrößern oder verkleinern können. Wenn Sie dies tun, hält Kylix das Programm mit einer Meldung an (Abbildung 2.9).
Abbildung 2.9: Kylix hat das Programm angehalten, weil das Fenster vergrößert oder verkleinert wurde.
Linux sendet einem Programm das Signal SIGWINCH (Signal Window Changed), wenn ein Fenster irgendwie verändert wurde. Dummerweise reagiert Kylix auf dieses Signal so, dass das Programm angehalten wird (was möglicherweise ein Bug in Kylix ist). In einem angehaltenen Programm können Sie nach Fehlern suchen. Das Debuggen beschreibe ich in Kapitel 4. Nach einer Veränderung der Fenstergröße der Konsole gibt es aber nichts zu debuggen. Um das Programm weiter auszuführen, betätigen Sie (F9), was auch manchmal mehrfach notwendig ist. Wenn das Programm dann wieder ausgeführt wird, können Sie zum bash-Debug-Fenster wechseln und das Programm weiter testen. Sie können das Programm aber auch einfach mit (Strg) (F2) beenden.
61
Sandini Bib
2.3
Eine Delphi/Kylix-Konsolenanwendung zur Berechnung eines Nettobetrags
Die ersten Konsolenanwendung, die Sie entwickelt haben, gab lediglich einen Text an der Konsole aus. Nun wollen wir eine Anwendung entwickeln, die ein wenig mehr macht. Das Programm soll vom Anwender zwei Eingaben anfordern: einen Bruttobetrag und einen Steuerwert. Daraus soll es dann den Nettobetrag ausrechnen und als Ergebnis ausgeben. Das Programm soll etwa so arbeiten, wie es Abbildung 2.10 zeigt.
Abbildung 2.10: Eine einfache Nettoberechnung unter Windows
2.3.1 Das Programm in einer ersten Version Erzeugen Sie in Delphi/Kylix zunächst ein neues Projekt vom Typ Konsolenanwendung. Speichern Sie dieses Projekt in einem neuen Ordner unter dem Namen Nettoberechnung. Den Ordner sollten Sie wieder im Projekte-Ordner anlegen. Nennen Sie die Projektdatei beim Speichern dann ebenfalls Nettoberechnung. Das Programm schreiben Sie wie bei dem Hello-World-Programm innerhalb von begin und end. Es enthält nun aber mehrere Anweisungen. Ich stelle diese Anweisungen in mehreren Schritten vor, damit Sie die Entwicklung nachvollziehen können. Text ausgeben Zunächst muss das Programm den Text „Geben Sie den Bruttobetrag ein:“ ausgeben. Dazu können Sie die writeln-Prozedur benutzen. Diese Prozedur erzeugt aber hinter dem Text einen Zeilenumbruch (das „ln“ im Namen steht für „Line“). Die folgende Eingabe würde dann in der nächsten Zeile erwartet. Um keinen Zeilenumbruch zu erzeugen, verwenden Sie stattdessen die write- Prozedur:
62
Sandini Bib
01 02 03 04 05 06 07 08 09
program nettoberechnung; {$APPTYPE CONSOLE} uses SysUtils; begin write('Geben Sie den Bruttobetrag ein: '); End.
Eingaben entgegennehmen Im nächsten Schritt soll das Programm auf eine Eingabe des Anwenders warten. Dazu können Sie die readln-Prozedur einsetzen. Diese Prozedur wartet darauf, dass der Anwender (¢) betätigt. readln ist aber auch in der Lage, die Zeichen, die der Anwender vor (¢) eingegeben hat, einzulesen und zwischenzuspeichern. Da diese Zeichen gespeichert werden müssen, benötigen Sie eine Variable, in die readln die eingelesenen Zeichen schreiben kann. Eine Variable ist im Prinzip so etwas wie ein Stück vom Arbeitsspeicher, in das Ihr Programm oder eine aufgerufene Prozedur Daten schreiben und diese auch wieder auslesen kann. Variablen müssen deklariert werden. Deklarieren bedeutet, dass Sie die Variable dem Compiler bekannt geben. Damit weiß dieser, dass er bei der Erzeugung des Programms dafür sorgen muss, dass bei der Ausführung ein entsprechend großes Stück vom Arbeitsspeicher für das Programm reserviert wird. Die Deklaration nehmen Sie in einer einfachen Konsolenanwendung oberhalb des Programmblocks (also oberhalb begin) vor. Kapitel 4 geht noch näher auf Variablen ein. Dort kläre ich, warum Sie bei der Deklaration einen Datentypen angeben müssen. Der Datentyp der Variablen des Beispielprogramms ist double. Damit legen Sie fest, dass die Variable Zahlen mit Dezimalstellen speichern kann. Außerdem erhält die Variable eine Namen, damit sie im Programm angesprochen werden kann. Diese Variable übergeben Sie dann der readlnProzedur (ähnlich wie Sie der write-Prozedur einen Text übergeben haben):
Variablen deklarieren und verwenden
63
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12
program nettoberechnung; {$APPTYPE CONSOLE} uses SysUtils; var brutto: double; begin write('Geben Sie den Bruttobetrag ein: '); readln(brutto); end.
Die Eingabe des Bruttowerts funktioniert nun bereits. Das Programm muss aber noch einen Steuerwert anfordern und den Nettowert berechnen. Für die Eingabe des Steuerwerts benötigen Sie eine weitere Variable. Aber auch zur Speicherung des berechneten Nettowerts müssen3 Sie eine Variable einsetzen. Den Inhalt dieser Variablen geben Sie schließlich nur noch aus. Die erste Version des Programms sieht dann so aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20
3.
64
program nettoberechnung; {$APPTYPE CONSOLE} uses SysUtils; var brutto: double; var steuer: double; var netto: double; begin write('Geben Sie den Bruttobetrag ein: '); readln(brutto); write('Geben Sie den Steuerwert ein: '); readln(steuer); netto := brutto * (1 - (steuer / (100 + steuer))); write('Der Nettobetrag ist: '); writeln(netto); readln; end.
„Müssen“ ist an dieser Stelle nicht ganz korrekt. Bei der Programmierung kann man viele Wege gehen. Das Ergebnis kann beispielsweise auch ohne Variable berechnet und direkt ausgegeben werden.
Sandini Bib
Ich denke, dass ich den Programm-Quelltext nicht komplett erläutern muss. Die meisten einzelnen Anweisungen kennen Sie ja bereits. Neu ist die Berechnung des Nettobetrags in Zeile 16. Die Berechnung Bei der Berechnung des Nettobetrags handelt es sich um eine Zuweisung. Dabei wird das Ergebnis der rechts von := stehenden Berechnung in die links stehende Variable geschrieben. Die Berechnung selbst verwendet die in der Mathematik üblichen Zeichen. Diese Zeichen gehören natürlich zur Syntax der Programmiersprache und werden vom Compiler entsprechend ihrer Bedeutung interpretiert. Neu ist auch, dass Sie der writeln-Prozedur (wie jeder anderen Prozedur) auch eine Variable übergeben können. writeln gibt dann einfach den Inhalt dieser Variablen aus, im Beispiel also den errechneten Nettobetrag. Das Programm funktioniert schon recht gut (Abbildung 2.11). Achten Sie aber zunächst darauf, dass Sie beim Testen nur Zahlen eingeben.
Abbildung 2.11: Die erste Version der Nettoberechnung unter Windows
Wie Sie Abbildung 2.11 entnehmen können, entspricht die Ausgabe des errechneten Nettobetrags nicht dem, was die Aufgabenstellung (Abbildung 2.10 auf Seite 62) fordert. writeln verwendet zur Ausgabe von Zahlen mit Dezimalstellen per Voreinstellung leider die wissenschaftliche Notation. Bei dieser Notation wird die Zahl so dargestellt, dass eine Zahl mit einer Ziffer vor dem Komma dargestellt und mit einer Potenz von 10 multipliziert wird. 5.1E+3 steht für 5,1 * 103, also 5,1 * 1000 (5100). Bei der Ausgabe wird zudem die englische Zahlendarstellung verwendet, bei der der Punkt das Dezimaltrennzeichen ist.
2.3.2 Formatieren der Ausgabe Sie können (natürlich) auch erreichen, dass die Zahl in einer besser lesbaren Form ausgegeben wird. Jede Programmiersprache stellt dazu Features zur Formatierung zur Verfügung (das Formatieren einer Ausgabe
65
Sandini Bib
ist eben ein wichtiges Thema). In Object Pascal können Sie zur Formatierung die Funktion FormatFloat verwenden. Wenn Sie den Nettobetrag mit führender Null zwei Stellen nach dem Komma formatieren wollen, verwenden Sie die folgende Anweisung: writeln(FormatFloat('0.00', netto));
Diese Anweisung ist bereits etwas komplexer. Die FormatFloat-Funktion wird in den Klammern der writeln-Prozedur aufgerufen. Der Compiler sorgt dafür, dass diese Funktion vor writeln aufgerufen wird. Eine Funktion gibt immer Daten zurück. Die FormatFloat-Funktion gibt die formatierte Zahl zurück. Damit diese Funktion weiß, wie sie formatieren soll und was formatiert werden soll, übergeben Sie einen Text mit FormatierInformationen und die zu formatierende Zahl. Die Syntax der Formatierzeichen ist für die FormatFloat-Funktion festgelegt. Sie können diese Syntax in der Hilfe zu dieser Funktion nachlesen. Der Text '0.00' sorgt dafür, dass die Zahl mit führender Null und zwei Nachkommaziffern ausgegeben wird. Das Ergebnis der Formatierung wird dann von der writeln-Prozedur weiterverarbeitet und an der Konsole ausgegeben. Das Resultat entspricht also bereits der gestellten Aufgabe (Abbildung 2.10 auf Seite 62).
2.3.3 Umgang mit Fehlern, die durch ungültige Eingaben verursacht werden Das Nettoberechnungs-Programm ist noch nicht perfekt. Wenn Sie das Programm ausführen und eine ungültige Zahl eingeben (z. B. 75oo, mit zwei kleinen O an Stelle der Null, was ein häufiger Fehler bei der Eingabe ist), erhalten Sie eine Fehlermeldung. Wird das Programm über Delphi/Kylix ausgeführt, hält die Entwicklungsumgebung das Programm an und meldet den Fehler in einem Dialog (Abbildung 2.12).
Abbildung 2.12: Fehlermeldung bei der Ausführung unter Kylix
Das Programm meldet eine Ausnahme (Exception). Ausnahmen werden während der Ausführung eines Programms erzeugt, nachdem ein Zustand eingetreten ist, der verhindert, dass das Programm weiter feh-
Ausnahmen, Laufzeitfehler und logische Fehler
66
Sandini Bib
lerfrei ausgeführt wird. Ausnahmen werden auch als Laufzeitfehler4 bezeichnet, weil es sich um Fehler handelt, die während der Laufzeit eines Programms auftreten. Die Ausnahme im Beispiel wurde dadurch verursacht, dass die Eingabe nicht in eine Zahl umgewandelt werden kann. In einfachen Programmen ist ein solcher Umwandlungsfehler eigentlich die einzig mögliche Ausnahme. Später, wenn Ihre Programme komplexer werden, werden Sie wahrscheinlich häufiger mit Ausnahmen konfrontiert. Wenn Ihr Programm beispielsweise eine Datei oder eine Datenbank öffnen will, die aber nicht vorhanden ist, resultiert dies in einer Ausnahme. Ausnahmen können Sie abfangen, was ich in Kapitel 4 zeige. Ausnahmen werden häufig durch Bugs verursacht. Ein Bug ist ein logischer Fehler im Programm. Ein Programm, das Bugs aufweist, enthält Fehler in der Programmlogik. Solche Fehler werden Sie in Kapitel 4 kennen lernen und dort auch erfahren, wie Sie diese beseitigen (Ihr Programm also „debuggen“) können. Der logische Fehler in unserem Nettoberechnungsprogramm ist der, dass das Programm fehlerhafte Eingaben nicht berücksichtigt. Delphi und Kylix halten das Programm beim Eintritt einer Ausnahme in der Entwicklungsumgebung an. Delphi markiert die fehlerauslösende Anweisung und gibt Ihnen die Möglichkeit, den Fehler zu debuggen (Abbildung 2.13).
Das Programm wird bei einem Fehler angehalten
Abbildung 2.13: Delphi hat das Programm aufgrund einer fehlerhaften Eingabe angehalten. 4.
Um genau zu sein handelt es sich bei Laufzeitfehlern nicht um Ausnahmen. Ein Laufzeitfehler führt dazu, dass ein Programm sofort (mit einer Fehlermeldung) beendet wird. Eine Ausnahme kann hingegen abgefangen und behandelt werden. Das Programm kann nach dem Eintritt einer Ausnahme in diesem Fall weiter ausgeführt werden. Unbehandelte Ausnahmen führen aber in der Regel zu einem Laufzeitfehler.
67
Sandini Bib
Viel können Sie hier noch nicht debuggen. Der Fehler tritt intern auf, bei der Konvertierung der Eingabe zu einer Zahl. Immerhin wissen Sie, von welcher Anweisung der Fehler ausgelöst wird. Beenden Sie das Programm einfach über (Strg) (F2). In Kapitel 4 erfahren Sie mehr über das Debuggen. In Kylix sieht das Ganze leider nicht so aus. Kylix hält das Programm zwar ebenfalls an, markiert allerdings nicht die fehlerauslösende Anweisung. Beenden können Sie aber auch hier über (Strg) (F2). Leider ist diese Tastenkombination (wie viele andere) in der KDE per Voreinstellung belegt (die KDE wechselt dann zum Arbeitsbereich 2). Sie können das Programm auch über den Befehl PROGRAMM ZURÜCKSETZEN im STARTMenü beenden. Beachten Sie meine Hinweise zu Linux auf Seite 50. Wenn Sie das Programm direkt über die Konsole des Betriebssystems ausführen, wird die Ausnahme an der Konsole gemeldet (Abbildung 2.14).
Fehlermeldung an der Konsole
Abbildung 2.14: Fehlermeldung aufgrund einer ungültigen Eingabe unter Windows
Ein Windows-Programm meldet den Fehler zudem direkt nach der Eingabe in einem separaten Dialog (Abbildung 2.15).
Abbildung 2.15: Zusätzliche Fehlermeldung unter Windows
Etwas verwirrend ist, dass Windows nicht die wahre Ursache des Fehlers erkennt und einen „Unbekannten Softwarefehler“ meldet. Nun wissen Sie, woher die manchmal sehr eigenartigen Windows-Fehlermeldungen kommen. Die Zahlen, die in den Fehlermeldungen angegeben werden, beziehen sich übrigens auf Speicheradressen. Dazu werden Sie im nächsten Kapitel noch mehr erfahren.
68
Sandini Bib
Damit Ausnahmen nicht zu den etwas undurchsichtigen Fehlermeldungen führen, können Sie diese abfangen und eine eigene Fehlermeldung ausgeben. Da es sich dabei bereits um eine komplexere Programmierung handelt, erfahren Sie erst in Kapitel 4, wie Sie dies programmieren.
2.4
Entwicklung einer einfachen Delphi/KylixAnwendung mit grafischer Oberfläche
Die wenigsten Programme arbeiten heutzutage noch an der Konsole. Ein modernes Programm besitzt normalerweise eine Oberfläche mit Fenstern und Bedienelementen. Sie kennen solche Programme bereits von Ihrer Arbeit mit Windows bzw. einem der Desktops von Linux (KDE, GNOME etc.). Programme mit einer grafischen Oberfläche bieten dem Anwender enorme Vorteile, allein schon dadurch, dass sie in der Regel wesentlich einfacher zu bedienen sind als Konsolenanwendungen. Solch ein Programm wollen wir nun auch entwickeln. Damit Sie einen direkten Vergleich haben, soll die Nettoberechnung in einer grafischen Anwendung umgesetzt werden. Um nicht immer „Anwendung mit grafischer Oberfläche“ schreiben zu müssen, bezeichne ich eine solche Anwendung ab hier einfach als „normale Anwendung“. Unter Windows wäre zwar auch der Begriff „Windows-Anwendung“ sinnvoll, unter Linux passt dieser Begriff aber (natürlich) nicht.
2.4.1 Ein Projekt für eine normale Anwendung Erzeugen Sie zur Erstellung einer normalen Anwendung ein neues Projekt. In Delphi wählen Sie dazu den Befehl APPLICATION im FILE/NEWMenü. In Kylix wählen Sie den Befehl NEUE ANWENDUNG im DATEIMenü. Beide Entwicklungsumgebungen erzeugen ein Projekt, das bereits ein Fenster enthält (Abbildung 2.16).
69
Sandini Bib
Abbildung 2.16: Eine neue normale Anwendung in Delphi
Ein Fenster wird üblicherweise auch als „Formular“ bezeichnet, was wohl dadurch begründet ist, dass ein Fenster, das Eingabeelemente enthält, wie ein (Papier-)Formular zur Eingabe von Daten verwendet werden kann. Speichern Sie das Projekt ab, bevor Sie beginnen zu programmieren. Damit erreichen Sie, dass Delphi das Programm beim Testen im Projektordner erzeugt und nicht irgendwo auf der Festplatte, und gehen sicher, dass Sie das Speichern später nicht vergessen. Später brauchen Sie dann immer nur (Strg) (s) zu betätigen, um die Datei, an der Sie gerade arbeiten, zu speichern. Machen Sie es sich zur Gewohnheit, in (mehr oder weniger) regelmäßigen Abständen zu speichern. Entwicklungsumgebungen stürzen schon einmal ab. Das Speichern umfasst nun zwei Dateien: die Projektdatei und die Formulardatei. Sie müssen beide Dateien unterschiedlich benennen. Nennen Sie die Projektdatei (die die Endung .dpr besitzt) vielleicht nettoberechnung und die Formulardatei (die die Endung .pas besitzt) fstart. In Wirklichkeit speichert Delphi übrigens noch eine dritte Datei für das Formular und weitere Dateien für das Projekt, die ich im nächsten Abschnitt beschreibe.
70
Sandini Bib
Sie können Delphi und Kylix auch so einstellen, dass beim Testen (mit (F9)) immer automatisch gespeichert wird. Diese Einstellung ist sehr zu empfehlen, denn auch beim Testen können Programme und Entwicklungsumgebungen abstürzen. In Delphi wählen Sie dazu den Befehl ENVIRONMENT OPTIONS im TOOLS-Menü. Im Dialog zur Einstellung der Umgebungs-Optionen schalten Sie die Option EDITOR FILES im AUTOSAVE OPTIONS-Block des PREFERENCES-Registers ein. In Kylix sieht das Ganze ähnlich aus, lediglich mit deutschen Bezeichnungen: Über das Menü TOOLS erreichen Sie die UMGEBUNGSOPTIONEN. Hier schalten Sie die Option EDITORDATEIEN im Block OPTIONEN FÜR AUTOSPEICHERN des PRÄFERENZEN-Registers ein. Die Dateien eines Projekts für eine normale Anwendung Für eine normale Anwendung erzeugt Delphi/Kylix zunächst, genau wie bei einer Konsolenanwendung, die Projektdatei mit der Endung .dpr. Zusätzlich werden pro Formular je zwei Dateien erzeugt. Die .dfm(Delphi Form) bzw. xfm-Datei (Kylix Form) enthält die Definition des Formulars, also dessen Einstellung und alle Steuerelemente, die auf dem Formular angelegt wurden. Die .pas-Datei enthält den Programmcode des Formulars, also alle Programme, die Sie für ein Formular schreiben. Daneben erzeugt die Entwicklungsumgebung oft noch eine Datei mit der Endung .res. Diese Datei verwaltet Bilder, die Sie auf den Formularen Ihrer Programme ablegen. Andere Projektdateien speichern wieder wie bei einer Konsolenanwendung projektspezifische Einstellungen.
2.4.2 Entwurf des Formulars Für unseren Zweck reicht eine Anwendung mit einem einzigen Formular aus. Sie müssen das Formular nun nur noch mit passenden Eingabeelementen versehen und ein wenig programmieren . Das Ergebnis soll in etwas so aussehen wie in Abbildung 2.17.
-
Abbildung 2.17: Eine Delphi-Nettoberechnung unter Windows
71
Sandini Bib
Die Arbeitsweise dieses Programms dürfte klar sein: Der Anwender kann den Bruttobetrag und den Steuerwert eingeben und den Nettobetrag über den RECHNEN-Schalter ausrechnen lassen. Der BEENDEN-Schalter soll das Programm beenden. Steuerelemente Zur Erstellung einer Programmoberfläche stellen Ihnen Delphi und Kylix so genannte Steuerelemente zur Verfügung. Ein Steuerelement wird, wie der Name schon sagt, zur Steuerung einer Anwendung verwendet. Normalerweise sind Steuerelemente dazu gedacht, dass der Anwender etwas darin eingibt, mit der Maus darauf klickt, etwas aus einer Liste auswählt usw. Sie kennen die meisten Steuerelemente wohl bereits von Ihrer täglichen Arbeit mit Windows bzw. Linux. Viele Steuerelemente werden übrigens mehr oder weniger direkt vom Betriebssystem (bzw. bei Linux von einem Fenstermanager) zur Verfügung gestellt und sehen deshalb in allen Anwendungen prinzipiell gleich aus. Die Delphi/Kylix-Steuerelemente finden Sie oben im Delphi/KylixHauptfenster (Abbildung 2.18 und Abbildung 2.19).
Abbildung 2.18: Die Delphi-Steuerelemente
Abbildung 2.19: Die Kylix-Steuerelemente
Die Steuerelemente sind in Kategorien eingeteilt, damit Sie diese besser finden. Lassen Sie sich von der Vielzahl der Steuerelemente nicht verwirren. Zunächst benötigen Sie nur die drei wichtigsten. Die anderen Steuerelemente können Sie später immer noch erforschen. Wie Sie sehen, besitzt Kylix wesentlich weniger Steuerelemente als Delphi. Die Steuerelemente von Delphi und Kylix sind übrigens zwar ähnlich, aber nicht identisch. Die Grundfunktionalität von Steuerelementen wird im Fall von Delphi und Kylix (wie auch bei den meisten anderen Sprachen mit Ausnahme von Java) vom Betriebssystem zur Verfügung gestellt, und das ist bei Delphi eben ein anderes als bei Kylix. Deshalb können Sie Delphi-Projekte mit Formularen und Steuerelementen zunächst auch nicht unter Kylix kompilieren. Die in Kylix verwendete Steuerelementbibliothek können Sie aber (in einer Windows-Adaption) auch in der Professional- und Enterprise-Edition von Delphi nutzen
Steuerelemente werden vom Betriebssystem zur Verfügung gestellt
72
Sandini Bib
und so Programme entwickeln, die Sie unter Windows und Linux kompilieren und ausführen können. Aber das ist nur ein Hinweis am Rande. Sie können einzelne Steuerelemente nun auf dem Formular platzieren, indem Sie diese im Steuerelement-Register anklicken und danach auf das Formular klicken. Mit der Hilfe der Maus können Sie die Steuerelemente dann an ihren vorgesehenen Ort bewegen. Über die Anfasser (das sind die kleinen Kästchen, die ein Steuerelement besitzt, wenn es aktiviert wurde) können Sie das Steuerelement in der Größe verändern. Für die Nettoberechnung benötigen Sie die folgenden Steuerelemente: • Label: Ein Label wird zur Beschriftung von Formularen und zur Ausgabe von Daten verwendet. Ein Label erlaubt keine Eingaben. • Edit: Ein Edit-Steuerelement (das oft auch als Textbox oder Textfeld bezeichnet wird) wird in der Regel dazu verwendet, den Anwender Texte oder Zahlen eingeben zu lassen. In einem solchen Steuerelement können Sie aber auch Daten ausgeben. • Button: Ein Button-Steuerelement ist ein Schalter, der vom Anwender betätigt werden kann um eine bestimmte Aktion auszulösen.
Sie können Steuerelemente, die bereits auf einem Formular angelegt sind, auch kopieren. Selektieren Sie die zu kopierenden Steuerelemente auf dem Formular, indem Sie mit der Maus einen „Rahmen“ um diese ziehen. Jedes Steuerelement, das vom Rahmen berührt wird, wird in die Selektion aufgenommen, wenn Sie die Maustaste loslassen. Kopieren Sie die Steuerelemente dann in die Zwischenablage ((Strg) (c)) und fügen Sie den Inhalt der Zwischenablage direkt wieder ein ((Strg) (v)). Die nun selektierten neuen Steuerelemente können Sie schließlich gemeinsam über die Maus an ihren vorgesehenen Ort bewegen. Legen Sie auf diese Art die für die Nettoberechnung benötigten Steuerelemente auf dem Formular an, bis dieses in etwa so aussieht wie in Abbildung 2.20.
73
Sandini Bib
Abbildung 2.20: Der erste Entwurf der Nettoberechnung in Kylix
2.4.3 Einstellen der Eigenschaften Nun müssen Sie einige Eigenschaften der Steuerelemente und des Formulars einstellen. Eigenschaften beeinflussen das Verhalten oder das Aussehen der Steuerelemente. Die Beschriftung des Formulars, der Label und der Schalter wird z. B. in der Eigenschaft Caption eingestellt. Steuerelemente besitzen daneben aber eine Vielzahl weiterer Eigenschaften. Wichtige sind z. B. die Breite (Width), die Höhe (Height), die Farbe (Color) und der Inhalt von Textboxen (Text). Die wichtigste Eigenschaft ist jedoch Name. Über diese Eigenschaft stellen Sie den Namen der Steuerelemente ein. Über diesen Namen können Sie die Steuerelemente später im Programm ansprechen. Eigenschaften stellen Sie ein, indem Sie das Steuerelement mit der Maus aktivieren und den Wert im Objektinspektor eintragen. Abbildung 2.21 zeigt, wie die Eigenschaft Name der ersten Textbox (die zurzeit noch Edit1 heißt) eingestellt wird.
Abbildung 2.21: Der Objektinspektor von Kylix. Eingestellt wird die Eigenschaft Name der ersten Textbox.
74
Sandini Bib
Falls der Objektinspektor nicht sichtbar ist, können Sie diesen über (F11) anzeigen. Suchen Sie dann die Eigenschaft in der linken Spalte und tragen Sie den Wert in der rechten Spalte ein. Mit (F11) können Sie auch sehr schnell in den Objektinspektor wechseln. Benennen Sie Steuerelemente, die Sie im Programm ansprechen wollen, immer mit einem aussagekräftigen Namen. Sie erreichen damit, dass Ihr Programm wesentlich übersichtlicher wird. Wenn Sie beispielsweise in einem Programm den Namen brutto sehen, erkennen Sie daran die Bedeutung des Namens recht gut. Übernehmen Sie hingegen die voreingestellten Namen (wie edit1, edit2 etc.), macht dies das Programm unnötig schwerer verständlich, als es möglicherweise sowieso schon ist. Viele Programmierer benennen Steuerelemente mit einer bestimmten Konvention. Einige stellen beispielsweise zwei bis drei kleingeschriebene Zeichen vor den eigentlichen Namen, die den Typ des Steuerelements angeben. Eine Textbox (die in Delphi ein Edit-Steuerelement ist), können Sie beispielsweise mit dem Präfix „txt“ versehen. Die Textbox zur Eingabe des Bruttobetrags würde demnach also txtBrutto heißen. Andere Programmierer hängen die Bezeichnung des Steuerelements an den Namen an: bruttoTextbox. Das Ganze bringt den Vorteil, dass Sie im Programm genau erkennen, um was es sich handelt. Die Verwendung solcher speziellen Bezeichner gehört allerdings schon zum Bereich „Programmier-Philosophie“ und wird im Internet teilweise heftig diskutiert. Ich benenne die Steuerelemente folgendermaßen: • txtBrutto: Textbox zur Eingabe des Bruttobetrags • txtSteuer: Textbox zur Eingabe des Steuerwerts • txtNetto: Textbox zur Ausgabe des errechneten Nettobetrags • btnRechnen: Schalter zur Ausführung der Berechnung • btnBeenden: Schalter zum Beenden des Programms Eine Benennung mit einer Konvention macht zwar etwas Arbeit, besitzt aber eigentlich nur Vorteile. Sie erkennen im Programm schon am Namen, worum es sich handelt, was in vielen Fällen ein weiteres Nachschauen unnötig macht, wenn Sie versuchen das Programm zu verstehen. Der Name txtBrutto sagt z. B. alles aus, was notwendig ist: Es handelt sich um eine Textbox, die einen Bruttowert verwalten soll. Das ist sehr eindeutig. Ich empfehle Ihnen, ebenfalls eine solche Konvention zu verwenden. Festgelegte Konventionen gibt es aber für Delphi und Kylix wohl nicht. Leiten Sie den Präfix einfach aus der Bezeichnung des
75
Sandini Bib
Steuerelements ab (wobei ich die Ausnahme bei dem Edit-Steuerelement mache, das üblicherweise als Textbox bezeichnet wird). Verwechseln Sie die Benennung eines Steuerelements nicht mit dessen Beschriftung. Ein Button-Steuerelement, das eine Beschriftung besitzt, kann beispielsweise calculate heißen und mit Rechnen beschriftet sein. Neben dem Namen müssen Sie noch die Eigenschaft Caption der Labelund der Button-Steuerelemente einstellen. Markieren Sie dazu jedes dieser Steuerelemente nacheinander und stellen Sie den neuen Wert ein. Wenn Sie die Eigenschaften der Steuerelemente nacheinander einstellen, erleichtern Sie sich die Arbeit. Stellen Sie also zuerst beispielsweise die Eigenschaft Name der ersten Textbox ein. Aktivieren Sie dann die zweite Textbox und betätigen Sie (F11). Da der Objektinspektor immer auf der zuletzt eingestellten Eigenschaft stehen bleibt und nach (F11) gleich in die Wertspalte dieser Eigenschaft wechselt, können Sie sofort den Namen der zweiten Textbox einstellen etc. Der Inhalt der Textboxen wird nicht in der Eigenschaft Caption (die diese gar nicht besitzen), sondern in der Eigenschaft Text eingestellt. Setzen Sie diese Eigenschaften auf einen Wert, der als Voreinstellung verwendet werden kann. Für die Nettoberechnung bietet sich die Zahl Null an. Das Formular sollte ebenfalls einen Namen und eine sinnvolle Beschriftung erhalten. Um die Eigenschaften des Formulars einstellen zu können, müssen Sie auf eine freie Stelle des Formulars klicken (also nicht auf ein Steuerelement). Ich benenne das Formular, mit dem meine Anwendungen starten, immer mit StartForm. Der Name eines Formulars hat nichts mit dem Namen der Datei zu tun, in der das Formular gespeichert ist. Ein Formular mit den Namen StartForm kann also z. B. in einer Datei fStart.pas gespeichert sein. Delphi und Kylix verlangen sogar (unsinnigerweise), dass der Name eines Formulars ein anderer ist als der Name der Formulardatei. Sie können ein Formular, das in einer Datei mit dem Namen StartForm.pas gespeichert ist, also nicht StartForm nennen. Deshalb habe ich die Formulardatei beim Speichern auch fStart.pas genannt .
-
Das Ergebnis soll dann in etwa aussehen wie in Abbildung 2.22.
76
Sandini Bib
Abbildung 2.22: Das Nettoberechnungs-Formular in Delphi
2.4.4 Ereignisorientiertes Programmieren Sie werden gleich beginnen, mit dem Formular zu programmieren. Dazu werden Sie Methoden erzeugen, die auf bestimmte Ereignisse reagieren. Sie sollten wissen, worum es sich dabei handelt. Methoden Eine Methode ist zunächst so etwas wie eine Prozedur (oder Funktion). Prozeduren haben Sie bereits verwendet. Das Nettoberechnungsprogramm von Seite 62 nutzt z. B. die Prozeduren write, writeln und readln. Methoden unterscheiden sich nur in geringem Maße von Prozeduren (und Funktionen). Eigentlich handelt es sich dabei nur um einen Begriff zur Kennzeichnung von Prozeduren und Funktionen, die in einem bestimmten Kontext verwendet werden. Methoden enthalten nämlich genau wie Prozeduren und Funktionen eine oder mehrere Programmanweisungen, die unter dem Namen der Methode aufgerufen werden können. Der Unterschied zwischen Prozeduren/Funktionen und Methoden ist, dass Methoden immer zu Objekten gehören. Ich will noch nicht auf dieses etwas komplexe Thema eingehen. In Kapitel 6 erfahren Sie mehr darüber. An dieser Stelle ist lediglich wichtig, dass ein Formular ein Objekt ist. Funktionen und Prozeduren, die zum Formular gehören, werden deshalb als Methoden bezeichnet. Ich will von vornherein die korrekten Fachbegriffe verwenden. Deswegen lesen Sie den Begriff „Methode“ bereits in diesem Kapitel recht häufig.
Methoden sind Prozeduren oder Funktionen in Objekten
Ereignisse In einer Anwendung, die mit Formularen arbeitet, werden Programme anders ausgeführt als in einer Konsolenanwendung. Wie Sie in den ersten Beispielen bereits gesehen haben, beginnt ein Konsolenprogramm immer an der ersten Anweisung hinter begin und endet an der letzten Anweisung vor end. Das Programm wird sequenziell von oben nach unten abgearbeitet. Ist das Programm unten angelangt, wird es beendet.
77
Sandini Bib
Eine Anwendung mit grafischer Oberfläche arbeitet aber ganz anders. Eine solche Anwendung startet, indem das Hauptformular geöffnet wird. Betätigen Sie für unsere Beispielanwendung einfach einmal (F9) und Sie wissen, was ich meine. Das Formular wird geöffnet und ... wartet. Es wartet so lange, bis etwas passiert. Wenn Sie auf den kleinen Schalter zum Minimieren in der Symbolleiste des Formulars klicken, reagiert es, indem es sich verkleinert. Wenn Sie mit der Maus auf den Rand des Formulars klicken und die Maus ziehen, regiert das Formular ebenfalls, dieses Mal, indem es sich verkleinert oder vergrößert. Klicken Sie auf den Symbolleistenschalter zum Schließen, reagiert das Formular wieder, es schließt sich. Dasselbe geschieht auch, wenn Sie bei geöffnetem Formular (Alt) (F4) betätigen. Das Formular reagiert auf verschiedene Ereignisse, teilweise mit unterschiedlichen Programmen.
Anwendungen mit grafischer Oberfläche arbeiten anders als Konsolenanwendungen
Dieses Basisverhalten des Formulars ist bereits vordefiniert. Sie müssen nichts weiter programmieren, damit ein Formular geschlossen, in der Größe verändert, minimiert oder maximiert werden kann. Ein Formular sollte aber natürlich ein wenig mehr können. Unser NettoberechnungsFormular muss z. B. darauf reagieren, dass der Anwender den RECHNENoder den BEENDEN-Schalter betätigt. Und hier kommen Ereignisbehandlungsmethoden ins Spiel. Ein Formular kann nämlich auf die verschiedensten Ereignisse reagieren. Dazu programmieren Sie eine Ereignisbehandlungsmethode. Die Entwicklungsumgebung hilft dabei, Sie müssen lediglich das richtige Ereignis für das richtige Steuerelement auswählen. Der Rumpf der Ereignisbehandlungsmethode wird automatisch erzeugt. Das einzige, was Sie machen müssen ist, die Reaktion auf das Ereignis zu programmieren.
Ereignisbehandlungsmethoden reagieren auf Ereignisse
Sie können für die einzelnen Steuerelemente und für das Formular eine große Anzahl an verschiedenen Ereignissen auswerten. Für ein Textfeld können Sie z. B. darauf reagieren, • dass der Anwender mit der Maus in dieses Feld klickt, • dass der Anwender auf diesem Feld einen Doppelklick ausführt, • dass der Anwender den Eingabefokus5 auf dieses Feld setzt, • dass der Anwender die Tastatur betätigt, • dass der Anwender das Feld mit dem Eingabefokus verlässt, • dass der Anwender die Maus über dem Feld bewegt oder • dass der Anwender Daten von einem anderen Programm in dieses Feld zieht. 5.
78
Der Eingabefokus bezeichnet das Steuerelement, das gerade Eingaben empfängt. Wenn Sie z. B. mit der Maus oder der Tab-Taste zu einem Textfeld wechseln, besitzt dieses den Eingabefokus und erhält alle Eingaben.
Sandini Bib
Das ist aber nur eine kleine Auswahl der möglichen Ereignisse. Die meisten Steuerelemente stellen noch eine Vielzahl weiterer Ereignisse zur Verfügung. Die Idee dahinter ist, dem Programmierer möglichst für alles, was vorkommen kann, ein Ereignis zu liefern. Sie können nun für jedes Steuerelement und für das Formular verschiedene Ereignisse auswerten, wenn Ihr Programm dies erfordert. So können Sie z. B. beim Verlassen eines Textfeldes den eingegebenen Wert überprüfen und dem Benutzer bei einer ungültigen Eingabe eine entsprechende Meldung übergeben. Dazu schreiben Sie eine Ereignisbehandlungsmethode für das „LostFocus“-Ereignis der Textbox. In vielen Fällen reicht aber die Reaktion auf die Betätigung der Schalter eines Formulars vollkommen aus. Manchmal müssen Sie auch auf das Öffnen eines Formulars reagieren, um dieses zu initialisieren, oder auf das Schließen, um abschließende Aufräumarbeiten zu erledigen. Die ersten Programme, die Sie in diesem Kapitel entwickeln, reagieren lediglich auf die Betätigung eines Schalters. In Kapitel 8 werden Sie aber auch das Öffnen und Schließen eines Formulars behandeln. Vielleicht wollen Sie wissen, wie Ereignisse prinzipiell funktionieren. Eigentlich ist das Ganze recht einfach. Formulare werden immer vom Betriebssystem erzeugt und stehen deswegen unter dessen Kontrolle. Das Betriebssystem behandelt daneben auch alle Eingaben des Anwenders. Klickt der Anwender mit der Maus auf ein Formular, erkennt das Betriebssystem, welches Formular unter der Maus liegt, und sendet dem Programm, das für das Formular verantwortlich ist, eine Nachricht mit der Information, auf welches Formular der Anwender geklickt hat und welche Position die Maus dabei hatte. Das Programm fängt diese Nachricht (intern) ab und wertet sie aus. An Hand der übergebenen Mausposition erkennt das Programm, auf welches Steuerelement geklickt wurde. Es überprüft, ob für dieses Steuerelement und das entsprechende Ereignis eine Ereignisbehandlungsmethode existiert und ruft diese gegebenenfalls auf.
2.4.5 Programmieren der Berechnung Das Programmieren der Berechnung ist nun, nach der ganzen LayoutArbeit und dem neuen Wissen über Ereignisse, ein Leichtes. Die Aufgabe bestimmt, dass die Berechnung ausgeführt wird, wenn der Anwender den RECHNEN-Schalter betätigt. Dazu müssen Sie eine Ereignisbehandlungsmethode erstellen, die auf die Betätigung des Schalters reagiert.
79
Sandini Bib
Bevor Sie beginnen zu programmieren, sollten Sie noch einmal die Namen Ihrer Steuerelemente überprüfen. Sie erzeugen gleich Methoden, die automatisch mit dem Namen der Steuerelemente versehen werden. Wenn Sie die Steuerelemente später umbenennen, werden Ihre Bezeichnungen im Programm inkonsistent. Beim Versuch, die erzeugten Methoden umzubenennen, können am Anfang allerlei Fehler auftreten, was besonders dann gilt, wenn Sie die Methode einfach löschen. Für den Anfang (und eigentlich auch später) sollten Sie diese Fehler möglichst vermeiden, indem Sie die Steuerelemente vor dem Programmieren sinnvoll benennen und den Namen danach nicht mehr ändern. Das Erzeugen einer Methode zur Reaktion auf die Betätigung des Schalters ist sehr einfach. Dazu klicken Sie doppelt auf den Schalter. Die Entwicklungsumgebung öffnet das Programmfenster des Formulars und erzeugt gleich eine passende Methode für die Auswertung der SchalterBetätigung (Abbildung 2.23).
Ereignisbehandlungsmethoden erzeugen
Abbildung 2.23: Das Programmfenster des Formulars mit einer Methode, die auf die Betätigung des Rechnen-Schalters reagiert
Den Aufbau dieser Methode erläutere ich jetzt nicht, weil dies schon einiges Grundwissen erfordert. In Kapitel 6 erfahren Sie mehr zu Methoden. Delphi/Kylix hat die Methode auf jeden Fall bereits mit der Betätigung des Schalters verknüpft. Immer wenn der Schalter betätigt wird, wird diese Methode aufgerufen. Sie müssen nun nur noch programmieren. Ereignisbehandlungsmethoden für die anderen Ereignisse eines Steuerelements oder des Formulars erzeugen Sie übrigens über den Objektinspektor, indem Sie im Register EREIGNISSE bzw. EVENTS auf das entsprechende Ereignis klicken.
80
Sandini Bib
Beim Programmieren in einem Formular beziehen Sie sich auf die Steuerelemente, die Sie auf dem Formular angelegt haben. Sie kennen ja deren Name (und der ist sogar aussagekräftig ). Um die Eingabe des Anwenders aus einer Textbox auszulesen, lesen Sie die Eigenschaft Text aus (die eben diese Eingabe verwaltet). In diese Eigenschaft können Sie auch Texte hineinschreiben um diese auszugeben. So können Sie in unserem Programm den Nettobetrag ausgeben. Um eine Eigenschaft eines Steuerelements im Programm anzusprechen, verwenden Sie die folgende Syntax:
-
Bezug auf Steuerelemente
Steuerelementname.Eigenschaftname
Die Eigenschaft Text der Brutto-Textbox erreichen Sie also z. B. so: txtBrutto.Text
Eigentlich könnte die Berechnung nun sehr einfach aussehen (und der Berechnung aus der Konsolenanwendung sehr ähnlich sein): 01 02 03 04
procedure TStartForm.btnRechnenClick(Sender: TObject); begin Netto.Text := Brutto.Text * (1-(Steuer.Text / (100 + Steuer.Text))); end;
Object Pascal, die Sprache von Delphi und Kylix, ist aber eine so genannte typsichere Sprache. Der Compiler lässt arithmetische Berechnungen nur mit Zahlen zu, nicht mit Texten. Eine Textbox speichert in ihrer Eigenschaft Text aber nun einen Text. Um mit den Texteingaben rechnen zu können, müssen Sie diese in eine Zahl konvertieren. Dazu können Sie Funktionen verwenden, die Ihnen Object Pascal zur Verfügung stellt. Die Funktion StrToFloat konvertiert beispielsweise eine Zeichenkette in eine Zahl mit Dezimalstellen. Zur Speicherung der konvertierten Zahlen können Sie wieder Variablen einsetzen. Innerhalb einer Methode müssen Sie Variablen über dem beginSchlüsselwort deklarieren. Ich deklariere im Beispiel (wie bei der Konsolenanwendung) zwei Variablen für die Eingabe und eine für den errechneten Nettobetrag: 01 02 03 04 05
Eingaben konvertieren
procedure TStartForm.btnRechnenClick(Sender: TObject); var brutto: double; var steuer: double; var netto: double; begin
81
Sandini Bib
Nun können Sie die Eingaben einlesen, konvertieren und den Variablen zuweisen: 06 brutto := StrToFloat(txtBrutto.Text); 07 steuer := StrToFloat(txtSteuer.Text);
Die Berechnung sieht dann genauso aus wie bei der Konsolenanwendung:
Rechnen
08 netto := brutto * (1 - (steuer / (100 + steuer)));
Nun müssen Sie das Ergebnis noch ausgeben. Die in der Variable netto gespeicherte Zahl können Sie nicht direkt der Eigenschaft Text der Textbox txtNetto zuweisen. Es handelt sich eben um eine Zahl, die vom Compiler nicht implizit in einen Text umgewandelt wird. Das ist aber nicht besonders schlimm. Da die Ausgabe sowieso formatiert werden soll, können Sie einfach die Funktion FormatFloat einsetzen, die Sie bereits von der Konsolen-Variante des Programms her kennen:
Ausgeben
09 txtNetto.Text := FormatFloat('0.00', netto); 10 end;
Netterweise gibt FormatFloat eine Zeichenkette zurück, die direkt in die Text-Eigenschaft der Textbox geschrieben werden kann. In der Konsolenanwendung wurden die notwendigen Konvertierungen automatisch von den Prozeduren readln, write und writeln vorgenommen. Deshalb mussten Sie dort nicht selbst konvertieren. Das komplette Programm zeigt Abbildung 2.24.
Abbildung 2.24: Die Methode zur Berechnung des Nettowerts in Kylix
82
Sandini Bib
2.4.6 Umgehen mit ungültigen Eingaben Genau wie die Konsolen-Variante dieses Programms reagiert auch die neue Anwendung allergisch auf ungültige Eingaben. Geben Sie beispielsweise einen Text ein, der nicht in eine Zahl konvertiert werden kann, erzeugt das Programm eine Ausnahme (Abbildung 2.25).
Abbildung 2.25: Ausnahme aufgrund einer ungültigen Eingabe in einem Kylix-Programm
Anders als bei der Konsolenanwendung wird das Programm aber nicht beendet. Der Debugger hält das Programm zwar wieder an, wenn Sie dort aber einfach (F9) betätigen, können Sie noch einmal eingeben und die Berechnung erneut ausführen. Normalerweise sollten Sie solche Ausnahmen abfangen, aber diese Technik erläutere ich erst in Kapitel 4. Wird die Anwendung übrigens direkt ausgeführt (über den Aufruf der erzeugten ausführbaren Datei), wird die Ausnahme zwar auch gemeldet. Das Programm springt dann aber nicht in den Debugger. Und das reicht für die erste Anwendung mit grafischer Oberfläche vollkommen aus.
2.4.7 Das Programm für den Beenden-Schalter Um das Programm nun abzuschließen, müssen Sie noch die Funktionalität des BEENDEN-Schalters programmieren. Klicken Sie dazu wieder doppelt auf den Schalter. Die erzeugte Methode programmieren Sie folgendermaßen: 11 12 13 14
procedure TStartForm.btnBeendenClick(Sender: TObject); begin Close(); end;
Sie rufen damit eine vordefinierte Methode des Formulars auf, die das Fenster schließt und die Anwendung damit beendet.
83
Sandini Bib
2.5
Grundlagen zum Umgang mit der Delphi/ Kylix-Entwicklungsumgebung
Die Borland-Entwicklungsumgebung ist sehr mächtig und kann im Rahmen dieses Buchs nicht erschöpfend behandelt werden. Damit Sie mit Ihren Projekten einigermaßen gut umgehen können, zeige ich aber wenigstens die wichtigsten von den Dingen, die Sie noch nicht kennen. Ein wichtiges Tool der Entwicklungsumgebung ist die Projektverwaltung, die Sie über das VIEW- bzw. ANSICHT-Menü oder über (Strg) (Alt) (F11) öffnen können. Über die Projektverwaltung erreichen Sie alle Dateien Ihres Projekts. Klicken Sie einfach doppelt auf den jeweiligen Eintrag, um die entsprechende Datei zu öffnen.
Die Projektverwaltung
Abbildung 2.26: Die Projektverwaltung von Delphi
(Strg) (Alt) + Funktionstaste schaltet unter Linux zu einer der Konsolen des Betriebssystems um. Deshalb funktioniert keine der Tastenkombinationen mit (Strg) (Alt) + Funktionstaste innerhalb von Kylix. Linux selbst startet mit sechs vordefinierten Konsolen, Desktops wie KDE und GNOME laufen auf der siebten. Die elfte, die Sie mit (Strg) (Alt) (F11) erreichen würden, ist per Voreinstellung nicht vorhanden, weswegen Linux nach der Betätigung dieser Tasten einen schwarzen Bildschirm anzeigt. Über (Strg) (Alt) (F6) kommen Sie wieder zu Ihrem Desktop zurück. Eine schnelle Übersicht über Ihre Formulare erhalten Sie über das Formulare-Fenster, das Sie mit (ª) (F12) öffnen können. Bei der Eingabe in Programm-Fenster können Sie ein nettes Feature von Delphi/Kylix nutzen: Wenn Sie nur den Anfang eines Namens eingeben und dann (Strg) (Leertaste) betätigen, öffnet die Entwicklungsumgebung ein Fenster mit einer Liste aller Möglichkeiten, die Sie an dieser Stelle verwenden können. Geben Sie z. B. nur „txt“ ein und betätigen (Strg) (Leertaste), finden Sie alle Textboxen in der Liste (womit die Namenskonvention schon einen großen Vorteil zeigt).
Die automatische Syntaxhilfe
84
Sandini Bib
Abbildung 2.27: Die Eingabehilfe von Delphi
Sie können nun einen Eintrag in der Liste auswählen (z. B. über die Cursortasten). Dann schreiben Sie einfach den Text weiter, der dem ausgewählten Namen folgen soll, im Beispiel also einen Punkt. Delphi und Kylix ergänzen Ihre Eingabe automatisch durch den ausgewählten Eintrag. Wenn Sie den Punkt schreiben, öffnet sich die Liste gleich wieder, dieses Mal automatisch. Dann sehen Sie alle Eigenschaften des Steuerelements und können diese wieder auswählen. Sie können auch die Anfangsbuchstaben des Namens eingeben, um den passenden Eintrag zu aktivieren. Schreiben Sie dann wieder einfach den Text, der dem ausgewählten Eintrag folgen soll.
2.6
Hello World in Java
Für Java verzichte ich zunächst auf eine Entwicklungsumgebung, damit Sie lernen, Java-Programme in einem einfachen Editor zu schreiben, „von Hand“ zu kompilieren und auszuführen.
2.6.1 Entwicklung einer Konsolenanwendung Das erste Java-Programm ist wieder eine einfache Konsolenanwendung. Dieses Programm schreiben Sie ganz ohne eine Entwicklungsumgebung und kompilieren es über den Java-Compiler. Erzeugen Sie dazu zunächst einen Ordner für Java-Projekte und einen Ordner für das neue Projekt in Ihrem Projekte-Ordner. Nennen Sie den Projektordner vielleicht hello. Unter Windows sollte dies nun der Ordner C:\Projekte\Java\Hello sein, unter Linux der Ordner /projekte/java/hello.
85
Sandini Bib
Unter Windows können Sie zur Erstellung der Java-Datei den StandardTexteditor verwenden. Ich verwende allerdings dazu den freien Programmeditor JCreator LE, den Sie auf der Buch-CD oder im Internet an der Adresse www.jcreator.com finden. Dieser hervorragende Editor bietet einige wichtige Hilfsmittel wie ein Syntax-Highlighting, bei dem die einzelnen Bestandteile eines Programms in unterschiedlichen Farben dargestellt werden, und eine Möglichkeit, ein Programm direkt über den Editor zu kompilieren und zu starten. Leider läuft dieser Editor nur unter Windows. Um den Dateinamen beim Speichern korrekt angeben zu können, sollten Sie allerdings darauf achten, dass Windows in Datei-Dialogen alle Dateiendungen anzeigt.6 Standardmäßig zeigt Windows die Endungen „registrierter“ Dateitypen nicht an. Zu den registrierten Dateien gehören beispielsweise Word-, Excel- und Bilddateien, aber eben auch Textdateien. Abgesehen davon, dass das Verbergen der Dateiendung registrierter Dateien in meinen Augen absolut keinen Sinn macht und oft zu Verwirrungen führt6: Im Windows-Editor führt diese Einstellung dazu, dass dieser beim Speichern einer Datei die Endung .txt immer automatisch an den Dateinamen anhängt. Wenn Sie eine Datei beispielsweise hello.java nennen, nennt der Editor diese hello.java.txt. Wenn Sie diese Datei dann kompilieren wollen, wundern Sie sich, dass der Compiler die Datei nicht findet. Sie sollten diese Einstellung auf jeden Fall korrigieren, falls Ihr System Dateiendungen registrierter Dateien nicht anzeigt. Wählen Sie dazu im Explorer den Befehl ORDNEROPTIONEN im EXTRAS-Menü. Klicken Sie auf das Register ANSICHT und schalten Sie die Option DATEINAMENERWEITERUNG BEI BEKANNTEN DATEITYPEN AUSBLENDEN aus. Unter Linux verwenden Sie einen der vielen Texteditoren (vi, emacs, kedit, kwrite etc.) zur Erzeugung der Java-Datei. Da ich eher aus der Windows-Welt stamme, verwende ich kwrite, den erweiterten Texteditor der KDE. Dieser Texteditor stellt Java-Programme mit einem sehr hilfreichen Syntax-Highlighting dar. Speichern Sie die neue Datei im neuen Ordner unter dem Namen hello.java. Achten Sie dabei auf die Kleinschreibung des Dateinamens. Java unterscheidet Groß- und Kleinschreibung. 6.
86
Bei Access-Datenbanken, die die Endung .mdb tragen, existiert beispielsweise oft eine gleichnamige Datei mit der Endung .ldb. Wenn Sie die Endung nicht sehen, wissen Sie nicht, welche Datei die Datenbankdatei ist.
Sandini Bib
In dieser Textdatei schreiben Sie nun das erste Programm: 01 public class hello 02 { 03 public static void main(String[] args) 04 { 05 System.out.println("Hello World"); 06 } 07 }
Achten Sie dabei auf die Schreibweise der einzelnen Bezeichner. Wie bereits gesagt, unterscheidet Java Groß- und Kleinschreibung. Wenn Sie beispielsweise versehentlich „system“ statt „System“ schreiben, meldet der Compiler beim Kompilieren einen Fehler (siehe Seite 90). Damit Sie den Quelltext einigermaßen verstehen, folgen hier einige Erläuterungen: Die erste Zeile leitet eine Klasse ein. Sie wissen jetzt wahrscheinlich nicht, was eine Klasse ist. Das ist auch gut so, denn wüssten Sie dies, müssten Sie dieses Buch nicht lesen. Java ist komplett objektorientiert. Klassen sind die Basis der objektorientierten Programmierung. Eine Klasse enthält – vereinfacht gesagt – Programmcode, der thematisch zusammengehört. Auf die objektorientierte Programmierung gehe ich noch später, in Kapitel 6, ein. Zunächst müssen diese Erläuterungen ausreichen. Eine Java-Anwendung besteht immer mindestens aus einer Klasse, mit der die Anwendung startet. Diese Klasse muss übrigens denselben Namen tragen wie die Datei, in der sie gespeichert ist (allerdings ohne Endung), wobei Sie auch wieder auf die Groß-/Kleinschreibung achten müssen.
Die Start-Klasse
In Zeile 2 wird über die geschweifte Klammer ein Block eröffnet, der in der letzten Zeile wieder abgeschlossen wird. Alle Anweisungen innerhalb dieser Klammern gehören zu diesem Block. Der erste Block im Hello-World-Programm steht für alles, was die Klasse beinhaltet. In der dritten Zeile wird eine Methode eingeleitet. Eine Methode gehört zu einer Klasse und enthält Programm-Anweisungen. Eine Klasse kann mehrere Methoden besitzen, die dann bei der weiterführenden Programmierung zur Strukturierung eines Programms und zur Wiederverwendung von Programmteilen eingesetzt werden. Diese Methoden erläutere ich in Kapitel 5 und 6. Genau wie bei der Klasse werden auch die Anweisungen der Methode in geschweifte Klammern eingeschlossen.
Methoden
87
Sandini Bib
Die Methode der Klasse des Beispiels besitzt eine besondere Bedeutung. Java-Programme starten immer mit einer Methode, die main heißt und die genauso aufgebaut ist wie die Methode im Beispiel. Der Inhalt der Klammern (String[] args) bezieht sich dabei auf Argumente, die Sie beim Aufrufen des Programms später übergeben können. Diese Argumente vernachlässige ich allerdings in diesem Kapitel. Innerhalb der Methode gibt das Programm den Text "Hello World" an der Konsole aus. Es nutzt dazu auch wieder eine Methode, nämlich die Methode println der Klasse out. Diese Klasse, die ein Bestandteil der JavaKlassenbibliothek ist, dient der Ausgabe von einfachen Daten an der Konsole und besitzt entsprechende Methoden. Die println-Methode gibt beispielsweise einen Text aus, der dieser Methode in den runden Klammern übergeben werden muss. Das „ln“ am Ende des Namens der println-Methode steht übrigens für „Line“: println gibt (im Gegensatz zu print) hinter dem Text noch einen Zeilenumbruch aus. Weil die outKlasse in einer Java-Bibliothek gespeichert ist, müssen Sie diese bei der Verwendung der Klasse noch zusätzlich angeben. Deswegen steht noch System vor der Klasse. Wenn Sie die Datei nun speichern, können Sie diese kompilieren.
2.6.2 Kompilieren des Programms Kompilieren unter Windows Unter Windows öffnen Sie zum Kompilieren idealerweise die Konsole und wechseln in den Ordner, in dem die Java-Datei gespeichert ist (falls Sie nicht mehr wissen, wie das geht: Geben Sie den Laufwerknamen ein und betätigen Sie (¢), falls die Klasse auf einem anderen Laufwerk gespeichert ist. Wechseln Sie gegebenenfalls mit cd \ in den Stammordner und dann mit cd Ordnername in den Ordner, in dem Sie die Datei gespeichert haben): cd \projekte\java\hello
88
Sandini Bib
Sie können den Windows-Explorer so einrichten, dass Sie jederzeit vom Explorer aus die Konsole für einen bestimmten Ordner öffnen können. Wählen Sie dazu den Befehl ORDNEROPTIONEN im EXTRASMenü des Explorers. Wechseln Sie zum Register DATEITYPEN und suchen Sie den Eintrag mit dem Dateityp Dateiordner (ab Windows Version NT 4 finden Sie in der linken Spalte als Erweiterung zu diesem Eintrag den Text N. ZUTR). Markieren Sie diesen Eintrag und betätigen Sie den Schalter ERWEITERT oder BEARBEITEN (je nach WindowsVersion). Im erscheinenden Dialog zur Bearbeitung von Dateitypen betätigen Sie den Schalter NEU. Geben Sie im Feld VORGANG dann eine Bezeichnung ein, z. B. „Konsole“. Im Feld ANWENDUNG FÜR DIESEN VORGANG geben Sie nun unter Windows NT, 2000 und XP Folgendes ein: C:\Winnt\System32\cmd.exe /K cd %1. Unter Windows 95, 98 und Me müsste die Einstellung folgendermaßen aussehen: C:\Windows\command.com /K cd %1. Den Pfad zu Ihrem Kommandointerpreter (cmd.exe, command.exe bzw. command.com) müssen Sie natürlich gegebenenfalls für Ihr System anpassen. Suchen Sie diese Datei dann idealerweise über den Windows-Suchen-Dialog. So wissen Sie auch, wie Ihr Kommandointerpreter wirklich heißt. Nachdem Sie diese Einstellungen vorgenommen haben, können Sie im Explorer einen beliebigen Ordner mit der rechten Maustaste anklicken und im Kontextmenü den neuen Befehl ausführen. Windows öffnet dann die Konsole und springt direkt in diesen Ordner. Zum Kompilieren rufen Sie nun den Java-Compiler unter Angabe des Dateinamens auf: javac hello.java
Wenn der Compiler keine Fehler meldet, ist Ihr erstes Programm fertig. Die Auswertung eventueller Fehler beschreibe ich nach dem Kompilieren unter Linux. Kompilieren unter Linux Unter Linux kompilieren Sie die .java-Datei in einer Shell. Um die Standard-Shell direkt in dem Ordner zu öffnen, das gerade im Konqueror der KDE angezeigt wird, können Sie dort einfach (Strg) (T) betätigen. Wechseln Sie in der Shell gegebenenfalls mit cd Ordnername in den Ordner, das die Datei speichert. Dort rufen Sie dann den Java-Compiler unter Übergabe des Dateinamens auf. Bei der Angabe des Dateinamens
89
Sandini Bib
müssen Sie den aktuellen Pfad in den Dateinamen aufnehmen, was durch die Angabe eines Punktes (der in Linux – und Windows – für den aktuellen Pfad steht) geschieht: javac ./hello.java
Auswertung eventueller Fehlermeldungen Natürlich sind Ihre Programme – genau wie meine – nicht immer (oder bei komplexeren Programmen eigentlich nie) fehlerfrei. Enthält das Programm Fehler, kompiliert der Compiler das Programm nicht und meldet die Fehler. Um dies zu demonstrieren, verändere ich den Programmcode des Beispiels ein wenig: 01 public class hello 02 { 03 public static void main(String[] args) 04 { 05 system.out.println("Hello World"); 06 } 07 }
Beim Kompilieren meldet javac nun einen Fehler (Abbildung 2.28).
Abbildung 2.28: javac-Fehlermeldung unter Linux
Diese Fehlermeldung ist recht eindeutig. Im Beispiel werden Sie die fehlerhafte Kleinschreibung des Namens System sehr schnell finden. Andere Fehlermeldungen sind oft leider weniger sprechend. Dann müssen Sie beim Anpassen des Quellcodes mehr oder weniger probieren. Hilfreich ist auf jeden Fall, dass der Compiler die Fehlerzeile (im Beispiel ist das Zeile 5) und die Position des Fehlers anzeigt.
90
Sandini Bib
2.6.3 Starten des Programms Wenn Sie das Programm erfolgreich kompiliert haben, hat der Compiler eine Datei mit dem Namen java.class erzeugt. Das ist die kompilierte Version Ihres Programms. Da Java-Programme nicht direkt vom Betriebssystem ausgeführt werden können (wozu Sie in Kapitel 3 mehr erfahren), starten Sie dieses Programm über den Java-Interpeter. Geben Sie dazu einfach java hello ein. Der Java-Interpreter sucht in diesem Fall eine Datei mit dem Namen hello.class und führt diese aus. Wenn Sie ein Java-Programm unter Windows oder Linux ausführen und bei der Angabe der auszuführenden Datei einen Pfad mit angeben (z. B. javac ./hallo oder javac /projekte/java/hello/hello), startet das Programm bei Ihnen eventuell mit der schwer nachvollziehbaren Ausnahme java.lang.NoClassDefFoundError. Diese Ausnahme, die vielfältige Ursachen haben kann, wird erzeugt, wenn der Java-Interpreter eine benötigte Klasse nicht findet. Es scheint so zu sein, dass der Java-Interpreter die Klassen der Java-Bibliothek nicht findet, wenn die Dateiangabe einen Pfad enthält. Starten Sie java also immer aus dem Ordner, in dem die .class-Datei gespeichert ist und geben Sie keinen Pfad an. Wenn keine Fehler auftreten, gibt das Programm „Hello World“ an der Konsole aus (Abbildung 2.29)
Abbildung 2.29: Das Java-Programm wurde unter Windows kompiliert und aufgerufen.
Auch nach dem Starten des Programms kann es wie in allen Programmen zu Fehlern kommen. Ein häufiger Fehler ist z. B., dass das Java-Programm die vom Java-Interpreter erwartete main-Methode nicht besitzt oder diese Methode falsch programmiert wurde. Im folgenden Programm fehlen beispielsweise die Argumente dieser Methode:
Fehler im Programmablauf
91
Sandini Bib
01 public class hello 02 { 03 public static void main() 04 { 05 System.out.println("Hello World"); 06 } 07 }
Kompilieren können Sie das Programm ohne Probleme. Beim Aufruf tritt aber die Ausnahme NoSuchMethodError auf (Abbildung 2.30).
Abbildung 2.30: Beim Aufruf des Programms meldet Java einen Fehler, weil die Methode main falsch programmiert wurde.
Ausnahmen sind in einfachen Java-Programmen recht selten. Die einzige Ausnahme, die zurzeit möglich ist, ist die, dass die main-Methode nicht korrekt programmiert wurde. Im nächsten Abschnitt erfahren Sie, wie Sie Anwendungen mit Formularen in Java programmieren. Dann können natürlich auch Ausnahmen auftreten, wenn Sie numerische Eingaben erwarten, diese in Zahlen umkonvertieren und der Anwender ungültige Daten eingibt (wie bei Delphi und Kylix).
2.7
Hello World in Java mit Sun ONE Studio 4
Damit Sie lernen, mit Sun ONE Studio 4 umzugehen, entwerfen Sie nun das Hello-World-Programm mit Hilfe dieser Entwicklungsumgebung.
92
Sandini Bib
Sun ONE Studio 4 ist sehr komplex. Sie können bei der Entwicklung eines Java-Programms mit Sun ONE Studio 4 recht viele verschiedene Wege gehen und eine Vielzahl an Features nutzen. Ich kann den Umgang mit dieser Entwicklungsumgebung im Rahmen dieses Buchs nur ansatzweise beschreiben und zeige deshalb einen Weg zur Erstellung eines Programms, den ich für den Anfang für möglichst einfach und ideal halte. Leider ist dieser Weg trotzdem ein wenig komplizierter, als ich es gerne hätte. Ich wollte aber nicht auf die grundsätzliche Beschreibung von Sun ONE Studio 4 verzichten, weil Sie mit dieser Entwicklungsumgebung sehr wichtige Hilfsmittel beim Schreiben von Programmen und vor allen Dingen einen Debugger zum Testen Ihrer Programme (den ich in Kapitel 4 beschreibe) besitzen. Wenn Sie intensiver mit Sun ONE Studio 4 programmieren wollen, müssen Sie sich wohl noch ein wenig mehr mit den Konzepten dieser Entwicklungsumgebung beschäftigen. Die Tastenkombinationen, die ich in diesem Abschnitt beschreibe, finden Sie in einer übersichtlichen Form auch im Anhang. Sun ONE Studio 4 benötigt scheinbar sehr viele Systemressourcen. Wenn Sie gleichzeitig mit dieser Entwicklungsumgebung andere Programme geöffnet haben, kann es schon einmal passieren, dass diese oder das gesamte System abstürzen (was bei mir allerdings nur unter Windows der Fall war). Speichern Sie also Ihre aktuelle Arbeit immer ab, wenn Sie mit Sun ONE Studio 4 arbeiten. Die Editoren von Sun ONE Studio 4 sind zudem leicht „buggy“ (fehlerbehaftet). Es kann schon einmal passieren, dass der Quellcode- oder der Formulareditor nicht auf das Klicken mit der Maus reagieren, sodass Sie nicht die Möglichkeit haben, Ihr Programm weiter zu bearbeiten. In einigen Fällen hilft dann ein Schließen und erneutes Öffnen der entsprechenden Datei. Häufig müssen Sie Sun ONE Studio 4 aber auch komplett neu starten. Unter Linux funktionieren zudem (wie bei Kylix) einige der Tastenkombinationen, die ich in im weiteren Verlauf beschreibe, nicht, wenn Sie Sun ONE Studio 4 in der KDE starten. Das liegt daran, dass die KDE einige Tastenkombinationen für sich in Anspruch nimmt. Beachten Sie dazu meine Hinweise auf Seite 50.
93
Sandini Bib
2.7.1 Erzeugung eines neuen Projekts Nachdem Sie Sun ONE Studio 4 gestartet und den eventuellen Willkommensdialog geschlossen haben, sieht die Entwicklungsumgebung in etwa aus wie in Abbildung 2.31.
Abbildung 2.31: Sun ONE Studio 4
Projekte In Sun ONE Studio 4 arbeiten Sie wie bei Delphi und Kylix mit einem Projekt. Das Projekt fasst (zumindest) alle Dateien zusammen, die zu einem Programm gehören. Beim ersten Start hat die Entwicklungsumgebung bereits ein Projekt geöffnet, das als Default bezeichnet wird. Dieses Projekt ist auch gleich ein Beweis dafür, dass Sun ONE Studio 4 etwas anders mit dem Begriff Projekt umgeht als andere Entwicklungsumgebungen (wie z. B. Delphi und Kylix). Das Default-Projekt besteht nämlich per Voreinstellung aus einem Ordner sampledir, der gleich mehrere Unterordner mit unterschiedlichen Programmen enthält. Sie können in einem Sun ONE Studio 4-Projekt also beliebig viele Programme verwalten.
Projekte verwalten die Dateien eines Programms
Wenn Sie allerdings nur ein Programm in einem Projekt verwalten, ermöglicht dies projektspezifische Compiler-Einstellungen und erleichtert das Kompilieren und Starten des Programms. Außerdem ist das Einbinden von externen (nicht zum Projekt gehörenden) Bibliotheken, das
94
Sandini Bib
ich in Kapitel 5 beschreibe, nur über ein Einzelprogramm-Projekt möglich. Wenn Sie mehrere Programme in einem Projekt verwalten, müssen Sie zum Kompilieren und Starten immer erst den Ordner auswählen, in dem das Programm gespeichert ist, und Sie können keine externen Bibliotheken einbinden. Erzeugen eines neuen Projekts Sie sollten für Ihre Java-Programme jeweils eigene Projekte anlegen, auch wenn diese zurzeit noch recht einfach strukturiert sind. Wenn Sie sich an die Arbeit mit Projekten gewöhnen, haben Sie weniger Probleme bei der Programmierung.
Projekt erzeugen
Erzeugen Sie für das erste mit Sun ONE Studio 4 erstellte Programm also ein neues Projekt. Dazu verwenden Sie den Projektmanager, den Sie über das PROJECT-Menü öffnen können. Legen Sie über den NEW-Schalter ein neues Projekt an, das Sie vielleicht HelloOne nennen. Über den Projektmanager können Sie Ihre Projekte später wieder öffnen. Sun ONE Studio 4 startet aber auch immer mit dem zuletzt aktuellen Projekt, sodass Sie beim nächsten Start direkt an diesem Projekt weiterarbeiten können. Sun ONE Studio 4 verwaltet die Einstellungen aller Projekte in einem Ordner, der so benannt ist wie das jeweilige Projekt und der im Ordner system\projects im Benutzerordner angelegt wird. Unter Windows haben Sie den Benutzerordner beim ersten Start der Entwicklungsumgebung angeben müssen. Wenn Sie meinem Vorschlag :_Artikel „Installation“ gefolgt sind, ist das der Ordner C:\Dokumente und Einstellungen\
\Eigene Dateien\OneStudio4 (sofern C: Ihr Systemlaufwerk ist). Unter Linux verwendet Sun ONE Studio 4 automatisch den Ordner ffjuser40ce in Ihrem Home-Ordner. Wenn Sie nichts weiter machen und in diesem Projekt neue Klassen anlegen, werden diese in dem Ordner system\projects\\Files im Benutzerordner gespeichert. Damit haben Sie immer alle Dateien, die zu einem Projekt gehören, und die zugehörigen Einstellungen gemeinsam in einem Ordner gespeichert. Eigentlich ist das auch eine Vorgehensweise, die Sie bevorzugen sollten, wenn Sie konsequent mit Sun ONE Studio 4 arbeiten. Dummerweise passt das nicht in unser Konzept, alle Projekte in einem speziellen Ordner zu speichern, auf das alle Benutzer Zugriff haben. Unter Windows hätten Sie zwar bei der Installation einen solchen allgemeinen Ordner für die Benutzerdaten angeben können (was Sie übrigens auch nachträglich über die Windows-Registry ändern können). Schauen Sie sich aber einfach einmal die vielen Ordner an, die Sun ONE Studio 4 im Benutzerordner angelegt hat, dann wis-
95
Sandini Bib
sen Sie, warum ich dieses Vorgehen nicht bevorzuge. Unter Linux können Sie den Benutzerordner erst gar nicht anpassen. Ich gehe also einen anderen Weg. Zur Verwaltung der Dateien eines Projekts können Sie auch einen spezifischen Ordner mit dem Projekt verbinden („mounten“). Wählen Sie dazu den Befehl MOUNT FILESYSTEM im FILE-Menü. Klicken Sie dann auf den Eintrag LOCAL DIRECTORY im erscheinenden NEW WIZARD-Dialog. Nachdem Sie den NEXT-Schalter betätigt haben, können Sie den Ordner auswählen, in dem Sie die Dateien des Projekts verwalten wollen.
Ordner mounten
Wenn Sie meinem Vorschlag bei der Entwicklung der ersten JavaKonsolenanwendung gefolgt sind, besitzen Sie bereits einen Ordner \Projekte\Java (Abbildung 2.32).
Abbildung 2.32: Auswahl eines Ordners zur Verbindung mit einem Projekt
Sie können über den New Wizard den für das Projekt benötigten Unterordner anlegen, indem Sie auf das Ordnersymbol (das dritte von rechts) klicken. Klicken Sie danach einmal auf den neuen Ordner und geben Sie den Namen ein. Für das Beispiel eignet sich der Name HelloOne, da Sie bereits ein Projekt Hello besitzen. Wählen Sie den Ordner nun aus und betätigen Sie den FINISH-Schalter, um diesen mit dem Projekt zu verbinden. Der neu verbundene Ordner wird im FILESYSTEMS-Register des Explorer-Fensters angezeigt.
96
Sandini Bib
Beachten Sie, dass Sun ONE Studio 4-Projekte nun in zwei verschiedenen Ordnern verwaltet werden. Ein Ordner speichert die Java-Dateien, im Sun ONE Studio 4-Benutzerordner werden die Projekteinstellungen verwaltet. Für Ihre ersten einfachen Projekte ist das aber nicht weiter schlimm. Sie sollten auf jeden Fall die Java-Dateien sichern, wenn Sie ein Backup Ihrer Arbeit machen. Da Sie zurzeit noch keine wesentlichen Änderungen an den Projekteinstellungen vornehmen, können Sie die Projekte im Notfall recht einfach wiederherstellen, indem Sie neue Projekte erzeugen und mit den bereits vorhandenen Projektordnern verbinden. Durch diese Möglichkeit, Ordner mit einem Projekt zu verbinden, können Sie übrigens auch mehrere Programme in einem Projekt zusammenfassen, so wie es im Default-Projekt für die Beispieldateien der Fall ist.
2.7.2 Anlegen einer Start-Klasse für das Projekt In dem neuen Projekt legen Sie nun eine Start-Klasse an. Beachten Sie, dass es unter Sun ONE Studio 4 (leider) eine Vielzahl an Wegen gibt, einem Projekt eine Datei hinzuzufügen. Einige davon führen nicht zum wahrscheinlich erwarteten Ergebnis. Ich kann hier nicht alle Wege beschreiben. Gehen Sie also bitte genauso vor, wie ich es hier zeige. Aktivieren Sie zunächst das Projekt-Register im Explorer und wählen Sie das Projekt aus (Abbildung 2.33).
Abbildung 2.33: Auswahl des Projekts im Sun ONE Studio 4-Explorer
Nun können Sie zwei Wege gehen, um dem Projekt eine neue Datei hinzuzufügen. Über den Befehl NEW im File-Menü fügen Sie die Datei in
97
Sandini Bib
den Sun ONE Studio 4-Projektordner (im Benutzerordner) hinzu (nicht in den mit dem Projekt verbundenen Ordner!). Mit diesem Befehl können Sie später das Projekt auswählen, dem die Datei hinzugefügt werden soll (Abbildung 2.35). Das aktuelle Projekt ist voreingestellt, Sie können aber auch eines der anderen bereits vorhandenen Projekte auswählen oder einen der vielen anderen Speicherorte auswählen, die der Dialog Ihnen anbietet. Wenn Sie hingegen mit der rechten Maustaste im Explorer auf das Projekt klicken und im Kontextmenü den Befehl ADD NEW wählen, speichern Sie die Datei ausschließlich im aktuellen Projekt und erhalten lediglich die Möglichkeit den Dateiordner auszuwählen (Abbildung 2.36). Zuvor wählen Sie aber unter den vordefinierten Vorlagen (Templates) eine für die neue Datei aus (Abbildung 2.34). Um dem Projekt eine StartKlasse für eine Konsolenanwendung hinzuzufügen, wählen Sie die Vorlage MAIN im CLASSES-Ordner.
Abbildung 2.34: Auswahl einer Vorlage für eine Konsolenanwendungs-Start-Klasse
Im nächsten Schritt geben Sie den Namen der neuen Klasse ein und wählen den Speicherort (das so genannte Paket) für diese Klasse aus. Wenn Sie zum Hinzufügen den Befehl NEW im FILE-Menü gewählt haben, können Sie aus der Vielzahl an grundsätzlichen Möglichkeiten wählen, wobei das aktuelle Projekt bereits voreingestellt ist (Abbildung 2.35).
Auswahl des Speicherorts in Variante 1
98
Sandini Bib
Abbildung 2.35: Auswahl des Projekts zur Speicherung der Datei
Haben Sie hingegen den Befehl ADD NEW im Kontextmenü des Projekteintrags gewählt, können Sie einen der verbundenen Dateiordner auswählen (Abbildung 2.36).
Auswahl des Speicherorts in Variante 2
Abbildung 2.36: Auswahl eines Dateiordners zur Speicherung der Datei unter Windows
Die erste Variante zum Hinzufügen (Abbildung 2.35) bietet im unteren Bereich auch die Möglichkeit, einen verbundenen Dateiordner auszuwählen. Wenn Sie aber diese Möglichkeit benutzen, wird die neue Datei nicht dem Projekt zugeordnet. Dann können Sie das Programm in der Entwicklungsumgebung nicht über das Projekt starten. Verwenden Sie also die zweite Variante, wenn Sie die neue Datei dem verbundenen Dateiordner hinzufügen wollen.
99
Sandini Bib
An dieser Stelle entscheidet sich auch, ob Sie Ihre Dateien im Projektordner im Benutzerordner von Sun ONE Studio 4 verwalten (Variante 1) oder in einem eigenen Projektordner (Variante 2). Diese Entscheidung überlasse ich Ihnen. Falls Sie den Benutzerordner von Sun ONE Studio 4 verwenden, können Sie den verbundenen Dateiordner wieder aus dem Projekt entfernen („unmounten“). Pakete
Da Sie nun ab hier immer wieder mit dem Begriff Paket (Package) konfrontiert werden, wollen Sie vielleicht wissen, was ein Paket ist. Also: Pakete sind ein spezielles Java-Konzept, über das Klassen thematisch organisiert werden. Eine Java-Datei enthält dazu die package-Anweisung, wenn die enthaltenen Klassen einem speziellen Paket zugeordnet sein sollen: package Paketname;
Damit können Klassen beliebigen Paketen zugeordnet werden, auch solchen, die noch gar nicht existieren. Ein Paket ist lediglich eine logische Organisationsform. Es kann durchaus sein, dass die Klassen eines Pakets in unterschiedlichen Dateiordnern gespeichert sind. Durch das Paketkonzept erleichtert Java zum einen das Finden von Klassen in einer Bibliothek. Da Pakete normalerweise thematisch organisiert sind, müssen Sie lediglich in der Dokumentation in einem zu Ihrem Problem passenden Paket suchen, um die benötigten Klassen zu finden. In der Java-Bibliothek sind alle Klassen z. B. in Paketen organisiert, deren Name mit java. beginnt. Das Paket java.print enthält beispielsweise Klassen, die zum Drucken verwendet werden. Der andere Vorteil von Paketen ist, dass so auch unterschiedliche Klassen mit gleichen Namen verwendet werden können, sofern diese in verschiedenen Paketen verwaltet werden. Auf das Paketkonzept komme ich noch einmal in Kapitel 4 bei der Besprechung der Benutzung der Java-Bibliothek zurück. Sun ONE Studio 4 betrachtet nun jeden mit einem Projekt verbundenen Dateiordner als ein Paket, obwohl das eigentlich nicht ganz korrekt ist (in einem Dateiordner können auch mehrere Java-Dateien gespeichert sein, die unterschiedlichen Paketen zugeordnet sind). In späteren Projekten werden Sie neben dem Ordner, der die Projektdateien enthält, auch weitere Ordner mit einem Projekt verbinden. Diese Ordner enthalten dann eigene Bibliotheken mit vorgefertigten Klassen, die Sie in verschiedenen Projekten wiederverwenden. Wenn Sie dem Sun ONE Studio 4-Konzept folgen, speichern Sie alle JavaDateien, die zu einem Paket gehören, in einem separaten Ordner. In Kapitel 5 zeige ich, wie Sie damit umgehen.
100
Sandini Bib
Im Feld PACKAGE können Sie nun einen eigenen Paketnamen eingeben. Sie sollten hier aber die Voreinstellung übernehmen. Wenn Sie in der zweiten Variante (Speicherung der Datei in einem verbundenen Ordner) einen eigenen Paketnamen eingeben, erzeugt Sun ONE Studio 4 zur Speicherung der Datei einen entsprechenden Unterordner im ausgewählten Dateiordner. Geben Sie nun noch gegebenenfalls einen Namen für die neue Klasse ein. Wenn Sie den Eintrag <default name> übernehmen, wird die Klasse mit Main benannt, was auch vollkommen in Ordnung ist. Wenn Sie nun den NEXT-Schalter betätigen, können Sie verschiedene Einstellungen für die Klasse vornehmen, was aber an dieser Stelle zu weit führen würde. Betätigen Sie also einfach den FINISH-Schalter. Sun ONE Studio 4 erzeugt nun eine Java-Datei mit einer Main-Klasse und einer main-Methode, ähnlich der, die Sie bereits von Hand erzeugt haben: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/* * Main.java * * Created on 3. August 2002, 11:29 */ /** * * @author Administrator */ public class Main { /** Creates a new instance of Main */ public Main() { } /** * @param args the command line arguments */ public static void main(String[] args) { } }
Wenn Sie die Datei im Sun ONE Studio 4-Benutzerordner verwalten, enthält diese noch eine zusätzliche Anweisung: package Projects.Projektname.Files;
101
Sandini Bib
Damit wird diese Datei einem entsprechenden Paket zugewiesen. Lassen Sie diese Anweisung in der Klasse stehen. Ohne eine korrekte Paketzuordnung können Sie das Programm in Sun ONE Studio 4 nicht starten und später auch nicht debuggen. Wenn Sie die Datei in einem speziellen Ordner verwalten, entfällt diese Zuweisung. Die Klasse wird dann im globalen Paket verwaltet, was auch vollkommen in Ordnung ist. Wenn Sie die Datei in einem verbundenen Ordner verwalten, legt Sun ONE Studio 4 im Projekt lediglich einen Verweis auf die Datei an. Deshalb können Sie das Programm dann aber auch über das Projekt starten. Im FILESYSTEMS-Register des Explorers finden Sie die neue Datei, wenn Sie den verbundenen Ordner öffnen. Sie können die Dateien eines Projekts jederzeit über das Projekt-Register oder das FILESYSTEMS-Register des Explorers öffnen. Unbekannte und überflüssige Anweisungen
Die automatisch erzeugte Datei enthält einige Anweisungen, die Sie wahrscheinlich noch nicht kennen. Alle Anweisungen, die in /* und */ eingeschlossen sind, sind Kommentare. Kommentare werden üblicherweise dazu verwendet, ein Programm zu dokumentieren, und werden vom Compiler nicht berücksichtigt. Sie können die wenig aussagekräftigen Kommentare der neuen Datei löschen, um mehr Übersicht zu erhalten. Das Schlüsselwort public vor der Klassendeklaration in Zeile 11 bewirkt, dass diese Klasse öffentlich ist. Klassen, die in Paketen organisiert sind, müssen öffentlich sein, damit sie von außerhalb verwendet werden können. Die Anweisungen in Zeile 14 und 15 definieren einen so genannten Konstruktor. Ein solcher wird lediglich bei „echten“ Klassen benötigt und kann daher gelöscht werden. Konstruktoren beschreibe ich in Kapitel 6. Auf das Wesentliche reduziert, sieht die Klasse dann so aus: 01 public class Main 02 { 03 public static void main(String[] args) 04 { 05 } 06 }
102
Sandini Bib
2.7.3 Programmieren und Starten des Programms Nun können Sie programmieren. Sie werden feststellen, dass Sun ONE Studio 4 einige hilfreiche Features besitzt. Neben einer farblichen Unterscheidung verschiedener Schlüsselwörter übernimmt der Editor beispielsweise das Einrücken von Programmcode fast automatisch für Sie. Ein anderes sehr hilfreiches Feature ist, dass Sun ONE Studio 4 Ihnen mehr oder weniger automatisch alle Möglichkeiten des aktuellen Quellcode-Kontextes anzeigt. Schreiben Sie beispielsweise nur „Sys“ und betätigen Sie danach (Strg) (Leertaste). Sun ONE Studio 4 zeigt eine Liste mit allen Elementen an, die mit „Sys“ beginnen und im aktuellen Kontext möglich sind.
Nützliche Features
Abbildung 2.37: Die Elementliste von Sun ONE Studio 4
Wählen Sie das benötigte Element aus der Liste aus (für unser Beispiel ist das der Eintrag SYSTEM). Anders als in Delphi und Kylix können Sie zum einfügen eines Elements in den Quellcode leider nicht einfach den Text weiter schreiben, der dem ausgewählten Element folgen soll. Sie müssen zur Auswahl eines Elements (¢) betätigen. Obwohl das im Vergleich zu Delphi und Kylix etwas umständlich ist, ersparen Sie sich damit einige Schreibarbeit und finden alle im Moment möglichen ProgrammElemente recht schnell. Wenn Sie den Punkt nach einem Klassen- oder Objektnamen schreiben, öffnet sich die Liste auch automatisch. Erzeugen Sie nun das einfache Programm, indem Sie über die printlnMethode „Hello World“ ausgeben. 01 public class Main 02 { 03 public static void main(String[] args) 04 { 05 System.out.println("Hello World"); 06 } 07 }
103
Sandini Bib
Kompilieren und Starten Sie können das Projekt nun über (Strg) (ª) (F9) kompilieren. Sun ONE Studio 4 kompiliert dann alle Dateien, die zum Projekt gehören, wobei allerdings immer nur die Dateien mit einbezogen werden, die seit dem letzten Kompilieren geändert wurden. Einzelne Dateien können Sie über (F9) kompilieren, indem Sie diese zuvor im Explorer markieren. Eventuelle Kompilierfehler zeigt Sun ONE Studio 4 im „Output Window“ an (Abbildung 2.38).
Abbildung 2.38: Sun ONE Studio 4 meldet Fehler beim Kompilieren, weil aus Versehen die out-Klasse großgeschrieben wurde.
Sehr hilfreich ist, dass Sun ONE Studio 4 die Fehlerzeile im Quellcode markiert, wenn Sie den Fehler im Output-Fenster anklicken. Sun ONE Studio 4 bietet wie viele andere Entwicklungsumgebungen neben dem Kompilieren auch ein Erzeugen an („to build“). Betätigen Sie dazu (Strg) (ª) (F11) für das gesamte Projekt und (F11) für einzelne Dateien. Der Unterschied ist, dass ein einfaches Kompilieren nur dann erfolgt, wenn der Quellcode seit dem letzten Kompilieren geändert wurde. Wenn Sie die Datei dagegen „erzeugen“, wird eine eventuell bereits vorhandene .class-Datei vor dem Kompilieren gelöscht. In einigen Fällen (vor allen Dingen in größeren Projekten), bei denen das Kompilieren nicht zum erwarteten Ergebnis führt, weil Sun ONE Studio 4 geänderte Dateien aus irgendwelchen Gründen nicht kompiliert, hilft ein explizites Erzeugen. Sie können das Projekt nun mit (Strg) (ª) (F6) starten, wobei auch gleich eine Kompilierung vorgenommen wird, sofern diese notwendig ist. Beim ersten Start müssen Sie zunächst die Klasse auswählen, mit der Ihr Programm starten soll. Diese Einstellung wird im Projekt für spätere Starts gespeichert. Alternativ können Sie auch mit (Strg) (ª) (F5) starten. Mit dieser Start-Variante besitzen Sie dann die Möglichkeit, das Programm zu debuggen, wie ich es in Kapitel 4 zeige.
104
Sandini Bib
Das Starten über (Strg) (ª) (F5) oder (Strg) (ª) (F6) funktionierte in meinem Fall unter Linux nicht immer, wobei ich nicht herausfinden konnte, woran das lag. Ein wiederholtes Betätigen dieser Tastekombinationen führte in einigen Fällen zum Erfolg. In anderen Fällen konnte ich ein Programm aber nur starten, indem ich die Startdatei im Explorer markierte und (F6) betätigte. (F6) steht für das Starten einer einzelnen Datei, was in Projekten verwendet wird, die mehr als eine ausführbare Datei besitzen. Die Ausgaben einer Konsolenanwendung erscheinen nicht in einer Konsole des Betriebssystems, sondern im „Output Window“ der Entwicklungsumgebung. Nach der Ausführung des Programms müssen Sie im Register der Entwicklungsumgebung (oben) auf EDITING klicken, um wieder zum Editierfenster zu gelangen. Sie können das Editierfenster aber auch alternativ im DEBUGGING-Register anzeigen, indem Sie dort (Strg) (3) betätigen. Dasselbe ist für den Explorer über (Strg) (2) möglich.
2.8
Grundlagen zum Umgang mit Sun ONE Studio 4
2.8.1 Pakete Wenn Sie eine Klasse direkt einem verbundenen Dateiordner unterordnen, wird diese per Voreinstellung in keinem speziellen, sondern dem globalen Paket verwaltet. Wenn Sie allerdings Klassen in untergeordneten Ordnern erzeugen, werden diese automatisch einem Paket zugeordnet, dessen Name vom Ordnernamen abgeleitet wird. Wenn Sie z. B. den Ordner C:\Projekte\Java\HelloOne (Windows) bzw. /usr/projekte/ java/HelloOne (Linux) mit einem Projekt verbinden und darin einen Ordner Start anlegen, werden alle in diesem Ordner angelegten Klassen dem Paket Start zugeordnet. Legen Sie in diesem Ordner einen Unterordner mit dem Namen Main an, werden die darin enthaltenen Klassen dem Paket Start.Main zugeordnet. Verwalten Sie Ihre Klassen hingegen im Benutzerordner von Sun ONE Studio 4, werden diese dem Paket Projects..Files zugeordnet. Das Ganze ist etwas kompliziert, aber leider notwendig zu wissen, denn:
105
Sandini Bib
Wenn Sie in Sun ONE Studio 4 ein Programm starten, wird dieses nur dann korrekt ausgeführt, wenn die Startklasse dem passenden Paket zugeordnet ist. Wenn Sie Klassen über Sun ONE Studio 4 anlegen, werden diese automatisch dem passenden Paket zugeordnet. Wenn Sie aber andere Klassen in ein Projekt integrieren, müssen Sie selbst auf eine korrekte Einstellung des Paketnamens achten. Wenn Sie dies unterlassen, meldet Sun ONE Studio 4 beim Start des Programms die Ausnahme NoClassDefFoundError mit dem Fehler „wrong name“. Hinzu kommt, dass Paketnamen keine Leerzeichen und andere Sonderzeichen beinhalten dürfen. Die Ordner müssen also entsprechend benannt sein. Wenn Sie bereits andere Java-Programme besitzen oder diese mit einer anderen Entwicklungsumgebung entwickeln, müssen Sie die entsprechenden Ordner zur Ausführung dieser Programme in Sun ONE Studio 4 mit einem neuen oder vorhandenen Projekt verbinden und die package-Anweisung in diesen Programmen entsprechend anpassen. Die Buchbeispiele konnte ich leider nicht so anpassen, dass Sie diese in einem einzigen verbundenen Ordner in Sun ONE Studio 4 darstellen und ohne Probleme aufrufen können. Neben der Tatsache, dass die Ordnernamen dann keine Leerzeichen beinhalten dürften, würden die resultierenden Paketnamen leider zu lang werden, sodass Sun ONE Studio 4 einige Programme in tief verschachtelten Ordnern nicht ausführen könnte. Legen Sie also ein neues Projekt für die Buchbeispiele und in diesem für jedes Beispielprogramm, das Sie in Sun ONE Studio 4 betrachten wollen, einen neuen verbundenen Ordner an.
2.8.2 Integration von externen Projekten Wenn Sie externe Projekte (beispielsweise die Beispiele dieses Buchs) in Sun ONE Studio 4 integrieren wollen, gehen Sie folgendermaßen vor: • Speichern Sie die Java-Dateien in einem Ordner Ihrer Festplatte, falls das noch nicht der Fall ist, • legen Sie in Sun ONE Studio 4 ein neues Projekt an, • verbinden Sie den Ordner der Java-Dateien im FILESYSTEMS-Register des Explorers mit diesem Projekt, • wechseln Sie zum Projekt-Register des Explorers, • klicken Sie mit der rechten Maustaste auf den Projekteintrag und wählen Sie den Befehl ADD EXISTING, • wählen Sie im erscheinenden Dialog die Klassen aus, die im verbundenen Dateiordner gespeichert sind. Sie können den gesamten Datei-
106
Sandini Bib
ordner oder mehrere einzelne Klassen auswählen, indem Sie beim Klicken mit der Maus (Strg) betätigen. Alternativ können Sie die erste Klasse auswählen, (ª) betätigen und danach auf die letzte Klasse klicken, • betätigen Sie den OK-Schalter, um Ihre Auswahl zu bestätigen, • falls Ihr Projekt mehrere Klassen enthält, klicken Sie die Start-Klasse nun im Explorer mit der rechten Maustaste an und wählen Sie den Befehl SET AS PROJECT MAIN CLASS im TOOLS-Menü des Kontextmenüs. Wenn Sie das Projekt nun mit (Strg) (ª) (F5) starten, müsste alles funktionieren.
2.9
Entwickeln einer Anwendung mit grafischer Oberfläche mit Sun ONE Studio 4
In Java können Sie nicht nur einfache Konsolenanwendungen programmieren. Java stellt auch eine Menge an Features für die Entwicklung von Anwendungen mit einer grafischen Oberfläche zur Verfügung, ähnlich denen, die Sie bereits in Delphi bzw. Kylix verwendet haben. Leider ist deren Programmierung nicht allzu einfach, was zumindest dann gilt, wenn Sie eine solche Anwendung ohne eine Entwicklungsumgebung entwickeln. Sun ONE Studio 4 stellt aber einen grafischen Editor zur Verfügung, der die Arbeit erheblich vereinfacht. Als Beispiel verwende ich die Nettoberechnung, die Sie bereits mit Delphi oder Kylix programmiert haben. So können Sie das, was Sie im ersten Teil des Kapitels gelernt haben, hier bereits anwenden. Java ist sehr komplex im Bereich der Programmierung von und mit Formularen und Steuerelementen. Ich kann aus Platzgründen hier nur einen kleinen Ausschnitt der Möglichkeiten zeigen. Damit sind Sie aber in der Lage, einfache Anwendungen mit einer grafischen Oberfläche zu entwickeln. Halten Sie sich möglichst an meine Anleitung, damit beim Programmieren nicht allzu viel daneben geht.
107
Sandini Bib
Ein erster Hinweis folgt direkt hier: Sun ONE Studio 4 funktioniert leider nicht immer zuverlässig. Neben dem scheinbar in einigen Fällen zu großen Ressourcen-Hunger, der dazu führen kann, dass andere Programme oder das ganze System (wenigstens unter Windows) abstürzen, werden Sie u. U. bei der Arbeit mit dem Formular- oder dem Quellcodeeditor Probleme haben. Es kann dabei schon einmal passieren, dass das Markieren von Steuerelemente über die Maus oder die Eingabe von Quellcode nicht möglich ist. In einigen Fällen hilft dann ein wiederholtes Öffnen der entsprechenden Daten. Manchmal müssen Sie aber Sun ONE Studio 4 komplett neu starten, um sinnvoll weiterarbeiten zu können.
2.9.1 Erzeugen des Projekts und des Startformulars Zur Entwicklung einer Anwendung mit grafischer Oberfläche erzeugen Sie zunächst ein neues Projekt, das Sie vielleicht Nettoberechnung nennen. Verbinden Sie dann einen neuen Ordner Nettoberechnung, den Sie in Ihrem Projekte-Ordner anlegen, mit diesem Projekt. Nun fügen Sie dem Projekt ein neues Fenster hinzu. Dazu selektieren Sie zunächst das Projekt im Projekt-Register des Explorers, klicken das Projekt mit der rechten Maustaste an und wählen wie bei der Konsolenanwendung den Befehl ADD NEW im Kontextmenü. Hier wählen Sie aber nun die Vorlage JFRAME aus dem Ordner GUI FORMS (Abbildung 2.39).
Abbildung 2.39: Auswahl eines JFrame-Formulars für eine Java-Anwendung
108
Sandini Bib
Ein JFrame ist ein Formular, das dem ähnlich ist, das Sie von Delphi bzw. Kylix her kennen. Dieses Formular eignet sich ideal für eine normale Anwendung mit Fenstern. Nachdem Sie den NEXT-Schalter betätigt haben, geben Sie im nächsten Schritt noch einen Namen ein. Nennen Sie das Formular vielleicht StartForm. Die folgenden Schritte können Sie übergehen, indem Sie einfach den FINISH-Schalter betätigen. Wenn das Formular hinzugefügt wurde, zeigt die Entwicklungsumgebung direkt den Form Editor an, mit dem Sie das Formular bearbeiten können. Diesen Editor erreichen Sie auch, indem Sie im Explorer auf das Formular doppelklicken. In einem zweiten Fenster wird eventuell der Quellcode des Formulars angezeigt. Diesen sollten Sie zunächst ignorieren. Java-Formulare werden – anders als Delphi/Kylix-Formulare – komplett als Quellcode implementiert. Damit haben Sie aber nicht viel zu tun, wenn Sie eine Entwicklungsumgebung verwenden. Diese bietet Ihnen einen einfachen Formular-Editor, den Sie mit der Maus bearbeiten können, und übernimmt die Erzeugung des Quellcodes automatisch für Sie.
Abbildung 2.40: Der Formulareditor von Sun ONE Studio 4
109
Sandini Bib
2.9.2 Entwurf des Formulars Als Erstes müssen Sie eine Einstellung ändern, damit Sie das Formular auf die gewohnte Weise entwerfen können. Java-Formulare arbeiten nämlich mit ziemlich komplizierten so genannten Layout-Managern, die das Layout eines Formulars automatisch einstellen. Der voreingestellte Layout-Manager flowLayout sorgt dafür, dass Ihre Steuerelemente immer automatisch auf die maximal mögliche Größe vergrößert werden. Und das ist eher störend als hilfreich. Klicken Sie zur Änderung des Layout-Managers mit der rechten Maustaste auf das Formular und wählen Sie den Befehl SET LAYOUT. Wählen Sie dann den Eintrag Null Layout (was dafür steht, dass kein Layout-Manager verwendet wird). Nun können Sie Steuerelemente auf dem Formular ablegen. Die Standard-Steuerelemente finden Sie im oberen Bereich des Formulareditors im Register SWING. Das erste Steuerelement in der Reihenfolge ist ein Label (JLabel) zur Beschriftung, das zweite ein normaler Schalter (JButton). Das neunte Steuerelement (JTextField) ist eine Textbox. Platzieren Sie drei Label, drei Textboxen und zwei Schalter auf dem Formular, indem Sie diese in der Toolbox anklicken und danach auf dem Formular klicken. Platzieren Sie diese Steuerelemente und stellen Sie die Größe ein, bis das Formular in etwa so aussieht wie in Abbildung 2.41.
Abbildung 2.41: Das Formular zur Nettoberechnung in einer ersten Rohfassung
Sie können die Steuerelemente auch auf dem Formular markieren und über (Strg) (c) in die Zwischenablage kopieren. Mehrer Steuerelemente markieren Sie, indem Sie beim Klicken (ª) betätigen. Das Einfügen mit (Strg) (v) funktioniert aber nur, wenn kein Steuerelement markiert ist. Klicken Sie dazu vor dem Einfügen auf einen freien Bereich des Formulars. Die eingefügten Steuerelemente werden über den Quell-Steuerelementen angelegt, so dass es so aussieht, als ob nichts passiert wäre. Ziehen Sie das eingefügte Steuerelement dann einfach an seinen Platz.
110
Sandini Bib
2.9.3 Einstellen der Eigenschaften Nun müssen Sie wie bei Delphi und Kylix zunächst einige Eigenschaften der Steuerelemente einstellen. Die wichtigste ist einmal wieder der Name der Steuerelemente. Wählen Sie die Steuerelemente dazu einzeln aus und klicken Sie im Eigenschaften-Fenster auf das Register CODE GENERATION. Den Namen stellen Sie in der Eigenschaft VARIABLE NAME ein (Abbildung 2.42).
Abbildung 2.42: Einstellung der Eigenschaft Variable Name für die erste Textbox
Vergeben Sie wieder sinnvolle Namen. Ich übernehme die Namen der Delphi/Kylix-Anwendung (txtBrutto, txtSteuer, txtNetto, btnRechnen und btnBeenden). Die Label benenne ich nicht, weil ich diese im Programm nicht anspreche. Nun stellen Sie noch den Text der Label, der Schalter und der Textfelder ein. Wählen Sie dazu das Register PROPERTIES im Eigenschaftenfenster und schreiben Sie entsprechende Werte in die Eigenschaft Text. Das Ergebnis soll so aussehen wie in Abbildung 2.43.
Abbildung 2.43: Das Formular zur Nettoberechnung im fertigen Entwurf
111
Sandini Bib
Stellen Sie dann die Eigenschaft title des Formulars ein, indem Sie auf einem freien Bereich des Formulars klicken und die Eigenschaft im PROPERTIES-Register des Eigenschaftenfensters suchen. Als Titel bietet sich natürlich „Nettoberechnung“ an. Verhindern, dass das Formular automatisch verkleinert wird
Abschließend sollten Sie noch dafür sorgen, dass das Formular beim Start nicht automatisch verkleinert wird, was ansonsten der Fall wäre. Bei markiertem Formular klicken Sie dazu auf das Register CODE GENERATION des Eigenschaftenfensters. Setzen Sie die Eigenschaft FORM SIZE POLICY auf GENERATE RESIZE CODE. Nun wird Ihr Formular immer auf die Größe eingestellt, die Sie in der Entwicklungsumgebung festlegen. Bei der Höhe müssen Sie allerdings die Höhe der hinzukommenden Titelleiste mit einrechnen. Gestalten Sie Ihr Formular also etwas höher als notwendig.
2.9.4 Programmieren Nun können Sie programmieren. Dazu klicken Sie wieder einfach doppelt auf den RECHNEN-Schalter. Sun One Studio 4 ist dabei etwas störrisch und öffnet in vielen Fällen die Beschriftung des Schalters zur Bearbeitung, anstatt den Quellcodeeditor zu öffnen. Probieren Sie es dann einfach mit einem schnelleren Doppelklick. Das funktioniert in den meisten Fällen. Falls Sie Probleme damit haben, können Sie die notwendige Methode auch über das Kontextmenü des Schalters erzeugen. Markieren Sie den Schalter, klicken Sie mit der rechten Maustaste darauf und wählen Sie den Befehl EVENTS / ACTION / ACTIONPERFORMED. Methode zur Ereignisbehandlung erzeugen
Die Entwicklungsumgebung erzeugt automatisch eine Methode zur Behandlung des Ereignisses „Betätigung des Schalters“ und zeigt den Quellcode des Formulars an. Lassen Sie sich nicht von dem komplizierten Code verwirren. Das, was Sie dort sehen, gehört bereits zu den erweiterten objektorientierten Techniken. Für Sie ist – wie bei Delphi und Kylix – lediglich wichtig, was Sie innerhalb der automatisch erzeugten Methoden programmieren. Bei dem Quellcode des Formulars habe sogar ich einige Probleme zu verstehen, was da wirklich passiert. Und ich programmiere nun schon einige Jahre.
112
Sandini Bib
Abbildung 2.44: Der Quellcode-Editor von Sun ONE Studio 4 mit einer Methode für die Betätigung des Rechnen-Schalters
Die blau hinterlegten Bereiche (in Abbildung 2.44 sind diese natürlich grau) stellen Quellcode dar, der von der Entwicklungsumgebung automatisch erzeugt wird und den Sie gar nicht bearbeiten können. So können Sie auch weniger Fehler machen. Nur den Quellcode in den weißen Bereichen können Sie bearbeiten. Die im unteren Bereich von Abbildung 2.44 sichtbare main-Methode zeigt übrigens, wie eine solche Anwendung startet, nämlich im Prinzip genau wie eine Konsolenanwendung. Nur dass hier das Formular erzeugt und geöffnet wird. Aber das gehört auch schon zu den erweiterten Techniken. Innerhalb der Methode btnRechnenActionPerformed können Sie nun programmieren. Zur Programmierung einer Nettoberechnung gehen Sie prinzipiell so vor wie in Delphi und Kylix. Zunächst benötigen Sie drei Variablen. In Java-Programmen deklarieren Sie diese einfach da, wo Sie sie benötigen. Die Deklaration sieht etwas anders aus als in Delphi und Kylix:
Programmierung der Berechnung
01 private void btnRechnenActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Deklaration der benötigten Variablen */ 03 double brutto; 04 double steuer; 05 double netto;
Nun können Sie die eingegebenen Daten einlesen. Anders als in Delphi und Kylix lesen Sie hier keine Eigenschaft aus. Sie rufen zum Einlesen der Eingaben eine Methode auf. Die Methode getText gibt das, was in der
113
Sandini Bib
Textbox gespeichert ist, als Text zurück. Die Eingaben müssen Sie auch in Java konvertieren. Dazu verwenden Sie die Java-Methode Double. parseDouble, der Sie den zu konvertierenden Text übergeben. Diese Methode arbeitet fast genauso wie die Object Pascal-Funktion StrToFloat. Sie übergeben den zu konvertierenden Text (den Sie über die getText-Methode ermitteln) und erhalten eine Zahl zurück: 06 07
brutto = Double.parseDouble(txtBrutto.getText()); steuer = Double.parseDouble(txtSteuer.getText());
Nun folgen nur noch die Berechnung: 08
netto = brutto * (1 - (steuer / (100 + steuer)));
und die Ausgabe des Ergebnisses. Wie bei Object Pascal müssen Sie die Zahl nun wieder in einen Text konvertieren, damit Sie diesen in die Textbox schreiben können. Leider stellt Java keine einfache Funktion dazu zur Verfügung. In Delphi und Kylix konnten Sie die Funktion FormatFloat verwenden. In Java sieht der Quellcode zur Umwandlung und zur Formatierung einer Zahl etwas komplizierter aus: 09 java.text.DecimalFormat df = new java.text.DecimalFormat("0.00"); 10 txtNetto.setText(df.format(netto)); 11 }
Zunächst erzeugen Sie in Zeile 9 ein Objekt. In Kapitel 6 werden Sie lernen, was das ist und wie Sie damit umgehen. Dieses spezielle Objekt wird in Java dazu verwendet, dezimale Zahlen zu formatieren. Dazu verwenden Sie die format-Methode. Im Prinzip ist das das, was Sie auch in Delphi bzw. Kylix mit der FormatFloat-Funktion gemacht haben. Der Unterschied ist hier nur, dass Sie statt einer einfachen Funktion ein Objekt und dessen Methode verwenden. Falls Sie nun Probleme haben, dies zu verstehen, versuchen Sie es erst gar nicht weiter. Im weiteren Verlauf des Buchs lernen Sie, was ein Objekt ist. Dann wissen Sie ziemlich genau, was hier passiert. Nehmen Sie den Quellcode als Beispiel dafür, wie Sie in Java Zahlen formatiert ausgeben können. Ich musste dieses Feature übrigens selbst erst recherchieren ... Anders als in Delphi und Kylix können Sie auch nicht in eine Eigenschaft schreiben, um den Text eines Textfelds zu setzen. Dazu verwenden Sie die setText-Methode des Textfelds, wie Sie es in Zeile 10 sehen. Programmieren des BeendenSchalters
114
Nun müssen Sie nur noch die Methode für den BEENDEN-Schalter programmieren. Klicken Sie im Formulareditor doppelt auf diesen Schalter, um eine Methode zu erzeugen, die auf die Betätigung des Schalters reagiert. Programmieren Sie dann die folgende Anweisung:
Sandini Bib
12 private void btnBeendenActionPerformed(java.awt.event.ActionEvent evt) 13 dispose(); 14 }
Damit ist die Programmierung abgeschlossen und Sie können das Programm testen.
2.9.5 Testen des Programms Nun können Sie das Programm testen. Starten Sie das Projekt wie bei einer Konsolenanwendung mit (ª) (Strg) (F6). Beim ersten Start müssen Sie wieder die Projekt-Startklasse angeben. Wenn der Compiler dann keine Fehler meldet, öffnet sich nach einer kurzen Zeit das Formular und Sie können Zahlen eingeben und rechnen (Abbildung 2.45).
Abbildung 2.45: Das Nettoberechnungs-Programm unter Linux
Wenn Sie einmal ungültige Zahlen oder nichts eingeben und den RECHNEN-Schalter betätigen, wird auch in diesem Programm eine Ausnahme erzeugt. Diese wird allerdings lediglich im Output Window von Sun ONE Studio 4 gemeldet. Das Programm läuft nach der Ausnahme einfach weiter. Wenn Sie das Programm direkt über den Java-Interpreter starten, wird die Ausnahme ebenfalls direkt in der Konsole gemeldet. Wie bei Delphi und Kylix sollten Sie solche Ausnahmen abfangen. In Kapitel 4 zeige ich, wie das geht. Zurzeit müssen Sie sich noch mit der einfachen Meldung in der Konsole begnügen.
Ausnahmen bei ungültigen Eingaben
2.9.6 Die vom Compiler erzeugten Dateien Wenn Sie ein Formular mit Sun ONE Studio 4 entwickeln und kompilieren, erzeugt die Entwicklungsumgebung mehrere .class-Dateien. In meinem Fall wurden z. B. die Dateien StartForm.class, StartForm$1.class, StartForm$2.class und StartForm$3.class erzeugt. Alle diese Dateien sind notwendig, wenn Sie das Programm über den Java-Interpreter (z. B. auf einem anderen Rechner) ausführen wollen. Was die Entwicklungsumgebung damit bezweckt, konnte ich allerdings nicht herausfinden.
115
Sandini Bib
2.10 Die Weitergabe einer Anwendung Die Weitergabe einer Anwendung ist eigentlich ein komplexes Thema, das gar nicht in ein Anfänger-Buch hineingehört. Es kann aber sein, dass Sie Ihre einfachen Anwendungen an Freunde weitergeben sollen. Deswegen, und weil die Weitergabe bei Java Probleme verursachen kenn, beschreibe ich wenigstens die Grundlagen. Die im unteren Bereich von Abbildung 2.44 sichtbare main-Methode zeigt übrigens, wie eine solche Anwendung startet, nämlich im Prinzip genau wie eine Konsolenanwendung. Nur dass hier das Formular erzeugt und geöffnet wird. Aber das gehört auch schon zu den erweiterten Techniken. Profis verwenden für die Weitergabe von Anwendungen spezielle Programme, die ein Installationsprogramm erzeugen und die automatisch alle benötigten Dateien auf den Rechner des Anwenders kopieren. Für eine Beschreibung dieser Tools bleibt mit hier kein Platz. Einfache Delphi/Kylix-Anwendungen wie die, die Sie in diesem Buch entwickeln, können Sie aber auch einfach auf den anderen Rechner kopieren. Da Sie keine speziellen externen Bibliotheken verwenden, funktioniert diese Technik meist ohne Probleme. Was allerdings nicht möglich ist, ist, dass Sie eine Kylix-Anwendung auf einem Windows-Rechner ausführen oder eine Delphi-Anwendung auf einem Linux-Rechner. Bei Java sieht das Ganze anders aus. Java-Programme laufen auf allen Betriebssystemen, auf denen Java installiert ist. Es ist also zunächst kein Problem, ein unter Linux entwickeltes Programm unter Windows auszuführen. In vielen Fällen reicht dazu das Kopieren der .class-Dateien, die das kompilierte Programm beinhalten. Leider ist das Ganze nicht mehr so einfach, wenn Ihre Programme Bibliotheken benutzen, die nicht zum Java-Standard gehören. Diese Bibliotheken müssen auf dem anderen Rechner dann ebenfalls, idealerweise im Lib-Ordner des Java-Runtime-Environment (JRE), verfügbar sein. Und das betrifft sogar bereits Ihre ersten Programme, die mit Formularen arbeiten, wenn Sie als Layout-Manager z. B. absoluteLayout verwenden. Denn dieser gehört (leider) nicht zum Java-Standard. Wenn Sie ein solches Programm auf einem Rechner ausführen, der die Java-Standardinstallation besitzt, resultiert das in der Ausnahme NoClassDefFoundError, weil der Java-Interpreter die benötigte Klasse nicht findet. Sie müssten sich dann damit auseinander setzen, in welcher Bibliothek die fehlenden Klassen zu finden sind, und diese dann auf den Zielrechner kopieren. Aber das soll nur ein Hinweis sein, für den Fall, dass Sie bei der Ausführung Ihrer Programme die Ausnahme NoClassDefFoundError erhalten.
116
Sandini Bib
2.11 Zusammenfassung In diesem Kapitel haben Sie zunächst gelernt, wie Sie in Java und Delphi/Kylix eine einfache Konsolenanwendung erzeugen. Sie kennen zwar die grundlegenden Techniken noch nicht, die beim Programmieren angewendet werden, können aber bereits Programme schreiben, die an der Konsole Eingaben entgegennehmen und Texte ausgeben. Dabei können Sie auch bereits mit den Fehlermeldungen des Compilers umgehen, auch wenn Sie diese zurzeit noch nicht richtig verstehen. Mit Ausnahmen, die bei ungültigen Eingaben entstehen, können Sie nur insoweit umgehen, dass Sie das Programm in der Entwicklungsumgebung beenden. Sie können aber nicht nur Konsolenanwendungen entwickeln, sondern auch einfache Anwendungen mit einer grafischen Oberfläche, also mit Fenstern und Steuerelementen. Sie wissen, was einfache Programme von solchen mit grafischer Oberfläche unterscheidet und wie Sie in diesen Programmen Eingaben entgegennehmen und Ergebnisse ausgeben. Im Prinzip kennen Sie nun bereits die zwei grundlegenden Arten der Programmierung: die sequentielle, bei der ein Programm von oben nach unten abgearbeitet wird, und die ereignisorientierte, bei der ein Programm immer erst dann aufgerufen wird, wenn der Anwender das Ereignis auslöst (also z. B. einen Schalter betätigt).
2.12 Fragen und Übungen 1. Wie erzeugen Sie aus einem Java-Quelltext (mit der Endung .java) ein
ausführbares Programm? 2. Wie führen Sie ein Java-Programm (das in einer Datei mit der Endung
.class gespeichert ist) aus? 3. Welche Vorteile bietet eine Entwicklungsumgebung wie beispielswei-
se Delphi oder Kylix gegenüber dem einfachen Entwickeln in einem Texteditor? 4. Was sollten Sie beachten, wenn Sie ein neues Delphi/Kylix-
Programm peichern? 5. Was ist ein Projekt? 6. Nennen Sie zwei Unterschiede zwischen einer Konsolenanwendung
und einer Anwendung mit grafischer Oberfläche.
=XVDPPHQIDVVXQJ
117
Sandini Bib
Sandini Bib
3
Basiswissen
le
Sie lernen in diesem Kapitel:
n e rn
• wie ein Computer grundsätzlich arbeitet und welche Bedeutung das Betriebssystem, das BIOS und der Arbeitsspeicher für Programmierer besitzen, • auf welche Weise Daten auf Speichermedien und im Arbeitsspeicher gespeichert werden, • wie Programme geschrieben und für den Computer übersetzt werden, • welche Anwendungsarchitekturen es gibt, • was die wichtigsten Programmiersprachen voneinander unterscheidet, • welche Bedeutung Algorithmen für die Programmierung haben, • welche Strukturelemente in Algorithmen verwendet werden und • welche Bedeutung Schleifen und Verzweigungen für die Darstellung eines Algorithmus haben. Nachdem Sie in Kapitel 2 Ihre ersten Erfolge beim Programmieren hatten, behandelt dieses Kapitel einige Basisfragen, die für das Verständnis des Buchs wichtig sind. Es zeigt, wie ein Computer und dessen Programme grundsätzlich funktionieren, was ein Programm aus der Sicht des Computers eigentlich ist, unter welchen Architekturen und mit welchen Programmiersprachen Sie heutzutage Software entwickeln können und was ein Algorithmus ist.
3.1
Wie arbeitet ein Computer?
Dieses Buch ist kein Buch über Computer an sich. Also keine Bedenken: Ich gehe nicht im Detail auf die Arbeitsweise eines Computers ein. Ein Programmierer muss aber dessen Funktionsweise wenigstens grundlegend kennen. Deswegen beschreibe ich, was die Aufgabe eines Betriebssystems ist, wie ein Programm prinzipiell arbeitet und welche Rolle der Arbeitsspeicher dabei spielt. Ich gehe in diesem Abschnitt übrigens da-
:LH DUEHLWHW HLQ &RPSXWHU"
119
Sandini Bib
von aus, dass Sie den grundsätzlichen Aufbau des Computers kennen (also wissen, was die Begriffe CPU, Arbeitsspeicher, Festplatte, CD-ROMLaufwerk etc. bedeuten).
3.1.1 Das Grundprinzip: EVA Das Grundprinzip eines Computers wird als EVA bezeichnet. EVA bedeutet „Eingabe, Verarbeitung und Ausgabe“. Daten werden in verschiedenen Formen eingegeben, verarbeitet (berechnet) und wieder ausgegeben. Eingabegeräte sind z. B. die Tastatur, die Maus, die Festplatte, ein Scanner oder ein (DSL-)Modem. Ausgabegeräte sind z. B. der Monitor, ein Drucker, die Festplatte oder wieder ein (DSL-)Modem. Wenn Sie in einer Textverarbeitung einen Text eingeben, werden Ihre Tastenbetätigungen (die Eingabe) vom Programm verarbeitet (analysiert) und auf dem Monitor und im Druck in Form von Zeichen ausgegeben. Wenn Sie die Datei speichern, führt Ihr Befehl (die Eingabe) dazu, dass die im Arbeitsspeicher gespeicherten Daten in eine Datei geschrieben werden. Öffnen Sie in derselben Textverarbeitung eine Datei, wird diese vom Datenträger gelesen (Eingabe), verarbeitet (korrekt interpretiert) und als Dokument auf dem Bildschirm ausgegeben. Immer wieder ist EVA im Spiel. EVA bedeutet also, dass Daten von einem Eingabemedium gelesen, verarbeitet und wieder ausgegeben werden. EVAs überall
Obwohl EVA das Grundprinzip des Computers ist, müssen Sie sich bei der Programmierung allerdings kaum Gedanken um dieses Prinzip machen: Jedes Programm besteht automatisch aus mindestens einem, meist aber ziemlich vielen EVAs. Die einfache Konsolen-Nettoberechnung aus Kapitel 2 besitzt beispielsweise nur ein EVA (Bruttobetrag und Steuerwert eingeben, Nettobetrag ausrechnen, Nettobetrag ausgeben). Die Fenster-Variante besitzt aber schon zwei: eines, das auf die Betätigung des RECHNEN-Schalters reagiert, und eines für den BEENDEN-Schalter. Ein komplexeres Rechen-Programm wie der Windows-Taschenrechner besteht aus recht vielen einzelnen EVAs, die Ihre einzelnen Tastaturoder Mauseingaben auswerten. Normalerweise wissen Sie recht sicher, woher die Daten stammen, die verarbeitet und ausgegeben werden sollen, und wohin die Daten ausgegeben werden sollen, weil die Aufgabenstellung Ihres Programms dies so festlegt. Ein einfaches Programm zur Berechnung eines Bruttobetrags erwartet die Eingabe z. B. entweder direkt über die Tastatur oder in einem Steuerelement und gibt das Ergebnis an der Konsole oder in einem weiteren Steuerelement aus. Ein Programm zur statistischen Auswertung der Umsätze eines Unternehmens wird die Basisdaten aus einer Datenbank oder einer Datei auslesen und wahrscheinlich in grafischer
120
%DVLVZLVVHQ
Sandini Bib
Form auf dem Drucker ausgeben. Die Frage ist dabei lediglich, wie Sie die Daten einlesen und wie Sie das Ergebnis ausgeben. Jede Programmiersprache bietet Ihnen dazu verschiedene Hilfsmittel, deren Handhabung zwar erlernt werden muss, die dann aber mit wenig Aufwand zu schnellen Ergebnissen führen.
3.1.2 Die CPU, Maschinensprache und Controller Bei der Verarbeitung von Programmen spielt die CPU (Central Processing Unit, der Prozessor des Rechners) die wichtigste Rolle. Die CPU kennt einige hundert fest verdrahtete einfache Befehle, über die grundlegende mathematische Operationen wie Additionen, Subtraktionen, Multiplikationen, Divisionen und das Ansprechen der Speichermedien und der Ausgabemedien möglich sind. Die Summe dieser Befehle wird als Maschinensprache bezeichnet. Jedes Programm führt auf der untersten Ebene meist mehrere Millionen Befehle in der CPU aus. Ein CPU-Befehl besteht aus einer Folge von Nullen und Einsen. Ein typischer Befehl sieht beispielsweise so aus: 00000000010000001000000001100100. Ein Teil der Folge steht für den Befehl selbst, ein weiterer Teil für variable Argumente des Befehls. Bei einer Multiplikation enthält die Befehlsfolge beispielsweise den Multiplikations-Befehl und die Adressen der zu multiplizierenden Speicherbereiche. Dass ein Maschinensprache-Befehl aus einzelnen Nullen und Einsen zusammengesetzt ist, liegt daran, dass die CPU im Prinzip aus sehr vielen kleinen elektronischen Schaltern besteht, die Strom entweder ein- oder ausschalten. Eine Null in einem Befehl repräsentiert einen ausgeschalteten, eine Eins einen eingeschalteten Schalter. Wenn die CPU einen Befehl ausführt, werden die für die Ausführung von Befehlen zuständigen Schalter entsprechend dem Befehl ein- bzw. ausgeschaltet, womit der Strom in spezielle Bahnen gelenkt wird. Was dabei wirklich in der CPU passiert, ist ziemlich komplex. Im Prinzip geht es hier nur darum, dass Sie wissen, warum Maschinensprache aus Nullen und Einsen besteht und warum Maschinensprache für den Menschen sehr schwer verständlich ist.
Maschinensprache
In den Anfangstagen des Computers wurden Programme oft in Maschinensprache geschrieben, weil zu dieser Zeit noch keine höheren Sprachen zur Verfügung standen. Die Programmierung sah damals allerdings (als die Computer noch kaum leistungsfähig waren) so aus, dass entweder Schalter auf einfachen Schalttafeln umgelegt oder Lochkarten gestanzt wurden (die dann in den Computer eingelesen wurden). Sie können auch heute noch in dieser Sprache entwickeln. Allerdings wird dazu meist eine Abstraktion der Maschinensprache verwendet. Diese Abstraktion wird als Assemblersprache bezeichnet. Eine Assemblerspra-
Assembler
:LH DUEHLWHW HLQ &RPSXWHU"
121
Sandini Bib
che stellt die CPU-Befehle in einer für Menschen einfacher lesbaren Form dar, wie Sie noch auf Seite 142 sehen. Ein spezielles Programm, ein Assembler, übersetzt diese Befehle dann in Maschinensprache. Die Entwicklung eines Assemblerprogramms ist allerdings keine einfache Aufgabe und heutzutage, wo meist grafische Oberflächen für Programme verwendet werden und Programme sehr komplex sind, eigentlich auch unsinnig. Schließlich gibt es moderne höhere Programmiersprachen, die die Programmentwicklung erheblich vereinfachen, wie Sie ja bereits in Kapitel 2 gesehen haben. Controller
Moderne Computer überlassen nicht nur der CPU die gesamte Arbeit. Verschiedene Controller übernehmen wichtige Teilaufgaben. Die Tastatur und eine PS/2-Maus werden beispielsweise vom Tastatur-Controller verarbeitet, ein spezieller I/O-Chip übernimmt das Handling der seriellen und parallelen Schnittstellen, der Diskettenlaufwerke und in einigen Fällen auch der Festplatten. Diese Controller sind meist Bestandteil des Motherboards1. Andere Controller sind Teil einer Erweiterungskarte. Moderne Festplatten, Grafik- und SCSI-Karten enthalten beispielsweise eigene Controller. Insgesamt wird die CPU damit von vielen Aufgaben entlastet und die Performance des Gesamtsystems erhöht.
Taktrate
Bei der Bewertung der Leistung einer CPU spielt neben der Anzahl und Mächtigkeit der Befehle (die bei neueren CPUs immer wieder erhöht wird) die Taktrate eine wichtige Rolle. Die Taktrate wird in Hertz gemessen. Hertz bedeutet „Takte pro Sekunde“. Eine CPU mit 2000 MHz ist also in der Lage 2000 Millionen Takte pro Sekunde auszuführen. Ein Takt ist im Prinzip das einmalige Ein- und Ausschalten des Stroms in der CPU. Ein CPU-(Maschinensprache-)Befehl benötigt eine festgelegte Anzahl an Takten. Ein einfacher Befehl wird beispielsweise in zwei Takten ausgeführt, komplexere Befehle benötigen mehr Takte. Daraus ergibt sich, dass eine CPU, die eine hohe Taktrate besitzt, mehr Befehle in einem gegebenen Zeitrahmen ausführen kann als eine CPU mit einer niedrigen Taktrate (also „schneller“ ist). Die Gesamt-Performance des Systems ergibt sich aber nicht nur aus der CPU-Taktrate, sondern auch aus der Anzahl der CPUs (mit speziellen Motherboards ist es auch möglich, mehrere CPUs gleichzeitig zu betreiben), der Geschwindigkeit des System-Bus (der für die Datenübertragung zwischen CPU, Arbeitsspeicher, Grafikkarte, Festplatte und anderen Medien verantwortlich ist), der Geschwindigkeit der Grafikkarte, der Größe des Arbeitsspeichers, der Geschwindigkeit der Festplatte und anderen Faktoren.
1.
122
Ein Motherboard ist eine große Platine mit vielen Schaltkreisen, die definierte Aufgaben im Computer übernehmen, und mit standardisierten Steckplätzen für die CPU, den Arbeitsspeicher, die Grafikkarte etc. Alle Bestandteile des Computers sind mehr oder weniger direkt mit dem Motherboard verbunden.
%DVLVZLVVHQ
Sandini Bib
3.1.3 Das BIOS und das Betriebssystem Das BIOS Die Befehle der CPU ermöglichen lediglich einfachste Operationen wie Additionen, Multiplikationen und das Kopieren von Daten zwischen Speicherbereichen. Programme, die lediglich CPU-Befehle nutzen könnten, wären sehr aufwändig. Wenn ein Programm direkt über CPUBefehle beispielsweise eine Datei von der Festplatte lesen wollte, müsste dieses Programm den genauen Aufbau der Festplatte kennen um die richtigen Speicherbereiche der Festplatte ansprechen zu können. Problematisch dabei ist, dass jeder Computer unterschiedliche Hardware besitzen kann, die auf verschiedene Weise angesprochen werden muss. Da die CPU nur grundlegende Befehle beherrscht, müssten Programme jede Hardware-Variante berücksichtigen, wenn sie nur auf der CPU aufsetzen würden. Um dieses Problem zu lösen besitzt jeder (normale) Computer ein so genanntes BIOS (Basic In and Out System, das Basis-Ein-und-AusgabeSystem). Das BIOS ist ein Programm, das im ROM (Read Only Memory) des Computers auf dem Motherboard gespeichert ist. Ein ROM ist ein Speicher, der einmal dauerhaft beschrieben wurde und mit normalen Mitteln dann nur noch gelesen werden kann. Das BIOS enthält spezielle Befehle, die jeweils mehrere CPU-Befehle aufrufen und damit wesentlich mächtiger sind. Eine Besonderheit des BIOS ist, dass dieses die verschiedenen Hardware-Varianten berücksichtigt, also für den Zugriff auf verschiedene Varianten dieselben Befehle zur Verfügung stellt.
Mächtigere Befehle
Aus diesem Grund verwaltet das BIOS auch Einstellungen zur Hardware des Systems. Diese Einstellungen können Sie erforschen und verändern, wenn Sie beim Booten des Systems eine bestimmte Tastenkombination betätigen (die kurz nach dem Start des Rechners auf dem Bildschirm angezeigt wird). Diese Einstellungen werden in einem speziellen überschreibbaren Speicher auf dem Motherboard gespeichert. Damit die so gespeicherten Konfigurationsdaten nicht verloren gehen, besitzt das Motherboard eine kleine Batterie, die ständig Strom für die Erhaltung des BIOS-Datenspeichers liefert.
Hardware-
Das BIOS ist daneben auch noch dafür zuständig, beim Booten des Rechners das Betriebssystem auf einem festgelegten Bereich der Festplatte, der CD bzw. eines anderen bootfähigen Speichermediums zu suchen, in den Arbeitsspeicher zu laden und auszuführen.
Booten
:LH DUEHLWHW HLQ &RPSXWHU"
Einstellungen
123
Sandini Bib
Das Betriebssystem und Hardware-Treiber Das Betriebssystem, das vom BIOS beim Bootvorgang in den Arbeitsspeicher geladen wird, ermöglicht erst die Arbeit mit dem Computer. Es bietet dem Anwender eine Infrastruktur, über die Programme ausgeführt und Dateien verwaltet werden können. Treiber
Für den Anwender unsichtbar verwaltet das Betriebssystem normalerweise verschiedene Treiber, die den Zugriff auf spezielle oder erweiterte Funktionen der Hardware ermöglichen. Diese Treiber sprechen die Hardware häufig direkt an, unter Umgehung der CPU. So ist es beispielsweise für Programme über das Betriebssystem möglich, die 3D-Funktionen einer Grafikkarte zu nutzen oder die grafischen Features eines Druckers anzusprechen. Der Zugriff über das Betriebssystem geschieht dabei immer auf die gleiche Weise, unabhängig von der Art der Hardware. Der Treiber, der normalerweise vom Hersteller der Hardware entwickelt wurde, übernimmt die physische Steuerung der Hardware, die bei verschiedener Hardware ganz unterschiedlich sein kann. Ein Drucker erwartet beispielsweise bestimmte Steuerungsbefehle zur Formatierung von Zeichen oder zum Wechsel der aktuellen Seite, verschiedene Drucker arbeiten mit vollkommen unterschiedlichen Befehlssätzen. Für ein Windows-Programm ist es prinzipiell unwichtig, welcher Drucker angeschlossen ist, es nutzt lediglich die zum Zugriff auf den Drucker vorgesehenen Betriebssystemfunktionen. Die eigentliche Steuerung des Druckers übernimmt der Druckertreiber, der vom Betriebssystem mit den Aufgaben betreut wird, die das Programm ursprünglich an das Betriebssystem übergeben hat.
BetriebssystemFunktionen
124
Das Betriebssystem enthält aber noch wesentlich mehr Funktionalität. Moderne Betriebssysteme bieten Programmen zusätzlich eine große Anzahl einfach anzuwendender Funktionen für allgemeine Aufgaben. Programme müssen nicht mehr auf die komplizierten BIOS- und CPU-Befehle zugreifen oder die Hardware (über den Treiber) direkt ansprechen, sondern können die wesentlich einfacher anzuwendenden Betriebssystem-Funktionen nutzen. Das Einlesen einer Datei erfordert unter Windows beispielsweise nur den Aufruf einer einzigen Funktion. Der Ausdruck von Text- oder Grafikdaten ist ebenfalls für Programme recht einfach über die dafür zuständigen Betriebssystemfunktionen. Grafische Betriebssysteme bieten Programmen zudem spezielle Fenster und Steuerelemente, die genutzt werden, um die Oberfläche einer Anwendung zu erzeugen. Daneben stehen einem Programm auch Funktionen zum Zeichnen von grafischen Elementen wie einfachen Rechtecken, Kreisen, aber auch zur Ausgabe von Bildern auf dem Bildschirm zur Verfügung.
%DVLVZLVVHQ
Sandini Bib
Viele Betriebssysteme wie Windows und Linux kapseln den Zugriff auf die Hardware komplett. Ein Programm muss nicht mehr direkt auf die CPU, das BIOS oder auf die Hardware zugreifen, sondern kann dazu Betriebssystemfunktionen nutzen. Bei „sicheren“ Betriebssystemen wie Windows 2000 und Windows XP besteht sogar gar keine Möglichkeit, direkt auf die Hardware zuzugreifen. Weil dieser Zugriff nur über Betriebssystemfunktionen möglich ist, kann das Betriebssystem sicherstellen, dass (möglichst) keine Hardware-Funktionen aufgerufen werden, die Probleme verursachen könnten (wie z. B. das versehentliche Überschreiben eines Festplattenbereichs).
Kapselung des Hardware-Zugriffs
Die Zusammenarbeit Abbildung 3.1 zeigt die Zusammenarbeit zwischen Anwendungen, dem Betriebssystem, dem BIOS und der Computer-Hardware (wozu auch die CPU gehört).
Programm Betriebssystem BIOS Hardware
Abbildung 3.1: Die Zusammenarbeit von Programmen, dem Betriebssystem und dem BIOS
Einfache Betriebssysteme wie z. B. DOS liefern ihren Anwendungen natürlich wesentlich weniger Funktionen als komplexe Betriebssysteme wie Windows oder Linux. Wenn Sie schon mal mit einem einfachen DOS-Programm gearbeitet haben, werden Sie wissen, dass Ein- und Ausgaben unter DOS normalerweise direkt an der Konsole erfolgen. Komplexere DOS-Programme, die dem Anwender Windows-ähnliche Fenster anbieten, müssen diese Funktionalität selbst implementieren,
:LH DUEHLWHW HLQ &RPSXWHU"
125
Sandini Bib
können dabei also nicht auf das Betriebssystem zurückgreifen. Windows und das X Window-System von Linux bieten Programmen dagegen eine Vielzahl von Funktionen für Ein- und Ausgaben. Jedes Fenster einer grafischen Anwendung und alle darin enthaltenen Elemente werden mit Hilfe von Betriebssystemfunktionen auf dem Bildschirm ausgegeben2, jeder Ausdruck auf dem Drucker erfolgt über Betriebssystemfunktionen usw. Den Ablauf der Erzeugung einer Programmoberfläche (in Form eines Fensters) illustriert Abbildung 3.2. Abbildung 3.3 zeigt, wie ein Programm prinzipiell Daten auf dem Drucker ausgibt. 1.
übergibt die Kontrolle an das Programm
Programm
3.
xxxx xxxx xxxx
fordert ein Fenster an erzeugt das Fenster
2. Betriebssystem
Abbildung 3.2: Ein Programm verwendet Betriebssystemfunktionen zum Erzeugen von Fenstern.
1.
Programm
3.
sendet Druckauftrag
steuert den Drucker 2. Betriebssystem
Abbildung 3.3: Ein Programm verwendet Betriebssystemfunktionen zum Drucken von Daten.
2.
126
Was bei Linux nicht so ganz korrekt ist, denn hier stellt nicht das Betriebssystem, sondern ein spezielles Programm (meist das X Window-System) die grundlegende Funktionalität für grafische Oberflächen zur Verfügung. Auf diesem Programm bauen Fenstermanager wie die KDE und GNOME auf, die einen Desktop zur Verfügung stellen und Fenster ganz individuell darstellen.
%DVLVZLVVHQ
Sandini Bib
3.1.4 Was ist ein Programm? Ein Programm ist eine gespeicherte Folge von Anweisungen. Diese Anweisungen können Aktionen bewirken (wie Eingaben entgegennehmen oder Dateien einlesen), Berechnungen ausführen, Daten ausgeben, aber auch dafür sorgen, dass ein Teil des Programms wiederholt oder abhängig von einer Bedingung der eine oder andere Teil des Programms ausgeführt wird. Beim Einlesen einer Textdatei werden beispielsweise häufig die einzelnen Zeilen in einer Schleife eingelesen, verarbeitet und ausgegeben, bis das Ende der Datei erreicht ist. Diese bei der Programmierung sehr wichtige Strukturierung eines Programms behandelt das Buch ausführlich in Kapitel 5. Ein einfaches Programm zur Berechnung einer mathematischen Formel enthält z. B. Anweisungen, die die Eingaben des Anwenders entgegennehmen, Anweisungen, die ein Ergebnis berechnen, und Anweisungen, die das Ergebnis auf dem Bildschirm oder dem Drucker ausgeben. Solch ein Programm haben Sie in Kapitel 2 schon selbst geschrieben. Da Programme immer von Betriebssystemen ausgeführt werden, kann ein Programm aus Betriebssystem-Funktionen und Struktur-Anweisungen (Schleifen, Verzweigungen) bestehen. Das Betriebssystem sorgt bei der Ausführung des Programms dafür, dass die Betriebssystem-Funktionen in passende CPU- oder BIOS-Befehle und Treiber-Funktionsaufrufe umgesetzt werden. Programme können, wenn es das Betriebssystem zulässt, aber auch direkte CPU- oder BIOS-Befehle enthalten. Einfache Programme können sogar nur aus CPU- oder BIOS-Befehlen bestehen.
3.1.5 Die Rolle des Arbeitsspeichers Programme bestehen also aus einzelnen Anweisungen. Diese Anweisungen sind zusammenhängend in Form einer Datei auf einem Speichermedium wie z. B. der Festplatte gespeichert. Eine Datei ist eine zusammengehörige Folge von Daten, die vom Betriebssystem als Ganzes eingelesen und verwendet werden kann. Im Falle eines Programms besteht eine Datei aus mehreren einzelnen Anweisungen. Solche Dateien besitzen unter Windows die Endung .exe, was für „executable“, also „ausführbar“ steht. Linux verwendet keine Dateiendungen für ausführbare Dateien, sondern erkennt den Typ der Daten an Informationen, die versteckt in der Datei gespeichert werden. Wenn Sie eine Programmdatei ausführen (in Windows z. B. über einen Doppelklick auf diese Datei oder über die Auswahl der Datei über das Startmenü), wird die Datei zunächst als Ganzes von der Festplatte in den Arbeitsspeicher gelesen. Windows erkennt an der Dateiendung,
:LH DUEHLWHW HLQ &RPSXWHU"
Schneller Speicher für Programme
127
Sandini Bib
dass es sich um eine ausführbare Datei handelt, Linux erkennt dies an der Datei selbst. Das Betriebssystem interpretiert die gespeicherten Daten also als Programm-Anweisungen, liest diese einzeln aus dem Arbeitsspeicher und führt Anweisung für Anweisung aus.
ausführbare Datei
Arbeitsspeicher
0010010100100110 0000011011000101 0100101001010000 0100100101001010 1110101001010010 0011010100110101 0110101000010010 0101010010100101
0010010100100110 0000011011000101 0100101001010000 0100100101001010 1110101001010010 0011010100110101 0110101000010010 0101010010100101
0010010100100110
CPU
Abbildung 3.4: Eine ausführbare Datei wird in den Arbeitsspeicher geladen und dann Anweisung für Anweisung von der CPU ausgeführt.
Dass die Datei dazu in den Arbeitsspeicher des Computers geladen wird, hat einen Grund: Fast alle Programme werden nicht einfach nur von oben nach unten abgearbeitet, sondern enthalten an vielen Stellen Rücksprünge zu vorhergehenden oder Sprünge zu später folgenden Anweisungen. Damit erreicht man beim Programmieren, dass bestimmte Programmteile mehrfach wiederholt und andere bedingungsabhängig ausgeführt werden können. Die dazu verwendeten Techniken werden in Kapitel 5 behandelt. Würde die CPU die einzelnen Anweisungen immer wieder von der im Vergleich zum Arbeitsspeicher sehr langsamen Festplatte lesen müssen, würden Programme nur sehr schleppend ausgeführt werden. Um die Ausführung zu beschleunigen, wird das gesamte Programm vor der Ausführung also in den Arbeitsspeicher geladen. Speichern von Daten
128
Der Arbeitsspeicher wird aber nicht nur zur Speicherung ausführbarer Programme verwendet. Viele Schritte zur Verarbeitung eingegebener Daten sind so komplex, dass ein Programm Zwischenergebnisse berechnen muss, die in späteren Anweisungen weiterverwendet werden. Diese
%DVLVZLVVHQ
Sandini Bib
Zwischenergebnisse werden dazu einfach im Arbeitsspeicher abgelegt. Viele Programme arbeiten auch mit variablen Daten, die beim Start des Programms oder bei der Ausführung bestimmter Aktionen dynamisch ermittelt werden. Zur Weiterverarbeitung dieser variablen Daten werden diese ebenfalls in den Arbeitsspeicher geschrieben. Eine weitere (aber nicht die letzte) Verwendung des Arbeitsspeichers ist das Einlesen von Dateien, damit diese schneller verarbeitet werden können, als wenn die Daten jeweils von der Festplatte gelesen werden. Programme nutzen dazu Variablen, wie Sie dies auch bereits in den Beispielen des vorherigen Kapitels gemacht haben. In Kapitel 4 erfahren Sie mehr dazu.
3.2
Wie werden Programme und Daten gespeichert?
Ein wichtiger Schlüssel zum Verständnis von Programmen und des Computers ist das Wissen, wie Programme und Daten gespeichert werden. Prinzipiell kommen Sie bei der Programmierung zwar eigentlich nie in Kontakt mit den Speichertechniken, die der jeweilige Computer verwendet. Begriffe wie Bit und Byte gehören aber zum allgemeinen Wissen und sollten von Programmierern schon verstanden werden. Auch die Kenntnis der Verwaltung von Zahlen und Zeichen im Speicher oder auf einem Speichermedium ist bei der Programmierung sehr häufig hilfreich und manchmal sogar notwendig. Ein Begriff wie Unicode taucht auch später noch häufiger auf. Wenn Sie mit einem Programm wie beispielsweise einer Textverarbeitung arbeiten, speichern Sie Ihre Arbeit normalerweise in eine Datei, sodass Sie diese zu einem späteren Zeitpunkt wieder verwenden können. Während der Arbeit verwaltet ein Programm seine Daten im Arbeitsspeicher. Die folgenden Abschnitte erläutern nun, in welcher Form dies erfolgt.
3.2.1 Bits und Bytes Auf allen persistenten3 Speichermedien werden Daten in Form von Dateien gespeichert. Eine Datei ist eine zusammenhängende Folge von Daten und besteht aus einzelnen Bytes. Aber auch im Arbeitsspeicher werden Daten in Form einzelner oder zusammenhängender Bytes verwaltet.
3.
persistent = dauerhaft
:LH ZHUGHQ 3URJUDPPH XQG 'DWHQ JHVSHLFKHUW"
129
Sandini Bib
Ein Byte ist die kleinste Speichereinheit im Computer und besteht aus acht Bits. Ein Bit kann nur den Zustand 0 oder 1 annehmen. Auf der niedrigsten Ebene rechnet ein Computer immer mit den beiden Zuständen Eingeschaltet (1) und Ausgeschaltet (0). Das liegt daran, dass die CPU und der Arbeitsspeicher im Prinzip aus vielen kleinen elektronischen Schaltern bestehen, die nichts weiter können, als Strom ein- und auszuschalten. Wenn der Rechner also Daten lesen, verarbeiten und ausgeben will, muss er die Daten irgendwie so verpacken, dass diese mit den beiden Zuständen 0 und 1 dargestellt werden können. Acht Bits sind ein Byte
Dass jeweils acht Bit zu einem Byte zusammengefasst werden, hat den Grund, dass das Verwalten einzelner Bits für Programmierer zu kompliziert sein würde. Die Verwaltung eines Byte ist dagegen wesentlich einfacher. Dass dazu genau acht Bits verwendet werden, hängt damit zusammen, dass acht Bits im häufig zur Darstellung von Zahlen verwendeten Hexadezimalsystem (Zahlensystem mit der Basis 16) sehr einfach dargestellt werden können. Ein Byte besteht also aus acht Bits, die jeweils den Zustand 0 (ausgeschaltet) oder 1 (eingeschaltet) annehmen können. Ein Programm kann nur ganze Bytes speichern, keine einzelnen Bits (obwohl es Möglichkeiten gibt, die einzelnen Bits eines Bytes zu setzen und abzufragen, aber das ist eine spezielle Programmiertechnik). Um Daten nun in einzelnen Bytes speichern zu können, müssen diese transformiert (umgewandelt) werden.
3.2.2 Zahlendarstellung im Computer Für Zahlen ist die Transformation in einzelne Bytes relativ unproblematisch. Jede (dezimale) Zahl kann in eine duale Zahl umgerechnet werden. Eine duale Zahl wird mit den Ziffern 0 und 1 dargestellt. Die dezimale Zahl 3 kann z. B. dual mit 00000011 dargestellt werden. Dateien und der Arbeitsspeicher speichern Daten ausschließlich mit den Zuständen 0 und 1. In einer Datei steht eine 1 für eine gesetzte Speicherstelle, eine 0 für eine nicht gesetzte. Auf einer Festplatte bedeutet dies, dass der jeweilige Speicherbereich entweder magnetisiert ist oder nicht. Auf einer CD wird eine 1 über ein (über einen CD-Brenner gebranntes) „Loch“ in der Oberfläche realisiert. Der Arbeitsspeicher besteht im Prinzip aus einer Vielzahl einzelner elektronischer Schalter (Transistoren), die entweder ein- oder ausgeschaltet sein können, also wieder die Zustände 1 und 0 annehmen können. Für Programmierer ist die Umrechnung dezimal dargestellter Zahlen in Dualzahlen nur in bestimmten Fällen wichtig. Programmiersprachen ermöglichen normalerweise immer auch die Verwendung dezimal dar-
130
%DVLVZLVVHQ
Sandini Bib
gestellter Zahlen, die bei der Übersetzung des Programms automatisch umgerechnet werden. Um aber auch spezifische Programmier-Situationen zu beherrschen, bei denen dual dargestellte Zahlen verwendet werden, sollten Sie verstehen, wie die Umrechnung erfolgt. Umrechnung vom Dezimal- in das Dualsystem Das dezimale Zahlensystem, mit dem wir normalerweise rechnen, basiert auf der Zahl 10. Mathematisch betrachtet bedeutet die Zahl 123 beispielsweise (3 * 100) + (2* 101) + (1 * 102), also (3 * 1) + (2 * 10) + (3 * 100). Die rechte Ziffer besitzt immer den Exponenten 0, die Ziffer daneben den Exponenten 1 usw. Natürlich rechnet wohl kaum ein Mensch bewusst eine im Dezimalsystem dargestellte Zahl in dieser Form um, wir kennen die Bedeutung der einzelnen Ziffern ja recht genau. Was im menschlichen Gehirn abläuft, ist aber eigentlich nichts anderes als die dargestellte mathematische Umrechnung. Wenn Sie nun eine Dezimalzahl in eine duale Zahl umrechnen wollen, müssen Sie die Wertigkeit der einzelnen Ziffern einer dualen Zahl berücksichtigen. Die äußerst rechte Ziffer hat die Wertigkeit 20 (also 1 im Dezimalsystem, weil eine Zahl mit dem Exponenten 0 immer 1 ergibt), die von rechts aus betrachtet zweite Ziffer besitzt die Wertigkeit 21 usw. Um eine duale Zahl in eine Dezimalzahl umzurechnen, multiplizieren Sie einfach die Wertigkeit mit der Ziffer. Steht an der rechten Ziffer eine 1, bedeutet dies so viel wie 1 * 20. Auf diese Weise addieren Sie die einzelnen Ziffern miteinander. Die duale Zahl 00001110 kann also z. B. so umgerechnet werden: 0*20 + 1*21 + 1*22 + 1*23 + 0*24 + 0*25 + 0*26 + 0*27 + 0*28 = 0*1 + 1*2 + 1*4 + 1*8 + 0*16 + 0*32 + 0*64 + 0*128 =0+2+4+8+0+0+0+0 = 1410 Um duale von dezimalen Zahlen unterscheiden zu können, werden diese häufig mit einer tief gestellten 10 (Zahl im Dezimalsystem) bzw. einer tief gestellten 2 (Zahl im Dualsystem) gekennzeichnet: 12310, 11110112. Wenn Sie von einer dezimalen Zahl in eine duale Zahl zurückrechnen wollen (was Sie eigentlich bei der Programmierung nur äußerst selten müssen), beginnen Sie mit der höchstmöglichen Wertigkeit. Soll z. B. die dezimale Zahl 195 umgerechnet werden, überprüfen Sie zuerst, ob die höchste Wertigkeit (in einem Byte ist das 27 = 128) in diese Zahl hineinpasst. Ist das der Fall, setzen Sie das äußerst linke Bit und ziehen die Wertigkeit von der Zahl ab:
:LH ZHUGHQ 3URJUDPPH XQG 'DWHQ JHVSHLFKHUW"
131
Sandini Bib
19510 = 10000002 Rest 6710
Dann überprüfen Sie für die nächste Wertigkeit (26 = 64), ob diese vom Rest abgezogen werden kann: 19510 = 11000002 Rest 310
So gehen Sie weiter vor, bis kein Rest mehr übrig bleibt: 19510 = 110000112 Rest 010
Eigentlich ist diese Umrechnung doch ziemlich einfach, oder? Wie werden ganze Zahlen und Zahlen mit Dezimalstellen gespeichert? Ganze Zahlen
Einfache ganze Zahlen ohne Vorzeichen werden in ihrer dualen Form in einzelnen Bytes gespeichert. Ein Byte kann die Zahlen 0 bis 255 verwalten. Ist die zu speichernde Zahl größer als 255, werden mehrere Bytes zu einer Speichereinheit zusammengefasst. Zwei Bytes können schon ganze Zahlen im Bereich von 0 bis 65535 speichern. Bei negativen und dezimalen Zahlen wird das Ganze etwas komplizierter. Bei negativen Zahlen verwenden die meisten Programme das linke Bit zur Kennzeichnung, ob es sich um eine negative oder positive Zahl handelt. Ein Byte, das Zahlen mit Vorzeichen verwaltet, kann den Wertebereich -128 bis +127 verwalten, zwei Byte verwalten den Bereich -32768 bis +32767. Dieser etwas eigenartige Bereich, bei dem der Absolutwert4 der negativen Zahl größer ist als der Absolutwert der positiven Zahl, ergibt sich aus speziellen Speichertechniken, mit denen verhindert wird, dass die Zahl 0 zweimal verwaltet werden kann (als -0 und +0). Der mögliche Zahlenbereich wird damit optimiert.
Festkomma- und Fließkommazahlen
Bei Dezimalzahlen werden solche mit Festkomma und mit Fließkomma unterschieden. Festkommazahlen werden genau wie ganze Zahlen gespeichert. Beim Interpretieren der gespeicherten Daten setzt das Programm das Komma an eine festgelegte Stelle, beispielsweise vor die vierte Ziffer von rechts. Die gespeicherte Zahl 1234 wird in diesem Beispiel also als 1,234 interpretiert. Komplizierter ist das Speichern von Zahlen mit möglichst großer Anzahl an Dezimalstellen. Bei diesen Zahlen wird – einfach ausgedrückt – ein Teil der Bitfolge dazu verwendet, festzulegen, an welcher Stelle sich das Dezimaltrennzeichen befindet. Daraus ergibt sich, dass nur Zahlen mit sehr kleinem Absolutwert viele Ziffern nach dem Komma verwalten können. In der Regel werden dabei maximal sieben oder maximal 16 Ziffern verwaltet (je nach Größe des verwendeten Speicherbereichs). Zahlen mit einem großen Absolutwert 4.
132
Als Absolutwert wird der Wert einer Zahl ohne Vorzeichen bezeichnet
%DVLVZLVVHQ
Sandini Bib
können nicht mehr die maximale Anzahl an Nachkommaziffern verwalten. Sehr große (bzw. bei negativen Zahlen sehr kleine) Zahlen können nur noch mit einer oder zwei Nachkomma-Ziffern verwaltet werden. Da das Komma bei dieser Speichertechnik je nach Größe der Zahl quasi fließt, werden diese Zahlen als Fließkommazahlen bezeichnet. Die Größe der Zahl und die Anzahl der Zahlen und der möglichen Nachkommaziffern hängt natürlich von der verwendeten Anzahl Bytes ab. Die meisten Programmiersprachen speichern Fließkommazahlen in vier oder acht Byte. Vier Byte ermöglichen normalerweise Zahlen im Bereich von ±1,5 * 10-45 bis ±3,4 * 1038 mit bis zu sieben Dezimalstellen, acht Byte ermöglichen Zahlen im Bereich von ±5,0 * 10-324 bis ±1,7 * 10308. Fragen Sie mich bloß nicht, wie diese Bereiche zustande kommen, die Logik der Speicherung ist schon recht komplex. Wichtig ist aber, dass ein Vier-Byte-Speicherbereich bei fünf Ziffern vor dem Komma schon nur noch zwei Ziffern nach dem Komma verwalten kann. Überzählige Ziffern werden in der Regel einfach abgeschnitten (einige Programmiersprachen runden die Zahl auch passend auf oder ab). Die Genauigkeit dezimaler Zahlen ist also bei der Speicherung eingeschränkt. Manchmal sind die errechneten Werte nicht so genau, wie sie eigentlich sein sollten. Beim Rechnen mit Dezimalzahlen muss ein Programmierer immer auf die verfügbare Genauigkeit achten.
Genauigkeit
Viele Programmiersprachen kennen aber auch Zahlen mit wesentlich mehr Dezimalziffern (in der Regel 28), über die hochgenaue Berechnungen ausgeführt werden können. Solche Berechnungen werden beispielsweise im wissenschaftlichen und finanzmathematischen Bereich eingesetzt. Falls Sie jetzt besorgt sind, dass Sie die Transformation von Zahlen in das Dualsystem und zurück zum dezimalen System durchführen müssen, kann ich Ihre Bedenken zerstreuen. Sie haben mit der Transformation der zu speichernden Zahlen normalerweise nichts zu tun. Diese Arbeit übernimmt das Betriebssystem oder die Programmiersprache. Sie teilen dem Computer lediglich (über bei der Programmierung verwendete Datentypen) mit, welchen Zahlenbereich Sie speichern wollen. Das Hexadezimalsystem Das hexadezimale Zahlensystem wird zwar nur selten von Programmiersprachen, aber häufig von Programmierern zur Darstellung von Zahlen verwendet. Wenn Sie in HTML-Dokumenten z. B. eine spezielle Farbe verwenden wollen, müssen Sie diese als Hexadezimalzahl angeben.
:LH ZHUGHQ 3URJUDPPH XQG 'DWHQ JHVSHLFKHUW"
133
Sandini Bib
Das Hexadezimalsystem arbeitet mit der Basis 16. Zur Darstellung der Zahlen 10 bis 15 werden die Buchstaben A bis F verwendet. Die hexadezimale Zahl 0F16 steht zum Beispiel für die dezimale Zahl 15, die hexadezimale Zahl 1016 steht für die dezimale Zahl 16. Die Umrechnung von dezimalen in hexadezimale Zahlen erfolgt ähnlich wie bei den dualen Zahlen, nur eben mit der Basis 16. Die Umrechnung soll deshalb hier kein Thema sein. Ein ganz normaler Taschenrechner hilft Ihnen dabei. Nibble
Interessant ist aber, dass Zahlen im Dualsystem sehr einfach mit Hexadezimalzahlen dargestellt werden können. Dazu wird ein Byte in zwei so genannte Nibble von je vier Bit aufgeteilt. Jedes Nibble kann von einer hexadezimalen Ziffer dargestellt werden, weil der Wertebereich eines Nibble von 0 bis 15 reicht. Die Zahl 1111|11112 (zum besseren Verständnis habe ich die einzelnen Nibble mit einem Strich getrennt dargestellt) entspricht z. B. der Zahl F|F 16, die Zahl 0011|11002 entspricht der Zahl 3|C16.
3.2.3 Wie werden Texte gespeichert? Texte, die im Arbeitsspeicher oder in einer Datei gespeichert werden sollen, müssen ähnlich Zahlen auch in einzelnen zusammenhängenden Bytes dargestellt werden. Wenn Sie beispielsweise in einer Textverarbeitung eine Zeichentaste betätigen, muss das Betriebssystem bzw. das Programm dafür sorgen, dass das gewählte Zeichen im Arbeitsspeicher gespeichert und als korrektes Zeichen auf dem Bildschirm ausgegeben wird. Die dazu notwendige Transformation übernimmt wieder das Betriebssystem oder die Programmiersprache. Sie brauchen sich also eigentlich nicht darum zu kümmern. Beim Programmieren werden Sie aber immer wieder mit dem für einzelne Zeichen verwendeten Zahlencode konfrontiert, z. B. dann, wenn Sie Texte sortieren oder einzelne Zeichen in Form ihres Zahlencodes in einen Programmquelltext eingeben wollen (bzw. müssen). Sie sollten also wissen, wie Zeichen transformiert werden. Ein-ByteSpeicherung
134
Im Prinzip ist die Transformation von Texten in Bytes sehr einfach. In einem Byte können, wie Sie ja bereits wissen, die Zahlen 0 bis 255 gespeichert werden. Ältere Systeme assoziieren ein Zeichen mit einer bestimmten in einem Byte speicherbaren Zahl. Das kleine a besitzt beispielsweise meist den Zahlencode 97. Auf diese Weise kann ein Byte 256 Zeichen verwalten. Welche Zahl welches Zeichen darstellt, ist im Verlauf der Entwicklung der ersten Computer von verschiedenen Instituten genormt worden und wird in Tabellen festgelegt, die für die einzelnen Sprachregionen meist sehr unterschiedlich aussehen. Da in den älteren Tabellen (die nur ein Byte zur Speicherung verwenden) lediglich
%DVLVZLVVHQ
Sandini Bib
256 Zeichen möglich sind, können nicht annähernd alle Zeichen der Welt-Sprachen in einer Tabelle dargestellt werden. Deshalb existieren für die verschiedenen Sprachregionen dieser Welt unterschiedliche (logische) Tabellen, die die möglichen Zahlencodes mit oft ganz anderen Zeichen assoziieren. Die bekannteste Tabelle ist die ASCII5-Tabelle, die in vielen älteren Betriebssystemen wie DOS und Windows 3.x eingesetzt wird. Diese Tabelle wird auch manchmal als ANSI6-Tabelle bezeichnet. Der Unterschied zwischen ASCII und ANSI ist nicht ganz klar oder im Lauf der Zeit verschwommen. Für einige Leute ist eine ASCII-Tabelle eine Tabelle, die nur Zeichen im Bereich von 0 bis 127 kennt (was auch die ursprüngliche Art der Speicherung war, denn dafür wurden nur sieben Bit pro Zeichen benötigt, was früher teuren Speicherplatz sparte). Alle Tabellen, die auch die Zeichen 128 bis 255 speichern, werden von diesen Leuten als ANSI-Tabelle bezeichnet. Für viele ist eine ASCII-Tabelle aber auch eine, die 256 Zeichen kennt. Ich verwende hier einfach den allgemeineren Begriff ASCII.
ASCII und ANSI
ASCII-Tabellen liegen in mehreren, sprachspezifischen Varianten vor. Die einzelnen Varianten wurden früher einfach mit Nummern gekennzeichnet. Die alte ASCII-Tabelle für die USA besitzt z. B. die Nummer 437, die für Westeuropa die Nummer 1250. Modernere ASCII-Tabellen sind von der „International Organization for Standardization“ (ISO) genormt, deshalb wesentlich besser standardisiert und werden etwas anders benannt. In westlichen Ländern wird z. B. fast ausschließlich die Tabelle ISO-8859-1 eingesetzt. In dem Zusammenhang sprechen Programmierer auch häufig von Zeichensätzen. Der Zeichensatz ISO-Latin-1 definiert beispielsweise die Zeichen, die in westlichen Ländern (in Ländern mit lateinischer Sprachabstammung) verwendet werden. Richtig klar wird der Zusammenhang zwischen dem Zeichensatz ISO-Latin-1 und der Zeichentabelle ISO-8859-1 allerdings nicht. Einige Leute meinen, dass es sich dabei um dasselbe handelt, andere denken, dass ISOLatin-1 nur definiert, welche Zeichen enthalten sein müssen, aber nicht, welchen Zeichencode diese besitzen (das macht dann ISO-88591). Im Allgemeinen ist mit beiden Begriffen aber dasselbe gemeint. Was Sie auch daran erkennen, das beispielsweise der Zeichensatz ISO-Latin-2 durch die ASCII-Tabelle ISO-8859-2 abgebildet wird.
ISO-Tabellen und Zeichensätze
Allen ASCII-Varianten (die in der modernen Variante nach Regionen wie Westeuropa/USA und Osteuropa unterteilt sind) ist gemeinsam, 5.
American Standard Code for Information Interchange
6.
American National Standards Institute, ein Institut zur Standardisierung von allgemein verwendeten Techniken ähnlich dem deutschen DIN-Institut (Deutsche Industrie Norm).
:LH ZHUGHQ 3URJUDPPH XQG 'DWHQ JHVSHLFKHUW"
135
Sandini Bib
dass die ersten 127 Zeichen immer dieselben sind. Erst ab dem Zeichen 128 unterscheiden sich die verschiedenen regionsspezifischen Varianten. Daneben ist allen Tabellen gemeinsam, dass die Zeichen bis zum Zeichen 31 Steuerzeichen sind. Viele dieser Steuerzeichen wurden früher unter textbasierten Betriebssystemen zur Steuerung des Druckers bzw. des Bildschirms verwendet, besitzen heute aber keine Bedeutung mehr. Einige Steuerzeichen, wie die Zeichen 10 (Zeilenvorschub), 13 (Wagenrücklauf) und 9 (Tabulator) werden aber heute immer noch verwendet. Wenn Sie z. B. mit Word einen Text schreiben und die ReturnTaste betätigen um eine neue Zeile zu beginnen, fügen Sie im Prinzip das Zeichen 10 in Ihren Text ein.7 Hervorragende Erläuterungen zu Zeichensätzen und Zeichentabellen und eine Übersicht über die zurzeit gängigen ISO-8859-Tabellen finden Sie unter der Adresse czyborra.com/charsets/iso8859.html. Tabelle 3.1 listet einige ASCII-Zeichen der in westlichen Ländern verwendeten Tabelle ISO-8859-1 auf. Im Anhang finden Sie eine komplette ISO-8859-1-Tabelle. Auf anderen Betriebssystemen werden übrigens auch andere Tabellen verwendet. Ältere IBM-Betriebssysteme arbeiten beispielsweise mit der EBCDIC-Tabelle. Zeichen Tabulator
9
Zeilenvorschub
10
Wagenrücklauf
13
Leerzeichen
32
0
48
1
49
...
...
9
57
A
65
B
66
...
...
Z
90
a
97
7.
136
Dezimalcode
Dass nur das Zeichen 10 verwendet wird, ist nicht ganz korrekt. Die meisten Programme verwenden leider immer noch eine Kombination der Zeichen 10 (Line Feed = Zeilenvorschub) und 13 (Carriage Return = Wagenrücklauf) zur Darstellung einer neuen Zeile. Diese (historisch gewachsene) Eigenart resultiert aus dem Verhalten einer Schreibmaschine: Bei dieser müssen Sie zum Beginnen einer neuen Zeile den Wagen zurückschieben und die Zeilenvorschubtaste betätigen.
%DVLVZLVVHQ
Sandini Bib
Zeichen
Dezimalcode
b
98
...
...
z
122
Tabelle 3.1: Auszug aus einer ASCII-Tabelle
ASCII-Tabellen werden auf modernen Systemen nur noch eingeschränkt eingesetzt. Bedeutung besitzen diese Tabellen aber noch im Internet. Hier werden Zeichendaten aufgrund der Beschränkungen von an der Datenübertragung beteiligten Systemen noch in Form von einzelnen Bytes übertragen. Internetprogramme verwenden in westlichen Ländern aber die Tabelle ISO-8859-1, weswegen normalerweise keine Fehlinterpretationen von Zeichen vorkommen (was früher, unter den alten ASCII-Tabellen noch sehr häufig vorkam). Moderne Betriebssysteme verwenden meist keine ASCII-Tabellen, sondern speichern ein Zeichen in zwei Byte, womit 65535 verschiedene Zeichen möglich sind. Dieser so genannte Unicode kann damit bis auf wenige Ausnahmen alle Sprachen dieser Welt abbilden. Ein großes Problem der ASCII-Tabellen, nämlich das schwierige Austauschen von Texten zwischen verschiedenen Regionen dieser Welt, entfällt damit weitgehend. Da die ersten 255 Zeichen der Unicode-Tabelle der ASCIITabelle ISO-8859-1 entsprechen, ist Unicode (wenigstens in westlichen Ländern) kompatibel zu ASCII. Das ist für uns Programmierer ziemlich wichtig zu wissen, denn so können wir die Standard-Zeichencodes anwenden, ohne uns Gedanken um die Art der Speicherung machen zu müssen. Der Zeichencode 13 steht immer für einen Wagenrücklauf, der Zeichencode 10 immer für einen Zeilenumbruch, das „a“ besitzt immer den Code 97.
Unicode
Eine sehr gute Beschreibung des Unicode-Standards finden Sie unter der Adresse czyborra.com/unicode/characters.html. Textdateien Einfache Textdateien speichern heute zumeist noch einzelne ASCIIZeichen, weil viele Betriebssysteme (wie Windows 95, 98 und Me) keinen oder nur einen eingeschränkten Unicode-Support bieten. Auf modernen Betriebssystemen werden Textdateien aber auch optional in Unicode-Form gespeichert. Textdateien ist gemeinsam, dass die einzelnen Zeilen entweder durch eine Kombination der Zeichen 13 (Carriage Return = Wagenrücklauf) und 10 (Line Feed = Zeilenvorschub) oder nur durch das Zeichen 10 abgeschlossen werden. Welche Technik verwendet wird, hängt vom Betriebssystem ab. Linux und Unix verwenden bei-
:LH ZHUGHQ 3URJUDPPH XQG 'DWHQ JHVSHLFKHUW"
137
Sandini Bib
spielsweise nur das Zeichen 10, Windows die Zeichen 13 und 10. Die Kombination der Zeichen 13 und 10 wurde übrigens von der alten Schreibmaschinen-Technik übernommen. Was immer das auch für einen Sinn hatte ... Formatierungen in Textdokumenten Einfache Textdateien speichern lediglich die einzelnen Zeichen des Textes. Viele Text-Dokumente verwalten daneben aber auch Formatierungen, wie z. B. die Schriftart, die Schriftgröße und die Schriftauszeichnung (fett, kursiv etc.). Diese Formatierungen werden neben den Bytes für die einzelnen Zeichen in Form separater Bytes gespeichert. Das Ganze kann natürlich sehr kompliziert werden, ist für Programmierer aber eigentlich nicht besonders wichtig, da es Programme, Tools oder Komponenten gibt, über die solche Dateien sehr einfach gelesen und geschrieben werden können. Oft ist das Schreiben und Lesen lediglich über die jeweilige Textverarbeitung möglich, einige Hersteller stellen aber auch Funktionen zur Verfügung, über die ein Programm solche Text-Dokumente verarbeiten kann. Für das relativ allgemeine RTF-Format (bei dem die Formatierungen in Form spezieller Befehle in Textform gespeichert sind) besitzen die meisten Programmiersprachen aber auch eigene Funktionen zum Lesen, Verarbeiten und Speichern.
3.2.4 Kombinierte Dateien Dateien speichern oft nicht nur Texte oder Zahlen, sondern eine Kombination von beidem. Stellen Sie sich eine Datei vor, die mehrere Adressen Ihrer Freunde speichert. Neben dem Namen jeder Person werden vielleicht noch die Straße, der Ort, die Telefonnummer und die Postleitzahl gespeichert. Eine solche Datei würde aus einzelnen Adressen bestehen, die sich aus den Teilen einer Adresse (den Feldern) zusammensetzen. Die einzelnen Felder speichern meist Texte, bei der Postleitzahl aber z. B. eine Zahl.
3.2.5 Binäre Daten: Speichern von Bildern, Musikstücken und anderen speziellen Daten Neben Programmcode, Zahlen und Zeichen werden natürlich noch andere Daten, wie z. B. Bilder oder Musik, in Dateien gespeichert. Ich will nicht näher darauf eingehen, wie diese Daten tatsächlich abgelegt werden, weil Programmierer eigentlich nie direkt mit der Speichertechnik konfrontiert werden. Ich erläutere aber die Grundlagen, damit Sie verstehen, was binäre Daten sind.
138
%DVLVZLVVHQ
Sandini Bib
Gespeicherte Texte setzen sich aus einzelnen Zeichen und Format-Informationen zusammen. Ein Text besteht also aus vielen einzelnen Bytes, die jeweils ein Zeichen darstellen, und aus dazugehörigen Bytes für die Formatierungen. Im Prinzip kann man sagen, dass ein Text eine Folge einzelner Einheiten ist. Jede Einheit verwaltet einzelne Zeichen und deren Formatierung. Bilder, Musikstücke, Videos und ähnliche Dateien bestehen hingegen nicht aus einzelnen Einheiten, sondern stellen im Prinzip eine einzige große Einheit dar. Deshalb werden bei diesen Dateien keine Bytes, sondern einzelne Bits verwaltet. Bei Bilddateien legt man z. B. fest, dass 1, 8, 15 oder 16 Bits (oder mehr) einen Bildpunkt darstellen. Ein Bild ist meist aus sehr vielen einzelnen Punkten zusammengesetzt. Eine BildSpeichereinheit legt die Farbe eines Bildpunktes fest. Wird ein Bildpunkt in einem Bit verwaltet, können nur die Farben Schwarz und Weiß dargestellt werden, bei 16 Bits pro Bildpunkt sind schon 65535 Farben möglich. Allgemeine Informationen zum Bild, wie beispielsweise Angaben zur Farbtiefe, zur Breite und Höhe und zu einer eventuellen Komprimierung werden in den ersten Bytes der Bilddatei, im so genannten Header gespeichert. Solche Daten, die Informationen in einzelnen Bits speichern, werden im Allgemeinen als binäre Daten bezeichnet, Dateien mit solchen Daten heißen binäre Dateien. Ein Programm, das Bilddateien speichert und auswertet, muss nun dafür sorgen, dass immer ganze Bytes gespeichert werden, weil dies vom Betriebssystem verlangt wird (ein Byte ist schließlich die kleinste Speichereinheit). Eine Bilddatei mit 1-Bit-Farbtiefe, die nur aus vier einzelnen Bildpunkten besteht (solche Bilddateien gibt es natürlich nicht, aber dieses Beispiel veranschaulicht die Thematik besser als eine Bilddatei mit 10.000 Bildpunkten), würde also für die Bildpunkte ein ganzes Byte speichern, wobei die oberen vier Bits einfach unbenutzt bleiben würden. Um zu erkennen, welches Format eine Bilddatei besitzt, speichert das Programm am Anfang der Datei, im so genannten Header, zusätzlich noch Informationen über die Farbtiefe und die Größe des Bildes. An Hand dieses Headers kann ein Programm beim Lesen der Datei ermitteln, wie die einzelnen Bits der Bilddaten ausgewertet werden müssen.
Datei-Header
Dieses bei Bilddateien verwendete Prinzip wird auch bei anderen Dateien, wie Musik- und Videodateien, angewendet. Natürlich ist das Speichern dieser Daten in der Praxis wesentlich komplexer, als ich es hier darstelle. Schließlich existieren die verschiedensten Formate zur Speicherung von Bild-, Musik- und Videodaten (bei Bildern z. B. in Form der
:LH ZHUGHQ 3URJUDPPH XQG 'DWHQ JHVSHLFKHUW"
139
Sandini Bib
Formate Bitmap, GIF, TIFF, JPEG etc.). Jedes Format verwaltet die Daten anders, einige Formate komprimieren die gespeicherten Daten sogar, sodass die Dateien insgesamt kleiner werden.
3.2.6 Speicheradressen und Variablen Der Arbeitsspeicher besteht aus einer meist sehr großen Anzahl einzelner Bytes. Wenn Daten im Arbeitsspeicher gespeichert werden, belegen diese immer eine bestimmte Anzahl zusammenhängender Bytes. ASCIITextdaten mit zwei Zeilen, die jeweils drei Zeichen enthalten, belegen beispielsweise unter Windows acht und unter Linux sieben Byte im Arbeitsspeicher. Eine Variable, die einen Wert zwischen 0 und 255 speichern soll, belegt dagegen nur ein Byte. Damit die so gespeicherten Daten vom verarbeitenden Programm wiedergefunden werden, wird der Arbeitsspeicher adressiert. Dabei werden einfach die Bytes gezählt. Ein Programm, das einen 2-Byte-Zahlwert an der Adresse 1000 abgelegt hat (also am Byte mit der Nummer 1000), kann den Speicher an Hand dieser Adresse wieder auslesen. Speicheradressen werden übrigens üblicherweise in hexadezimaler Form angegeben. Variablen
Bei der modernen Programmierung müssen Sie eigentlich nie die Adressen kennen, an denen Ihr Programm Daten verwaltet (früher war das allerdings anders, da musste ein Programmierer die Adressen sehr wohl kennen). Programmiersprachen stellen Ihnen, wie Sie ja bereits wissen, Variablen zur Verfügung. Wenn Sie eine Variable deklarieren, sorgt der Compiler oder Interpreter, der das Programm in Maschinencode übersetzt (siehe ab Seite 143), automatisch dafür, dass bei der Ausführung des Programms ein passender Speicherbereich reserviert wird. Beim Übersetzen des Programms ersetzt der Compiler bzw. Interpreter alle Stellen, an denen Sie die Variable verwenden, durch die entsprechende Speicheradresse. Sie müssen sich also nicht weiter darum kümmern.
Direkter
Einige Programmiersprachen wie C und C++ erlauben aber auch den direkten Zugriff auf Speicheradressen, mit der Begründung, dass damit in manchen Situationen schnellere Programme geschrieben werden können. Da ein Programmierer dadurch aber auch sehr viele komplizierte Fehler verursachen kann (wenn das Programm versehentlich auf einen Speicherbereich zugreift, der gar nicht für das Programm reserviert ist oder der andere Daten verwaltet als eigentlich erwartet), lassen moderne Programmiersprachen den direkten Zugriff normalerweise erst gar nicht mehr zu.
Speicherzugriff
140
%DVLVZLVVHQ
Sandini Bib
3.2.7 Wer liest und interpretiert die gespeicherten Daten? Das Betriebssystem speichert die Daten. Die Interpretation der gespeicherten Daten ist allerdings nicht Sache des Betriebssystems, sondern Aufgabe des Programms. Das Betriebssystem ermöglicht dem Programm lediglich, seine Daten (in Form von Dateien) auf ein Speichermedium oder im Arbeitsspeicher zu speichern und wieder einzulesen. Einfache Daten, wie Zahlen und Texte, werden vom Compiler bzw. Interpreter automatisch korrekt ausgewertet. Zum Lesen und Schreiben von Daten, die in speziellen Formaten vorliegen (z. B. Bilder), finden Sie oft in der Bibliothek der Programmiersprache passende Features. Falls diese nicht in der Programmiersprache vorhanden sind, können Sie in der Regel externe Komponenten benutzen, die Sie teilweise kaufen müssen, die aber auch häufig kostenlos im Internet zur Verfügung stehen (besonders für freie Programmiersprachen wie Java). Eine sehr häufige Aufgabe beim Programmieren, das Speichern von vielen verschiedenen zusammenhängenden Informationen, wie z. B. der Kunden-, Artikel- und Bestellinformationen in einem Verkaufsgeschäft, übernehmen so genannte Datenbanksysteme. Im Vergleich zum reinen Speichern von Dateien sind Programme, die Datenbanksysteme zum Speichern der Daten verwenden, für Programmierer meist sehr einfach zu erstellen und vor allen Dingen auch sehr schnell im Zugriff. Datenbanken behandelt das Buch in Kapitel 9.
3.3
Wie werden Programme geschrieben und für den Computer übersetzt?
3.3.1 Texteditoren, Programmiereditoren, Entwicklungsumgebungen Wie Programme heutzutage geschrieben werden, wissen Sie bereits. Sie haben ja schließlich schon eigene Programme entwickelt. Programme werden eigentlich immer zunächst als Quellcode in einer Textdatei geschrieben. Im einfachsten Fall nutzen Sie dazu einen Texteditor. Für viele Programmiersprachen können Sie aber auch Entwicklungsumgebungen nutzen, die das Programmieren über zahlreiche Features erheblich vereinfachen. Ein Zwischending sind Programmier-Editoren. Diese bieten nur grundlegende Features, wie beispielsweise die farbliche Hervorhebung spezieller Programmteile, die Verwaltung von Projekten und den Aufruf des Compilers. Spezielle Features wie Debugger und eine visuelle Formulargestaltung (wie bei Delphi/Kylix) fehlen diesen einfa-
:LH ZHUGHQ 3URJUDPPH JHVFKULHEHQ XQG IU GHQ &RPSXWHU EHUVHW]W"
141
Sandini Bib
chen (und meist kostenfreien) Editoren normalerweise. Im Internet finden Sie für die verschiedenen Sprachen eine Vielzahl an teilweise sehr guten Programmiereditoren. Einige Vertreter dieser Spezies sind sogar in der Lage, verschiedene Programmiersprachen zu erkennen und einen Quelltext mit korrekten farblichen Hervorhebungen darzustellen.
3.3.2 Maschinencode und Assembler Früher wurden Programme direkt in der Sprache geschrieben, die der Computer versteht: der Maschinensprache. Wie Sie ja bereits wissen, besteht diese Sprache aus einzelnen Befehlen, die aus Nullen und Einsen zusammengesetzt sind. Über diese Befehle können Sie in Maschinensprache Daten im Speicher ablegen, zwischen Speicherbereichen kopieren, Speicherbereiche addieren, multiplizieren, dividieren und so weiter. Die ersten Computer waren so einfach, dass der Prozessor (der damals noch aus Schaltrelais oder Schaltröhren bestand) nur sehr wenige Befehle verstand. Damals war ein Programmierer froh, wenn er es schaffte, eine einfache mathematische Berechnung zu programmieren, die dann vielleicht Daten von einer Lochkarte einlas (die natürlich auch binär kodiert waren) und das Ergebnis wieder auf einer Lochkarte ausgab. Damals war Maschinensprache noch übersichtlich, weil der Computer und die zu lösenden Probleme sehr einfach waren. Die ersten Maschinensprache-Programme wurden übrigens über elektrische Steckoder Schalttafeln „geschrieben“, die modernere Variante nutzte dann bereits Lochkarten (bei denen ein Loch eine 1 darstellte). Heute lässt wohl kein Computer mehr zu, Programme direkt in Maschinensprache zu entwickeln. Wenn Sie einige Monate Zeit haben, können Sie aber versuchen, das Format von ausführbaren Dateien zu erforschen, und diese mit den entsprechenden Maschinensprachebefehlen dann selbst erzeugen. Assembler
Die ersten echten Programmiersprachen waren so genannte Assemblersprachen. Eine Assemblersprache besteht aus Befehlen, die die einzelnen Maschinensprache-Befehl in einer für den Menschen leichter lesbaren Form darstellen, und einer Syntax zur Programmierung. Ein Assembler-Programm wird in einer Textdatei gespeichert. Damit das Programm ausgeführt werden kann, wird es von einem Assembler in Maschinencode übersetzt. Ein einfaches Assembler-Programm, das nichts anderes macht als in einer Konsolenanwendung den Bildschirm zu löschen, sieht beispielsweise so aus: mov cx, 2000 mov bx, 0B800h mov es, bx
142
%DVLVZLVVHQ
Sandini Bib
mov di, 0 mov ax, 0720h rep stosw
Assemblerprogramme sind sehr schwer zu verstehen, weil sie lediglich auf den Maschinensprachebefehlen einer CPU-Familie basieren und weil Sie dazu gute Kenntnisse des internen Aufbaus der CPU, des Arbeitsspeichers und anderer Computerbestandteile besitzen müssen. Assemblerbefehle sind aber wesentlich leichter zu lesen als Maschinencode. Zudem bieten viele Assembler auch schon erweiterte Features, wie das Entwickeln und Benutzen von Funktionen und sogar objektorientierte Programmierung. Heute werden nur noch sehr wenige Programme in einer Assemblersprache programmiert. Die Assemblerprogrammierung ist in Bezug auf die heutigen komplexen Probleme einfach viel zu aufwändig. Zwei Zahlen über ein Assemblerprogramm zu addieren ist einfach, eine Anwendung zu entwickeln, die Adressen in einer Datenbank verwaltet und die Suche nach diesen ermöglicht, ist nahezu unmöglich. Assemblerprogramme kommen teilweise noch in Computerspielen vor, die zwar grundsätzlich meist in den höheren Programmiersprachen C und C++ geschrieben werden. Besonders zeitkritische Routinen werden aber auch heute noch in einigen Fällen in einer Assemblersprache entwickelt. Virenprogrammierer werden Ihre Programme wohl auch noch in einigen Fällen in Assembler entwickeln. Assemblerprogramme werden, wenn sie korrekt programmiert sind, in der Regel etwas schneller ausgeführt als Programme, die in einer höheren Programmiersprache geschrieben wurden. Das liegt daran, dass Assemblerprogramme sehr direkt mit der CPU und dem Arbeitsspeicher arbeiten. Deshalb können Sie diese Programme in modernen Programmiersprachen auch häufig noch in höhere Programme einbinden. Aber selbst ganze Betriebssysteme wurden und werden nicht in einer Assemblersprache, sondern einer höheren, einfacher anzuwendenden Programmiersprache entwickelt. Bei Linux ist das z. B. die Sprache C (allerdings mit eingebetteten Assembler-Routinen).
3.3.3 Compiler Programme werden heute fast ausschließlich in einer höheren Programmiersprache geschrieben, die wesentlich einfacher anzuwenden ist als eine Assemblersprache und viel mehr Features bietet. Diese Programme werden meist von einem Compiler in Maschinencode übersetzt. Ein Compiler speichert die übersetzten Befehle (genau wie ein Assembler) in eine ausführbare Datei (Abbildung 3.5).
:LH ZHUGHQ 3URJUDPPH JHVFKULHEHQ XQG IU GHQ &RPSXWHU EHUVHW]W"
143
Sandini Bib
Quellcodedatei Write(”Zahl 1: ”); Readln(zahl1); Write(”Zahl 2”); readln(zahl2); Writeln(zahl1 + zahl2);
ausführbare Datei
Compiler
0010010100100110 0000011011000101 0100101001010000 0100100101001010 1110101001010010 0011010100110101 0110101000010010 0101010010100101 1110010100101001 1001010010100000 1101001100011111 1101001100011000 0011011000110101 1101101010011001 Abbildung 3.5: Kompilieren einer Quellcodedatei in eine ausführbare Datei
Die in der ausführbaren Datei gespeicherten Befehle können nun vom Betriebssystem oder direkt von der CPU ausgeführt werden, wenn die Datei gestartet wird. Für die Ausführung des Programms ist nur noch die ausführbare Datei notwendig. Der Anwender startet diese Datei (über die Betriebssystem-Oberfläche oder eine Konsole), die Datei wird in den Arbeitsspeicher geladen und Anweisung für Anweisung von der CPU ausgeführt.
3.3.4 CPU- und Betriebssystem-spezifische Programme Eine CPU besitzt einen bestimmten Satz an Befehlen. Diese Befehle sind bei unterschiedlichen CPU-Typen (z. B. Intel- und Sun-CPUs) teilweise komplett verschieden. Ein Programm, das für eine Intel-CPU entwickelt wurde, kann nicht auf einer Sun-CPU mit deren völlig anderen Befehlssatz ausgeführt werden. Um aber Programme, die für ältere Versionen einer CPU entwickelt wurden, auch auf neueren Versionen ausführen zu können, enthalten die neue Versionen immer den kompletten Befehlssatz der alten CPU. Dabei werden CPUs in Familien eingeteilt. X86
144
Eine wichtige Familie ist dabei die X86-Familie. Diese steht für alle Prozessoren, die auf der uralten 8086-CPU basieren (also vom 8086-Prozessor über den 80486 bis zum Pentium IV und den AMD-Prozessoren). Alle CPUs, die den ursprünglichen Befehlssatz besitzen, werden dieser
%DVLVZLVVHQ
Sandini Bib
Familie zugerechnet (also auch AMD-Prozessoren). Es gibt natürlich auch neuere Familien, weil neuere Prozessoren immer auch mächtigere Befehle besitzen, auf die moderne Programme oft nicht verzichten können. Zur Pentium-Familie gehören z. B. alle Pentium- und die Pentiumkompatiblen AMD-Prozessoren. Assembler und Compiler erzeugen Maschinencode, der normalerweise einer CPU-Familie zugeordnet ist (in Einzelfällen auch einzelnen CPUTypen). Diese Programme laufen also auf allen CPUs einer bestimmten Familie, aber nicht auf CPUs anderer Familien. Hinzu kommt, dass Programme immer auch BIOS- und Betriebssystembefehle nutzen. Ein Maschinencode-Programm, das unter Windows entwickelt wurde, kann deswegen nicht direkt8 unter Linux ausgeführt werden.
3.3.5 Interpreter Ein Interpreter arbeitet etwas anders als ein Compiler. Er übersetzt die Befehle einer höheren Programmiersprache nicht in eine ausführbare Datei, sondern interpretiert und übersetzt einzelne Anweisungen und sendet diese direkt an die CPU bzw. an das Betriebssystem. Ein Interpreter benötigt immer die Quellcodedatei zur Ausführung des Programms. Beispiele für moderne interpretierte Programmiersprachen sind Perl, VBScript und JavaScript. (Perl-)Quellcodedatei write “x:\n”; $x = <>; chop(x); write “y:\n”; $y = <>; chop($y); $z = $x * $y; print “$x * $y = $z”; Print Zahl1 +
write “x:\n”;
Interpreter
0010010100100110 0110101001010010 0101010010100111 0000001110101000
CPU
Abbildung 3.6: Ausführung eines Perl-Programms durch den Perl-Interpreter
8.
Verschiedene spezielle Programme sind aber auch in der Lage, Windows unter Linux (oder umgekehrt) zu simulieren, sodass ältere Windows-Programme auch unter Linux laufen (und umgekehrt).
:LH ZHUGHQ 3URJUDPPH JHVFKULHEHQ XQG IU GHQ &RPSXWHU EHUVHW]W"
145
Sandini Bib
Vor- und Nachteile
Ein Vorteil eines Interpreters ist, dass ein Programm ohne große Probleme auch auf anderen Betriebssystemen ausgeführt werden kann, sofern dieses Programm keine für ein Betriebssystem spezifischen Features nutzt und ein passender Interpreter zur Verfügung steht. Dieser Vorteil wird in der Praxis allerdings recht selten genutzt. Der andere wichtige Vorteil ist die Tatsache, dass ein Programm ohne große Probleme direkt auf dem Rechner, auf dem das Programm ausgeführt wird, nachträglich geändert werden kann. Zur Änderung eines kompiliertes Programms benötigen Sie hingegen immer den zur Programmiersprache passenden Compiler, der meistens auf den Computern, auf denen das Programm ausgeführt wird, nicht vorliegt. Dieser Nachteil von Compilern ist allerdings in der Praxis relativ unwichtig und kann durch moderne Programmiertechniken ohne große Probleme ausgeglichen werden. Interpretierte Programme besitzen also keine wesentlichen Vorteile gegenüber kompilierten. Ein gravierender Nachteil eines Interpreters gegenüber einem Compiler ist die langsame Ausführung des Programms. Das Einlesen, das Überprüfen auf mögliche Fehler und das schließliche Übersetzen jeder einzelnen Anweisungen kostet Zeit. Der andere Nachteil ist, dass der Programmierer immer den Quellcode mitliefern muss, wenn sein Programm auf einem anderen Computer ausgeführt werden soll.
Interpreter in Browsern
Ausnahmen
In einigen Programmiersprachen ist aber der Einsatz eines Compilers gar nicht möglich. JavaScript-Programme sind beispielsweise oft in HTML-Dateien eingebunden. Auf diese Weise werden spezielle Features wie Menüs auf Webseiten programmiert, die mit HTML alleine nicht machbar sind. Da HTML-Dateien in Textform vorliegen, ist ein Kompilieren der enthaltenen Programme nicht möglich. Also wird der JavaScript-Programmcode von einem Interpreter ausgeführt, der Teil des Browsers ist. Ausnahmen von der Regel sind spezielle Programmiersprachen wie Java, C# und Visual Basic .NET. Der mit diesen Sprachen erzeugte Programmcode wird zwar auch von Interpretern ausgeführt. Diese interpretieren aber keinen Quellcode, sondern einen speziellen Zwischencode.
3.3.6 Zwischencode- und Just-In-Time-Compiler Ein Mittelding zwischen Compiler- und Interpretersprachen sind Programmiersprachen wie Java und die neuen Microsoft .NET-Sprachen (wie C# und Visual Basic .NET). Die Compiler dieser Programmiersprachen übersetzen einen Quellcode nicht in Maschinencode, sondern in speziellen Zwischencode. Zwischencode ist bereits so etwas wie Maschinensprache (enthält also einzelne sehr einfache Befehle), allerdings in einer sehr allgemeinen Form, die nicht spezifisch für eine bestimmte
146
%DVLVZLVVHQ
Sandini Bib
CPU-Familie oder ein bestimmtes Betriebssystem gilt. Ein kompiliertes Java-, C#- oder Visual Basic .NET-Programm kann deswegen auch nicht direkt von der CPU oder dem Betriebssystem ausgeführt werden. Im einfachsten Fall ist zur Ausführung eines Zwischencode-Programms ein spezieller Interpreter notwendig. Dieser übersetzt nun aber keinen Quellcode, sondern die speziellen Zwischenbefehle in Maschinencode. (Java-)Quellcodedatei class Hello { public static void main(...) { System.out.println(“Hello”); } }
Compiler Zwischencodedatei 01101100 11011000 01101111 01101010 01101010 00110100 11010100 00101001
01110000 01101010 01101010 00001001 01010100 11101110 00000000 00101010
Abbildung 3.7: Übersetzung eines Java-Quellcodes in eine Java-Zwischencodedatei Zwischencodedatei 01101100 11011000 01101111 01101010 01101010 00110100 11010100 00101001
01110000 01101010 01101010 00001001 01010100 11101110 00000000 00101010 01101100 01110000
Interpreter 0010010100100110
CPU
Abbildung 3.8: Ausführung einer Java-Zwischencodedatei durch einen Java-Interpreter
:LH ZHUGHQ 3URJUDPPH JHVFKULHEHQ XQG IU GHQ &RPSXWHU EHUVHW]W"
147
Sandini Bib
Vor- und Nachteile
Zwischencode-Programme besitzen gegenüber interpretierten Quellcode-Programmen und auch gegenüber Maschinencode-Programmen einige Vorteile. Interpreter, die Quellcode in Maschinencode umsetzen, sind langsamer als Interpreter, die Zwischencode übersetzen. Das liegt einmal daran, dass Quellcode vor der Übersetzung immer erst noch auf die Einhaltung der Syntaxregeln der Programmiersprache überprüft werden muss, und zum anderen, dass Zwischencode den Befehlen einer Maschinensprache bereits sehr nahe kommt und die Übersetzung deswegen wesentlich einfacher ist. Ein anderer, nicht unwesentlicher Vorteil von Zwischencode-Programmen (gegenüber interpretierten Quellcode-Programmen) ist, dass der Quellcode des Programms nicht mit ausgeliefert werden muss, damit dieses auf einem Computer ausgeführt werden kann. Ein wichtiger Vorteil gegenüber Maschinencode-Programmen ist, dass ein Zwischencode-Programm erst bei der Ausführung in Maschinencode umgesetzt wird. Auf dem Betriebssystem muss prinzipiell nur ein passender Interpreter vorliegen. Wenn gleichzeitig noch alle im Programm benötigten Bibliotheken für das Betriebssystem vorhanden sind, kann dasselbe Programm prinzipiell ohne Probleme auf verschiedenen Betriebssystemen ausgeführt werden. Ein unter Windows kompiliertes Java-Programm kann beispielsweise auch ohne Probleme unter Linux ausgeführt werden. Der Interpreter setzt die Zwischencode-Befehle ja schließlich erst in spezifischen Maschinencode um, wenn das Programm ausgeführt wird. Das führt in der Regel zu einem anderen Vorteil. Maschinencode-Programme werden normalerweise (automatisch) so kompiliert, dass sie auf allen CPU-Typen einer Familie ausgeführt werden können und möglichst auch alle zusammengehörigen Betriebssystem-Varianten berücksichtigen (also beispielsweise auf Windows 95 bis XP laufen). Die erzeugten Befehle sind deswegen nicht für alle Systeme optimal. Zwischencode-Interpreter können jedoch sehr optimierten Maschinencode erzeugen, wenn sie speziell für einen CPU-Typ und/oder ein spezielles Betriebssystem entwickelt wurden. Damit wird der Nachteil von interpretierten Zwischencode-Programmen, dass diese grundsätzlich langsamer ausgeführt werden als Maschinencode-Programme, ein wenig aufgehoben (und fällt beim Just-In-Time-Kompilieren, wie ich es im nächsten Abschnitt beschreibe, fast ganz weg). Ein anderer Nachteil dieser Programme ist, dass sie natürlich keine Betriebssystem-spezifischen Features benutzen können, sondern immer nur die Features, die auf allen relevanten Betriebssystemen verfügbar sind.
Laufzeitum-
Der Interpreter und die Bibliotheken einer Zwischencode-Programmiersprache werden auch als Laufzeitumgebung oder Virtuelle Maschine bezeichnet. Eine Laufzeitumgebung beinhaltet alles das, was zur Ausführung von interpretierten Programmen notwendig ist (also den
gebung, Virtuelle Maschine
148
%DVLVZLVVHQ
Sandini Bib
Interpreter, die Bibliotheken und andere notwendige Dinge). Bei java.sun.com können Sie Java-Laufzeitumgebungen für die verschiedensten Betriebssysteme downloaden. Jede dieser Laufzeitumgebungen enthält einen spezifischen Java-Interpreter und alle allgemeinen JavaBibliotheken. Java-Programme, die auf einem beliebigen System kompiliert wurden, können deswegen auf verschiedenen Betriebssystemen ausgeführt werden. Laufzeitumgebungen müssen auch nicht immer komplett vorliegen. Die Laufzeitumgebung für C#- und Visual Basic .NET-Programme ist zurzeit beispielsweise komplett nur für Windows verfügbar. Für Linux gibt es eine Variante, die einfache Konsolenanwendungen ausführen kann. Die Umsetzung der Bibliothek, die die Formulare und Steuerelemente enthält, fehlt in dieser Variante noch. Just-In-Time-Compiler Zwischencode-Programme sind im Vergleich zu Maschinencode-Programmen recht langsam, wenn sie interpretiert werden. Ein Interpreter muss eben alle Programmteile immer wieder neu übersetzen, auch wenn einzelne mehrfach verwendet werden. Dieser große Nachteil wird in vielen Laufzeitumgebungen dadurch ausgeglichen, dass für die Übersetzung keine Interpreter, sondern so genannte Just-In-Time-Compiler (abgekürzt als „JIT“) eingesetzt werden. Ein Just-In-Time-Compiler arbeitet ähnlich einem Interpreter. Er speichert aber bereits übersetzte Programmteile im Arbeitsspeicher (in seinem Cache9). Dabei werden immer nur die beim Ablauf des Programms gerade aktuellen Programmteile übersetzt (weswegen es auch „Just-In-Time“ heißt). Werden diese bereits übersetzten Programmteile dann im weiteren Verlauf des Programms noch einmal aufgerufen, liest der Just-In-Time-Compiler diese einfach aus dem Cache und sendet den Maschinencode direkt zur CPU bzw. zum Betriebssystem. Ein Just-In-Time-Compiler ist also nur bei der ersten Ausführung eines Programmteils langsamer als ein MaschinencodeProgramm. Ab der zweiten Ausführung ist die Geschwindigkeit aber mindestens dieselbe. In der Regel führt die bessere Optimierung des interpretierten Programms ab der zweiten Ausführung eines Programmteils auch zu einer höheren Performance als bei den meist allgemein gehaltenen Maschinencode-Programmen. Just-In-Time-Compiler sind häufig mit Zwischencode-Interpretern verwoben. Der Java-Interpreter benutzt ab der Version 1.2 z. B. implizit den Java-JIT (was Sie über eine Umgebungsvariable aber auch ausschalten 9.
Ein Cache ist im Allgemeinen ein schneller Speicherbereich (z. B. im Arbeitsspeicher), in dem ein Programm oder das Betriebssystem Daten, die von einem langsamen Speicher (z. B. der Festplatte) gelesen werden, für eine weitere Benutzung temporär zwischenspeichert. Werden diese Daten mehr als einmal benötigt, können sie aus dem schnelleren Cache gelesen werden.
:LH ZHUGHQ 3URJUDPPH JHVFKULHEHQ XQG IU GHQ &RPSXWHU EHUVHW]W"
149
Sandini Bib
können). Einige Just-In-Time-Compiler sind auch in der Lage, aus dem Zwischencode eine komplette ausführbare Datei zu erzeugen. Wird diese dann aufgerufen, entfällt das Interpretieren und Just-In-TimeKompilieren komplett, womit die Ausführungsgeschwindigkeit erheblich verbessert wird. Da die ausführbare Datei die Zwischencodedatei nicht ersetzt und in der Regel erst auf dem Zielsystem erzeugt wird, bleibt die Portabilität von Zwischencodeprogrammen auch mit dieser Variante erhalten. Just-In-Time-Compiler sind mittlerweile so gut, dass moderne Programmier-Systeme wie Java und Microsoft .NET erst gar keine Maschinencode-Compiler mehr verwenden, sondern direkt mit Just-In-TimeCompilern arbeiten.
3.4
Übersicht über die aktuellen Software-Architekturen
Software wird heutzutage in verschiedenen Architekturen entwickelt. Zwei Software-Architekturen kennen Sie bereits aus Kapitel 2: Konsolenund einfache normale Anwendungen mit Fenstern und Steuerelementen. Darauf will ich hier auch nicht weiter eingehen. Ein Programmierer sollte aber noch ein wenig mehr von dem wissen, was auf modernen Systemen möglich ist. Deshalb finden Sie hier eine kurze Beschreibung der aktuellen Software-Architekturen. Sie werden diese aber nicht im Rahmen dieses Buchs näher kennen lernen oder selbst Programme dafür entwickeln. Sie müssen schließlich erst einmal die Grundlagen kennen.
3.4.1 Makros Als Makro werden im Allgemeinen selbst geschriebene Programme bezeichnet, die in eine Standardanwendung integriert sind. Viele dieser Anwendungen, wie Microsoft Office oder Corel Draw, ermöglichen Ihnen, ihre bereits vorhandene Funktionalität über benutzerdefinierte Programme zu erweitern. Dabei können Sie natürlich immer auch auf die Funktionen der Anwendung zurückgreifen. Viele Anwendungen machen es Ihnen mit der Möglichkeit Makros aufzuzeichnen sogar sehr einfach, eigene Programme zu erstellen. Makros erleichtern die Arbeit mit Standardanwendungen
150
Makros helfen Ihnen bei Aufgaben, die immer wieder anfallen, für die die Anwendung aber keine direkte und einfach anzuwendende Unterstützung bietet. Wenn Sie beispielsweise in einer Textverarbeitung häufiger ein Wort fett, kursiv und in roter Schrift formatieren müssen, hilft
%DVLVZLVVHQ
Sandini Bib
ein Makro, das diese Schritte automatisch ausführt, enorm bei Ihrer täglichen Arbeit. Aber Makros sind auch häufig wesentlich komplexer und führen Aufgaben aus, die die Anwendung selbst nicht beherrscht. In den Büchern der Lernen-Reihe werden beispielsweise alle größeren Quellcodes üblicherweise mit Zeilennummern versehen. Es wäre nun sehr aufwändig gewesen, diese immer wieder selbst von Hand hinzuzufügen, besonders dann, wenn ein Quellcode nachträglich doch noch einmal geändert werden muss. Also hat ein Mitarbeiter des Verlags ein Makro geschrieben, das die aktuell selektierten Zeilen einliest, diese mit einer Zeilennummer versieht und wieder in das Dokument zurückschreibt. Um eine Startnummer angeben zu können (für Quellcodes, die in mehreren Teilen dargestellt werden), hat der Programmierer ein Formular in das Makro integriert, in dem vor der Ausführung der Nummerierung die notwendigen Einstellungen vorgenommen werden können. Die Erstellung und das Debuggen des Makros hat wohl etwas Zeit gekostet, dafür können Autoren ihre Quellcodes nun innerhalb von Bruchteilen einer Sekunde ohne viel Arbeit nummerieren. In den meisten Anwendungen können Sie Makros über selbst definierte Menü- oder Symbolleisten-Befehle oder über zugewiesene Tastenkombinationen aufrufen. Die Arbeit mit dem Makro ist dann für Sie (bzw. den Anwender sehr einfach). Anwender, die Ihr Makro nicht kennen, wissen dann häufig gar nicht, dass es sich nicht um eine Funktionalität der Anwendung handelt. Makros werden nicht nur für einfache Aufgaben eingesetzt. Einige Firmen setzen für ihre Geschäfte auch komplexe Anwendungen ein, die über Makros in Standardanwendungen programmiert wurden. Sehr häufig werden dazu Datenbankprogramme verwendet, weil diese für eine häufige Aufgabe beim Programmieren, die Verwaltung von Daten in einer Datenbank, bereits eine gute Grundfunktionalität bieten. Der Anwender sieht bei diesen häufig gar nicht mehr, dass eine Standardanwendung dahintersteckt, weil die Anwendung alle Standardfenster, Menü- und Symbolleisten versteckt und nur die eigenen anzeigt.
Makros auch
Die Programmierung mit Makros ist jedoch häufig gegenüber echten Programmen eingeschränkt. Oft beherrscht die Programmiersprache nicht alle notwendigen Techniken (weil es sich häufig um ScriptingSprachen handelt, siehe Seite 157). Besonders aber bei der Gestaltung der Benutzeroberfläche lassen viele Standardanwendungen zu wünschen übrig. Deshalb muss ein Programmierer sehr gut überlegen, ob zur Lösung einer Aufgabe Makros oder separate, echte Anwendungen entwickelt werden sollten.
Eingeschränkte
für komplexe Programme
Programmierung
151
Sandini Bib
3.4.2 Komponentenbasierte Anwendungen Moderne Anwendungen nutzen, neben den Funktionen der Bibliothek der Programmiersprache, meist auch Komponenten. Die Steuerelemente, die Sie in der Delphi/Kylix-Anwendung aus Kapitel 2 auf dem Formular platziert haben, sind z. B. solche Komponenten. Eine Komponente enthält einen fertig programmierten, allgemein anwendbaren Programmteil. Komponenten können in den verschiedensten Anwendungen eingesetzt werden. Eine Komponente, die das Drucken von Texten und Grafiken und eine Vorschau des Ausdrucks ermöglicht, kann beispielsweise von einem Textverarbeitungsprogramm, aber auch von einer Adressverwaltungs-Anwendung genutzt werden. Komponenten werden normalerweise von Programmierern so entwickelt, dass eine Nutzung nicht nur für den aktuell benötigten Zweck, sondern auch in anderen Software-Projekten möglich ist. Dazu werden Komponenten oft auch sehr genau auf Fehler überprüft, sodass der Programmierer sich bei deren Verwendung auf die Funktionalität verlassen kann. Komponenten auch im Computer
Im Prinzip können Sie komponentenbasierte Anwendungen mit einem Computer vergleichen: Ein solcher besteht nämlich aus einer Menge Komponenten, wie z. B. der CPU, der Grafikkarte und der Festplatte, und der eigentlichen „Anwendung“, nämlich dem Motherboard, auf dem alle Komponenten aufgesteckt und durch das diese gesteuert werden. Einzelne Komponenten sind zwar für sich alleine nicht lauffähig, können aber ohne Probleme auch in anderen (kompatiblen) Computern eingesetzt werden. Einige Computerkomponenten, wie beispielsweise der Drucker, können ohne Probleme auch von mehreren Computern gleichzeitig verwendet werden. Der Sinn dabei ist, dass Computerkomponenten nur einmal entwickelt werden müssen und dann in verschiedenster Weise wiederverwendet werden können. Müsste ein Computerhersteller hingegen bei der Entwicklung eines neuen Computers alle Bestandteile immer wieder neu entwickeln, weil diese fest zum Motherboard (zum „Programm“) gehören, hätte dieser sehr viel Arbeit. Durch die Verwendung fertiger, getesteter und qualitativ hochwertiger Komponenten spart der Hersteller eine Menge Arbeit und erreicht eine höhere Qualität (was auch immer Ziel bei der Anwendungsentwicklung ist).
Interne und externe Komponenten
152
Programmkomponenten gibt es in zwei Varianten. Interne Komponenten gehören entweder zur Bibliothek der Programmiersprache (wie z. B. die Steuerelemente) oder sind in separaten, mit dem Compiler der Programmiersprache vorkompilierten Dateien gespeichert und werden über programmiersprachen-abhängige Techniken in ein Quellcode-Programm eingebunden. Wird die Anwendung kompiliert, wird der Programmcode dieser Komponenten vom Compiler in die erzeugte ausführbare
%DVLVZLVVHQ
Sandini Bib
Datei kopiert (weswegen ich diese Komponenten auch „interne Komponenten“ nenne). Die ausführbare Datei ist also im Ergebnis für sich alleine lauffähig. Externe Komponenten liegen hingegen immer in einer separaten Datei vor. Sie werden zwar ähnlich internen Komponenten in ein QuellcodeProgramm eingebunden, beim Kompilieren wird deren Programmcode aber nicht in die ausführbare Datei kopiert. Um diese auszuführen, muss immer auch die Komponentendatei vorhanden sein. Solche Komponenten können meist auch von Anwendungen verwendet werden, die in verschiedenen Programmiersprachen entwickelt wurden (was bei internen Komponenten prinzipiell nicht möglich ist). Ich selbst habe beispielsweise vor einigen Monaten mit Visual Basic 6 eine Komponente zum schnellen Ausdruck von Berichten, die ihre Daten aus einer Datenbank beziehen, entwickelt. Diese Komponente kann ich heute ohne Probleme auch in meinen neuen Delphi-Programmen einsetzen. Die Arbeit hat sich gelohnt, die Komponente ist einfach einzusetzen und macht ihre Arbeit sehr gut (ohne mich zu sehr zu loben, aber das muss ja auch einmal sein). Die Zusammenarbeit von Programmen mit Komponenten stellt Abbildung 3.9 dar.
Adressverwaltung
Textverarbeitung
Koponente zum Bearbeiten von Dokumenten
Komponente zum Drucken
Komponente zum DatenbankZugriff
Dokument
Drucker
Datenbank
Abbildung 3.9: Ein Beispiel für die Zusammenarbeit von Anwendungen mit Komponenten
153
Sandini Bib
Schnittstelle
Damit externe Komponenten von verschiedenen Anwendungen eingesetzt werden können (die in verschiedenen Programmiersprachen entwickelt wurden), basieren diese meist auf einem allgemeinen Kommunikationsmodell, das die Schnittstelle zur Komponente beschreibt. Eine Schnittstelle definiert, auf welche Weise die Kommunikation zwischen zwei Objekten stattfindet. Das mag etwas abstrakt klingen, ist aber eigentlich ganz einfach. Hier hilft wieder die Computer-Analogie: Um beispielsweise Grafikkarten verschiedener Hersteller in einem Computer einsetzen zu können, wurden und werden immer wieder neue Standards entwickelt. Der derzeit aktuelle Standard ist AGP. Dieser Standard definiert, wie die Grafikkarten-Steckplätze auf dem Motherboard aussehen müssen, wie die Anschlüsse dieser Steckplätze angesprochen werden und welche Befehle darüber gesendet werden können. Bei Programmkomponenten werden dazu spezielle Modelle wie COM (altes MicrosoftKonzept), .NET (neues Microsoft-Konzept), JavaBeans (Java-Konzept) oder CORBA (allgemeines, systemunabhängiges Konzept) verwendet. Auf diese Konzepte gehe ich im Buch allerdings nicht weiter ein. Für den Anwender sehen komponentenbasierte Anwendungen aus wie ganz normale Anwendungen. Lediglich wenn eine Komponente, die von einer Anwendung verwendet wird, nicht auf dem Computer des Anwenders installiert ist, kommt der Anwender mit dieser Art der Softwarearchitektur in Form einer entsprechenden Fehlermeldung in Berührung. Um beim Vergleich mit dem Computer zu bleiben: Interne Komponenten können Sie mit den internen Komponenten des Computers vergleichen. Um zwei Computer herzustellen, benötigen Sie auch zwei „Kopien“ einer Grafikkarte, einer CPU etc., die quasi in das Ergebnis „hineinkopiert“ werden. Wenn Sie allerdings zwei Computer mit einem Drucker ausstatten wollen, können Sie einfach dazu einen einzigen Drucker verwenden. Das wäre dann das Äquivalent zu einer externen Komponente. Ein Beispiel für eine externe Komponente ist die von Microsoft entwickelte Komponente Microsoft Rich Textbox Control, mit der ein Programm dem Anwender eine Möglichkeit zur Verfügung stellen kann, RTF10Dateien zu erzeugen und zu bearbeiten. Damit können Sie ohne großen Aufwand eine einfache Textverarbeitung programmieren. Wie Sie aus diesem Beispiel erkennen, werden Komponenten nicht nur vom Programmierer selbst, sondern auch von anderen Programmierern bzw. Firmen entwickelt und vertrieben. Ihnen stehen unzählige fertige kaufbare oder kostenfreie Komponenten zur Verfügung (die Sie natürlich über das Internet suchen und erwerben können). 10. RTF (Richt Text Format)-Dateien sind Textdateien mit Formatierungen ähnlich den Dateien, die Microsoft Word erzeugt.
154
%DVLVZLVVHQ
Sandini Bib
3.4.3 Verteilte Anwendungen Mit Hilfe der Komponentenstandards COM, .NET, JavaBeans und CORBA ist es ohne große Probleme möglich, Komponenten auf anderen Rechnern laufen zu lassen als die eigentliche Anwendung. Die komplette Anwendung wird damit auf mehrere Computer verteilt. Diese Art der Architektur wird meist nur in Firmen eingesetzt. So kann in einer Firma z. B. ein Computer dafür zuständig sein, Lieferscheine und Rechnungen zu drucken. Dieser Computer führt dazu eine Komponente mit Funktionen für das Drucken aus. Die Sachbearbeiter der Firma arbeiten mit einer Auftragsverwaltungsanwendung, die zwar teilweise lokal auf ihren Computern ausgeführt wird, die aber eben auch diese DruckKomponente nutzt, um Lieferscheine und Rechnungen zu drucken. Die Komponente enthält natürlich eine Menge an Programmierung, so dass die Auftragsverwaltungsanwendungen auf den einzelnen Computern nur die auszudruckenden Daten an die jeweilige Funktion übergeben müssen und keine Information darüber, wie gedruckt werden soll. Abbildung 3.10 veranschaulicht dieses Szenario. (Client-)Computer mit derAuftragsverwaltungsanwendung
(Server-)Computer mit Komponente zum Drucken von Lieferscheinen und Rechnungen
Drucker
Abbildung 3.10: Beispiel für eine einfache verteilte Softwarearchitektur
155
Sandini Bib
An einer verteilten Anwendung sind immer mindestens ein Server und meist mehrere Clients beteiligt. Der Server („Bediener“) stellt den Clients (den „Kunden“) bestimmte Dienste zur Verfügung, wie z. B. das Drucken von Lieferscheinen und Rechnungen. Deshalb werden verteilte Anwendungen häufig auch als Client/Server-Anwendung bezeichnet. Verteilte Anwendungen, die eigentlich nur in größeren Firmen eingesetzt werden, besitzen (natürlich) einige Vorteile gegenüber normalen Anwendungen: • Die auf einem Server ausgeführten Komponenten können von mehreren Anwendungen auf verschiedenen Clients verwendet werden. Die einzelnen Clients können deswegen recht leistungsschwach sein, wenn die größte Rechenarbeit in der Komponente ausgeführt wird (der Rechner, der die Komponente speichert, muss dann natürlich sehr leistungsstark sein). • Falls eine Änderung der Programmierung der Komponente notwendig ist, muss eine neue Version nur ein einziges Mal (auf dem Server) installiert werden. Im Idealfall ist keine Änderung der Programme auf den Clients notwendig. Dieser Vorteil wird besonders dann deutlich, wenn mehrere hundert Clients mit einer Server-Komponente arbeiten. • Ein weiterer Vorteil einer verteilten Architektur ist, dass über ServerKomponenten sehr gut Benutzerrechte verwaltet werden können. Das kann so programmiert sein, dass Benutzer in der Client-Anwendung auf dem Client einen Benutzernamen und ein Passwort eingeben müssen und die Client-Anwendung diese Login-Informationen an die Komponente weitergibt. Die Komponente überprüft dann bei der Ausführung bestimmter Aktionen, ob der eingeloggte Benutzer das dazu notwendige Recht besitzt. In komplexen Systemen sind Änderungen an den Benutzerrechten auf diese Weise recht einfach über die zentrale Komponente möglich und unabhängig von allen anderen Technologien, die die Server-Komponente selbst wieder einsetzt. Für den Einsatz einer verteilten Architektur gibt es noch mehr Gründe, die an dieser Stelle allerdings zu weit führen würden. Schließlich beschreiben ganze Bücher das Prinzip der verteilten Anwendung. Die aufgelisteten Features sind allerdings in meinen Augen schon die wichtigsten. Verteilte Anwendungen werden, genau wie einfache Anwendungen, mit modernen Programmiersprachen wie C++, C#, Delphi, Kylix, Java oder Visual Basic .NET entwickelt.
156
%DVLVZLVVHQ
Sandini Bib
3.4.4 Scripting-Programme Scripting-Programme sind einfache Anwendungen, die nicht kompiliert sind, sondern im Quellcode einer speziellen Scriptsprache gespeichert sind. Diese Anwendungen werden bei der Ausführung von einem Interpreter ausgeführt. Der Windows Scripting Host (WSH) ermöglicht z. B. die Ausführung von Visual Basic Script-Dateien (mit der Endung .vbs) oder JScript11-Dateien (mit der Endung .js) direkt in Windows (z. B. über einen Doppelklick auf der Datei). Die verschiedenen Linux-Shells erlauben die Ausführung von Shell-Skripten (die nichts anderes sind als Scripting-Programme) in von der Shell abhängigen Programmiersprachen. Über solche Programme können Sie immer wiederkehrende gleich bleibende Aufgaben wie das Kopieren von Dateien oder das Anlegen neuer Benutzer auf Ihrem System vereinfachen. Unter Windows spielen Scripting-Programme keine große Rolle. In Linux werden allerdings sehr viele Anwendungen über solche Programme installiert oder gestartet. Die meisten Scriptsprachen bieten jedoch keine Möglichkeit, eine Benutzeroberfläche zu erzeugen, was deren Verwendung erheblich einschränkt. Aber diese Sprachen sind eben nicht für komplette Programme, sondern für einfache „Skripts“ vorgesehen (wie der Name schon sagt). Scripting-Programme finden Sie aber auch in HTML-Dokumenten, wo diese die HTML-Seite um Features erweitern, die mit reinem HTML nicht möglich sind. Viele HTML-Seiten werden beispielsweise über JavaScript-Programme um Menüs erweitert, die vom Benutzer sehr einfach bedient werden können. Hier spielt die Anwendung einer Scriptsprache eine sehr große Rolle.
3.4.5 Internetanwendungen Haben Sie schon einmal bei www.amazon.de ein Buch gekauft? Oder bei www.ebay.de irgendetwas ersteigert? Oder eine Suchmaschine wie www.google.de benutzt? Dann haben Sie bereits mit einer Internetanwendung gearbeitet. Und dann wissen Sie auch, dass diese Anwendungsarchitektur gar nicht mehr aus unserem täglichen Leben wegzudenken ist. Falls Sie diese Art Anwendung noch nicht kennen, sollten Sie schnell einmal bei www.amazon.de vorbeischauen, dort ein wenig herumsurfen und vielleicht ein Buch kaufen. Ich warte so lange ...
11. JScript ist das Microsoft-Äquivalent zu JavaScript
157
Sandini Bib
HTML und Webserver
Die Basis einer reinen Internetanwendung (die im Browser ausgeführt wird) ist HTML (Hypertext Markup Language). HTML ist eine spezielle, textbasierte Sprache zur Definition von Dokumenten mit formatierten Texten, Bildern und verschiedenen Ein- und Ausgabeelementen. HTML ist allerdings keine Programmiersprache, sondern dient lediglich der Beschreibung des Aussehens einer Webseite. Ein Webserver stellt HTMLDateien (und andere Dateien) zur Verfügung. Wenn Sie im Browser www.amazon.de eingeben, sprechen Sie bei Amazon den Webserver mit dem Namen „www“ an und rufen dort die HTML-Datei ab, die Amazon als Start-Dokument für den Online-Shop vorgesehen hat. Viele HTMLDateien werden jedoch nicht statisch auf einem Webserver gespeichert, sondern bei jeder Anforderung erneut dynamisch erzeugt. Das ist dann die reine Form einer Internetanwendung. Über verschiedene HTMLEingabeelemente (wie Verweise, Schalter und Texteingabefelder) erlaubt eine HTML-Seite häufig auch Eingaben, die auf dem Webserver ausgewertet werden. Wenn Sie z. B. bei www.google.de einen Suchbegriff eingeben und die Suche über den GOOGLE-SUCHE-Schalter starten, rufen Sie auf dem Webserver ein Programm auf, das Ihre Eingabe auswertet, ein entsprechendes Ergebnis-HTML-Dokument erzeugt und Ihnen dieses zusendet. Internetanwendungen ähneln verteilten Anwendungen. Der Hauptteil des Programms wird auf einem Server ausgeführt. Im Normalfall ist der Client aber hier eben ein Browser, der HTML-Dokumente darstellt. Der Server, der in Wirklichkeit ein programmierbarer Webserver ist, stellt diese HTML-Dokumente dynamisch zusammen.
Der Vorteil
158
Der enorme Vorteil einer Internetanwendung ist, dass diese normalerweise über einen einfachen Browser von jedem Ort der Welt aus verwendet werden kann (einige spezielle Internetanwendungen erfordern aber auch die Installation einer separaten Client-Software). Abbildung 3.11 demonstriert dies am Beispiel eines Online-Bestellshops.
%DVLVZLVVHQ
Sandini Bib
Client Client WebBrowser
WebBrowser
Client
WebBrowser
Webserver
ShopAnwendung
BestellDatenbank
Abbildung 3.11: Ein Online-Bestellshop im Süden Deutschlands, der über das Internet von Clients im Norden von Deutschland verwendet wird
Internetanwendungen können Sie mit den meisten modernen Programmiersprachen entwickeln. Einige, wie Perl, PHP und die Programmierumgebung ASP, sind sogar primär für die Entwicklung solcher Anwendungen gedacht. Die mit diesen Programmiersprachen entwickelten Internetanwendungen werden normalerweise vollständig auf dem Webserver ausgeführt. Der Browser übernimmt im einfachsten Fall nur die Darstellung des HTML-Dokuments. In einigen Internetanwendungen werden aber auch Teile des Programms auf dem Client ausgeführt. Das hat den Grund, dass das zur Darstellung der Oberfläche im Internet-Browser verwendete HTML nur wenige dynamische Elemente besitzt und nur über Schalter und Verweise auf Benutzereingaben reagieren kann. Komplexe Benutzeroberflä-
Programme in HTML-Seiten
159
Sandini Bib
chen mit einer Menüstruktur sind mit HTML alleine nicht möglich. Dazu werden in HTML eingebundene Programme verwendet, wozu meist die Programmiersprache JavaScript und eher selten VBScript verwendet wird. Eine andere Möglichkeit zur Erweiterung von HTML-Seiten sind Java-Applets. Java-Applets sind spezielle in Java entwickelte Komponenten, die primär für den Gebrauch in HTML-Seiten vorgesehen sind. Mit Java-Applets können Sie die Oberfläche eines HTML-Dokuments nahezu mit allen Steuerelementen erweitern, die Sie auch von der grafischen Oberfläche Ihres Betriebssystems her kennen.
3.5
Übersicht über die aktuellen Programmiersprachen
Es gibt eine Unmenge an Programmiersprachen, die alle ihre Vor- und Nachteile haben. Jede Sprache verwendet einen etwas anderen Ansatz zur Entwicklung eines Programms. Die Grundlagen (die Sie in diesem Buch ja lernen) sind aber immer dieselben (wenn auch manche Programmiersprachen eine etwas exotische Vorstellung davon haben). Damit Sie einen kleinen Überblick über die zurzeit aktuellen Programmiersprachen erhalten, stelle ich diese kurz vor. Auf ältere und heute nicht mehr allzu häufig eingesetzte Sprache gehe ich dabei allerdings nicht ein. An der Adresse www.ualberta.ca/HELP/TUTOR/ProgTut.html finden Sie eine Übersicht über die aktuellen Programmiersprachen mit Links zu weiteren Informationen.
3.5.1 C und C++ C ist eine sehr alte Sprache, die aber auch heute noch eine große Bedeutung besitzt. Die Syntax ähnelt der von Java12. C ist aber darauf ausgerichtet, möglichst direkt auf die Hardware, den Speicher oder das Betriebssystem zuzugreifen. C-Programmierung ist deswegen nicht allzu einfach. Sie können mit C dafür aber sehr schnelle Programme entwickeln (was auch daran liegt, dass diese von hochoptimierten Compilern kompiliert werden). C++ basiert auf C, besitzt aber wesentlich mehr vorgefertigte Komponenten und die Möglichkeit, objektorientiert (und damit moderner) zu programmieren. Die hohe Geschwindigkeit von Cund C++-Programmen ist der Grund dafür, dass viele Betriebssysteme (!) wie z. B. Linux, viele Spiele und Hardware-Treiber komplett in diesen Sprachen geschrieben wurden und noch immer werden. 12. Java orientiert sich allerdings an C und C++ und nicht umgekehrt.
160
%DVLVZLVVHQ
Sandini Bib
Ein typisches C++-Programm, das nur Daten an der Konsole einliest und ausgibt, sieht beispielsweise so aus (ohne dass ich dieses hier allerdings erkläre!): #include #include <string.h> int main() { string Eingabe, Ausgabe; cout << "Ihr Name: "; cin >> Eingabe; Ausgabe = "Hallo " + Eingabe; cout << Ausgabe << endl; return 0; }
Was Sie an dem Quelltext vielleicht bereits erahnen, ist die Tatsache, dass C- und C++-Programme häufig sehr kryptisch und damit schwer verständlich sind. Das liegt aber auch oft am (manchmal etwas eigenartigen) Stil des Programmierers. Mit C und C++ können Sie auf jeden Fall Programme schreiben, die zwar sehr schnell sind, die aber außer Ihnen kaum jemand versteht .
-
Da diesen Sprachen häufig Komponenten für viele heutzutage benötigte Aufgaben fehlen, muss der Programmierer meist sehr viel selbst entwickeln. Besonders die Erstellung einer Oberfläche ist im Vergleich mit modernen Entwicklungsumgebungen in der Microsoft Entwicklungsumgebung für C++ sehr kompliziert und arbeitsaufwändig. Borland bietet aber mit dem C++-Builder auch für C++ eine sehr gute Entwicklungsumgebung an, die mit der von Delphi fast identisch ist und über die der Programmierer auf dieselben umfangreichen Komponenten zurückgreifen kann.
3.5.2 Java Java kennen Sie bereits. Java besitzt viel Ähnlichkeit mit C++, ist aber lange nicht so kryptisch und bietet dem Programmierer wesentlich mehr einfach anzuwendende Funktionen als C++. Die Programmierung der Oberfläche einer Anwendung ist mit Java z. B. viel einfacher. JavaProgramme werden von einem Zwischencode-Compiler kompiliert und können deswegen auf allen Betriebssystemen ausgeführt werden, auf
161
Sandini Bib
denen eine Java-Laufzeitumgebung installiert ist. Java beinhaltet mittlerweile alle Komponenten, die ein Programmierer zur Erzeugung der verschiedensten Anwendungsarchitekturen benötigt. Über die Java Enterprise Edition ist es beispielsweise kein großes Problem (wenn man weiß, wie es geht), verteilte oder Internetanwendungen zu erzeugen. Für Java gibt es einige unterschiedliche Entwicklungsumgebungen, die die Programmierung komplexer Anwendungen erheblich vereinfachen. Java ist ein großer Konkurrent zum Microsoft .NET-Konzept. Der Vorteil von Java ist dabei, dass Java-Programme auf den verschiedensten Betriebssystemen laufen. So können Sie Java-Komponenten für eine verteilte Anwendung auf einem Sun-Server unter dem Betriebssystem Solaris ausführen und diese mit Java-Programmen auf einem Linux- oder Windows-Computer verwenden. Bis so etwas mit .NET möglich ist, werden wohl noch einige Jahre vergehen (wenn es überhaupt jemals möglich wird).
3.5.3 Visual Basic 6 Das mittlerweile etwas veraltete Visual Basic 6 ist eine Programmiersprache, die auf dem alten Basic basiert. Basic wurde vor etlichen Jahren als Anfängersprache entwickelt und bietet sehr einfache und für den Anfänger verständliche Sprachkonstrukte (die den Profi aber eher verwirren). Leider hat Microsoft diese Sprachkonstrukte bei der Entwicklung von Visual Basic übernommen. Trotzdem ist diese Sprache ein gutes und vor allen Dingen einfaches Werkzeug zur Softwareentwicklung. Visual Basic 6 besitzt eine Vielzahl an vorgefertigten Komponenten und erleichtert die Windows-Programmierung über eine sehr gute Entwicklungsumgebung, die der von Delphi und Kylix ähnlich ist. Visual Basic wurde mit Blickrichtung auf eher unerfahrene Programmierer entwickelt und bietet deswegen, neben einer gut lesbaren Syntax, keine direkten Speicher- oder Hardwarezugriffe. Deshalb werden bei der Programmierung aber auch so einige komplexe Fehler von vornherein vermieden. In Verbindung mit der einfachen Erstellung der Anwendungsoberfläche führte das dazu, dass auch professionelle Programmierer diese Sprache einsetzen. Um das Jahr 2000 haben in den USA angeblich etwa 80 Prozent der Softwareentwickler mit Visual Basic 6 gearbeitet (nach einem Interview mit David Stutz, dem Chefentwickler von Visual Basic). Das folgende Beispiel zeigt das Delphi/Kylix-Nettoberechnungsprogramm in Visual Basic. Das Programm läuft wie bei Delphi und Kylix in einem Fenster und arbeitet mit den darauf angelegten Steuerelementen (das Fenster stelle ich hier nicht dar):
162
%DVLVZLVVHQ
Sandini Bib
Private Sub cmdRechnen_Click() txtNetto.Text = txtBrutto.Text * _ (1 - (txtNetto.Text / (100 + txtSteuer.Text))) End Sub
Wie Sie sehen, ist das Programm (abgesehen von der unterschiedlichen Syntax) dem Delphi/Kylix-Programm sehr ähnlich. Der Programmcode ist aber wesentlich einfacher aufgebaut. Das liegt daran, dass Visual Basic 6 alle notwendigen Konvertierungen implizit vornimmt. Das explizite Konvertieren, wie es in vielen Programmiersprachen notwendig ist, entfällt. Das ist aber nicht unbedingt ein Vorteil, denn dadurch kann ein Programmierer auch viele Fehler in das Programm einbauen. Mit Visual Basic 6 erzeugte Programme sind zwar echte MaschinencodeProgramme (die nur unter Windows laufen), benötigen aber immer eine so genannte Laufzeitbibliothek. Diese enthält alle Funktionen und Komponenten der Sprache und ist in einer separaten Datei auf dem System gespeichert. Da viele Anwendungen wie Microsoft Word und Excel mit derselben Laufzeitbibliothek arbeiten, können Sie in diesen Anwendungen ebenfalls mit Visual Basic Programme schreiben (die dann oft als „Makro“ bezeichnet werden). Über solche Programme werden recht häufig zusätzliche Funktionalitäten in Office-Dokumente (Word-, Excel-, Access-Dateien etc.) integriert. Dabei werden sogar komplette Anwendungen entwickelt, die die Grundfunktionalität des OfficeProgramms einsetzen, dem Anwender aber eine eigene Oberfläche mit zusätzlichen oder vereinfachten Features bieten. Sie können mit Visual Basic 6 zwar fast alle Anwendungsarchitekturen entwickeln, die Features der Programmiersprache entsprechen aber nicht mehr dem heutigen Standard. Das größte Manko von Visual Basic ist, dass diese Sprache nur eine sehr eingeschränkte objektorientierte Programmierung ermöglicht.
3.5.4 Delphi und Kylix Delphi und Kylix verfolgen eigentlich denselben Grundsatz wie Visual Basic 6: die Programmierung von Anwendung der verschiedensten Architekturen unter Verwendung vieler vorgefertigter Komponenten in einer Entwicklungsumgebung, die die Erstellung der Oberfläche mit Hilfe der Maus ermöglicht. Als direkter Konkurrent zu Visual Basic 6 spricht Delphi aber eher „echte“ Programmierer an. Die Syntax der Sprache ist wesentlich restriktiver als bei Visual Basic 6 (Visual Basic 6 lässt in vielen Fällen mehrere Varianten zu). Daneben können Sie in Delphi auch näher am Computer programmieren, die von Delphi verwendete Sprache Object Pascal lässt beispielsweise direkten Speicherzu-
163
Sandini Bib
griff und sogar das Einbinden von Assembler-Befehlen zu. In Delphi kann es Ihnen aber deswegen auch schnell passieren, dass Sie Fehler erzeugen, die nicht mehr allzu einfach zu debuggen sind. Sie müssen schon ein im Vergleich zu Visual Basic 6 disziplinierterer Programmierer sein, um mit Delphi zu arbeiten (was aber eher ein Vorteil ist, denn nur so werden Sie zu einem echten Programmierer). Delphi bietet wesentlich mehr Komponenten als Visual Basic 6 und ermöglicht damit die Erstellung der verschiedensten Anwendungsarchitekturen. Besonders im Bereich der verteilten und Internetanwendungen ist Delphi sehr stark. Trotzdem setzen nur relativ wenig Programmierer bzw. Firmen Delphi ein. Viele Firmen arbeiten auch heute noch mit C++, weil die erzeugten Programme schneller sind und weil sie C++ „schon immer“ verwendet haben (und deshalb eine Menge eigener Bibliotheken besitzen), anderen setzen lieber Visual Basic ein, weil diese Sprache einfacher zu programmieren ist. Kylix ist, wie Sie ja bereits wissen, die Entwicklungsumgebung für Linux. Kylix bietet ähnliche Features wie Delphi. Mit Delphi erzeugte Programme laufen nur unter Windows (ab Version 95), Kylix-Programme laufen unter Linux.
3.5.5 C#, J# und Visual Basic .NET C# (gespochen als „CSharp„), Visual J# .NET und Visual Basic .NET sind recht neue Programmiersprachen von Microsoft. Alle diese Sprachen sind von der Syntax her zwar recht unterschiedlich, basieren aber auf derselben Laufzeitumgebung und demselben Konzept (dem Microsoft .NET13-Konzept). Ähnlich wie Java bietet dieses Konzept dem Programmierer eine sehr große Anzahl an vorgefertigten Komponenten, die die Programmierung aller möglichen Anwendungsarchitekturen ermöglichen und sehr einfach machen. Microsoft bietet für C#, Visual J# .NET und Visual Basic .NET eine sehr gute Entwicklungsumgebung, deren Verwendung für einen professionellen Programmierer obligatorisch ist. Diese Entwicklungsumgebung fasst quasi die Features von Delphi und Visual Basic zusammen und ermöglicht so die recht einfache Entwicklung komplexer Anwendungen. C# ähnelt von der Syntax her Java, arbeitet aber ausschließlich mit den speziellen Komponenten der sehr umfangreichen .NET-Laufzeitumgebung. Visual J# .NET ist für Java-Programmierer gedacht und enthält spezielle Komponenten, die den Java-Komponenten ähnlich sind. Visual Basic .NET besitzt eine ähnliche Syntax wie das alte Visual Basic 13. gesprochen als „Dotnet“
164
%DVLVZLVVHQ
Sandini Bib
6 (und ist eben für Visual Basic-Kenner gedacht). C#- und Visual Basic .NET-Programme unterscheiden sich (abgesehen von der Syntax) nur wenig. Es ist beispielsweise recht einfach, ein C#-Programm in ein Visual Basic .NET-Programm umzusetzen. Visual Basic .NET enthält zudem aber spezielle Funktionen, die denen des alten Visual Basic 6 entsprechen. Mit allen .NET-Sprachen können Sie alle möglichen Anwendungsarchitekturen erzeugen. Da dies oft um einiges einfacher ist als bei Java, ist .NET ein sehr starker Konkurrent zu dieser Sprache. Eine Internetanwendung mit C# zu entwickeln ist beispielsweise nur noch sehr wenig Arbeit und unterscheidet sich kaum von der Entwicklung einer normalen Anwendung. Auch für verteilte Anwendungen bietet .NET sehr viele einfach anzuwendende Features. Compiler für C#, Visual J# .NET und Visual Basic .NET erzeugen (nahezu) identischen Zwischencode, der von einem Just-In-Time-Compiler ausgeführt wird. Da dieser sehr optimiert arbeitet und auch bei der Erzeugung von Fenstern und Steuerelementen die Features des Betriebssystems nutzt, sind die erzeugten Programme recht schnell. Wegen der Nutzung dieser speziellen Betriebssystem-Funktionen ist es aber nicht allzu einfach, eine Laufzeitumgebung für ein spezielles Betriebssystem zu entwickeln. Deswegen existiert diese zurzeit nur komplett für Windows. Für Linux wird im Mono-Projekt (www.go-mono.com) gerade eine Laufzeitumgebung entwickelt, die aber erst in Juni 2003 fertig gestellt sein soll.
3.5.6 JavaScript JavaScript ist eine Java-ähnliche Programmiersprache (die aber mit Java nichts zu tun hat, der Hersteller von JavaScript ist nicht Sun, sondern Netscape). JavaScript wird allerdings nur für Scripting-Anwendungen, hauptsächlich in HTML-Dateien eingebunden, eingesetzt. Die Erstellung einer Anwendungsoberfläche ist bei JavaScript auf die Verwendung von HTML eingeschränkt. Richtige Anwendungen können Sie damit nicht programmieren. Die meisten Webseiten mit Menüs und ähnlichen Bestandteilen enthalten aber JavaScript-Programme. Eigentlich ist es nur mit JavaScript möglich, ein HTML-Dokument, das selbst recht statisch ist, mit dynamischen Features zu versehen. Die Ausführung eines in einem HTML-Dokument eingebundenen JavaScriptProgramms übernimmt ein Interpreter, der in den jeweiligen Browser integriert ist.
165
Sandini Bib
3.5.7 VBScript VBScript (Visual Basic Script) entspricht von der Syntax her Visual Basic 6, ist aber wie Java Script für Scripting-Anwendungen gedacht. In HTML-Dokumenten findet man diese Sprache eher selten, was daran liegt, dass lediglich der Internet Explorer einen Interpreter dafür integriert. Bedeutung besitzt diese Sprache bei der Erstellung von Internetanwendungen mit ASP (Active Server Pages) und WSH-Programmen (Windows Scripting Host). ASP ermöglicht die recht einfache Erzeugung dynamischer Webseiten, beispielsweise für einen Online-Shop, ist aber mittlerweile durch ASP.NET ersetzt. WSH-Programme sind Programme, die in einfachen Textdateien gespeichert sind und bei der Ausführung unter Windows vom Windows Scripting Host interpretiert werden. Damit können Sie einfache Programme entwickeln, die immer wiederkehrende Aufgaben wie das Kopieren von Dateien oder das Anlegen eines Benutzerkontos erleichtern, genau wie bei einfachen interpretierten Programmen.
3.5.8 Perl, PHP, TCL/TK, Python und andere Exoten Programmiersprachen wie Perl, PHP, TCL/TK und Python spielen zur Zeit nur bei wenigen Programmierern eine Rolle. Viele dieser Sprachen werden interpretiert, einige können auch eine Oberfläche darstellen. In der Regel finden Sie im Internet freie Interpreter zur Ausführung der Quellcode-Programme. Die Syntax dieser Sprachen ist manchmal sehr gewöhnungsbedürftig, einige Programmierer schwören aber darauf. Das Einsatzgebiet dieser Sprachen liegt eher im Bereich der Programmierung kleiner Tools und der Programmierung im Internet.
3.6
Algorithmen
Wie Sie bereits gesehen haben, arbeitet ein Programm grundsätzlich immer nach dem EVA-Prinzip. Am Anfang der Programmierung steht immer ein Problem: Das Programm soll bestimmte Daten einlesen, nach festgelegten Regeln verarbeiten und wieder ausgeben. Die Eingabe kann vom Anwender (über die Tastatur oder die Maus), aus einer Datei oder von verschiedenen an den Computer angeschlossenen Geräten, wie z. B. einem an der seriellen Schnittstelle angeschlossenen Messgerät, stammen. Das Einlesen der Daten, die Verarbeitung und die Ausgabe ist dann das, was Sie programmieren müssen. Die Ein- und Ausgabe stellt dabei meist kein großes Problem dar, die Verarbeitung der Daten ist das eigentliche Problem. Denn dazu müssen Sie einen Algorithmus entwickeln. Ein Algorithmus beschreibt Schritt für Schritt, was getan werden
166
%DVLVZLVVHQ
Sandini Bib
muss, um die eingehenden Daten so zu verarbeiten, dass das gewünschte Ergebnis resultiert. Bei der Programmierung müssen Sie dem Computer genau mitteilen, was er zu tun hat. Sie müssen definieren, auf welche Weise Daten eingelesen werden, wie diese verarbeitet und wieder ausgegeben werden sollen. Sie müssen in Ihren Programmen sehr genau festlegen, was mit den eingehenden Daten geschehen soll, damit das gewünschte Ergebnis herauskommt. Diese Genauigkeit spielt eine große Rolle. Ein Mensch ist in der Lage, Aussagen zu interpretieren und gegebenenfalls fehlende Informationen durch Assoziation mit eigenem Wissen, Experimentieren oder Nachfragen zu ergänzen. Ein Computer kann das nicht. Einem Computer müssen Sie jeden einzelnen zur Erzielung des Ergebnisses notwendigen Schritt mitteilen. Einen Menschen können Sie auffordern, Kaffee zu kochen. Der Mensch wird eventuell nachfragen, wo die Kaffeemaschine steht und wo der Kaffee zu finden ist. Die restlichen Informationen wird er durch Assoziationen mit bereits gespeichertem Wissen (z. B. dem Wissen, wie eine Kaffeemaschine aussieht) oder durch Suchen und Experimentieren ermitteln. Der Algorithmus zum Kaffeekochen sieht für einen Menschen demnach sehr einfach aus:
Computer müssen genau instruiert werden
• Koche Kaffee (bitte) Leider funktioniert ein solcher Algorithmus im Computer (noch) nicht. Erst wenn Computer intelligent werden und selbst lernen können, werden solche einfachen Algorithmen möglich sein. Zurzeit sind Computer aber noch ziemlich unintelligent. Angenommen Sie wollten einen Roboter programmieren, so dass dieser in der Lage ist, Kaffee zu kochen, würde der Algorithmus dazu etwa so aussehen: • Gehe zur Kaffeemaschine, • nimm den Kaffeebehälter heraus, • gehe mit dem Kaffeebehälter zum Wasserhahn, • drehe den Wasserhahn auf, bis genügend Wasser herauskommt, • halte den Kaffeebehälter unter den Wasserhahn, bis dieser ausreichend gefüllt ist, • ziehe den Kaffeebehälter zurück, • drehe den Wasserhahn zu, • gehe zur Kaffeemaschine, • fülle das Wasser ein, • stelle den Kaffeebehälter in die Kaffeemaschine, • sehe zum Küchenschrank, • nimm den Kaffee heraus, • nimm einen Kaffeefilter heraus,
$OJRULWKPHQ
167
Sandini Bib
• gehe zur Kaffeemaschine, • lege den Kaffeefilter ein, • fülle Kaffee ein, bis genügend Kaffee enthalten ist, • schalte die Kaffeemaschine ein, • gehe zum Küchenschrank, • stelle den Kaffee dort hinein, • warte, bis der Kaffee fertig ist, • nimm den Kaffeebehälter, • bringe den Kaffeebehälter zu mir. Dachten Sie, dass Kaffeekochen so kompliziert ist? Für einen Roboter (der ja von einem Computer gesteuert wird) wäre dies schon der Fall. Dieser muss eben genau wissen, was zu tun ist. Mein KaffeekochenAlgorithmus setzt dabei sogar noch einige Dinge voraus. Eine Aussage wie „Gehe zur Kaffeemaschine“ verlangt vom ausführenden Roboter ja, dass dieser weiß, wie er zur Kaffeemaschine gelangt. Diese „Funktion“ muss also bereits im Computer verfügbar sein (vielleicht als Betriebssystem-Funktion). Ein Roboter könnte diese Anweisung beispielsweise so auswerten, dass er mit Hilfe seiner Video-Augen in allen Räumen des Hauses nach einem Muster sucht, das dem in seinem Arbeitsspeicher verwalteten Grundmuster einer Kaffeemaschine ähnlich ist. Der Kaffeekochen-Algorithmus ist außerdem nicht komplett, weil er einige Sonderfälle nicht berücksichtigt. Was passiert beispielsweise, wenn kein Wasser, kein Kaffeefilter oder kein Kaffee verfügbar ist? Der dazu notwendige Algorithmus lässt sich allerdings nicht mehr so einfach wie oben beschreiben. Dazu benötigen Sie Pseudocode, Programmablaufpläne oder Struktogramme. Pseudocode beschreibe ich im nächsten Abschnitt. Programmablaufpläne werden heutzutage nicht mehr allzu häufig eingesetzt, weshalb ich auf eine Beschreibung verzichte. Für die Behandlung von Struktogrammen bleibt in diesem Buch leider kein Platz. Eine Beschreibung dieser Technik finden Sie aber im Artikel „Programmentwurf“ auf der Buch-CD.
3.6.1 Pseudocode, Schleifen und Verzweigungen Pseudocode ist ein einfaches Mittel zur Beschreibung eines Algorithmus. Pseudocode arbeitet mit den üblichen Programmstrukturen, ohne diese jedoch in einer bestimmten Programmiersprache zu formulieren. Algorithmen in Pseudocode arbeiten mit einer verbalen Beschreibung der auszuführenden Aktionen. Im Pseudocode würde der KaffeekochenAlgorithmus etwa so aussehen:
168
%DVLVZLVVHQ
Sandini Bib
Gehe zur Kaffeemaschine Nimm den Kaffeebehälter heraus Gehe mit dem Kaffeebehälter zum Wasserhahn Bis genügend Wasser herauskommt Drehe den Wasserhahn auf Wiederhole Bis der Kaffeebehälter ausreichend gefüllt ist Halte den Kaffeebehälter unter den Wasserhahn Wiederhole Ziehe den Kaffeebehälter zurück Bis der Wasserhahn geschlossen ist Drehe den Wasserhahn zu Wiederhole Gehe zur Kaffeemaschine Fülle das Wasser ein Stelle den Kaffeebehälter in die Kaffeemaschine Gehe zum Küchenschrank Nimm den Kaffee heraus Nimm einen Kaffeefilter heraus Gehe zur Kaffeemaschine Lege den Kaffeefilter ein Bis genügend Kaffee eingefüllt ist Fülle Kaffee ein Wiederhole Schalte die Kaffeemaschine ein Gehe zum Küchenschrank Stelle den Kaffee hinein Bis der Kaffee fertig ist Warte Wiederhole Nimm den Kaffeebehälter Bringe den Kaffeebehälter zu mir
Dieser Algorithmus arbeitet schon mit wichtigen ProgrammstrukturElementen, nämlich Schleifen. Die erste Schleife
Schleifen
Bis genügend Wasser herauskommt Drehe den Wasserhahn auf Wiederhole
führt z. B. die Anweisung Drehe den Wasserhahn auf so oft wiederholt aus, bis die Bedingung, die oben im Schleifenkopf angegeben ist (Bis genügend Wasser herauskommt), erfüllt ist. Ist die Bedingung erfüllt, wird die nächste Anweisung nach Wiederhole ausgeführt. Die in einer Schleife wiederholten Anweisungen – abweichend vom obigen Beispiel können das auch mehrere sein – rückt man üblicherweise wie im Beispiel etwas ein, um zu erkennen, dass diese Anweisungen zu der Schleife gehören.
$OJRULWKPHQ
169
Sandini Bib
Aber der Algorithmus ist, wie schon gesagt, noch nicht perfekt. Was passiert z. B., wenn kein Wasser vorhanden ist? Der Algorithmus bleibt in einer Endlosschleife hängen (wenigstens so lange, bis wieder Wasser vorhanden ist). Wir müssen also noch etwas nachbessern, nämlich so, dass wir nachfragen, ob Wasser vorhanden ist: Gehe zur Kaffeemaschine Nimm den Kaffeebehälter heraus Gehe mit dem Kaffeebehälter zum Wasserhahn Drehe den Wasserhahn auf Wenn Wasser aus dem Wasserhahn kommt Bis genügend Wasser herauskommt Drehe den Wasserhahn auf Wiederhole Bis der Kaffeebehälter ausreichend gefüllt ist Halte den Kaffeebehälter unter den Wasserhahn Wiederhole ... Sonst Breche ab und melde, dass kein Wasser vorhanden ist Ende Wenn Verzweigungen
Sie sehen den kleinen Unterschied: Der Algorithmus sorgt dafür, dass der Roboter den Wasserhahn vorher aufdreht und überprüft, ob Wasser herauskommt. Wenn Wasser herauskommt, wird der Wasserhahn weiter aufgedreht, bis genügend Wasser eingefüllt ist, und danach der weitere Algorithmus abgearbeitet. Wenn kein Wasser vorhanden ist, wird nicht weiter gearbeitet, sondern nur eine Meldung ausgegeben. Diese Verzweigung ist, neben der Schleife, eine andere wichtige Programmstruktur. In diesem einfachen Algorithmus, der ja aus unserer bekannten Umwelt stammt, kommen bereits die wichtigsten Programm-Anweisungen vor: Elementare Anweisungen, die zu Aktionen führen, Schleifen, die Programmteile wiederholt ausführen, und Verzweigungen, die Programmteile bedingungsabhängig ausführen. Der Algorithmus zeigt zudem auch, dass Verzweigungen und Schleifen geschachtelt werden können. Die Schleifen Bis genügend Wasser herauskommt und Bis der Kaffeebehälter ausreichend gefüllt ist werden ja (zusammen mit dem anderen Programmcode, der im Algorithmus oben nicht dargestellt wird) nur dann ausgeführt, wenn die Bedingung Wenn Wasser aus dem Wasserhahn kommt erfüllt ist.
170
%DVLVZLVVHQ
Sandini Bib
3.6.2 Der komplette Kaffeekochen-Algorithmus Am kompletten Algorithmus zum Kaffeekochen erkennen Sie, dass die einzelnen Anweisungen eines Programms zwar normalerweise recht einfach zu verstehen sind, das komplette Programm aber meist nicht auf den ersten Blick erfassbar ist. Ähnlich einem Computer müssen Sie die einzelnen Anweisungen durchgehen, um zu erkennen, was das Programm macht. Das Verstehen eines fremden Programms ist aber normalerweise weitaus schwieriger als die eigene Entwicklung eines Programms. Das größte Problem dabei ist, dass jeder Programmierer eine etwas andere Logik verwendet. Versuchen Sie doch zur Übung einmal, einen eigenen Kaffeekochen-Algorithmus zu entwickeln. Ich denke, Ihr Algorithmus wird wenigstens in Teilen anders aussehen als meiner. Der komplette Kaffeekochen-Algorithmus zeigt aber auch, dass Sie beim Programmieren jeden einzelnen Schritt integrieren müssen und immer auch an Sonderfälle (wie den, dass kein Wasser vorhanden ist) denken müssen. Wenn eine Kaffeemaschine vorhanden ist Gehe zur Kaffeemaschine Wenn ein Kaffeebehälter vorhanden ist Nimm den Kaffeebehälter heraus Wenn ein Wasserhahn vorhanden ist Gehe mit dem Kaffeebehälter zum Wasserhahn Drehe den Wasserhahn auf Wenn Wasser herauskommt Bis genügend Wasser herauskommt Drehe den Wasserhahn auf Wiederhole Bis der Kaffeebehälter ausreichend gefüllt ist Halte den Kaffeebehälter unter den Wasserhahn Wiederhole Ziehe den Kaffeebehälter zurück Bis der Wasserhahn geschlossen ist Drehe den Wasserhahn zu Wiederhole Gehe zur Kaffeemaschine Fülle Wasser ein Stelle den Kaffeebehälter in die Kaffeemaschine Gehe zum Kaffeeschrank Wenn Kaffee vorhanden ist Nimm den Kaffee heraus Wenn Kaffeefilter vorhanden sind Nimm einen Kaffeefilter heraus Gehe zur Kaffeemaschine Lege den Kaffeefilter ein
$OJRULWKPHQ
171
Sandini Bib
Bis genügend Kaffee eingefüllt ist Fülle Kaffee ein Wiederhole Schalte die Kaffeemaschine ein Gehe zum Kaffeeschrank Stelle den Kaffee wieder hinein Bis der Kaffee fertig ist Warte Wiederhole Sonst Melde, dass kein Kaffeefilter vorhanden ist Ende Wenn Sonst Melde, dass kein Kaffee vorhanden ist Ende Wenn Sonst Melde, dass kein Wasser vorhanden ist Ende Wenn Sonst Melde, dass kein Wasserhahn vorhanden ist Ende Wenn Sonst Melde, dass kein Kaffeebehälter vorhanden ist Ende Wenn Sonst Melde, dass keine Kaffeemaschine vorhanden ist Ende Wenn
Dieses Beispiel zeigt eines sehr deutlich: Computer sind ziemlich dumm. Sie müssen dem Computer alles, aber auch wirklich alles sagen, damit dieser korrekt arbeitet. Sie können nicht voraussetzen, dass der Computer abstrakt formulierte Anweisungen wie ein Mensch interpretieren kann. Einem Menschen können Sie einfach sagen: „Geh Kaffee kochen“, einem Computer nicht.
3.6.3 Programme im Computer: Anweisungen, Befehle, Variablen, Schleifen und Verzweigungen Wie Sie nun wissen, bestehen Programme im Computer zumindest aus einzelnen Anweisungen, die je nach Bedarf über Schleifen wiederholt und über Verzweigungen bedingungsabhängig ausgeführt werden. Anweisungen in einer Programmiersprache sind entweder Befehle oder Berechnungen. Befehle führen zu irgendwelchen Aktionen wie beispielsweise dem Einlesen einer Datei oder der Ausgabe einer Meldung. Bei der Verarbeitung von Daten werden auch häufig Ergebnisse berechnet. Eine
172
%DVLVZLVVHQ
Sandini Bib
Anweisung, die eine Berechnung enthält, speichert das Ergebnis der Berechnung häufig zwischen, damit dieses Zwischenergebnis in späteren Anweisungen weiterverwendet werden kann. Zur Zwischenspeicherung werden Variablen verwendet. Variablen sind, wie Sie ja bereits wissen, prinzipiell Teile des Arbeitsspeichers, die unter dem Namen der Variable angesprochen werden können. Variablen besitzen zudem einen Datentyp, der festlegt, wie der Speicherbereich der Variable ausgewertet werden soll, also beispielsweise als Ganzzahl, als Zahl mit Dezimalstellen oder als Text. Variablen werden aber nicht nur zur Speicherung von Zwischenergebnissen, sondern auch zur Speicherung von Benutzereingaben und vielen anderen Daten verwendet, die im Programm an anderer Stelle weiter verwendet werden sollen. Diese Grundbestandteile eines Programms will ich nun an einem Delphi/Kylix-Konsolenprogramm erläutern. Das Programm fordert den Benutzer ähnlich dem Beispielprogramm aus Kapitel 2 auf, einen Bruttobetrag und einen Steuerwert einzugeben, berechnet aus diesen Eingaben einen Nettowert und gibt diesen Wert schließlich aus. Bei der Entwicklung des Algorithmus habe ich hier jedoch Wert darauf gelegt, dass eventuelle Fehleingaben des Benutzers abgefangen werden, der Benutzer in diesem Fall eine entsprechende Meldung erhält und aufgefordert wird, seine Eingabe zu wiederholen. Außerdem soll das gesamte Programm vom Benutzer auch wiederholt werden können. Zunächst stelle ich den Algorithmus im Pseudocode dar: 01 Wiederhole 02 Wiederhole 03 Bruttobetrag eingeben 04 Wenn Bruttobetrag kleiner oder gleich 0 05 Fehlermeldung ausgeben 06 Ende Wenn 07 Bis Bruttobetrag größer 0 08 Wiederhole 09 Steuerwert eingeben 10 Wenn Steuerwert kleiner oder gleich 0 11 Fehlermeldung ausgeben 12 Ende Wenn 13 Bis Steuerwert größer 0 und Steuerwert kleiner / gleich 100 14 Nettobetrag berechnen 15 Nettobetrag ausgeben 16 Eingeben, ob wiederholt werden soll 17 Solange der Anwender wiederholen will
Zum besseren Verständnis habe ich die einzelnen Zeilen mit Zeilennummern versehen.
$OJRULWKPHQ
173
Sandini Bib
Verstehen Sie diesen Algorithmus? Dann kennen Sie bereits die wichtigen Programmier-Grundlagen! Für den Fall, dass Sie noch Verständnisprobleme haben (was für Programmier-Anfänger absolut in Ordnung ist), beschreibe ich kurz, wie der Algorithmus funktioniert. Das Programm startet in Zeile 1, direkt mit einer Schleife, die in der letzten Zeile abgeschlossen wird. Die Schleife soll bewirken, dass die komplette Berechnung so lange wiederholt wird, wie der Anwender sich dazu entscheidet, die Berechnung noch einmal auszuführen. Die Entscheidung, ob wiederholt werden soll, trifft der Anwender in Zeile 16. In Zeile 2 beginnt eine innere Schleife, die in Zeile 7 abgeschlossen wird. Innerhalb dieser Schleife wird der Anwender aufgefordert, einen Bruttobetrag einzugeben. Der eingegebene Betrag wird in einer Variablen zwischengespeichert. Danach überprüft das Programm, ob der eingegebene Bruttobetrag größer als 0 ist, und gibt im Fehlerfall eine entsprechende Meldung aus. Die Bedingungsprüfung im Schleifenabschluss (in Zeile 7) sorgt dafür, dass die Schleife (also die Eingabe des Bruttobetrags) so lange wiederholt ausgeführt wird, wie der Anwender einen ungültigen Betrag eingibt. Ist der eingegebene Bruttobetrag größer als 0, wird die erste Eingabeschleife beendet und das Programm an Zeile 8 weiter ausgeführt. In dieser Zeile beginnt direkt eine neue Schleife, die der ersten Eingabeschleife ähnelt. Im Unterschied zu dieser wird hier aber ein Steuerwert eingegeben. Wenn auch der Steuerwert gültig eingegeben wurde, erfolgt in Zeile 14 dann die eigentliche Berechnung. Der so berechnete Nettowert wird in Zeile 15 ausgegeben. In Zeile 16 erwartet das Programm dann noch eine Entscheidung des Anwenders für die Wiederholung der gesamten Berechnung. In Delphi/Kylix programmiert, sieht das Programm dann so aus:
174
%DVLVZLVVHQ
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
program Nettorechner; {$APPTYPE CONSOLE} uses SysUtils; var var var var var var
weiter: string; eingabe: string; brutto: double; steuer: double; netto: double; nettoFormatiert: string;
begin repeat repeat write('Geben Sie den Bruttobetrag ein: '); readln(eingabe); brutto := StrToFloatDef(eingabe, -1); until brutto > 0; repeat write('Geben Sie den Steuerwert ein: '); readln(eingabe); steuer := StrToFloatDef(eingabe, -1); until (steuer > 0) and (steuer <= 100); netto := brutto * (1 - (steuer / (100 + steuer))); write('Nettobetrag: '); nettoFormatiert := FormatFloat('0.00', netto); writeln(nettoFormatiert); write('Noch einmal? '); readln(weiter); until weiter <> 'j'; end.
Die Zeilennummern im Beispiel dienen lediglich der besseren Übersicht und sind nicht Teil des Programms. Sie dürfen diese Nummern nicht eingeben, wenn Sie das Programm nachvollziehen. Das Programm finden Sie auf der beiliegenden CD im Ordner Beispiele\Kapitel 03\B Nettoberechnung unter dem Namen Nettorechner.dpr. Das eigentliche Programm beginnt erst in Zeile 15. Die Zeilen davor und Zeile 33 gehören zum Rahmen einer Konsolenanwendung in Delphi und Kylix bzw. deklarieren Variablen. Den Rahmen einer Konsolenanwendung in Delphi/Kylix kennen Sie ja bereits. Zeile 1 legt fest, dass es
$OJRULWKPHQ
175
Sandini Bib
sich um ein Programm handelt (und nicht beispielsweise um eine Funktions-Bibliothek). In Zeile 3 wird der Typ des Programms definiert (hier eben eine Konsolenanwendung). Zeile 5 bestimmt, dass die Bibliothek Sysutils eingebunden werden soll. Diese Bibliothek enthält viele wichtige grundlegende Funktionen der Sprache. Die Zeilen 7 bis 12 sind bereits Teil des Programms. Hier werden Variablen deklariert, die im Programm benötigt werden. In Delphi/Kylix müssen Variablen wie in den meisten anderen Programmiersprachen vor der Verwendung deklariert (angemeldet) werden, damit der Compiler diese Variablen kennt und weiß, dass er einen entsprechend großen Speicherplatz reservieren muss. Zeile 14 steht lediglich dafür, dass das eigentliche Programm beginnt. Das Programm wird dann auch in Zeile 33 passend abgeschlossen. Eingabe in Schleifen
In Zeile 15 beginnt das Programm dann mit einer Schleife, wie es der Pseudocode vorsieht. Zeile 17 gibt über die Write-Prozedur einen Text an der Konsole aus, der den Anwender auffordert, einen Bruttobetrag einzugeben. Zeile 18 liest die Eingabe des Anwenders über die Readln-Prozedur ein und speichert diese in der Variablen eingabe. Diese Variable besitzt den Datentyp String und kann deshalb beliebige Textdaten verwalten. Da der Anwender aber Zahlen eingeben soll (sonst kann das Programm nicht rechnen), konvertiert das Programm in Zeile 19 die Texteingabe in eine Zahl. Dazu verwende ich die Funktion StrToFloatDef, die ein Teil der Bibliothek von Delphi/Kylix ist. Diese Funktion versucht, einen übergebenen Text (hier die Variable eingabe) in eine Dezimalzahl zu konvertieren. Schlägt die Konvertierung fehl, weil der Text keine gültige Zahl darstellt, gibt diese Funktion die Zahl zurück, die ihr an letzter Stelle übergeben wurde (hier -1). Gibt der Anwender eine gültige Zahl ein, wird also diese in die Variable brutto geschrieben, ansonsten die Zahl -1. Die Verwendung dieser Funktion ist ein guter Trick zum Abfangen ungültiger Zahleingaben in Delphi oder Kylix. Wie Sie ja bereits aus Kapitel 2 wissen, erzeugt Delphi/Kylix eine Ausnahme, wenn Sie Eingaben direkt in eine Zahlvariable schreiben und der Anwender ungültige Daten eingibt. Mit der Eingabe in eine Textvariable und der Anwendung von StrToFloatDef vermeiden Sie diese Ausnahme. Diesen Trick habe ich übrigens über die Newsgroup-Archiv-Suche von Google recherchiert .
-
Wie im Pseudocode wird diese Eingabe innerhalb der inneren Schleife so lange wiederholt, bis der eingegebene Bruttobetrag größer als Null ist, was in Zeile 20 überprüft wird. Die gesamte Schleife zeigt Ihnen auch, dass die Umsetzung von Algorithmen, die in Pseudocode oder ähnlichen Techniken vorliegen, manchmal etwas mehr Arbeit erfordert, als es im Algorithmus sichtbar ist.
176
%DVLVZLVVHQ
Sandini Bib
Die Eingabe des Steuerwerts beginnt dann in Zeile 21 und ist (natürlich) prinzipiell auf dieselbe Weise programmiert wie die Eingabe des Bruttobetrags. Der Steuerwert wird in der Variablen steuer gespeichert. Die Bedingungsprüfung am Ende der Schleife (Zeile 25) ist etwas komplexer, weil hier zwei Bedingungen überprüft werden. In Zeile 26 wird im Anschluss dann der Nettobetrag in die Variable netto berechnet. An dieser Stelle kann das Programm absolut sicher sein, dass in den Variablen brutto und steuer nur Zahlen gespeichert sein können, was daran liegt, dass diese Variablen einen entsprechenden Datentyp aufweisen.
Berechnung
Zeile 27 gibt dann den beschreibenden Text für das Ergebnis an der Konsole aus. Zeile 28 stellt wieder eine Besonderheit dar. In dieser Zeile wird der berechnete Nettobetrag so umformatiert, dass dieser mit zwei Stellen hinter dem Komma dargestellt wird. Das Ergebnis wird zunächst in die Variable nettoFormatiert geschrieben. Diese Formatierung ist notwendig, da die Writeln-Prozedur (über die Daten so ausgegeben werden, dass hinter der Ausgabe ein Zeilenumbruch erfolgt) numerische Werte (leider) normalerweise in der wissenschaftlichen Darstellung ausgibt. Die Zahl 1000 würde ohne eine Formatierung beispielsweise als 1.00000000000000E+0003 ausgegeben werden (was so viel heißt wie 1,0 * 103). Der besser lesbare formatierte Zahlwert wird dann in Zeile 29 ausgegeben.
Ausgabe
In Zeile 30 schreibt das Programm schließlich den Text „Noch einmal?“ in die Konsole und wartet in Zeile 31 auf die Eingabe des Anwenders. Nur die Betätigung von (j) wird als Wiederholungs-Wunsch gewertet, in diesem Fall wird die äußere Schleife aufgrund der Bedingungsprüfung in Zeile 32 wiederholt und der Anwender kann die Berechnung erneut ausführen.
Wiederholung des Programms
Abbildung 3.12 zeigt das Programm in Aktion. Die äußere Schleife ist bereits zweimal durchgelaufen und wartet zurzeit auf die Eingabe des Anwenders. In der ersten Schleife zur Eingabe des Bruttobetrags hat das Programm festgestellt, dass der Anwender einen ungültigen Betrag (75oo) eingegeben hat und die Eingabeaufforderung wiederholt.
Abbildung 3.12: Das Beispielprogramm in Aktion
$OJRULWKPHQ
177
Sandini Bib
3.7
Zusammenfassung
Um die Arbeitsweise des Computers zu erklären, kennen Sie nun die Begriffe CPU, Arbeitsspeicher, Controller, Treiber, Betriebssystem und BIOS. Sie wissen, was Maschinensprache prinzipiell ist und warum der Computer nur diese versteht. Sie können die wichtige Bedeutung des Betriebssystems für die Ausführung von Programmen beschreiben. Daneben wissen Sie, was das EVA-Prinzip ist, und können erklären, wie Daten prinzipiell im Computer gespeichert werden. Begriffe wie Duales Zahlensystem oder Hexadezimalsystem sind für Sie keine Fremdworte. Sie kennen den Unterschied zwischen der Speicherung von Zahlen, Texten und binären Daten und wissen in diesem Zusammenhang, was einen ASCII-Zeichensatz vom Unicode-Zeichensatz unterscheidet. Weiterhin können Sie erläutern, was einen Assembler von einem Compiler und diesen von einem Interpreter unterscheidet, und kennen die wichtigsten Vor- und Nachteile. Der Unterschied zwischen einem Maschinencode-Compiler und einem Zwischencode-Compiler ist Ihnen bekannt. Sie wissen nun, warum ein unter Linux in Maschinencode kompiliertes Programm nicht unter Windows ausgeführt werden kann und warum dies andererseits bei Java-Programmen möglich ist. Dann haben Sie noch einen Überblick über die verschiedenen aktuellen Anwendungsarchitekturen und die zurzeit wichtigsten Programmiersprachen, ohne diese allerdings näher zu kennen. Schließlich wissen Sie, wie Sie einen Algorithmus zur Lösung einer gestellten Aufgabe zunächst theoretisch, quasi „auf dem Papier“ entwickeln. Dabei kennen Sie mit dem Pseudocode ein Mittel zur Darstellung eines Algorithmus. Sie wissen nun, dass Algorithmen eigentlich immer mit Schleifen und Verzweigungen arbeiten. Einfache Schleifen und Verzweigungen können Sie vielleicht bereits in Delphi oder Kylix umsetzen, in Kapitel 5 erfahren Sie aber noch mehr dazu.
178
%DVLVZLVVHQ
Sandini Bib
3.8
Fragen und Übungen
1. Warum kann ein Programm mathematische Berechnungen nur mit
einer eingeschränkten Genauigkeit ausführen? 2. Was unterscheidet einen ASCII-Zeichensatz vom Unicode-Zeichen-
satz? 3. Nennen Sie zwei Unterschiede zwischen einem Quellcode-Interpre-
ter und einem Maschinencode-Compiler. 4. Was unterscheidet ein Zwischencode- von einem Maschinencode-
Programm? 5. Nennen Sie zwei Vorteile einer verteilten gegenüber einer normalen
Anwendung. 6. Warum müssen Sie einem Computer absolut exakt mitteilen, was er
zu tun hat, wenn Sie ein Programm schreiben?
179
Sandini Bib
Sandini Bib
4
Grundlagen der Programmierung
Sie lernen in diesem Kapitel:
le
n e rn
• wie Sie grundsätzlich mit Variablen umgehen, • welche Bedeutung Datentypen besitzen und wie Sie mit diesen im Programm umgehen, • wie elementare Anweisungen in Java und Delphi/Kylix geschrieben werden und welche verschiedenen elementaren Anweisungen es gibt, • welche Bedeutung die Bibliotheksfunktionen der Programmiersprachen besitzen und wie Sie dazu Informationen finden, • wie Sie logische Fehler in Ihren Programmen finden und beseitigen und • wie Sie Fehler, die durch ungültige Eingaben entstehen (und andere Ausnahmen) in Ihren Programmen abfangen. In diesem Kapitel vermittle ich das grundlegende Handwerkszeug eines Programmierers. Sie lernen zunächst mit Variablen umzugehen, um Daten zwischenspeichern zu können. Dann erfahren Sie alles Wichtige über Datentypen (mit denen Sie im Programm sehr häufig arbeiten). Danach beginnt der praktische Teil, in dem ich Ihnen zeige, wie Sie einfache Algorithmen in Delphi, Kylix und Java umsetzen. Die Strukturierung eines Programms über Verzweigungen und Schleifen wird übrigens erst im nächsten Kapitel behandelt. Schließlich erfahren Sie noch, wie Sie den in Delphi, Kylix und Sun ONE Studio 4 integrierten Debugger nutzen, um Fehler in Ihren Programmen zu suchen und wie Sie Fehler, die in der Laufzeit des Programms auftreten, behandeln.
181
Sandini Bib
4.1
Variablen
Variablen sind, wie Sie nun wohl schon gemerkt haben, ein sehr wichtiges Werkzeug beim Programmieren. Variablen nutzen Sie immer dann, wenn Sie Daten im Arbeitsspeicher ablegen müssen, um diese im weiteren Verlauf des Programms weiter zu verwenden. In den meisten Programmiersprachen müssen Sie Variablen, die Sie verwenden wollen, vor der Verwendung deklarieren. Mit einer Deklaration teilen Sie dem Compiler oder Interpreter mit, • dass dieser einen zum Datentyp der Variablen ausreichend großen Speicherbereich im Arbeitsspeicher reservieren soll und • Ihnen im Programm über den Namen der Variablen Zugriff auf diesen Speicherbereich gibt und • dass der Compiler bzw. Interpreter den Speicherbereich entsprechend dem bei der Deklaration angegebenen Datentyp auswerten soll. Bei der Deklaration geben Sie den Datentyp der Variablen an. Die einzelnen möglichen Datentypen und den Umgang damit bespreche ich im nächsten Abschnitt. Hier erfahren Sie zunächst, wie Sie Variablen deklarieren. Sie lernen den Umgang mit Variablen bereits hier kennen, weil die folgenden Abschnitte schon sehr viel Gebrauch von Variablen machen. Wenn Sie z. B. in Delphi/Kylix eine Variable vom Typ Integer deklarieren, reserviert der Pascal-Compiler einen vier Byte großen Speicherbereich für diese Variable und verbindet diesen mit Ihrem Variablennamen. Über den Namen haben Sie Zugriff auf die Variable. Der Compiler erkennt an der Deklaration den Datentyp und weiß damit, wie er den reservierten Speicherbereich auswerten soll, wenn Sie diese Variable in einer Anweisung verwenden: Var i: Integer; begin i := 10; end;
Wenn Sie eine Variable deklarieren, gehört der entsprechende Speicherbereich Ihrem Programm. Kein anderes Programm kann auf diesen Speicherbereich zugreifen und den gespeicherten Wert überschreiben oder auslesen. Sie können absolut sicher sein, dass nur Ihr Programm mit den Variablen arbeiten kann. Und Sie können absolut sicher sein, dass der Wert der Variablen so lange bestehen bleibt, bis Ihr Programm die Variable auf einen anderen Wert setzt.
182
Sandini Bib
Der Bezeichner einer Variablen Eine Variable wird durch einen Bezeichner benannt, den Sie bei der Deklaration angeben. Dieser Bezeichner muss bestimmten Regeln folgen, damit der Compiler damit umgehen kann. Sie können nicht alle Zeichen für einen Bezeichner verwenden. In allen Sprachen können Sie Bezeichner aber nach dem folgenden Schema bilden: • Der Bezeichner muss mit einem Buchstaben beginnen, • er kann danach Buchstaben, Zahlen und den Unterstrich enthalten, • er darf nicht wie ein Schlüsselwort der Programmiersprache benannt sein, • er sollte keine Umlaute beinhalten (manche Sprachen mögen keine Umlaute). Halten Sie sich möglichst immer an dieses Schema, auch dann, wenn Ihre Programmiersprache auch andere Zeichen erlaubt. Wenn Sie Ihr Programm einmal in eine andere Sprache umsetzen müssen, haben Sie damit ein Problem weniger. Manche Sprachen schränken die maximal mögliche Länge von Variablennamen ein. Diese Länge ist jedoch normalerweise so groß definiert, dass Sie eigentlich nie Probleme mit der Benennung von Variablen haben werden. Ich habe in meiner ganzen Programmierpraxis noch nie die Maximallänge für einen Bezeichner erreicht. Benennen Sie Ihre Variablen immer sehr aussagekräftig. Eine Bezeichnung wie „Variable1“ sagt nichts über den Zweck der Variablen aus. Aussagekräftig benannte Variablen machen ein Programm (in Verbindung mit anderen Techniken wie Kommentaren) leichter lesbar. Nennen Sie eine Variable, die eine Einkommensteuer speichert, also z. B. ruhig einkommensteuer. Vielen Programmierern ist das zu viel Arbeit und sie kürzen Variablennamen kryptisch ab. Für die Speicherung einer Einkommensteuer würden diese Programmierer vielleicht eine mit est bezeichnete Variable verwenden. Wenn ein solches Programm dann aber von einem anderen Programmierer verstanden werden muss, z. B. weil er das Programm abändern soll, führen solche kryptischen Bezeichner zu zusätzlichen Verständnisproblemen. Das kann sogar Ihnen mit Ihren eigenen Programmen passieren, wenn Sie diese einige Monate nach der ersten Programmierung verändern müssen. Dann wissen Sie häufig nicht mehr, was Sie mit dem einen oder anderen Bezeichner gemeint haben. Aussagekräftige Bezeichner sind (neben einer guten Kommentierung und anderen Dingen) übrigens auch ein wichtiges Kriterium bei der Bewertung der Qualität einer Programmierung. Eine Ausnahme von dieser Regel ist der Name i. i kennt jeder Programmierer als Ganzzahl-Variable, die für Zählzwecke verwendet wird.
9DULDEOHQ
183
Sandini Bib
Konvention zur Benennung
Ich verwende bei der Benennung meiner Variablen eine Konvention, die sehr häufig in Java- und in C#-Programmen eingesetzt wird. Variablen, die nur eine lokale Gültigkeit besitzen (wie Sie in Kapitel 5 sehen werden, gibt es auch Variablen, die global, im gesamten Programm gelten) beginnen danach immer mit einem Kleinbuchstaben und werden zunächst auch klein weitergeschrieben. Setzt sich der Name aus mehreren Teilnamen zusammen, werden diese durch einen groß geschriebenen Buchstaben voneinander optisch getrennt. Eine Variable, die einen Vornamen speichern soll, heißt demnach vorname. Eine Variable, die den linken Rand beim Ausdruck von Texten verwalten soll, heißt linkerRand. Die Deklaration Variablen können an verschiedenen Stellen im Programm deklariert werden. Ich gehe darauf noch nicht ein, weil Sie dazu erst wissen müssen, was eine Prozedur, Funktion, Methode, ein Modul und eine Klasse ist. Der Ort der Deklaration bestimmt den Gültigkeitsbereich von Variablen, also an welchen Stellen die Variable im Programm verwendet werden kann. Dieses wichtige Thema bespreche ich in Kapitel 5. Für die einfachen Beispiele reichen Variablen aus, die nur dort gelten, wo sie deklariert sind.
Variablen in Java
In Java-Programmen können Sie Variablen an der Stelle deklarieren, an der Sie diese benötigen. Die Deklaration beginnt immer mit dem Datentyp, gefolgt von einem oder mehreren Bezeichnern für einzelne Variablen und wird mit einem Semikolon abgeschlossen: class variablen { public static void main(String args[]) { /* Deklaration einer Variablen vom Typ Integer */ int i; /* Deklaration zweier Variablen vom Typ String */ String vorname, nachname; ... } }
184
Sandini Bib
Die Anweisungen in den Zeichen /* und */ sind Kommentare, die vom Compiler einfach ignoriert werden. Ich verwende diese, um Ihnen den Quelltext zu erläutern. Bei der Deklaration können Sie die Variablen in Java gleich initialisieren (mit einem Wert versehen). Geben Sie den Wert dazu einfach in einer Zuweisung hinter dem Variablennamen an: int i = 1; String vorname = "-", nachname = "-";
Wenn Sie keine Initialisierung vornehmen, wird die Variable per Voreinstellung mit einem Leerwert initialisiert. Bei numerischen Variablen ist das die Zahl 0, bei Zeichenketten-Variablen eine leere Zeichenkette. Die Variable kann nun in Anweisungen unterhalb der Deklaration verwendet werden. Deklarieren Sie die Variable innerhalb eines Anweisungsblocks (siehe Seite 217), so gilt diese Variable nur innerhalb dieses Blocks. Außerhalb des Blocks können Sie die Variable nicht verwenden.
Blockweise Gültigkeit
01 class variablen 02 { 03 public static void main(String args[]) 04 { 05 int i; 06 for (i=0; i<3; i++) 07 { 08 int ergebnis = ergebnis * i; 09 } 10 System.out.println(ergebnis); // Fehler "cannot resolve symbol" 11 } 12 }
Die Variable ergebnis ist in diesem Beispiel im Anweisungsblock der forSchleife deklariert. Die for-Schleife wird verwendet, um einen Programmteil eine festgelegte Anzahl an Durchgängen zu wiederholen. Die Deklaration innerhalb der Schleife bewirkt, dass außerhalb der Schleife kein Zugriff möglich ist. In Delphi- und Kylix-Programmen werden Variablen so deklariert, dass Sie zuerst das Schlüsselwort var angeben, gefolgt von einem oder mehreren Bezeichnern für einzelne Variablen, die durch Kommata getrennt angegeben werden. Die Deklaration wird mit einem Doppelpunkt, dem Datentyp und einem Semikolon abgeschlossen:
Variablen in Delphi/Kylix
var i: Integer; var vorname, nachname: String;
9DULDEOHQ
185
Sandini Bib
In Delphi/Kylix können Sie Variablen leider nicht da deklarieren, wo Sie diese gerade verwenden wollen. Variablen werden immer mindestens im Kopfbereich einer Prozedur, Funktion oder Methode deklariert (mindestens deswegen, weil es auch noch die Möglichkeit gibt, Variablen global zu deklarieren, aber das beschreibe ich erst in Kapitel 5). Eine in einer Funktion zur Berechnung der Fakultät einer Zahl benötigte Variable muss beispielsweise zwischen dem Funktionskopf und dem Beginn des Funktionsrumpfes deklariert werden: function Fakultaet(Zahl: integer): integer; { Deklaration der Variablen, die innerhalb der Funktion verwendet werden } var i: integer; begin ... end;
Der Startblock einer Delphi/Kylix-Anwendung bildet die einzige Ausnahme. Da hier kein Kopf existiert, müssen Sie Variablen, die im Startblock verwendet werden sollen, über dem Begin-Schlüsselwort deklarieren: program Hello_World; uses SysUtils; var eingabe: string; begin { ... } end.
Die Zuweisung eines Initialwerts ist in Delphi/Kylix bei der Deklaration leider nicht möglich. Variablen werden aber genau wie bei Java implizit mit einem Leerwert initialisiert. Variablen in Anweisungen Variablen können Sie überall dort einsetzen, wo ein bestimmter Datentyp erwartet wird. Der Datentyp der Variablen muss lediglich zu dem erwarteten Datentyp passen. Eine Variable können Sie somit z. B. in arithmetischen Ausdrücken einsetzen. Das folgende Java-Beispiel demonstriert dies: int i = 10; int ergebnis = i + 1;
186
Sandini Bib
Sie können Variablen auch an Funktionen, Prozeduren oder Methoden übergeben: int i = 10; double ergebnis = Math.sin(i);
oder in Vergleichsausdrücken verwenden: int i = 10; if (i == 10) System.out.println("i ist gleich 10");
Vergleichsausdrücke beschreibe ich erst in Kapitel 5. Ich denke aber, der hier dargestellte Ausdruck spricht für sich ...
4.2
Grundlagen zu Datentypen
Bevor Sie in diesem Kapitel mit dem Programmieren beginnen, sollten Sie zunächst noch einige wichtige Dinge über Datentypen wissen. In Programmen arbeiten Sie sehr häufig mit Datentypen. Gut, wenn Sie dann wissen, worum es sich dabei handelt und welche Fallen sich Ihnen bei der Arbeit in den Weg stellen. Der sichere Umgang mit Datentypen ist für Programmierer eine der wichtigsten Grundlagen für eine saubere und fehlerfreie Programmierung. Deshalb (leider) ein wenig weitere Theorie:
4.2.1 Datentypen in einem Programm Programme speichern sehr häufig Daten in Variablen, übergeben Daten an eine Prozedur oder Funktion, verwenden diese in Berechnungen usw. Der folgende Delphi/Kylix-Quellcode enthält z. B. einige Daten, die hervorgehoben dargestellt werden. Zur besseren Übersicht habe ich Kommentare in den Quellcode eingefügt. Kommentare werden von Object Pascal in die Zeichen { und } eingeschlossen und vom Compiler einfach ignoriert. 01 02 03 04 05 06 07 08 09
program Data_Demo; uses SysUtils; {$APPTYPE CONSOLE} { Eine Variable } var i: integer;
187
Sandini Bib
10 begin 11 { Eine Variable und zwei konstante Werte } 12 i := 1 + 2; 13 { Aufruf einer Prozedur unter Übergabe eines Datums } 14 writeln(i); 15 16 readln; 17 end.
Für den Fall, dass Sie sich über den letzten Kommentar im Quellcode wundern: Die Einzahl des Begriffs „Daten“ ist „Datum“. i ist eine Variable, die ein Datum speichert, die Zahlen 1 und 2 sind so genannte Konstante (weil deren Werte im laufenden Programm nicht geändert werden können). In Zeile 12 wird die Summe aus diesen Konstanten der Variablen i zugewiesen. Diese Variable wird in Zeile 14 als Argument einer Prozedur eingesetzt. Alle diese Daten besitzen einen Datentyp. Der Datentyp legt, wie der Name schon sagt, den Typ der Daten fest. Für den Compiler oder Interpreter ist ein Datentyp eine Aussage, auf welche Weise er die entsprechenden Speicherbereiche auswerten soll und wie groß diese sind. Die Variable i besitzt im Quellcode beispielsweise den Datentyp Integer, der aussagt, dass ein 4-Byte-Speicherbereich verwendet wird und dass dieser als Ganzzahl mit Vorzeichen ausgewertet werden soll. Datentypen begegnen Ihnen in einem Programm immer wieder. Wenn Sie eine Variable deklarieren, müssen Sie den Datentyp angeben. Wenn Sie an eine Prozedur ein Argument übergeben, muss dieses einen passenden Datentyp besitzen. Wenn Sie eine Berechnung vornehmen und das Ergebnis dieser Berechnung in eine Variable schreiben, muss der Datentyp der Berechnung zu dem Datentyp der Variable passen. Daten im Arbeitsspeicher
Der Compiler/Interpreter sorgt dafür, dass immer für alle Daten eines Programms im Arbeitsspeicher Platz reserviert wird. Dazu gehören nicht nur Variablen, sondern auch Konstanten und sogar die Ergebnisse von Berechnungen. Dass Konstanten auch Platz im Arbeitspeicher belegen (und auch einen Datentyp besitzen), können Sie noch sehr einfach mit der Überlegung nachvollziehen, dass diese ja irgendwo gespeichert sein müssen, wenn das Programm ausgeführt wird. Dass aber auch für das Ergebnis einer Berechnung Platz benötigt wird, ist nicht mehr so einfach nachzuvollziehen. Eine nette Testfrage für erfahrene Programmierer wäre die folgende: Wie viele Speicherplätze enthält die folgende Anweisung? i := 1 + 2;
188
Sandini Bib
Richtig: Vier. Die Variable, die beiden Konstanten und das Ergebnis der Berechnung. Sie werden sich vielleicht fragen, warum für das Ergebnis der Berechnung auch Speicherplatz benötigt wird, wo dieses doch in die Variable geschrieben wird. Die Antwort auf diese Frage ist, dass Anweisungen nicht immer so einfach sind wie im Beispiel und der Compiler/ Interpreter deshalb gar nicht anders vorgehen kann. Lösen Sie doch bitte einmal die folgende Berechnung auf: (2 + 3) * 4
Wie haben Sie das gemacht? Sie haben wahrscheinlich erst die Zahlen in den Klammern addiert und das Ergebnis dann zwischengespeichert, um dieses danach mit 4 zu multiplizieren. Anders können Sie bereits diese einfache Aufgabe gar nicht lösen. Und genau das macht auch der Compiler. Er rechnet also erst die Addition in den Klammern aus, speichert das Ergebnis in einem separaten Speicherbereich (quasi in einer internen Variablen) und multipliziert diesen dann mit der 4.
Implizites Speichern von Zwischenergebnissen
Wenn das Ergebnis dieser Berechnung dann in eine Variable geschrieben würde, i := (2 + 3) * 4;
wüssten wir, dass wir dies sofort ausführen könnten. Ein Compiler/ Interpreter kann aber normalerweise nicht erkennen, dass mit dem Ergebnis nicht noch weiter gerechnet werden soll. Es kann z. B. ja auch sein, dass das Ganze noch durch 2 geteilt wird: i := ((2 + 3) * 4) / 2;
Deshalb sieht ein Compiler oder Interpreter für alle Zwischenergebnisse einen separaten Speicherbereich vor, der dann im jeweils nächsten Schritt weiterverarbeitet wird. Jeder dieser Speicherbereiche besitzt natürlich auch einen Datentyp. Die verschiedenen Programmiersprachen kennen nun eine große Anzahl an Datentypen, wobei sich die einzelnen Sprachen natürlich etwas unterscheiden. Grundsätzlich kann man aber zunächst einmal zwischen einfachen und komplexen Datentypen unterscheiden. Einfache Datentypen speichern einen einfachen Wert, z. B. eine Ganzzahl, eine Dezimalzahl oder einen Text. Komplexe Datentypen speichern gleich mehrere Werte. Komplexe Datentypen werden erst im nächsten Kapitel beschrieben, hier geht es erst einmal um die einfachen. Einfache Datentypen können unterteilt werden in:
Einfache und komplexe Datentypen
• Datentypen für Zahlen, • Datentypen für Datumswerte, • Datentypen für Zeichenketten
189
Sandini Bib
• und den Datentyp für boolesche Werte.
4.2.2 Numerische Datentypen Mit Daten, die einen numerischen Datentyp besitzen, können Sie im Programm arithmetische Berechnungen durchführen (einfacher gesagt: Sie können mit diesen Datentypen rechnen). Diese Daten können nur gültige Zahlen speichern (keine Texte und andere Daten wie Datumswerte). Wie Sie bereits in Kapitel 3 erfahren haben, werden Zahlen im Computer auf verschiedene Weise gespeichert: Als Ganzzahl mit und ohne Vorzeichen und als Dezimalzahl. Dabei werden unterschiedlich große Speicherbereiche verwendet, sodass die Zahlen mehr oder weniger groß sein und – bei Dezimalzahlen – mehr oder weniger viele Nachkommaziffern verwalten können. Da die grundlegende Speicherung im Computer immer dieselbe ist, sind die Zahldatentypen bei den verschiedenen Programmiersprachen größtenteils identisch. Den Datentypen Integer (der allerdings manchmal anders bezeichnet wird) finden Sie beispielsweise in allen modernen Sprachen. Einige Programmiersprachen wie C und C++ bieten auch mehr Datentypen als andere Sprachen. Das ist aber oft eher verwirrend als hilfreich. Grob können Sie die Datentypen für ganze Zahlen und die für Dezimalzahlen unterscheiden. Ganze Zahlen werden im allgemeinen Sprachgebrauch als Integer-Zahlen bezeichnet, Dezimalzahlen als Fließkommaoder Reelle Zahlen bzw. Festkommazahlen. In der Delphi-Hilfe finden Sie eine Übersicht über die Zahldatentypen, wenn Sie im Index nach „integer types“ und „real types“ suchen, in Kylix suchen Sie dazu nach „Integer-Typen“ und „Reelle Typen“. Die Java-Beschreibung dieser Typen finden Sie in der Sprachspezifikation an der Adresse java.sun.com/docs/books/jls/second_edition/html/ jTOC.doc.html im Inhaltsverzeichnis unter der Überschrift 4 TYPES, VALUES, AND VARIABLES. Integer-Datentypen Integer-Datentypen speichern Ganzzahlen, deren Wertebereich vom verwendeten Speicher und davon abhängt, ob ein Vorzeichen verwaltet wird. Die Grundlagen dieser Speichertechnik habe ich bereits in Kapitel 3 erläutert. Integer-Zahlen werden normalerweise in einem, zwei, vier oder acht Byte gespeichert. Damit ergeben sich verschiedene Zahlbereiche. Ein Zwei-Byte-Integertyp kann beispielsweise ohne Vorzeichen Zahlen von 0 bis 65536 und mit Vorzeichen Zahlen von –32768 bis
190
Sandini Bib
32767 speichern. Das liegt, wie Sie ja bereits wissen, daran, dass die Zahlen in Form einzelner Bits im Dualsystem gespeichert werden und dass bei Zahlen mit Vorzeichen das linke Bit für das Vorzeichen verwendet wird. Tabelle 4.1 zeigt die Integer-Datentypen von Delphi/Kylix und Java. Die wichtigsten habe ich hervorgehoben. Wie Sie sehen, besitzt Java weniger Zahldatentypen als Delphi/Kylix. Für die „normale“ Programmierung reichen diese aber vollkommen aus. Allgemeiner Name
Wertebereich
Delphi/KylixName
Java-Name
Größe (Byte)
Byte
0 bis 255
Byte
-
1
Signed Byte
–128 bis 127
Shortint
byte
1
Short Integer; Small Integer
–32768 bis 32767
Smallint
short
2
Word (Unsigned Small Integer)
0 bis 65535
Word
-
2
Integer
–2.147.483.648 bis 2.147.483.647
Integer; Longint
int
4
Unsigned Integer
0 bis 4.294.967.295
Cardinal; Longword
-
4
Long Integer
–9.223.372.036.854.775.808 bis 9.223.372.036.854.775.807
Int64
long
8
Tabelle 4.1: Die Datentypen für ganze Zahlen
Dass Delphi und Kylix in einigen Fällen zwei gleichwertige Datentypen anbieten, ist darin begründet, dass in diesen Fällen einer der Datentypen generisch ist, d. h. vom Betriebssystem verwaltet wird, und der andere von der Programmiersprache verwaltet wird. Generische Datentypen beschreibe ich auf Seite 196. Wie Sie in Tabelle 4.1 sehen, haben Delphi und Java unterschiedliche Meinungen darüber, was „Byte“ und „Short“ bedeuten. Im allgemeinen Sprachgebrauch ist ein Byte-Datentyp ein solcher, der die Zahlen 0 bis 255 speichern kann. Der Java-Byte-Typ ist eher ein „Signed Byte“ (Byte mit Vorzeichen). Unüblich ist die Delphi/Kylix-Bezeichnung für einen 16-Bit-Integerwert mit Vorzeichen. Ein „Short Integer“ („kurzer Integer“) ist genau wie ein „Small Integer“ („kleiner Integer“) normalerweise ein 32-Bit-Integerwert mit Vorzeichen. C++ verwendet beispielsweise (wie Java) für einen 32-Bit-Integertyp mit Vorzeichen den Namen short. Eine bei Ihnen nun möglicherweise aufkommende Frage will ich gleich klären: Warum gibt es so viele unterschiedliche Integer-Datentypen? Die Antwort darauf ist: Weil ein Programm manchmal sehr viele Daten
Warum so viele Datentypen?
191
Sandini Bib
speichern muss. Würde zur Speicherung von Ganzzahlen nur ein LongInteger-Datentyp zur Verfügung stehen, würden viele Programme viel zu viel Speicher in Anspruch nehmen, den sie gar nicht benötigen. Bei einzelnen Variablen müssen Sie sich kaum Gedanken machen, ob Sie das eine oder andere Byte verschwenden. Wenn Sie aber mehrere Tausend oder vielleicht sogar Millionen Daten einlesen, sollten Sie schon überlegen, welcher Datentyp dazu am besten geeignet ist. Zahlen mit Dezimalstellen Wie ich es in Kapitel 3 bereits erläutert habe, werden bei Zahlen mit Dezimalstellen Festkomma- und Fließkommazahlen unterschieden. Bei den Festkommazahlen befindet sich das Dezimaltrennzeichen immer an einer festgelegten Stelle. Delphi und Kylix besitzen nur einen Festkomma-Datentypen, den Currency, der mit seinen vier Dezimalstellen für Währungsberechnungen vorgesehen ist (Währungen werden international normalerweise mit vier Dezimalstellen gerechnet und mit zwei Dezimalstellen dargestellt). Java besitzt keinen Festkommadatentypen. Fließkommadatentypen finden Sie bei Delphi und Kylix auch in einer größeren Anzahl als bei Java. Tabelle 4.2 stellt diese Datentypen dar. Allgemeiner Name
Wertebereich
Delphi/KylixName
JavaName
Größe (Byte)
Currency
–9.223.372.036.854.77,5808 bis 9.223.372.036.854.77,5807
Currency
-
8
Float
1,5 * 10–45 bis 3,4 * 1038 mit bis zu acht Dezimalstellen
Single
float
4
Double
5,0 * 10–324 bis 1,7 * 10308 mit bis zu 16 Dezimalstellen
Double; Real
double
8
Long Double
3,6 * 10–4951 bis 1,1 * 104932 mit bis zu 20 Dezimalstellen
Extended
-
10
Tabelle 4.2: Die Datentypen für Zahlen mit Dezimalstellen Eingeschränkte Genauigkeit
192
Fließkommadatentypen besitzen entsprechend ihrer Speicherung im Computer nur eine eingeschränkte Genauigkeit, die zudem davon abhängt, wie viele Ziffern die Zahl vor dem Komma verwaltet. Ein Singlebzw. float-Wert kann bereits bei einer Zahl mit fünf Ziffern vor dem Komma nur noch zwei Ziffern nach dem Komma genau verwalten. Der Rest wird einfach abgeschnitten, ungenau dargestellt oder manchmal auch gerundet. Um das einmal auszuprobieren, schreiben Sie ein kleines Java-Programm:
Sandini Bib
01 class singleDemo 02 { 03 public static void main(String args[]) 04 { 05 System.out.println("Das Fließkomma-Problem"); 06 float zahl1, zahl2, ergebnis; 07 zahl1 = 1; 08 zahl2 = 3; 09 ergebnis = zahl1 / zahl2; 10 System.out.println(ergebnis); 11 zahl1 = 100000; 12 ergebnis = zahl1 / zahl2; 13 System.out.println(ergebnis); 14 } 15 }
In Zeile 6 deklariert dieses Programm drei Variablen, die den Datentyp float besitzen. Nach der Zuweisung von konstanten Werten in Zeile 7 und 8 teilt das Programm diese Zahlen in Zeile 9 und weist das Ergebnis der Variablen ergebnis zu. Nachdem der Inhalt dieser Variablen ausgegeben wurde, wird die Berechnung in Zeile 12 mit einer neuen ersten Zahl wiederholt. Das Teilen einer 10er-Potenz von 1 durch 3 ergibt eine endlose Periode der Ziffer 3. Daran können Sie sehr schön erkennen, wie genau die Speicherung ist. Das Programm gibt die folgenden Zahlen aus: 0.33333334 33333.332
Wie Sie sehen, kann ein Java-Programm in einem float-Datentypen eine sehr kleine Zahl mit maximal sieben genauen Stellen speichern. Die 4 an der letzten Ziffer deutet bereits auf eine Ungenauigkeit hin. Resultiert allerdings eine Zahl mit fünf Vorkomma-Ziffern, sind nur noch zwei genaue Ziffern hinter dem Komma möglich.
193
Sandini Bib
Zum Vergleich entwickeln wir ein ähnliches Programm in Delphi/Kylix: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17
program SingleDemo; {$APPTYPE CONSOLE} var zahl1, zahl2, ergebnis: Single; begin Writeln('Das Fließkomma-Problem'); zahl1 := 1; zahl2 := 3; ergebnis := zahl1 / zahl2; Eriteln(ergebnis); zahl1 := 100000; ergebnis := zahl1 / zahl2; Eriteln(ergebnis); readln; end.
Das Ergebnis dieses Programms ist: 3.33333343267441E-0001 3.33333320312500E+0004
Die Writeln-Prozedur stellt Fließkommazahlen leider in der wissenschaftlichen Notation dar. Bei dieser Darstellung wird das Komma einer Zahl so verschoben, dass eine Ziffer vor dem Komma übrig bleibt und die Zahl dann mit einer passenden Zehnerpotenz multipliziert. Auf unsere normale Darstellung umgerechnet, sieht das Ergebnis (in der englischen Zahlschreibweise) so aus: 0.333333343267441 33333.3320312500
Wie Sie sehen, hört die Genauigkeit der ersten Zahl wieder an der siebten Ziffer hinter dem Komma auf. Die folgenden Ziffern sind ungenau. Bei der zweiten, größeren Zahl hört die Genauigkeit bereits an der zweiten Ziffer auf. Anders als Java stellt Delphi wesentlich mehr ungenaue Ziffern dar, was aber für die Praxis kaum eine Rolle spielt. Die etwas eigenartigen Daten an den ungenauen Ziffern resultieren aus der komplexen Technik, die bei der Speicherung von Fließkommazahlen eingesetzt wird. Diese Art der Speicherung kann ich im Rahmen dieses Buchs nicht näher erläutern (und ist eigentlich für Sie auch nicht interessant). Im Internet finden Sie mehr dazu im Artikel „Darstellung von Fließkommazahlen“ bei www.it-academy.cc: www.it-academy.cc/content/article_browse.php?ID=377.
194
Sandini Bib
Wenn Sie das Programm etwas abändern, sodass ein genauer Datentyp verwendet wird, erhalten Sie ein wesentlich besseres Ergebnis: 01 class doubleDemo 02 { 03 public static void main(String args[]) 04 { 05 System.out.println("Das Fließkomma-Problem"); 06 double zahl3, zahl4, ergebnis2; 07 zahl1 = 1; 08 zahl2 = 3; 09 ergebnis = zahl1 / zahl2; 10 System.out.println(ergebnis); 11 zahl1 = 100000; 12 ergebnis = zahl1 / zahl2; 13 System.out.println(ergebnis); 14 } 15 }
Das Ergebnis ist nun 0.3333333333333333 33333.333333333336
und schon wesentlich genauer. In einem Delphi/Kylix-Programm können Sie die Genauigkeit noch weiter über den Datentyp Extended steigern. Wenn Sie Fließkommazahlen verwenden, weil Sie mit Dezimalzahlen rechnen müssen, verwenden Sie möglichst nicht den Single-Datentyp. Da dieser Datentyp schon bei mageren fünf Ziffern vor dem Komma nur noch zwei Ziffern hinter dem Komma genau darstellen kann, sind Rechenfehler quasi vorprogrammiert. Wenn Sie hochgenaue Berechnungen vornehmen müssen, verwenden Sie wenn möglich einen passenden Datentyp. Delphi bietet Ihnen dazu den sehr genauen Datentyp Extended.
195
Sandini Bib
Ein Tipp für die Praxis Achten Sie darauf, dass die Datentypen, die Sie (für Variablen, Argumente von Prozeduren etc.) verwenden, ausreichend groß sind. Die kleineren Datentypen (z. B. der Byte-Typ) sind eigentlich nur noch Überbleibsel aus alten Programmier-Zeiten, wo der Speicherplatz im Rechner noch sehr klein war. Heutzutage besitzen Computer so viel Arbeitsspeicher, dass es normalerweise absolut nichts ausmacht, wenn Sie einzelne Daten größer definieren, als es eigentlich sein muss. Wenn Sie aber einen Datentyp zu klein definieren, kommt es schnell zu einem so genannten Überlauf. Ein Überlauf liegt immer dann vor, wenn Sie versuchen, in einen Datentyp einen Wert zu schreiben, der zu groß für diesen Datentyp ist. Überläufe führen immer zu Fehlern in Ihren Programmen, weswegen ich diese vertieft ab Seite 206 behandle. Generische Datentypen Viele Programmiersprachen kennen so genannte generische Datentypen. Ein generischer Datentyp wird direkt vom Betriebssystem verwaltet und ist so groß, dass er genau in ein oder zwei Register der CPU passt. In ihren Registern speichert die CPU bei der Ausführung von arithmetischen Operationen Zwischenergebnisse. Bei 32-Bit-Betriebssystemen sind die Register immer auch 32 Bit groß. Ein generischer Datentyp kann vom Betriebssystem optimal behandelt werden und führt deshalb zu einer sehr guten Performance. Die generischen Datentypen von Delphi und Kylix sind Integer (32-Bit-Ganzzahl mit Vorzeichen), Cardinal (32-Bit-Ganzzahl ohne Vorzeichen) und Real (64-Bit-Fließkommazahl). Java besitzt keine generischen Datentypen, weil Java-Programme auf verschiedenen Betriebssystemen ausgeführt werden (die unterschiedliche generische Datentypen besitzen). Generische Datentypen müssen Sie ein wenig im Auge behalten, wenn Sie auf einem anderen Betriebssystem programmieren. Handelt es sich zum Beispiel um ein 64-Bit-Betriebssystem, so ist ein Integer-Typ ebenfalls 64 Bit groß und besitzt damit einen wesentlich größeren Wertebereich als auf einem 32-Bit-Betriebssystem. Das ist auch der Grund dafür, dass viele Programmiersprachen entweder gar keine generischen Datentypen verwenden (wie bei Java) oder zusätzlich zu generischen Datentypen identische Datentypen mit einer festen Größe anbieten. An Stelle des Datentyps Real können Sie in Delphi/Kylix beispielsweise auch Double verwenden. Bei diesem Typ können Sie sich darauf verlassen, dass dessen Größe nicht auf verschiedenen Betriebssystemen variiert (sofern Delphi/Kylix oder ein ähnliches System noch für andere Betriebssysteme zur Verfügung gestellt werden).
196
Sandini Bib
4.2.3 Datentypen für Zeichen und Zeichenketten In Programmen setzen Sie nicht nur Zahlen ein. Sehr häufig arbeiten Sie mit einzelnen Zeichen und Zeichenketten. Wenn Sie beispielsweise den Namen einer im Programm zu öffnenden Datenbank in einer Konfigurationsdatei verwalten (damit dieser vom Anwender eingestellt werden kann), lesen Sie diesen Namen beim Start des Programms in eine Zeichenketten-Variable ein, um jene dann weiter zu verarbeiten. Die grundlegenden Datentypen für die Arbeit mit Zeichen und Zeichenketten sind Char und String. Ein Char-Datentyp speichert genau ein Zeichen, ein String-Datentyp normalerweise nahezu beliebig viele Zeichen (meist nur durch den verfügbaren Arbeitsspeicher begrenzt). Wie ein Computer Zeichen speichert, wissen Sie bereits aus Kapitel 3. Zeichen werden immer mit Hilfe einer (logischen) Zeichentabelle in ganze, vorzeichenlose Zahlen umgesetzt. Je nach der Art der Tabelle werden dazu zwei Byte (Unicode) oder nur ein Byte (ASCII) verwendet. Unicode-Zeichen-Typen können deswegen 65535 verschiedene Zeichen, ASCII-Zeichen-Typen nur 256 verschiedene Zeichen verwalten. Programmierer müssen sich normalerweise nicht um diese Zeichentabellen kümmern. Eine Programmiersprache sorgt automatisch dafür, dass Zeichen in die entsprechende Zahl umgewandelt werden und umgekehrt. Das gilt besonders dann, wenn das Betriebssystem und die Programmiersprache mit Unicode-Zeichen arbeiten. Da dieser Zeichensatz fast alle Zeichen der Sprachen dieser Welt abbilden kann, besteht kaum Bedarf, eine andere als die Standard-Unicode-Tabelle zu verwenden (neben der Standard-Tabelle existieren noch weitere Unicode-Tabellen für spezielle Sprachen, wie z. B. die chinesische Silbenschrift). Wenn Sie dennoch einmal eine andere Tabelle verwenden müssen, können Sie in vielen Programmiersprachen die zu verwendende Zeichentabelle für einzelne Programme angeben. Vielleicht müssen Sie ja in Deutschland einmal ein Programm schreiben, dessen Textausgaben in chinesischer Sprache erfolgen ... Tabelle 4.3 zeigt die Datentypen, die in Delphi/Kylix und Java zur Speicherung von Zeichen und Zeichenketten verwendet werden. Delphi und Kylix überlassen Ihnen die Entscheidung, ob Sie mit ASCII- oder Unicode-Daten arbeiten, und bieten für beide Varianten Datentypen an. Um das Ganze noch ein wenig komplizierter zu machen, kennen beide Sprachen auch einen generischen Datentyp, der zurzeit noch per Voreinstellung ASCII-Daten speichert (was aber durch spezielle Compilereinstellungen auch geändert werden kann), in Zukunft aber auf Unicode umgestellt wird. Die Begründung dafür ist, dass viele ältere Betriebssysteme wie Windows 95 noch keine direkte Unterstützung für Unicode bieten. Sie können zwar auch auf diesen Betriebssystemen im
ASCII oder Unicode
197
Sandini Bib
Programm mit Unicode-Zeichen arbeiten. Wenn Sie allerdings Daten in Dateien speichern oder aus diesen auslesen, würde eine Konvertierung erfolgen, was einmal die Performance vermindert und zum anderen auch zu Problemen führen kann, wenn Sie Zeichen mit einem Zeichencode größer als 255 einsetzen (wie Sie ja wissen, sind die ersten 256 Zeichen im Unicode-Zeichensatz mit dem ASCII-Zeichensatz ISO-8859-1 identisch, weswegen eine Konvertierung in Grenzen möglich ist). Java speichert Zeichen grundsätzlich in Unicode, weswegen Sie sich bei dieser Sprache keine Gedanken machen müssen. Unicode-Zeichen werden übrigens auch als Wide Char („Breite Zeichen“) oder Wide String („Breite Zeichenkette“) bezeichnet. Allgemeiner Name
Wertebereich
Delphi/Kylix-Typ
Java-Typ
Größe (Byte)
Char
alle Zeichen
AnsiChar; WideChar; Char
char
1 (ASCII) bis 2 (Unicode)
String
beliebige Zeichenketten
AnsiString; WideString; String
String
1 (ASCII) bis 2 (Unicode) * Anzahl der Zeichen
Tabelle 4.3: Die Datentypen für Zeichen und Zeichenketten
Der Datentyp Char kann immer nur genau ein Zeichen speichern. Dieser Datentyp wird nicht von allen Programmiersprachen unterstützt. Der Datentyp String ist bei fast allen Programmiersprachen in der Lage, nahezu beliebig lange Zeichenketten zu speichern. Natürlich ist der Speicherbereich irgendwo begrenzt, aber diese Grenze liegt meist bei 2 GB (231 Byte). Sie können also riesige Texte in einem Speicherbereich vom Typ String speichern. Voraussetzung dafür ist allerdings, dass der Arbeitsspeicher des Computers ausreichend groß dimensioniert ist. Normalerweise sind die Zeichenketten, die ein Programm speichern muss, aber lange nicht so groß, dass der Arbeitsspeicher damit ausgelastet ist. In Delphi/Kylix sollten Sie die Unicode-Variante einsetzen, wenn Sie Programme für moderne Betriebssystem-Versionen entwickeln. Einmal können Sie wesentlich mehr Zeichen verwalten als mit ASCIITypen, zum anderen ist die Speicherung dieser Daten in Dateien wesentlich performanter, weil das Betriebssystem dann keine Konvertierung vornehmen muss. Außerdem arbeiten viele andere Programmiersprachen wie Java, Visual Basic 6, C#, Visual J# und Visual Basic .NET grundsätzlich nur noch mit Unicode-Zeichen.
198
Sandini Bib
4.2.4 Datentypen für Datumswerte Viele Programmiersprachen kennen einen speziellen Datentyp für die Speicherung von Datumswerten inklusive einer genauen Zeit. Dieser Datentyp besitzt meist einen Wertebereich, der vom 01.01.100 bis zum 31.12.9999 reicht. In einigen Sprachen (z. B. in Delphi, Kylix und Visual Basic 6) ist ein Datums-Datentyp eine Fließkommazahl vom Typ Double. Im Vorkommaanteil wird die Anzahl der Tage verwaltet, die seit einem festgelegten Basisdatum vergangen ist. Bei Delphi und Kylix ist das der 30.12.1899. Die Zahl 1 würde hier also den 31.12.1899 darstellen. Die Zeit wird dann einfach im Nachkommaanteil verwaltet. Die Zahl 0,25 steht beispielsweise für 06:00, 0,5 steht für 12:00 und 0,75 für 18:00. Da Double maximal 16 Ziffern hinter dem Komma erlaubt, kann die Zeit so sehr genau gespeichert werden. Beides zusammen ergibt ein genaues Datum. Jetzt gerade (wo ich diese Zeilen schreibe) ist beispielsweise das Datum 37453,8968392476818, also der Tag 37453 nach dem 30.12.1899. Dabei sind bereits schon etwa 0,897 Tage verstrichen. Mit anderen Worten: Es ist der 16.7.2002 21:31:26. Andere Sprachen verwalten ein Datum als ganze Zahl, einfach indem sie die Anzahl der Millisekunden speichern, die seit einem festgelegten Basisdatum vergangen sind. Dazu gehören Java, C#, Visual J# und Visual Basic .NET. Das Basisdatum von Java ist der 1.1.1970, allerdings mit der Zeit 01:00 (warum auch immer ...). Der 2.1.1970, 01:00 wird in Java also durch die Zahl 86.400.000 gekennzeichnet (1 Tag * 24 Stunden * 60 Minuten * 60 Sekunden * 1000 Millisekunden). Welche Zahl hinter einem Datum steckt, ist für Sie allerdings unerheblich. Sie arbeiten einfach mit den entsprechenden Datentypen. In Delphi und Kylix ist das der Typ TDateTime, in Java der Typ Date. Die spezielle Speichertechnik erlaubt es aber, mit Datumswerten zu rechnen. So ist es kein Problem, einem Datum eine bestimmte Anzahl Tage, Stunden, Minuten etc. aufzuaddieren, davon abzuziehen oder die Differenz zwischen zwei Datumswerten zu berechnen. Die meisten Programmiersprachen (und natürlich auch Delphi, Kylix und Java) liefern in ihrer Bibliothek dazu einfach anzuwendende Funktionen. Datumswerte, die kleiner sind als das Basisdatum, sind übrigens deshalb möglich, weil dazu einfach negative Werte gespeichert werden.
4.2.5 Der boolesche Datentyp Der boolesche Datentyp (dessen Name auf der vom englischen Mathematiker und Logiker George Boole entwickelten logischen Algebra basiert) ist ein recht einfacher Datentyp, der nur die Werte True und False
199
Sandini Bib
speichern kann. Diese beiden Werte stehen für „Wahr“ bzw. „Falsch“ und sind meist das Ergebnis eines logischen Ausdrucks, der den Regeln der booleschen Algebra folgt. Ein solcher Ausdruck, der häufig als Bedingung von Verzweigungen und Schleifen eingesetzt wird, kann nur wahr oder falsch ergeben. Der folgende (Object Pascal-)Quellcode überprüft z. B., ob in der Variablen i der Wert 1 gespeichert ist: if i = 1 then writeln('i ist gleich 1') else writeln('i ist ungleich 1'); True und False
Die Bedingung in dieser Verzweigung (i = 1) ist entweder wahr oder falsch, je nachdem, ob in der Variablen i die Zahl 1 oder eine andere Zahl gespeichert ist. Diese Zustände werden in Delphi und Kylix (und in vielen anderen Sprachen) durch die Konstanten True und False ausgedrückt, Java verwendet dazu die Konstanten true und false (Java unterscheidet ja die Groß- und Kleinschreibung). Weil die Bedingung nur einen booleschen Wert ergeben kann, besitzt sie den Datentyp Boolean (Object Pascal) bzw. boolean (Java). Sie können diesen Datentyp auch für Ihre eigenen Zwecke einsetzen, z. B. dann, wenn Ihr Programm einen Zustand, der nur wahr oder falsch sein kann, zwischenspeichern muss.
4.2.6 Datentyp-Konvertierungen Wenn Sie mit Datentypen arbeiten, verwenden Sie häufig nicht genau die Datentypen, die gerade gefordert sind. In diesen Fällen muss konvertiert werden. Einige Konvertierungen übernimmt der Compiler implizit, andere müssen Sie explizit ausführen. Implizite Konvertierung
200
Die Object Pascal-Funktion Sqr, die das Quadrat einer übergebenen Zahl berechnet, erwartet z. B. ein Argument vom Typ Extended. Wenn Sie an dieser Stelle einen Datentyp einsetzen, der ohne Probleme konvertiert werden kann, lässt der Compiler dieses zu. Der Sqr-Funktion können Sie z. B. auch einen Integer-Wert übergeben. Ein solcher Wert ist ebenfalls eine Zahl und kleiner (in Bezug auf die Vorkomma- und Nachkommastellen) als ein Extended-Wert, passt also ohne Probleme in diesen Datentyp hinein:
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12
program Konvertierungen; {$APPTYPE CONSOLE} var i: Integer; var ergebnis: Extended; begin i := 10; ergebnis := Sqr(i); writeln(ergebnis); end.
Manchmal kann der Compiler einen Wert allerdings nicht selbstständig in einen anderen Datentyp konvertieren. Das gilt immer dann, wenn beim Konvertieren Daten verloren gehen würden oder wenn die Konvertierung wegen ungültiger Daten fehlschlagen kann. Soll beispielsweise ein Double-Wert in eine Integer-Variable geschrieben werden, ist die Wahrscheinlichkeit sogar sehr groß, dass dabei Daten verloren gehen: Ein Integer-Typ kann keine Dezimalstellen speichern und besitzt daneben auch einen kleineren Wertebereich: 01 02 03 04 05 06 07 08 09 10 11 12
Explizite Konvertierung
program Konvertierungen; {$APPTYPE CONSOLE} var i: Integer; var x: double; begin x := 1.5; i := x; // Fehler "Inkompatible Typen 'Integer' und 'Double'" writeln(i); end.
Der Compiler meldet den Fehler „Inkompatible Typen 'Integer' und 'Double'“. Ähnliches tritt auf, wenn Sie einer Integer-Variablen einen String zuweisen. Ein String kann ja schließlich alle möglichen Zeichen speichern.
201
Sandini Bib
In Java sieht das Ganze ähnlich aus: 01 class Test 02 { 03 public static void main(String args[]) 04 { 05 double x; 06 x = 1.5; 07 int i; 08 i = x; // Fehler "possible loss of precision" 09 String s; 10 s = "123"; 11 i = s; // Fehler "incompatible types" 12 } 13 }
Programme arbeiten sehr häufig mit Strings, die Zahl- oder Datumsangaben speichern sollten. In den meisten Fällen handelt es sich dabei um Benutzereingaben. Wenn Sie mit diesen Eingaben rechnen wollen, müssen Sie die Strings konvertieren. Und manchmal müssen Sie auch einen zu großen Datentypen einem kleineren zuweisen. Dann müssen Sie ebenfalls explizit konvertieren. Der Compiler schützt Sie durch den Zwang zur expliziten Konvertierung davor, aus Versehen Daten mit nicht 100-prozentig kompatiblen Datentypen zu verwenden und eventuell Teile dieser Daten zu verlieren. Typumwandlung
Beim Konvertieren wird zwischen einer Typumwandlung (einem Typecast) und einer Konvertierung über Funktionen unterschieden. Eine Typumwandlung kann immer dann angewendet werden, wenn die Konvertierung zwar zum Verlust von Daten führen könnte, aber nicht fehlschlagen kann. So können Sie in Java z. B. einen double-Wert einer int-Variablen zuweisen: double x; x = 1.6; int i; i = (int)x;
Eine Typumwandlung wird in Java (wie auch in C, C++, C# und anderen Sprachen) einfach durch das Voranstellen des Zieldatentyps in Klammern erreicht. Der Compiler wandelt in diesem Beispiel den double-Typ in einen int-Typ um und weist das Ergebnis der Variablen i zu. Dabei gehen natürlich die Nachkommastellen verloren. Die Variable i speichert im Beispiel nach der Typumwandlung von x den Wert 1. Speichert der double-Typ einen zu großen Wert, resultiert ein Überlauf, den ich ab Seite 206 näher erläutere.
202
Sandini Bib
In Delphi und Kylix wenden Sie eine Typumwandlung ähnlich an. Dazu setzen Sie den Namen des neuen Datentyps vor den zu konvertierenden Wert und schließen den Wert in Klammern ein. Object Pascal ist allerdings wesentlich restriktiver als Java und lässt potenziell gefährliche Typumwandlungen nicht zu. Die Typumwandlung eines Fließkommain einen Ganzzahlwert ist z. B. nicht möglich (weil dabei eben die Nachkommastellen verloren gehen). Typumwandlungen werden in Delphi und Kylix nur im Programm verwendet, um Datentypen, die von Haus aus kompatibel sind, ineinander umzuwandeln. So können Sie über einen Typecast einen AnsiChar-Typ in den korrespondierenden Byte-Wert (den Zeichencode) konvertieren und umgekehrt (was bei der Programmierung auch öfter benötigt wird): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
program Konvertierungen; {$APPTYPE CONSOLE} var b: byte; var c: char; begin b := 97; c := char(b); writeln(c); // 'a' c := 'x'; b := byte(c); writeln(b); // 120 end.
Für Konvertierungen, die nicht implizit vorgenommen werden und über eine Typumwandlung nicht möglich (oder so nicht erwünscht) sind, bieten Ihnen nahezu alle Programmiersprachen spezielle Funktionen. Im Besonderen sind dies in Delphi und Kylix die Funktionen IntToStr, FloatToStr, StrToInt, StrToIntDef, StrToFloat und StrToFloatDef, mit denen Sie Strings in Zahlen umwandeln können und umgekehrt. Besonders die Funktionen StrToIntDef und StrToFloatDef sind hilfreich, weil Sie diesen einen Defaultwert übergeben, der einfach zurückgegeben wird, wenn der String nicht konvertiert werden kann. So können Sie beispielsweise die Eingabe des Anwenders in eine Fließkommazahl umwandeln und dann damit rechnen:
Konvertieren über Funktionen
203
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17
program Eingabe_Konvertieren; {$APPTYPE CONSOLE} uses SysUtils; var input: string; var zahl: double; begin write('Geben Sie eine Zahl ein: '); readln(input); zahl := StrToFloatDef(input, 0); zahl := zahl * 2; writeln(zahl); end.
Die Delphi/Kylix-Konvertierung-Funktionen zur Umwandlung von und in Strings berücksichtigen automatisch die Systemeinstellung zur Darstellung von Zahlen. In Deutschland wird also das Komma als Dezimaltrennzeichen berücksichtigt, läuft das Programm hingegen in den USA, berücksichtigt die Funktion den Punkt. Der String muss allerdings eine gültige Zahl darstellen. Im anderen Fall erzeugt die Funktion zur Konvertierung eine Ausnahme. Konvertierungen von Fließkomma- in Ganzzahlen sind in Delphi/Kylix übrigens nicht möglich (und eigentlich auch nicht sinnvoll). In Java verwenden Sie zur Konvertierung von Strings in Zahlen (und anderen Datentypen) und umgekehrt spezielle Methoden der Datentypen. Diese Methoden sind leider recht umfangreich, etwas kompliziert anzuwenden und erfordern viel Kenntnis der objektorientierten Programmierung. Deshalb zeige ich hier nur kurz, wie Sie einen String in einen double-Wert umwandeln können (ohne den Quellcode näher zu erläutern). Das Beispiel berücksichtigt aber bereits das im System eingestellte Zahlformat. Da die Eingabe von Daten in der Konsole in Java auch recht kompliziert ist, verzichte ich in diesem Beispiel einfach darauf:
204
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import java.text.*; import java.util.*; class konvertierungen { public static void main(String[] args) { string input; double zahl; input = "1,5"; try { NumberFormat nf = NumberFormat.getNumberInstance(); zahl = nf.parse(input).doubleValue(); } catch (ParseException e) { /* Die Konvertierung ist fehlgeschlagen */ System.out.println("Konvertierung fehlgeschlagen"); zahl = 0; } System.out.println(zahl); } }
Wenn Sie nun etwas erschrocken sind: Java ist eine konsequent objektorientierte Programmiersprache. Wenn Sie die objektorientierte Programmierung verstehen, fällt Ihnen die Arbeit mit Java wesentlich leichter. Außerdem müssen Sie solche komplexen Programmierungen wie im Beispiel oben nicht „aus dem Ärmel schütteln“. Als ich diesen Abschnitt schrieb, wusste ich beispielsweise gar nicht, wie in Java Strings in double-Werte konvertiert werden (ich wusste aber, dass es möglich sein muss). Die Lösung habe ich einfach durch eine Newsgroup-Recherche bei Google gefunden (vgl. Kapitel 1). Und so sollten Sie auch vorgehen. Auf der Seite mindprod.com/converter.html finden Sie ein kleines JavaApplet, in dem Sie die gewünschte Konvertierung eingeben können und das Ihnen ein entsprechendes Beispiel liefert. Leider fehlt dort ein Beispiel zur Konvertierung von Strings, die Zahlen in lokalen Zahlformaten (wie dem deutschen Format) speichern. Aber das finden Sie ja hier ...
205
Sandini Bib
4.2.7 Überläufe Überläufe sind ein häufiges Problem bei Zahldatentypen. Wenn ein Programm versucht, in einen Zahldatentyp einen Wert hineinzuschreiben, der zu groß ist für diesen Datentyp, führt das dazu, dass der Speicherbereich quasi überläuft. Ist der Maximalwert eines Zahldatentyps erreicht, kippt das Programm einfach alle Bits um und rechnet mit der resultierenden Zahl weiter. Bei einem Byte resultiert aus der Addition von 255 (11111111) mit 1 die Zahl 0 (00000000). Wird der Maximalwert um mehr als 1 überschritten, wird die Differenz einfach auf das Ergebnis aufgerechnet. Die Addition von 255 mit 2 ergibt demnach 1. Bei Datentypen mit Vorzeichen resultieren durch das Umkippen der Bytes die jeweils umgekehrten Zahlen. In Java ergibt die Addition von 1 zu einem Byte-Datentyp, der die Zahl 127 speichert, die Zahl –128. Das liegt daran, dass das erste Bit bei Datentypen mit Vorzeichen ja das Vorzeichen verwaltet. Zur Demonstration eines Überlaufs weist das folgende Delphi/KylixProgramm einem Integer-Datentyp zunächst den maximal möglichen Wert zu und erhöht diesen dann um 1: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21
program Ueberlaeufe; {$APPTYPE CONSOLE} uses SysUtils; var i: integer; begin writeln('Demo für einen Überlauf'); { Der Wert 2.147.483.647 passt noch in die Variable } i := 2147483647; { Wird die Zahl allerdings erhöht, resultiert ein Überlauf } i := i + 1; { Je nach Compiler-Einstellung wird dann eine Ausnahme erzeugt, oder die Zahl speichert einfach den übergelaufenen Wert } writeln('i ist gleich ', i); // -2147483648 readln; end.
Das Programm zeigt in diesem Beispiel die Zahl –2147483648 an, was natürlich vollkommen falsch ist.
206
Sandini Bib
Überläufe verursachen in Ihren Programmen schwer zu findende Fehler. Sie werden ja gar nicht in Kenntnis gesetzt, dass ein Überlauf aufgetreten ist. Das Programm arbeitet mit den vollkommen falschen Daten weiter.
Schwere Fehler durch Überläufe
Bei den meisten Compilern können Sie aber einstellen, dass das Programm bei einem Überlauf eine Ausnahme erzeugt. Dann reagiert Ihr Programm bei Überläufen mit einer entsprechenden Fehlermeldung. Sie haben nun die Chance, den Fehler zu finden. Die dazu notwendige Prüfung, die der Compiler in das Programm integriert, kostet etwas Rechenzeit. Für Programme, die sehr schnell ausgeführt werden müssen, mag es sinnvoll sein, die Überlaufprüfung abzuschalten (was leider meist per Voreinstellung der Fall ist). Für „normale“ Programme macht diese Prüfung aber eigentlich keinen Unterschied. Mit der Überlaufprüfung sind Sie aber auf der sicheren Seite. Verwenden Sie immer ausreichend große Datentypen für Ihre Daten (Variablen, Argumente von Funktionen etc.). Überläufe können dann eigentlich nicht mehr auftreten. Um sich (und vor allen Dingen auch die Personen, die Ihre Programme einsetzen) trotzdem vor den schweren logischen Fehlern, die Überläufe verursachen, zu schützen, schalten Sie die Compiler-Option ein, die bei einem Überlauf dafür sorgt, dass eine Ausnahme eintritt. Achten Sie darauf, dass Sie die Bibliothek Sysutils einbinden. Nur dann wird bei einem Überlauf eine Ausnahme erzeugt. Ohne die Einbindung dieser Bibliothek erzeugt das Programm stattdessen einen Laufzeitfehler 215. In Delphi schalten Sie die Überlaufprüfung über die Optionen OVERFLOW CHECKING und RANGE CHECKING ein, die Sie im Register COMPILER der Projektoptionen (Menü PROJECT/OPTIONS) finden. In Kylix suchen Sie nach den entsprechenden deutschen Begriffen: PROJEKT/OPTIONEN/ COMPILER/ÜBERLAUFPRÜFUNG und BEREICHSÜBERPRÜFUNG Alternativ können Sie auch die spezielle Compileranweisung {$Q+} für die Überlaufund {$R+} für die Bereichsüberprüfung oben im Quellcode eintragen. Das müssten Sie dann aber für alle Dateien eines Projekts machen.
Überlaufprüfung in Delphi und Kylix
Diese Optionen gelten nur für Ganzzahltypen, Überläufe in Fließkommatypen führen immer zu einer Ausnahme. Dass Delphi und Kylix zwei Optionen für Überläufe in Ganzzahlen verwaltet, besitzt einen etwas eigenartigen Grund. Die Option Überlaufprüfung gilt nur für IntegerTypen mit einer Größe ab vier Byte, also für Integer, Longint, Cardinal, Longword und Int64. Diese Typen können tatsächlich überlaufen. Die Typen Byte, Shortint, Smallint und Word hingegen werden vom Compiler
207
Sandini Bib
(wohl aus Optimierungsgründen) im rechten Teil eines Vier-Byte-Speichers abgelegt. Der linke Teil bleibt leer. Wenn ein Programm nun einen zu großen Wert in einen solchen Datentyp schreibt, läuft der Speicher normalerweise nicht über, sondern in den linken Teil hinein, womit der Bereich des Datentyps verletzt wird. Deshalb müssen Sie für diese Datentypen die Bereichsprüfung einschalten. Warum Borland das so kompliziert gestaltet hat, kann ich allerdings nicht sagen. Delphi und Kylix gehen leider auch mit Überläufen in Festkommaund Fließkommazahlen uneinheitlich um. Überläufe in einem Fließkomma- oder einem Currency-Typ erzeugen in Delphi/Kylix-Programmen immer eine Ausnahme, auch bei ausgeschalteter Überlaufprüfung. Ein Currency-Typ erzeugt dann eine Ausnahme „Invalid floating point operation“, ein Fließkommatyp erzeugt eine Ausnahme „Floating point overflow“. Leider hält der Delphi-Debugger (nicht der Kylix-Debugger!) an diesen Ausnahmen nicht an, sodass das Programm bei der Ausführung über die Entwicklungsumgebung einfach abgebrochen wird. Sie sehen die Ausnahme allerdings, wenn Sie das Programm ganz normal unter Windows starten. Aber Überläufe in diesen Datentypen sind auch eigentlich relativ selten wenn nicht sogar (bei korrekter Programmierung) unmöglich. Keine Überlaufprüfung in Java
Der Java-Compiler besitzt leider keine Möglichkeit, Überläufe zu überprüfen und in eine Ausnahme umzusetzen. Überläufe in Ganzzahltypen führen immer zu fehlerhaften Werten: 01 class overflow 02 { 03 public static void main(String[] args) 04 { 05 System.out.println("Überlauf-Demo"); 06 int i = 2147483647; 07 i = i + 1; // -2147483648 08 System.out.println("i ist gleich " + i); 09 } 10 }
In Java-Programmen müssen Sie also immer darauf achten, Datentypen zu verwenden, die ausreichend groß sind und deshalb keine Überläufe erzeugen können. Bei Fließkommazahlen resultiert in Java allerdings der spezielle Wert Infinity (Unendlich) wenn ein Überlauf eintritt: double d = 1.7E308; d = d * 1000; // Infinity
208
Sandini Bib
Mit diesem Wert können Sie weiterarbeiten, das Ergebnis wird aber immer Infinity sein. Sie können allerdings überprüfen, ob eine Variable diesen Wert speichert: if (Double.isInfinite(d)) System.out.println("d ist unendlich groß");
4.2.8 Das Schreiben von Konstanten (Literalen) In einem Programm müssen Sie sehr häufig konstante Werte verwenden. Diese so genannten Konstanten können sich, im Gegensatz zu Variablen, nicht mehr verändern, wenn das Programm läuft. Da es auch Konstanten gibt, die über ein Symbol repräsentiert werden (wie z. B. die Zahl Pi), werden als Wert geschriebene Konstanten (wie z. B. die Zahl 3.1415927) auch als Literal bezeichnet. Literale können eigentlich nur in zwei verschiedenen Varianten vorkommen: als Zahl und als Zeichenkette. Der folgende Quellcode benutzt z. B. zwei Zahl-Konstanten, die miteinander addiert werden, wobei das Ergebnis dieser Addition noch in eine Variable geschrieben wird: i := 1 + 2;
Wenn Sie Konstanten im Quelltext verwenden, müssen Sie sich an die Schreibregeln halten. Die folgenden Seiten beschreiben die Regeln für die verschiedenen Datentypen. Zahlen Zahlen können Sie in allen Programmiersprachen als normale Dezimalzahl, allerdings mit dem Punkt als Dezimaltrennzeichen, schreiben (programmiert wird eben in englischer Sprache): i := 1.234 * -2.5;
In den meisten Programmiersprachen können Sie Zahlen auch in der wissenschaftlichen Notation angeben: 7E2 bedeutet dann 7 * 102, 3E-5 steht für 3 * 10-5. In der Praxis besteht aber seltener Notwendigkeit dafür. Viele Programmiersprachen lassen zudem noch eine duale, oktale oder hexadezimale Schreibweise von Zahlen zu. In Object Pascal können Sie z. B. eine Zahl hexadezimal mit einem $-Zeichen als Präfix darstellen: $FFFF
In Java stellen Sie der Zahl dazu ein 0x voran: 0xFFFF
209
Sandini Bib
Den Umgang mit dual und hexadezimal dargestellten Zahlen habe ich bereits in Kapitel 3 beschrieben. Das oktale Zahlsystem wird heutzutage nicht mehr verwendet. Zeichenketten Die andere Form einer Konstante ist die Zeichenkette. Zeichenketten müssen immer in Anführungszeichen (alle Sprachen außer Object Pascal) bzw. Apostrophen (Object Pascal) geschrieben werden. In Object Pascal sieht eine konstante Zeichenkette (die einer Variablen übergeben wird) z. B. so aus: hello := 'Hello World';
In Java tragen Sie eine Zeichenkette in Anführungszeichen ein: hello = "Hello World";
Meine Erfahrungen in vielen Seminaren haben mir gezeigt, dass Programmier-Anfänger sehr oft Probleme damit haben, Zeichenketten korrekt zu schreiben. Wenn Sie damit auch Probleme haben, stellen Sie sich vor, Sie wären ein Compiler. Ein Compiler liest eine Anweisung und versucht diese zu interpretieren. Er zerlegt die Anweisung in einzelne Begriffe, wobei er festgelegte Trennzeichen, wie das Leerzeichen, runde Klammern, verschiedene Operatoren usw., verwendet. Er muss die Chance haben eine Zeichenkette korrekt zu erkennen. Würden Sie z. B. den folgenden Quelltext schreiben: hello = Hello World;
würde der Compiler annehmen, dass Hello und World je ein Bezeichner für eine Variable, Funktion, Prozedur o. Ä. ist. Er würde dann einen Fehler melden. Leider ist diese Fehlermeldug nicht immer eindeutig. Für die obige Anweisung wird z. B. der Fehler „Missing operator or semicolon“ gemeldet. Aber das ist nur verständlich, denn der Compiler kann nicht erkennen, um was es sich handelt. Er nimmt in diesem Fall an, es handle sich um Variablen, und erwartet deshalb entweder einen Operator zwischen diesen oder ein Semikolon hinter der ersten. Der Abschnitt „Grundlagen zum Schreiben von Anweisungen“ geht ab Seite 215 noch einmal auf Zeichenketten-Konstanten ein für den Fall, dass diese in die nächste Zeile umbrochen werden müssen.
210
Sandini Bib
Escape-Sequenzen in Java-Zeichenketten Java-Zeichenketten kennen so genannte Escape-Sequenzen. Diese werden mit einem Backslash eingeleitet und besitzen eine Sonderbedeutung. Die wichtigsten Escape-Sequenzen sind: • \t: Tabulator, • \r: Wagenrücklauf, • \n: Zeilenumbruch, • \": Anführungszeichen innerhalb einer Zeichenkette und • \\: Backslash innerhalb einer Zeichenkette. Damit ist es in Java z. B. sehr einfach, in einer Zeichenkette einen Zeilenumbruch zu erzeugen: String s = "Hallo Welt.\r\nWie geht es dir?";
Leider führt das dazu, dass der Backslash nicht einfach in eine Zeichenkette integriert werden kann. Dazu müssen Sie diesen verdoppeln. Sie können also z. B. unter Windows nicht String dateiname = "C:\Bilder\Logo.gif"
schreiben, wenn Sie einen Dateinamen angeben wollen. Der Compiler meldet den Fehler „Illegal escape character“, weil er die Escape-Sequenz \L nicht kennt (\b ist ein Rückschritt). Die folgende Anweisung führt aber zum Erfolg: String dateiname = "C:\\Bilder\\Logo.gif"
Zeichen Programmiersprachen, die einen Char-Datentyp kennen, erlauben Ihnen natürlich auch, Zeichen-Konstanten zu schreiben. In Delphi und Kylix schließen Sie diese einfach in die gewohnten Apostrophe ein: var c: Char; begin c := 'a';
In Java und anderen Sprachen wie C++, C# und JavaScript verwenden Sie ebenfalls Apostrophe, obwohl bei diesen Sprachen Zeichenketten in Anführungszeichen gesetzt werden müssen: char c; c = 'a';
211
Sandini Bib
4.3
Strukturen
In Programmen besteht häufig der Bedarf, zusammenhängende Daten zu speichern. Wenn Sie beispielsweise die Daten einer Person speichern wollen, gehören dazu Angaben zum Vor- und zum Nachnamen, zum Land, zum Wohnort und zur Straße. Wenn Sie dazu allerdings einzelne Variablen verwenden, wird ein Programm sehr unübersichtlich. Damit Sie zusammenhängende Daten auch zusammenhängend speichern können, bieten Ihnen viele Programmiersprachen die Möglichkeit, so genannte Strukturen zu deklarieren. Eine Struktur ist ein neuer Datentyp, der aus mehreren vordefinierten Datentypen zusammengesetzt ist. Strukturen werden heutzutage nicht mehr allzu häufig eingesetzt. Klassen (die in Kapitel 6 beschrieben werden) sind zur Speicherung zusammenhängender Daten wesentlich besser geeignet, u. a., weil diese auch Methoden beinhalten können. Ich gehe deshalb hier nicht allzu tief auf Strukturen ein. Sie sollten aber wenigstens wissen, was eine Struktur ist (auch, weil ich in Kapitel 6 noch einmal darauf zurückkomme). Java lässt die Deklaration von Strukturen nicht zu, weil diese nicht zur objektorientierten Programmierung passen. In Delphi und Kylix können Sie eine Struktur, die die Daten einer Person speichern soll, folgendermaßen deklarieren: 01 02 03 04 05 06 07
type Person = record Vorname: string; Nachname: string; Land: string; Postleitzahl: integer; Ort: string; end;
Strukturen werden in Delphi und Kylix auch als Record (Datensatz) bezeichnet, was das Schlüsselwort record erklärt, das Sie nach dem Namen der Struktur angeben müssen. Auf eine ähnliche Weise erzeugen Sie in Delphi und Kylix Klassen, dann setzen Sie statt record das Schlüsselwort class ein. Mit dem Beispiel erzeugen Sie einen neuen Datentyp Person, der aus mehreren Feldern besteht. Jedes Feld ist im Prinzip eine Variable. Sie können für die einzelnen Felder alle Datentypen verwenden, die aktuell zur Verfügung stehen (also neben den Standardtypen z. B. auch andere Strukturen, die zuvor deklariert wurden).
212
Sandini Bib
Die Deklaration einer Struktur erzeugt lediglich einen neuen Datentyp und noch keine Variablen. Diese können Sie dann wie gewohnt deklarieren. Als Datentyp geben Sie den Namen der Struktur an: var person1, person2: Person;
Wenn Sie diese Variablen nun im Programm verwenden, müssen Sie die einzelnen Felder der Struktur angeben und diese durch einen Punkt vom Variablennamen trennen: person1.Vorname := 'Fred-Bogus'; person1.Nachname := 'Trumper'; person1.Land := 'USA'; person1.Postleitzahl := 10000; person1.Ort := 'New York'; person2.Vorname := 'Merril'; person2.Nachname := 'Overturf'; person2.Land := 'Österreich'; person2.Postleitzahl := 1010; person2.Ort := 'Wien'; writeln(person1.Vorname + ' ' + person1.Nachname + ' wohnt in ' + person1.Ort); writeln(person2.Vorname + ' ' + person2.Nachname + ' wohnt in ' + person2.Ort);
Im ersten Moment sieht das Ganze vielleicht etwas komplex aus. Sie erreichen aber mit der Anwendung von Strukturen (oder besser Klassen) für zusammenhängende Daten, dass Ihr Programm wesentlich übersichtlicher und verständlicher wird. Versuchen Sie einmal, die Daten von zehn Personen in einzelnen Variablen zu verwalten, und Sie wissen, was ich meine. Ein weiterer wesentlicher Vorteil von Strukturen ist, dass Sie diese auch als Argument von eigenen Funktionen und Prozeduren einsetzen und so recht einfach komplexe Daten an diese übergeben können. Funktionen und Prozeduren werden im nächsten Kapitel behandelt.
4.4
Elementare Anweisungen
So, nun, nachdem Sie die wichtigsten theoretischen Grundlagen der Programmierung kennen, geht es an die Praxis. Dazu erfahren Sie zunächst etwas zu elementaren Anweisungen.
213
Sandini Bib
Wie Sie nun bereits wissen, wird ein Programm von oben nach unten, Anweisung für Anweisung ausgeführt. Einfache Anweisungen, die auch als elementare Anweisungen bezeichnet werden, sorgen dabei für die Behandlung von Eingaben, für Berechnungen und für die Ausgabe von Ergebnissen. Über Strukturanweisungen wird ein Programm gesteuert, z. B. in einer Schleife ausgeführt, aber das behandle ich erst in Kapitel 5. Hier geht es erst einmal um die Basis eines jeden Programms, die elementare Anweisung. Eine elementare Anweisung ist in einer modernen Programmiersprache: • der Aufruf einer Prozedur, Funktion oder Methode, die die Programmiersprache oder eine verwendete Komponente zur Verfügung stellt, oder • die Zuweisung eines Wertes oder einer Berechnung an eine Variable1. Über den Aufruf von Prozeduren, Funktionen und Methoden nutzen Sie fertig gestellte Programme, die Sie entweder selbst entwickelt haben, die Teil der Bibliothek der Programmiersprache sind oder die aus einer externen Bibliothek eines Drittherstellers stammen. Moderne Programmiersprachen enthalten mittlerweile meist eine sehr große Bibliothek, in der Sie Lösungen für die meisten kleineren Programmierprobleme finden. Wenn Sie beispielsweise in Ihrem Programm eine E-Mail versenden wollen, werden Sie mit großer Wahrscheinlichkeit in der Bibliothek der Programmiersprache oder in einer externen Bibliothek (die Sie über das Internet finden können) eine einfach anzuwendende Lösung finden. Bei der Suche nach der Lösung Ihres Problems hilft Ihnen natürlich auch wieder das Internet, wie ich es in Kapitel 3 gezeigt habe. Ab Seite 231 beschreibe ich, wie Sie grundsätzlich mit der Bibliothek von Delphi, Kylix und Java arbeiten. Dort erfahren Sie auch, was Funktionen, Prozeduren und Methoden voneinander unterscheidet. Der Aufruf von Prozeduren, Funktionen und Methoden reicht aber natürlich nicht aus, um Programme zu schreiben. Sehr häufig müssen Sie auch einfach nur Ergebnisse berechnen, Variablen mit Werten beschreiben und Ähnliches. Deshalb beschreibe ich zunächst die Grundlagen zum Schreiben von Anweisungen.
1.
214
Dass Werte oder das Ergebnis einer Berechnung nur an Variablen zugewiesen werden können, ist eigentlich nicht korrekt. Moderne Programmiersprachen arbeiten häufig mit Objekten (u. a. für die Ein- und Ausgabe von Daten), denen man genauso gut Werte oder Berechnungen zuweisen kann. An dieser Stelle spielt das aber noch keine Rolle.
Sandini Bib
4.4.1 Grundlagen zum Schreiben von Anweisungen Das Anweisungs-Endezeichen Wenn ein Compiler oder Interpreter einen Quelltext durchgeht, muss er die Möglichkeit haben, einzelne Anweisungen im Quellcode zu erkennen. Zeilen müssen deswegen mit einem festgelegten Zeilenendezeichen gekennzeichnet werden. In einigen Programmiersprachen wie Visual Basic und VBScript werden Zeilen mit einem Zeilenumbruch abgeschlossen, die meisten Programmiersprachen, wie auch Object Pascal und Java, erwarten allerdings ein Semikolon am Zeilenende. Die Verwendung eines solchen speziellen Zeichens hat den Vorteil, dass so ohne weiteres Anweisungen in mehrere Zeilen umbrochen und auch mehrere Anweisungen in einer Zeile geschrieben werden können. Bei Visual Basic und VBScript muss zum Umbrechen oder zum Schreiben mehrerer Anweisungen in einer Zeile ein spezielles Zeichen verwendet werden. Compiler älterer Programmiersprachen wie Cobol waren da übrigens wesentlich restriktiver und verlangten teilweise sogar, dass der Programmierer bestimmte Teile der Anweisung in festgelegten Spalten schrieb. Aber die Zeiten sind Gott sei Dank vorbei. Anweisungen in mehreren Zeilen Anweisungen werden manchmal so lang, dass eine vernünftige Übersicht über das Programm nur möglich ist, wenn diese in mehrere Zeilen umbrochen werden. Bei den Sprachen, die ein spezielles Anweisungsende-Zeichen (Semikolon) verlangen, ist dies kein Problem. Brechen Sie die Anweisung in Delphi/Kylix oder Java einfach in mehrere Zeilen um: ausgabe := 'Hello World ' + name;
Wie Sie dem Beispiel entnehmen können, können Sie die einzelnen Elemente einer Anweisung beliebig einrücken, was auch für die übersichtliche Strukturierung eines Programms sehr wichtig ist. Der Compiler/ Interpreter ignoriert Zeilenumbrüche und mehrfache Leerzeichen bei der Auswertung des Quellcodes (wahrscheinlich, indem er diese einfach aus dem Quellcode löscht). In den Sprachen, die nicht mit einem speziellen AnweisungsendeZeichen arbeiten, ist der Umbruch einer Zeile nur über ein spezielles Zeichen möglich, aber das soll hier kein Thema sein. Wenn Sie eine Anweisung mitten in einer Zeichenkette umbrechen wollen, müssen Sie die Zeichenkette abschließen, in der nächsten Zeile wie-
Zeichenketten umbrechen
215
Sandini Bib
der beginnen und diese nun separaten Zeichenketten über eine Addition miteinander verknüpfen: writeln('Das ist ein korrekter Zeilenumbruch ' + 'mitten in einer Zeichenkette');
Würden Sie die Zeichenkette hingegen ohne Abschluss und ohne Addition umbrechen, würde der Compiler/Interpreter einen Fehler melden (der je nach Compiler/Interpreter variiert): writeln('Das ist ein inkorrekter Zeilenumbruch mitten in einer Zeichenkette');
Falls Sie sich fragen, warum solch ein einfacher Umbruch nicht möglich ist: Wäre dieser möglich, hätte der Compiler/Interpreter keine Möglichkeit, zu erkennen, ob die Leerzeichen hinter der oberen Zeile und die Leerzeichen vor der unteren Zeile zu der Zeichenkette gehören oder nicht. Compiler/Interpreter, die Groß- und Kleinschreibung unterscheiden Einige Compiler bzw. Interpreter unterscheiden bei der Auswertung eines Quellcodes Groß- und Kleinschreibung. Dazu gehören die Compiler für C, C++, C#, Java und JavaScript. Alle Schlüsselwörter der Sprache, alle Variablen- und sonstige Bezeichner müssen bei der Verwendung im Quellcode genauso geschrieben werden, wie diese ursprünglich deklariert wurden. Die Schlüsselwörter dieser Sprachen sind meist komplett kleingeschrieben, einige spezielle Bezeichner (wie z. B. der Datentyp String) beginnen jedoch auch mit einem Großbuchstaben. In der Praxis ist es manchmal nicht einfach, die richtige Schreibweise zu finden. Schreiben Sie z. B. in einem Java-Programm den Datentyp String aus Versehen klein, meldet der Compiler, dass er diesen Bezeichner nicht kennt. Ob die Unterscheidung der Groß- und Kleinschreibung Sinn macht oder nicht, ist eher eine philosophische Frage. Viele Compiler bzw. Interpreter, wie die für Object Pascal und Visual Basic, unterscheiden die Schreibweise grundsätzlich nicht. Der Zwang zum korrekten Schreiben aller Schlüsselwörter und Bezeichner ist beim Programmieren mit C, C++, C#, Java und JavaScript schon manchmal recht nervig. In einigen Fällen, besonders bei der objektorientierten Programmierung, bringt diese Unterscheidung jedoch auch Vorteile. Nehmen wir es also, wie es ist.
216
Sandini Bib
Anweisungsblöcke Alle Sprachen kennen so genannte Anweisungsblöcke. Ein solcher Block fasst mehrere Anweisungen so zusammen, dass der Compiler diese als einen Block erkennt. In einer Delphi/Kylix-Anwendung müssen z. B. die Anweisungen, die beim Start der Anwendung ausgeführt werden sollen, als ein Block definiert sein, damit der Compiler erkennt, mit welcher Anweisung die Anwendung startet und mit welcher diese beendet ist: 01 02 03 04 05 06 07 08 09 10 11 12 13 14
program Hello; {$APPTYPE CONSOLE} uses SysUtils; var eingabe: string; begin write('Geben Sie Ihren Namen ein: '); readln(eingabe); write('Hallo ', eingabe); readln; end.
Anweisungsblöcke werden aber auch noch für andere Programmstrukturen verwendet: • für Funktionen und Prozeduren, • für Verzweigungen und Schleifen, • für die Deklaration von Strukturtypen und Klassen. Ein bereits etwas komplexeres Beispiel soll dies verdeutlichen. Die folgende Object Pascal-Funktion errechnet die Fakultät einer Zahl. Die Fakultät einer Zahl wird berechnet, indem alle ganzen Zahlen im Bereich von 1 bis zu dieser Zahl miteinander multipliziert werden. Die Funktion enthält bereits einige Anweisungsblöcke:
217
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
function Fakultaet(Zahl: integer): integer; var i: integer; begin if Zahl < 1 then begin Result := 0; end else begin Result := 1; for i := 2 to Zahl do begin Result := Result * i; end; end; end;
Wie Sie dem Beispiel entnehmen können, beginnt ein Block in Object Pascal immer mit dem Schlüsselwort begin und wird mit end abgeschlossen. Java verwendet (wie C, C++, C# und JavaScript) zur Kennzeichnung von Anweisungsblöcken geschweifte Klammern: 01 private int fakultaet(int zahl) 02 { 03 int result; 04 int i; 05 if (zahl < 1) 06 { 07 result = 0; 08 } 09 else 10 { 11 result = 1; 12 for (i=2; i<=zahl; i++) 13 { 14 result = result * i; 15 } 16 } 17 return result; 18 }
218
Sandini Bib
4.4.2 Kommentare Kommentare sind spezielle Anweisungen, die nur dazu gedacht sind, das Verständnis eines Programms zu erhöhen. Kommentare werden mit speziellen Zeichen begonnen oder in spezielle Zeichen eingeschlossen. Einfache Kommentare können Sie einer Anweisung in Delphi/Kylix über die Zeichen // anhängen:
Einzeilige Kommentare
i := 1; // i initialisieren
In Java verwenden Sie dieselben Zeichen: i = 1; // i initialisieren
Solche Kommentare können nur an das Ende einer Zeile angehängt werden und werden deshalb als einzeilige Kommentare bezeichnet. Sie können auch Zeilen schreiben, die nur Kommentare beinhalten: // Das ist eine reine Kommentarzeile // Das ist ebenfalls eine reine Kommentarzeile
Delphi/Kylix und Java erlauben neben den einzeiligen Kommentaren auch solche, die mitten in den Quelltext eingebunden werden können. Diese Kommentare werden dazu in spezielle Zeichen-Paare eingeschlossen. In Delphi/Kylix sind dies die Zeichen-Paare {} und (* *):
InlineKommentare
{ Das ist ein Delphi/Kylix-Inline-Kommentar } (* Das ist ebenfalls ein Delphi/Kylix-Inline-Kommentar *)
In Java verwenden Sie dazu das Zeichen-Paar /* */: /* Das ist ein Java-Inline-Kommentar */
Da Inline-Kommentare mitten im Quelltext stehen können, können Sie damit sehr flexibel kommentieren. Sie können den Kommentar an das Ende einer Anweisung anhängen: i := 1; { i initialisieren }
oder an den Anfang stellen: { i initialisieren } i := 1;
oder mitten in eine Anweisung eintragen (was allerdings nur selten Sinn macht): i := { i initialisieren } 1;
Wichtig ist, dass Inline-Kommentare mehrzeilig sein können: /* Das ist ein mehrzeiliger Java-Inline-Kommentar */
219
Sandini Bib
Mehrzeilige Kommentare können Sie auch einer Anweisung anhängen: i := 1; { Hier wird die Variable i initialisiert }
Die zweite Delphi/Kylix-Variante mit (* und *) erlaubt das Schachteln von Kommentaren: (* Das ist ein äußerer Inline-Kommentar { Das ist ein innerer Inline-Kommentar } *) Auskommentieren
Dieses Schachteln, das leider in Java nicht möglich ist, erlaubt das einfache Auskommentieren von ganzen Programmteilen (das aber in Java auch möglich ist, nur nicht so einfach). Beim Programmieren kommt es sehr häufig vor, dass Programmteile neu entwickelt werden müssen (weil diese fehlerhaft arbeiten). Aus Sicherheitsgründen gehen vorsichtige (und damit vernünftige) Programmierer dann oft so vor, dass sie den Programmteil kopieren und den ursprünglichen Teil in einen Inline-Kommentar einschließen (auskommentieren). Damit stellen diese Programmierer sicher, dass der alte Zustand wiederhergestellt werden kann, wenn die Änderung des Programms doch nicht zum Erfolg (oder vielleicht sogar zu neuen Fehlern) führt. Die zweite Delphi/KylixVariante erlaubt, einen Programmteil, der bereits Inline-Kommentare besitzt, auszukommentieren. Dazu müssen Sie dann nämlich für die inneren Kommentare andere Zeichen verwenden als für den äußeren. Wenn Sie dieselben Zeichen verwenden, erhalten Sie beim Kompilieren bzw. Interpretieren einen Fehler. Delphi und Kylix melden den etwas undurchsichtigen Fehler „Illegales Zeichen in Eingabedatei“: { { Das ist ein innerer Inline-Kommentar } }
Dass der Compiler diese Kommentare nicht auswerten kann, ist nur logisch: Da das Zeichen } bei Delphi und Kylix als Kommentar-Endezeichen gewertet wird, ist der äußere Kommentar für den Compiler in der zweiten Zeile beendet. Die letzte schließende Klammer wird dann als ungültiges Zeichen erkannt. Wenn Sie mit unterschiedlichen InlineKommentaren schachteln, kann das nicht passieren. In Java müssen Sie beim Auskommentieren anders vorgehen, wenn der Programmteil Inline-Kommentare enthält. Ich schreibe dazu einfach die Zeichen für einzeilige Kommentare vor die Anweisungen:
220
Sandini Bib
// // // // // // // //
/* i hochzählen */ i++; /* i vergleichen */ if (i == 2) { // Ausgeben einer Meldung System.out.println("i ist gleich 2"); }
Jetzt, wo Sie wissen, wie Sie Kommentare schreiben, sollen Sie einen sehr wichtigen Grundsatz beim Programmieren beherzigen:
Kommentare zum Verständnis
Kommentieren Sie Ihre Programme immer möglichst ausführlich. Gut kommentierte Programme enthalten in der Praxis etwa so viel Kommentare wie Quellcodezeilen. Manchmal enthält ein Programm sogar wesentlich mehr Kommentare als Quellcode. Denken Sie daran, dass Sie in einigen Monaten die Programmlogik, die Sie gerade entwickeln, vielleicht nur noch sehr schwer verstehen, weil Sie dann die Details des Zusammenhangs dieses Programmteils mit anderen Programmteilen nicht mehr genau in Erinnerung haben. Kommentieren Sie aber nicht unbedingt jede einzelne Anweisung. In der Regel reicht es aus, wenn Sie einen Block von logisch zusammenhängenden Anweisungen mit einem Kommentar versehen. Wenn Sie diesen Block noch zusätzlich durch eine Leerzeile von anderen Blöcken abtrennen, wird Ihr Programm sehr übersichtlich und gut lesbar. Damit erfüllen Sie ein wichtiges Qualitätskriterium bei der Softwarentwicklung: Ihr Programm wird für andere verständlich und damit auch möglichst einfach wart- und veränderbar.
4.4.3 Zuweisungen und arithmetische Ausdrücke Zuweisungen Eine Zuweisung ist so ziemlich die einfachste Form einer Anweisung. Bei einer Zuweisung gibt es immer einen linken und einen rechten Teil, die durch den Zuweisungsoperator voneinander getrennt sind. In Object Pascal sieht eine Zuweisung z. B. so aus: i := 1 + 2;
Der Zuweisungsoperator ist in den meisten Sprachen, außer in Object Pascal, das Gleichheitszeichen (=). Object Pascal verwendet für Zuweisungen, wie Sie dem Beispiel entnehmen können, den Operator :=. In Java würde die Zuweisung so aussehen: i = 1 + 2;
221
Sandini Bib
In einer Zuweisung berechnet ein Programm immer zuerst den rechten Teil. Der rechte Teil einer Zuweisung ist deshalb normalerweise ein arithmetischer Ausdruck. Arithmetische Ausdrücke Ein arithmetischer Ausdruck ist ein Ausdruck, der eine Berechnung enthält und der deswegen einen Wert ergibt. Dieser Ergebnis-Wert kann z. B. einer Variablen zugewiesen, aber auch in weiteren Ausdrücken oder als Argument einer Prozedur, Funktion oder Methode weiter verwendet werden. Arithmetische Ausdrücke führen Berechnungen auf einzelnen Operanden mit festgelegten Operatoren aus. Die Operanden des Ausdrucks 1 + 2 sind z. B. die Zahlen 1 und 2, der Operator ist das Plus-Zeichen. Die Operanden müssen nicht unbedingt Zahlwerte sein, die meisten Programmiersprachen erlauben auch grundlegende Operationen mit Zeichenketten (z. B. das Zusammenfügen von zwei Zeichenketten zu einer Zeichenkette über eine Textaddition). Eine arithmetische Operation muss also nicht unbedingt nur aus Zahl-Operanden bestehen. Die Programmiersprache muss lediglich Operatoren zur Verfügung stellen, die mit den im Ausdruck verwendeten Datentypen zurechtkommen. Die grundlegenden Operatoren sind in den meisten Programmiersprachen identisch. Einige Programmiersprachen bieten zusätzliche Operatoren an. Tabelle 4.4 zeigt die wichtigsten Operatoren für arithmetische Ausdrücke von Delphi/Kylix und Java. Operation
Delphi/Kylix-Operator
Java-Operator
Addition
+
+
Erhöhung
Inc(Wert)
++
Subtraktion
-
-
Verminderung
Dec(Wert)
--
Multiplikation
*
*
Division
/
/
Restwert-Division
mod
%
Ganzzahl-Division
div
kein Operator verfügbar
Klammern
()
()
Tabelle 4.4: Die wichtigsten arithmetischen Operatoren von Delphi, Kylix und Java
Die Arbeitsweise der Operatoren +, - und * und / muss ich wohl nicht näher erläutern. Leider verhalten sich die Compiler von Delphi/Kylix und Java bei diesen Operatoren etwas unterschiedlich. Grundsätzlich ist dabei aber eines wichtig:
222
Sandini Bib
Jeder Ausdruck besitzt einen Datentyp (wie der Compiler das Ergebnis des Ausdrucks in einen temporären Speicherbereich schreiben muss, um dieses weiter verarbeiten zu können). Der Datentyp eines Ausdrucks wird vom Compiler normalerweise an Hand der Datentypen der Operanden ermittelt, wobei der größere Datentyp bestimmend ist. Wenn Sie zwei Integerwerte addieren, erhält der Speicher für das Ergebnis den Typ Integer:
Ausdrücke besitzen einen Datentyp
int integerZahl1 = 1; int integerZahl2 = 2; int integerErgebnis; integerErgebnis = integerZahl1 + integerZahl2;
Addieren Sie einen Integer- mit einem Doublewert, erhält der Speicherbereich für das Zwischenergebnis den Typ Double (auch wenn der Double-Typ eine Zahl ohne Dezimalstellen speichert!): int integerZahl1 = 1; int doubleZahl1 = 2; int doubleErgebnis; doubleErgebnis = doubleZahl1 + integerZahl1; //
Sind an einem Ausdruck mehr als zwei Operanden beteiligt, bestimmt natürlich der größte der Operanden den Datentyp. Wenn Ihnen dies klar ist, werden Sie so einiges verstehen. In vielen arithmetischen Ausdrücken kommen konstante Werte vor, meist als Literale. Diese besitzen natürlich auch einen Datentyp. Ganzzahlen werden eigentlich immer als 32-Bit-Integer mit Vorzeichen ausgewertet, sofern die Zahl nicht zu groß dafür ist. Bereits die Zahl 1 besitzt diesen Datentyp. Fließkommazahlen werden als Double ausgewertet, wenn die angegebene Zahl in einen Double-Typ passt. Ist die Zahl größer, wird ein entsprechend größerer Datentyp verwendet.
Konstante in
Der Java-Compiler verbietet nun, dass Sie einen Ausdruck an Daten übergeben, die keinen kompatiblen Datentyp besitzen. So können Sie einen Ausdruck, der einen int-Typ ergibt, nur den Typen int, long, float und double zuweisen, nicht den Typen byte und short. Im Fehlerfall meldet der Compiler, dass ein bestimmter Typ für die Zuweisung benötigt wird. Der folgende Quellcode:
Java
arithmetischen Ausdrücken
class demo { public static void main(String args[]) { int zahl1 = 1, zahl2 = 2; byte ergebnis = zahl1 + zahl2;
223
Sandini Bib
} }
ergibt z. B. einen Fehler beim Kompilieren: demo.java:6: possible loss of precision found : int required: byte byte ergebnis = zahl1 + zahl2; ^ 1 error
Wenn Sie für das Ergebnis einen int-Typ oder einen größeren Typ einsetzen, kann der Quellcode kompiliert werden. Ich finde diese Prüfung sehr gelungen. Dadurch können Sie kaum Fehler in ein Programm einbauen, die durch den Verlust von Daten aufgrund falscher Datentypen entstehen. Dadurch gleicht der Java-Compiler ein wenig seine fehlende Überlaufprüfung aus. Überläufe können aber trotzdem auftreten, denn die Addition oder Multiplikation zweier Datentypen kann immer einen größeren Wert ergeben als der mit dem Datentyp maximal speicherbare. Bei einer Division müssen Sie in Java noch etwas beachten: Ist der Datentyp des Ausdrucks ein Ganzzahlwert, resultieren keine Dezimalzahlen, auch wenn Sie das Ergebnis einem Fließkommatypen zuweisen: int integerZahl1 = 5; int integerZahl2 = 2; double doubleErgebnis = integerZahl1 / integerZahl2; Ganzzahl-Division bei Java
Das Ergebnis dieser Berechnung ist 2,0 und nicht wie erwartet 2,5. Der Compiler führt eine Ganzzahl-Division durch, weil der Ausdruck einen Ganzzahl-Wert ergibt. Bei einer Ganzzahl-Division werden nur die ganzen Anteile des Ergebnisses zurückgegeben, die Dezimalstellen werden abgeschnitten. Bei der Division von 5 durch 2 passt 2 genau zweimal in 5. Das ist dann auch das Ergebnis. Wenn Sie Ganzzahl-Divisionen noch nicht kennen, werden Sie sich vielleicht fragen, was Sie damit anfangen können. Abgesehen von mathematischen Fragestellungen („Wie oft passt eine ganze Zahl in eine andere Zahl hinein“) setzen Sie diese Division recht selten ein. Angenommen, Sie schreiben ein Programm für einen Betrieb, der irgendwelche Artikel herstellt und diese zu je 1000 Stück in Kartons verpackt. Ihre Anwendung zählt die hergestellten Artikel mit. Um nun herauszufinden, wie viele ganze Kartons bereits bestückt wurden, setzen Sie eine Ganzzahldivision ein:
224
Sandini Bib
int artikelAnzahl; ... int kartonAnzahl = artikelAnzahl / 1000:
Wenn Sie keine Ganzzahl-Division ausführen wollen (was eigentlich auch die Regel ist), müssen Sie entweder einen Fließkomma-Typ im Ausdruck verwenden oder Sie müssen einen Ganzzahl-Typ mit einem Typecast umwandeln. Eine normale Division zweier Integerwerte können Sie also so ausführen:
Normale Division
int integerZahl1 = 5; int integerZahl2 = 2; double doubleErgebnis = integerZahl1 / (double)integerZahl2;
Welchen der Operanden Sie umwandeln, spielt dabei keine Rolle. Hauptsache ist, dass der Compiler erkennt, dass er einen FließkommaTypen für das temporäre Ergebnis verwendet. Der Delphi/Kylix-Compiler verlangt wie der Java-Compiler, dass der Datentyp eines Ausdrucks zum Datentyp passt, dem das Ergebnis zugewiesen werden soll. Wenn Sie einen Ausdruck, der einen Double-Wert ergibt, einer Integer-Variablen zuweisen wollen, erhalten Sie den Fehler „Incompatible types 'Integer' and 'Extended'“:
Delphi/Kylix
var integerZahl1, integerErgebnis: integer; var doubleZahl1 : double; begin integerZahl1 := 1; doubleZahl1 := 0.5; integerErgebnis := integerZahl1 + doubleZahl1; { Fehler "Incompatible types 'Integer' and 'Extended'" }
Delphi und Kylix sind allerdings nicht ganz so restriktiv wie Java und erlauben eine Zuweisung auch dann, wenn dadurch zwar keine Präzision verloren gehen, aber ein Überlauf entstehen kann: var integerZahl1, integerZahl2: integer; var byteErgebnis: byte; begin integerZahl1 := 1; integerZahl2 := 1000; byteErgebnis := integerZahl1 + integerZahl2;
225
Sandini Bib
Die Ergebnisvariable speichert in diesem Beispiel den Wert 233, was natürlich überhaupt nicht erwartet wird und zu Fehlern im Programm führt. Wenn Sie aber die Überlauf- und die Bereichsüberprüfung eingeschaltet haben (vgl. Seite 206), wird dann wenigstens eine Ausnahme generiert und Sie wissen, dass Ihr Programm fehlerhaft ist. Verzichten Sie aber möglichst auf die kleineren Integer-Datentypen. Auch bei Divisionen verhält sich Delphi/Kylix anders als Java. Die normale Division über den Operator / führt hier immer zu einem Ergebnis vom Typ Extended (das kompatibel ist zu Single und Double, aber einen Überlauf verursachen kann). Wenn Sie also zwei Integerwerte dividieren, ist das Ergebnis u. U. eine Dezimalzahl: var integerZahl1, integerZahl2: integer; var doubleErgebnis: double; begin integerZahl1 := 1; integerZahl2 := 2; doubleErgebnis := integerZahl1 / integerZahl2;
Das Ergebnis ist wie erwartet 0,5. Dieses Verhalten führt dazu, dass Sie das Ergebnis einer normalen Division zweier Ganzzahlen keinem Ganzzahltyp zuweisen können (weil eben immer eine Fließkommazahl dabei herauskommt): var integerZahl1, integerZahl2: integer; var integerErgebnis: integer; begin integerZahl1 := 1; integerZahl2 := 2; integerErgebnis := integerZahl1 / integerZahl2; { Fehler "Incompatible types 'Integer' and 'Extended'" Ganzzahl-Division bei Delphi/Kylix
Um in Delphi- und Kylix-Programmen eine Ganzzahl-Division durchzuführen, müssen Sie den div-Operator verwenden. Wenn Sie beispielsweise wissen wollen, wie oft die Zahl 5 in einen gespeicherten Wert passt, können Sie die folgende Berechnung verwenden: integerErgebnis := integerZahl div 5;
Restwert-Division
Die Restwert-Division, die auch als Modulo-Division bezeichnet wird, ermöglicht Ihnen herauszufinden, welche Zahl übrig bleibt, wenn Sie eine Zahl mit einer Ganzzahldivision durch eine andere Zahl teilen. Die Operation 5 mod 2
226
Sandini Bib
ergibt in Delphi/Kylix z. B. 1, weil sich maximal 4 durch 2 ohne Rest teilen lässt und damit der Wert 1 (5 – 4) übrig bleibt. Java verwendet dafür den Operator %: 5 % 2
Solche Operationen benötigen Sie wie Ganzzahl-Divisionen sehr selten. Ein Beispiel, bei dem Sie den Modulo-Operator benötigen, ist das Ermitteln, ob eine Zahl eine Primzahl ist (Primzahlen sind Zahlen größer 1, die nur durch sich selbst und durch 1 ohne Rest teilbar sind): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
program Modulo_Operator; uses SysUtils; {$APPTYPE CONSOLE} var zahl, i: integer; primzahl: boolean; begin writeln('Beispiel fuer den Modulo-Operator'); writeln('Primzahlen-Berechnung'); { Zahl eingeben lassen } Write('Zahl: '); Readln(zahl); { Annehmen, dass es sich um eine Primzahl handelt } primzahl := true; { In einer Schleife alle Zahlen von 2 bis zur eingegebenen Zahl - 1 durchgehen, um zu überprüfen, ob sich die Zahl durch eine andere Zahl ohne Rest teilen lässt } for i := 2 to zahl - 1 do begin if zahl mod i = 0 then { Die Zahl lässt sich durch eine andere Zahl, die nicht 1 ist, teilen, ist also keine Primzahl } primzahl := false; end; { Das Ergebnis ausgeben } if primzahl = true then Writeln('Diese Zahl ist eine Primzahl') else Writeln('Diese Zahl ist keine Primzahl'); Readln; end.
227
Sandini Bib
Dieses Beispielprogramm deklariert in Zeile 7 zunächst einige Variablen. In Zeile 15 muss der Anwender eine Zahl eingeben. In Zeile 18 wird die Variable primzahl auf True gesetzt, weil zunächst angenommen wird, dass es sich um eine Primzahl handelt. Das ist übrigens eine beliebte Programmiertechnik: Zuerst nimmt das Programm an, dass ein bestimmter Zustand vorliegt. Im weiteren Verlauf wird dann überprüft, ob dieser Zustand eventuell nicht zutrifft, und die Annahme in diesem Fall wieder revidiert. Diese Überprüfung der Annahme beginnt im Beispiel an der Zeile 23 mit einer Schleife, die die Variable i von 2 bis zu der eingegebenen Zahl - 1 hochzählt. Diese Schleife wird in Kapitel 5 behandelt. In der Schleife wird nun mit Hilfe des Modulo-Operators überprüft, ob die eingegebene Zahl durch die in i gespeicherte Zahl ohne Rest teilbar ist. Ist dies der Fall, wird die Variable primzahl auf False gesetzt (die Annahme damit also revidiert). Unter der Schleife wird diese Variable dann ausgewertet, um entscheiden zu können, ob es sich um eine Primzahl handelt oder nicht. Die Incrementund DecrementOperatoren
Java und Object Pascal kennen zur Addition und Subtraktion zwei spezielle Operatoren, mit denen Sie sehr einfach den Wert 1 auf eine Zahl aufaddieren oder davon subtrahieren können. Statt i = i + 1;
sollten Sie in Java besser i++;
schreiben. Dasselbe gilt für eine Subtraktion, bei der Sie den Operator -- verwenden. Bei der Verwendung dieser Operatoren haben nicht nur Sie weniger Arbeit. Auch der Compiler kann eine solche Addition oder Subtraktion wesentlich besser optimieren, als wenn Sie die Variable selbst hochzählen. In Delphi und Kylix setzen Sie dazu die Prozeduren Inc und Dec ein: Inc(i); Dec(i); Inc und Dec sind zwar laut Dokumentation keine Operatoren (sondern eben Prozeduren), werden vom Compiler aber mit ziemlich hoher Wahrscheinlichkeit genauso optimiert wie die Operatoren bei Java. Bei diesen Prozeduren können Sie übrigens noch zusätzlich den zu addierenden bzw. subtrahierenden Wert angeben: Inc(i, 2); Dec(i, 2);
228
Sandini Bib
Die Operatoren ++ und -- enthalten in Java eine Falle. Sie können diese Operatoren in der Präfix- und der Postfix-Notation einsetzen: i++ ++i; i--; --i;
Wichtig ist der Unterschied nur, wenn Sie einen solchen Ausdruck in weiteren Ausdrücken oder in Zuweisungen verwenden: int zahl1 = 1; int zahl2 = zahl1++;
Die Postfix-Notation bewirkt, dass der Compiler zuerst den Rest des Ausdrucks auswertet und danach die Variable hochzählt. Die Variable zahl2 enthält nach Ausführung dieser Anweisungen also den Wert 1! Wenn Sie hingegen die Präfix-Notation einsetzen, wird der Wert zuerst hochgezählt und danach zugewiesen: int zahl1 = 1; int zahl2 = ++zahl1;
Nun enthält zahl2 wie erwartet den Wert 2. Setzen Sie die Operatoren ++ und -- in Java möglichst nicht in Ausdrücken ein. Das schwer verständliche Verhalten dieser Operatoren führt sonst zu kryptischen Programmen und vor allen Dingen auch zu potenziellen Fehlern. Sie können diese Operatoren anwenden, aber möglichst nur, um einzelne Variablen in eigenen Anweisungen hoch- oder herunterzuzählen. Die Reihenfolge der Operatoren In arithmetischen Ausdrücken müssen Sie die Reihenfolge der Operatoren beachten. Sie kennen das ja wahrscheinlich aus der Schule: Punktrechnung vor Strichrechnung. Die höchste Priorität besitzen die Operatoren *, /, div, % und mod, danach werden die Operatoren + und – ausgewertet. Die Reihenfolge der Auswertung können Sie durch Klammern verschieben. Wollen Sie z. B. den Mittelwert von zwei Zahlen berechnen, wäre die folgende Anweisung falsch: mittelwert := zahl1 + zahl2 / 2;
Da der Divisionsoperator vor dem Additionsoperator ausgewertet wird, würde der Compiler dafür sorgen, dass zuerst zahl2 durch 2 geteilt wird und danach erst der Inhalt von zahl1 auf das Ergebnis addiert wird. Die
229
Sandini Bib
korrekte Variante verwendet zur Verschiebung der Rechenpriorität Klammern: mittelwert := (zahl1 + zahl2) / 2;
Der Compiler sorgt dafür, dass zuerst die Ausdrücke in den innersten Klammern (falls mehrere Klammern ineinander geschachtelt sind) aufgelöst und das Zwischenergebnis in der Berechnung dann weiterverwendet wird. Arithmetische Ausdrücke überall Ein ganz wichtiger Merksatz bei der Verwendung von arithmetischen Ausdrücken ist, dass Sie diese überall da einsetzen können, wo ein bestimmter Datentyp erwartet wird. Der Ausdruck muss lediglich einen passenden (konvertierbaren) Datentyp besitzen. Herauszufinden, welchen Datentyp ein Ausdruck hat, ist relativ einfach. Handelt es sich bei dem Ausdruck um eine Berechnung, kommt auf jeden Fall schon mal ein Zahldatentyp heraus. Dann müssen Sie eigentlich nur noch schauen, wie groß der Ergebniswert ist und ob es sich um eine Dezimalzahl handelt. Einen Ausdruck können Sie so z. B. auch als Argument einer Funktion oder Prozedur verwenden: writeln('42 / 11 + 7 ergibt: ', 42 / 11 + 7);
Spezielle Java-Zuweisungsoperatoren Java kennt neben dem = einige spezielle Zuweisungsoperatoren, die einige Operationen vereinfachen. Statt i = i + 100;
können Sie z. B. auch i += 100;
schreiben. Für den Compiler sind beide Ausdrücke identisch (möglicherweise kann er den zweiten aber auch etwas optimiert kompilieren). Für Sie bedeutet die zweite Variante weniger Schreibarbeit. Die wichtigsten dieser Operatoren sind: +=, -=, *=, /= und %=.
230
Sandini Bib
4.5
Verwenden der Bibliotheken einer Programmiersprache
Ohne die Verwendung der Funktionen, Prozeduren oder Methoden der Bibliothek einer Programmiersprache kommt kein Programm aus. Bereits die einfachsten Anweisungen wie eine Ausgabe an der Konsole greifen auf die Bibliotheken der Sprache zurück. Und das ist auch gut so, denn müssten Sie die Assemblerbefehle für die Ausgabe von Daten an der Konsole jedes Mal selbst schreiben, hätten Sie viel zu tun. Die Object Pascal-Prozedur Writeln und die Java-Methode System.out.println nehmen Ihnen diese lästige Arbeit ab. Bei modernen Sprachen finden Sie aber noch wesentlich mehr fertige Lösungen in den Bibliotheken. Sie sollten also mit den Möglichkeiten der Bibliotheken Ihrer Programmiersprache vertraut sein und den Umgang damit verstehen. Dazu sollten Sie zuerst den Unterschied zwischen Funktions- und Klassenbibliotheken kennen.
4.5.1 Funktions- und Klassenbibliotheken Programmiersprachen unterscheiden zwei Arten von Bibliotheken, die als Funktions- und Klassenbibliotheken bezeichnet werden. Der Unterschied zwischen diese beiden Varianten ist einfach und gleichzeitig auch schwierig: Funktionsbibliotheken enthalten Module mit Funktionen und Prozeduren, Klassenbibliotheken enthalten Klassen mit Methoden. Um den Unterschied zu verstehen, müssen Sie ein wenig in die beiden grundsätzlichen Programmier-Techniken eintauchen: in die strukturierte und die objektorientierte Programmierung. In Kapitel 6 erfahren Sie noch genauer, worum es sich dabei handelt. Wenn Sie strukturiert programmieren (was Sie heutzutage allerdings kaum noch machen), schreiben Sie Funktionen und Prozeduren, wenn Sie eine Programmlösung entwickeln, die Sie an mehreren Stellen in einem Programm oder in mehreren Programmen einsetzen können. Wenn Sie beispielsweise in einem Programm die Fakultät einer Zahl berechnen müssen, und Sie benötigen diese Berechnung mehr als ein Mal (also an verschiedenen Stellen im Programm), dann entwickeln Sie dazu eine Funktion. Diese Funktion bringen Sie normalerweise in einem Modul des Programms unter, das im Pascal-Sprachgebrauch als Unit (Element, Teil) bezeichnet wird. Ein Modul enthält meist mehrere Funktionen oder Prozeduren, die thematisch zusammenpassen. Alle Funktionen, die mathematische Berechnungen ausführen, wie beispielsweise Funktionen zur Berechnung der Fakultät einer Zahl und der Ermittlung, ob eine Zahl eine Primzahl ist, würde ich beispielsweise in einem Modul mit dem Namen Mathematische_Funktionen unterbringen. Funktionen
Strukturierte Programmierung
231
Sandini Bib
zur Berechnung von statistischen Daten würde ich in einem separaten Modul entwickeln, das ich Statistik_Funktionen nennen würde. Wenn Sie so vorgehen, wissen Sie zum einen, welches Modul welche Funktionen beinhaltet, und können zum anderen dieses Modul einfach in Ihre neuen Programme einbinden, wenn Sie die enthaltenen Funktionen dort benötigen. Das Ganze nennt sich „Wiederverwendung von Programmcode“ und ist bei der Programmierung sehr wichtig, aber irgendwie auch logisch. Wer macht schon gerne identische Arbeit mehrfach, wenn es mit einem Mal getan sein kann und die fertige Arbeit später einfach wiederverwendet werden kann? Funktionen und Prozeduren
Der Unterschied zwischen Funktionen und Prozeduren ist nicht schwierig: Prozeduren bestehen aus mehreren zusammengehörigen Anweisungen, die unter dem Namen der Prozedur an verschiedenen Stellen des Programms aufgerufen werden können. Wenn Sie beispielsweise in einem Buchhaltungsprogramm öfter eine Rechnung drucken müssen, würde sich anbieten, den Rechnungsdruck in einer Prozedur zu entwickeln und diese dann an den Stellen, an denen eine Rechnung gedruckt werden muss, aufzurufen. Die Prozedur soll einfach nur ihre Arbeit machen und nichts zurückmelden. In vielen Fällen muss das Programm aber einen Ergebniswert erhalten. Was wäre eine Funktion zur Berechnung der Fakultät einer Zahl, wenn sie das Ergebnis nicht zurückgeben würde? Und genau dafür benötigen Sie Funktionen. Funktionen machen eigentlich genau das, was auch Prozeduren machen, geben aber zusätzlich noch einen Wert zurück. Die Fakultätsberechnungs-Funktion würde also die errechnete Fakultät zurückgeben. Es gibt aber auch Funktionen, die nichts errechnen, sondern einfach nur zurückgeben, ob die ausgeführte Aktion erfolgreich war oder nicht. Die Rechnungs-DruckProzedur könnte beispielsweise auch eine Funktion sein, die zurückgibt, ob der Druck erfolgreich war oder nicht. So kann der Programmierer, der diese Funktion nutzt, sehr einfach feststellen, ob der Ausdruck eventuell fehlgeschlagen ist, und darauf entsprechend reagieren.
Argumente zur
Funktionen und Prozeduren müssen häufig beim Aufruf gesteuert werden können. Um die Fakultät einer Zahl zu berechnen, muss ja schließlich die Zahl übergeben werden. Um eine Rechnung ausdrucken zu können, benötigt die entsprechende Funktion Informationen über die Kundenadresse, die Artikeldaten, die Anzahl der gelieferten Artikel und deren Netto-Preise. Diese Daten werden Funktionen oder Prozeduren an Argumenten übergeben. Die Funktion zur Berechnung der Fakultät würde lediglich ein Argument besitzen und in Object Pascal beispielsweise so aufgerufen werden:
Steuerung
ergebnis := fakultaet(4);
232
Sandini Bib
Das Beispiel übergibt der Funktion den Wert 4. Die Funktion errechnet daraus die Fakultät und gibt diese zurück. Das Ergebnis wird dann im Beispiel in eine Variable geschrieben. Eine Prozedur zum Ausdruck von Rechnungen würde mehrere Argumente besitzen, an die die Daten für die Rechnung übergeben werden: rechnungDrucken(kundenAdresse, artikelName, artikelPreis, anzahl);
Im Beispiel werden an den einzelnen Argumenten der Prozedur Variablen übergeben (die natürlich zuvor mit entsprechenden Werten versehen werden müssen). Das Beispiel ist bewusst vereinfacht und lässt nur den Druck von Rechnungen mit einem Artikel zu. Die Übergabe einer Liste von Artikeln gehört schon zu den Programmiertechniken, die erst später besprochen werden. Die Prozedur ist einfach anzuwenden, erledigt aber eine ganze Menge Arbeit. Sie muss den Brief optisch aufbereiten, die Summe berechnen, eventuelle Rabatte oder Sonderaktionen berücksichtigen, die Mehrwertsteuer berechnen, die Gesamtsumme berechnen und schließlich noch den Drucker korrekt ansteuern. Viel Arbeit, aber den Programmierer, der die Prozedur aufruft, interessiert das nicht. Und das ist auch gut so. Denn das ist schließlich der Sinn von Funktionen und Prozeduren. Die Bibliotheken von Delphi und Kylix beinhalten, da deren Sprache Object Pascal auf dem alten, strukturierten Pascal basiert, eine Vielzahl an Modulen (die hier in Units verwaltet werden) mit Funktionen und Prozeduren. Die Unit Math enthält z. B. mathematische Funktionen, die Unit Sysutils enthält einfache Basisfunktionen, die immer wieder benötigt werden, wie beispielsweise Funktionen zum Formatieren und zum Konvertieren. Delphi und Kylix enthalten aber nicht nur Bibliotheken mit einfachen Modulen, sondern auch solche, die Klassen verwalten. Dabei kann das Ganze noch gemischt sein, sodass eine Bibliothek gleichzeitig eine Funktions- und eine Klassenbibliothek ist. Die Bibliotheken von Java sind, da Java eine vollständig objektorientierte Programmiersprache ist, reine Klassenbibliotheken. Dass eine Programmiersprache mehrere Bibliotheken mitliefert (die zumeist in separaten Dateien gespeichert sind), hat übrigens natürlich auch Gründe. Zum einen haben Programmierer so wesentlich mehr Überblick, weil auch die Bibliotheken thematisch organisiert sind. Und zum anderen ist es einfacher, eine relativ kleine Bibliothek gegen eine neue Version auszutauschen, als eine sehr große, die alle Features der Programmiersprache enthält. Außerdem kommen immer wieder neue Bibliotheken hinzu, die teilweise von externen Programmierern entwickelt werden. Klassenbibliotheken enthalten keine Module mit Funktionen und Methoden, sondern Klassen. Klassen gehören zur objektorientierten Pro-
Klassenbibliotheken
233
Sandini Bib
grammierung und sind schon etwas schwieriger zu verstehen. In Kapitel 6 gehe ich auf die Grundlagen der objektorientierten Programmierung ein und erläutere dort auch die Vorteile. Für die Anwendung von Klassenbibliotheken reichen aber einige wenige Grundlagen aus. Auf diese einfache Weise betrachtet, ist eine Klasse so etwas wie ein Modul, enthält also auch Funktionen und Prozeduren. Dieser werden zwar bei Klassen als Methoden bezeichnet, ansonsten ist die Bedeutung und die Funktionsweise (und auch die Programmierung) von Methoden dieselbe wie bei Funktionen und Prozeduren. Klassen enthalten ähnlich Modulen nur Methoden, die thematisch zusammenpassen. Die Java-Klasse Math enthält z. B. eine Vielzahl mathematischer Methoden (ähnlich der Math-Unit von Delphi/Kylix). Klassenbibliotheken enthalten Klassen nun in zwei Varianten. Die ersten Variante ist nahezu identisch zu den Modulen der strukturierten Programmierung. Diese Variante setzen Sie auch hauptsächlich ein, wenn Sie mit den Klassen der Java-Klassenbibliothek arbeiten. Wie bei Modulen können Sie die Methoden der Klasse einfach über deren Name aufrufen, wenn Sie diese verwenden wollen. Nur der Aufruf unterscheidet sich, denn hier müssen Sie immer den Namen der Klasse als Präfix, durch einen Punkt getrennt vor den Methodennamen schreiben. Die Methode Drucken der Klasse Rechnung würden Sie also z. B. so aufrufen: Rechnung.Drucken(kundenAdresse, artikelName, artikelPreis, anzahl);
Wie auch bei Funktionen und Prozeduren müssen Sie Methoden häufig Argumente übergeben, über die der Ablauf der Methode gesteuert wird. Der Aufruf von solchen Methoden ist also bis auf das Voranstellen des Klassennamens identisch zur Verwendung von Funktionen oder Prozeduren. Die zweite Variante von Klassen ist schon etwas schwieriger zu verstehen. Um diese Methoden aufrufen zu können, benötigen Sie eine Instanz der Klasse. Ich will an dieser Stelle noch nicht klären, um was es sich dabei handelt, weil Kapitel 6 näher auf dieses Thema eingeht. Im vorliegenden Kapitel muss Ihnen ausreichen, dass Sie Instanzen erst erzeugen müssen, bevor Sie diese verwenden können. Wenn Sie beispielsweise mit Datumswerten arbeiten wollen, müssen Sie in Java eine Instanz der Klasse Date erzeugen, wozu Sie den new-Operator verwenden: Date datum = new Date();
Dann können Sie mit dem Datum arbeiten. Dazu stellt Ihnen die Klasse Date eine Vielzahl an Methoden zur Verfügung. So können Sie das Datum z. B. über die Methode toLocaleString() als Zeichenkette im aktuellen Systemformat ausgeben:
234
Sandini Bib
System.out.println(datum.toLocaleString());
Ein wesentlicher Vorteil dieser Art Klasse ist, dass Sie auch mehrere Instanzen mit unterschiedlichen Daten verwalten können, aber darauf komme ich in Kapitel 6 zurück. Eine wichtige Klassenbibliothek ist übrigens die Bibliothek, die die Steuerelemente enthält, die Sie auf Formularen ablegen können. Bei diesen Steuerelementen handelt es sich nämlich auch um Klassen, von denen Instanzen erzeugt werden. Das Erzeugen der Instanzen übernimmt allerdings die Entwicklungsumgebung automatisch. Wo sind Bibliotheken gespeichert? Bibliotheken sind meist in vorkompilierten Dateien gespeichert, seltener liegen diese im Quellcode vor. In Delphi und Kylix finden Sie die Bibliotheken der Sprache im Ordner Lib des Delphi/Kylix-Installationsordners (also bei Delphi normalerweise im Ordner C:\Programme\ Borland\Delphi6\Lib und bei Kylix im Ordner /usr/local/kylix2/lib). Die Bibliotheken erkennen Sie an der Dateiendung .dcu (Delphi Compiled Unit). Die Java-Standardbibliotheken finden Sie im Ordner lib des Ordners, in dem die Java-Laufzeitumgebung (JRE) installiert ist. Java verwendet wesentlich weniger einzelne Bibliotheken als Delphi und Kylix, diese sind aber mindestens genauso mächtig. Das Einbinden von Bibliotheken In allen Programmiersprachen müssen Bibliotheken im Programm eingebunden sein, bevor diese genutzt werden können. Der Compiler muss schließlich wissen, wo er die Bibliotheken findet, deren Funktionen, Prozeduren und Klassen Sie benutzen. In den meisten Programmiersprachen ist das aber für die Standardbibliotheken bereits bei der Installation geschehen. In Delphi und Kylix finden Sie diese Einstellung in den Umgebungsoptionen (im Menü TOOLS / ENVIRONMENT OPTIONS im Register LIBRARY bzw. im Menü TOOLS / UMGEBUNGSOPTIONEN im Register BIBLIOTHEK). Hier können Sie auch zusätzliche Pfade zu Bibliotheken eintragen, die Sie separat erworben (oder selbst entwickelt) und an anderer Stelle gespeichert haben. Das reicht aber noch nicht aus, um diese Bibliotheken verwenden zu können. Sie müssen dem Compiler in jeder Quellcodedatei noch mitteilen, dass Sie bestimmte Bibliotheken nutzen wollen. Dazu verwenden Sie die uses-Anweisung oben im Programmquellcode. Mehrere Bibliotheken können Sie durch Kommata getrennt angeben:
Delphi und Kylix
uses SysUtils, Math;
235
Sandini Bib
Die in diesen Bibliotheken enthaltenen Funktionen, Prozeduren und Klassen können Sie verwenden, ohne den Bibliotheksnamen mit anzugeben: ergebnis := Sinh(10); // Sinus berechnen
Sie können den Namen aber auch angeben, was in seltenen Fällen notwendig ist, wenn identische Funktionen, Prozeduren oder Klassen in zwei verschiedenen Bibliotheken gespeichert sind: ergebnis := Math.Sinh(10); // Sinus berechnen
Die Dokumentation der Delphi-Bibliotheken und eine (allerdings recht klick-intensive ...) Übersicht darüber erreichen Sie über die Hilfe, indem Sie im Inhaltsverzeichnis dem folgenden Pfad folgen: Visual Component Library Reference / Routines Listing, by Unit. In Kylix folgen Sie dem Pfad CLX-Referenz / Routinen nach Units. Java
Java geht seit der Version 1.2 etwas anders vor, um die Standardbibliotheken zu lokalisieren: Der Java-Compiler sucht einfach im Java-Ordner nach einem Unterordner mit Namen lib. Für eigene oder externe Bibliotheken können Sie die Betriebssystem-Umgebungsvariable CLASSPATH setzen (ähnlich wie Sie bei der Java-Installation die Variable Path bzw. PATH umdefiniert haben) und hier die Pfade zu den Ordnern eintragen, die diese speziellen Bibliotheken enthalten. In Java-Programmen sind alle Bibliotheken, die der Compiler in den Bibliotheksordnern findet, schon automatisch eingebunden. Java arbeitet nicht wie Delphi und Kylix mit vielen einzelnen, thematisch organisierten, sondern mit wenigen, sehr umfangreichen Bibliotheken. Damit Sie sich in diesen zurechtfinden, sind die enthaltenen Klassen in so genannten Paketen (packages) organisiert. Ein Paket ist einfach nur eine logische Zusammenfassung von thematisch zusammengehörigen Klassen. Wenn Sie eine eigene Klasse erzeugen, können Sie angeben, welchem Paket diese Klasse angehören soll. Dabei können Sie bereits existierende Pakete angeben oder neue Pakete „erzeugen“. Paketnamen bestehen aus mindestens einem Teil, können aber auch mehrere, durch Punkte getrennte Teile enthalten. Java fügt alle Elemente von Bibliotheken damit in eine eindeutige Hierarchie ein. Alle Java-Standardklassen gehören z. B. zum Basispaket java. Klassen externer Hersteller würden einem anderen Basispaket zugeordnet sein. Die Klassen zum Drucken finden Sie im Paket java.print. Im Paket java.util finden Sie allgemeine Utility2-Klassen. 2.
236
Für dieses Wort gibt es wohl keine sinnvolle deutsche Übersetzung. „Utility“ heißt übersetzt „Nützlicher Helfer“, womit bei der Programmierung kleine, aber sehr nützliche Hilfsfunktionen bzw. –Klassen gemeint sind. Aber man kann bei diesen wohl kaum von „nützlichen Helferklassen“ sprechen ...
Sandini Bib
Die Pakete java.util.zip und java.util.regex enthalten Klassen, die sehr spezielle Utilities darstellen. Diese Pakete gehören aber zum übergeordneten Paket java.util. Das Einzige, was damit erreicht wird, ist, dass Sie eine sehr einfache Übersicht über die Java-Bibliotheken erhalten. Und das ist auch gut so. Wenn Sie sich einmal daran gewöhnt haben, wissen Sie mit ziemlicher Sicherheit, wo Sie nach einer gewünschten Funktionalität suchen müssen. Wollen Sie z. B. eine Textdatei einlesen, werden Sie recht schnell auch ohne viel Wissen zum Paket java.io gelangen und dort weiter suchen. Dieses Konzept benutzt übrigens in nahezu identischer Form auch Microsoft in .NET (C#, Visual J#, Visual Basic .NET). Wenn Sie eine Klasse verwenden wollen, müssen Sie (bis auf eine Ausnahme, die ich noch erläutere) normalerweise den Namen des Pakets mit angeben:
Pakete importieren
double sinus = java.lang.Math.sin(10); java.util.Date datum = new java.util.Date(); ...
Sie können den Inhalt eines Pakets aber auch „importieren“, was durch die Anweisung import java.util.* oben in der Quellcode-Datei geschieht. Dann müssen Sie den Paketnamen nicht mehr angeben: import java.util.*; class JavaTest { public static void main(String args[]) { Date datum = new Date(); System.out.println(datum.toLocaleString()); } }
Statt des Sterns, der alle enthaltenen Klassen importiert, können Sie auch einzelne Klassen importieren, z. B. nur die Date-Klasse: import java.util.Date;
Das macht aber eigentlich nicht viel Sinn. Auch wenn Sie alle Klassen importieren, müssen Sie keine Bedenken haben, dass dadurch Ihr Programm größer wird oder dass der Compiler zum Kompilieren mehr Zeit braucht.
237
Sandini Bib
Das Paket java.lang, das fundamentale Klassen enthält, wird übrigens vom Compiler implizit importiert. Deshalb können Sie z. B. die MathKlasse immer auch ohne Paketname verwenden. Die Dokumentation der Java-Standardpakete erreichen Sie über die Java-SDK-Dokumentation, indem Sie auf der Startseite auf den Link JAVA 2 PLATFORM API SPECIFICATION klicken. Finden der richtigen Funktion/Prozedur/Methode Das Finden der richtigen Funktion, Prozedur bzw. Methode ist in der Praxis nicht immer allzu einfach. Orientieren Sie sich am Namen der Bibliothek bzw. des Pakets. Mathematische Funktionen finden Sie in Delphi und Kylix wahrscheinlich eher in der Math-Bibliothek als in der Bibliothek Printers. In Java suchen Sie zuerst das vom Namen her passende Paket und in diesem dann eine vom Namen her passende Klasse. Weil dies alles nicht so einfach ist, habe ich in Kapitel 1 bereits gezeigt, wie Sie im Internet nach Problemlösungen suchen. Nutzen Sie diese Möglichkeiten. Dann lernen Sie die Bibliotheken ganz von alleine kennen. Kein Programmierer kennt wohl alle Möglichkeiten der Bibliotheken seiner Programmiersprache. Dazu sind diese einfach viel zu mächtig. Programmierer wollen ihr Gehirn ja schließlich auch noch für andere Dinge benutzen (wie zum Snowboarden, Inlinern, Motorradfahren ...). Wichtig ist, dass Sie die Grundlagen verstehen. Im Internet oder in einem guten Buch finden Sie dann mit ziemlicher Sicherheit die Lösung Ihrer Probleme, wenn Sie diese nicht durch eigenes Suchen in der Dokumentation der Bibliothek ermitteln können.
4.5.2 Der Aufruf von Prozeduren, Funktionen und Methoden Prozeduren, Funktionen und Methoden werden in jeder Programmiersprache prinzipiell gleich aufgerufen. Der einzige Unterschied ist, dass Funktionen (und Methoden, die Funktionen sind) einen Wert zurückgeben und dass beim Aufruf von Methoden der Name der Klasse oder der Name einer Instanz der Klasse vorangestellt werden muss. Meist erwarten Prozeduren, Funktionen und Methoden Argumente, über die der Ablauf gesteuert wird oder einfach nur Daten übergeben werden. Diese Argumente werden in Klammern angegeben, wobei die einzelnen Argumente durch Kommata getrennt werden:
238
Sandini Bib
{ Aufruf von Delphi/Kylix-Prozeduren writeln('Hello World'); writeln; ergebnis := Power(2, 8);
} // Text ausgeben // Leerzeile ausgeben // 28 ausrechnen
/* Aufruf von Java-Methoden */ System.out.println("Hello World"); System.out.println(); ergebnis = Math.pow(2, 8);
// Text ausgeben // Leerzeile ausgeben // 28 ausrechnen
Jetzt, wo Sie wissen, wie Java-Bibliotheken organisiert sind, wollen Sie bestimmt wissen, was System.out bedeutet. System kann ja eigentlich kein Paket sein, weil diese standardmäßig mit java. beginnen. Das ist auch richtig. System ist eine Klasse im Paket java.lang. Der Bezeichner out steht für eine Eigenschaft dieser Klasse. Diese Eigenschaft ist ein Objekt (eine Instanz der Klasse PrintStream, um genau zu sein) und besitzt deswegen auch wieder Methoden (wie z. B. print und println). Das ist bereits etwas kompliziert. Warten Sie bis Kapitel 6 mit dem Verstehen der dabei angewendeten Techniken. Wie Sie dem Beispiel entnehmen können, unterscheiden sich Delphi/ Kylix und Java kaum beim Aufrufen von Prozeduren, Funktionen bzw. Methoden. Der einzige Unterschied ist, dass Delphi und Kylix keine Klammern erlauben, wenn eine Prozedur, Funktion oder Methode ohne Argumente aufgerufen wird. Jede Prozedur, Funktion oder Methode besitzt einen festen Satz an Argumenten. Dieser wird beim Entwickeln der Prozedur, Funktion oder Methode genau definiert. Aber warum können Sie dann die writeln-Prozedur und die println-Methode einmal mit und einmal ohne Argument aufrufen? Die Antwort ist einfach: Weil es sich dabei um verschiedene Prozeduren bzw. Methoden handelt. Diese tragen zwar denselben Namen, sind aber tatsächlich in der entsprechenden Bibliothek separat gespeichert. Unterscheiden können Sie die einzelnen Varianten nur an Hand der Argumentliste. Und genau das macht auch der Compiler, wenn er eine solche Anweisung findet. Er sucht einfach nach der Variante der Prozedur, Funktion oder Methode, die von ihren Argumenten her dem Aufruf entspricht. In diesem Zusammenhang spricht man übrigens von überladenen Prozeduren, Funktionen oder Methoden.
Variable
Funktionen (bzw. Methoden, die Funktionen sind) werden, wie bereits gesagt, prinzipiell genauso aufgerufen wie Prozeduren. Da Funktionen aber einen Wert zurückgeben und Sie diesen Wert in Ihrem Programm wahrscheinlich weiterverwenden wollen, stehen Funktionen häufig auf der rechten Seite einer Zuweisung. Die Sin-Funktion von Delphi und
Funktionen
Argumente
239
Sandini Bib
Kylix berechnet z. B. den Sinus einer übergebenen Zahl und gibt diesen zurück. Der folgende Quelltext speichert die Rückgabe dieser Funktion in einer Variablen: i := Sin(20);
Eine Funktion können Sie überall dort einsetzen, wo ein bestimmter Datentyp erwartet wird. Die Funktion muss lediglich einen passenden Datentyp zurückgeben. Dieser Merksatz ist für die Anwendung von Funktionen ziemlich wichtig. Sie können Funktionen z. B. in arithmetischen Ausdrücken einsetzen: ergebnis := Sin(20) * 100;
Bei einem solchen Ausdruck ruft der Compiler zuerst die Funktion auf und speichert den Rückgabewert in einem temporären Speicherbereich zwischen. Erst danach wird die eigentliche Operation ausgeführt und das Ergebnis dieser Operation der Variablen zugewiesen. Weil Sie Funktionen überall da einsetzen können, wo ein passender Datentyp erwartet wird, können Sie diese auch als Argument einer anderen Funktion oder Prozedur eintragen: write('Der Sinus von 20 ist ', Sin(20));
Die write-Prozedur erwartet in ihrem (optionalen) zweiten Argument entweder eine Zeichenkette oder eine Zahl. Da die Sin-Funktion eine Zahl zurückgibt, kann diese dort eingesetzt werden. Der Compiler ruft erst die innere Funktion auf um das Ergebnis dieser Funktion dann an die write-Prozedur zu übergeben. Der Datentyp von Argumenten Argumente besitzen, wie alle Daten in einem Programm, einen Datentyp. Wenn Sie eine Prozedur, Funktion oder Methode mit Argumenten aufrufen, müssen Sie an der Stelle der Argumente einen zum Datentyp passenden Wert übergeben. Die meisten Programmiersprachen sind dabei recht genau. Sie müssen zwar nicht wirklich den genauen Datentyp übergeben, aber wenigstens einen, der in den geforderten Datentyp konvertiert werden kann. Erwartet die Prozedur an einem Argument eine Zahl, können Sie in den meisten Programmiersprachen z. B. keine Zeichenkette übergeben, auch wenn diese eine Zahl beinhaltet. Abbildung 4.1 zeigt den Versuch, in Delphi die Min-Funktion, die das Minimum zweier übergebener Zahlen berechnet, mit zwei Zeichenketten an den Argumenten aufzurufen.
240
Sandini Bib
Abbildung 4.1: Versuch, in Delphi die Min-Funktion mit zwei Zeichenketten an den Argumenten aufzurufen
Delphi meldet, dass keine überladene Version der Min-Funktion existiert, die den Argumenten entspricht. Unpassende Datentypen können Sie natürlich konvertieren, wie ich es bereits auf Seite 200 beschrieben habe. Einige Prozeduren, Funktionen und Methoden wie z. B. writeln und println liegen aber auch in verschiedenen Varianten vor, denen Sie unterschiedliche Datentypen übergeben können und deshalb nicht konvertieren müssen.
4.5.3 Ein Delphi/Kylix-Beispiel Um wieder einmal ein wenig Praxis in die ganze Theorie zu bringen, wollen wir nun eine kleine statistische Anwendung entwickeln, in die der Benutzer zwei Zahlen eingeben kann. Das Programm soll dann aus diesen Zahlen die kleinere, die größere und den Mittelwert berechnen. Um nicht immer nur Konsolenanwendungen zu erzeugen, entwickeln Sie dazu eine Anwendung mit Fenstern. Erzeugen Sie dazu in Delphi oder Kylix ein neues Projekt vom Typ Anwendung. Speichern Sie dieses Projekt in Ihrem Projekte-Ordner in einem neuen Unterordner unter einem sinnvollen Namen ab (mein Vorschlag: StatistikTool). Nennen Sie die Formulardatei dabei vielleicht fStart. Legen Sie nun auf dem Formular die benötigten Steuerelemente ab. Abbildung 4.2 zeigt, wie das Formular aussehen soll:
241
Sandini Bib
Abbildung 4.2: Das Formular für die kleine Statistik-Anwendung
Benennen Sie die Textboxen der Reihe nach von oben nach unten mit txtZahl1, txtZahl2, txtKleinereZahl, txtGroessereZahl und txtMittelwert, indem Sie diese einzeln anklicken und im Eigenschaftenfenster die Eigenschaft Name entsprechend einstellen. Benennen Sie den Schalter mit dem Namen btnBerechnen. Dann geben Sie dem Formular selbst noch einen aussagekräftigen Namen, indem Sie auf einem freien Bereich des Formulars klicken und die Eigenschaft Name z. B. auf frmStart einstellen. Nun müssen Sie nur noch programmieren. Klicken Sie doppelt auf den Schalter, um eine Methode für die Reaktion auf die Betätigung des Schalters zu erzeugen. In dieser Methode programmieren Sie nun. Ich verwende im Programm absichtlich einige Variablen, obwohl das Ganze auch etwas direkter geht. Aber wir wollen ja den Umgang mit dem Gelernten üben. Und außerdem machen Variablen das Programm auch häufig leichter lesbar. Zunächst benötigen Sie Variablen für die beiden Zahlen, die kleinere Zahl, die größere Zahl, den Mittelwert und die Standardabweichung. Diese Variablen müssen Sie zwischen dem Methodenkopf und dem begin-Schlüsselwort deklarieren. Sie sollten sich natürlich Gedanken um den Datentyp machen. Welcher Datentyp ist zur Speicherung der Eingaben geeignet? Ein Integer-Typ fällt aus, weil das Programm auch mit Dezimalzahlen arbeiten soll. Der Typ Single ist gefährlich, weil er bei größeren Zahlen zu wenig Dezimalstellen zulässt. Der Typ Double scheint geeignet zu sein. Wenn Sie aber sehr genau rechnen wollen, verwenden Sie lieber den Typ Extended. Dieser Typ ist dann auch für die Ergebnisse geeignet: 01 procedure TfrmStart.btnBerechnenClick(Sender: TObject); 02 var zahl1, zahl2, kleinereZahl, 03 groessereZahl, mittelwert: extended; 04 begin
242
Sandini Bib
Nun müssen Sie zunächst die Eingaben in die Variablen schreiben. Dabei tritt bereits das erste Problem auf. Der Benutzer kann auch nicht konvertierbare Zahlen eingeben. Also müssen Sie nach einem Weg suchen, wie Sie herausfinden können, ob die Eingaben in Ordnung sind. Wie gehen Sie dabei vor? Richtig (so denke ich ...). Sie suchen bei Google nach der Lösung dieses Problems (falls Sie die Lösung nicht mehr im Kopf haben). Jedes andere Vorgehen würde viel zu viel Zeit kosten (die Sie besser mit Snowboarden oder anderen schönen Dingen verbringen sollten). Die Recherche bei groups.google.com/advanced_group_search nach „check numeric user input“ in den Newsgroups, deren Name dem Muster „*delphi*“ entspricht (was auch bei diesem Problem für Kylix gilt), führt wahrscheinlich zum Erfolg. Ansonsten suchen Sie bei www.google.com oder stellen selbst eine Frage in eine der Delphi-Newsgroups. So habe ich auf jeden Fall die Lösung gefunden. Die Lösung des Problems ist die Verwendung der Funktion StrToFloatDef, die eine Zeichenkette in einen Extended-Wert umwandelt. Nun müssen Sie vielleicht noch herausfinden, wie diese Funktion arbeitet. Suchen Sie dazu im Index der Delphi/Kylix-Hilfe nach dem Namen der Funktion. Was stellen Sie fest? Die Funktion wird gar nicht aufgeführt3. Auch ein Durchsuchen der Hilfe führt nicht zum Erfolg. Da hat Borland wohl vergessen, diese Funktion zu dokumentieren. Das zeigt Ihnen auch, dass Sie sich nie ausschließlich auf die Dokumentation verlassen können und immer auch noch an anderen Stellen suchen müssen. Wäre die Funktion dokumentiert, würden Sie aber eine gute Hilfestellung zur Benutzung erhalten. Nützlich wäre auch, dass Sie dann erfahren würden, in welcher Unit diese Funktion gespeichert ist, um diese einbinden zu können. Sie finden StrToFloatDef in der Unit SysUtils, die standardmäßig bereits in die Formulardatei eingebunden ist. Die Anwendung dieser Funktion ist aber einfach. Am ersten Argument übergeben Sie den zu konvertierenden String, am zweiten einen Defaultwert. Kann der String nicht konvertiert werden, gibt die Funktion den Defaultwert zurück. Am ersten Argument können Sie die Eigenschaft Text der Zahleingabe-Textboxen übergeben, weil diese den Typ String besitzt: 05 06 07
3.
{ Zahlen einlesen } zahl1 := StrToFloatDef(txtZahl1.Text, -1); zahl2 := StrToFloatDef(txtZahl2.Text, -1);
Das kann natürlich bei Ihnen auch anders sein, wenn Sie eine neuere Version von Delphi oder Kylix verwenden
243
Sandini Bib
Nun können Sie sehr einfach überprüfen, ob der Anwender eine gültige Zahl eingegeben hat. Dazu verwenden Sie die If-Verzweigung (die eigentlich erst später behandelt wird, aber doch sehr selbst erklärend ist): 08 09 10 11
{ Eingaben überprüfen. Nur numerische Eingaben größer als -1 werden als gültig anerkannt } if (zahl1 > -1) and (zahl2 > -1) then begin
In der Verzweigung können Sie nun rechnen, hier können Sie absolut sicher sein, dass die Zahlen gültig sind. Die Variablen besitzen schließlich einen numerischen Datentyp. Für die Berechnung der kleineren und der größeren Zahl finden Sie schnell eine Lösung in der Unit Math. Hier müssen Sie nicht im Internet suchen. Sie müssen diese Unit lediglich in der uses-Anweisung oben in der Datei einbinden. 12 13
kleinereZahl := Min(zahl1, zahl2); groessereZahl := Max(zahl1, zahl2);
Nun suchen Sie nach der Lösung zur Berechnung des Mittelwerts. Natürlich können Sie diesen selbst berechnen, aber vielleicht gibt es ja eine fertige Lösung. Als Hilfestellung können Sie im Quellcode einfach Math gefolgt von einem Punkt eingeben. Delphi und Kylix zeigen Ihnen dann den Inhalt der Unit Math in einer Liste an. Hier können Sie sehr schnell und einfach nach einer Funktion suchen. Die Funktion Mean, die Sie dort finden, ist allerdings für den Anfang noch ein wenig kompliziert. Also berechnen Sie dieses Mal einfach selbst: 14
mittelwert := (zahl1 + zahl2) / 2;
Nun müssen Sie das Ergebnis noch in die Ergebnis-Textboxen eintragen. Da die Eigenschaft Text den Typ String besitzt, müssen Sie wieder konvertieren: 15 16 17
txtKleinereZahl.Text := FloatToStr(kleinereZahl); txtGroessereZahl.Text := FloatToStr(groessereZahl); txtMittelwert.Text := FloatToStr(mittelwert);
Schließlich müssen Sie die Verzweigung noch abschließen: 18
end
Um dem Benutzer eine Meldung zu übergeben, wenn dessen Eingaben nicht korrekt waren, fügen Sie noch einen else-Block an. Die Anweisungen in diesem Block werden ausgeführt, wenn die Bedingung der If-Verzweigung nicht wahr wird. Nun müssen Sie noch nach einer Möglich-
244
Sandini Bib
keit suchen, in einer normalen Anwendung eine Meldung auszugeben. Dazu können Sie die Prozedur ShowMessage verwenden: 19 else 20 begin 21 { Meldung ausgeben } 22 ShowMessage('Sie haben mindestens eine ungültige ' + 23 'Zahl eingegeben'); 24 end 25 end;
Damit ist das Programm abgeschlossen.
4.6
Einfaches Debuggen
Nun, da Sie bereits einfache Programme schreiben können, werden Sie wohl auch den einen oder anderen Fehler machen. Die Suche nach Programmfehlern, das so genannte Debugging, ist zwar bereits eine fortgeschrittene Technik, die in einem Anfänger-Buch eigentlich erst in späteren Kapiteln beschrieben werden sollte. Nach meiner Erfahrung machen aber gerade Anfänger viele Fehler, die nur über wenigstens grundlegende Debugging-Techniken effizient (und damit Zeit und vor allen Dingen Nerven sparend) gefunden werden können. Damit will ich mich aber nicht davon freisprechen. Ohne einen Debugger wäre ich nicht in der Lage, Programme zu schreiben .
-
Delphi, Kylix und Sun ONE Studio 4 integrieren einen jeweils sehr guten Debugger, den Sie relativ einfach nutzen können. Diese Debugger sind sehr mächtig und bieten viele spezielle Features zur Fehlersuche. Am Anfang und auch eigentlich in den meisten Fällen in der Praxis reichen einfache, grundlegende Techniken aber aus. Und diese beschreibe ich hier.
4.6.1 Grundlagen Ausnahmen und logische Fehler Die beiden Fehlerarten, die Sie debuggen müssen, sind Ausnahmen und logische Fehler. Ausnahmen treten ein, wenn Ihr Programm auf einen Fehler trifft, der eine weitere Ausführung verhindert. Wenn Sie beispielsweise eine Datei einlesen wollen, und diese Datei ist nicht vorhanden, tritt beim Versuch, die Datei zu öffnen, eine Ausnahme ein. Eine andere Ausnahme haben Sie bereits im Delphi/Kylix-Bruttoberechnungsprogramm aus Kapitel 2 für den Fall kennen gelernt, dass die Ein-
245
Sandini Bib
gaben des Anwenders nicht in eine Zahl konvertiert werden können. Ausnahmen können Sie abfangen, wie ich es ab Seite 250 noch beschreibe. Nicht abgefangene Ausnahmen führen allerdings dazu, dass das Programm im Debugger anhält. Ausnahmen sind meist recht einfach zu debuggen, weil der Debugger direkt an der Anweisung anhält, die die Ausnahme verursacht hat. Viel schwieriger ist dagegen das Finden logischer Fehler. Diese Fehler führen in der Regel nicht zu Ausnahmen, sondern zu einem Fehlverhalten des Programms. Wenn Sie beispielsweise die Berechnung des Bruttobetrags in der Bruttoberechnungs-Anwendung falsch programmiert haben, wird lediglich ein falsches Ergebnis ausgegeben. In einigen Fällen führen logische Fehler auch in nachfolgenden Anweisungen zu Ausnahmen, beispielsweise, wenn der Name einer zu öffnenden Datei im Programm dynamisch ermittelt wird, aber das entsprechende Teilprogramm fehlerhaft arbeitet. Die Ursache eines logischen Fehlers zu finden ist in größeren Programmen recht schwierig, weil Sie nicht genau wissen, welche Anweisung den Fehler verursacht hat. Haltepunkte Zum Lokalisieren logischer Fehler können Sie in fast allen Entwicklungsumgebungen im Quellcode so genannte Haltepunkte setzen. Ein Haltepunkt bewirkt, dass der Debugger das Programm bei der Ausführung anhält. Haltepunkte setzen Sie dort, wo Sie die Ursache eines logischen Fehlers vermuten. Das kann auch durchaus eine Anweisung sein, die ganz am Anfang eines Programms steht. Ein Debugger bietet immer die Möglichkeit, ein angehaltenes Programm schrittweise weiter auszuführen und dabei die Inhalte von Variablen zu beobachten. So können Sie logische Fehler recht einfach finden und beseitigen. Die folgenden Abschnitte zeigen nun, wie Sie diese Technik in Sun ONE Studio 4 und Delphi/Kylix nutzen. Ich gehe dabei nicht auf Ausnahmen ein, sondern nur auf logische Fehler. Ausnahmen können Sie prinzipiell auf dieselbe Art debuggen. Ein Beispiel Um ein sinnvolles Beispiel für das Debugging zu verwenden, setze ich ein Programm ein, das ermitteln soll, ob eine Zahl eine Primzahl ist. Das Programm arbeitet fehlerhaft. Zahlen, die eindeutig Primzahlen sind, werden nicht als solche erkannt:
246
Sandini Bib
01 public class Main 02 { 03 public static void main(String args[]) 04 { 05 int zahl = 7, teiler; 06 boolean primzahl = false; 07 08 for (teiler = 2; teiler <= zahl; teiler++) 09 { 10 if (zahl % teiler == 0) 11 primzahl = false; 12 } 13 14 if (primzahl) 15 System.out.println(zahl + " ist eine Primzahl"); 16 else 17 System.out.println(zahl + " ist keine Primzahl"); 18 } 19 } 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
program Debugging; {$APPTYPE CONSOLE} uses SysUtils; var zahl, teiler: integer; var primzahl: boolean; begin zahl := 7; primzahl := true; for teiler := 2 to zahl do begin if zahl mod teiler = 0 then primzahl := false; end; if primzahl = true then writeln(zahl, ' ist eine Primzahl.') else writeln(zahl, ' ist keine Primzahl.'); end.
247
Sandini Bib
4.6.2 Einfaches Debuggen mit Sun ONE Studio 4 In Sun ONE Studio 4 setzen Sie zum Debuggen einen Haltepunkt, indem Sie im Quellcode-Editor in der Zeile, an der der Debugger anhalten soll, mit der Maus in der linken grauen Spalte klicken. Zum Debuggen des Primzahlen-Programms sollten Sie den Haltepunkt in einer Anweisung oberhalb der Schleife setzen, da diese mit ziemlich großer Wahrscheinlichkeit den Fehler verursacht. Wenn Sie das Programm nun ausführen ((Alt) (F5)), hält der Debugger am Haltepunkt an (Abbildung 4.3).
Abbildung 4.3: Der Sun ONE Studio 4-Debugger hat das Programm an einem Haltepunkt angehalten.
Sie können das Programm nun mit (F8) schrittweise weiter ausführen. (F8) führt dazu, dass das Programm bei eventuell aufgerufenen eigenen Funktionen nicht in diese hineinspringt. Mit (F7) würde der Debugger auch in Funktionen hineinspringen, damit Sie diese ebenfalls debuggen können. Überwachungsausdrücke
248
Beim schrittweisen Durchgehen sollten Sie die Inhalte von Variablen beobachten. Dazu können Sie den Mauscursor einfach auf eine Variable im Editor bewegen. ONE zeigt den Inhalt nach einer kurzen Wartezeit automatisch an. Besser ist jedoch in den meisten Fällen, einen so genannten Überwachungsausdruck (englisch: Watch) anzulegen. Betätigen Sie dazu (Strg) (ª) (F7) und geben Sie den Namen einer zu überwachenden Variablen ein. Für unser Beispiel eignen sich die Variablen zahl und teiler. ONE erlaubt zudem, komplexe Ausdrücke zur Überwachung einzugeben. Legen Sie also einen neuen Überwachungsausdruck
Sandini Bib
zahl % teiler an, um das jeweilige Ergebnis der Restwert-Division beob-
achten zu können. Wenn Sie nun schrittweise durch das Programm gehen, sehen Sie in jedem Schleifendurchlauf im „Watches“-Fenster die aktuellen Inhalte der Variablen und das Ergebnis des arithmetischen Ausdrucks.
Abbildung 4.4: Das Watches-Fenster beim letzten Schleifendurchlauf
Beim letzten Schleifendurchlauf erkennen Sie, dass die Restwert-Division Null ergibt. Das Programm verzweigt dann auch zur Anweisung, die die Variable primzahl auf false setzt. Den Fehler haben Sie auf diese Weise recht schnell lokalisiert: Die Schleife zählt den Teiler bis zu der zu prüfenden Zahl selbst hoch, da der Vergleich des Teilers mit der Zahl fälschlicherweise mit <= und nicht mit < erfolgt, und teilt im letzten Durchlauf die Zahl durch sich selbst. Wenn Sie den Fehler dann gefunden haben, können Sie diesen gleich beseitigen. Beachten Sie dabei, dass der Debugger diese Änderungen nicht berücksichtigt, so lange Sie das Programm weiter testen. Führen Sie das Programm also mit (Strg) (F5) zu Ende aus und starten Sie dieses erneut, um zu überprüfen, ob der Fehler beseitigt ist.
4.6.3 Einfaches Debuggen mit Delphi und Kylix In Delphi und Kylix setzen Sie einen Haltepunkt wie bei Sun ONE Studio 4, indem Sie im Quellcode-Editor in der linken, grauen Spalte klicken. Ein Haltepunkt wird durch eine rot markierte Zeile angezeigt. Starten Sie das Programm dann über (F9), hält der Debugger am Haltepunkt an. Nun können Sie wie bei ONE mit (F8) und (F7) schrittweise durch das Programm gehen, wobei (F7) dazu führt, dass das Programm auch in eventuell aufgerufene eigene Funktionen verzweigt. Zur Überprüfung des Werts einer Variablen können Sie den Mauscursor einfach auf die Variable setzen. Delphi und Kylix zeigen nach einer kurzen Zeit den Inhalt automatisch an. Sie können aber auch zur Überwachung von Variablen wie bei ONE Überwachungsausdrücke einsetzen. Betätigen Sie (Strg) (F5), um einen solchen anzulegen. Delphi und Kylix ermöglichen einige erweiterte Einstellungen für Überwachungsausdrücke, die aber an dieser Stelle nicht wichtig sind. Legen Sie je einen Überwachungsausdruck für die Variablen zahl und teiler an. Wie bei
249
Sandini Bib
ONE können Sie auch einfache arithmetische Ausdrücke überwachen. Erzeugen Sie also noch einen Überwachungsausdruck zahl mod teiler.
Abbildung 4.5: Anlegen eines Überwachungsausdrucks in Delphi
In der Überwachungsausdruckliste, die Sie über (Strg) (Alt) (w) öffnen können, sehen Sie die aktuelle Werte Ihrer Überwachungsausdrücke, wenn Sie schrittweise durch das Programm gehen. So finden Sie den Fehler wieder meist recht schnell. Wenn Sie diesen direkt beim Debuggen beseitigen, fragt Delphi/Kylix nach, ob Sie das Programm neu erzeugen wollen, wenn Sie dieses weiter ausführen. Wenn Sie mit Ja antworten, wird das Programm automatisch neu kompiliert und erneut gestartet. Wenn Sie mit Nein antworten, können Sie zwar weiter debuggen, das Programm verwendet dann aber die alte Version. Ihre Änderungen werden nicht berücksichtigt. Das sollten Sie immer beachten, wenn Sie ein Programm innerhalb einer Debugging-Sitzung verändern. Führen Sie das Programm nach dem Debuggen gegebenenfalls über (F9) zu Ende aus oder beenden Sie es über (Strg) (F2).
4.7
Abfangen von Ausnahmen
Wie Sie wahrscheinlich bereits festgestellt haben, treten Ausnahmen in einem Programm recht häufig auf. Beispiele dafür sind nicht numerische Eingaben, mit denen Ihr Programm rechnen will (wie in der Nettoberechnung aus Kapitel 2) oder Dateien, die Ihr Programm öffnen will, die aber nicht vorhanden sind. Eine Ausnahme informiert den Anwender bzw. den Programmierer darüber, dass ein besonderer Status eingetreten ist, der beachtet werden muss. Wenn Sie Ausnahmen nicht behandeln, werden diese im Programm an der Konsole oder in einem Dialog angezeigt, und das Programm danach beendet. Ausnahmen müssen Sie normalerweise immer abfangen. Der Java-Compiler zwingt Sie sogar in einigen Fällen dazu, Ausnahmen, die von Methoden erzeugt werden, entweder abzufangen oder explizit zu ignorieren. Beim Abfangen können Sie entscheiden, ob
250
Sandini Bib
Sie alle Ausnahmen gleich behandeln, oder ob Sie spezielle Ausnahmen separat abfangen. Zum Abfangen von Ausnahmen stehen Ihnen unter Delphi, Kylix und Java spezielle Sprachkonstrukte zur Verfügung. Aufgrund der Komplexität dieser Konstrukte beschreibe ich hier nur, wie Sie diese auf eine einfache Weise nutzen.
4.7.1 Abfangen in Delphi und Kylix In Delphi und Kylix nutzen Sie zum Abfangen von Ausnahmen das tryexcept-Konstrukt nach dem folgenden Grundschema: 01 02 03 04 05 06 07 08 09 10 11 12 13 14
writeln('Zahl eingeben: '); readln(eingabe); try { Anweisungen, die eine Ausnahme erzeugen können } zahl := StrToFloat(eingabe); writeln(zahl * 2); except { Ausnahme abfangen } on e: Exception do begin writeln('Fehler aufgetreten: ', e.Message); end; end;
Innerhalb des try-Blocks, der bis zum except reicht, programmieren Sie alle Anweisungen, die eine Ausnahme erzeugen können, und alle Anweisungen, die nur bei einer fehlerfreien Ausführung der anderen Anweisungen ausgeführt werden sollen. Im Beispiel erzeugt Zeile 6 eine Ausnahme wenn der eingegebene Wert nicht in eine Zahl konvertiert werden kann. Die Anweisung in Zeile 7 soll nur dann ausgeführt werden, wenn die Konvertierung erfolgreich war und ist deswegen auch im try-Block programmiert. In Zeile 8 beginnt dann der except-Block, der auf eingetretene Ausnahmen reagiert. Wenn eine Anweisung im try-Block eine Ausnahme erzeugt, verzweigt das Programm sofort in den except-Block und arbeitet dessen Anweisungen ab. Die Anweisung in Zeile 10 bewirkt, dass das Programm auf alle Ausnahmen reagiert und weist die erzeugte Ausnahme gleich noch der Variablen e zu. In Zeile 12 wird der Fehler schließlich dem Anwender gemeldet. Die Variable e verweist auf ein Objekt vom Typ Exception. Dieses Objekt besitzt einige Eigenschaften. Die
251
Sandini Bib
wichtigste ist die Eigenschaft Message, die den Fehlertext enthält. In Zeile 12 wird dieser Fehlertext dann auch ausgegeben, damit der Anwender (und der Programmierer) weiß, was für ein Fehler aufgetreten ist. Umgang mit der Delphi/Kylix-Ausnahmemeldung Wenn Sie das Programm nun unter Delphi bzw. Kylix ausführen, werden Sie feststellen, dass der Debugger trotz Ausnahmebehandlung anhält, wenn Sie eine ungültige Zahl eingeben (Abbildung 4.6).
Abbildung 4.6: Delphi hat das Programm aufgrund einer Ausnahme angehalten.
Standardmäßig hält der Debugger nämlich an allen Ausnahmen an, auch an denen, die behandelt werden. Das ist schon etwas nervig, schließlich behandeln Sie die Ausnahme ja. Sie können den Dialog einfach bestätigen und danach (F9) betätigen, um das Programm weiter auszuführen. Um dieses Verhalten umzustellen, bleibt Ihnen bei Delphi und Kylix nur die Wahl, den Debugger entweder für alle Ausnahmen oder nur für spezifische abzuschalten. Die dazu notwendigen Einstellungen finden Sie in den Debugger-Optionen (im Menü TOOLS/DEBUGGER OPTIONS bzw. TOOLS/DEBUGGER-OPTIONEN). Im Register LANGUAGE EXCEPTIONS bzw. SPRACH-EXCEPTIONS können Sie festlegen, dass der Debugger bei Ausnahmen grundsätzlich nicht anhält, oder dass dieser spezielle Ausnahmen ignoriert. Den Typ der Ausnahme finden Sie heraus, wenn Sie das Programm testen und einen ungültigen Wert eingeben. In der Fehlermeldung zeigt Delphi/Kylix auch den Namen der Ausnahme an. Die Ausnahme, die in unserem Fall erzeugt wird, ist vom Typ EConvertError. Diese Ausnahme können Sie in die Liste der zu ignorierenden Ausnahmen einfügen (Abbildung 4.7).
252
Sandini Bib
Abbildung 4.7: Einfügen der Ausnahme EConvertError in die Liste der zu ignorierenden Ausnahmen in Kylix (der Text „Bei Delphi-Exceptions stoppen“ scheint ein kleiner Bug zu sein ...)
Leider hält der Debugger nun auch nicht mehr an unbehandelten Ausnahmen dieses Typs an. Ausnahmen werden dann aber im Programm gemeldet. Wenn Sie diese debuggen wollen, können Sie die entsprechende Option in der Ignorieren-Liste der Debugger-Optionen wieder abschalten. Das Ganze ist zwar recht nett, es ist aber schon sehr eigenartig, dass Delphi und Kylix keine Möglichkeit bieten, den Debugger so einzustellen, dass dieser nur an unbehandelten Ausnahmen anhält (so wie es in Visual Studio .NET möglich ist, der Entwicklungsumgebung für C#, Visual J# und Visual Basic .NET). Abfangen spezieller Ausnahmen Wenn Sie Ausnahmen auf die anfangs beschriebene einfache Art abfangen, können Sie nicht entscheiden, welche Art von Ausnahmen aufgetreten ist. Es kann z. B. auch sein, dass beim Einlesen der Daten von der Konsole ein interner Systemfehler auftritt. Das erste Beispiel fängt zwar alle Ausnahmen ab, kann aber keine spezielle Meldung ausgeben und muss sich auf die Meldung der Ausnahme verlassen. Sie können z. B. nicht melden, dass die Eingabe des Anwenders fehlerhaft ist. Tritt eine
253
Sandini Bib
andere Ausnahme ein, z. B. weil das System keinen Speicher mehr verfügbar hat, wäre Ihre Fehlermeldung vollkommen deplaziert. Um genauere Fehlermeldungen ausgeben zu können, können Sie spezielle Ausnahmen separat abfangen. Das ist nun einfacher, als Sie wahrscheinlich denken. Sie müssen lediglich wissen, welche Art von Ausnahme erzeugt wird. Und das ist auch nicht besonders schwierig. Den Namen der Ausnahme melden Delphi und Kylix immer dann, wenn der Debugger das Programm aufgrund einer Ausnahme angehalten hat. Schalten Sie also gegebenenfalls zur Überprüfung der Art der Ausnahme den Debugger wieder ein. In unserem Fall wird eine Ausnahme vom Typ EConvertError erzeugt, wenn eine ungültige Zahl eingegeben wird. Diese Ausnahme können Sie nun separat abfangen: 01 try 02 { Anweisungen, die eine Ausnahme erzeugen können } 03 writeln('Zahl eingeben: '); 04 readln(eingabe); 05 zahl := StrToFloat(eingabe); 06 writeln(zahl * 2); 07 except 08 { Spezielle Ausnahme abfangen } 09 on e: EConvertError do 10 begin 11 writeln('Sie haben eine ungültige Zahl eingegeben'); 12 end; 13 { Alle anderen Ausnahmen abfangen } 14 on e: Exception do 15 begin 16 writeln('Unerwarteter Fehler aufgetreten: ', e.Message); 17 end; 18 end;
Sie fügen dazu lediglich einen on-Block in den except-Block ein, der die Ausnahme bezeichnet. In diesem Block können Sie dann eine spezifische Fehlermeldung ausgeben. Beachten Sie, dass die Behandlung der anderen möglichen Ausnahmen (ab Zeile 14) immer hinter der Behandlung der speziellen Ausnahmen stehen muss. Wäre dies umgekehrt, wäre das Programm bei jeder Ausnahme mit der allgemeinen Behandlung zufrieden und würde die speziellen Behandlungen erst gar nicht mehr berücksichtigen.
254
Sandini Bib
4.7.2 Abfangen in Java Java stellt zum Abfangen von Ausnahmen ein ähnliches Konzept zur Verfügung wie Delphi und Kylix, lediglich mit einer anderen Syntax. Sie können über die Klasse Exception alle Ausnahmen gemeinsam oder über spezielle Ausnahmeklassen Ausnahmen separat abfangen. Anders als Delphi bzw. Kylix zwingt der Java-Compiler Sie sogar häufig dazu, alle Ausnahmen, die eine Anweisung erzeugen kann, abzufangen. Da die Eingabe in Java etwas kompliziert ist, verwende ich im folgenden Beispiel einfach eine Stringvariable, die mit einer ungültigen Zahl initialisiert wird: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22
String eingabe = "75oo"; int zahl; try { /* Anweisungen, die eine Ausnahme erzeugen können, oder die nur dann ausgeführt werden sollen, wenn keine Ausnahme eintritt */ zahl = Integer.parseInt(eingabe); System.out.println(zahl * 2); } /* Koonvertierungs-Ausnahmen abfangen */ catch (NumberFormatException e) { System.out.println("Sie haben keine Zahl eingegeben"); } /* Alle anderen Ausnahmen abfangen */ catch (Exception e) { System.out.println("Unerwarteter Fehler: " + e.getMessage()); }
Java arbeitet mit ähnlichen Blöcken wie Delphi und Kylix, nur dass der except-Block nun catch („fangen“) heißt. Das Prinzip ist aber dasselbe:
Zuerst werden die speziellen Ausnahmen abgefangen, dann alle anderen. Das Programm erzeugt in Zeile 9 eine Ausnahme vom Typ NumberFormatException wenn die Konvertierung fehlschlägt. Ich habe die Konvertierung über die Methode parseInt der Klasse Integer vorgenommen. Diese Methode gibt den konvertierten Wert zurück, falls keine Ausnahme eintritt.
255
Sandini Bib
In Zeile 12 wird diese spezielle Ausnahme separat abgefangen (wie im Delphi/Kylix-Programm). Den Typ der erzeugten Ausnahme finden Sie heraus, indem Sie die Anweisung zunächst ohne Ausnahmebehandlung programmieren. Wenn Sie das Programm dann ausführen und die Ausnahme provozieren (im Beispiel durch eine fehlerhafte Eingabe), erscheint der Name der Ausnahme in der von Java automatisch ausgegebenen Fehlermeldung. In der allgemeinen Ausnahmebehandlung ab Zeile 18 liest das Programm zusätzlich die Fehlermeldung aus. Dazu verwenden Sie die Methode getMessage, die jedes Ausnahmeobjekt besitzt. Ich denke, ich muss nicht mehr dazu schreiben. Schließlich sind die Ausnahmebehandlungen in Delphi/Kylix und Java (abgesehen von der unterschiedlichen Syntax) fast identisch.
4.8
Zusammenfassung
Zur Umsetzung einfacher Algorithmen können Sie nun mit den wichtigsten Handwerkszeugen des Programmierers umgehen. Sie können Variablen deklarieren und anwenden, Anweisungen schreiben und kennen die Standarddatentypen von Delphi/Kylix und Java. Mit Datentypen können Sie bereits recht sicher umgehen, weil Sie wissen, wie der Compiler diese behandelt. Sie wissen, dass Sie immer mit den für Ihren Zweck korrekten Datentypen arbeiten sollten, und können Datentypen auch in andere Datentypen umwandeln, falls dies notwendig sein sollte. In dem Zusammenhang kennen Sie auch die Gefahren von Überläufen und wissen, wie Sie idealerweise damit umgehen. Sie kennen die wichtigen Operatoren für arithmetische Ausdrücke und auch die „Falle“, die der Divisionsoperator von Java beinhaltet. Damit Ihre Programme von anderen Programmierern verstanden werden, setzen Sie aussagekräftige Bezeichner für Ihre Variablen ein und kommentieren Ihren Quellcode möglichst ausführlich. Bei der Suche nach Lösungen für Ihr Programm kennen Sie die Bedeutung der Bibliotheken von Delphi/Kylix und Java und wissen, wie Sie grundsätzlich damit umgehen. Schließlich können Sie die Ausnahmen, die Ihre Programme bei der Benutzung von Bibliotheks-Funktionen und -Methoden erzeugen, gezielt abfangen und dem Anwender eine aussagekräftige Fehlermeldung übergeben.
256
Sandini Bib
4.9
Fragen und Übungen
1. Wie viele verschiedene Speicherbereiche verwendet ein Programm
bei der Ausführung der folgenden Berechnung: i = (1 + 2) * 3;
i ist dabei eine Variable. 2. Wieso kann ein Short-Integer-Datentyp lediglich Zahlen im Bereich
von –32768 bis 32767 speichern? 3. Was sollten Sie immer beachten, wenn Sie mit den Datentypen Float
oder Double arbeiten? 4. Wieso können Sie mit Datumsdatentypen arithmetische Berechnun-
gen ausführen (z. B. einem Datum 100 Tage aufaddieren)? 5. Wann muss ein Datentyp, der einem anderen Datentyp zugewiesen
wird, explizit konvertiert werden? 6. Warum ergibt die folgende Division in einem Java-Programm nicht
wie erwartet 0,25, sondern 0,0? int zahl1 = 1; int zahl2 = 4; double ergebnis = zahl1 / zahl2; 7. Welchen Sinn hat die Einteilung der Klassen der Java-Bibliothek in
einzelne Pakete?
257
Sandini Bib
Sandini Bib
5
Die Strukturierung eines Programms
Sie lernen in diesem Kapitel:
le
n e rn
• was ein Vergleichsausdruck ist und wie Sie Vergleichsausdrücke zur Formulierung von Bedingungen für Schleifen und Verzweigungen schreiben, • wie Sie Schleifen in Delphi, Kylix und Java programmieren und welche Möglichkeiten Sie dabei besitzen, • wie Sie ein Programm über die zwei grundsätzlich möglichen Verzweigungen bedingungsabhängig ausführen, • wie Sie Programmteile, die Sie häufiger benötigen oder in anderen Projekten wiederverwenden können, in Prozeduren oder Funktionen umsetzen, • wie Sie mit globalen Variablen umgehen. Nachdem Sie im vorherigen Kapitel die grundlegenden Handwerkszeuge eines Programmierers kennen gelernt haben, erfahren Sie in diesem Kapitel, wie Sie Algorithmen in einer Programmiersprache formulieren. Mit dem bereits Gelernten können Sie zwar sehr einfache Algorithmen in Programme umsetzen, die meisten Algorithmen verlangen aber, dass Programmteile bedingungsabhängig verzweigt oder mehrfach wiederholt werden. Dazu bieten alle Programmiersprachen spezielle Anweisungen zum Programmieren von Schleifen und Verzweigungen. Wenn Sie diese korrekt einsetzen, programmieren Sie strukturiert. Damit Sie wissen, warum die strukturierte Programmierung überhaupt so heißt, erfahren Sie zuvor an einem einfachen Beispiel, was ein unstrukturiertes Programm ist. Bevor ich dann auf die verschiedenen Möglichkeiten zur Programmierung von Schleifen und Verzweigungen in Delphi, Kylix und Java eingehe, lernen Sie, wie Sie Vergleichsausdrücke programmieren, um die Bedingungen von Schleifen und Verzweigungen zu formulieren. Nebenbei erfahren Sie im Schleifen-Abschnitt gleich noch, wie Sie ein Pro-
259
Sandini Bib
gramm möglichst optimiert gestalten und wie Sie die Performance eines Programms messen können. Schließlich erfahren Sie noch, wie Sie eine wichtige Technik bei der Programmierung, die Wiederverwendung von Programmcode, über eigene Funktionen und Prozeduren in Ihren Programmen umsetzen.
5.1
Strukturierte und unstrukturierte Programme
Wenn Sie das, was Sie in diesem Kapitel lernen, konsequent einsetzen, programmieren Sie strukturiert. Nur damit Sie wissen, dass es auch anders geht und vorher der Begriff „Strukturierte Programmierung“ kommt, zeige ich kurz, wie Sie unstrukturiert programmieren können. Die unstrukturierte Programmierung arbeitet nicht mit Schleifen und Verzweigungen, sondern mit einer Anweisung, die bei den meisten Programmierern unbeliebt ist. Über die Anweisung goto, die in fast allen Programmiersprachen (leider) immer noch verfügbar ist, können Sie mitten im Programm an eine andere Anweisung springen. In den Anfängen der Programmierung, als es noch keine Schleifen und Verzweigungen gab, war das die einzige Möglichkeit, Programmteile wiederholt oder bedingungsabhängig auszuführen. Das Programm zur Berechnung eines Nettobetrages aus dem vorherigen Kapitel können Sie auch so programmieren: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
260
program Bruttorechner; {$APPTYPE CONSOLE} uses SysUtils; var var var var var var
weiter: string; eingabe: string; brutto: double; steuer: double; netto: double; nettoFormatiert: string;
label bruttoeingabe; label steuereingabe;
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
begin bruttoeingabe: write('Geben Sie den Bruttobetrag ein: '); readln(eingabe); brutto := StrToFloatDef(eingabe, -1); if brutto < 0 then goto bruttoeingabe; steuereingabe: write('Geben Sie den Steuerwert ein: '); readln(eingabe); steuer := StrToFloatDef(eingabe, -1); if (steuer < 0) or (steuer > 100) then goto steuereingabe; netto := brutto * (1 - (steuer / (100 + steuer))); write('Nettobetrag: '); nettoFormatiert := FormatFloat('0.00', netto); writeln(nettoFormatiert); write('Noch einmal? '); readln(weiter); if weiter = 'j' then goto bruttoeingabe; end.
Das Programm arbeitet mit so genannten Labeln. Ein Label ist eine Markierung, an die Sie mit goto springen können. Die Abfragen unter den Eingaben und unter der letzten Anweisung führen zu einer wiederholten Ausführung des jeweiligen Programmteils bzw. des Programms, wenn die Bedingung jeweils erfüllt ist. Das Programm springt dazu an die Anweisung hinter dem jeweiligen Label zurück. Um wenigstens entscheiden zu können, dass an ein Label gesprungen werden muss, kennen bereits unstrukturierte (alte) Programmiersprachen die if-Verzweigung. Was erkennen Sie an diesem Beispiel? Unstrukturierte Programmierung ist sehr unübersichtlich (eben unstrukturiert). Das Programm springt an diversen Anweisungen einfach an andere, ohne dass Sie ohne weiteres erkennen können, was da überhaupt passiert. Vergleichen Sie dieses Programm mit dem Beispiel aus Kapitel 5 und Sie erkennen wohl den wichtigen Unterschied zwischen der strukturierten und der unstrukturierten Programmierung. Die strukturierte Programmierung ist wesentlich übersichtlicher. Wenn Sie strukturiert programmieren, machen Sie weniger Fehler (weil diese eher auffallen). Wenn Sie aber viel Zeit, keine Freunde, keine Beziehung, keinen Job und auch keine Hobbys und weiteren Interessen (außer dem Programmieren) haben, entwickeln Sie Ihre Programme unstrukturiert. Dann haben Sie genügend Beschäftigung ...
Unübersichtliche Programme
261
Sandini Bib
SpaghettiProgrammierung
Unstrukturierte Programmierung wird auch als „Spaghetti-Programmierung“ bezeichnet. Dieser Begriff kommt daher, dass der goto-Befehl Sprünge zu allen (im aktuelle Kontext erreichbaren) Anweisungen in einem Programm erlaubt. Wenn Sie ein Programm, das intensiven Gebrauch von der goto-Anweisung macht, auf einem Blatt Papier nachvollziehen und mit einem Stift die einzelnen der Reihe nach ausgeführten Anweisungen (über schwungvolle Linien) verbinden, sieht das Papier nachher aus wie ein Teller Spaghetti. Verbannen Sie das goto aus Ihrem Wortschatz. Sie können jedes Problem auch mit den Mitteln der strukturierten Programmierung lösen. In seltenen Fällen ist ein Programmierer zwar versucht, goto zu verwenden, weil es im Moment einfacher erscheint (vergleichen Sie dazu die Hilfe zu goto in der Delphi/Kylix-Dokumentation). Bedenken Sie aber, dass es dann sehr schwierig wird, Ihre Programme zu verstehen.
5.2
Vergleichsausdrücke
Schleifen und Verzweigungen werden bedingungsabhängig ausgeführt. Die Bedingung wird in einem Vergleichsausdruck überprüft. Ein Vergleichsausdruck arbeitet wie ein arithmetischer Ausdruck mit Operanden und Operatoren und besitzt ein Ergebnis. Das Ergebnis eines Vergleichsausdrucks ist allerdings immer nur „Wahr“ oder „Falsch“. Für diese Zustände stellen Programmiersprachen normalerweise die Konstanten true und false zur Verfügung, die Sie ja bereits vom booleschen Datentyp her kennen. Der Datentyp eines Vergleichsausdrucks ist dann auch boolean. Die Operanden eines Vergleichausdrucks können alle Standarddatentypen und häufig auch spezielle Datentypen wie die von Klassen besitzen (allerdings müssen Sie bei Objekten ein wenig aufpassen, was ich aber erst im nächsten Kapitel beschreibe). Sie können dort Konstanten einsetzen oder Variablen, Funktionen, arithmetische Ausdrücke oder sogar wieder Vergleichsausdrücke. So können Sie beispielsweise in Delphi/ Kylix eine Variable mit dem Ergebnis einer Funktion vergleichen: if zahl1 = Sqr(10) then ...
oder mit einem arithmetischen Ausdruck, der natürlich auch Funktionsaufrufe beinhalten kann: if zahl1 = (Sqr(10) + 1) / 3 then ...
262
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Wie bei arithmetischen Ausdrücken auch, verlangen die meisten Compiler, dass die Datentypen der Operanden in einem Vergleichsausdruck zueinander kompatibel sind. Sie können beispielsweise in Java keinen String mit einer Zahl vergleichen: int i = 10; String s = "10"; if (i == s) // Fehler "operator == cannot be applied to // int,java.lang.String"
Wenn Sie solche Vergleiche ausführen wollen, müssen Sie einen der Datentypen konvertieren. Das kennen Sie ja bereits aus Kapitel 4. Die Operatoren für Vergleichsausdrücke (die auch „relationale Operatoren“ genannt werden) unterscheiden sich von denen, die für arithmetische Ausdrücke verwendet werden. Tabelle 5.1 zeigt die Vergleichsoperatoren von Delphi/Kylix und Java. Vergleich
Delphi/ Kylix
Java
Beschreibung
Gleich
=
==
Prüft darauf, ob beide Operanden gleich groß sind
Ungleich
<>
!=
Prüft darauf, ob beide Operanden ungleich sind
Größer
>
>
Prüft darauf, ob der linke Operand größer als der rechte ist
Größer oder gleich
>=
>=
Prüft darauf, ob der linke Operand größer als der rechte ist oder ob beide Operanden gleich groß sind
Kleiner
<
<
Prüft darauf, ob der linke Operand kleiner als der rechte ist
Kleiner oder gleich
<=
<=
Prüft darauf, ob der linke Operand kleiner als der rechte ist oder ob beide Operanden gleich groß sind
Tabelle 5.1: Die Vergleichsoperatoren von Delphi, Kylix und Java
Der Operator für die Überprüfung auf Gleichheit ist in Delphi/Kylix und Java unterschiedlich. Das liegt daran, dass der Compiler den Gleichheitsoperator vom Zuweisungsoperator unterscheiden muss. Der Zuweisungsoperator ist in Object Pascal das Symbol :=, deshalb kann hier das Zeichen = als Gleichheitsoperator verwendet werden. Java setzt für Zuweisungen das Zeichen = ein. Um Zuweisungen von Vergleichen unterscheiden zu können, ist also ein anderes Symbol notwendig. Java verwendet deswegen für eine Überprüfung auf Gleichheit die Zeichen ==.
9HUJOHLFKVDXVGUFNH
263
Sandini Bib
Ein Vergleichsausdruck, der überprüft, ob zwei Variablen den gleichen Wert speichern, sieht zum Beispiel in Java so aus: if (zahl1 == zahl2)
Vergleich von Zeichen und Strings Wenn Sie zwei Zeichen miteinander vergleichen, vergleicht der Compiler den ASCII- bzw. Unicode-Code dieser Zeichen. Der Object PascalVergleich if 'a' > 'A' then
ergibt z. B. true, weil das Zeichen a immer einen größeren Zeichencode besitzt als das A. Der Compiler vergleicht hier die Zahlen 97 (a) mit 65 (A). Die Zeichencodes finden Sie übrigens in der ASCII-Tabelle im Anhang. Besonders beim Vergleich von Strings führt das manchmal zu „eigenartigen“ Ergebnissen (die aber jeder Programmierer kennt). Wenn Sie beispielsweise zwei Strings miteinander vergleichen, die die Zahlwerte 2 und 1000 speichern: if '2' > '1000' then
ergibt dieser Vergleich true! Die „Zahl“ "2" ist größer als die „Zahl“ "1000". Für den Compiler sind das aber keine Zahlen. Er führt einen Stringvergleich durch. Und bei diesem besitzt das linke Zeichen die höchste Priorität. Ist der Code dieses Zeichens größer als der des ersten Zeichens im anderen String, ist der gesamte String größer. Ist der Code identisch, wird am nächsten Zeichen gesucht etc. Darauf müssen Sie immer achten, wenn Sie Strings miteinander vergleichen oder Strings sortiert ausgeben.
Vorsicht bei Stringvergleichen in Java
In Java-Programmen können Sie Strings nicht direkt miteinander vergleichen. Strings sind in Java in Wirklichkeit Objekte. Objekte lernen Sie erst in Kapitel 6 kennen. Leider werden Sie hier bereits damit konfrontiert. Wenn Sie zwei Strings vergleichen: if ("a" == "a")
vergleichen Sie nicht den Wert der Strings (also die Zeichen). Der JavaCompiler erzeugt aus den einfachen Stringliteralen jeweils ein eigenständiges Objekt, das die Strings speichert. Ein Vergleich zweier Stringobjekte überprüft die Werte der Referenzen, die im Programm auf die Objekte zeigen. Referenzen lernen Sie ebenfalls in Kapitel 6 kennen. Der Vergleich zweier Referenzen führt, wenn überhaupt, dann nur zufällig
264
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
zu einer Gleichheit. Der Vergleich oben ergibt also in Java prinzipiell immer false. In Java müssen Sie Methoden der Stringobjekte verwenden, um einen Vergleich durchzuführen. Jedes Stringobjekt besitzt dazu die Methoden compareTo und compareToIgnoreCase:
Strings in Java vergleichen
String s1 = "a"; String s2 = "b"; int i = s1.compareToIgnoreCase(s2); if (i < 0) System.out.println("s1 ist kleiner s2"); else if (i == 0) System.out.println("s1 ist kleiner s2"); else System.out.println("s1 ist größer s2"); compareToIgnoreCase vergleicht unter Vernachlässigung der Groß-/Klein-
schreibung (ein großes A wird als gleichwertig zu einem kleinen a gewertet), compareTo vergleicht normal. Beide Methoden geben die folgenden Werte zurück: • 0 , wenn beide Strings gleich sind, • einen Wert kleiner 0, wenn der String, von dem aus die Methode aufgerufen wurde, kleiner ist als der andere, • einen Wert größer 0, wenn der String, von dem aus die Methode aufgerufen wurde, größer ist als der andere. Vergleichsausdrücke in Bedingungen In Bedingungen von Schleifen und Verzweigungen werden Vergleichsausdrücke immer auf das Ergebnis true überprüft. Eine in einer Verzweigung eingesetzte Bedingung sieht in Delphi/Kylix z. B. so aus: if zahl1 > 0 then begin ... end;
Das Programm wertet zuerst den Vergleichsausdruck aus und führt den bedingungsabhängigen Teil nur dann aus, wenn der Vergleichsausdruck true ergibt.
9HUJOHLFKVDXVGUFNH
265
Sandini Bib
Vergleichsausdrücke mit mehreren Bedingungen Vergleichsausdrücke arbeiten häufig nicht nur mit einer, sondern mit mehreren Bedingungen. Solch ein komplexer Vergleichsausdruck setzt sich aus einzelnen Vergleichsausdrücken zusammen, die mit Hilfe von speziellen logischen Operatoren miteinander kombiniert werden. Tabelle 5.2 beschreibt die logischen Operatoren von Delphi, Kylix und Java. Operation
Delphi/ KylixOperator
JavaOperator
Bedeutung
UndVerknüpfung
and
&&
Kombiniert zwei Teilausdrücke so, dass das Ergebnis true ist, wenn beide Teilausdrücke true ergeben.
OderVerknüpfung
or
||
Kombiniert zwei Teilausdrücke so, dass das Ergebnis true ist, wenn nur einer der Teilausdrücke true ergibt.
Negierung
not
!
negiert einen Teilausdruck. Aus true wird false und aus false wird true.
Tabelle 5.2: Die logischen Operatoren von Delphi, Kylix und Java Und- und OderVerknüpfung
Wenn Sie zwei Teilausdrücke (Vergleiche) über eine Und-Verknüpfung kombinieren, ist das Ergebnis des Gesamtausdrucks nur dann true, wenn beide Teilausdrücke true ergeben. Kombinieren Sie zwei Teilausdrücke über eine Oder-Verknüpfung, ist das Gesamtergebnis true, wenn nur einer der Teilausdrücke true ergibt. Der Java-Ausdruck (1 == 1) && (2 == 3)
ergibt false, der Ausdruck (1 == 1) || (2 == 3)
ergibt true.
266
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Sie sollten jeden einzelnen Teilausdruck einer zusammengesetzten Bedingung grundsätzlich immer klammern. Bei einigen Sprachen, wie Object Pascal und Visual Basic, besitzen die Operatoren not, and und or (leider) eine Doppelbedeutung. Sie können diese Operatoren nicht nur für die Verknüpfung von Vergleichsausdrücken, sondern auch dazu verwenden, die einzelnen Bits einer numerischen Variablen zu manipulieren und abzufragen. Mit dieser Technik können Sie in einer ByteVariablen acht verschiedene boolesche Zustände verwalten. Diese sehr spezielle Technik beschreibe ich im Buch nicht weiter. Ein einfaches Beispiel soll hier ausreichen. Wenn Sie beispielsweise in einem Delphi/ Kylix-Programm in einer Integer-Variablen das dritte Bit von rechts (das mit der Wertigkeit 4) setzen wollen, verwenden Sie den folgenden Ausdruck: i := i or 4;
Unabhängig davon, ob das dritte Bit vorher bereits gesetzt war, wird es durch diesen Ausdruck auf 1 gesetzt. Auf eine ähnliche Weise können Sie abfragen, ob ein Bit gesetzt ist: if (i and 4) = 4 then ...
Wenn Sie nun einen Ausdruck wie den folgenden formulieren: if zahl1 > 1 and zahl2 > 1 then
meldet der Compiler den Fehler „Incompatible types“. Er werten nämlich das and als bitweise Operation und führt diese zuerst aus (1 and zahl2). Der resultierende Ausdruck ist dann syntaktisch falsch, was Sie erkennen können, wenn Sie entsprechen der Compiler-Priorität klammern: if zahl1 > (1 and zahl2) > 1 then
Dem zweiten Vergleich fehlen nun der logische Operator und der linke Operand. Wenn Sie die Teilausdrücke klammern, ist alles in Ordnung: if (zahl1 > 1) and (zahl2 > 1) then
Jetzt erkennt der Compiler, dass es keine bitweise Operation sein kann. In einigen Fällen wird bei einer inkorrekten Klammerung übrigens auch der Fehler „Operator not applicable to this operand type“ gemeldet. Das folgende Beispiel demonstriert die Anwendung des and-Operators: Ein Handlesvertreter soll nur dann zehn Prozent Bonus erhalten, wenn er mindestens zehn Verträge mit einem Gesamtwert von mindestens 50.000 Euro abgeschlossen hat:
9HUJOHLFKVDXVGUFNH
267
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19
program Vergleichsausdruecke; {$APPTYPE CONSOLE} uses SysUtils; var anzahlVertraege: integer; var vertragsSumme: double; begin write('Anzahl Verträge: '); readln(anzahlVertraege); write('Vertragssumme: '); readln(vertragsSumme); if (anzahlVertraege >= 10) and (vertragsSumme >= 50000) then writeln('Der Vertreter erhält 10% Bonus') else writeln('Der Vertreter erhält keinen Bonus'); readln; end.
In Java sieht das Ganze folgendermaßen aus (auf die Eingabe der Daten habe ich allerdings verzichtet): 01 class vergleichsausdruecke 02 { 03 public static void main(String args[]) 04 { 05 int anzahlVertraege = 10; 06 double vertragsSumme = 51000; 07 if ((anzahlVertraege >= 10) && (vertragsSumme >= 50000)) 08 System.out.println("Der Vertreter erhält 10% Bonus"); 09 else 10 System.out.println("Der Vertreter erhält keinen Bonus"); 11 } 12 }
Ein etwas abgewandeltes Beispiel demonstriert den Oder-Operator: Ein Vertreter soll nur dann zehn Prozent Bonus erhalten, wenn er in einem Monat mindestens zehn Verträge abgeschlossen hat oder die Summe aller Verträge mindestens 50.000 Euro beträgt: 14 if (anzahlVertraege >= 10) or (vertragsSumme >= 50000) then 15 writeln('Der Vertreter erhält 10% Bonus') 16 else 17 writeln('Der Vertreter erhält keinen Bonus');
268
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Sie können die Und- und die Oder-Verknüpfung natürlich beliebig kombinieren. Dabei müssen Sie aber immer darauf achten, dass die Und-Verknüpfung eine höhere Priorität besitzt als die Oder-Verknüpfung und demnach immer als erste ausgewertet wird (wenn Sie nicht klammern). Das folgende Beispiel demonstriert die Probleme, die durch eine fehlende Klammerung entstehen können. Ein Handelsvertreter soll nur dann zehn Prozent Bonus erhalten, wenn er • mehr als zwei neue Kunden akquiriert und mindestens zehn Verträge mit einem Gesamtwert von mindestens 50.000 Euro abgeschlossen hat oder • mehr als zwei neue Kunden akquiriert hat und beliebig viele Verträge mit einem Gesamtwert von mindestens 100.000 Euro abgeschlossen hat. Zur Lösung dieses Problems können Sie nicht den folgenden Quellcode verwenden: 10 11 12 13 14 15 16 17 18 19 20
write('Anzahl neuer Kunden: '); readln(anzahlNeueKunden); write('Anzahl Verträge: '); readln(anzahlVertraege); write('Vertragssumme: '); readln(vertragsSumme); if (anzahlNeueKunden > 2) and (anzahlVertraege >= 10) and (vertragsSumme >= 50000) or (vertragsSumme >= 100000) then writeln('Der Vertreter erhält 10% Bonus') else writeln('Der Vertreter erhält keinen Bonus');
Da der Compiler erst alle Und-Verknüpfungen auflöst und erst danach die Oder-Verknüpfung, führt dieses Programm in einigen Fällen zu einem falschen Ergebnis. Wenn Sie z. B. für die Anzahl der Verträge 5, für die Anzahl neuer Kunden 1 und für die Summe 120000 eingeben, erhält der Vertreter den Bonus, obwohl er diesen eigentlich nicht erhalten sollte, da er nicht mindestens drei Neukunden akquiriert hat.
9HUJOHLFKVDXVGUFNH
269
Sandini Bib
Verwenden Sie immer Klammern, wenn Sie Teilausdrücke miteinander kombinieren und sicherstellen wollen, dass ein Teilausdruck komplett und unabhängig von anderen Teilausdrücken ausgewertet wird. Die Fehler, die ohne Klammern durch die Priorität der logischen Operatoren in einigen Programmen entstehen, sind nur sehr schwer zu finden. Es kann sein, dass Sie beim Testen des Programms zufällig Werte eintragen, die zu einem korrekten Ergebnis führen. Beim Kunden verursacht Ihr Programm dann aber Probleme. Und wenn Sie und Ihr Kunde Pech haben, werden die fehlerhaften Daten erst viel zu spät entdeckt. Das obige Beispiel sollte demnach so aussehen: 16 17 18 19 20
if (anzahlNeueKunden > 2) and (((anzahlVertraege >= 10) and (vertragsSumme >= 50000)) or (vertragsSumme >= 100000)) then writeln('Der Vertreter erhält 10% Bonus') else writeln('Der Vertreter erhält keinen Bonus');
Das Beispiel ist ein wenig konstruiert. Normalerweise würde ich die Teilbedingungen im Programm auch so definieren, wie ich diese verbal formuliert habe. Eine verbal formulierte Bedingung ist meist automatisch so aufgebaut, dass kaum Fehler entstehen können. Das Beispiel würde etwas natürlicher formuliert auch ohne Klammern funktionieren: 16 17 18 19 20 21 22
if (anzahlNeueKunden > 2) and (anzahlVertraege >= 10) and (vertragsSumme >= 50000) or (anzahlNeueKunden > 2) and (vertragsSumme >= 100000) then writeln('Der Vertreter erhält 10% Bonus') else writeln('Der Vertreter erhält keinen Bonus'); readln;
Diese Bedingung funktioniert auch ohne Klammern, weil die Priorität der Operatoren dafür sorgt, dass zuerst die Und-Verknüpfungen ausgewertet werden. Klammern Sie aber trotzdem immer, um potenzielle Fehler zu vermeiden und um Ihre Programme übersichtlicher zu gestalten. Auflösen von komplexen Vergleichsausdrücken Wenn Sie einen komplexen Vergleichsausdruck verstehen oder überprüfen wollen, setzen Sie für die Variablen Beispiel-Werte ein und lösen die einzelnen Teilbedingungen von innen nach außen auf (Klammern werden immer von innen nach außen aufgelöst). Bei der Eingabe
270
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
anzahlNeueKunden := 2; anzahlVertraege := 11; vertragsSumme := 110000;
können Sie die einzelnen Teilausdrücke des Ausdrucks (anzahlNeueKunden > 2) and (((anzahlVertraege >= 10) and (vertragsSumme >= 50000)) or (vertragsSumme >= 100000))
etwa wie folgt auflösen: • Schritt 1: Werte einsetzen: (2 > 2) and (((11 >= 10) and (110000 >= 50000)) or (110000 >= 100000))
• Schritt 2: Innere Klammern auflösen: (2 > 2) and ((true and true) or (110000 >= 100000))
• Schritt 3: Nächste Klammerebene auflösen (2 > 2) and (true or true)
• Schritt 4: Nächste Klammerebene auflösen false and true
Das Ergebnis ist demnach false. Das ist doch einfach, oder? Der Not-Operator Der Not-Operator gehört ebenfalls zu den logischen Operatoren und wird bei Vergleichsausdrücken eingesetzt. Dieser Operator negiert den rechts von ihm stehenden Ausdruck. Aus true wird false, aus false wird true. Üblicherweise setzt man den Vergleichausdruck, der negiert werden soll, in Klammern: if not (zahl1 = zahl2) then // Delphi/Kylix if (!(zahl1 == zahl2)) // Java
Das Java-Beispiel wirkt etwas komplex. Java verlangt, dass die Bedingungen der if-Verzweigung immer in einer umschließenden Klammer angegeben werden. Der Operator ! kann nur auf Ausdrücke angewendet werden, die true oder false ergeben. Eine Bedingung wie if (!zahl1 == zahl2) // fehlerhafte Bedingung in Java
würde der Compiler mit dem Fehler „operator ! cannot be applied to int“ abweisen.
9HUJOHLFKVDXVGUFNH
271
Sandini Bib
Angenommen, in zahl1 und zahl2 sind dieselben Zahlen gespeichert, so geht das Programm bei der Auswertung dieses Ausdrucks etwa so vor: 1. if (!(zahl1 == zahl2)) 2. if (!true) 3. if (false)
Der Negierungs-Operator besitzt die höchste Priorität bei der Auswertung von Vergleichsausdrücken. Sind im Vergleichsausdruck Teilausdrücke enthalten, die mit einer Und- oder Oder-Verknüpfung miteinander kombiniert sind, wird immer zuerst die Negierung des Ausdrucks ausgeführt, der rechts vom Negierungs-Operator steht. Wenn Sie die Teilausdrücke nur minimal klammern, kann es dabei wieder passieren, dass in Ihrem Programm schwere logische Fehler entstehen. Wenn Sie z. B. ermitteln wollen, ob zwei Zahlen nicht einen Wert größer als 1 speichern, können Sie nicht die folgende (in Object Pascal programmierte) Variante verwenden: if not (zahl1 > 1) and (zahl2 > 1) then
Der Compiler wertet zwar zuerst die Klammern aus, dann aber sofort den not-Operator. Der Vergleich der beiden Ausdrücke erfolgt also mit dem negierten Ergebnis des linken Ausdrucks. Wenn Sie not einsetzen wollen, müssen Sie immer entsprechend klammern (es sei denn, der Ausdruck ist bereits in Ordnung): if not ((zahl1 > 1) and (zahl2 > 1)) then
Nun wird erst das Endergebnis negiert, was der Aufgabenstellung entspricht. Versuchen Sie, wo immer es geht, den Negierungs-Operator zu vermeiden. Seine hohe Priorität führt in vielen Fällen zu schweren logischen Fehlern im Programm und zu unübersichtlichen Programmen. Verwenden Sie stattdessen einen expliziten Vergleich mit false oder passen Sie die Operatoren an. Die Bedingung des obigen Beispiels kann z. B. auch so formuliert werden: if (zahl1 <= 1) or (zahl2 <= 1) then
272
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
In Delphi/Kylix (und in Visual Basic) müssen Sie wieder aufpassen: Der not-Operator besitzt neben seiner logischen auch eine bitweise Bedeutung. Er kippt die Bits einer Zahl um. Wenn eine Byte-Variable den Wert 0 speichert, macht der not-Operator daraus 255: byteZahl := 0; byteZahl := not byteZahl; // 255, weil alle Bits umgekippt wurden
Wenn Sie Zahlen in Vergleichsausdrücken einsetzen, den not-Operator verwenden und nicht klammern, sind Fehler wieder vorprogrammiert: if not byteZahl < 255 then // Fehlerhafter Vergleich
Diese Abfrage soll eigentlich prüfen, ob der in byteZahl gespeicherte Wert nicht kleiner ist als 255. Ist in byteZahl z. B. der Wert 128 gespeichert (der ja eindeutig kleiner ist), wird die Bedingung nicht wahr. Der Compiler wertet das not als bitweise Operation, kippt die Bits in byteZahl um und vergleicht dann 127 mit 255. 127 ist kleiner als 255, weswegen der Vergleich fälschlicherweise true ergibt. Das Verstehen dieses Vorgehens des Compilers und das Nachvollziehen der eventuellen Fehler, die dabei entstehen, sind so schwierig, dass selbst ich Probleme hatte, ein Beispiel zu finden, das zu einem Fehler führt. Vermeiden Sie diese Fehler und auch die Verständnisprobleme, die durch den Negierungs-Operator entstehen, idealerweise, indem Sie diesen erst gar nicht benutzen. Bedingungen können Sie immer so umformulieren, dass Sie den Negierungs-Operator nicht benötigen. Wenn Sie diesen trotzdem benutzen, klammern Sie: if not (byteZahl < 255) then // Korrekter Vergleich
5.3
Grundlagen zu Schleifen und Verzweigungen
Einrücken von Anweisungen Innerhalb einer Schleife oder Verzweigung werden oft sehr viele Anweisungen ausgeführt. Damit Sie und andere Programmierer die Übersicht nicht verlieren, sollten Sie alle Anweisungen innerhalb einer Schleife oder Verzweigung etwas einrücken. So können Sie sehr gut erkennen, welche Anweisungen zur Schleife bzw. Verzweigung gehören und welche nicht. Die meisten Programmierer verwenden dazu drei Leerzeichen oder einen Tabulator. Das folgende Deplhi/Kylix-Beispiel demonstriert dies:
273
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14
{ Anweisung vor der Verzweigung } write('Quadrat- und Wurzelberechnung'); { Hier beginnt eine Verzweigung } if zahl > 0 then begin wurzel := Sqrt(zahl); quadrat := Sqr(zahl); writeln('Quadrat: ', quadrat); writeln('Wurzel: ', wurzel); end; { Eine Anweisung nach der Verzweigung } writeln('Copyright (c) 2002 by EarthWare');
In Java-Programmen sollten Sie ähnlich einrücken: 01 02 03 04 05 06 07 08 09 10 11 12 13 14
/* Anweisung vor der Verzweigung */ System.out.println("Quadrat- und Wurzelberechnung"); /* Hier beginnt eine Verzweigung */ if (zahl > 0) { double wurzel = Math.sqrt(zahl); double quadrat = Math.pow(zahl, 2); System.out.println("Quadrat: " + quadrat); System.out.println("Wurzel: " + wurzel); } /* Eine Anweisung nach der Verzweigung */ System.out.println("Copyright (c) 2002 by EarthWare");
Wo Sie in Delphi und Java das Blockbeginn- und das Blockende-Schlüsselsymbol anbringen, ist Geschmackssache. Manche Programmierer gehen dabei auch so vor: if zahl > 0 then begin ... end; if (zahl > 0) { ... ...}
Ich finde diese Art allerdings eher unübersichtlich.
274
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Da Verzweigungen und Schleifen oft geschachtelt werden, werden die inneren Anweisungen immer weiter eingerückt: 01 02 03 04 05 06 07 08 09 10 11 12 13 14
System.out.println("Quadrat- und Wurzelberechnung"); if (zahl > 0) { int i; for (i=0; i
Wenn Sie konsequent einrücken, erhöhen Sie die Lesbarkeit Ihres Quellcodes. Vergleichen Sie dazu die nicht eingerückte Variante: 01
01 02 03 04 05 06 07 08 09 10 11 12 13 14
System.out.println("Quadrat- und Wurzelberechnung"); if (zahl > 0) { int i; for (i=0; i
Anweisungsblöcke und einfache Anweisungen Dass Schleifen und Verzweigungen häufig Anweisungsblöcke enthalten, wissen Sie nun. Es gibt aber immer auch eine einfache Variante, die nur einzelne Anweisungen ausführt. Für diese müssen Sie dann keine Blöcke erzeugen: If i < 10 then writeln('i ist gleich 10');
275
Sandini Bib
Ist kein Block enthalten, erkennt der Compiler das Ende der Schleife bzw. Verzweigung am abschließenden Semikolon. Dabei sollten Sie beachten, dass Sie so auch fehlerhafte Verzweigungen und Schleifen erzeugen können. Wenn Sie für einfache Verzweigungen und Schleifen auf die Symbole zur Kennzeichnung des Schleifenblocks verzichten, kommt es mit Sicherheit vor, dass Ihr Programm in der Schleife bzw. Verzweigung einmal doch mehr als eine Anweisung ausführen muss (wenn Sie Ihr Programm z. B. nachträglich ändern): If i < 10 then writeln('i ist gleich 10'); writeln('Die Wurzel aus ', i , 'ist ', Sqrt(i));
Für den Compiler gehört die zweite Anweisung aber nicht mehr zu der Verzweigung. Diese Anweisung wird auf jeden Fall ausgeführt, auch wenn die Verzweigungsbedingung false ergibt. Das Programm arbeitet fehlerhaft und führt u. U. zu einigen Problemen beim Kunden. Verwenden Sie möglichst immer (oder wenigstens so lange Sie noch lernen) die Block-Symbole, wenn Sie Verzweigungen und Schleifen programmieren, auch wenn ein Block nur eine einzige Anweisung enthält. Sie stellen damit sicher, dass Ihr Programm keine durch fehlerhafte Verzweigungen bedingten logischen Fehler enthält.
5.4
Schleifen
In Programmen ist es häufig notwendig, Programmteile wiederholt ausführen zu lassen. Dazu verwenden Sie eine der Schleifen Ihrer Programmiersprache. Eine normale Schleife (keine Zählschleife) prüft eine Bedingung, die in einem Vergleichsausdruck formuliert ist, und läuft so lange, wie die Bedingung wahr ist, oder so lange, bis die Bedingung wahr ist. Zu allem Überfluss kann die Bedingungsprüfung entweder im Kopf der Schleife oder im Fuß erfolgen. Kopfgesteuerte Schleifen Kopfgesteuerte Schleifen prüfen die Schleifenbedingung im Kopf, also ganz oben. In Object Pascal sieht eine kopfgesteuerte Schleife, die die Wurzel der ganzen Zahlen von 1 bis 9 berechnet, so aus:
276
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
i := 1; writeln('Wurzelberechnung'); while i < 10 do begin wurzel := Sqrt(i); writeln(i, ': ', FormatFloat('0.00000', wurzel)); inc(i); // i hochzählen end;
Java kennt natürlich auch eine solche kopfgesteuerte Schleife: int i = 1; System.out.println("Wurzelberechnung"); while (i < 10) { double wurzel = Math.sqrt(i); System.out.println(i + ": " + wurzel); i++; // i hochzählen }
Das Ergebnis einer mit Java entsprechend programmierten Konsolenanwendung zeigt Abbildung 5.1.
Abbildung 5.1: Wurzelberechnung in einer Schleife
In der Schleife können Sie (natürlich) beliebig viele Anweisungen programmieren. Dabei können Sie auch untergeordnete Schleifen oder Verzweigungen in die Schleife integrieren. In einer kopfgesteuerten Schleife wird der Vergleichsausdruck überprüft, bevor das Programm in die Schleife verzweigt. Ergibt der Vergleichsausdruck bereits beim Eintritt in die Schleife false, wird das Programm direkt an der ersten Anweisung nach der Schleife ausgeführt. Ergibt der Vergleichsausdruck beim Eintritt allerdings true, verzweigt das Programm in den Schleifenkörper und führt die Anweisungen aus. Am Schleifenfuß springt das Programm dann wieder an den Anfang der Schleife und überprüft die Bedingung erneut. So geht es nun weiter, bis
6FKOHLIHQ
277
Sandini Bib
die Bedingung false ergibt. Das ist einfach, oder? OK, dann können Sie die folgende Testfrage beantworten: Wie lange läuft die folgende JavaSchleife: int i = 1; while (i < 10) { double wurzel = Math.sqrt(i); System.out.println(i + ": " + wurzel); }
Richtig. So lange, bis der Computer ausgeschaltet wird, bis Sie das Programm über Ihr Betriebssystem „abschießen“, bis der Computer sein Leben aushaucht oder einfach bis an das Ende aller Tage. Denn bei dieser Schleife handelt es sich um eine Endlosschleife. Solche Schleifen können Sie ganz einfach programmieren. Und nun wissen Sie auch, warum manche Programme bei der Ausführung bestimmter Aktionen einfach nicht mehr reagieren. Achten Sie immer darauf, dass Sie die Bedingung so formulieren, dass diese auch wahr werden kann und dass Sie innerhalb der Schleife Variablen, die Sie in der Bedingung abfragen, auch wirklich verändern, damit Sie keine Endlosschleifen produzieren. Und es wird Ihnen trotzdem passieren. Wenn ich ein Programm entwickle, sind immer wieder Endlosschleifen mit im Spiel ... Wenn Sie ein Programm testen, können Sie eine Endlosschleife in Delphi über den Befehl PAUSE im RUN-Menü und in Kylix über den Befehl PROGRAMM PAUSE im START-Menü abbrechen. Nun können Sie schrittweise mit (F8) durch das Programm gehen, um das Problem zu suchen. Betätigen Sie dann (Strg) (F2), um das Programm zu beenden. In Sun ONE Studio 4 verwenden Sie zur Unterbrechung eines Programms den Befehl PAUSE im DEBUG-Menü, was aber nur funktioniert, wenn Sie das Programm im Debugmodus gestartet haben ((Strg) (ª) (F5) für das Projekt, (Alt) (F5) für eine einzelne Datei). Über den Befehl FINISH im DEBUG-Menü können Sie das Programm auch direkt beenden. Programme, die direkt an der Konsole ausgeführt werden, können Sie übrigens mit (Strg) (c) beenden. Als kleine Übung soll nun ein Java-Programm entwickelt werden, das alle Primzahlen von 2 bis zu einer festgelegten Zahl berechnet. Eine Primzahl ist eine Zahl, die genau zwei Teiler besitzt, 1 und sich selbst. Anders ausgedrückt ist eine Primzahl eine natürliche (ganze) Zahl grö-
278
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
ßer 1, die nur durch 1 und sich selbst ohne Rest teilbar ist. Beispiele für Primzahlen sind 2, 3, 5, 7 und 11. Zur Ermittlung, ob eine Zahl eine Primzahl ist, gibt es keine einfache Formel. Primzahlen sind ein mathematisches Phänomen. Zur Berechnung von Primzahlen existieren einige komplizierte Algorithmen. Sie können jedoch auch einen einfachen (und dafür recht langsamen) Algorithmus entwickeln. Um alle Primzahlen von 2 bis zu einer gegebenen Zahl zu berechnen, können Sie mit Schleifen arbeiten. Eine äußere Schleife geht alle Zahlen von 2 bis zu der festgelegten Zahl durch. In einer inneren Schleife ermitteln Sie, ob diese Zahl durch eine andere Zahl, die im Bereich von 2 bis zur Endzahl -1 liegt, ohne Rest teilbar ist. Um auf die in Java komplizierte Eingabe zu verzichten, lege ich die Endzahl einfach fest: 01 class primzahlen 02 { 03 public static void main(String args[]) 04 { 05 int endzahl = 100; 06 System.out.println("Alle Primzahlen von 1 bis " + endzahl);
Dann beginnt die äußere Schleife, die die aktuelle Zahl von 1 bis zur Endzahl durchzählt: 07 08 09
int aktuelleZahl = 2; while (aktuelleZahl <= endzahl) {
In der inneren Schleife soll überprüft werden, ob die aktuelle Zahl eine Primzahl ist. Hinter dieser Schleife muss entschieden werden können, ob es sich um eine Primzahl handelt. Als wichtige Programmiertechnik setze ich dazu eine boolesche Variable ein, die zunächst auf true gesetzt wird: 10
boolean primzahl = true;
Damit treffe ich die Annahme, dass es sich um eine Primzahl handelt. Die innere Schleife tritt den Gegenbeweis an. Dazu zähle ich zunächst einen Teiler von 2 bis zu der aktuellen Zahl -1 hoch: 11 12 13
int teiler = 2; while (teiler < aktuelleZahl) {
Um zu überprüfen, ob die aktuelle Zahl eine Primzahl ist, setze ich den Modulo-Operator ein (der ja eine Restwert-Division bewirkt. Wenn bei
6FKOHLIHQ
279
Sandini Bib
der Division kein Rest übrig bleibt, handelt es sich nicht um eine Primzahl. Dann wird die Annahme revidiert: 14 15 16 17
if (aktuelleZahl % teiler == 0) { primzahl = false; }
Dann muss noch der Teiler hochgezählt werden, womit die innere Schleife auch schon beendet ist: 18 19
teiler++; }
Hinter der inneren Schleife kann ich an der Variablen primzahl erkennen, ob es sich um eine Primzahl handelt (die dann einfach ausgegeben wird); 20 21 22 23
if (primzahl == true) { System.out.print(aktuelleZahl + " "); }
Schließlich wird noch die aktuelle Zahl hochgezählt und die Schleife beendet: 24 25
aktuelleZahl++; }
Bevor das Programm beendet wird, gebe ich dann noch einen Zeilenumbruch aus: 26 System.out.println(); 27 } 28 }
Die Überprüfung der Bedingung im Schleifenkopf der äußeren Schleife bewirkt, dass die Schleife erst gar nicht durchlaufen wird, wenn als letzte Zahl eine Zahl kleiner als 2 gewählt wird. Dasselbe gilt für die innere Schleife, die bei Zahlen kleiner als 3 auch nicht durchlaufen wird. Zur besseren Übersicht folgt hier noch der komplette Quellcode mit Kommentaren:
280
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
01 class primzahlen 02 { 03 public static void main(String args[]) 04 { 05 /* Endzahl festlegen */ 06 int endzahl = 100; 07 08 System.out.println("Alle Primzahlen von 1 bis " + endzahl); 09 10 /* Äußere Schleife, die alle Zahlen von 2 bis zu der 11 Endzahl durchgeht */ 12 int aktuelleZahl = 2; 13 while (aktuelleZahl <= endzahl) 14 { 15 /* Annehmen, dass es sich um eine Primzahl handelt */ 16 boolean primzahl = true; 17 18 /* Innere Schleife, die alle Zahlen von 2 bis zur 19 aktuellen Zahl -1 durchgeht */ 20 int teiler = 2; 21 while (teiler < aktuelleZahl) 22 { 23 /* Ermitteln, ob die aktuelle Zahl durch den Teiler 24 ohne Rest teilbar ist */ 25 if (aktuelleZahl % teiler == 0) 26 { 27 /* Keine Primzahl */ 28 primzahl = false; 29 } 30 31 /* Teiler hochzählen */ 32 teiler++; 33 } 34 35 /* Überprüfen, ob der Primzahltest true ergab */ 36 if (primzahl == true) 37 { 38 System.out.print(aktuelleZahl + " "); 39 } 40 41 /* Aktuelle Zahl hochzählen */ 42 aktuelleZahl++; 43 } 44 45 /* Zeilenumbruch erzeugen */ 46 System.out.println(); 47 } 48 }
6FKOHLIHQ
281
Sandini Bib
Fußgesteuerte Schleifen Fußgesteuerte Schleifen überprüfen ihre Bedingung erst am Schleifenfuß. Der einzige Unterschied zu einer kopfgesteuerten Schleife ist also, dass die fußgesteuerte Schleife mindestens einmal durchlaufen wird, auch wenn die Schleifenbedingung am Anfang der Schleife nicht erfüllt ist. Eine solche Schleife setzen Sie dann ein, wenn der Programmcode der Schleife auf jeden Fall einmal durchlaufen werden soll. Object Pascal kennt eine fußgesteuerte Schleife, die mit repeat eingeleitet und mit until beendet wird. i := 0; repeat i := i + 1; writeln(i); until i = 10;
Der Anweisungsblock der repeat-Schleife wird ausnahmsweise nicht mit begin und end gekennzeichnet, sondern mit der until-Anweisung abgeschlossen. Deshalb müssen Sie die Bedingungsanweisung auch mit einem Semikolon abschließen, damit der Compiler das Ende der Anweisung erkennt. Diese Schleife ist aber eine der wenigen Ausnahmen, bei der Object Pascal von dem sonst üblichen Block-Konzept abweicht. Die repeat-Schleife läuft so lange, bis die Bedingung im Fuß erfüllt ist. Die Beispielschleife läuft also genau zehn Mal. In Java wird eine fußgesteuerte Schleife mit do eingeleitet. Die Bedingung wird im Fuß angegeben: i = 0; do { i++; System.out.println(i); } while (i <= 10);
Beachten Sie, dass Sie die Bedingungsanweisung wie bei der Delphi/ Kylix-Schleife mit einem Semikolon abschließen müssen. Die fußgesteuerte Java-Schleife unterscheidet sich etwas von der, die in Object Pascal verwendet wird. In Object Pascal läuft die Schleife so lange, bis die Bedingung erfüllt ist, hier läuft die Schleife so lange, wie die Bedingung erfüllt ist. Im Prinzip macht das aber keinen Unterschied, Sie müssen die Bedingung lediglich umformulieren. Ob Sie nun in einem Delphi/Kylix-Programm schreiben:
282
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
repeat { ... } until i > 10;
oder in Java: do { /* ... */ } while (i <= 10);
macht für den Compiler keinen Unterschied. Sie müssen lediglich die Bedingung entsprechend umformulieren. Das folgende Beispielprogramm demonstriert die fußgesteuerte Schleife. Ein einfaches Multiplikationsprogramm soll dem Anwender mindestens einmal ermöglichen, zwei Zahlen einzugeben, und diese Zahlen dann multipliziert ausgeben. Der Anwender wird schließlich gefragt, ob er die Berechnung wiederholen will. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Ein Beispiel
program Schleifen; {$APPTYPE CONSOLE} uses SysUtils; var var var var
i: integer; wurzel: double; zahl1, zahl2: double; eingabe: string;
begin { Beginn der fußgesteuerten Schleife } repeat write('Zahl 1: '); readln(zahl1); write('Zahl 2: '); readln(zahl2); writeln('Zahl 1 * Zahl 2 ist: ', zahl1 * zahl2); write('Wiederholen? '); readln(eingabe); { Abfrage der Bedingung der Schleife } until eingabe <> 'j'; end.
6FKOHLIHQ
283
Sandini Bib
Die For-Schleife Die For-Schleife ist eine Schleifen-Sonderform, die eine numerische Variable von einem anzugebenden Startwert bis zu einem ebenfalls anzugebenden Endwert in Schritten hochzählt, deren Schrittweite Sie bei einigen Sprachen auch angeben können. In Object Pascal sieht eine ForSchleife folgendermaßen aus: var i: integer; begin for i := 1 to 10 do begin writeln(i); end; ...
In der Schleife wird die Variable, die immer ein Integer-Typ sein muss, ausgehend vom Startwert mit jedem Schleifendurchlauf automatisch bis zum Endwert + 1 hochgezählt. Die Schrittweite ist auf 1 voreingestellt und kann in Object Pascal nicht eingestellt werden, was aber in der Praxis auch nur selten notwendig ist. Beim letzten Schleifendurchlauf besitzt die Variable den Endwert. Nach der Schleife ist dieser Wert um den Wert 1 hochgezählt. Eine For-Schleife vereinfacht die Programmierung von Schleifen mit einer festgelegten Anzahl Durchläufen. Da der Compiler eine solche Schleife besser optimieren kann als eine normale Schleife (weil die Bedingung einfacher ist), wird die For-Schleife in vielen Fällen auch einen Hauch schneller ausgeführt als eine äquivalente normale Schleife. Sie können mit einer ähnlichen Schleife eine Variable auch herunterzählen: for i := 10 downto 1 do begin writeln(i); end;
In Java ist die for-Schleife etwas komplexer, dafür aber auch flexibler. Die Syntaxbeschreibung sieht so aus: for (Initialisierung; Bedingung; Zähloperation) { }
284
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Im ersten Teil initialisieren Sie die Zählvariable. Im zweiten Teil formulieren Sie über einen Vergleichsausdruck eine Bedingung für die Schleife (ähnlich wie bei einer normalen Schleife). Im dritten Teil zählen Sie die Variable hoch oder herunter. Eine einfache for-Schleife, die von 1 bis 9 zählt, sieht dann so aus: int i; for (i = 1; i < 10; i++) { System.out.println(i); }
Anders als in Object Pascal können Sie die Schrittweite angeben und die Bedingung genauer formulieren. Um eine Schleife zu erzeugen, die eine Zahl in Zweier-Schritten hochzählt, können Sie den folgenden Quellcode verwenden: int i; for (i = 1; i < 10; i += 2) { System.out.println(i); }
Eine Schleife, die eine Zahl herunterzählt, sieht so aus: int i; for (i = 10; i > 0; i--) { System.out.println(i); }
In Java können Sie die Zählvariable auch direkt in der Schleife deklarieren: for (int i = 0; i < 10; i++) { System.out.println(i); }
Die Variable gilt dann nur in dem Block der Schleife. Beachten Sie, dass der Java-Compiler verbietet, dass eine blocklokale Variable mit demselben Namen wie eine Variable aus einem Block höherer Ebene deklariert wird. Ist bereits eine Variable i in dem Block deklariert, in dem die Schleife programmiert ist, müssen Sie die Zählvariable anders benennen.
6FKOHLIHQ
285
Sandini Bib
Schleifen abbrechen In der Praxis ist es häufig notwendig, eine Liste von Daten (ein Array oder Ähnliches, wie ich es in Kapitel 7 bespreche) in einer Schleife durchzugehen, um ein bestimmtes Datum zu suchen. Dazu verwenden Sie normalerweise eine Schleife, die eine Variable vom Start der Liste bis zum Ende hochzählt. Wenn Sie das gesuchte Datum gefunden haben, ist es eigentlich nicht mehr notwendig, dass die Schleife weiter ausgeführt wird. Dann sollten Sie die Schleife abbrechen, um Ihr Programm von vornherein möglichst performant zu programmieren. Warum sollte eine Schleife, die eine Liste mit 10.000 Einträgen durchsucht, noch durchschnittlich 5.000 Einträge weitersuchen, wenn das gesuchte Datum bereits gefunden wurde? Aber auch in anderen Programmen ist der Abbruch einer Schleife manchmal sehr sinnvoll. Bei der Primzahlenberechnung muss die Schleife ja auch nicht mehr weiter ausgeführt werden, wenn festgestellt wurde, dass die aktuelle Zahl keine Primzahl ist. Besonders hier führt ein Abbruch der Schleife bei großen Zahlen zu einem erheblichen Performance-Vorteil. Delphi, Kylix und Java stellen Ihnen zum Abbruch einer Schleife die break-Anweisung zur Verfügung. break bricht immer nur die aktuelle Schleife ab, nicht eine eventuelle äußere. Mit diesem Wissen können Sie das Primzahlen-Programm mit for-Schleifen und einem SchleifenAbbruch optimieren: 01 class primzahlen 02 { 03 public static void main(String args[]) 04 { 05 int endzahl = 100; 06 07 System.out.println("Alle Primzahlen von 1 bis " + endzahl); 08 09 for (int aktuelleZahl = 2; aktuelleZahl <= endzahl; 10 aktuelleZahl++) 11 { 12 boolean primzahl = true; 13 14 for (int teiler = 2; teiler < aktuelleZahl; teiler++) 15 { 16 if (aktuelleZahl % teiler == 0) 17 { 18 primzahl = false; 19 20 /* Innere Schleife abbrechen */ 21 break;
286
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
22 } 23 } 24 25 if (primzahl == true) 26 { 27 System.out.print(aktuelleZahl + " "); 28 } 29 } 30 31 System.out.println(); 32 } 33 }
Einige Programmier-Puristen meinen, der Abbruch einer Schleife sei eine unstrukturierte Technik und sollte deshalb vermieden werden. Nach diesen Leuten sollte eine Schleife immer einen einzigen definierten Ausgang besitzen (und nicht zwei, wie bei einer Schleife, die abgebrochen wird). Ich bin auch Anhänger der strukturierten (und vor allen Dingen der objektorientierten) Programmierung. Der Abbruch einer Schleife ist in meinen Augen aber ein sehr gutes Mittel, eine Anwendung zu optimieren. Und außerdem kennt jeder Programmierer diese Technik ... Um zu beweisen, dass ein Schleifenabbruch zu einem Performancevorteil führt, habe ich das Primzahlen-Programm um eine Zeitmessung erweitert:
Zeitmessung zum PerformanceCheck
long startTime = System.currentTimeMillis(); /* Primzahlenberechnung */ ... long endTime = System.currentTimeMillis(); System.out.println("Benötigte Zeit: " + (endTime - startTime) + "ms");
Das Programm berechnet alle Primzahlen von 2 bis 20.000, gibt diese aber nicht aus, um möglichst wenig äußere Faktoren in die Messung einfließen zu lassen. Dann habe ich eine Kopie von diesem Programm erzeugt, die den Schleifenabbruch nicht enthält. Nach dem Kompilieren habe ich beide Programme in der Konsole mehrfach ausgeführt. Das Ergebnis war wie erwartet: Die Variante ohne break benötigte ca. 11,5 Sekunden, die Variante mit dem Schleifenabbruch nur ca. 1,2 Sekunden. Das ist eine enorme Performance-Steigerung. Und das nur durch ein simples break an der richtigen Stelle.
6FKOHLIHQ
287
Sandini Bib
Die ermittelten Werte sind natürlich stark vom System abhängig. Wichtig ist nur die Relation. Achten Sie bei solchen Performancemessungen darauf, dass keine äußeren Faktoren (wie parallel laufende Programme) die Messung beeinflussen, dass Sie mehrmals testen und den Durchschnitt der gemessenen Zeiten ermitteln. Einige Programmierer gehen sogar so weit, dass sie vor jeder Messung den Computer neu starten. Aber das ist wohl nur selten wirklich notwendig. Beachten Sie außerdem, dass die Zeitmessung auf diese Weise systembedingt lediglich eine Genauigkeit von 55 Millisekunden besitzt. Achten Sie also darauf, dass Sie genügend Schleifendurchläufe vorsehen, sodass die benötigte Zeit deutlich über diesem Wert liegt. Zeitmessung in Delphi und Kylix
In Delphi und Kylix können Sie zur Zeitmessung die folgenden Anweisungen verwenden: var startTime, endTime: double; begin startTime := Now(); { zu prüfende Anweisungen } endTime := Now(); writeln('Benötigte Zeit: ', FormatFloat( '0', (endTime - startTime) * 24 * 60 * 60 * 1000), ' ms');
5.5
Verzweigungen
Verzweigungen sind neben den Schleifen das zweite Strukturelement einer Programmiersprache. Mit einer Verzweigung können Sie Anweisungen bedingungsabhängig ausführen lassen. Die Beispiele der vorhergehenden Seiten verwenden häufig schon einfache If-Verzweigungen, weil die meisten Programme ohne Verzweigungen gar nicht auskommen. Für einfache Verzweigungen stellen Ihnen alle modernen Programmiersprachen die If-Verzweigung, für komplexere Mehrfachverzweigungen die Case-Verzweigung zur Verfügung. Die If-Verzweigung Die If-Verzweigung ist eigentlich sehr einfach zu verstehen. Im Prinzip programmieren Sie damit den Quellcode, der für Aussagen wie „Wenn eine bestimmte Bedingung erfüllt ist, mache dies, ansonsten mache das“ notwendig ist.
288
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Die If-Verzweigung sieht in Object Pascal in der einfachsten Form so aus: if Vergleichsausdruck then begin { Anweisungen, die bedingungsabhängig ausgeführt werden sollen, wenn der Vergleichsausdruck true ergibt } end;
Die Anweisungen im Block der If-Anweisung werden nur dann ausgeführt, wenn der Vergleichsausdruck true ergibt. Sie können in einer IfVerzweigung auch darauf reagieren, dass der Vergleichsausdruck nicht wahr wird. Dazu verwenden Sie den else-Block: if Vergleichsausdruck then begin { Anweisungen, die bedingungsabhängig ausgeführt werden sollen, wenn der Vergleichsausdruck true ergibt } end else { Anweisungen, die bedingungsabhängig ausgeführt werden sollen, wenn der Vergleichsausdruck false ergibt } end;
Der else-Block ist optional. Wenn Ihr Programm für den Fall, dass der Vergleichsausdruck false ergibt, nichts weiter machen soll, lassen Sie diesen Block einfach weg. Die Anweisungen im else-Block werden immer dann (und nur dann) ausgeführt, wenn der Vergleichsausdruck false ergibt und der If-Block deshalb nicht ausgeführt wird. Die folgende Verzweigung überprüft, ob in der Variablen zahl ein Wert größer als Null gespeichert ist, um in diesem Fall die Wurzel und das Quadrat zu berechnen. Im anderen Fall wird eine Meldung ausgegeben: if zahl > 0 then begin writeln('Die Wurzel aus ', zahl, ' ist ', Sqrt(zahl)); writeln('Das Quadrat von ', zahl, ' ist ', Sqr(zahl)); end else begin writeln('Die Zahl muss größer Null sein'); end;
Beachten Sie, dass der Compiler die gesamte Verzweigung quasi als eine große Anweisung betrachtet, die deshalb am Ende mit einem Semikolon abgeschlossen werden muss. Würden Sie den else-Block weglassen, müssten Sie das Semikolon hinter das end des if-Blocks schreiben.
9HU]ZHLJXQJHQ
289
Sandini Bib
Wenn einer der Blöcke der If-Verzweigung nur eine einzige Anweisung enthält, können Sie das begin und das end dieses Blocks auch weglassen. Um gleich ein typisches Problem zu demonstrieren, reduziere ich den oberen Block ebenfalls auf eine Anweisung: if zahl > 0 then writeln('Die Wurzel aus ', zahl, ' ist ', Sqrt(zahl)) else writeln('Die Zahl muss größer Null sein');
Da der Compiler die Anweisung als eine einzige Anweisung betrachtet, dürfen Sie diese erst ganz am Ende abschließen. Deswegen fehlt hinter der Anweisung im If-Block das Semikolon. Das ist allerdings eine Object Pascal-Eigenart. In anderen Sprachen wie Java und C# würden Sie auch die Anweisung in If-Block abschließen. Beachten Sie dabei aber immer, dass Sie sehr schnell fehlerhafte Programme erzeugen können, wenn Sie auf die Symbole zur Kennzeichnung der Blöcke verzichten. In Java hat die If-Verzweigung die folgende Form: if (Vergleichsausdruck) { /* Anweisungen, die bedingungsabhängig ausgeführt werden sollen, wenn der Vergleichsausdruck true ergibt */ } else { /* Anweisungen, die bedingungsabhängig ausgeführt werden sollen, wenn der Vergleichsausdruck false ergibt */ }
Wie bei Delphi und Kylix ist auch bei Java der else-Block optional. Ein Beispiel mit geschachtelten Verzweigungen
Das folgende Java-Beispiel verwendet eine If-Verzweigung, um zu ermitteln, wie viel Bonus ein Handelsvertreter erhalten soll. Der Bonus soll nach den folgenden Regeln ermittelt werden: • Der Vertreter erhält zehn Prozent Bonus, wenn er einen Umsatz von mindestens 50000 Euro erreicht hat; • er erhält fünf Prozent Bonus, wenn er weniger als 50000 Euro, aber mindestens 30000 Euro umgesetzt hat; • er erhält ein Prozent Bonus, wenn er weniger als 30000 Euro, aber mindestens 15000 Euro umgesetzt hat. Zur Lösung dieses Problems benötigen Sie mehrere geschachtelte Verzweigungen. Sie müssen zuerst überprüfen, ob der Umsatz größer ist als
290
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
50000 Euro. Ist dies nicht der Fall, überprüfen Sie, ob der Umsatz größer als 30000 Euro ist. Trifft dies auch nicht zu, müssen Sie schließlich noch überprüfen, ob der Umsatz größer als 15000 Euro ist: 01 class umsatzberechnung 02 { 03 public static void main(String args[]) 04 { 05 System.out.println("Bonusberechnung"); 06 double umsatz = 31000; 07 double bonus; 08 09 if (umsatz >= 50000) 10 { 11 bonus = umsatz * 0.1; 12 } 13 else 14 { 15 if (umsatz >= 30000) 16 { 17 bonus = umsatz * 0.05; 18 } 19 else 20 { 21 if (umsatz >= 15000) 22 { 23 bonus = umsatz * 0.01; 24 } 25 else 26 { 27 bonus = 0; 28 } 29 } 30 } 31 32 System.out.println("Umsatz: " + umsatz); 33 System.out.println("Bonus: " + bonus); 34 } 35 }
Statt die einzelnen Verzweigungen so zu schachteln, wie es in dem obigen Beispiel der Fall ist, können Sie noch eine etwas kürzere Form verwenden, die jeweils den Else-Block einer Verzweigung mit einem neuen If-Block beginnt:
9HU]ZHLJXQJHQ
291
Sandini Bib
09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
if (umsatz >= 50000) { bonus = umsatz * 0.1; } else if (umsatz >= 30000) { bonus = umsatz * 0.05; } else if (umsatz >= 15000) { bonus = umsatz * 0.01; } else { bonus = 0; }
Beachten Sie, dass bei dieser Kurzform die Else-Blöcke, bis auf den letzten, selbst nicht in die Blocksymbole eingeschlossen sind. Würden Sie dann noch die Blocksymbole weglassen (weil ja nur eine Anweisung enthalten ist), wäre der Programmcode noch wesentlich kürzer (und eigentlich auch übersichtlicher). Geschachtelte Verzweigungen in Delphi/Kylix
In Delphi und Kylix müssen Sie bei geschachtelten Anweisungen die Eigenart des Compilers beachten, dass dieser eine komplette If-Verzweigung mit einem Semikolon abgeschlossen sehen möchte. Das macht die Programmierung solcher Verzweigungen in vielen Fällen ein wenig kompliziert. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18
292
program Bonusberechnung; {$APPTYPE CONSOLE} uses SysUtils; var zahl: double; var umsatz, bonus: double; begin write("Umsatz: "); readln(umsatz); if umsatz >= 50000 then begin bonus := umsatz * 0.1; end
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
19 else 20 begin 21 if umsatz >= 30000 then 22 begin 23 bonus := umsatz * 0.05; 24 end 25 else 26 begin 27 if umsatz >= 15000 then 28 begin 29 bonus := umsatz * 0.01; 30 end 31 else 32 begin 33 bonus := 0; 34 end; 35 end 36 end; 37 38 { Formatierte Ausgabe des Bonus } 39 writeln('Umsatz ', FormatFloat('0.00', umsatz), ' Euro'); 40 writeln('Bonus ', FormatFloat('0.00', bonus), ' Euro'); 41 end.
In diesem Beispiel müssen die innere Verzweigung, die den Umsatz auf einen Wert größer/gleich 15000 überprüft, und die äußere Verzweigung korrekt abgeschlossen werden. Wenn Sie die kürzere Form verwenden, vermeiden Sie solche Probleme, denn bei dieser Variante wird lediglich das abschließende end mit einem Semikolon versehen: 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
if umsatz >= 50000 then begin bonus := umsatz * 0.1; end else if umsatz >= 30000 then begin bonus := umsatz * 0.05; end else if umsatz >= 15000 then begin bonus := umsatz * 0.01; end else begin bonus := 0; end;
9HU]ZHLJXQJHQ
293
Sandini Bib
Die Case-Verzweigung Eine If-Verzweigung kann immer nur zwei Fälle überprüfen. Entweder ist eine Bedingung wahr oder nicht. Für komplexere Überprüfungen mit mehreren Fallmöglichkeiten müssen Sie die If-Verzweigung schachteln. Wenn Sie einen einfachen Ausdruck (normalerweise eine Variable) daraufhin überprüfen wollen, ob dieser bestimmte Werte ergibt, können Sie stattdessen aber auch die Case1-Verzweigung verwenden. Diese Verzweigung vergleicht den Wert des Ausdrucks mit beliebig vielen Vergleichswerten (den einzelnen Fällen) und springt bei Gleichheit in den Block des jeweiligen Falls. In Object Pascal sieht eine einfache Case-Verzweigung, die eine Variable darauf überprüft, ob der Wert 1, 3 oder 5 gespeichert ist, folgendermaßen aus: var i: integer; begin i := 1; case i of 1: writeln('i ist gleich 1'); 3: writeln('i ist gleich 3'); 5: writeln('i ist gleich 5'); end;
Wenn die Variable die Zahl 1 beinhaltet, werden die Anweisungen des ersten Blocks ausgeführt. Ist dies nicht der Fall, wird der nächste Wert überprüft. Passt der Wert dort, werden dessen Anweisungen ausgeführt. Die Case-Verzweigung kennt natürlich auch Anweisungsblöcke für den Fall, dass Sie mehrere Anweisungen unterbringen müssen: case i of 1: begin writeln('i ist gleich { weitere Anweisungen end; 3: begin writeln('i ist gleich { weitere Anweisungen end;
1.
294
1'); }
3'); }
Englisch für „Fall“
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
5: begin writeln('i ist gleich 5'); { weitere Anweisungen } end; end;
Sie können wie bei der If-Verzweigung auch einen Else-Block einrichten. Die Anweisungen in diesem Block werden ausgeführt, wenn keiner der vorhergehenden Fälle eingetreten ist. case i of 1: writeln('i ist gleich 1'); 3: writeln('i ist gleich 3'); 5: writeln('i ist gleich 5'); else writeln('i ist nicht 1, 3 oder 5'); end;
Als Vergleichswerte kommen in Object Pascal nur Integer- und CharTypen (die kompatibel sind zu Integer-Typen) in Frage. Sie können für einen Fall aber auch eine Liste von Werten angeben. Darin trennen Sie einzelne Werte durch Kommata oder geben einen Bereich an, den Sie mit zwei Punkten kennzeichnen: var zeichen: char; begin zeichen = 'ä'; case zeichen of 'a'..'z': writeln('Kleiner normaler Buchstabe'); 'A'..'Z': writeln('Großer normaler Buchstabe'); 'ä', 'ö', 'ü': writeln('Kleiner Umlaut'); 'Ä', 'Ö', 'Ü': writeln('Großer Umlaut'); 'ß': writeln('Scharfes S'); else writeln('Anderes Zeichen'); end;
Das Beispiel überprüft im ersten Fall, ob die Variable zeichen eines der Zeichen im Bereich von a bis z speichert. Zeichen werden, wie Sie ja bereits wissen, immer mit ihrem Zeichencode gespeichert. Das kleine a besitzt den Code 97 (vergleichen Sie dazu die ASCII-Tabelle im Anhang). Die kleinen Buchstaben werden in der ASCII-Tabelle zusammenhängend gespeichert, das z besitzt den Code 122. Deshalb können Sie den Bereich zwischen a und z für alle Kleinbuchstaben angeben. Dasselbe gilt auch für die großgeschriebenen normalen Buchstaben. Die Umlaute werden allerdings nicht mehr zusammenhängend gespeichert. Hier
9HU]ZHLJXQJHQ
295
Sandini Bib
müssen Sie mit den einzelnen Werten vergleichen, die Sie dann in einer kommabegrenzten Liste angeben können. In Java wird die Case-Verzweigung mit switch eingeleitet. Die einzelnen Fälle werden wie bei Object Pascal mit case überprüft. Der (optionale) Else-Block wird hier allerdings mit default eingeleitet: switch (i) { case 1: System.out.println("i break; case 3: System.out.println("i break; case 5: System.out.println("i break; default: System.out.println("i }
ist 1");
ist 3");
ist 5");
nicht 1, 3 oder 5");
Wie Sie am Beispiel erkennen, müssen Sie – anders als bei Object Pascal – für die einzelnen Blöcke keine Blocksymbole angeben. Außerdem müssen Sie normalerweise am Ende jedes Blocks außer beim letzten die Anweisung break unterbringen. Der Java-Compiler wertet (anders als der Compiler von Delphi und Kylix) in einer switch-Verzweigung immer alle Fälle aus, auch wenn ein vorhergehender Fall bereits wahr wurde und dieser nicht explizit abgebrochen wurde. Wenn Sie auf das break in den einzelnen Blöcken verzichten, geht das sogar so weit, dass auch der default-Block ausgeführt wird, wenn bereits ein vorhergehender Block ausgeführt wurde. Damit ermöglicht Java (C++-like) die Programmierung komplizierter Konstrukte, die kaum noch jemand versteht . Um die Case-Verzweigung in Java so einzusetzen, wie es auch in anderen Sprachen der Fall ist, und um die komplizierten Fehler, die sich durch mehrere ausgeführte case-Blöcke ergeben, zu vermeiden, sollten Sie jeden Block (außer den default-Block) mit break explizit abbrechen, wie ich es im Beispiel oben bereits gemacht habe.
-
Würden Sie die break-Anweisungen nicht in die einzelnen Blöcke einfügen, würde Ihr Programm die nächste Bedingung auch dann überprüfen, wenn eine Bedingung bereits wahr geworden ist. Das könnte dazu
296
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
führen, dass das Programm noch einmal in einen anderen Block verzweigt, obwohl ein vorhergehender bereits ausgeführt wurde. Ähnlich wie in Delphi und Kylix können Sie in der switch-Verzweigung nur die Datentypen char, byte, short und int einsetzen. Eine Liste von Vergleichswerten ist aber leider nicht möglich.
5.6
Funktionen und Prozeduren
Nun, da Sie die Strukturelemente einer Programmiersprache kennen und bereits einfache Programme schreiben können, wird es Zeit, dass Sie sich mit der Wiederverwendung von Programmcode auseinander setzen. Sie entwickeln ja nicht für jedes Programm neue Lösungen, wenn Sie diese in vorherigen Programmen bereits fertig gestellt hatten. Aber es kommt auch sehr häufig vor, dass eine bestimmte Funktionalität innerhalb ein und desselben Programms mehrfach benötigt wird. Die Eingabe von Daten an der Konsole ist in Java z. B. sehr kompliziert zu programmieren (wie sie dem Artikel „Konsole-IO“ entnehmen können). Wäre es nicht schön, eine Funktion zu besitzen, die folgendermaßen aufgerufen werden kann:
Wiederverwendung von Programmcode
double zahl = ZahlEingabe("Geben Sie eine Zahl ein: ", -1);
Die Funktion kümmert sich um alles Notwendige, was heißt, dass sie den übergebenen Text ausgibt, die Eingabe entgegennimmt und diese auf Gültigkeit überprüft. Ist die Eingabe nicht in Ordnung, schreibt die Funktion eine entsprechende Meldung an die Konsole und gibt den am zweiten Argument übergebenen Defaultwert zurück. Ist die Eingabe in Ordnung, gibt die Funktion den eingegebenen Wert als double-Zahl zurück. Die Anwendung der Funktion ist einfach (die Programmierung nicht unbedingt). Wenn Sie die Funktion in Ihrem Programm öfter benötigen, rufen Sie diese einfach auf. Ähnliche Beispiele sind die Berechnung der Fakultät einer Zahl und die Ermittlung, ob eine Zahl eine Primzahl ist. Wenn Sie solche Berechnungen öfter benötigen, sollten Sie dafür Funktionen schreiben, damit Sie diese einfach nur aufrufen müssen. Ich denke, Sie haben nun verstanden, worum es beim Schreiben von Funktionen und Prozeduren geht. Nun müssen Sie nur noch wissen, was Prozeduren und Funktionen voneinander unterscheidet und wie Sie diese in Delphi/Kylix und Java selbst entwickeln. Der Unterschied zwischen Funktionen und Prozeduren ist so gering, dass Java gar keinen Unterschied macht. Eine Prozedur ist eine Funktion, die nichts zurückgibt. Funktionen geben immer einen Wert zu-
Funktionen versus Prozeduren
297
Sandini Bib
rück, den Sie in Ausdrücken oder Zuweisungen weiterverwenden können. Eine Funktion, die die Fakultät einer Zahl berechnet, wäre wohl ziemlich sinnlos, wenn sie das Ergebnis nicht zurückgeben würde. Prozeduren geben hingegen keinen Wert zurück. Prozeduren werden für Programmteile verwendet, die zwar in einem Programm des Öfteren ausgeführt werden sollen, aber eben nichts zurückliefern müssen. In einem Buchhaltungsprogramm kann es z. B. eine Prozedur zum Druck einer Rechnung geben, der die benötigten Daten übergeben werden, die aber eben nichts weiter macht, als die Rechnung zu drucken. Aber auch dafür schreibt ein Programmierer häufig eine Funktion. Eine Rechnungsfunktion könnte z. B. mit einem booleschen Wert zurückgeben, ob der Druck der Rechnung korrekt ausgeführt werden konnte. Ich beschreibe deshalb auch die wichtigsten Möglichkeiten, die Sie beim Entwickeln von Funktionen und Prozeduren haben, anhand von Funktionen, und zeige erst danach kurz, wie Sie Prozeduren schreiben.
5.6.1 Einfache Funktionen Bevor Sie eine Funktion entwickeln, müssen Sie erst einmal auf die Idee kommen, eine zu schreiben. Bei der Entwicklung eines Programms ist es manchmal nicht eindeutig ersichtlich, dass an der einen oder anderen Stelle der Einsatz einer Funktion angebracht wäre. Merken Sie sich deswegen: Immer wenn Sie identischen oder ähnlichen Quellcode mehrfach in einem Programm schreiben, sollten Sie sofort drüber nachdenken, daraus eine Funktion (oder eine Prozedur) zu erzeugen. Ist der Quellcode nicht komplett identisch, sondern unterscheidet sich an einigen wenigen Stellen, können Sie der Funktion einfach Argumente übergeben, die deren Ausführung beim Aufruf steuern. Sie sollten aber auch immer an später denken. Wenn Sie nur die Idee haben, Sie könnten einen Programmteil, den Sie gerade entwickeln, später wiederverwenden, sollten Sie für diesen Teil des Programms eine Funktion entwickeln, auch wenn Sie diese zurzeit nur an einer einzigen Stelle des Programms aufrufen. Sie sollten das Ganze natürlich nicht übertreiben. Nur dann, wenn Funktionen an verschiedenen Stellen eines Programms aufgerufen oder in verschiedenen Programmen wiederverwendet werden, machen diese auch Sinn. Die Arten der Wiederverwendung
298
Eine wichtige Frage bei der Entwicklung von Funktionen ist, ob diese einen eher allgemeinen oder eher projektspezifischen Charakter besitzen. Eine Funktion zur Berechnung der Fakultät einer Zahl ist beispielsweise so allgemein, dass Sie diese auch sehr gut in anderen Programmen einsetzen können. Eine Funktion zum Druck einer (speziell formatierten)
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Rechnung gehört aber eher nur zu einem Projekt. Diese Frage ist deswegen wichtig, weil Sie entscheiden müssen, wo Sie die Funktion speichern. Allgemeine Funktionen sollten so gespeichert werden, dass Sie in anderen Projekten ohne weiteres darauf zugreifen können. In Delphi/ Kylix und Java ist das ganz einfach. Sie entwickeln separate Module (in Delphi/Kylix sind das Units, in Java Klassen), die jeweils mehrere Funktionen speichern, die thematisch zusammengehören (z. B. mathematische oder statistische Funktionen). Der Begriff „Modul“ steht im Allgemeinen für einen Programmteil, der zusammengehörige Funktionen, Prozeduren oder Methoden und/ oder Daten (in Form von Variablen) enthält. So könnte ein Buchhaltungsprogramm beispielsweise ein Modul zum Rechnungsdruck und eines zur Verwaltung von Bestellungen enthalten. Die Programmierung mit Modulen macht ein Programm übersichtlicher, fehlerfreier, leichter wartbar und leichter erweiterbar. Was aber auch sehr wichtig ist, ist die Tatsache, dass Module, sofern diese umsichtig programmiert wurden, meist sehr einfach in anderen Projekten wiederverwendet werden können. Das sind alles wichtige Forderungen der modernen, qualitativ hochwertigen Programmierung. Eine modulare Programmierung ist die wichtigste Voraussetzung dafür. Der Begriff „Modul“ wird aber auch von vielen Programmierern etwas einschränkend verwendet. Für diese Programmierer enthält ein Modul klassische Funktionen, Prozeduren und Variablen, aber niemals Klassen. Eine Klasse wäre demnach die moderne Form eines Moduls. Ich meine mit diesem Begriff aber Module eines Programms, die alles mögliche enthalten können, eben auch Klassen. Allgemein verwendbare Module speichern Sie in einem Ordner, den Sie für allgemeine eigene Bibliotheken (denn nichts anderes programmieren Sie dann) vorgesehen haben, und müssen dann nur noch dafür sorgen, dass der Compiler diese auch findet. Wie das geht, zeige ich ab Seite 316. Projektspezifische Funktionen bleiben dagegen in den Dateien des Projekts gespeichert. Bei diesen Funktionen müssen Sie eine weitere Entscheidung treffen. Programme bestehen sehr häufig nicht nur aus einem einzigen Modul (wie die, die Sie bisher entwickelt haben), sondern sehr häufig aus recht vielen einzelnen Modulen. Die Delphi/KylixNettoberechnung mit Fenstern, die Sie in Kapitel 2 entwickelt haben, gehört prinzipiell schon dazu, denn solche Anwendungen bestehen bereits aus zwei Modulen: der Projekt-Startdatei und der Formulardatei.
299
Sandini Bib
Wird die Funktion nur in einem Modul Ihres Projekts (in einer Datei) benötigt, können Sie diese auch dort speichern. Benötigen Sie die Funktion hingegen in mehreren verschiedenen Modulen Ihres Projekts, muss diese in einer separaten Datei entwickelt werden, die im Projektordner gespeichert und in die anderen Module des Projekts eingebunden wird. Im Prinzip handelt es sich dann um eine allgemeine Funktion, die aber nicht so allgemein ist, dass sie in anderen Projekten wiederverwendet werden kann. Ich will im Folgenden bereits praxisorientierte Programme entwickeln. Deshalb werden Sie allgemeine Funktionen in separaten Modulen schreiben und diese in einem allgemeinen Ordner speichern. Aber zuerst müssen Sie Funktionen und Prozeduren schreiben können. Eine einfache Funktion in Delphi und Kylix Den Umgang mit Funktionen erläutere ich an einem einfachen Beispiel: Eine Konsolenanwendung soll den Mittelwert zweier Zahlen und die Fakultät einer Zahl berechnen. Erzeugen Sie dazu zunächst in Delphi oder Kylix ein neues Projekt für eine Konsolenanwendung, das Sie vielleicht Funktionsdemo nennen. Um den Anfang nicht zu kompliziert zu machen, schreiben Sie die Funktion zunächst in der Startdatei der Anwendung, so als würden Sie diese nur dort benötigen (was zurzeit ja auch noch der Fall ist). Funktionen in der Startdatei
In der Startdatei eines Delphi/Kylix-Programms müssen Sie Funktionen oberhalb des begin-Blocks schreiben. Dieses Schreiben einer Funktion wird übrigens auch als „Deklarieren“ bezeichnet. Wenn Sie eine Funktion schreiben, melden Sie beim Compiler an, dass dieser beim Ablauf des Programms daraus eine Funktion im Arbeitsspeicher generiert. In Object Pascal wird eine einfache Funktion nach dem folgenden Schema deklariert: function Funktionsname([Argumentliste]): Rückgabedatentyp; begin Anweisungen result := Wert end;
Die Funktion erhält einen Namen, über den Sie diese später aufrufen können. Optional können Sie in den Klammern Argumente angeben, die prinzipiell wie Variablen deklariert werden. Über diese Argumente können Sie die Funktion dann gesteuert aufrufen. Wenn Sie mehrere Argumente übergeben wollen, trennen Sie diese einfach durch Kommata. Falls Sie sich jetzt fragen, warum die Argumentliste optional ist: Es gibt
300
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
auch viele Fälle, in denen eine Funktion einfach nur einen Job ausführen soll, der nicht gesteuert werden muss. Dann müssen eben keine Argumente übergeben werden. Die Variante der writeln-Prozedur (ok, das ist eine Prozedur, keine Funktion ...), die ohne Argument aufgerufen werden kann und dann nur einen Zeilenumbruch erzeugt, ist ein Beispiel dafür. Da eine Funktion immer einen Wert zurückgibt, besitzt diese einen Datentyp. Diesen müssen Sie im Funktionskopf angeben. Für die Argumente und den Rückgabewert können Sie alle verfügbaren Datentypen einsetzen (auch z. B. Klassen, aber das ist ein spezielles Thema). Innerhalb der Funktion schreiben Sie dann eine oder mehrere Anweisungen. In einigen dieser Anweisungen greifen Sie auf die übergebenen Argumente zu, indem Sie diese wie Variablen behandeln. Den Wert, den die Funktion zurückgeben soll, schreiben Sie in die vom Compiler implizit erzeugte Variable result. Damit das Ganze nicht zu theoretisch wird, folgt hier eine Funktion, die den Mittelwert zweier Zahlen berechnet: 01 02 03 04 05 06 07 08 09 10 11 12
Eine MittelwertFunktion
program Funktionsdemo; {$APPTYPE CONSOLE} uses SysUtils; { Funktion zur Berechnung des Mittelwerts ziwschen zwei Zahlen } function Mittelwert(zahl1: double; zahl2: double): double; begin { Berechnung und Rückgabe des Ergebniswerts } result := (zahl1 + zahl2) / 2; end;
Wie Sie sehen, werden die Argumente der Funktion wie Variablen deklariert (allerdings ohne das var-Schlüsselwort). Achten Sie darauf, dass Sie den Funktionskopf und das end mit einem Semikolon abschließen. Der Compiler meldet sich aber im Fehlerfall schon bei Ihnen ... Beachten Sie, dass Sie Funktionen normalerweise oberhalb deren ersten Verwendung deklarieren müssen. Deklarieren Sie eine Funktion unterhalb deren erster Verwendung (was in der Startdatei allerdings gar nicht möglich ist), kann der Compiler einen Funktionsaufruf nicht mit der Funktion in Verbindung bringen.
301
Sandini Bib
Aufrufen der Funktion Das Ergebnis der Funktion, also der Wert, den die Funktion zurückgeben soll, schreiben Sie in die Variable result Diese Variable wird vom Compiler zur Verfügung gestellt. Der Wert, der am Ende der Funktion in result gespeichert ist, wird zurückgegeben. Sie können mit dieser Variablen arbeiten wie mit normalen Variablen, also auch deren Wert mehrfach verändern und wieder auslesen. Eine solche Funktion kann dann wie jede andere Funktion verwendet werden: 13 var zahl1, zahl2, ergebnis: double; 14 15 begin 16 zahl1 := 7; 17 zahl2 := 42; 18 19 { Aufruf der Mittelwert-Funktion } 20 ergebnis := Mittelwert(zahl1, zahl2); 21 writeln('Der Mittelwert von ', zahl1, ' und ', 22 zahl2, ' ist ', ergebnis); 23 24 zahl1 := 3; 25 zahl2 := 7; 26 27 { Zweiter Aufruf der Mittelwert-Funktion } 28 writeln('Der Mittelwert von ', zahl1, ' und ', 29 zahl2, ' ist ', Mittelwert(zahl1, zahl2)); 30 end.
Sie können die Funktion – wie jede andere Funktion auch – überall da einsetzen, wo der Datentyp der Funktion passt oder kompatibel ist. In der letzten Anweisung wird die Funktion z. B. innerhalb der Argumentliste der writeln-Prozedur aufgerufen. Was passiert beim Aufruf einer Funktion? Beim Aufruf einer Funktion verzweigt das Programm in die Funktion. Es arbeitet die einzelnen Anweisungen der Funktion ab (die dann natürlich kompiliert sind) und kehrt danach wieder an die Anweisung zurück, die dem Aufruf folgt. Das Ganze können Sie am besten in Delphi oder Kylix selbst einmal nachvollziehen. Setzen Sie an der Anweisung, die die Funktion aufruft, einen Haltepunkt, indem Sie in der linken
302
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Spalte des Editier-Fensters klicken. Starten Sie das Programm dann mit (F9). Der Debugger hält das Programm beim Erreichen des Haltepunkts an. Wenn Sie nun (F7) betätigen, können Sie schrittweise durch das Programm gehen. Dann sehen Sie schon selbst, was beim Aufruf einer Funktion geschieht. Sie können das Programm dann mit (F9) zu Ende ausführen oder mit (Strg) (F2) beenden. Wie werden die Argumente übergeben? Für das spätere Verständnis einer anderen Aufrufart als der, die unser Programm verwendet, des „Call by Reference“, ist es sehr wichtig, dass Sie verstehen, was mit den übergebenen Daten passiert. Beim Aufruf der Funktion schreibt das Programm die an den Argumenten übergebenen Daten in implizit erzeugte Variablen, die den Namen der Argumente erhalten. Das Programm erzeugt eine Kopie der übergebenen Daten. Abbildung 5.2 stellt einen solchen „normalen“ Funktionsaufruf dar. Arbeitsspeicher { Funktion zur Mittelwertberechnung } function Mittelwert(zahl1: double; zahl2: double): double; begin result := (zahl1 + zahl2) / 2; lokale Kopie end; Das übergebene Datum wird in die lokale Variable kopiert { Startblock der Anwendung } var zahl1, zahl2, ergebnis: double; begin zahl1 := 7; zahl2 := 42; { Aufruf der Mittelwert-Funktion } ergebnis := Mittelwert(zahl1, zahl2); end.
Speicherbereich von zahl1
Abbildung 5.2: Darstellung eines „normalen“ Funktionsaufrufs. Die gestrichelten Linien symbolisieren die Interaktion mit dem Arbeitsspeicher, die durchgezogene Linie einen Kopiervorgang.
303
Sandini Bib
Innerhalb der Funktion können Sie mit den Argumenten arbeiten, als ob es sich um Variablen handelt. Sie können den Wert deshalb auch verändern. Da es sich aber um eine Kopie handelt, verändern Sie von außen eventuell übergebene Variablen nicht. Sie können also sicher sein, dass nach außen nichts verändert wird, wenn Sie mit den Argumenten arbeiten. Und das ist auch gut so (und deswegen die Standardeinstellung). Funktionen in Java-Programmen Java ist eine komplett objektorientierte Programmiersprache und kennt deswegen eigentlich keine Funktionen, sondern nur Methoden. Eine Methode gehört immer zu einer Klasse. Die Bedeutung und Anwendung von Klassen zeige ich erst in Kapitel 6 (auch für Delphi und Kylix), ich kann hier also nicht näher darauf eingehen. An dieser Stelle ist nur wichtig, dass Java das Deklarieren von Methoden erlaubt, die sich wie normale Funktionen verhalten. Dazu ist lediglich das Schlüsselwort static notwendig. Die Startmethode einer JavaAnwendung ist ebenfalls mit diesem Schlüsselwort deklariert, eben aus dem Grund, weil es sich dabei um eine „normale Funktion“ handeln muss, damit der Interpreter diese beim Aufruf des Programms findet (dieser sucht nämlich in der auszuführenden .class-Datei nach einer einfachen, statischen Methode mit dem Namen main und den Argumenten, die diese Methode besitzt). Sie können einfache Funktionen direkt in der Klasse der Startanwendung unterbringen. Deklarieren Sie diese Funktion oberhalb der mainFunktion nach dem folgenden Schema: static Rückgabetyp Funktionsname([Argumentliste]) { Anweisungen return Ergebnis; }
Abgesehen von der etwas anderen Syntax wird der Funktionskopf im Gegensatz zu Object Pascal nicht mit einem Semikolon abgeschlossen. Wenn Sie mehrere Argumente deklarieren, trennen Sie diese durch Kommata. Den Rückgabewert geben Sie über die return-Anweisung zurück. In Java müssen Sie beachten, dass return sofort dazu führt, dass die Funktion beendet wird (was auch häufig zum vorzeitigen Abbruch einer Funktion genutzt wird). Wenn Sie diese Anweisung mitten in die Funktion einfügen, wird die Funktion nicht zu Ende ausgeführt.
304
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Ansonsten gilt aber alles das, was auch für Object Pascal gilt: Sie können mehrere Argumente in die Argumentliste eintragen, die Sie wie Variablen deklarieren und durch Kommata trennen; die Argumente und der Rückgabetyp können prinzipiell jeden Datentyp annehmen. Das Delphi/Kylix-Programm zur Mittelwertberechnung sieht in Java dann so aus: 01 class funktionsdemo 02 { 03 /* Funktion zur Mittelwertberechnung */ 04 static double Mittelwert(double zahl1, double zahl2) 05 { 06 double ergebnis = (zahl1 + zahl2) / 2; 07 /* Ergebnis zurückgeben */ 08 return ergebnis; 09 } 10 11 public static void main(String args[]) 12 { 13 System.out.println("Funktionsdemo"); 14 15 /* Aufruf der Funktion */ 16 double zahl1 = 7; 17 double zahl2 = 42; 18 double ergebnis = Mittelwert(zahl1, zahl2); 19 20 /* Ergebnis ausgeben */ 21 System.out.println("Der Mittelwert zwischen " + zahl1 + 22 " und " + zahl2 + " ist " + ergebnis); 23 } 24 }
Variablen in Funktionen und die Datentypen der Argumente und des Rückgabewerts In Object Pascal-Funktionen müssen Sie Variablen, die Sie innerhalb der Funktion benötigen, zwischen dem Funktionskopf und dem beginSchlüsselwort deklarieren. Das folgende Beispiel demonstriert dies an einer Funktion zur Berechnung der Fakultät einer Zahl:
305
Sandini Bib
01 02 03 04 05 06 07 08 09
function Fakultaet(zahl: byte): longint; { Deklaration der benötigten Variablen } var i: integer; begin { Berechnung der Fakultät, direkt in die Ergebnisvariable } result := 1; for i := 2 to zahl do result := result * i; end;
Das Beispiel berechnet die Fakultät direkt in die Ergebnisvariable (womit eine zusätzliche Variable eingespart wird). In Java können Sie Variablen direkt da deklarieren, wo Sie sie benötigen. Java-Funktionen besitzen keine Rückgabe-Variable, weswegen Sie für eine Funktion zur Fakultätsberechnung eine Variable mehr benötigen: 01 static long Fakultaet(byte zahl) 02 { 03 /* Deklaration der benötigten Variablen */ 04 int ergebnis = 1; 05 for (int i = 2; i <= zahl; i++) 06 ergebnis *= i; 07 08 /* Ergebnis zurückgeben */ 09 return ergebnis; 10 }
Was in beiden Funktionen sichtbar wird, ist, dass Sie immer auf sinnvolle Datentypen für die Argumente und den Rückgabetyp achten sollten. Mit gut gewählten Datentypen schaffen Sie eine möglichst hohe Sicherheit in Ihren Programmen. Um beim Beispiel zu bleiben: Die Fakultät einer Zahl kann sehr groß werden. Deshalb lässt die Funktion maximal einen Byte-Wert zu und gibt einen Longint-Wert zurück. Damit sind Überläufe wenigstens minimiert (obwohl die Fakultät von 17 bereits zu einem Überlauf führt). Die Argumente und der Rückgabewert von Funktionen können prinzipiell jeden Datentyp annehmen, der im Programm an der Stelle der Funktionsdeklaration gültig ist. Neben den Standardtypen können Sie in Delphi und Kylix z. B. auch Strukturen übergeben, sofern diese in der aktuellen oder in einer (über uses) eingebundenen Unit deklariert sind. Damit können Sie sehr einfach auch komplexe Daten an eine Funktion übergeben oder von einer Funktion zurückgeben lassen. Ich verzichte hier auf ein Beispiel. Probieren Sie es einfach aus.
306
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
5.6.2 Prozeduren Prozeduren sind nun, da Sie wissen, was Funktionen sind, sehr schnell abgehandelt: Eine Prozedur ist eine Funktion, die nichts zurückgibt. In Java wird eine Prozedur deshalb auch mit dem speziellen Rückgabetyp void (englisch für „Nichts“) deklariert: void Prozedurname([Argumentliste]) { Anweisungen }
In Java ist eine Prozedur also tatsächlich eine Funktion, die nichts zurückgibt. In Delphi und Kylix verwenden Sie das Schlüsselwort procedure: procedure Prozedurname([Argumentliste]); begin Anweisungen end;
Eine Prozedur, die eine Zahl an der Konsole in einem zu übergebenden Format ausgibt und dabei noch so gesteuert werden kann, dass sie wahlweise einen Zeilenumbruch erzeugt, sieht in Object Pascal z. B. so aus: procedure ZahlAusgabe(zahl: double; format: string; umbruch: boolean); var ausgabe: string; begin { Ausgabe formatieren } ausgabe := FormatFloat(format, zahl); { Formatierte Zahl ausgeben } write(ausgabe); { Zeilenumbruch erzeugen, wenn gefordert } if umbruch = true then writeln; end;
Der Aufruf der Prozedur entspricht dann wieder dem normalen Aufruf ZahlAusgabe(zahl, '0.000');
Da die Zahlausgabe-Funktion sehr hilfreich ist, sollten Sie diese in einem separaten Modul unterbringen. Das Modul werden Sie dann (wahrscheinlich) in Ihren zukünftigen Projekten einsetzen. Erzeugen Sie deshalb gegebenenfalls auf Ihrem Festplattenlaufwerk einen neuen Ordner C:\Module\Delphi bzw. /module/kylix, in dem Sie Ihre eigenen Module ablegen (falls Sie dieses noch nicht bereits angelegt haben). Starten Sie Delphi bzw. Kylix und erzeugen Sie eine neue Konsolenanwendung (zum Test des neuen Moduls). Speichern Sie das Projekt im Projekte-
Ein Modul für Konsolen-Ein- und -Ausgaben
307
Sandini Bib
Ordner in einem neuen Unterordner. Nennen Sie den Unterordner und das Projekt z. B. KonsolenToolsTest. Fügen Sie diesem Projekt eine neue Unit hinzu. Speichern Sie diese Unit im Modulordner, vielleicht unter dem Namen KonsolenTools. In dieser Unit schreiben Sie nun die Funktion. Beachten Sie, dass eine Unit aus zwei Abschnitten besteht und dass Sie den Kopf der Funktion im interface-Abschnitt separat deklarieren müssen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
unit KonsolenTools; interface uses SysUtils; procedure ZahlAusgabe(zahl: double; format: string; umbruch: boolean); implementation { Prozedur zur formatierten Ausgabe einer Zahl } procedure ZahlAusgabe(zahl: double; format: string; umbruch: boolean); var ausgabe: string; begin { Ausgabe formatieren } ausgabe := FormatFloat(format, zahl); { Formatierte Zahl ausgeben } write(ausgabe); { Zeilenumbruch erzeugen, wenn gefordert } if umbruch = true then writeln; end; end.
Diese Prozedur testen Sie nun in der Konsolenanwendung. Wenn Sie den Ordner, in dem die Unit gespeichert ist, in Delphi oder Kylix im Bibliothekspfad aufgenommen haben, können Sie die Unit dann in Ihren neuen Projekten durch einen einfachen Eintrag in der uses-Anweisung einbinden. Mit der Verwendung der Funktion ZahlAusgabe sparen Sie damit bereits ein wenig Arbeit ein. Ich greife hier dem Abschnitt über eigene Bibliotheken vor (Seite 316). Dort finden Sie die notwendigen Informationen zum Entwickeln und zum Einbinden derselben in Ihre Programme.
308
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Für Ihre Java-Programme sollten Sie ebenfalls ein Modul erzeugen, das Sie auch wieder KonsolenTools nennen können, und in einem Ordner für Java-Module speichern. Erzeugen Sie in diesem Modul eine gleichnamige Klasse. Die Zahlausgabe-Prozedur sieht dann in Java programmiert so aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19
Die AusgabeProzedur in Java
import java.text.*; import java.util.*; class KonsolenTools { /* Prozedur, die eine Zahl an der Konsole formatiert ausgibt */ static void ZahlAusgabe(double zahl, String format, boolean umbruch) { /* Zahl formatiert ausgeben */ NumberFormat nf = NumberFormat.getInstance(); if (nf instanceof DecimalFormat) { ((DecimalFormat)nf).applyPattern("0.00000"); } System.out.print(nf.format(zahl)); if (umbruch) System.out.println(); } }
Schreiben Sie zum Test des Moduls eine kleine Konsolenanwendung, die Sie zunächst im Ordner des Moduls speichern. Der Aufruf der Prozedur sieht dann beispielsweise so aus: double zahl = 1.234567890; KonsolenTools.ZahlAusgabe(zahl, "0.0000", true);
Java-Module müssen Sie auf eine spezielle Weise in ein Projekt einbinden, wie ich es ab Seite 321 zeige. Das war schon alles. Nun fragen Sie sich wahrscheinlich, wozu Sie Prozeduren benötigen. Prozeduren setzen Sie immer dann ein, wenn Sie mehrere Anweisungen zusammenfassen wollen, aber keinen Rückgabewert benötigen. Eine Prozedur zum Ausdruck einer Rechnung muss beispielsweise nichts zurückliefern, sondern einfach nur die Rechnung drucken. Prozeduren werden aber eher selten verwendet. In vielen Fällen macht es Sinn, wenigstens über einen booleschen Rückgabewert zurückzumelden, ob die Anweisungen erfolgreich ausgeführt werden konnten, auch, wenn dieser Rückgabewert im Moment vielleicht noch gar nicht benötigt wird (aber vielleicht im weiteren Verlauf der Programmierung).
309
Sandini Bib
Falls Funktionen oder Prozeduren fehlschlagen können, z. B. weil eine Datei, die gelesen werden soll, nicht vorhanden ist, erzeugen moderne Programme an Stelle der Rückgabe eines booleschen Wertes eine Ausnahme. Ein boolescher Rückgabewert kann vom Programmierer versehentlich ignoriert werden, eine Ausnahme allerdings nicht. Wird diese nicht abgefangen, führt sie im Fehlerfall beim Kunden zu einer entsprechenden Meldung im Programm. Der korrekte Umgang mit Ausnahmen ist für moderne Programme sehr wichtig. Für einfache Aufgaben eignen sich boolesche Rückgabewerte aber oft auch sehr gut. Eine Funktion mit einem solchen Rückgabewert ist wesentlich einfacher anzuwenden, weil keine Ausnahmebehandlung vorgesehen werden muss. Das Beispiel im nächsten Abschnitt demonstriert diese Technik.
5.6.3 Funktionen und Prozeduren mit Referenzargumenten Wie ich auf Seite 303 gezeigt habe, werden die an eine Funktion oder Prozedur übergebenen Argumente normalerweise automatisch in neue Variablen in der Funktion bzw. Prozedur kopiert. Wenn Sie innerhalb der Funktion/Prozedur mit diesen Argumenten arbeiten, verändern Sie den Wert eventuell am Argument übergebener Variablen nicht. Das ist sehr gut so, denn so können Sie einfach weniger Fehler machen. In einigen Fällen ist es aber auch sinnvoll oder notwendig, dass der Wert einer übergebenen Variable verändert wird. Dann müssen Sie mit Referenzargumenten arbeiten. Diese Art der Übergabe von Argumenten wird auch als „Call by Reference“ bezeichnet, die normale Übergabeart als „Call by Value“. Referenzargumente in Delphi und Kylix DecodeDate als Beispiel
310
Die Bibliotheksprozedur DecodeDate zerlegt ein Datum in die Teile Jahr, Monat und Tag. Damit können Sie das Jahr, den Monat oder den Tag eines Datums herausfinden. Sie müssen dieser Prozedur am ersten Argument ein Datum (in Form eines TDateTime-Typs) und an den folgenden drei Argumenten Variablen übergeben. DecodeDate zerlegt das Datum und trägt die ermittelten Werte in die Variablen ein:
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
var jahr, monat, tag: word; begin DecodeDate(Now, jahr, monat, tag); writeln('Jahr: ', jahr); writeln('Monat: ', monat); writeln('Tag: ', tag);
Das Beispiel übergibt am ersten Argument die Now-Funktion, die einen TDateTime-Typ (der ja eigentlich nur ein double-Typ ist) mit dem aktuellen Datum zurückgibt. In die an den folgenden Argumenten übergebenen Variablen trägt DecodeDate das Jahr, den Monat und den Tag des Datums ein. DecodeDate ist übrigens veraltet. Verwenden Sie stattdessen lieber die Funktionen YearOf, MonthOf und DayOf aus der Unit DateUtils, die einfacher anzuwenden sind: jahr := YearOf(Now); monat := MonthOf(Now); tag := DayOf(Now); DecodeDate ist aber ein gutes Beispiel für Referenzargumente. Und gleichzeitig auch ein Beispiel dafür, dass der Umgang mit Referenzargumenten oft ziemlich umständlich ist. Sie müssen für jedes dieser Argumente eine Variable deklarieren, auch wenn Sie den Wert gar nicht benötigen. Eine Funktion wie YearOf können Sie aber auch direkt in einem Ausdruck einsetzen, ohne dafür eine Variable deklarieren zu müssen: writeln('Heute ist das Jahr ', YearOf(Now));
Was ich damit meine, ist: Verzichten Sie in Ihren eigenen Funktionen und Prozeduren möglichst auf Referenzargumente. Trotzdem müssen Sie wissen, wie Sie damit umgehen. Und manchmal kommen Sie um die Verwendung solcher Argumente vielleicht auch nicht (direkt) herum. Die Programmierung von Referenzargumenten ist einfach. Das Verständnis ist allerdings für die meisten Leute recht schwierig. Referenzargumente deklarieren Sie wie normale Argumente, nur dass Sie das Schüsselwort var voranstellen. Um gleich sinnvoll zu arbeiten, demonstriere ich diese Technik am Beispiel einer Funktion, die die Eingabe einer Zahl an der Konsole erleichtern soll. Diese Funktion soll:
Referenzargumente in Object Pascal
• einen Text ausgeben, der beim Aufruf übergeben werden kann, • den Anwender zur Eingabe auffordern,
311
Sandini Bib
• die Eingabe daraufhin überprüfen, ob es sich um eine gültige Zahl handelt, und • die eingegebene Zahl als double-Wert zurückgeben. Bei der Überlegung, eine solche Funktion zu entwickeln, stellt sich die Frage, was passieren soll, wenn der Anwender keine gültige Zahl eingegeben hat. Die Funktion könnte einfach eine Meldung ausgeben und die Zahl 0 zurückgeben. Leider erfährt dann aber das Programm, das diese Funktion aufgerufen hat (der so genannte Aufrufer), nicht, dass der Benutzer keine gültige Zahl eingegeben hat. Natürlich könnten Sie auch so vorgehen wie die Funktion StrToFloatDef und einen Defaultwert übergeben, der im Fehlerfall zurückgegeben wird. Das aufrufende Programm könnte dann am zurückgegebenen Defaultwert erkennen, dass die Eingabe nicht in Ordnung war. Eine solche Funktion wäre aber nicht immer brauchbar, denn es kann sein, dass der Anwender die Möglichkeit besitzen soll, alle Zahlen einzugeben. Also müssen Sie eine andere Lösung finden. Eine Lösung dieses Problems ist, dass die Funktion einen booleschen Wert zurückgibt, der eine Aussage darüber macht, ob die Eingabe erfolgreich war oder nicht. Die Frage ist jetzt nur, wo die Eingabe zurückgegeben wird. Und genau hier kommen Referenzargumente ins Spiel: Die Funktion gibt die Eingabe des Anwenders einfach in einer an einem Referenzargument übergebenen Variablen zurück. Das Programm, das diese Funktion aufruft, kann anhand des Rückgabewerts entscheiden, ob die Eingabe in Ordnung war, und die Eingabe aus der Variablen auslesen. Die Funktion, die zur Überprüfung auf eine gültige Zahl die Konvertierung einfach in einer Ausnahmebehandlung integriert, sieht dann so aus: 01 function ZahlEingabe(ausgabeText: string; var zahl: double): boolean; 02 var eingabe: string; 03 begin 04 { Ausgabe des Textes für den Anwender (mit Leerzeichen) } 05 write(ausgabeText, ' '); 06 07 { Eingabe entgegennehmen } 08 readln(eingabe); 09 10 { Eingabe in eine Zahl konvertieren } 11 try 12 { Zahl konvertieren und in die übergebene Variable schreiben } 13 zahl := StrToFloat(eingabe); 14
312
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
15 { Wenn das Programm hier ankommt, konnte die Eingabe 16 konvertiert werden. Die Funktion gibt dann true zurück } 17 result := true; 18 except 19 { Eingabe konnte nicht konvertiert werden. Die Funktion gibt 20 false zurück } 21 result := false; 22 end; 23 end;
Die Funktion schreibt die konvertierte Zahl in Zeile 13 in die am zweiten Argument übergebene Variable. Da die Konvertierung in einer Ausnahmebehandlung vorgenommen wird (die in Zeile 11 beginnt), wird eine eventuell dabei auftretende Ausnahme (im except-Block ab Zeile 18) abgefangen. In diesem Fall gibt die Funktion in Zeile 21 false zurück. Konnte die Konvertierung ausgeführt werden, erreicht das Programm die Anweisung unterhalb der Konvertierung (Zeile 17), und die Funktion gibt true zurück. Speichern Sie diese Funktion idealerweise in Ihrem Modul KonsolenTools, damit Sie sie in Ihren zukünftigen Projekten einfach wiederverwenden können. Die Funktion kann nun sehr einfach und trotzdem flexibel aufgerufen werden. Der Aufrufer kann entscheiden, was passiert, wenn die Eingabe fehlschlägt. Lassen Sie immer den Aufrufer entscheiden, wie auf eine Fehlersituation reagiert werden soll. Geben Sie möglichst niemals in einer allgemeinen Funktion, Prozedur oder Methode Fehlermeldungen oder Ähnliches aus. Sie wissen nie, was der Aufrufer (der Sie selbst, aber auch ein anderer Programmierer sein kann) wirklich machen will, wenn eine besondere Situation eintritt. Überlassen Sie einfach dem Aufrufer die Entscheidung und melden Sie lediglich einen Zustand zurück, z. B. über einen booleschen Rückgabewert. Ausnahmen von dieser „Regel“ sind Funktionen, Prozeduren und Methoden, die nur privat in einem Programm (z. B. einem Formular) verwendet werden. Beim Aufruf müssen Sie eine Variable vom Typ double am zweiten Argument übergeben (aber der Compiler meldet sich im Fehlerfall schon bei Ihnen ...). In diese Variable schreibt die Funktion die konvertierte Eingabe:
313
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Das Prinzip der Referenzübergabe
program Referenzargumente; {$APPTYPE CONSOLE} uses SysUtils,KonsolenTools; var zahl: double; begin { Funktion zur Eingabe einer Zahl aufrufen } if ZahlEingabe('Geben Sie eine Zahl ein: ', zahl) = true then begin { Eingabe war erfolgreich, Zahl (über die andere im Modul enthaltene Prozedur) formatiert ausgeben } write('Ihre Zahl: '); ZahlAusgabe(zahl, '0.0000', true); end else begin { Eingabe war nicht erfolgreich } writeln('Sie haben keine gültige Zahl eingegeben'); end; end.
Damit Sie besser verstehen, was dabei passiert, zeigt Abbildung 5.3, wie das Programm prinzipiell arbeitet. Arbeitsspeicher { Funktion mit Referenzargument } function ZahlEingabe(ausgabeText: string; var zahl: double): boolean; begin ... try zahl := StrToFloat(eingabe); ... end; Der Compiler übergibt die Adresse der Variablen an die Funktion { Startblock der Anwendung } Variable zahl var zahl: double; begin if ZahlEingabe('Zahl: ', zahl) = true then ... end;
Abbildung 5.3: Symbolische Darstellung einer Argumentübergabe mit Referenzargument
314
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Die gestrichelten Linien symbolisieren Speicherzugriffe des Programms. Der Compiler sorgt dafür, dass an dem Referenzargument die Speicheradresse der Variablen (die auch als Referenz bezeichnet wird) übergeben wird. Hier wird keine Kopie erzeugt, wie dies bei der normalen Übergabe der Fall ist (vgl. Seite 303). Schreibt die Funktion über die Referenz Daten, wird der Speicherbereich der übergebenen Variablen damit beschrieben. Deshalb ist die Variable natürlich auch nach außen hin verändert. Eine Referenz ist übrigens eine Speicheradresse mit zusätzlicher Information, welchen Datentyp der Speicher an dieser Adresse besitzt (was enorm wichtig ist), die gleichzeitig auch davor geschützt ist, auf einen anderen Speicherbereich „umgebogen“ zu werden. Eine einfache Speicheradresse ohne Datentypinformation ist ein so genannter Zeiger. Aber mit diesen haben Sie glücklicherweise2 in Object Pascal und Delphi nichts zu tun (mit Referenzen schon, wie Sie im nächsten Kapitel sehen). Referenzargumente in Java Java kennt keine Referenzargumente im eigentlichen Sinn. Eine Funktion wie die Zahleingabefunktion des Delphi/Kylix-Programms ist so in Java nicht möglich. Damit steht Java ziemlich alleine, klassische Referenzargumente (an die einfache Variablen übergeben werden können) sind in den meisten Sprachen möglich. Sie können in Java aber – wie auch in Delphi und Kylix – Instanzen von Klassen (Objekte) an eine Methode übergeben. Instanzen von Klassen werden implizit als Referenz übergeben. Schreibt die Methode Daten in das Objekt, ist diese Änderung auch nach außen sichtbar. Alternativ können Sie auch Funktionen schreiben, die Objekte zurückgeben. Damit können Sie recht einfach Funktionen schreiben, die mehrere Daten zurückgeben (was aber auch wieder in Delphi und Kylix möglich ist). Da das Ganze schon zu den erweiterten objektorientierten Techniken gehört, bespreche ich diese Technik allerdings nicht in diesem Buch.
5.6.4 Weitere Techniken, die hier nicht besprochen werden Funktionen und Prozeduren können noch einiges mehr als das, was Sie hier gelernt haben. Ein schwieriges Thema haben Sie bereits kennen gelernt, die Übergabe von Argumenten als Referenz (By Reference). Die weiteren Techniken sind vielleicht nicht mehr ganz so kompliziert, wür2.
In C und C++ können Sie mit Hilfe von Zeigern „wild“ im Arbeitsspeicher agieren und ohne Probleme auch Speicher überschreiben, der gar nicht zu Ihrem aktuellen Kontext oder zu Ihrem Programm gehört.
315
Sandini Bib
den aber den Rahmen dieses Buchs sprengen. Damit Sie wissen, was Sie noch lernen können, folgt hier eine kurze Liste: • die Übergabe und die Rückgabe von Arrays (Variablen-Listen), • die Übergabe und die Rückgabe von Strukturen, • die Übergabe und die Rückgabe von Objekten, • Funktionen und Prozeduren mit optionalen Argumenten, • Überladene Funktionen und Prozeduren (mehrere Varianten mit unterschiedlichen Argumenten). In Kapitel 6 bespreche ich allerdings überladene Methoden, deren Prinzip dem von überladenen Funktionen und Prozeduren gleicht, • Funktionen und Prozeduren mit variabler Anzahl Argumente (ähnlich writeln).
5.7
Bibliotheken: Funktionen und Prozeduren in eigenen Modulen
Funktionen und Prozeduren besitzen häufig einen so allgemeinen Charakter, dass Sie diese zumindest im aktuellen, aber häufig auch in anderen Programmen wiederverwenden können. Solche Funktionen und Prozeduren sollten Sie in eigenen Modulen speichern. Können Sie diese auch in anderen Projekten wiederverwenden, sollten Sie diese Module in einem allgemeinen Ordner speichern, den Sie für Ihre eigenen Bibliotheken vorgesehen haben. Nur so erreichen Sie, dass Sie einmal erledigte Arbeit im aktuellen Projekt und in anderen Projekten nicht wiederholt ausführen müssen und vor allen Dingen auch ohne Probleme wiederfinden. Das Ganze fällt unter das Stichwort „Wiederverwendung“ von Programmcode. Die Funktion zur Mittelwertberechnung, die wir ab Seite 300 entwickelt haben, ist beispielsweise eine solche allgemeine Funktion. Das folgende Beispiel fügt diese Funktion und eine weitere, zur Berechnung der Fakultät einer Zahl, zunächst in ein separates Modul im Projektordner ein.
5.7.1 Delphi/Kylix Module im Projektordner Fügen Sie dem Funktionsdemo-Programm (oder einer Kopie davon) in Delphi über das Menü FILE / NEW eine neue UNIT hinzu. In Kylix verwenden Sie dazu den Befehl NEU im DATEI-Menü und wählen dann im Objektgalerie-Dialog im ersten Register den Eintrag UNIT. Ihr Projekt besteht nun aus zwei Dateien: der Projekt-Start-Unit und der neuen. Spei-
316
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
chern Sie die neue Unit unter dem Namen MyMath (oder ähnlich) zunächst im Projektordner ab. Dieses Modul soll ausschließlich mathematische Funktionen speichern. Mit einer solchen thematischen Organisation Ihrer Module erreichen Sie, dass Sie genau wissen, welche Ihrer Module welche Funktionen enthalten. Schauen Sie jetzt einmal in den Quelltext der Projekt-Startdatei. Delphi bzw. Kylix hat das neue Modul automatisch in die uses-Anweisung eingetragen: uses SysUtils, MyMath in 'MyMath.pas';
Und genau das ist auch notwendig, um Module in anderen Modulen eines Projekts zu verwenden. Die Deklaration der uses-Anweisung ist ein wenig anders, als Sie diese bisher kennen. Delphi/Kylix hat zusätzlich noch angegeben, wo die neue Unit zu finden ist. Das ist für die Entwicklungsumgebung wichtig, die daran erkennt, wo die Datei gespeichert ist. Sie können eine in der uses-Anweisung eingebundene Unit, die im Quellcode vorliegt, übrigens schnell öffnen (falls diese geschlossen ist), indem Sie den Cursor auf den Dateinamen setzen und (Strg) (¢) betätigen. Ansonsten hilft Ihnen auch der Projektmanager ((Strg) (Alt) (F11)) dabei, die im Projekt eingebundenen Units zu finden. Das neue Modul besteht nun aus zwei Bereichen: unit MyMath; interface implementation end.
Im implementation-Abschnitt deklarieren Sie alle Funktionen. Die deutsche Übersetzung von „Implementation“ heißt übrigens Implementierung. Damit ist das Schreiben von Programmcode, quasi das Einbauen von Programmteilen in ein Programm, gemeint. Wenn Sie eine Funktion nur in diesem Abschnitt schreiben und nichts weiter machen, ist diese Funktion eine so genannte private Funktion. Private Funktionen gelten nur in dem Modul, in dem sie deklariert sind. Solche Funktionen werden häufig als Hilfsfunktionen für die anderen, öffentlichen Funktionen des Moduls verwendet.
317
Sandini Bib
Um Funktionen auch von außen zugreifbar zu machen, müssen Sie den Kopf der Funktion in den interface-Abschnitt einfügen. Interface ist der englische Begriff für Schnittstelle. Dieser Abschnitt ist also die Schnittstelle des Moduls nach außen (genau wie ein Computer auch einige Schnittstellen besitzt, aber lang nicht alle seine „Funktionen“ nach außen freigibt). Ein Modul mit einer Mittelwert- und einer Fakultätsberechnungs-Funktion, die beide von außen (also im Beispiel vom Startmodul aus) aufgerufen werden können, sieht dann so aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
318
unit MyMath; interface { Deklaration der Funktionen, um diese zu veröffentlichen } function Mittelwert(zahl1: double; zahl2: double): double; function Fakultaet(zahl: byte): longint; implementation { Implementierung der Funktionen } { Funktion zur Berechnung des Mittelwerts ziwschen zwei Zahlen } function Mittelwert(zahl1: double; zahl2: double): double; begin { Berechnung und Rückgabe des Ergebniswerts } result := (zahl1 + zahl2) / 2; end; { Funktion zur Berechnung der Fakultät einer Zahl } function Fakultaet(zahl: byte): longint; { Deklaration der benötigten Variablen } var i: integer; begin { Berechnung der Fakultät, direkt in die Ergebnisvariable } result := 1; for i := 2 to zahl do result := result * i; end; end.
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Die vom Ur-Pascal übernommene arbeitsintensive Unterteilung in einen interface- und einen implementation-Abschnitt habe ich nie richtig verstanden. Andere Programmiersprachen verwenden einfach die Schlüsselwörter private und public um private und öffentliche Funktionen zu kennzeichnen. In allen anderen Modulen des Programms, die diese Unit einbinden, können Sie die darin enthaltenen Funktionen nun ganz normal aufrufen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
program Funktionsdemo; {$APPTYPE CONSOLE} uses SysUtils, MyMath in 'MyMath.pas'; var zahl1, zahl2: integer; begin writeln('Funktionsdemo'); zahl1 := 7; zahl2 := 42; { Aufruf der Mittelwert-Funktion } writeln('Der Mittelwert von ', zahl1, ' und ', zahl2, ' ist ', Mittelwert(zahl1, zahl2)); { Aufruf der Fakultätsberechnungs-Funktion } writeln('Die Fakultät von ', zahl1, ' ist ', Fakultaet(zahl1)); readln; end.
Allgemeine Module in speziellem Ordner Wenn Sie allgemeine Module in einem speziell dafür vorgesehenen Ordner speichern (also nicht im Projektordner), können Sie diese ganz einfach in unterschiedlichen Projekten einsetzen. Die Unit MyMath ist beispielsweise ein solch allgemeines Modul. Verschieben Sie diese Datei in einen allgemeinen Modulordner, den Sie natürlich u. U. zuvor anlegen
319
Sandini Bib
müssen. Erzeugen Sie z. B. dazu den Ordner C:\Module\Delphi (Windows) bzw. /module/kylix (Linux). Zur Einbindung eigenen Bibliotheken können Sie in Delphi und Kylix zwei Wege gehen. Sie können ein allgemeines Modul zum einen einfach über das Menü PROJECT/ADD TO PROJECT bzw. PROJEKT/DEM PROJEKT HINZUFÜGEN in das Projekt einbinden. Ein anderer, meist sinnvollerer Weg ist, Ihren Modulordner im Delphi/Kylix-Bibliothekspfad als Bibliotheks-Ordner anzugeben. In Delphi wählen Sie den Befehl ENVIRONMENT OPTIONS im TOOLS-Menü. In Kylix heißt dieser Befehl UMGEBUNGSOPTIONEN. Dort fügen Sie den Pfad zu Ihrem Modulordner einfach an den vorhandenen Pfad an (Abbildung 5.4).
Abbildung 5.4: Anfügen eines Pfads zum eigenen Modulordner in den Bibliothekspfad von Delphi
Lassen Sie den vordefinierten Pfad aber bitte unangetastet, sonst findet Delphi/Kylix die Standardbibliotheken nicht mehr. Beachten Sie, dass Sie die einzelnen Pfade unter Windows durch Semikola und unter Linux durch Doppelpunkte voneinander trennen. Nun können Sie Ihre Unit wie alle anderen Units über die uses-Anweisung einbinden (ohne den Dateinamen der Quellcode-Datei mit anzugeben). Wenn Sie den Cursor auf den Unitnamen stellen, können Sie die Datei sogar mit (Strg) (¢) sehr schnell im Editor öffnen.
320
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
5.7.2 Java In Java erzeugen Sie für allgemeine Module ähnlich wie in Delphi und Kylix eine separate Datei. Nennen Sie diese z. B. MyMath.java. Speichern Sie die Datei zunächst im Projektordner. Ein Modul besteht in Java aus einer oder mehreren Klassen, die einzelne Methoden (und Eigenschaften und andere Dinge) enthalten. Für unser Modul reicht eine Klasse aus, die Sie ebenfalls MyMath nennen können. In dieser Klasse entwickeln Sie die Funktionen: 01 public class MyMath 02 { 03 /* Funktion zur Mittelwertberechnung */ 04 static double Mittelwert(double zahl1, double zahl2) 05 { 06 double ergebnis = (zahl1 + zahl2) / 2; 07 /* Ergebnis zurückgeben */ 08 return ergebnis; 09 } 10 11 /* Funktion zur Fakultätsberechnung */ 12 static long Fakultaet(byte zahl) 13 { 14 /* Deklaration der benötigten Variablen */ 15 int ergebnis = 1; 16 for (int i = 2; i <= zahl; i++) 17 ergebnis *= i; 18 19 /* Ergebnis zurückgeben */ 20 return ergebnis; 21 22 } 23 }
Die Unterschiede zur Startklasse einer Java-Anwendung sind, dass die main-Methode fehlt (diese wird ja nur zum Start der Anwendung benötigt) und dass die Klasse über das public-Schlüsselwort als öffentliche Klasse gekennzeichnet ist. Öffentliche Klassen können von anderen Paketen aus verwendet werden, was bei privaten Klassen, bei denen das public-Schlüsselwort fehlt, nicht möglich ist. Kompilieren Sie dieses Modul. In einer Anwendung können Sie das kompilierte Modul nun verwenden:
321
Sandini Bib
01 class funktionsdemo 02 { 03 public static void main(String args[]) 04 { 05 System.out.println("Funktionsdemo"); 06 07 int zahl1 = 17; 08 int zahl2 = 42; 09 10 /* Aufruf der Mittelwert-Funktion */ 11 double ergebnis = MyMath.Mittelwert(zahl1, zahl2); 12 13 /* Ergebnis ausgeben */ 14 System.out.println("Der Mittelwert zwischen " + zahl1 + 15 " und " + zahl2 + " ist " + ergebnis); 16 17 /* Fakultäts-Funktion aufrufen */ 18 System.out.println("Die Fakultät von " + zahl1 + " ist " + 19 MyMath.Fakultaet((byte)zahl1)); 20 } 21 }
Achten Sie darauf, dass Sie bei der Verwendung der Funktionen nun die Klasse mit angeben müssen, weil diese in einer anderen Klasse gespeichert sind. Falls Sie dies vergessen, meldet der Compiler, dass er die „Symbole“ Mittelwert und Fakultaet nicht auflösen kann („cannot resolve symbol“). Nennen Sie diese Anwendung funktionsdemo und kompilieren Sie sie, wie ich es auf den folgenden Seiten beschreibe. Kompilieren mit dem Java-Compiler Der Classpath
Wenn Sie Java-Dateien mit dem Java-Compiler direkt kompilieren, oder über den Java-Interpreter ausführen, müssen Sie mit dem so genannten Classpath (Klassenpfad) umgehen können. Dieser Pfad ist für Pfade zu eigenen Bibliotheken vorgesehen. Sie können diese Pfade beim direkten Aufruf des Java-Compilers und des Interpreters über die Option classpath mit angeben. In einigen Fällen (siehe unten) ist es beispielsweise notwendig, den aktuellen Ordner, der in Windows und Linux durch einen Punkt gekennzeichnet ist, im Klassenpfad anzugeben, damit der Compiler und der Interpreter die Dateien dieses Ordners mit einbeziehen: javac -classpath . funktionsdemo.java
322
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Falls Sie ohne Angabe des aktuellen Ordners im Klassenpfad kompilieren und die Umgebungsvariable CLASSPATH auf Ihrem System bereits gesetzt ist (siehe Seite 324), bezieht der Compiler den aktuellen Pfad nicht in die Suche nach der Klasse MyMath mit ein (es sei denn, der aktuelle Pfad ist dort eingetragen). Sie erhalten dann beim Kompilieren den Fehler „cannot resolve symbol“. Verwalten eines eigenen Modulordners Wie bei Delphi und Kylix sollten Sie allgemeine Module in einem speziell dafür vorgesehenen Ordner verwalten. Dabei sollten Sie sich direkt Gedanken um eine sinnvolle Unterteilung machen. Java setzt, wie Sie ja bereits wissen, das Paketkonzept ein. Sie sollten dieses Konzept auch in Ihren eigenen Klassen umsetzen. Um beim Java-Standard zu bleiben, sollten Ihre eigenen Klassen immer in einem Paket verwaltet werden, dessen Name mit einem Kürzel Ihres Namens oder Ihrer Firma beginnt. Ich beginne die Namen meiner Pakete in Anlehnung an eine von mir und Freunden geplante Firma mit earthware. Dann sollten Sie noch eine sinnvolle Unterteilung vornehmen. Alle Klassen, die die Java-Sprachfunktionalität erweitern (wie z. B. die MyMath-Klasse), sollten Sie z. B. dem Paket lang unterordnen. In meinem Fall kommt dabei der Paketname earthware.lang heraus. Klassen, die Sie zum Drucken entwickeln, können Sie z. B. dem Unterpaket print zuordnen.
Sinnvolle Paketnamen
Diesen Paketnamen weisen Sie einer Klasse über die package-Anweisung zu, die Sie oben in der Datei anbringen: package earthware.lang;
Wenn Sie solche Klassen nun in anderen Klassen verwenden, müssen Sie wie gewohnt den Paketnamen mit angeben oder das Paket importieren. Um dem Konzept von Sun ONE Studio 4 zu folgen, sollten Sie Ihre Bibliotheksklassen im Modulordner in Unterordnern speichern, die so aufgebaut sind wie der Paketname. Wenn Sie unter Linux z. B. einen Ordner /module/java für Ihre Module verwenden, sollten Sie für das Paket eartware.lang einen Ordner /module/java/eartware/lang erzeugen. Damit haben Sie dann auch keine Probleme, wenn Sie diese Klassen in ONE verwenden. Ähnlich sollten Sie natürlich auch in Delphi und Kylix vorgehen, damit Sie Ihre Module, die mit der Zeit immer mehr werden, ohne Probleme wiederfinden.
323
Sandini Bib
Die Variable CLASSPATH Den Pfad zu eigenen Modulordnern können Sie beim Kompilieren über den Java-Compiler direkt in der classpath-Option angeben. Java kennt aber auch eine Variable CLASSPATH, über die Sie Pfade zu eigenen Bibliotheken systemweit verwalten können. Wenn Sie Ihre allgemeinen Module in einem speziell dafür vorgesehenen Ordner speichern, sollten Sie den Pfad zu diesem Ordner in die Variable CLASSPATH aufnehmen. Der Java-Compiler und der Interpreter lesen diese Variable aus und verwenden die dort angegebenen Pfade als Klassenpfad, wenn beim Aufruf die classpath-Option nicht angegeben wurde. Wie Sie mit Umgebungsvariablen umgehen, habe ich für die PATH-Variable bereits in Kapitel 1 gezeigt. Die CLASSPATH-Variable müssen Sie u. U. erst anlegen. Tragen Sie dann den Pfad zu Ihrem Modulordner in diese Variable ein. Fügen Sie auch den Punkt in den Pfad mit ein, damit der Compiler und der Interpreter die Klassen des aktuellen Ordners mit einbeziehen (was diese ja sonst nicht machen, wenn ein Klassenpfad angegeben ist). Ein typischer Klassenpfad sieht dann unter Windows vielleicht so aus: c:\module\java;
und unter Linux so: /module/java:
Nun können Sie auch unter Java eigene Bibliotheken erzeugen, in einem separaten Ordner ablegen und in Ihren Programmen wiederverwenden. Beachten Sie aber, dass Sie diese Bibliotheken – anders als bei Delphi und Kylix – mit ausliefern müssen, wenn Sie Ihre JavaProgramme weitergeben. Kompilieren mit Sun ONE Studio 4 Sun ONE Studio 4 ignoriert (leider) die CLASSPATH-Variable. Wenn Sie eigene Bibliotheken in Projekten verwenden wollen, die Sie in Sun ONE Studio 4 entwickeln, müssen Sie die Ordner dieser Bibliotheken mit dem Projekt verbinden, so wie Sie dies bereits in Kapitel 2 für die Projektdateien selbst gezeigt habe. Sun ONE Studio 4 integriert automatisch alle Dateien, die im Projekt selbst oder in verbundenen Dateiordnern gespeichert sind, in das Projekt. Für die Ausführung des Programms über den Java-Interpreter ist dann aber wieder die korrekte Einstellung der CLASSPATH-Variable wichtig. Wenn Sie Ihre Bibliotheken mit Sun ONE Studio 4 entwickeln, müssen Sie beachten, dass diese Entwicklungsumgebung automatisch Paketnamen für die Klassen nach den Ordnern erzeugt, in denen die Klassenda-
324
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
teien gespeichert sind. Klassen, die direkt in einem verbundenen Ordner gespeichert werden, erhalten keinen Paketnamen und werden damit dem globalen Paket zugeordnet. Wenn Sie in einem verbundenen Ordner Unterordner anlegen, werden enthaltene Klassen aber bei der Erzeugung automatisch Paketen zugeordnet, die entsprechend den Ordnern benannt sind. Eine Klasse im Unterordner earthware\utils\print wird beispielsweise dem Paket earthware.utils.print zugeordnet. Ein Paket korrespondiert also mit den Unterordnern eines verbundenen Dateiordners. Wenn Sie Ihre Klassen in sinnvollen Paketen verwalten wollen, verbinden Sie zunächst Ihren allgemeinen Modulordner mit einem Projekt. Nun legen Sie über den Sun ONE Studio 4-Explorer in diesem Ordner ein neues Paket an. Klicken Sie dazu mit der rechten Maustaste auf den Ordner und wählen Sie den Befehl NEW / JAVA PACKAGE im Kontextmenü. Geben Sie im erscheinenden New Wizard einen sinnvollen Paketnamen ein, beispielsweise .lang für Klassen, die die Funktionalität der Sprache erweitern (wie die MyMath-Klasse). Sun ONE Studio 4 legt automatisch ein zum Paketnamen passendes OrdnerSystem an. Wenn Sie nun über den Explorer in diesen Ordnern Klassen anlegen, fügt die Entwicklungsumgebung automatisch eine entsprechende package-Anweisung hinzu, die die Klassen dem entsprechenden Paket zuordnet. Diesen Paketnamen müssen Sie bei der Benutzung der Klasse in anderen Klassen wie gewohnt angeben oder den Inhalt des Pakets über die import-Anweisung importieren.
5.7.3 Komplettierung der Java-Konsolen-Tool-Klasse Damit die Klasse KonsolenTools, die wir im Buch ab Seite 307 entwickelt haben, auch für Java einigermaßen komplett wird, sollten Sie eine Funktion zur Eingabe integrieren. Da Sie Referenzargumente zurzeit in Java-Programmen noch nicht realisieren können, schreiben Sie eine einfache Funktion, die neben dem auszugebenden Text einen Defaultwert übergeben bekommt. Im Fehlerfall soll die Funktion den Defaultwert zurückgeben. Diese Lösung ist zwar nicht besonders elegant, funktioniert aber zunächst. Die Funktion soll folgendermaßen aufgerufen werden: double zahl = ZahlEingabe("Geben Sie eine Zahl ein: ", -1);
Das erste Argument steht für den auszugebenden Text, das zweite für den Defaultwert. Schreiben Sie die Funktion in der Klasse KonsolenTools wie folgt:
325
Sandini Bib
01 static double ZahlEingabe(String ausgabeText, double defaultWert) 02 { 03 /* DataInputStream erzeugen */ 04 DataInputStream in; 05 in = new java.io.DataInputStream(System.in); 06 07 /* Daten einlesen */ 08 try 09 { 10 System.out.print(ausgabeText); 11 String eingabe = in.readLine(); 12 13 /* Eingabe konvertieren */ 14 try 15 { 16 NumberFormat nf2 = NumberFormat.getNumberInstance(); 17 double zahl = nf2.parse(eingabe).doubleValue(); 18 19 /* Ergebnis zurückgeben */ 20 return zahl; 21 } 22 catch (ParseException ex) 23 { 24 /* Die Konvertierung ist fehlgeschlagen */ 25 return defaultWert; 26 } 27 } 28 catch (IOException ex) 29 { 30 /* Die Daten konnten nicht eingelesen werden */ 31 return defaultWert; 32 } 33 }
Diese Funktion ist leider ein wenig komplex, weil die Eingabe und die Konvertierung von Daten in Java leider nicht einfach ist. Nehmen Sie die Funktion wie sie ist. Sie müssen an dieser Stelle noch nicht verstehen, was dort passiert. Falls Sie dies doch wissen wollen, lesen Sie den Abschnitt zu Ende: Daten über einen Stream lesen
326
Zur Eingabe benötigen Sie ein Objekt der Klasse DataInputStream. Bei einem solchen Objekt handelt es sich um einen Stream. Ein „Stream“ ist ein Strom von Daten mit einem Anfangs- und einem Endpunkt. Stellen Sie sich einen Stream als eine Datenleitung vor. Auf der einen Seite fließen die Daten hinein. Daten, die zurzeit nicht gelesen werden, werden
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
einfach im Stream zwischengespeichert. Sie können die Daten auf der anderen Seite des Stream über die read- oder die readLine-Methode zeichen- oder zeilenweise auslesen. Ein DataInputStream ist bereits ein erweiterter Stream, der spezielle Methoden anbietet (z. B. readLine zum Lesen einer ganzen Zeile). Solch ein Stream arbeitet intern mit einem einfachen Basis-Stream (der nur grundlegende Methoden anbietet). Diesen Basis-Stream müssen Sie dem DataInputStream-Objekt im Konstruktor übergeben. Für eine Konsoleneingabe verwenden Sie den vordefinierten Stream System.in, der seine Daten von der Konsole liest. Sie könnten aber z. B. auch ein neues FileInputStream-Objekt übergeben, das die Daten dann aus einer Datei lesen würde. Dem DataInputStream-Objekt ist es egal, was für ein Basis-Stream die Daten liefert, weil ein solcher immer dieselben Methoden und Eigenschaften besitzt (die vom DataInputStreamObjekt aufgerufen werden). Über die readlLine-Methode liest das Programm dann in Zeile 11 die Eingabe des Anwenders ein. Im Prinzip arbeitet das Programm nun wie eine Object Pascal-Anwendung, die die readln-Prozedur aufruft. Nun muss die Eingabe noch konvertiert werden. Dazu können Sie eine Instanz der Klasse NumberFormat verwenden. Ein solches Objekt verwaltet intern Informationen über das aktuelle Zahlformat. In Zeile 16 wird dieses Objekt auf eine eher komplexe Weise über eine statische Methode seiner eigenen Klasse erzeugt. Über die parse-Methode wird die Eingabe dann in Zeile 17 in eine Zahl konvertiert. Dabei müssen Sie beachten, dass parse den übergebenen String leider schrittweise durchgeht und nur dann eine Ausnahme erzeugt, wenn die Zahl mit einem Zeichen beginnt, das keine Ziffer ist. Ansonsten wird die Konvertierung einfach abgebrochen, wenn die Methode auf ein ungültiges Zeichen trifft. parse gibt dann einfach die bis dahin ermittelte Zahl zurück. Die Eingabe "1xyz" führt dann beispielsweise zum Ergebnis 1. Für dieses Problem habe ich leider bisher keine einfache Lösung gefunden.
Eingaben konvertieren
Etwas schwierig an dieser Funktion ist zudem, dass sie mit einer geschachtelten Ausnahmebehandlung arbeitet. Die äußere, die in Zeile 8 beginnt, reagiert auf Ausnahmen, die bei der Eingabe direkt auftreten können (obwohl das wohl nie der Fall sein wird). Die innere (Zeile 14) reagiert dann, wenn die Eingabe erfolgt ist, auf die Ausnahme, die bei der Konvertierung der Eingabe in eine Zahl auftreten kann. Im Fehlerfall gibt die Funktion den Defaultwert zurück (Zeile 25 und 31), ansonsten den konvertierten Zahlwert (Zeile 20). Wenn Sie diese Funktion aber einmal fertig gestellt, kompiliert und getestet haben, ist die Anwendung sehr einfach. Sie müssen die Funktion lediglich aufrufen. Und das ist der Sinn des Ganzen: (Einfache) Wiederverwendung von teilweise komplexem Programmcode.
Einfache Wiederverwendung
327
Sandini Bib
5.8
Variablen in Modulen
Bisher haben Sie Variablen prinzipiell immer da deklariert, wo Sie diese unmittelbar benötigten: innerhalb einer Funktion, Prozedur oder Methode. Eine Ausnahme davon war das Startmodul einer Delphi/KylixAnwendung, das keine Funktion, Prozedur oder Methode enthält. Hier haben Sie die benötigten Variablen oberhalb des begin-Schlüsselworts deklariert. Es gibt aber auch andere Möglichkeiten. Sie können eine Variable in einem normalen Delphi/Kylix-Modul auch im interface- oder implementation-Abschnitt deklarieren: unit Modulvariablen; interface var x: integer; implementation var y: integer; end. Globale Variablen
x und y sind so genannte globale Variablen. Diese Variablen gelten zumindest im gesamten Modul. Variablen, die Sie innerhalb einer Funktion oder Prozedur deklarieren (so genannte lokale Variablen), gelten hingegen nur in dieser Funktion bzw. Prozedur. Innerhalb anderer Funktionen oder Prozeduren können Sie nicht auf lokale Variablen zurückgreifen. Bei globalen Variablen ist das anders. Eine solche Variable können Sie zumindest in allen Funktionen und Prozeduren des Moduls lesen und beschreiben, in dem diese deklariert ist. Wenn eine Funktion den Wert der Variablen ändert, kann eine andere Funktion genau diesen Wert auslesen. Dabei wird noch weiter unterschieden in modulglobale und programmglobale Variablen. Eine modulglobale Variable gilt nur in dem Modul, in dem sie deklariert ist. Eine programmglobale Variable gilt im gesamten Programm. Modulglobale Variablen deklarieren Sie in Object Pascal im implementation-Abschnitt, programmglobale im interface-Abschnitt. Prinzipiell ist das Ganze einfach: Auf den Wert von lokalen Variablen (die innerhalb einer Funktion oder Prozedur deklariert sind) können Sie innerhalb dieser einen Funktion oder Prozedur zugreifen. Auf die Werte von modulglobalen Variablen können Sie in allen Funktionen und Prozeduren zugreifen, die im selben Modul gespeichert sind. Auf programmglobale Variablen können Sie in allen Funktionen und Prozeduren des gesamten Projekts zugreifen, unabhängig davon, in welchem Modul diese gespeichert sind.
328
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Die Frage ist nur: Wann benötigen Sie solche Variablen? Die Antwort ist: Wenn Sie modern und damit objektorientiert programmieren, möglichst gar nicht! Bevor ich diese Antwort näher erläutere, beschreibe ich erst einmal an einem einfachen Beispiel, wie Sie solche Variablen einsetzen können. Das Beispiel ist bewusst sehr einfach, aber es zeigt die Bedeutung und die Anwendung globaler Variablen.
Warum globale Variablen?
Ein Programm für die Pförtner einer Firma soll die Möglichkeit zur Verfügung stellen, die ein ein- und ausfahrenden Fahrzeuge zu zählen. Beim Erreichen einer bestimmten Anzahl an Fahrzeugen auf dem Gelände soll das Programm eine Warnmeldung ausgeben, sodass der Pförtner ein weiteres Einfahren verhindert, bis wieder Fahrzeuge ausgefahren sind. Da vermutet wird, dass sich an den Regeln des Programms in Zukunft einiges ändert oder dass das Programm eventuell in Zukunft an eine automatische Zählanlage angeschlossen wird und ebenso automatisch eine Schranke bedient, wird das Programm direkt in einem Modul entwickelt. Das Modul soll eine Funktion zum Hochzählen und eine zum Herunterzählen beinhalten. Beide Funktionen sollen einen booleschen Wert zurückgeben. Bei der Eingang-Funktion soll dieser Wert aussagen, ob weitere Fahrzeuge einfahren dürfen. Die Ausgang-Funktion soll false zurückgeben, wenn versucht wird, ein Fahrzeug bei einer aktuellen Anzahl von Null auszubuchen. Die Funktion zum Hochzählen soll nur dann hochzählen, wenn Fahrzeuge einfahren dürfen. Damit von außen festgelegt werden kann, wie viele Fahrzeuge sich gleichzeitig auf dem Gelände befinden können, wird eine programmglobale Variable eingesetzt. Zum Zählen der aktuell auf dem Gelände befindlichen Fahrzeuge setzt das Modul eine modulglobale Variable ein: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
unit Zaehler; interface { Deklaration der programmglobalen Variablen } var MaximaleAnzahl: integer; { Veröffentlichen der Prozeduren } function Eingang(): boolean; function Ausgang(): boolean; implementation { Deklaration der modulglobalen Variablen } var aktuelleAnzahl: integer;
329
Sandini Bib
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
{ Prozedur zum Hochzählen } function Eingang(): boolean; begin { Überprüfung der Regeln } if aktuelleAnzahl >= MaximaleAnzahl then begin result := false; end else begin inc(aktuelleAnzahl); result := true; end; end; { Funktion zum Herunterzählen } function Ausgang(): boolean; begin { Überprüfung der Regeln } if aktuelleAnzahl <= 0 then begin result := false; end else begin dec(aktuelleAnzahl); result := true; end; end; end.
Beachten Sie bitte, dass die Funktion Eingang in Zeile 21 die aktuelle Anzahl über >= mit der Maximalanzahl vergleicht. Ein Vergleich mit = würde zwar auch ausreichen, der Vergleich mit >= führt aber zu einer erhöhten Sicherheit. Falls es aus irgendwelchen Gründen (bei einer späteren Weiterentwicklung des Moduls) doch einmal vorkommt, dass die aktuelle Anzahl größer ist als die Maximalanzahl, würde ein Vergleich mit = nicht mehr funktionieren. Ähnliches gilt für den Vergleich der aktuellen Anzahl in der Ausgang-Funktion in Zeile 36. Der Name der programmglobalen Variable beginnt übrigens mit einem Großbuchstaben. Das ist eine übliche Vorgehensweise, um programmglobale von modulglobalen und lokalen Variablen zu unterscheiden.
330
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Das Programm nutzt die modulglobale Variable aktuelleAnzahl zum Zählen der aktuell auf dem Gelände befindlichen Fahrzeuge. Diese Variable muss global sein, weil die Funktionen Eingabe und Ausgabe darauf zugreifen müssen. Durch die lediglich modulglobale Deklaration kann die Variable aber nicht von außen gelesen und beschrieben werden, und das ist auch gut so! Damit verhindern Sie, dass die Variable von außen manipuliert werden kann. Stellen Sie sich vor, Sie geben Ihr Modul in kompilierter Form an andere Programmierer weiter, die die eigentliche Anwendung entwickeln. Sie sind verantwortlich für die Regeln der Fahrzeug-Ein- und -Ausfahrten. Die anderen Programmierer sollen zwar die eigentliche Anwendung entwickeln, sich aber genau an diese Regeln halten. Eine modulglobale Variable kann von außen nicht verändert werden. Damit sichern Sie ab, dass kein Programmierer (auch nicht Sie selbst) seine Anwendung bewusst oder unbewusst so entwickeln kann, dass die Regeln verletzt werden.
Sicherung der Datenintegrität
Da von außen aber möglich sein soll, die Maximalanzahl der Fahrzeuge festzulegen, ist die Variable MaximaleAnzahl programmglobal deklariert. Damit kann ein Programm beliebig (!) auf diese Variable zugreifen. Das Ganze ist in dieser einfachen Anwendung sehr vage. Natürlich kann eine Anwendung einfach die Maximalanzahl hochsetzen, um mehr Fahrzeuge zuzulassen, als eigentlich möglich ist. Denken Sie aber daran, dass es sich hierbei nur um ein möglichst einfaches Beispiel handelt. Andererseits ist genau das das Problem programmglobaler Variablen: Jeder Programmteil kann beliebig auf diese Variablen zugreifen und diese so manipulieren, dass das Programm eventuell fehlerhaft ausgeführt wird. Und es ist sehr gut, dass Sie das jetzt bereits erkennen. Ein Programm, das dieses Modul nutzt, kann nun die Maximalanzahl setzen und die Funktionen aufrufen, um Fahrzeugein- oder -ausgänge zu registrieren. Als Beispiel verwende ich eine normale Anwendung mit einem Formular (weil dort die Eingabe einfacher ist). Das Formular soll etwa so aussehen wie in Abbildung 5.5.
Verwendung in einem Programm
Abbildung 5.5: Das Formular für die Zähl-Anwendung
Fügen Sie der uses-Anweisung des Formularmoduls die Unit Zaehler hinzu, damit Sie auf die enthaltenen Funktionen zugreifen können.
331
Sandini Bib
In der Ereignisbehandlungsmethode des EINGANG-Schalters (btnEingang) soll die Eingang -Funktion aufgerufen werden: 01 02 03 04 05 06 07 08
procedure TfrmStart.btnEingangClick(Sender: TObject); begin { Aufruf der Eingang-Funktion } if Eingang() = false then begin ShowMessage('Zurzeit können keine Fahrzeuge einfahren'); end end;
Das Programm ruft in Zeile 4 die Eingang-Funktion auf und gibt in Zeile 6 über die ShowMessage-Prozedur eine passende Meldung aus, wenn kein weiteres Fahrzeug einfahren kann. Die Ereignisbehandlungsmethode des AUSGANG-Schalters sieht ähnlich aus: 09 10 11 12 13 14 15 16
procedure TfrmStart.btnAusgangClick(Sender: TObject); begin { Aufruf der Ausgang-Funktion } if Ausgang() = false then begin ShowMessage('Es befindet sich kein Fahrzeug auf dem Gelände'); end end;
Nun muss das Programm nur noch die Maximalanzahl initialisieren. Da die entsprechende Variable programmglobal deklariert ist, können Sie im Formular darauf zugreifen. Um die Initialisierung zu programmieren, klicken Sie doppelt auf einen freien Bereich des Formulars. Sie erzeugen damit eine Ereignisbehandlungsmethode für das FormCreateEreignis des Formulars, das immer dann aufgerufen wird, wenn das Formular vom Betriebssystem erzeugt wird, in unserem Fall also beim Start der Anwendung. Setzen Sie die Variable dort einfach auf einen beliebigen Wert: 17 18 19 20 21
procedure TfrmStart.FormCreate(Sender: TObject); begin { Zähler initialisieren } MaximaleAnzahl := 3; end;
Fertig. Wenn Sie das Programm nun testen, können immer nur drei Fahrzeuge gleichzeitig auf dem Gelände sein.
332
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
Das Schöne an diesem Programm ist, dass Sie die Regeln der Fahrzeug Ein- und Ausfahrt-Möglichkeiten sehr einfach im Modul manipulieren können. Wenn beispielsweise nach einer Sperrung der Einfahrt erst wieder mindestens zwei Fahrzeuge ausfahren müssen, ist die Änderung des Moduls (über eine weitere modulglobale Variable, die die Sperrung verwaltet) kein großes Problem. Solche in einem Modul verwalteten Regeln können auch ziemlich komplex werden. Sie liefern das kompilierte Modul aber einfach an die anderen Entwickler Ihrer Firma bzw. der einzelnen Filialen aus, die dann entsprechende Programme entwickeln (die sich aber eben an die Regeln halten müssen).
Verwaltung der Regeln ausschließlich im Modul
Das Ganze sollte Ihnen zeigen, dass globale Variablen ihren Sinn haben, dass es aber häufig der sicherere Weg ist, diese modulglobal zu deklarieren, damit kein anderer Programmteil – eventuell nur versehentlich – diese Variablen verändern kann. Manchmal müssen die Variablen aber auch programmglobal deklariert sein, damit von außen darauf zugegriffen werden kann. Halten Sie sich aber, um schwere Fehler durch versehentliches Überschreiben globaler Variablen zu vermeiden, immer an die eiserne (!) Programmierer-Regel: Deklarieren Sie Variablen immer auf der niedrigstmöglichen Ebene. Wenn Sie eine Variable nur innerhalb einer Funktion, Prozedur oder Methode benötigen, deklarieren Sie diese Variable auch dort. Wenn Sie in 500 Funktionen eine Zählvariable i benötigen, deklarieren Sie 500 einzelne Variablen. Kommen Sie nicht auf die Idee, dazu aus Faulheit nur eine einzige Variable zu verwenden, die Sie modul- oder sogar programmglobal deklarieren. Sie erzeugen damit zwangsläufig sehr schwere logische Fehler. Es wird garantiert passieren, dass eine der vielen Funktionen eines Ihrer Programme eine solche Variable benutzt, die dann aber von einer untergeordnet aufgerufenen Funktion verändert wird. Und schon läuft Ihr Programm nicht mehr korrekt und Sie müssen aus Ihrem Bahamas-Urlaub zurückkehren und den Fehler beheben. Verzichten Sie wo immer es geht auf programm- und auch auf modulglobale Variablen. Da diese im gesamten Programm gültig sind, wissen Sie nie genau, welche Funktion, Prozedur oder Methode Ihres Programms diese Variablen verwendet. Wenn Sie einer Funktion, Prozedur oder Methode Daten übergeben wollen, verwenden Sie wenn möglich dazu Argumente. Das Problem der globalen Daten spreche ich übrigens noch einmal im nächsten Kapitel, bei der objektorientierten Programmierung (OOP) an. Denn die OOP löst dieses Problem sehr elegant.
333
Sandini Bib
Die Lebensdauer von Variablen Variablen besitzen nicht nur einen Gültigkeitsbereich, sondern auch eine Lebensdauer. Eine Variable, die in einer Prozedur, Funktion oder Methode deklariert ist, lebt nur, so lange diese ausgeführt wird. Danach gibt der Compiler den Speicherbereich wieder frei. Bei jedem Aufruf der Prozedur, Funktion oder Methode legt der Compiler für alle Variablen neue Speicherbereiche an und initialisiert diese mit einem Leerwert. Für Sie bedeutet das, dass die Werte, die Sie in eine solche Variable schreiben, beim nächsten Aufruf nicht mehr zur Verfügung stehen (was aber nur selten ein Problem ist). Globale Variablen leben allerdings, so lange das Programm lebt. Werte, die Sie in diese Variablen schreiben, werden erst gelöscht, wenn das Programm beendet wird (aber dann benötigen Sie diese auch nicht mehr). Das Zähl-Programm hat das bereits demonstriert. In Einzelfällen kann es nun sein, dass Sie eine Variable zwar nur in einer Prozedur, Funktion oder Methode benötigen, diese aber ihren Wert nicht verlieren darf. Dann müssen Sie diese Variable ausnahmsweise modulglobal deklarieren. Für diesen Fall ist die Modulebene die niedrigstmögliche Ebene der Deklaration. Globale Variablen bei Java Java ermöglicht genau wie Delphi und Kylix auch die Deklaration globaler Variablen. Echt globale Variablen werden ähnlich den Funktionen, die Sie bisher für Java-Programme geschrieben haben, mit dem Schlüsselwort static deklariert. Modulglobale Variablen versehen Sie dann mit dem Schlüsselwort private, programmglobale mit dem Schlüsselwort public: 01 class Zaehler 02 { 03 /* Deklaration der programmglobalen Variablen */ 04 public static int MaximaleAnzahl; 05 06 /* Deklaration der modulglobalen Variablen */ 07 private static int aktuelleAnzahl; 08 09 /* Funktion zum Hochzählen */ 10 public static boolean Eingang() 11 { 12 /* Überprüfung der Regeln */ 13 if (aktuelleAnzahl >= MaximaleAnzahl) 14 {
334
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 }
return false; } else { aktuelleAnzahl++; return true; } } /* Funktion zum Herunterzählen */ public static boolean Ausgang() { /* Überprüfung der Regeln */ if (aktuelleAnzahl <= 0) { return false; } else { aktuelleAnzahl--; return true; } }
Der Zugriff auf dieses Modul sieht dann in Java so aus: /* Initialisieren des Zählers */ Zaehler.MaximaleAnzahl = 2; ... /* Fahrzeug einfahren lassen */ if (Zaehler.Eingang() == false) System.out.println("Zurzeit können keine weiteren " + "Fahrzeuge einfahren"); else System.out.println("Fahrzeug darf einfahren"); ... /* Fahrzeug ausbuchen */ if (Zaehler.Ausgang() == false) System.out.println("Es befindet sich kein Fahrzeug auf dem Gelände"); else System.out.println("Fahrzeug ausgebucht");
335
Sandini Bib
5.9
Zusammenfassung
In diesem Kapitel haben Sie die Techniken der strukturierten Programmierung gelernt. Sie können Algorithmen nun in Delphi, Kylix oder Java umsetzen und dabei auch schon erweiterte Techniken wie fußgesteuerte Schleifen und die Case-Verzweigung verwenden. Sie kennen die Tücken von mehrteiligen logischen Ausdrücken, die mit den Operatoren not, and und or bzw. !, && und || arbeiten. Um in Ihren Programmen wiederverwendbaren Code zu erzeugen können Sie Funktionen und Prozeduren schreiben. Sie können mit deren Argumenten und dem Rückgabetyp umgehen und sind sogar in der Lage, Variablen, die von außen an eine Funktion oder Prozedur übergeben wurde, innerhalb derselben zu manipulieren. Um sich eine Funktionsbibliothek zu schaffen, sind Sie in der Lage, eigene Module (in Java: Klassen) zu entwickeln, diese in einem separaten Ordner abzulegen und in Ihre Programme einzubinden. Schließlich kennen Sie noch den Unterschied zwischen lokalen und globalen Variablen und wissen, warum Sie möglichst auf globale Variablen verzichten sollten.
336
'LH 6WUXNWXULHUXQJ HLQHV 3URJUDPPV
Sandini Bib
5.10 Fragen und Übungen 1.
Welchen Datentyp besitzen Vergleichsausdrücke?
2.
Warum ergibt der Vergleich "a" == "a" in Java false?
3.
Worauf müssen Sie immer achten, wenn Sie komplexe Vergleichsausdrücke formulieren, die mit Und-Verknüpfungen, Oder-Verknüpfungen und/oder Negierungen arbeiten?
4.
Warum sollten Sie die Anweisungen innerhalb einer Schleife und einer Verzweigung immer etwas nach rechts einrücken?
5.
Was unterscheidet eine kopfgesteuerte von einer fußgesteuerten Schleife?
6.
Welchen Wert besitzt die Variable i nach der Ausführung der folgenden Schleife? for i := 1 to 10 do begin writeln(i); end;
7.
Schreiben Sie eine Java-Konsolenanwendung, bei der Sie eine Eingabe simulieren, indem Sie eine Variable zahl auf den Wert 7 festlegen. Die Anwendung soll dann für alle Zahlen von 1 bis zu dieser Zahl die Fakultät berechnen und ausgeben. Die Fakultät einer Zahl berechnet sich, indem alle Zahlen von 1 bis zu der Zahl miteinander multipliziert werden. Achten Sie darauf, dass möglichst keine Überläufe entstehen können.
8.
Warum sollten Sie in einer Java-switch-Verzweigung immer alle Blöcke für die einzelnen Fälle mit break abschließen?
9.
Was ist der Unterschied zwischen einer Funktion und einer Prozedur?
10. Schreiben Sie eine Java-Funktion, die ermittelt, ob eine Zahl eine
Primzahl ist. 11. Wo sollten Sie die Funktion zur Primzahlenermittlung idealerweise
speichern? 12. Wie lange lebt eine Variable, die in einer Funktion deklariert ist? 13. Warum sollten Sie wenn möglich auf globale Variablen verzichten?
337
Sandini Bib
Sandini Bib
6
Objektorientierte Programmierung
Sie lernen in diesem Kapitel:
le
n e rn
• was objektorientierte Programmierung eigentlich ist, • was die objektorientierte Programmierung von der klassischen strukturierten Programmierung unterscheidet, • welche Vorteile die objektorientierte Programmierung bietet, • wie Sie eigene Klassen mit einfachen Eigenschaften und Methoden erzeugen und anwenden, • was Konstruktoren und Destruktoren sind, • wie Sie das Konzept der Datenkapselung anwenden und • was Vererbung bedeutet. Wenn Sie diesem Buch gefolgt sind, haben Sie bisher mehr oder weniger klassisch strukturiert programmiert. In diesem Kapitel treten Sie nun in die Welt der Objekte ein. Fast jede moderne Programmiersprache besitzt Möglichkeiten zur Erzeugung und Anwendung eigener Klassen (die zur Erzeugung von Objekten benötigt werden) und zur Umsetzung der Konzepte der objektorientierten Programmierung (OOP). Mit einigen Programmiersprachen wie Java, C# und Smalltalk können Sie sogar nur noch objektorientiert programmieren (obwohl diese Sprachen über Klassen mit statischen Elementen, die Sie bereits im vorhergehenden Kapitel angewendet haben, auch die herkömmliche, strukturierte Programmierung unterstützen). Andere Sprachen wie C++, Visual Basic und Object Pascal erlauben neben der OOP auch die klassische strukturierte Programmierung. Ohne OOP kommt allerdings heutzutage keine Sprache mehr aus. Ausgenommen sind wohl einfache Scripting-Sprachen wie JavaScript, die meist nur eine sehr eingeschränkte OOP-Unterstützung bieten. In diesem Kapitel erfahren Sie zunächst, was ein Objekt ist und was die objektorientierte Programmierung von der strukturierten unterscheidet. Wenn Sie dann wissen, welche Vorteile die OOP hat, lernen Sie, wie
339
Sandini Bib
Sie eigene Klassen deklarieren und anwenden. Danach erfahren Sie, wie Sie Objekte direkt bei deren Erzeugung über Konstruktoren initialisieren und wie Sie Destruktoren einsetzen, um automatisch Aufräumarbeiten auszuführen, die bei der Zerstörung eines Objekts notwendig sind. Wenn Sie diese Grundkonzepte dann kennen, lernen Sie das wichtige Konzept der Datenkapselung kennen, über das Objekte den Zugriff auf Ihre Daten selbstständig kontrollieren. Um das Ganze abzurunden, erfahren Sie dann auch noch, wie Sie neue Klassen von bereits vorhandenen Klassen ableiten, um deren Eigenschaften und Methoden zu erben und wiederzuverwenden. Ich kann die OOP in diesem Kapitel nur grundlegend behandeln. Komplexe Konzepte lasse ich außen vor. Wenn Sie mehr wissen wollen, können Sie den Artikel „OOP-Grundlagen“ lesen, den Sie auf der Buch-CD im Ordner Dokumente finden.
6.1
Was ist objektorientierte Programmierung?
Die objektorientierte Programmierung (OOP) ist die moderne Form der Programmierung, die strukturierte Programmierung die klassische Form. Bei beiden arbeiten Sie mit den Techniken, die Sie in den vorhergehenden Kapiteln kennen gelernt haben, wovon allerdings die Module, die Sie erzeugt haben, ausgenommen sind (denn hier beginnen die Unterschiede). Die Grundprinzipien der strukturierten Programmierung (Datentypen, Variablen, Anweisungen, arithmetische und Vergleichsausdrücke, Schleifen und Verzweigungen) wenden Sie auch bei der OOP an. Also können Sie erst einmal aufatmen: Alles was Sie bisher gelernt haben, hat seinen Sinn. Der Begriff „strukturierte Programmierung“, der für die klassische Form der Programmierung im Allgemeinen verwendet wird, ist vielleicht etwas irreführend. OOP-Programme arbeiten mit denselben Grundtechniken zur Strukturierung eines Programms, sind also an der Basis auch strukturiert. OOP-Programme gehen aber bei der Wiederverwendung von Programmteilen einen anderen Weg und setzen dazu keine Module mit Prozeduren, Funktionen und Variablen, sondern Klassen ein. Die dadurch entstehenden Unterschiede beschreibe ich auf den folgenden Seiten.
340
Sandini Bib
6.1.1 Unterschiede zur strukturierten Programmierung Die OOP unterscheidet einiges von der klassischen strukturierten Programmierung. Den wichtigsten Unterschied (oder vielleicht den maßgeblichen) kläre ich gleich. Einige weitere Unterschiede betreffen aber komplexere Konzepte, wie das der Vererbung und des Polymorphismus, die in der strukturierten Programmierung nicht möglich sind. Diese Konzepte kann ich in diesem Buch nicht bzw. nur ansatzweise erläutern. Grundsätzlich liegen die Unterschiede zwischen der strukturierten und der objektorientierten Programmierung in der Wiederverwendung von Programmcode. Bei der strukturierten Programmierung setzen Sie dazu Module mit Funktionen, Prozeduren und globalen Variablen ein. Solche Module werden bei der OOP nicht verwendet. OOP-Programme arbeiten mit Klassen. Vielleicht fragen Sie sich nun, wo denn da der Unterschied liegt. Sie haben in Kapitel 5 ja bereits Klassen mit Java programmiert. Und diese haben sich eigentlich nicht weiter von den Units in Delphi und Kylix unterschieden. Und da haben Sie sogar Recht. Diese Klassen unterscheiden sich auch nicht von den Modulen der strukturierten Programmierung. Denn das sind Klassen mit so genannten statischen Elementen. Solche Elemente stellen Java und andere Sprachen zur Verfügung, damit Sie die Module der strukturierten Programmierung imitieren können. Richtige Klassen werden anders verwendet. Diese Klassen setzen Sie ein, um daraus Objekte zu erzeugen, mit denen Sie dann im Programm arbeiten. Um dies einmal zu demonstrieren, deklariert der folgende Java-Quellcode eine normale Klasse, die zur Speicherung von Personendaten verwendet werden soll. Diese Klasse können Sie in einer Konsolenanwendung oberhalb der Klasse deklarieren, die die main-Methode enthält. 01 class Person 02 { 03 /* Eigenschaften zur Verwaltung der Personendaten */ 04 String Vorname; 05 String Nachname; 06 07 /* Methode zur Rückgabe des vollen Namens */ 08 String VollerName() 09 { 10 return Vorname + " " + Nachname; 11 } 12 }
:DV LVW REMHNWRULHQWLHUWH 3URJUDPPLHUXQJ"
341
Sandini Bib
Wie Sie sehen, unterscheidet sich diese Deklaration kaum von der, die Sie bereits prinzipiell aus Kapitel 5 kennen. Lediglich das Schlüsselwort static fehlt. Die „Variablen“ Vorname und Nachname scheinen global zu sein. Die Funktion VollerName kann scheinbar, weil sie mit public deklariert ist, vom gesamten Projekt aus aufgerufen werden. Aber das ist nicht der Fall. Wenn Sie versuchen, im Programm ein Element der Klasse zu verwenden: 13 class EinfacheKlassen 14 { 15 public static void main(String args[]) 16 { 17 Person.Vorname = "Zaphod"; 18 } 19 }
meldet der Compiler den Fehler „non-static variable Vorname cannot be referenced from a static context“: Die nicht statische Variable Vorname kann nicht in diesem Kontext verwendet werden. Instanzen
Eine solche normale Klasse ist quasi nur noch ein Bauplan für Objekte. Um mit dieser Klasse zu arbeiten, müssen Sie eine Instanz der Klasse erzeugen: 13 class EinfacheKlassen 14 { 15 public static void main(String args[]) 16 { 17 /* Instanz (Objekt) erzeugen */ 18 Person p1 = new Person(); 19 20 /* Mit dem Objekt arbeiten */ 21 p1.Vorname = "Zahphod"; 22 p1.Nachname = "Beeblebrox"; 23 System.out.println(p1.VollerName()); 24 } 25 }
Die Instanz einer Klasse wird üblicherweise als Objekt bezeichnet. Der Begriff Instanz ist für die OOP im deutschen Sprachraum etwas unklar. Für viele von uns (und für den Duden) ist eine Instanz eine „zuständige Stelle bei Behörden und Gerichten“. Das englische Instance (Beispiel, Fall, Gelegenheit, Umstand, Vorgang ) ist zwar auch nicht eindeutig, aber etwas deutlicher. Nehmen Sie den Begriff also, wie er in der OOP verwendet wird: Ein konkretes Objekt ist eine Instanz einer Klasse.
342
Sandini Bib
Wo liegen denn nun die Vorteile bzw. die Unterschiede zu den Klassen aus Kapitel 5? Die Antwort auf diese Frage ist etwas komplexer und wird auf den folgenden Seiten geklärt. Ein wesentlicher Vorteil ist, dass Sie so viele Objekte erzeugen können, wie Sie benötigen: 13 class EinfacheKlassen 14 { 15 public static void main(String args[]) 16 { 17 /* Eine Instanz der Klasse erzeugen und initialisieren */ 18 Person p1 = new Person(); 19 p1.Vorname = "Zahphod"; 20 p1.Nachname = "Beeblebrox"; 21 22 /* Eine weitere Instanz erzeugen und initialisieren */ 23 Person p2 = new Person(); 24 p2.Vorname = "Tricia"; 25 p2.Nachname = "McMillan"; 26 27 /* Noch eine Instanz erzeugen und initialisieren */ 28 Person p3 = new Person(); 29 p3.Vorname = "Ford"; 30 p3.Nachname = "Prefect"; 31 32 /* Für jede Instanz die Methode VollerName aufrufen */ 33 System.out.println(p1.VollerName()); 34 System.out.println(p2.VollerName()); 35 System.out.println(p3.VollerName()); 36 } 37 }
Jede Instanz einer Klasse ist ein eigenständiges Objekt, das seine eigenen Daten verwaltet. Wenn Sie das Beispiel nachvollziehen, erkennen Sie dies daran, dass die Methode VollerName für p1 "Zaphod Beeblebrox", für p2 "Tricia McMillan" und für p3 "Ford Prefect" zurückgibt. Das ist der große Unterschied: Objekte verwalten ihre eigenen Daten, die von den Daten anderer Objekte unabhängig sind. Abbildung 6.1 zeigt, wie das im Arbeitsspeicher aussieht.
:DV LVW REMHNWRULHQWLHUWH 3URJUDPPLHUXQJ"
Objekte sind eigenständig
343
Sandini Bib
Instanz 1 Instanz 2 Zaphod Beeblebrox
Tricia McMillan
Instanz 3
Ford Prefect
Klasse Person public String VollerName() { return Vorname + " " + Nachname; }
Abbildung 6.1: Drei Instanzen einer Klasse Methoden werden nicht in den Objekten gespeichert
Abbildung 6.1 zeigt auch, dass die Methoden der Klasse nicht in den Objekten verwaltet werden. Methoden sind im Arbeitsspeicher – wenn das Programm ausgeführt wird – genau wie Funktionen und Prozeduren einfacher kompilierter Programmcode. Würde ein Programm in jeder Instanz einer Klasse nicht nur die Daten, sondern auch die Methoden verwalten, würde das viel zu viel Speicherplatz kosten. Da die Methoden für alle Instanzen einer Klasse dieselben sind, werden diese deshalb nur ein einziges Mal gespeichert. Wenn Sie im Programm eine Methode eines Objekts aufrufen, weiß das Programm, zu welcher Klasse das Objekt gehört, weil die Variable, mit der Sie arbeiten, den Datentyp dieser Klasse besitzt. Damit kann das Programm den Speicherort lokalisieren, in dem die Methoden verwaltet werden, und diese aufrufen. Diese Interaktion zwischen Objekten und den Methoden ihrer Klasse symbolisieren die gestrichelten Linien in Abbildung 6.1. Eine Frage, die Sie sich jetzt vielleicht stellen, nämlich die, woher denn eine Methode weiß, welche Instanz sie bearbeiten soll, kläre ich später auf Seite 380, nachdem Sie erfahren haben, was Referenzen sind.
344
Sandini Bib
Mit den Modulen der klassischen strukturierten Programmierung ist eine solche getrennte Speicherung von Daten prinzipiell nicht möglich. Wenn Sie in einem klassischen Modul globale Variablen zur Speicherung von Daten (z. B. eines Vor- und eines Nachnamens) verwalten, gibt es nur genau eine Instanz dieser Daten (die vom Compiler automatisch erzeugt wird). Wenn Sie hingegen mit echten Klassen arbeiten, können Sie so viele Instanzen erzeugen, wie Sie benötigen, und jede dieser Instanzen ist von den anderen unabhängig. Trotzdem haben alle Instanzen einer Klasse eines gemeinsam: Sie besitzen den gleichen Satz an Eigenschaften und Methoden. Sie gehören eben einer bestimmten Klasse an. Genauso, wie Sie und ich der Klasse Mensch angehören.
6.1.2 Was ist denn nun ein Objekt? Objekte kennen Sie bereits, seit Sie in Ihrer Kindheit gelernt haben, Dinge voneinander zu unterscheiden. Jeden Tag sind Sie von Objekten umgeben, arbeiten damit und nutzen deren Fähigkeiten. Für Sie ist es kein Problem, ein Handy vom Typ Nokia 8310 eindeutig der Klasse Nokia 8310 zuzuordnen (vorausgesetzt natürlich, Sie kennen diese Handy-Klasse), dessen Eigenschaften zu erkennen (z. B. die Farbe des Covers) und dessen Methoden zu benutzen (z. B. über die Zahltasten eine Nummer einzugeben und diese über die Wahltaste zu wählen). Für Sie ist es auch sonnenklar, dass alle Handys vom Typ Nokia 8310 dieselben Eigenschaften und Methoden besitzen. Und Sie erkennen auch, dass die Eigenschaften schon einmal anders ausgeprägt sein können. Das Cover eines solchen Handys kann ja eine Vielzahl an verschiedenen Farben aufweisen. Die Eigenschaft Coverfarbe ist quasi bei den einzelnen Instanzen der Klasse Nokia 8310 unterschiedlich eingestellt.
Objekte in der realen Welt
Objekte sind Ihnen sehr vertraut, weil Sie jeden Tag mit Objekten arbeiten. Wenn Sie Ihr Auto starten, nutzen Sie eine Methode des Autos. Wenn Sie Gas geben, nutzen Sie eine weitere Methode. Wenn Ihnen Ihr Auto nicht mehr gefällt, lassen Sie vielleicht von einem Lackierer die Eigenschaft Lackfarbe ändern. Wenn das Auto zu langsam ist, gehen Sie zu einem Tuner und lassen die Eigenschaft Leistung erhöhen. Bei der objektorientierten Programmierung arbeiten Sie auch mit Objekten, und diese unterscheiden sich nur dadurch von realen Objekten, dass sie virtuell sind, also nicht wirklich existieren und nicht berührt werden können. Ansonsten sind reale Objekte und Objekte in Programmen aber eigentlich identisch: Beide Varianten besitzen Eigenschaften und Methoden.
:DV LVW REMHNWRULHQWLHUWH 3URJUDPPLHUXQJ"
345
Sandini Bib
Eigenschaften beeinflussen das Aussehen oder das Verhalten
Methoden erlauben die Arbeit mit Objekten
Objektorientierte Denkweise
Einige Eigenschaften bestimmen, wie Sie nun wahrscheinlich bereits erkannt haben, das Aussehen eines Objekts. Deutlich erkennbar ist das am Beispiel der Eigenschaft Coverfarbe eines Handys oder der Eigenschaft Lackfarbe eines Autos. Im Computer gilt das natürlich nur für Objekte, die auf dem Bildschirm oder auf einem anderen Ausgabemedium sichtbar sind. Aber es gibt auch Eigenschaften, die nicht das Aussehen, sondern das Verhalten beeinflussen. Bei einem Auto wäre dies z. B. die Eigenschaft Motorleistung. Diese Eigenschaft bestimmt, wie stark das Auto beschleunigt und wie schnell es fahren kann. Dabei dürfen Sie Eigenschaften allerdings immer nur für die jeweilige Klasse betrachten. Ein Porsche 911 beschleunigt mit 400 PS Motorleistung besser als mit 250 PS. Ein LKW vom Typ Mercedes-Benz Actros beschleunigt mit seinen 313 PS wesentlich schlechter als ein Porsche. Aber dabei handelt es sich ja auch um eine andere Klasse. Über Methoden können Sie ein Objekt für sich arbeiten lassen. Wenn Sie eine Methode aufrufen, fordern Sie das Objekt auf, einen Job für Sie auszuführen. Wenn Sie die Wählen-Methode eines Handys aufrufen, erwarten Sie, dass das Handy die eingegebene Telefonnummer anruft. Wenn Sie die Starten-Methode eines Autos aufrufen, erwarten Sie, dass das Auto startet, also die Benzinpumpe mit Strom versorgt und die Einspritzung und den Anlasser einschaltet (und noch einige andere Dinge macht, die ich jetzt vergessen habe). Möglicherweise wissen Sie gar nicht, was eine Einspritzung oder eine Benzinpumpe ist und dass diese beim Starten eines Autos mit einbezogen werden. Vielleicht wollen Sie gar nicht wissen, was alles im Hintergrund passiert, wenn Sie ein Auto starten oder mit Ihrem Handy einen Freund oder eine Freundin anrufen. Und das ist auch gut so. Denn das ist die objektorientierte Denkweise, die auch beim Programmieren wichtig ist. Sie lassen das Auto oder das Handy die Arbeit machen und interessieren sich nicht dafür, was alles im Hintergrund passiert. Wenn Sie in einem Programm beispielsweise mit einem Druck-Objekt arbeiten und über eine Methode dieses Objekts Daten ausdrucken, ist es für Sie vollkommen unerheblich, was da alles im Hintergrund passiert. Sie nutzen die Möglichkeiten des Objekts über dessen Eigenschaften und Methoden. Um mit einem Druck-Objekt zu drucken, müssen Sie vielleicht nur die Eigenschaft Druckername einstellen und die Methoden Drucken und NeueSeite aufrufen. Dass das Objekt dabei intern vielleicht komplizierte Funktionen des Betriebssystems nutzt, ist für Sie nicht wichtig. Das Objekt macht seinen Job. Und das reicht. Natürlich muss irgendjemand die Klassen entwickeln, aus denen später Objekte erzeugt werden. Diese Arbeit ist häufig wesentlich schwieriger als die spätere Benutzung der daraus erzeugten Objekte. Bei der Pro-
346
Sandini Bib
grammierung einer Klasse sollte der Entwickler dafür sorgen, dass die später daraus erzeugten Objekte möglichst einfach anzuwenden sind und möglichst alle Anforderungen erfüllen. Wenn die Klasse aber einmal fertig gestellt ist und angewendet werden kann, hat sich die ganze Arbeit sehr schnell bezahlt gemacht.
6.2
Welche Vorteile bietet die OOP?
In vielen Programmiersprachen können Sie sich zwischen der klassischen und der objektorientierten Programmierung entscheiden. Die meisten modernen Programme werden aber objektorientiert entwickelt. Die OOP muss also einige Vorteile bieten. Diese Vorteile will ich hier kurz klären. Damit Sie die Vorteile besser verstehen, zeige ich aber zunächst die Probleme der strukturierten Programmierung auf.
6.2.1 Die Probleme der strukturierten Programmierung Bei der strukturierten Programmierung setzen Sie Prozeduren und Funktionen ein, die Sie in Modulen organisieren. Eine wohl überlegte Organisation Ihrer Module sorgt für eine gute Wiederverwendbarkeit, Wartbarkeit und Erweiterbarkeit Ihrer Programme. Trotzdem leidet die strukturierte Programmierung unter einigen Problemen, die besonders bei großen Programmen auftreten. Funktionen ohne Kontext Der Einsatz von Modulen für einzelne Programmteile macht ein Programm übersichtlicher und die einzelnen Programmteile (in demselben Projekt oder in anderen Projekten) wiederverwendbar. Eine Aufteilung der Aufgaben eines Moduls in mehrere Funktionen verbessert die Übersichtlichkeit und die Wiederverwendbarkeit und macht ein Programm besser wartbar. Manche Module enthalten nur sehr wenige öffentliche Funktionen, die von außen aufgerufen werden können. Diese Funktionen nutzen aber recht häufig weitere, allerdings private Funktionen, die von außen nicht aufgerufen werden können. Eine Anwendung zur Abwicklung von Bestellungen kann z. B. ein Modul für die Arbeit mit Lieferscheinen enthalten, das u. a. eine öffentliche Funktion LieferscheinDrucken besitzt. Diese Funktion muss beim Drucken für jede neue Seite einen Seitenfuß und einen Seitenkopf drucken. Der Seitenfuß und der Seitenkopf werden von jeweils einer privaten Funktion gedruckt, die in demselben Modul gespeichert sind. Dieses
347
Sandini Bib
Aufteilen in einzelne Funktionen führt dazu, dass ein Modul leichter wartbar, wiederverwendbarer und übersichtlicher wird. Das sind alles Anforderungen, die an ein qualitativ hochwertiges Programm gestellt werden. Globale Funktionen
Private Funktionen führen nicht zu Problemen. Ein Programmierer, der ein Modul in seinen Programmen verwendet, kann private Funktionen nicht aufrufen. Besonders interessant ist das bei Modulen, die lediglich in kompilierter Form vorliegen (bei Delphi und Kylix als .dcu-Datei, bei Java als .class-Datei). In Programmen besteht aber auch sehr häufig Bedarf an globalen (öffentlichen) Funktionen, die von verschiedenen Teilen des Programms aus aufgerufen werden können. Eine Bestellverwaltungs-Anwendung muss z. B. an verschiedenen Stellen Lieferscheine, Rechnungen, Artikellisten und Personallisten drucken. In einem gut modularisierten Programm würden die entsprechenden Funktionen in verschiedenen Modulen gespeichert sein. Ein Modul ist für den Lieferscheindruck zuständig, ein anderes für den Rechnungsdruck usw. Die einzelnen Funktionen dieser Module müssen einzelne Textzeilen, Linien, evtl. Kreise und vielleicht auch Bilder ausdrucken. Die Funktionen, die ein Betriebssystem direkt zum Drucken bereitstellt, sind aber sehr kompliziert. Falls die Programmiersprache, mit der das Programm entwickelt wird, keine einfach zu handhabenden und ausreichend flexiblen Druckfunktionen anbietet, und wenn keine Bibliothek mit Druckfunktionen verfügbar ist, müssen diese eben selbst programmiert werden. Zu diesen Funktionen gehören dann solche zur Ausgabe von Text, zum Zeichnen von Linien, zur Ausgabe von Bildern usw. Diese Funktionen würden natürlich in einem separaten Modul gespeichert und öffentlich deklariert sein, damit sie in den verschiedenen Modulen zum Drucken von Lieferscheinen, Rechnungen usw. verwendet werden können.
Das Problem, die richtige Funktion zu finden
348
Größere Programme enthalten nun häufig sehr viele globale Funktionen. Für den Programmierer, der ein Programm unter Benutzung vorgefertigter Module entwickelt, oder den Programmierer, der ein bereits vorhandenes Programm weiterentwickeln soll, ist es nun häufig ein großes Problem, herauszufinden, welche Funktionen er im Kontext des Programmteils, an dem er gerade arbeitet, sinnvoll und sicher verwenden kann. Die modulare Programmierung hilft dabei ein wenig dadurch, dass Funktionen, die denselben Kontext besitzen, in einem separaten Modul zusammengefasst sind. Aber das Herausfinden, welche Funktionen in einem Modul gespeichert sind, ist häufig mit einiger Arbeit verbunden. Viele Programmierer behelfen sich damit, dass der Name zusammengehöriger Funktionen mit einem bestimmten Präfix beginnt. So könnte der Name von Druckfunktionen z. B. mit „druck_“
Sandini Bib
beginnen. Die meisten Entwicklungsumgebungen bieten Tools zur Anzeige der verfügbaren Funktionen an. Mit Hilfe einer Namenskonvention und dieser Tools können Sie Funktionen, die in Zusammenhang stehen, recht schnell finden. Auch, wenn eine Namenskonvention in der Praxis eher umständlich zu handhaben ist. Abgesehen davon, dass es manchmal schwierig ist, die richtige Funktion in einem Modul zu finden, sind Sie beim Programmieren aber nicht auf die in Zusammenhang stehenden Funktionen eingeschränkt. Beim Entwickeln eines Moduls zum Drucken von Lieferscheinen können Sie neben den eigentlichen Druckfunktionen (TextDrucken, KreisDrucken etc.) z. B. auch – aus Versehen – eine Funktion TextSenden aufrufen, die in einem ganz anderen Modul gespeichert ist und die leider nicht druckt, sondern den übergebenen Text an die serielle Schnittstelle sendet. Dieses Beispiel mag etwas konstruiert sein, stellt aber das Problem recht gut dar. Diese Möglichkeit, an jeder Stelle des Programms alle globalen Funktionen aufrufen zu können, kann zu schweren Fehlern führen: Sie denken, Sie haben die richtige Funktion aufgerufen, verwenden aber eine, die in einem falschen Kontext steht. Die Zuordnung der für den aktuellen Kontext verwendbaren Funktionen ist für Sie also schwierig und fehlerträchtig. Und dieses Problem wird deutlicher, wenn mehrere Programmierer gleichzeitig an einem großen Software-Projekt arbeiten oder wenn Sie Module verwenden, die von anderen Programmierern entwickelt wurden. Ungeschützter Zugriff auf globale Daten Dass bei der strukturierten Programmierung zusammengehörige Funktionen nicht in einem Kontext verwaltet werden, ist bereits ein Problem. Ein viel größeres Problem ist aber, dass der Zugriff auf globale Daten nicht geschützt ist. Obwohl bei der strukturierten Programmierung immer versucht wird, möglichst keine globalen Daten zu verwenden, lassen sich diese manchmal nicht vermeiden. Die im vorigen Abschnitt genannten Druckfunktionen sind ein gutes Beispiel dafür. Funktionen zum Drucken sollten so flexibel sein, dass neben den auszudruckenden Daten mindestens auch der Drucker angegeben werden kann, auf dem ausgedruckt werden soll. Daneben sollten auch die Ränder definiert werden können, die beim Drucken berücksichtigt werden sollen. Jede der einzelnen Druckfunktionen muss nun wissen, auf welchem Drucker gedruckt wird und wie die Druckränder eingestellt sind. Diese Funktionen könnten dazu einfach mit Argumenten ausgestattet werden. Dummerweise müssen diese Argumente dann aber bei jedem Aufruf mit übergeben werden. In der Praxis kann das recht schnell nervig werden. Um sich die Arbeit zu erleichtern, verwalten viele Programmierer solche
349
Sandini Bib
Einstellungen einfach in globalen Variablen. So müssen die Einstellungen nur einmal vor dem Ausdrucken vorgenommen werden. Alle Programmteile können auf globale Daten zugreifen
Das Problem ist nun, dass diese Variablen nicht nur für die Druckfunktionen global sind, sondern für das gesamte Programm. Angenommen der Ausdruck eines Lieferscheins ist sehr kompliziert und besteht aus sehr vielen einzelnen Funktionsaufrufen. Dann kann es sein, dass mittendrin irgendeine, vielleicht untergeordnete Funktion den Wert einer dieser globalen Variablen ändert. Das Programm läuft dann fehlerhaft weiter. Der zugrunde liegende Fehler ist in solchen Fällen nur sehr schwer zu finden. Dieser Nachteil ist für Sie eventuell schwer nachvollziehbar, weil Sie selbst noch nicht viel programmiert haben. In der Praxis ist es aber häufig so, dass ab einer bestimmten Programmgröße niemand mehr sagen kann, welche Funktion welche globale Daten verwendet. Eigentlich ist in strukturierten Programmen alles irgendwie miteinander verknüpft. Jede Funktion kann auf alle globalen Daten zugreifen und diese verändern. Die Fehler, die dadurch entstehen, führen oft zu nächtelangen Fehlersuchen und lassen Programmierer sehr vorsichtig werden, wenn es darum geht, strukturierte Programme nachträglich zu verändern. In großen Programmen weiß häufig niemand mehr, was alles passiert, wenn ein neuer Programmteil geschrieben wird, dessen Funktionen irgendwelche globale Daten verändern. Möglicherweise läuft dann zwar der neu geschriebene Programmteil fehlerfrei, irgendein anderer aber nicht mehr.
6.2.2 Die Lösung der Probleme der strukturierten Programmierung durch die OOP Die OOP löst natürlich die Probleme, die ich im vorigen Abschnitt genannt hatte. Das Problem mit dem Kontext von Funktionen existiert nicht mehr, weil es keine globalen Funktionen mehr gibt. Ein Objekt zum Lieferschein-Druck besitzt Eigenschaften und Methoden. Wenn Sie mit einem solchen Objekt arbeiten, wissen Sie immer ganz genau, welche Daten Sie im Kontext „Lieferschein drucken“ verwenden und welche Methoden Sie dabei aufrufen können. Andere Methoden als die, die das Objekt besitzt, sind (zunächst1) nicht möglich und normalerweise auch nicht sinnvoll. Das erleichtert die Programmierung ungemein, besonders dann, wenn Sie Objekte einsetzen, deren Klassen Sie nicht selbst programmiert haben. Sie müssen lediglich in der Dokumen1.
350
Zunächst deshalb, weil es auch möglich ist, ein Objekt an eine Methode eines anderen Objekts oder an eine statische Methode einer Klasse zu übergeben, die die Daten des Objekts dann weiterverarbeitet oder verändert.
Sandini Bib
tation der Klasse nachlesen, um zu erfahren, was das Objekt alles kann. In modernen Entwicklungsumgebungen wie Delphi und Kylix erhalten Sie sogar automatisch eine Liste der Eigenschaften und Methoden eines Objekts, wenn Sie den Punkt hinter einen Objektnamen schreiben. Da Sie wissen, dass Eigenschaften das Verhalten oder Aussehen beschreiben und dass Sie über Methoden mit dem Objekt arbeiten können, finden Sie so sehr schnell heraus, wie Sie Ihre Programmier-Probleme mit einem bestimmten Objekt lösen können. Das gilt natürlich nur dann, wenn das Objekt zu Ihren Problemen passt. Aber Sie werden wohl kaum ein Objekt zur Kommunikation über die serielle Schnittstelle verwenden, wenn Sie Texte auf dem Drucker ausgeben wollen. Das Problem der globalen Daten ist ebenfalls gelöst, ähnlich wie bei den globalen Funktionen: Mit Ausnahme von Klassen mit statischen Eigenschaften (die ja nur die strukturierte Programmierung simulieren) gibt es bei der OOP keine globalen Daten mehr. Jedes Objekt verwaltet seinen eigenen Satz an Daten. So kann es nicht ohne weiteres passieren, dass ein Programmteil versehentlich Daten überschreibt, die ein anderer Programmteil verwendet. Die OOP bietet aber neben der Lösung der Probleme der strukturierten Programmierung noch weitere Vorteile, die die folgenden Seiten klären.
6.2.3 Die weiteren Vorteile der OOP Neben der Lösung der Probleme der strukturierten Programmierung bietet die OOP noch einige weitere Vorteile, die teilweise mit den bereits genannten zusammenhängen und die ich hier nur kurz beschreibe: • Die OOP erleichtert die Programmierung. Objekte sind wesentlich einfacher anzuwenden als die recht zusammenhanglosen Funktionen und Prozeduren der strukturierten Programmierung. Wenn Sie ein Objekt einsetzen, wissen Sie nach der Erforschung seiner Eigenschaften und Methoden in der Regel recht genau, was das Objekt kann und was mit dem Objekt nicht möglich ist. Sie müssen nicht lange suchen, um eine Funktion oder Prozedur zu finden, die das macht, was Sie wollen, sondern nutzen einfach eine Methode des Objekts. Klassen werden zudem häufig so entwickelt, dass besondere, in bestimmten Situationen hilfreiche Methoden enthalten sind. Die Person-Klasse aus dem Beispiel von Seite 341 enthält z. B. eine Methode VollerName, die eigentlich nicht notwendig ist (weil der volle Name ja auch im Programm aus dem Vor- und dem Nachnamen gebildet werden kann). Diese Methode erleichtert aber den Umgang mit Person-Objekten ungemein. Der volle Name einer Person muss in Programmen, die mit Personendaten arbeiten,
351
Sandini Bib
häufiger ausgegeben werden. Es ist enorm hilfreich, wenn Sie dazu lediglich eine einfache Methode aufrufen müssen. Bei der strukturierten Programmierung wäre so etwas kaum möglich. Sie könnten dort zwar auch Daten in Strukturen (siehe Kapitel 4) strukturiert speichern. Da diese aber keine Methoden besitzen können (dann wären es ja Klassen), fehlt die enorme Arbeitserleichterung, die Objekte bieten. • OOP-Programme sind weniger fehleranfällig. Weil Objekte ihre Daten selbst verwalten und es (idealerweise) keine globalen Daten mehr gibt, können Sie im Programm nicht ohne weiteres versehentlich Daten überschreiben, die in anderen Programmteilen verwendet werden, was bei strukturierten Programmen eine sehr häufige Fehlerquelle ist. Um dies am Beispiel zu erläutern, verwende ich das Zähl-Modul des vorhergehenden Kapitels. Angenommen, Sie erweitern ein Programm, das mit diesem Modul arbeitet, sodass der Pförtner zwei Eingänge zu verschiedenen Teilen des Firmengeländes verwalten kann. Der neue Programmteil arbeitet nun aber mit denselben globalen Variablen wie der alte. Schon läuft das Programm fehlerhaft. Registriert der Pförtner einen Eingang auf dem einen Geländeteil, wird die Anzahl auch für den anderen Geländeteil erhöht. Würde das Zählen aber über zwei voneinander unabhängige Instanzen einer Zähl-Klasse realisiert, wäre dieser Fehler gar nicht möglich, weil jede Instanz ihre eigenen Daten verwaltet. • Die OOP verbessert die Wiederverwendung von Programmcode Wie Sie ja wahrscheinlich schon gemerkt haben, ist die Wiederverwendung von Programmcode ein sehr wichtiges Thema bei der Programmierung. Lösungen, die Sie einmal entwickelt haben, wollen Sie ja nicht noch einmal komplett neu entwickeln, wenn Sie diese in ein und demselben oder in einem anderen Projekt noch einmal benötigen. Wenn Sie strukturiert programmieren, ist die Wiederverwendung aufgrund der Probleme der strukturierten Programmierung (Funktionen ohne Kontext, versehentliches Überschreiben globaler Daten) aber oft sehr schwierig. Klassen wiederzuverwenden ist dagegen deutlich einfacher. Zur Wiederverwendung gehört aber auch, dass Sie den Programmcode an Ihre Bedürfnisse anpassen können. Wenn Sie beispielsweise ein Modul mit Funktionen besitzen, mit denen Sie Textdaten über eine serielle Schnittstelle des Computers senden und empfangen können, wollen Sie dieses Modul vielleicht einmal so erweitern, dass die Daten nach einem bestimmten Protokoll2 gesendet und empfangen werden. Nun müssen Sie entscheiden, ob Sie das vorhandene Modul erweitern wollen oder ob Sie ein neues Modul erzeugen, das die Funktionen des alten benutzt. Im ers-
352
Sandini Bib
ten Fall wird das Modul nach einiger Zeit sehr unübersichtlich, wenn Sie zur Erweiterung neue Funktionen schreiben. Häufig werden Sie deshalb die alten Funktionen entsprechend ändern, wobei der Aufruf dann über zusätzliche Argumente gesteuert wird. Das führt aber fast zwangsläufig zu Fehlern in anderen Programmen oder Programmteilen, die die alte Version dieser Funktionen nutzen, weil Sie beim Programmieren der neuen Funktionalität nicht immer auf die Kompatibilität zu alten Programmen achten werden. Der zweite Fall ist aber auch nicht gerade optimal. Hier besitzen Sie zwei Module, eins mit den alten und den grundlegenden Funktionen und eins mit den neuen. Wenn Sie die neue Funktionalität nutzen wollen, müssen Sie immer beide Module einbinden. Einige Funktionen, die Sie dann nutzen, kommen aus dem alten, andere aus dem neuen Modul. Wenn Sie hingegen objektorientiert programmieren, ist diese Art der Wiederverwendung kein Problem. Die OOP stellt dazu das Konzept der Vererbung zur Verfügung. Wenn Sie eine Klasse mit einer bestimmten Grundfunktionalität besitzen und diese für Ihre eigenen Zwecke erweitern wollen, erzeugen Sie einfach eine neue Klasse, die Sie von der anderen Klasse ableiten (womit gemeint ist, dass Sie die andere Klasse bei der Deklaration der neuen Klasse als Basis angeben). Die neue Klasse erbt alle Eigenschaften und Methoden der Basisklasse und kann ohne Probleme erweitert werden. Dabei können Sie sogar geerbte Eigenschaften und Methoden mit einer neuen Implementierung versehen, sodass diese bei Objekten der neuen Klasse zu anderen Ergebnissen oder Reaktionen führen als bei Objekten der Basisklasse. Wenn Sie beispielsweise eine Klasse besitzen, die Eigenschaften und Methoden zur Kommunikation über die serielle Schnittstelle bereitstellt, können Sie mit Hilfe dieser Klasse sehr einfach eine neue erzeugen, die Sie dann nur um die benötigte neue Funktionalität erweitern. Dabei können Sie in der neuen Klasse auch auf Eigenschaften und Methoden der Basisklasse zurückgreifen. Eine Methode, die Daten nach einem bestimmten Protokoll sendet, würde beispielsweise die zu sendenden Daten in Pakete auftrennen und mit einer Checksumme versehen. Zum eigentlichen Senden würde diese Methode die geerbte Senden-Methode verwenden. Wenn Sie die neue Klasse einsetzen, merken Sie davon nichts. Für Sie sieht diese Klasse aus wie jede andere.
2.
Ein Protokoll beschreibt (im Computer), wie Daten gesendet und empfangen werden. Ein typisches Protokoll bestimmt z. B., dass Daten in einzelne Pakete zerlegt und mit einer berechneten Checksumme versehen werden. Das letzte Paket wird dabei häufig mit einem speziellen Ende-Zeichen gekennzeichnet. So kann der Empfänger die einzelnen Pakete daraufhin überprüfen, ob die Übertragung fehlerfrei war, und diese dann wieder zusammensetzen.
353
Sandini Bib
• OOP-Programme können die Realwelt besser abbilden. In den meisten Fällen ist ein Programmierer bestrebt, in seinen Programmen die Realwelt möglichst genau abzubilden. Damit erhöht er zum einen das Verständnis des Programms und erleichtert zum anderen auch dessen Benutzung. Da wir nun in der realen Welt jeden Tag mit Objekten arbeiten, ist es wesentlich einfacher, ein Programm zu verstehen, das mit Objekten arbeitet, die den Objekten der Realwelt ähnlich sind. Es ist einfach zu verstehen, wenn eine Rechnung, die für uns ja (wenigstens in ausgedruckter Form) ein Objekt darstellt, auch im Programm als Objekt verwaltet wird. Genauso einfach ist zu verstehen, wenn auch Kunden und Artikel als Objekte verwaltet werden. So können Sie beim Versuch, ein Programm nachzuvollziehen, Assoziationen mit der Realwelt knüpfen. Bei der strukturierten Programmierung fehlt hingegen häufig die offensichtliche Verbindung eines Programmteils zur Realität, weil dort mit mehr oder weniger zusammenhanglosen Daten und Funktionen gearbeitet wird. Die OOP kann aber noch mehr als nur die Abbildung von Objekten der Realwelt. Über die OOP können Sie auch die Beziehungen zwischen Objekten der Realwelt sehr gut abbilden. Wenn Sie beispielsweise ein Programm zur Erzeugung und Verwaltung des Stammbaums einer Familie programmieren wollen, können Sie dazu einzelne PersonenObjekte verwenden, die Sie direkt miteinander in Beziehung setzen. Das zu erläutern ist an dieser Stelle etwas verfrüht, aber eben ein wichtiger Vorteil. Ein Objekt kann nämlich Eigenschaften besitzen, die auf andere Objekte verweisen. Ein Person-Objekt kann z. B. je eine Eigenschaft Vater und Mutter besitzen, die die Objekte referenzieren, die den Vater bzw. die Mutter der Person repräsentieren. Genauso kann ein Person-Objekt auch eine Eigenschaft Kinder besitzen, über die die Kinder der Person referenziert werden. So können Sie ausgehend von einer bestimmten Person ganz einfach deren Vater, Mutter, Kinder, ja sogar deren Groß- und Urgroßeltern etc. ermitteln. Nur um zu zeigen, wie das im Programm aussehen würde, zeigt das folgende Beispiel die Ermittlung des Namens der Großmütter einer bestimmten Person in einem Stammbaum-Programm: grossmutter1 = p1.Mutter.Mutter.VollerName(); grossmutter2 = p1.Vater.Mutter.VollerName();
Das ist genau die Vorgehensweise, die wir auch in der Realität verwenden würden. Mit einem strukturierten Programm wäre so etwas nicht möglich. Sie könnten zwar auch einen Stammbaum abbilden, aber die Darstellung und Auflösung der Beziehungen zwischen den einzelnen Personen wäre weitaus schwieriger. Den Beweis dafür bleibe ich hier schuldig, weil dies eine recht komplexe Programmierung
354
Sandini Bib
erfordern würde. Probieren Sie es einfach aus, wenn Sie die dazu notwendigen Techniken beherrschen (vorwiegend sind das Arrays, die ich in Kapitel 7 beschreibe). • Die OOP erlaubt die Erzeugung neuer Datentypen. Wenn Sie in der Praxis programmieren, werden Sie schon einmal vor die Anforderung gestellt, neue Datentypen zu erzeugen. Stellen Sie sich vor, Sie müssen häufig mit XY-Koordinaten oder mit komplexen Zahlen3 arbeiten. Bei der strukturierten Programmierung haben Sie lediglich die Möglichkeit, für diese neuen Datentypen eine Struktur zu erzeugen. Die Struktur setzt sich aus einzelnen Basisdatentypen zusammen und ermöglicht so das gemeinsame Speichern von mehreren unterschiedlichen Daten. Ein großes Problem dabei stellen die Operationen dar, die Sie auf diesen neuen Datentypen ausführen wollen bzw. müssen. Das Problem beginnt schon dann, wenn Sie einfach nur die Werte einer Strukturvariable in eine andere kopieren oder einer Strukturvariablen Werte aufaddieren wollen. Die meisten Programmiersprachen lassen solche Operationen mit Strukturen nicht zu. Werden die Operationen dann komplizierter, wie das z. B. bei komplexen Zahlen der Fall ist, werden auch die Probleme größer. Bei der strukturierten Programmierung müssen Sie dazu separate Funktionen oder Prozeduren schreiben, die diese Arbeit übernehmen. Das Problem dabei ist wieder, dass diese Funktionen nicht im Kontext des neuen Datentyps stehen und damit schwierig anzuwenden sind. Wenn Sie die neuen Datentypen stattdessen in Klassen definieren, können Sie die Operationen auf diesen Datentypen gleich mit in die Klassen integrieren. In allen Programmiersprachen können Sie dazu zumindest Methoden entwickeln (z. B. eine Methode zum Addieren von einer XY-Koordinate auf eine andere). Einige Programmiersprachen wie C++ und C# erlauben aber auch das so genannte Überschreiben der Standardoperatoren (+, -. *. / etc.). Damit können Sie Operationen wie eine Addition für Ihre neuen Datentypen so umdefinieren, dass dabei ein korrektes Ergebnis herauskommt. Die Anwendung Ihrer neuen Datentypen ist mit diesen Operatoren sehr einfach und vor allen Dingen auch intuitiv, ganz anders als bei der strukturierten Programmierung. 3.
Eine komplexe Zahl besteht aus zwei Zahlteilen, einem so genannten Realteil und einem Imaginärteil. Komplexe Zahlen wurden entwickelt, weil sich einige Probleme in der Mathematik nicht mit den uns bekannten reellen Zahlen erklären lassen. Ein Beispielproblem dazu ist die Frage, welche Zahl ins Quadrat genommen -1 ergibt. Keine der uns bekannten Zahlen erfüllt diese Bedingung (wahrscheinlich, weil die Anzahl der Dezimalstellen viel zu groß ist). Mit Hilfe der komplexen Zahlen können Sie, auf sehr komplizierte Art und Weise, solche Probleme lösen. Eine gute Beschreibung der komplexen Zahlen finden Sie im Internet bei www.koopiworld.de/pub/komplex.htm.
355
Sandini Bib
6.3
Einfache Klassen und deren Anwendung
Nachdem Sie nun wissen, warum Sie überhaupt objektorientiert programmieren, zeige ich zunächst, wie Sie in Delphi, Kylix und Java eigene einfache Klassen erzeugen, daraus Objekte erzeugen und diese anwenden.
6.3.1 Klassen deklarieren Delphi und Kylix In Delphi und Kylix legen Sie Klassen in einer Unit an. Sie können mehrere Klassen in einer Unit deklarieren, was auch sehr häufig für zusammengehörige Klassen genutzt wird. So müssen Sie lediglich eine Unit einbinden, wenn Sie zusammengehörige Klassen verwenden wollen. Delphi und Kylix trennen die Deklaration einer Klasse von der Implementierung der Methoden. Als Deklaration wird hier das Schreiben des Rumpfes der Klasse bezeichnet, also der Angabe, welche Eigenschaften und Methoden die Klasse beinhaltet. Der Programmcode der Methoden gehört nicht zur Deklaration, sondern zur Implementierung. Soll eine Klasse öffentlich sein, d.h. von außerhalb der Unit verwendet werden können (was bei den meisten Klassen sinnvoll ist), muss diese im interface-Abschnitt der Unit deklariert werden. Wenn Sie die Klasse im implementation-Abschnitt deklarieren, ist sie privat und kann nur von anderen Klassen innerhalb der Unit verwendet werden. Private Klassen werden eher selten als Hilfs-Klassen zur Verwendung in den öffentlichen Klassen einer Unit eingesetzt. Die Methoden einer Klasse werden immer im implementation-Abschnitt separat von der Klasse implementiert. Eine Unit mit einer einfachen, öffentlichen Klasse zur Speicherung von Personendaten sieht in Delphi und Kylix so aus: 01 02 03 04 05 06 07 08 09 10
356
unit CPerson; interface { Deklaration der Klasse } type Person = class { Eigenschaften zur Speicherung des Vor- und des Nachnamens } Vorname: string; Nachname: String;
Sandini Bib
11 12 13 14 15 16 17 18 19 20 21 22 23
{ Methode, die den vollen Namen zurückgibt } function VollerName(): string; end; implementation { Implementierung der Methoden der Klasse } function Person.VollerName: string; begin result := Vorname + ' ' + Nachname; end; end.
Ich habe die Unit CPerson genannt, weil Delphi und Kylix es nicht zulassen, dass der Dateiname einer Unit derselbe ist wie der Name einer Klasse. Das C im Dateinamen steht dann einfach für Class. Die Deklaration der Klasse beginnt mit dem type-Schlüsselwort, das in Delphi und Kylix zur Deklaration neuer Typen verwendet wird. Strukturen werden beispielsweise auch mit type deklariert. Danach folgt der Name der neuen Klasse. Der Name der Klasse sollte eindeutig und aussagekräftig sein. Verwenden Sie keine Namenskonventionen zur Benennung der Klasse, da dies im Allgemeinen nicht üblich ist (obwohl Borland dem Namen eigener Klassen häufig ein T voranstellt, das für Typ steht). Innerhalb der Klasse werden Eigenschaften ähnlich Variablen deklariert (Zeile 8 und 9), lediglich ohne das Schlüsselwort var. Die Datentypen von Eigenschaften können aber dieselben sein wie die von Variablen. Methoden (Zeile 12) werden genau wie Funktionen oder Prozeduren deklariert, allerdings fehlt hier die Implementierung. Diese nehmen Sie dann im implementation-Abschnitt vor (Zeile 18 bis 21). Der einzige Unterschied zu Funktionen bzw. Prozeduren ist, dass Sie hier den Klassennamen als Präfix vor den Methodennamen setzen müssen, damit der Compiler weiß, zu welcher Klasse diese Implementierung gehört. Das ist deshalb notwendig, weil Sie auch mehrere Klassen in einer Unit deklarieren können. Innerhalb der Methoden der Klasse können Sie auf alle Eigenschaften und auf die anderen Methoden der Klasse zugreifen, einfach indem Sie deren Namen verwenden. Das Beispiel liest die Eigenschaften Vorname und Nachname aus und formt daraus einen vollen Namen. Die Trennung der Deklaration der Methoden von deren Implementierung ist in Delphi und Kylix einmal wieder (ähnlich wie bei Funktionen
357
Sandini Bib
und Prozeduren) etwas umständlich. Sie müssen die Deklaration immer wieder anpassen, wenn Sie die Argumente oder den Rückgabetyp einer Methode ändern. Aber leider gibt es nur diese eine Möglichkeit. Java In Java können Sie Klassen in einer separaten .java-Datei oder in der Startdatei der Anwendung deklarieren. Da es aufgrund der Wiederverwendbarkeit unüblich ist, dazu die Startdatei zu verwenden, deklariere ich Klassen immer in einer separaten Datei. Wie bei Delphi und Kylix können Sie mehrere Klassen in einer Datei deklarieren. Diese Datei muss aber nicht anders benannt sein als die enthaltenen Klassen. Sie können die Datei jedoch – anders als bei der Startklasse – auch anders benennen, was im Besonderen dann unumgänglich ist, wenn Sie mehrere Klassen in einer Datei verwalten. Die Deklaration einer einfachen Klasse zur Speicherung von Personendaten sieht in Java so aus: 01 class Person 02 { 03 /* Eigenschaften zur Speicherung des Vor- und des Nachnamens */ 04 String Vorname; 05 String Nachname; 06 07 /* Methode, die den vollen Namen zurückgibt */ 08 String VollerName() 09 { 10 return Vorname + " " + Nachname; 11 } 12 }
Eigenschaften werden (wie eigentlich in allen Programmiersprachen) wie Variablen deklariert, Methoden wie Funktionen. Innerhalb von Methoden können Sie auf die Eigenschaften und die anderen Methoden der Klasse zugreifen. Öffentliche Klassen und Pakete
Java unterstützt keine privaten Klassen, die wie bei Delphi und Kylix nur in der Datei gelten, in der sie deklariert sind. Wenn Sie eine Klasse so deklarieren wie im obigen Beispiel, ist diese öffentlich, was allerdings nur für das aktuelle Paket gilt. Das Ganze ist leider ein wenig komplex, weswegen ich in diesem Buch auf die Deklaration eigener Pakete verzichte. Damit Sie aber verstehen, worum es dabei geht, beschreibe ich kurz die Bedeutung der Deklaration für Pakete. Wie Sie ja bereits wissen, sind Java-Klassen normalerweise in Paketen (Packages) organisiert. Die Klasse System, mit der Sie u. a. Ausgaben an der Konsole programmieren, gehört z. B. zum Paket java.lang. Sie kön-
358
Sandini Bib
nen Ihre Klassen in eigenen Paketen organisieren, indem Sie die packageAnweisung oben in der Datei angeben: package addisonWesley.utils;
Wenn Sie Klassen, die in Paketen organisiert sind, im Programm verwenden wollen, müssen Sie bei der Deklaration entweder den Namen des Pakets mit angeben oder den Inhalt des Pakets über die importAnweisung importieren. Davon ausgenommen sind Klassen, die im selben Paket verwaltet werden. Die Verwaltung von Klassen in Paketen macht eigentlich nur für Klassen Sinn, die einen allgemeinen Charakter besitzen und deshalb in anderen Projekten wiederverwendet werden können. Dann sollten Sie das Paketkonzept auch konsequent einsetzen. Verwenden Sie vielleicht einen Paketnamen, der aus zwei Teilen besteht: einem Kürzel Ihres Namens bzw. Ihrer Firma und einem Kürzel für das Thema dieses Pakets. Über die Verwaltung allgemeiner Klassen in eigenen Paketen erreichen Sie, dass keine Namenskonflikte mit den Klassen anderer Pakete auftreten bzw. dass diese sehr einfach über die Angabe des Pakets bei der Deklaration von Variablen aufgelöst werden können. Das ist einer der Vorteile von Paketen (der andere ist, dass die Suche nach Lösungen innerhalb einer Bibliothek über das Paketkonzept vereinfacht wird). Wenn Sie nun die in einem Paket enthaltenen Klassen ohne das Schlüsselwort public deklarieren, können Sie nur innerhalb des Pakets auf diese Klassen zugreifen. Klassen in anderen Paketen können diese paketprivaten Klassen nicht verwenden. Diese Technik nutzen Sie wahrscheinlich – genau wie ich – eher selten für Hilfsklassen, die nur innerhalb eines Pakets genutzt werden, aber nicht veröffentlicht werden sollen. Das Ganze ist hinfällig, wenn Sie für eine Klasse kein Paket angeben. Dann gehört diese nämlich dem vordefinierten globalen Paket an. Klassen aus dem globalen Paket können immer ohne Angabe des Paketnamens verwendet werden, unabhängig davon, ob diese mit oder ohne public deklariert sind. Ich denke, die Klassen, die speziell zu einem Projekt gehören, sollten Sie, wie die Programm-Startdatei, nicht unbedingt in einem Paket verwalten. Es macht einfach zu viel Arbeit, in den Klassen eines Projekts immer wieder den Paketnamen mit angeben zu müssen. Verwenden Sie dazu einfach das globale Paket, indem Sie auf die package-Anweisung verzichten. Ihre allgemeinen Klassen sollten Sie aber auf jeden Fall in Paketen organisieren, um Namenskonflikten vorzubeugen.
359
Sandini Bib
6.3.2 Instanzen erzeugen Wenn Sie nun mit Ihren Klassen arbeiten wollen, müssen Sie daraus Instanzen erzeugen. In den meisten Programmiersprachen erzeugen Sie diese dynamisch, d. h. im Verlauf des Programms. Dazu nutzen Sie entweder einen speziellen Operator oder eine statische Methode der Klasse. Einige Programmiersprachen wie z. B. C++ erlauben neben der dynamischen Erzeugung auch statische Deklarationen, die denen von einfachen Variablen gleichen. Bei dieser Art Deklaration legt der Compiler die Instanzen automatisch an. Deshalb ist es aber auch unmöglich, eine beliebige, bei der Entwicklung des Programms unbekannte Anzahl an Instanzen zu erzeugen (was bei der dynamischen Erzeugung ohne weiteres möglich ist, Sie müssen die Instanzen lediglich über eine dynamisch erweiterbare Liste von Variablen verwalten, die ich allerdings erst im nächsten Kapitel beschreibe). Referenzen Wenn Sie mit Objekten arbeiten, verwenden Sie immer Referenzen, um auf die Eigenschaften oder Methoden der Objekte zuzugreifen. Eine Referenz ist im Prinzip eine Variable, die die Adresse des Arbeitsspeichers verwaltet, an dem ein Objekt gespeichert ist. Eine Objektvariable ist also etwas anderes als eine einfache Variable. Eine einfache Variable speichert ihren Wert, bei einer Integer-Variablen ist das z. B. eine 32-BitGanzzahl. Eine Referenz speichert aber nicht den Wert eines Objekts, sondern lediglich dessen Speicheradresse. Es ist ziemlich wichtig, dass Sie diese Technik verstehen. Abbildung 6.2 zeigt, wie das im Arbeitsspeicher aussieht. Arbeitsspeicher Referenz
Objekt
Abbildung 6.2: Eine Referenz zeigt auf ein Objekt.
Daraus ergeben sich einige Vorteile und Besonderheiten gegenüber einfachen Variablen. Weil eine Referenz dir Speicheradresse eines Objekts verwaltet, können auch mehrere Referenzen auf ein Objekt zeigen. Sie können ein Objekt beispielsweise an eine Funktion, Prozedur oder Methode übergeben:
360
Sandini Bib
01 class ObjektDemo 02 { 03 public static void demo(Person p) 04 { 05 System.out.println(p.VollerName()); 06 } 07 08 public static void main(String args[]) 09 { 10 /* Eine Instanz der Klasse erzeugen und initialisieren */ 11 Person person1 = new Person(); 12 person1.Vorname = "Zahphod"; 13 person1.Nachname = "Beeblebrox"; 14 15 /* Diese Instanz an die Methode demo übergeben */ 16 demo(person1); 17 } 18 }
Beim Aufruf der Methode (Zeile 16) existieren dann zwei Referenzen auf dieses Objekt: die Referenz person1 im Hauptprogramm und die Referenz p in der Methode (Abbildung 6.3). Arbeitsspeicher Referenz im Hauptprogramm
Objekt
Referenz in der Methode Abbildung 6.3: Zwei Referenzen zeigen auf ein Objekt.
Wenn Sie Objekte an Prozeduren, Funktionen oder Methoden übergeben, erzeugen Sie in den meisten Programmiersprachen (und in Delphi, Kylix und Java) keine Kopie, wie es bei einfachen Variablen der Fall wäre. Sie übergeben lediglich die Referenz auf das Objekt. Wenn Sie innerhalb der Prozedur, Funktion oder Methode mit dieser Referenz arbeiten, bearbeiten Sie das Objekt, das auch im Hauptprogramm referenziert wird.
Keine Kopie bei der Übergabe an Methoden
Das hört sich vielleicht im Moment etwas eigenartig an (vor allen Dingen, weil ich im vorherigen Kapitel vor der Verwendung von Referenz-
361
Sandini Bib
argumenten für einfache Variablen gewarnt habe), wird aber in vielen Programmen gewinnbringend genutzt. Einige wenige Programmiersprachen wie C++ erlauben auch über spezielle Techniken das Kopieren eines Objekts bei der Weitergabe einer Referenz. Es macht das Verständnis eines Programms aber nicht gerade einfach, da Sie bei solchen Sprachen nie genau wissen, ob nun eine Referenz übergeben oder eine Kopie erzeugt wird. Glücklicherweise wird dieses Konzept von modernen Programmiersprachen nicht unterstützt. Eine andere Besonderheit ist, dass Sie auch keine Kopie des Objekts erzeugen, wenn Sie einer Objektvariablen eine andere zuweisen: Person person2 = person1;
Wie bei der Übergabe an eine Funktion, Prozedur oder Methode erhalten Sie bei einer Zuweisung zwei Referenzen auf dasselbe Objekt. Sie können das Objekt dann über beide Referenzen bearbeiten. Änderungen, die Sie über die eine Referenz vornehmen, sind dann (natürlich) auch über die andere Referenz sichtbar. Das ist ein großer Unterschied zu normalen Variablen. Bei einer einfachen Variablen (beispielsweise vom Typ int) erzeugen Sie bei einer Zuweisung eine Kopie der gespeicherten Daten (aber das kennen Sie ja), bei Referenzen eben nicht. Sie können sich dieses „Phänomen“ übrigens sehr einfach erklären. Bei einer Zuweisung einer Objektvariablen an eine andere oder bei der Übergabe einer Objektvariablen an eine Funktion. Prozedur oder Methode erzeugen Sie nämlich prinzipiell doch eine Kopie. Sie kopieren aber (wie auch bei normalen Variablen) den Inhalt der Variablen. Und das ist nun einmal die Speicheradresse des Objekts (und eben nicht das Objekt selbst). Die Frage, warum die OOP überhaupt mit Referenzen arbeitet, lässt sich recht einfach beantworten: Jedes Objekt ist individuell, speichert seine eigenen Daten und besitzt seine eigene Identität. Wenn nun der Bedarf besteht, dass mehrere Programmteile (Funktionen, Prozeduren, andere Objekte etc.) mit demselben Objekt arbeiten, lässt sich dieses Problem nur mit Referenzen lösen. Wenn Sie eine einfache Variable an eine Prozedur oder Funktion übergeben, wird (normalerweise, wenn Sie die Variable nicht schon „By Reference“ übergeben) der Wert der Variable in das Argument kopiert. Bei einfachen Variablen macht das in der Regel Sinn, bei Objekten aber nicht. Verschiedene Programmteile sollen schließlich mit demselben Objekt arbeiten und nicht mit Kopien eines Objekts.
362
Sandini Bib
Nullreferenzen Eine besondere Art von Referenzen sind so genannte Nullreferenzen. Sie müssen mit diesen Referenzen umgehen können, weswegen ich diese hier beschreibe. Eine Nullreferenz steht für eine Referenz, die gar kein Objekt referenziert. Besonders in Java geben viele Bibliotheks-Klassenmethoden Referenzen auf Objekte zurück. Diese Methoden liefern im Fehlerfall in der Regel eine Nullreferenz. Mit Nullreferenzen können Sie nicht arbeiten. Diese zeigen ja nicht auf Objekte. Beim Versuch, mit einer Variablen zu arbeiten, die eine Nullreferenz speichert, generiert der Compiler eine Ausnahme. Sie können Objektvariablen in Java aber mit dem speziellen Wert null vergleichen, um herauszufinden, ob diese eine Nullreferenz speichern: if (objektvariable == null)
In Object Pascal vergleichen Sie mit dem Wert nil: if objektvariable = nil then
Referenzen vergleichen Für Ihre weitere Arbeit mit Objekten sollten Sie eine weitere Eigenart kennen: Wenn Sie zwei Objektvariablen vergleichen, vergleicht der Compiler nicht den Inhalt der Objekte. Wenn Sie z. B. zwei PersonObjekte besitzen und diese vergleichen, if (person1 == person2)
führt dieser Vergleich zum Ergebnis false, auch wenn beide Objekte dieselben Daten speichern. Sie vergleichen hier nämlich die Referenzen. Und die sind nun einmal unterschiedlich, da es sich um zwei verschiedene Objekte handelt. Lediglich, wenn Sie zwei Referenzen vergleichen, die auf dasselbe Objekt zeigen, führt ein solcher Vergleich zum Ergebnis true. Das sollten Sie immer im Auge behalten, wenn Sie mit Objekten arbeiten. Wenn Sie mit Objekten arbeiten, deren Klassen aus einer Bibliothek stammen, können Sie zum Vergleich häufig eine Methode der Objekte verwenden, die normalerweise compare oder compareTo heißt. Instanzen in Delphi und Kylix erzeugen In Delphi und Kylix erzeugen Sie eine Instanz einer Klasse über die statische Methode Create, die der Compiler der Klasse automatisch hinzufügt. Statische Methoden können, wie Sie ja bereits aus Kapitel 5 wissen,
363
Sandini Bib
immer ohne eine Instanz der Klasse, lediglich unter der Angabe des Klassennamens aufgerufen werden. Create ist keine Methode, die Sie innerhalb der Klasse sehen. Verstehen Sie diese Methode einfach als das Werkzeug, das Sie benutzen, um dem Compiler mitzuteilen, dass Sie eine Instanz der Klasse benötigen. Create erzeugt die Instanz und gibt eine Referenz darauf zurück. Üblicherweise weisen Sie diese Referenz einer Variablen vom Typ der Klasse zu, damit Sie mit dem Objekt arbeiten können: 01 02 03 04 05 06 07 08 09 10 11 12 13 14
program EinfacheKlassen; {$APPTYPE CONSOLE} uses SysUtils, CPerson in 'CPerson.pas'; { Variablen für die Instanzen der Klasse } Var person1, person2, person3: Person; begin { Eine Instanz der Klasse erzeugen } person1 := Person.Create();
Nun können Sie mit dem Objekt arbeiten, d. h. dessen Eigenschaften beschreiben und auslesen und dessen Methoden aufrufen. Dazu verwenden Sie wie bei der OOP üblich das folgende Schema: Objektreferenz.Eigenschaft = Wert; Objektreferenz.Methode([Argumente]);
Der Punkt trennt dabei den Objektnamen vom Methoden- bzw. Eigenschaftennamen. Ansonsten unterscheidet sich die Verwendung von Methoden syntaktisch nicht von der Verwendung von Funktionen bzw. Prozeduren und die Verwendung von Eigenschaften nicht von der von Variablen: Methoden werden aufgerufen, Eigenschaften werden beschrieben oder gelesen. Sie können also z. B. die Eigenschaften der erzeugten Instanz der Person-Klasse beschreiben: 15 16
person1.Vorname := 'Zahphod'; person1.Nachname := 'Beeblebrox';
und die zurzeit einzige Methode dieser Klasse aufrufen: 17
364
writeln(person1.VollerName());
Sandini Bib
Sie können so viele Instanzen erzeugen, wie Sie benötigen: 18 { Eine weitere Instanz der Klasse erzeugen und initialisieren } 19 person2 := Person.Create(); 20 person2.Vorname := 'Tricia'; 21 person2.Nachname := 'McMillan'; 22 23 { Noch eine Instanz der Klasse erzeugen und initialisieren } 24 person3 := Person.Create(); 25 person3.Vorname := 'Ford'; 26 person3.Nachname := 'Prefect'; 27 28 { Die Methode dieser Objekte aufrufen } 29 writeln(person2.VollerName()); 30 writeln(person3.VollerName()); 31 end.
Jede Instanz verwaltet ihre eigenen Daten. Wenn Sie die Daten einer Instanz ändern, betrifft das nicht die Daten anderer Instanzen dieser Klasse. Speicherfehler beim Verwenden von nicht instanzierten Klassen In Ihren Programmen wird es sehr häufig vorkommen, dass Sie Instanzen verwenden wollen, die gar nicht existieren, weil Sie vergessen haben, diese zu erzeugen. Delphi-Programme reagieren auf solche Fehler recht rüde: Sie stürzen einfach ab. Wenn Sie das Programm direkt unter Windows ausführen, meldet Windows einen Speicherzugriffsfehler (Abbildung 6.4).
Abbildung 6.4: Windows meldet einen Speicherzugriffsfehler aufgrund der Verwendung eines nicht instanzierten Objekts.
Diese Fehler können Sie nur sehr eingeschränkt debuggen, weil das Programm keine Ausnahme erzeugt. Sie können sich lediglich über einen gesetzten Haltepunkt zu der Anweisung vortasten, die den Fehler verursacht. Das macht eine Menge Arbeit und kostet viel Nerven. Achten Sie also darauf, dass Sie Objekte, die Sie verwenden, immer instanzieren.
365
Sandini Bib
Kylix ist bei der Verwendung nicht instanzierter Objekte etwas netter und erzeugt eine Ausnahme vom Typ EAccessViolation, die Sie aber leider nicht debuggen können. Zuvor meldet Kylix, dass der Prozess das Signal SIGSEGV4 erhalten hat. Nach dem Schließen dieser Meldung und einer erneuten Betätigung von (F9) wird dann die Ausnahme gemeldet. Der Java-Compiler vermeidet solche Fehler, indem er erkennt, dass eine Objektvariable nicht initialisiert wurde, und solche Programme erst gar nicht kompiliert. Es ist schon eigenartig, dass Delphi und Kylix das nicht erkennen ... Instanzen in Java erzeugen In Java erzeugen Sie Instanzen über den new-Operator: 01 class EinfacheKlassen 02 { 03 public static void main(String args[]) 04 { 05 /* Eine Instanz der Klasse erzeugen */ 06 Person p1 = new Person();
Ansonsten sieht der Zugriff auf die Eigenschaften und Methoden aus wie bei Object Pascal: 07 08 09 10 11 12
/* Eigenschaften beschreiben */ p1.Vorname = "Zaphod"; p1.Nachname = "Beeblebrox"; /* Methode aufrufen */ System.out.println(p1.VollerName());
Sie können natürlich auch mehrere Instanzen erzeugen: 13 14 15 16 17 18 19 20 21 22 23 4.
366
/* Eine weitere Instanz erzeugen und initialisieren */ Person p2 = new Person(); p2.Vorname = "Tricia"; p2.Nachname = "McMillan"; /* Noch eine Instanz erzeugen und initialisieren */ Person p3 = new Person(); p3.Vorname = "Ford"; p3.Nachname = "Prefect"; /* Die Methode dieser Objekte aufrufen */ Signal Segment Violation, eine Speichersegment-Zugriffsverletzung
Sandini Bib
24 System.out.println(p2.VollerName()); 25 System.out.println(p3.VollerName()); 26 } 27 }
Wie Sie sehen, unterscheidet sich die Erzeugung und Verwendung von Klassen und deren Instanzen in Delphi/Kylix und Java kaum voneinander. Und das bleibt auch bei den weiteren OOP-Techniken prinzipiell so, wenn es auch manchmal geringe Unterschiede gibt. Die Konzepte der OOP sind eben in allen Programmiersprachen gleich. Implizite Vererbung Wenn Sie eigene Klassen erzeugen, werden diese in Delphi, Kylix und Java implizit bereits von einer Basisklasse abgeleitet. In Delphi und Kylix ist das die Klasse TObject, in Java die Klasse Object. Deshalb besitzen Ihre Klassen bereits einige Elemente, die von den impliziten Basisklassen geerbt werden. Diese Elemente sehen Sie nicht in Ihrer Klassendeklaration, aber Sie sehen sie, wenn Sie Ihre Klassen oder Instanzen Ihrer Klassen in einer Entwicklungsumgebung verwenden, Sie den Punkt nach dem Klassen- oder Objektnamen schreiben und die Elementliste der Entwicklungsumgebung aufklappt. Delphi/Kylix-Klassen erben z. B. die wichtigen Methoden Create zur Erzeugung und Free zur Zerstörung einer Instanz. Java-Klassen erben u. a. die Methoden clone (zur Erzeugung einer Kopie des Objekts) und equals (Überprüfung, ob zwei Objekte dieselben Daten speichern). Besonders bei Java sind viele der geerbten Methoden erst dann sinnvoll, wenn diese in der neuen Klasse mit einer neuen Implementierung versehen werden (also neu programmiert werden). Die geerbte clone-Methode kann z. B. ja gar nicht wissen, wie die neue Instanz erzeugt werden muss, weil sie in der Basisklasse Object implementiert ist. Diese Methoden sind dann auch so deklariert, dass sie erst neu definiert werden müssen, bevor Sie sie verwenden können. Das geht aber an dieser Stelle bereits zu weit. Wichtig ist nur, dass Sie wissen, woher diese Elemente stammen.
6.3.3 Wie werden Objekte zerstört? Alle Objekte müssen irgendwann einmal sterben, also aus dem Speicher entfernt werden. Bei Java müssen Sie sich nicht selbst darum kümmern. Java-Objekte werden automatisch aus dem Speicher entfernt, wenn sie nicht mehr referenziert werden. Objekte, die in Methoden erzeugt werden und deren Referenzvariable in der Methode deklariert ist, werden automatisch zerstört, wenn die Methode beendet wurde. Referenziert
367
Sandini Bib
hingegen eine Eigenschaft eines anderen Objekts ein Objekt, wird dieses erst dann zerstört, wenn auch das andere Objekt zerstört wird. Wird ein Objekt über eine statische Eigenschaft einer Klasse referenziert, lebt es so lange, wie das Programm lebt. Der Garbage Collector zerstört Objekte automatisch
Delphi/KylixObjekte müssen explizit zerstört
In Java kümmert sich ein so genannter Garbage Collector („Müllsammler“) um dieses Zerstören. Der Garbage Collector ist ein separater Prozess, der in mehr oder weniger regelmäßigen Abständen im Programm nach verwaisten Objekten sucht und diese dann aus dem Speicher entfernt. Diese Technik ist sehr hilfreich für uns Programmierer, denn so müssen wir uns nicht um das Zerstören von Objekten kümmern und können deswegen auch keine Fehler machen und Objekte versehentlich im Speicher lassen. In Delphi und Kylix sieht das Ganze allerdings anders aus. Hier müssen Sie die Objekte, die Sie erzeugt haben, selbst zerstören. Dazu verwenden Sie die von TObject geerbte Methode Free:
werden
begin { Instanzen der Klasse erzeugen } person1 := Person.Create(); person2 := Person.Create(); ... { Instanzen zerstören } person1.Free; person2.Free; end.
Wenn Sie das Zerstören vergessen, werden die Speicherbereiche der Objekte erst dann freigegeben, wenn das Programm beendet wird. Erzeugen Sie Objekte z. B. in einer Methode, die häufiger aufgerufen wird, belegen die nach der Abarbeitung der Methode verwaisten Objekte unnötigen Speicher. Um das einmal selbst zu überprüfen, schreiben Sie vielleicht ein kleines Programm:
368
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
program ObjektZerstoerungsProblemDemo; {$APPTYPE CONSOLE} uses SysUtils, CPerson; { Variable für die Instanzen der Klasse } Var p: Person; weiter: string; i: integer; begin writeln('Demonstration des Problems mit ', 'nicht zerstörten Objekten'); repeat writeln('Erzeuge 1000 Instanzen ...'); for i := 1 to 1000 do begin p := Person.Create(); p.Vorname := 'Zaphod'; p.Nachname := 'Beeblebrox'; end; write('Noch einmal? '); readln(weiter); until weiter <> 'j'; end.
Das Programm erzeugt in einer Schleife jeweils 1000 Instanzen der Person-Klasse, ohne diese wieder zu zerstören. Dass die einzelnen Instanzen dabei derselben Variablen zugewiesen werden ist für das Beispiel in Ordnung. Bei jeder Erzeugung wird die Referenz auf das neue Objekt gesetzt. Das Objekt, auf das die Referenz vorher zeigte, bleibt dann zwar im Speicher, kann im Programm aber nicht mehr angesprochen werden (weil eben die Referenz fehlt). Wenn Sie das Programm nun ausführen, können Sie unter Windows im Task-Manager ((Strg) (ª) (Entf), TASK-MANAGER) im PROZESSE-Register das Programm (ObjektZerstoerungsProblemDem.exe ) suchen und in der Spalte SPEICHER den Speicherverbrauch beobachten. Unter Linux verwenden Sie in der KDE dazu die Systemüberwachung, die Sie mit (Strg) (Esc) aufrufen. Suchen Sie das Programm in der Spalte NAME und beobachten Sie den Wert in der Spalte VM-GRÖßE. Sie werden erkennen, dass Ihr Programm mit jeder Ausführung der Schleife mehr Speicher benötigt. Sorgen Sie also dafür, dass Sie Ihre Delphi/Kylix-Objekte immer nach der Verwendung zerstören.
369
Sandini Bib
Wenn Sie dasselbe mit einem Java-Programm testen, werden Sie zu ähnlichen Ergebnissen kommen. Das liegt dann daran, dass der Garbage Collector von Java nur dann arbeitet, wenn das Programm selbst nicht allzu viel macht. Es kann sein, dass er erst jeweils sehr spät durch den Speicher geht und die Objekte zerstört. Sie müssen also gegebenenfalls nach der Ausführung einer Schleife einige Zeit warten.
6.4
Grundsätze zum Entwurf von Klassen
Beim Entwurf einer Klasse machen Sie sich Gedanken darüber, wie ein Objekt der Klasse möglichst einfach verwendet werden kann. Dazu legen Sie fest, welche Methoden und Eigenschaften die Klasse besitzen sollte und wie diese aussehen sollten. Sie denken an dieser Stelle noch nicht über die (eventuell schwierige) Implementierung der Methoden der Klasse nach. Die Frage nach den Methoden und Eigenschaften ist nicht immer einfach zu beantworten. Als Beispiel für die Lösung dieses Problems setze ich eine Klasse ein, die zum Drucken von Daten verwendet werden soll. Prinzipiell speichert ein Objekt in den Eigenschaften seine Daten. Über Methoden können Sie mit dem Objekt arbeiten. Methoden können dabei mit den Daten des Objekts (mit den Eigenschaften) arbeiten. Sie können aber Methoden auch Argumente übergeben, um diese gesteuert aufzurufen. Beim Entwurf eines Druck-Objekts können Sie zuerst festlegen, welche Daten das Objekt verwalten muss, also welche Eigenschaften es besitzen sollte. Sie könnten aber auch zuerst die Methoden festlegen, die Reihenfolge spielt eigentlich keine Rolle, weil Eigenschaften und Methoden meist ziemlich stark miteinander verwoben sind. Die Antwort auf die Frage nach den Eigenschaften ist einfach, aber etwas vage: Alle Daten, die sich innerhalb der Lebenszeit eines Objekts nicht oder nur selten ändern, sollten als Eigenschaft deklariert werden. Bei einem Druck-Objekt wären das z. B. zumindest die folgenden Eigenschaften: • Druckername • LinkerRand • RechterRand • ObererRand • UntererRand
370
Sandini Bib
Diese Daten sind als Eigenschaft sinnvoll. Zum einen sollten diese Werte grundsätzlich einstellbar sein, damit ein Programm, das Instanzen dieser Klasse verwendet, entscheiden kann, welcher Drucker verwendet wird und wie groß die beim Drucken berücksichtigten Seitenränder sind. Zum anderen können Sie davon ausgehen, dass diese Daten bei der Verwendung von Instanzen der Klasse nur sehr selten geändert werden. Wahrscheinlich wird ein Programm eine Instanz der Klasse nur einmal initialisieren und danach die Druck-Methoden mehrfach aufrufen, um Daten auszudrucken. Müsste das Programm den Druckernamen und die Seitenränder stattdessen beim jedem Aufruf einer Methode als Argument übergeben, wäre das sehr umständlich. Um das Ganze noch weiter zu vereinfachen, sollte die Eigenschaft Druckername so ausgewertet werden, dass das Objekt beim Drucken den Standarddrucker des Systems verwendet, wenn diese Eigenschaft nicht gesetzt ist. Eine zusätzliche Eigenschaft Schriftart wäre bereits ein Sonderfall. Es kann sein, dass ein Programm die Schriftart, die beim Drucken verwendet wird, nur ein einziges Mal setzt oder nur sehr selten ändert. Es kann aber auch sein, dass die Schriftart sehr häufig geändert werden muss. Hier müssen Sie dann entscheiden, ob es sinnvoll ist, dazu eine Eigenschaft zu verwenden (die eben bei jeder Änderung der Schriftart gesetzt werden muss), oder ob Sie dazu ein Argument der Druck-Methoden verwenden. Wahrscheinlich wird die Schriftart nicht allzu häufig geändert, weswegen eine Eigenschaft angebracht erscheint. Die Eigenschaft Schriftart müsste übrigens eine Eigenschaft sein, die selbst ein Objekt verwaltet. Dieses Schriftart-Objekt würde die Eigenschaften einer typischen Schriftart besitzen: Name, Größe, Farbe, Fett, Kursiv, Unterstrichen etc. Die weiteren Eigenschaften PapierBreite und PapierHöhe wären ebenfalls ein Sonderfall. Da die Werte dieser Eigenschaften nicht vom Programm festgelegt werden können, sondern vom Druckertreiber bzw. vom Drucker bestimmt werden, müssten diese Eigenschaften solche sein, die das Objekt selbst bestimmt. Diese Eigenschaften sollten nicht vom Programm verändert werden können und müssten daher schreibgeschützt sein. Das Objekt würde die Eigenschaften bei seiner Erzeugung (auf die Einstellungen des Standarddruckers) und immer dann anpassen, wenn die Eigenschaft Druckername geändert würde. Wie Sie so etwas grundsätzlich programmieren, lernen Sie im weiteren Verlauf dieses Kapitels. Als Methoden müsste ein einigermaßen gut verwendbares Druck-Objekt solche zur Ausgabe von Text, zum Zeichnen von Linien und Rechtecken, zum Zeichnen von Kreisen und zur Ausgabe von Bilddateien besitzen. Beim Entwurf der Methoden müssen Sie sich Gedanken um die
371
Sandini Bib
Argumente machen. Dass die Methoden mit den Daten der Eigenschaften arbeiten, ist ja bereits klar. Eine Methode zum Ausdrucken von Text sollte natürlich ein Argument besitzen, über das der Text übergeben werden kann. Hier wird dann auch der Unterschied zwischen Eigenschaften und Argumenten deutlich: Der auszudruckende Text ändert sich bei der Benutzung eines Druck-Objekts andauernd. Es wäre also nicht sinnvoll, dieses als Eigenschaft zu deklarieren. Schwieriger wird die Entscheidung, wie bereits gesagt, bei der Entscheidung, ob die Schriftart in einer Eigenschaft oder in einem Argument definiert werden soll. Aber dafür bietet sich auch eine Lösung an: Entwickeln Sie einfach zwei Methoden zum Ausdruck von Text, eine mit und eine ohne das Argument Schriftart. Die Variante ohne das Argument liest die Eigenschaft aus um die Schriftart zu bestimmen, die Variante mit Argument verwendet natürlich die im Argument übergebenen Schriftart-Einstellungen. So überlassen Sie dem Programmierer, der Ihre Klasse benutzt (auch, wenn Sie das selbst sind), die Wahl und gestalten die Klasse möglichst flexibel. Diese Denkweise sollten Sie bei der Programmierung von Klassen immer berücksichtigen. Auch wenn Sie Ihre Klassen selbst verwenden, sollten Sie den Entwickler einer Klasse von dem Anwender der Klasse getrennt betrachten. Entwickeln Sie Klassen immer so, dass die Anwendung möglichst einfach und flexibel ist. Besonders, wenn es sich um Klassen handelt, die wiederverwendet werden können, sollten Sie bei der Entwicklung immer schon zusätzliche Features integrieren, die Sie vielleicht im Moment noch gar nicht benötigen, die aber in späteren Verwendungen offensichtlich nützlich sein könnten. Alle Druckmethoden sollten zusätzliche Argumente besitzen, über die die Position auf dem Papier festgelegt werden kann, bei der der Druck beginnt. Diese Position würde sich natürlich auf die gesetzten Papierränder beziehen. Die Methoden zum Zeichnen und zur Ausgabe von Bildern müssen weitere Argumente für die Größe und die Farbe besitzen. Der Programmierer kann die Position und die anderen Werte der auszudruckenden Daten beim Drucken dann sehr flexibel selbst definieren und vor dem Druck mit Hilfe der Eigenschaften PapierBreite und PapierHöhe überprüfen, ob die Ausgabe noch auf das aktuelle Blatt Papier passt. Um eine neue Seite zu erzeugen, sollte eine Methode NeueSeite verfügbar sein. Das Objekt sollte also vielleicht die folgenden Methoden besitzen: • TextDrucken(Text, x, y) • TextDrucken(Text, x, y, Schriftart) • LinieZeichnen(x, y, Breite, Farbe) • RechteckZeichnen(x, y, Breite, Höhe, Randfarbe, Füllfarbe)
372
Sandini Bib
• KreisZeichnen(x, y, Radius, Randfarbe, Füllfarbe) • BildZeichnen(Dateiname, x, y, Breite, Höhe) • NeueSeite Ein Objekt einer solchen Klasse könnte nun recht flexibel verwendet werden und würde nicht allzu viel Programmierarbeit verursachen. Dieser erste Entwurf ist aber lange noch nicht perfekt. Ein Problem ist z. B., dass beim Drucken von Text der rechte Rand berücksichtigt werden müsste. Die Methode TextDrucken müsste also den Text Wort für Wort drucken und bei jedem Wort überprüfen, ob dieses noch in die aktuelle Zeile passt. Passt das Wort nicht mehr, müsste die Methode die Zeile automatisch umbrechen (d. h, die y-Position um eine Zeilenhöhe erhöhen und die x-Position auf Null setzen). Ähnliches wäre beim Erreichen des unteren Randes notwendig. Hier müsste diese Methode automatisch eine neue Seite erzeugen und die x- und die y-Position auf Null setzen. Das Programm müsste dann aber darüber informiert werden, dass sich etwas geändert hat. Dazu wären zusätzliche Eigenschaften sinnvoll, die dem Programm Informationen über die aktuelle x- und y-Position liefern. Diese könnte das Programm dann beim Ausdrucken berücksichtigen. Damit dem Programmierer dabei nicht zu viel Arbeit entsteht, wäre es dann noch sinnvoll, wenn eine weitere Variante der TextDrucken-Methode keine x- und y-Argumente besitzt und einfach die aktuellen Werte verwendet. An diesem Beispiel erkennen Sie sehr gut, dass die Entwicklung einer Klasse häufig viel Arbeit macht. Dafür ist die Anwendung solch umsichtig programmierter Klassen dann sehr einfach und intuitiv.
6.5
Eine kleine Übung
Als Übung wollen wir nun in Object Pascal eine Klasse entwerfen, die Ein- und Ausgaben an der Konsole erleichtert. In Kapitel 5 haben Sie bereits ein einfaches Modul für diese Aufgabe kennen gelernt. Nun soll dafür eine echte Klasse entwickelt werden, aus der einzelne Instanzen erzeugt werden können. Da Sie bei der Entwicklung der Klasse bereits ein wenig weiter denken, weil die Klasse sehr gut wiederverwendet werden kann, sollten Sie neben einer Methode zur Ausgabe von einfachen Texten zusätzliche Methoden zur Ausgabe von formatierten Zahlen und Datumswerten integrieren. Die Klasse sollte also die folgenden Methoden besitzen:
Bestimmung der Methoden
• TextAusgeben • ZahlAusgeben
373
Sandini Bib
• StringEingeben • ZahlEingeben Aus Gründen der Übersichtlichkeit habe ich Methoden zur Ein- und Ausgabe von Datumswerten in diesem Beispiel weggelassen. Nun sollten Sie sich zunächst Gedanken um die Eigenschaften machen. Wie Sie ja wissen, speichert ein Objekt in den Eigenschaften seine Daten. Die Daten einer Klasse zur Arbeit mit der Konsole sind ein wenig schwierig festzulegen. Ich würde vorschlagen, dass die Formate für die Ausgabe von Zahlen und Datumswerten in Eigenschaften verwaltet werden. Sie können davon ausgehen, dass diese Formate nur sehr selten im Programm geändert werden. Bei den Methoden zur Eingabe bieten sich auch eine Eigenschaften an. Damit die Methoden einfach zu verwenden sind, könnte eine Eigenschaft EingabeOK eine Information darüber geben, ob die Eingabe gültig war. Die eingegebenen Daten könnten dann in weiteren Eigenschaften verwaltet werden, die ein weiteres Verwenden dieser Daten vereinfachen: Wenn Sie dazu Eigenschaften verwenden, muss das Programm, das eine Instanz dieser Klasse verwendet, nicht unbedingt separate Variablen deklarieren. Eine Verwendung eines Objekts dieser Klasse könnte dann beispielsweise so aussehen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
374
program KonsoleTest; {$APPTYPE CONSOLE} uses SysUtils, CKonsoleTool; var konsoleTool1: KonsoleTool; wurzel: extended; begin konsoleTool1 := KonsoleTool.Create(); konsoleTool1.Zahlformat := '0.00'; konsoleTool1.TextAusgeben('Wurzelberechnung', true); konsoleTool1.ZahlEingeben('Geben Sie eine Zahl ein: '); if konsoleTool1.EingabeOK then begin konsoleTool1.TextAusgeben('Wurzel: ', false); wurzel := Sqrt(konsoleTool1.EingegebeneZahl); konsoleTool1.ZahlAusgeben(wurzel, true); end else konsoleTool1.TextAusgeben( 'Sie haben keine Zahl eingegeben', true); end.
Sandini Bib
Das letzte Argument der Ausgabemethoden soll bestimmen, ob nach der Ausgabe ein Zeilenumbruch erfolgt. Die Klasse müsste also die folgenden Eigenschaften besitzen:
Bestimmung der Eigenschaften
• Zahlformat • EingabeOK • EingegebenerText • EingegebeneZahl Während des Entwurfs der Klasse können Sie die Klassendeklaration bereits festlegen. Die Methoden werden später implementiert.
Deklaration der Klasse
Die KonsoleTool-Klasse sollte in einer separaten Unit deklariert sein (die idealerweise in Ihrem Module-Ordner gespeichert wird) und in etwa folgendermaßen aussehen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19
unit CKonsoleTool; interface uses SysUtils; type KonsoleTool = class { Deklaration der Eigenschaften } Zahlformat: string; EingabeOK: boolean; EingegebenerText: string; EingegebeneZahl: extended; { Deklaration der Methoden } procedure TextAusgeben(Text: string; Zeilenumbruch: boolean); procedure ZahlAusgeben(Zahl: extended; Zeilenumbruch: boolean); procedure TextEingeben(InfoText: string); procedure ZahlEingeben(InfoText: string); end;
Nun, da der Entwurf der Klasse „perfekt“ ist, müssen Sie nur noch die Methoden implementieren. Das macht in der Praxis natürlich häufig recht viel Arbeit, aber das gehört eben zum Programmieren. Die Implementierung der Methoden der KonsoleTool-Klasse sieht dann so aus:
Implementierung der Methoden
375
Sandini Bib
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
376
implementation { Implementierung der Methoden } procedure KonsoleTool.TextAusgeben(Text: string; Zeilenumbruch: boolean); begin { Text ausgeben } write(Text); { Zeilenumbruch erzeugen, wenn notwendig } if Zeilenumbruch then writeln; end; procedure KonsoleTool.ZahlAusgeben(Zahl: extended; Zeilenumbruch: boolean); var formatierteZahl: string; begin { Zahl formatieren bzw. in einen String konvertieren } if Zahlformat > '' then formatierteZahl := FormatFloat(Zahlformat, Zahl) else formatierteZahl := FloatToStr(Zahl); { Zahl ausgeben } write(formatierteZahl); { Zeilenumbruch erzeugen, wenn notwendig } if Zeilenumbruch then writeln; end; procedure KonsoleTool.TextEingeben(InfoText: string); var eingabe: string; begin { Den Benutzer einen Text eingeben lassen } write(InfoText); readln(eingabe); { Eingabe in der Eigenschaft ablegen } EingegebenerText := Eingabe; { bei Texteingaben ist die Eingabe immer OK } EingabeOK := true; end; procedure KonsoleTool.ZahlEingeben(InfoText: string); var eingabe: string; begin { Den Benutzer einen Text eingeben lassen } write(InfoText); readln(eingabe);
Sandini Bib
65 { Konvertieren der Zahl und gleichzeitiges Überprüfen, 66 ob die Eingabe in eine Zahl konvertiert werden kann. 67 Dazu wird eine Ausnahmebehandlung eingesetzt. } 68 try 69 EingegebeneZahl := StrToFloat(eingabe); 70 { Wenn das Programm hier ankommt, konnte die Eingabe 71 konvertiert werden } 72 EingabeOK := true; 73 except 74 { Die Eingabe konnte nicht konvertiert werden } 75 EingabeOK := false; 76 end; 77 end; 78 end.
Die Methode TextAusgeben (Zeile 23) ist sehr einfach und muss wohl nicht näher erläutert werden). Die Methode ZahlAusgeben (Zeile 32) überprüft, ob überhaupt ein Zahlformat definiert wurde, und formatiert die übergebene Zahl nur dann. Falls das Zahlformat nicht gültig ist, erzeugt die Formatierung eine Ausnahme. Diese wird nicht behandelt, sondern einfach weitergegeben. Das aufrufende Programm wird so auf jeden Fall über ungültige Formate informiert. Die Methode TextEingeben (Zeile 47) ist wieder sehr einfach. Der eingegebene Text wird lediglich in der Eigenschaft EingegebenerText gespeichert. Da bei der Eingabe von Text keine Fehler auftreten können, wird die Eigenschaft EingabeOK in Zeile 56 bei jeder Eingabe auf true gesetzt. Es ist wichtig, dass Sie auch an solche Dinge denken. Die Eigenschaft EingabeOK kann aufgrund vorhergehender fehlerhafter Zahleingaben auch auf false gesetzt sein. Sie wissen nicht, wie ein Programmierer, der diese Klasse anwendet, mit dieser Eigenschaft umgeht. Auch wenn diese Eigenschaft bei der Eingabe von Text eigentlich nicht sinnvoll ist: Sie existiert und deshalb kann es sein, dass sie von einem Programmierer bei der Eingabe von Text berücksichtigt wird. Wenn die Methode TextEingabe diese Eigenschaft nicht auf true setzen würden, würde Ihre Klasse u. U. logische Fehler im Programm verursachen, die dann vielleicht nur sehr schwierig lokalisiert werden könnten. Die Methode zur Eingabe einer Zahl (Zeile 59) ist etwas komplexer. Hier kann es sein, dass die Eingabe des Anwenders nicht in eine Zahl konvertiert werden kann. Deshalb wird die Eingabe zunächst in einer StringVariablen entgegengenommen (Zeile 64). Um zu erkennen, ob die Eingabe konvertiert werden kann, wird die Konvertierung innerhalb einer Ausnahmebehandlung vorgenommen, die in Zeile 68 beginnt. Falls die
377
Sandini Bib
Konvertierung fehlerfrei ausgeführt werden kann, führt das Programm die Anweisung in Zeile 72 aus und setzt damit die Eigenschaft EingabeOK auf true. Schlägt die Konvertierung fehl, verzweigt das Programm in den except-Block, der in Zeile 73 beginnt, und setzt die Eigenschaft EingabeOK auf false. Wenn Sie diese Klasse ausprobieren, hält der Delphi/Kylix-Debugger das Programm per Voreinstellung an, wenn Sie eine ungültige Zahl eingeben und folglich die Ausnahme eintritt. Dieses Problem habe ich bereits in Kapitel 5, bei der Besprechung von Referenzargumenten, angesprochen. Um dieses Verhalten umzustellen, müssen Sie festlegen, dass der Debugger entweder bei allen Ausnahmen oder nur bei spezifischen nicht anhält. Die dazu notwendigen Einstellungen finden Sie in den Debugger-Optionen (im Menü TOOLS/DEBUGGER OPTIONS bzw. TOOLS/DEBUGGER-OPTIONEN). Im Register LANGUAGE EXCEPTIONS bzw. SPRACH-EXCEPTIONS können Sie festlegen, dass der Debugger bei Ausnahmen grundsätzlich nicht anhält oder dass er lediglich spezielle Ausnahmen ignoriert. Den Typ der Ausnahme finden Sie heraus, wenn Sie das Programm testen und einen ungültigen Wert eingeben. Delphi und Kylix nennen den Typ der Ausnahme in der erscheinenden Ausnahme-Meldung. Die Ausnahme, die in unserem Fall erzeugt wird, ist vom Typ EConvertError.
6.6
Die Referenz self bzw. this
Innerhalb einer Methode können Sie auf die anderen Methoden oder die Eigenschaften des Objekts zugreifen. Die Methoden der KonsoleToolKlasse greifen beispielsweise auf alle Eigenschaften des Objekts zurück. Normalerweise schreiben Sie dazu einfach den Namen des jeweiligen Elements, wie dies in der Methode ZahlAusgeben in Zeile 38 und 40 z. B. der Fall ist: 36 37 38 39 40
{ Zahl formatieren if Zahlformat > '' formatierteZahl else formatierteZahl
bzw. in einen String konvertieren } then := FormatFloat(Zahlformat, Zahl) := FloatToStr(Zahl);
Wenn Sie einen Bezeichner wie formatierteZahl verwenden, sucht der Compiler zunächst innerhalb der aktuellen Methode nach einer Variable oder einem Argument, die/das einen solchen Namen trägt. Wird er dort nicht fündig, sucht er eine Ebene höher, also in den Eigenschaften und Methoden. Findet er dort auch nichts, sucht er noch nach globalen Variablen, Funktionen und Prozeduren mit dem eingegebenen Namen.
378
Sandini Bib
In unserem Beispiel funktioniert der gewünschte Zugriff, weil innerhalb der Methode keine Variablen oder Argumente mit einem identischen Namen deklariert sind. Der Compiler schreibt die Daten in die Eigenschaften. Wenn aber innerhalb einer Methode gleichnamige Variablen oder Argumente vorkommen, müssen Sie dem Compiler explizit mitteilen, dass Sie auf Eigenschaften bzw. Methoden zugreifen wollen. Als (zugegebenermaßen etwas konstruiertes) Beispiel deklariere ich die Methode TextEingeben ein wenig um: 47 procedure KonsoleTool.TextEingeben(InfoText: string); 48 var EingegebenerText: string; 49 begin 50 { Den Benutzer einen Text eingeben lassen } 51 write(InfoText); 52 readln(EingegebenerText); 53 { Eingabe in der Eigenschaft ablegen } 54 EingegebenerText := EingegebenerText; 55 { bei Texteingaben ist die Eingabe immer OK } 56 EingabeOK := true; 57 end;
Das Problem wird hier deutlich in Zeile 54 sichtbar. Für den Compiler bedeutet diese Anweisung, dass das Programm den Wert der Variablen EingegebenerText aus dieser Variable ausliest und wieder dort hineinschreibt. Abgesehen davon, dass der Compiler solche Anweisungen einfach wegoptimiert, ist die Zuweisung unsinnig. Um solche Probleme zu lösen, bieten Delphi, Kylix und Java eine spezielle Referenz an, die die Instanz referenziert, mit der das Programm gerade arbeitet. In Delphi und Kylix heißt diese Referenz self, in Java this. Diese Referenzen werden innerhalb der Methoden der Klasse verwendet wie eine normale Referenz. Sie können also z. B. explizit auf Elemente der Klasse verweisen: 54
self und this referenzieren das aktuelle Objekt
self.EingegebenerText := EingegebenerText;
Auch wenn Sie es jetzt vielleicht noch nicht glauben, werden Sie self und this sehr häufig benötigen. Eine typische Anwendung ist z. B. die in Methoden, die zum Setzen von Eigenschaften verwendet werden (wie es bei der Kapselung verwendet wird, die ich ab Seite 396 beschreibe):
'LH 5HIHUHQ] VHOI E]Z WKLV
379
Sandini Bib
01 public class Kreis 02 { 03 double Radius; 04 void setRadius(double Radius) 05 { 06 this.Radius = Radius; 07 } 08 }
Setzen Sie self bzw. this konsequent ein, wenn Sie innerhalb von Methoden auf Elemente der Klasse zugreifen. Dann erkennen Sie am Quellcode sehr deutlich, dass es sich dabei um einen Methoden- oder Eigenschaften-Zugriff handelt. Ihr Programm wird leichter lesbar, verständlicher und damit auch besser wartbar. Ich halte mich daran und setze im weiteren Verlauf dieses Kapitels self bzw. this ein, wenn eine Methode auf Eigenschaften oder andere Methoden einer Klasse zugreift. Woher stammt this bzw. self? Es ist ziemlich interessant zu erfahren, woher self und this stammen. Wenn Sie das wissen, können Sie auch erklären, wie es sein kann, dass ein Programm die Methoden einer Klasse lediglich einmalig speichert, auch wenn mehrere Instanzen der Klasse erzeugt werden. Wie Sie ja bereits wissen, werden die Methoden einer Klasse immer nur einmalig im Arbeitsspeicher abgelegt. Eine Instanz einer Klasse speichert lediglich die Eigenschaften, also die Daten der Objekte. In Objekten einer Klasse, die Kreisdaten verwalten soll, 01 public class Kreis 02 { 03 double Radius; 04 double Umfang() 05 { 06 return Radius * 2 * 3.1415927; 07 } 08 }
wird der Radius beispielsweise in den Instanzen verwaltet, die Methode Umfang hingegen separat. Wenn Sie z. B. drei Instanzen dieser Klasse erzeugen:
380
Sandini Bib
Kreis kreis1 = new Kreis(); kreis1.Radius = 100; Kreis kreis2 = new Kreis(); kreis2.Radius = 150; Kreis kreis3 = new Kreis(); kreis3.Radius = 200;
belegt das Programm zwar drei Bereiche im Arbeitspeicher. Diese speichern aber nur die Daten, in unserem Fall den Radius. Wenn Sie im Programm eine Methode aufrufen, System.out.println(kreis1.Umfang());
erkennt der Compiler am Datentyp der Referenz, die Sie verwenden (kreis1), um welche Klasse es sich handelt. Da der Compiler die Methoden der Klasse selbst im Speicher ablegt, weiß er, wo diese zu finden sind (relativ von der Start-Adresse der Anwendung im Arbeitsspeicher aus gesehen). Er setzt den Methodenaufruf so um, dass die Methode im Programm korrekt aufgerufen wird. Die Methode, die ja lediglich einmalig vorhanden ist, muss aber wissen, von welcher Instanz aus sie aufgerufen wurde. Wenn die Methode Eigenschaften der Klasse liest oder beschreibt, muss ja schließlich die richtige Instanz dazu verwendet werden. Und hier kommen self und this ins Spiel. Der Compiler erweitert die Argumentliste von Methoden nämlich implizit um ein Argument vom Typ der Klasse, das bei Delphi und Kylix self und bei Java this heißt. Die Methode Umfang sieht dann im kompilierten Programm prinzipiell so aus: 04 05 06 07
double Umfang(Kreis this) { return this.Radius * 2 * 3.1415927; }
Beim Aufruf einer Methode übergibt der Compiler automatisch eine Referenz auf die Instanz, von der aus der Aufruf erfolgt, an dieses neue Argument (Zeile 4). Die Methode kann darüber nun die Instanz lokalisieren und verwendet immer die korrekten Daten. Um dies zu ermöglichen, assoziiert der Compiler beim Kompilieren Eigenschaften und Methoden, die innerhalb der Methode verwendet werden, zudem automatisch mit der this- bzw. self-Referenz, falls dies noch nicht der Fall ist (Zeile 6).
'LH 5HIHUHQ] VHOI E]Z WKLV
381
Sandini Bib
6.7
Private und öffentliche Elemente einer Klasse
Eine Klasse sollte natürlich Eigenschaften und Methoden besitzen, die von außen über eine Referenz auf eine Instanz dieser Klasse verwendet werden können. Diese Elemente werden als öffentliche (Public-)Elemente bezeichnet. Innerhalb einer Klasse gibt es aber auch immer wieder den Bedarf, Daten zu speichern und Methoden zu programmieren, die privat sind und nur innerhalb der Klasse verwendet werden können. Private Eigenschaften werden häufig für die Kapselung verwendet, die ich weiter unten, ab Seite 396, beschreibe. Private Methoden werden häufig verwendet, wenn ein bestimmter Algorithmus innerhalb mehrerer (öffentlicher) Methoden einer Klasse benötigt wird. Im Prinzip ist das vergleichbar mit privaten Variablen und privaten Funktionen/Prozeduren in den Modulen der strukturierten Programmierung. In dem Zusammenhang spricht man übrigens häufig von der Sichtbarkeit von Klassenelementen. Öffentliche Elemente sind innerhalb und außerhalb der Klasse sichtbar, private Elemente nur innerhalb der Methoden der Klasse. Jede Programmiersprache stellt Ihnen Sprachelemente zur Verfügung, über die Sie die Sichtbarkeit der Klassenelemente festlegen können. Sichtbarkeit in Delphi/Kylix-Klassen In einer Object Pascal-Klasse sind alle Elemente per Voreinstellung öffentlich. Sie können über die Schlüsselwörter private und public aber auch Blöcke für private bzw. öffentliche Elemente einrichten. Das folgende Beispiel demonstriert dies an einer Klasse, die zur Speicherung von Kreisdaten verwendet werden soll. Die Methode PI, die den Wert der gleichnamigen Kreiszahl berechnen und zurückgeben soll, ist als private Methode deklariert. Diese Methode wird von den Methoden Umfang und Oberflaeche aufgerufen, die PI für die Berechnung benötigen. PI wird berechnet, weil diese Zahl möglichst genau ermittelt werden soll. Um das Beispiel übersichtlich zu programmieren, habe ich nur eine einfache Berechnung von PI implementiert. Komplexere Algorithmen wie der von Euler berechnen PI wesentlich genauer. Im Internet finden Sie mehr Informationen dazu.
382
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
unit CKreis; interface uses Math; type Kreis = class { Private Klassenelemente } private function PI(): extended; { Öffentliche Klassenelemente } public Radius: double; function Umfang(): extended; function Oberflaeche(): extended; end; implementation function Kreis.PI(): extended; var i: integer; begin { PI möglichst einfach berechnen } result := 4 * ArcTan(1); end; function Kreis.Umfang(): extended; begin result := self.PI() * 2 * self.Radius; end; function Kreis.Oberflaeche(): extended; begin result := self.PI() * self.Radius * self.Radius; end; end.
Alle Deklarationen unterhalb von private oder public bis zum nächsten Sichtbarkeitsschlüsselwort bzw. bis zum Ende der Klassendeklaration besitzen die entsprechende Sichtbarkeit. Die Methode PI kann in diesem Beispiel nur innerhalb der Klasse aufgerufen werden, was ja auch in den Methoden zur Berechnung des Umfangs und der Oberfläche in Zeile 30 und 35 geschieht. Von außen – über eine Instanz dieser Klasse –
383
Sandini Bib
können nur die öffentlichen Elemente, also im Beispiel die Eigenschaft Radius und die Methoden Umfang und Oberflaeche verwendet werden: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
program PrivateElemente; {$APPTYPE CONSOLE} uses SysUtils, CKreis; var kreis1, kreis2: Kreis; begin { Zwei Instanzen der Klasse erzeugen } kreis1 := Kreis.Create(); kreis2 := Kreis.Create(); { Instanzen initialisieren } kreis1.Radius := 0.5; kreis2.Radius := 25; { und verwenden } writeln('Ein Kreis mit einem Radius von ', kreis1.Radius, ' besitzt einen Umfang von ', kreis1.Umfang()); writeln('Ein Kreis mit einem Radius von ', kreis2.Radius, ' besitzt einen Umfang von ', kreis2.Umfang()); end.
Wenn Sie versuchen, die Methode PI aufzurufen, writeln(kreis1.PI());
meldet der Compiler den Fehler „Undeclared identifier: 'PI'“. Diese Fehlermeldung ist etwas irreführend, weil PI ja deklariert ist. Der Compiler sollte besser melden, dass diese Methode von außen nicht aufgerufen werden kann.
384
Sandini Bib
Sie können in Delphi und Kylix private und public auch so verwenden, wie es in anderen Sprachen üblich ist, und diese Schlüsselwörter direkt vor jede Deklaration setzen: type Kreis = class private function PI(): extended; public Radius: double; public function Umfang(): extended; public function Oberflaeche(): extended; end;
Diese Art der Deklaration ist wesentlich sprechender und auch näher am allgemeinen Standard. Sichtbarkeit in Java-Klassen In Java deklarieren Sie private und öffentliche Elemente ebenfalls über die Schlüsselwörter private und public. Sie müssen diese Schlüsselwörter allerdings jeder Eigenschaft und Methode voranstellen: 01 public class Kreis 02 { 03 public double Radius; 04 05 private double PI() 06 { 07 return 4 * Math.atan(1); 08 } 09 10 public double Umfang() 11 { 12 return this.PI() * this.Radius * 2; 13 } 14 15 public double Oberflaeche() 16 { 17 /* PI möglichst einfach berechnen */ 18 return this.PI() * this.Radius * this.Radius; 19 } 20 21 }
Geben Sie weder private noch public an, ist das Element automatisch öffentlich. Wie bei Delphi und Kylix können die Methoden der Klasse auf alle Elemente zugreifen, auch auf private. Von außen kann ein Programm aber nur öffentliche Elemente verwenden:
385
Sandini Bib
01 public class PrivateElemente 02 { 03 public static void main(String[] args) 04 { 05 /* Zwei Instanzen der Kreis-Klasse erzeugen */ 06 Kreis kreis1 = new Kreis(); 07 Kreis kreis2 = new Kreis(); 08 09 /* Instanzen initialisieren */ 10 kreis1.Radius = 0.5; 11 kreis2.Radius = 25; 12 13 /* und verwenden */ 14 System.out.println("Ein Kreis mit einem Radius von " + 15 kreis1.Radius + " besitzt einen Umfang von " + 16 kreis1.Umfang()); 17 18 System.out.println("Ein Kreis mit einem Radius von " + 19 kreis2.Radius + " besitzt einen Umfang von " + 20 kreis2.Umfang()); 21 22 } 23 24 }
Wenn Sie in Java-Programmen versuchen, ein privates Element einer Klasse zu verwenden, meldet der Compiler den Fehler „Elemementname has private access5 in Klassenname“. Diese Fehlermeldung ist wesentlich aussagekräftiger als die des Delphi/Kylix-Compilers.
6.8
Überladen von Methoden
In einigen Fällen ist es sinnvoll, mehrere Varianten einer Methode zu besitzen, die unterschiedliche Argumente besitzen. Dieses Konzept setzt besonders Java sehr intensiv ein. Die println-Methode des System.outObjekts existiert in Java 1.4 z. B. in zehn verschiedenen Varianten. Die erste Variante besitzt kein Argument, die anderen Varianten arbeiten mit einem Argument unterschiedlichen Datentyps. So können Sie beim Aufruf dieser Methode entweder kein Argument oder eines mit einem nahezu beliebigen Datentyp übergeben. In Wirklichkeit handelt es sich aber nicht um nur eine einzige Methode. println ist tastsächlich zehn Mal deklariert. Lediglich die Argumentliste 5.
386
Zugriff
Sandini Bib
unterscheidet sich bei den verschiedenen Deklarationen. Und das ist genau der Punkt: Überladene Methoden besitzen denselben Namen, unterscheiden sich aber in der Argumentliste. Mit diesem Wissen können Sie eigene überladene Methoden in Ihre Klassen implementieren. In einer Klasse zur Speicherung von Personendaten wäre es z. B. sinnvoll, Methoden zu besitzen, über die die Eigenschaften der Objekte über eine einzige Anweisung beschrieben werden könnten. Wenn die Klasse folgende Eigenschaften besitzt:
Überladene Methoden in Java
01 public class Person 02 { 03 /* Eigenschaften */ 04 public String Vorname; 05 public String Nachname; 06 public String Ort;
wären zwei Varianten einer Methode Init zum Setzen dieser Eigenschaften sinnvoll: 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 }
/* Methode zum Schreiben des Vor- und Nachnamens */ public void Init(String vorname, String nachname) { this.Vorname = vorname; this.Nachname = nachname; } /* Zweite Variante der Init-Methode */ public void Init(String vorname, String nachname, String ort) { this.Vorname = vorname; this.Nachname = nachname; this.Ort = ort; }
Wenn Sie nun Instanzen dieser Klasse besitzen, können Sie die eine oder andere dieser Methoden aufrufen: /* Instanzen der Person-Klasse erzeugen */ Person person1 = new Person(); Person person2 = new Person(); /* Instanzen initialisieren */ person1.Init("Zaphod", "Beeblebrox"); person2.Init("Fred-Bogus", "Trumper", "New York");
387
Sandini Bib
Der Compiler entscheidet an Hand der Anzahl und des Datentyps der übergebenen Argumente, welche Variante er aufrufen muss. Im Beispiel sucht er beim ersten Aufruf nach einer Variante mit Argumenten, die dem folgenden Muster entsprechen: String, String. Beim zweiten Aufruf wird das Muster String, String, String mit den vorhandenen Methoden verglichen. Daraus ergibt sich, dass überladene Methoden sich entweder in der Anzahl oder den Datentypen der einzelnen Argumente unterscheiden müssen. Der Rückgabewert von Methoden wird nicht vom Compiler zur Unterscheidung verwendet (weil der Compiler nicht erkennen kann, welcher Rückgabetyp in einem Ausdruck, der eine Methode aufruft, erwartet wird). Überladene Methoden in Delphi und Kylix
In Delphi und Kylix funktioniert das Ganze prinzipiell auf dieselbe Weise. Sie müssen überladene Methoden hier allerdings mit dem Schlüsselwort overload kennzeichnen, das Sie an die Methodendeklaration anhängen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
388
unit CPerson; interface type Person = class public Vorname: string; public Nachname: string; public Ort: string; { Methode zum Schreiben des Vor- und Nachnamens } public procedure Init(vorname: string; nachname: string); overload; { Zweite Variante der Init-Methode } public procedure Init(vorname: string; nachname: string; ort: string); overload; end; implementation { Methode zum Schreiben des Vor- und Nachnamens } procedure Person.Init(vorname: string; nachname: string); begin self.Vorname := vorname; self.Nachname := nachname; end;
Sandini Bib
28 { Zweite Variante der Init-Methode } 29 procedure Person.Init(vorname: string; nachname: string; 30 ort: string); 31 begin 32 self.Vorname := vorname; 33 self.Nachname := nachname; 34 self.Ort := ort; 35 end; 36 37 end.
Im nächsten Abschnitt beschreibe ich Konstruktoren, über die Sie Instanzen direkt bei der Erzeugung initialisieren können. Diese machen die hier entwickelte Init-Methode scheinbar überflüssig. Bedenken Sie aber, dass diese Methode lediglich ein Beispiel für das Überladen von Methoden sein soll. Außerdem ist diese Methode u. U. auch nützlich, wenn Konstruktoren vorhanden sind, da sie auch nach der Erzeugung einer Instanz aufgerufen werden kann.
6.9
Initialisieren von Klassen: Konstruktoren
Objekte müssen recht häufig bei der Erzeugung initialisiert werden. Bisher sind wir dabei so vorgegangen, dass wir nach der Erzeugung die entsprechenden Eigenschaften beschrieben haben: Person person1 = new Person(); person1.Vorname = "Zahphod"; person1.Nachname = "Beeblebrox";
Das ist allerdings etwas umständlich. Es wäre wesentlich besser, wenn das Objekt direkt bei der Erzeugung initialisiert werden könnte: Person person1 = new Person("Zaphod", "Beeblebrox");
Und genau dafür setzen Sie Konstruktoren ein. Ein Konstruktor wird immer dann automatisch aufgerufen, wenn ein Objekt erzeugt wird. Sie können eigene Konstruktoren in eine Klasse integrieren, um das Objekt bei der Erzeugung zu initialisieren. Konstruktoren sind Methoden sehr ähnlich, werden aber etwas anders deklariert. Grundsätzlich geben Konstruktoren keinen Rückgabewert zurück, dürfen also nicht als Funktion deklariert werden.
389
Sandini Bib
6.9.1 Konstruktoren in Java In Java muss ein Konstruktor den Namen der Klasse tragen und darf nicht mit einem Rückgabetyp gekennzeichnet werden. Ansonsten gleicht die Deklaration der einer Methode. Sie können so viele Argumente übergeben, wie Sie benötigen: 01 public class Person 02 { 03 /* Eigenschaften */ 04 String Vorname; 05 String Nachname; 06 07 /* Konstruktor */ 08 public Person(String vorname, String nachname) 09 { 10 this.Vorname = vorname; 11 this.Nachname = nachname; 12 } 13 14 }
Innerhalb des Konstruktors können Sie alles mögliche programmieren. In den meisten Konstruktoren werden Eigenschaften mit den Werten übergebener Argumente initialisiert, so wie es im Beispiel auch der Fall ist. In seltenen Fällen werden Konstruktoren aber auch verwendet, um Ressourcen zu öffnen, mit denen das Objekt arbeiten muss. Ein Objekt zum Schreiben einer Textdatei sollte z. B. im Konstruktor die zu beschreibende Datei öffnen oder erzeugen, damit die Methode, die zum Schreiben verwendet wird, eine geöffnete Datei benutzen kann. Solch ein Konstruktor würde dann den Dateinamen übergeben bekommen. Wenn Sie nun eine Instanz der Klasse erzeugen, sind Sie gezwungen, die Argumente entsprechend zu belegen: Person person1 = new Person("Zaphod", "Beeblebrox");
Der Compiler lässt eine Erzeugung ohne Argumente nicht mehr zu: Person person1 = new Person(); // Fehler
Er meldet den Fehler „Cannot resolve symbol“, weil er keinen Konstruktor findet, der keine Argumente besitzt. Der Standardkonstruktor
390
Sie werden sich nun wahrscheinlich fragen, warum es denn vorher immer möglich war, Instanzen ohne Argumente zu erzeugen. Die Antwort darauf ist, dass der Compiler einer Klasse automatisch einen Konstruktor hinzufügt, wenn diese keinen eigenen besitzt. Dieser Konstruktor
Sandini Bib
macht nichts weiter und besitzt deshalb auch keine Argumente, kann aber eben bei der Erzeugung einer Instanz der Klasse aufgerufen werden. Dieser Konstruktor wird auch als Standardkonstruktor bezeichnet. Der Java-Compiler fügt einen Standardkonstruktor nur dann hinzu, wenn die Klasse keine eigenen Konstruktoren implementiert. Wenn Sie ermöglichen wollen, dass eine Instanz der Klasse auch ohne Argumente erzeugt werden kann, müssen Sie einen eigenen Standardkonstruktor implementieren: 01 public class Person 02 { 03 /* Eigenschaften */ 04 String Vorname; 05 String Nachname; 06 07 /* Standardkonstruktor, der nichts weiter macht, aber ermöglicht, 08 dass Instanzen der Klasse auch ohne Argumente erzeugt werden 09 können */ 10 public Person() 11 { 12 } 13 14 /* Spezieller Konstruktor */ 15 public Person(String vorname, String nachname) 16 { 17 this.Vorname = vorname; 18 this.Nachname = nachname; 19 } 20 21 }
Sie können nun so viele Konstruktoren in die Klasse integrieren, wie Sie benötigen. Wie bei überschriebenen Methoden müssen sich diese lediglich in den Argumenten unterscheiden. Wenn die Klasse z. B. weitere Eigenschaften zur Speicherung der Adresse besitzt, können Sie in einem weiteren Konstruktor zusätzlich zum Namen auch den Ort und die Straße übergeben. Dabei müssen Sie lediglich überlegen, welche Varianten sinnvoll sind. Da Sie über das Vorhandensein des Standardkonstruktors auch entscheiden können, ob eine Instanz ohne Argumente erzeugt werden kann, können Sie auch erzwingen, dass eine erzeugte Instanz immer initialisiert werden muss. Lassen Sie den Standardkonstruktor dazu einfach weg. Eine Klasse, die zum Schreiben von Textdateien verwendet werden soll und die im Konstruktor diese Datei öffnen muss, muss beispielsweise mit einem Dateinamen initialisiert werden. Ein Standard-
Initialisierung erzwingen
391
Sandini Bib
konstruktor wäre hier nicht angebracht, weil in diesem der Dateiname nicht bekannt wäre.
6.9.2 Konstruktoren in Delphi und Kylix Konstruktoren werden in Delphi und Kylix über das Schlüsselwort constructor nach dem folgenden Schema deklariert: constructor Name([Argumentliste]);
Als Name können Sie prinzipiell jeden verwenden, Sie sollten sich aber an das Delphi/Kylix-Schema halten und den Konstruktor Create nennen. Konstruktoren können wie bei Java überladen werden. Da Delphi/Kylix-Klassen den Konstruktor zumindest von der impliziten Basisklasse TObject erben, sollten Sie sich angewöhnen, als erste Anweisung in einem Konstruktor den geerbten Konstruktor aufzurufen. Delphi und Kylix stellen dazu das Schlüsselwort inherited („vererbt“) zur Verfügung. Da Sie über dieses Schlüsselwort auch geerbte Methoden aufrufen können, müssen Sie den Namen des Konstruktors angeben: inherited Create;
Sie stellen damit sicher, dass der Konstruktor der Basisklasse auf jeden Fall und in der richtigen Reihenfolge aufgerufen wird. Wird ein Objekt Ihrer Klasse erzeugt, wird erst der Konstruktor von TObject aufgerufen und danach erst Ihrer. Für TObject ist das zwar scheinbar nicht unbedingt notwendig (es funktioniert auch ohne einen Aufruf des geerbten Konstruktors). Spätestens aber, wenn Sie Ihre Klassen von speziellen anderen Klassen ableiten, wird der Aufruf des geerbten Konstruktors sehr wichtig. In Java ist dieser Aufruf übrigens nur in Sonderfällen notwendig. Der Java-Compiler integriert einen Aufruf des geerbten Konstruktors automatisch in die Konstruktoren neuer Klassen. Die Java-Klasse zur Speicherung von Personendaten sieht in Delphi und Kylix dann so aus:
392
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
unit CPerson; interface type Person = class public Vorname: string; public Nachname: string; public Ort: string; { Konstruktor } constructor Create(vorname: string; nachname: string); overload; { Zweite Variante des Konstruktors } constructor Create(vorname: string; nachname: string; ort: string); overload; end; implementation { Konstruktor } constructor Person.Create(vorname: string; nachname: string); begin { Aufruf des geerbten Konstruktors } inherited Create; { Initialisieren der Eigenschaften } self.Vorname := vorname; self.Nachname := nachname; end; { Zweite Variante des Konstruktors } constructor Person.Create(vorname: string; nachname: string; ort: string); begin { Aufruf des geerbten Konstruktors } inherited Create; { Initialisieren der Eigenschaften } self.Vorname := vorname; self.Nachname := nachname; self.Ort := ort; end; end.
393
Sandini Bib
Beim Erzeugen von Instanzen können Sie nun die entsprechenden Argumente übergeben: person1 := Person.Create('Zaphod', 'Beeblebrox'); person2 := Person.Create('Fred-Bogus', 'Trumper', 'New York');
Anders als viele andere Compiler integriert der Delphi/Kylix-Compiler den Standardkonstruktor auch in Klassen, die eigene Konstruktoren implementieren. Sie können also auch eine Instanz der Person-Klasse ohne Argumente erzeugen, ohne dass Sie einen eigenen Standardkonstruktor in die Klasse integrieren müssen.
6.10 Aufräumarbeiten: Destruktoren Destruktoren werden für Aufräumarbeiten verwendet, die beim Zerstören eines Objekts automatisch ausgeführt werden müssen. Ein Destruktor wird immer dann automatisch aufgerufen, wenn ein Objekt zerstört wird. Destruktoren benötigen Sie eigentlich seltener. Nur dann, wenn ein Objekt in einem Konstruktor externe Ressourcen öffnet, z. B. eine Datei, werden diese üblicherweise im Destruktor wieder geschlossen. Verwenden Sie Destruktoren möglichst sparsam und bedächtig. Es ist wesentlich sinnvoller, einer Klasse, die mit einer externen Ressource (Datei, Datenbank etc.) arbeiten soll, Methoden zum Öffnen und Schließen dieser Ressource hinzuzufügen, als die Ressource im Konstruktor zu öffnen und im Destruktor zu schließen. Über separate Methoden kann der Programmierer, der die Klasse einsetzt, entscheiden, wann die Ressource geöffnet und wieder geschlossen wird. Ein entsprechend erstellter Konstruktor und Destruktor lässt dem Programmierer aber keine Wahl. Die vielen Klassen aus verschiedenen Bibliotheken, die genau diesen Weg gehen, den ich hier vorschlage, beweisen, dass es der richtige Weg ist. Ein großes Problem mit Destruktoren in Java ist, dass Sie nie wissen, wann der Garbage Collector endlich Zeit hat, die verwaisten Objekte zu zerstören. Erst dann wird nämlich der Destruktor aufgerufen. In JavaProgrammen kann es deswegen vorkommen, dass eine externe Ressource, die erst im Destruktor geschlossen wird, wesentlich länger geöffnet bleibt, als Sie dies erwarten. Das kann dann auch zu Problemen führen, wenn nämlich ein anderer Programmteil genau diese Ressource benötigt. Ich beschreibe Destruktoren deshalb hier nur an einem abstrakten Beispiel.
394
Sandini Bib
6.10.1 Destruktoren in Delphi und Kylix In Delphi wird ein Destruktor mit dem Schlüsselwort destructor ähnlich einem Konstruktor deklariert. Als Name müssen Sie Destroy angeben, obwohl Sie ja zum Zerstören eines Objekts die Free-Methode aufrufen und nicht Destroy. Die Erklärung dafür ist, dass Free noch ein wenig mehr macht, als nur das Objekt zu zerstören. Free überprüft nämlich zuvor, ob das Objekt überhaupt noch existiert, und ruft nur dann Destroy auf. Im Fehlerfall erzeugt Free eine Ausnahme. Damit stellen Delphi und Kylix sicher, dass beim mehrfachen Zerstören keine kritischen Speicherfehler entstehen können. Da ein Destruktor automatisch aufgerufen wird, darf er keine Argumente besitzen (wer sollte diese auch übergeben!?). Ähnlich dem Konstruktor sollten Sie im Destruktor den von TObject geerbten Destruktor aufrufen, damit dieser seine eventuellen Aufräumarbeiten ausführen kann. Dieser Aufruf sollte immer am Ende Ihres Destruktors erfolgen, damit die korrekte Aufrufreihenfolge beim Zerstören gewährleistet ist. Eine Demo-Klasse mit einem einfachen Destruktor sieht dann so aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19
unit CDemo; interface type Demo = class { Destruktor } destructor Destroy; override; end; implementation { Destruktor } destructor Demo.Destroy; begin writeln('Der Destruktor wurde aufgerufen'); { Aufruf des geerbten Destruktors } inherited destroy; end; end.
395
Sandini Bib
Der Destruktor muss mit dem Schlüsselwort override gekennzeichnet werden. Damit stellen Sie sicher, dass beim Aufruf von Free auch wirklich Ihr und nicht der von TObject geerbte Destruktor aufgerufen wird. Ohne dieses Schlüsselwort kompiliert Delphi bzw. Kylix das Programm zwar auch, beim Aufruf von Free wird Ihr Destruktor aber gar nicht aufgerufen. Probieren Sie das einfach einmal selbst mit einem Destruktor mit und ohne override aus.
6.10.2 Destruktoren in Java In Java wird ein Destruktor folgendermaßen deklariert: protected void finalize() { }
Das protected-Schlüsselwort besitzt eine besondere Bedeutung, das bei der Vererbung wichtig wird. Der Destruktor sollte auf jeden Fall mit diesem Schlüsselwort deklariert werden. Eine public-Deklaration dürfen Sie nicht vornehmen, weil der Destruktor nicht von außen aufgerufen werden darf. Eine private-Deklaration würde bei abgeleiteten Klassen verhindern, dass diese in ihren Destruktoren den geerbten Destruktor aufrufen. Ansonsten müssen Sie nichts weiter beachten. Der Garbage Collector ruft den Destruktor automatisch auf, wenn er ein Objekt zerstört. Der Compiler integriert den Aufruf des von Object geerbten Destruktors automatisch in neue Destruktoren, so dass Sie diese in Java nicht explizit aufrufen müssen.
6.11 Datenkapselung Die Datenkapselung, die auch kurz als Kapselung bezeichnet wird, ist ein wichtiges Grundkonzept der OOP. Kapselung bedeutet, dass Objekte den Zugriff auf ihre Daten selbst kontrollieren. Besitzt ein Objekt nur einfache Eigenschaften, kann es den Zugriff auf seine Daten nicht kontrollieren. Ein Programmierer, der dieses Objekt benutzt, kann in die Eigenschaften hineinschreiben, was immer zu dem Datentyp der Eigenschaften passt. Das kann dazu führen, dass das Objekt ungültige Daten speichert und damit u. U. beim Aufruf von Methoden fehlerhaft reagiert. Das Objekt zum Drucken von Daten, das wir ab Seite 370 entworfen haben, ist ein gutes Beispiel dafür. Stellt der Programmierer die Eigenschaften, die die Ränder bestimmen, z. B. auf negative Werte ein, wird der Ausdruck wahrscheinlich etwas konfus aussehen. Kontrolliert das Ob-
396
Sandini Bib
jekt jedoch den Zugriff auf diese Eigenschaften, so ist ein fehlerhaftes Einstellen (wie z. B. auf negative Werte) erst gar nicht möglich. Das Objekt kann dann unter allen Umständen sicher arbeiten. Kapselung können Sie auf zwei Arten realisieren. Die ältere Methode ist, die zu kapselnden Daten des Objekts in privaten Eigenschaften zu speichern und für den Zugriff auf diese Daten jeweils eine Methode zum Schreiben und eine zum Lesen des Werts zur Verfügung zu stellen. Diese Methoden können den Zugriff auf die Daten sehr gut kontrollieren. Die andere, modernere Variante stellen nicht alle Programmiersprachen zur Verfügung: In dieser Variante können Sie Eigenschaften so deklarieren, dass diese die Methoden zum Lesen und Schreiben bereits integrieren. Solche Eigenschaften sehen nach außen aus wie normale Eigenschaften, rufen aber beim Schreiben und beim Lesen eine spezielle Methode auf.
Klassische und moderne Kapselung
6.11.1 Die klassische Kapselung in Java In Java können Sie Daten lediglich auf die klassische Art kapseln. Verwalten Sie die Daten dazu in privaten Variablen und stellen Sie eine Lese- und eine Schreibmethode zur Verfügung. Die Schreibmethode kann entscheiden, welche Daten in das Objekt geschrieben werden, die Lesemethode entscheidet, wie die Daten zurückgegeben werden. Das folgende Beispiel demonstriert die klassische Kapselung an einer KreisKlasse, die den Zugriff auf den Radius kontrolliert: 01 public class Kreis 02 { 03 /* Private Eigenschaft für den Radius */ 04 private double radius; 05 06 /* Methode zum Setzen des Radius */ 07 public void setRadius(double wert) 08 { 09 /* Überprüfen des Werts */ 10 if (wert > 0) 11 this.radius = wert; 12 } 13 14 /* Methode zum Lesen des Radius */ 15 public double getRadius() 16 { 17 return this.radius; 18 } 19
'DWHQNDSVHOXQJ
397
Sandini Bib
20 21 22 23 24 25 26 }
/* Methode zum Lesen des Umfangs */ public double getUmfang() { return this.radius * 2 * 3.1415927; }
Der Radius ist nun so gekapselt, dass nur das Schreiben eines Wertes größer Null möglich ist. Das Lesen ist ohne Einschränkungen möglich. Diese Klasse implementiert noch eine weitere Methode zum gekapselten Zugriff. Die Methode getUmfang ermittelt den Umfang und gibt diesen zurück. Im Prinzip handelt es sich dabei auch um gekapselte Daten. Der Umfang ist sogar so gekapselt, dass er nur gelesen werden kann. Das Datum Umfang wird also noch wesentlich besser geschützt als der Radius. Dass der Umfang in Wirklichkeit aus dem Radius berechnet wird, muss das Programm, das diese Klasse anwendet, nicht wissen. Ich habe die Namen der Methoden mit set und get begonnen, weil das in Java so üblich ist. Bei der Verwendung werden nun die Methoden zum Setzen und zum Lesen aufgerufen: /* Instanz der Klasse Kreis erzeugen */ Kreis kreis1 = new Kreis(); /* Radius setzen */ kreis1.setRadius(100); /* Radius und Umfang ausgeben */ System.out.println("Ein Kreis mit einem Radius von " + kreis1.getRadius() + " besitzt einen Umfang von " + kreis1.getUmfang());
6.11.2 Bessere Kapselung mit Ausnahmen Etwas ungeschickt an dem vorherigen Beispiel ist, dass das aufrufende Programm nicht mitbekommt, dass die Schreibmethode fehlgeschlagen ist, wenn ein ungültiger Wert übergeben wird. Ein Anwender erkennt höchstens, dass die Berechnungen falsch sind. Das Programm arbeitet mit einem logischen Fehler weiter, wenn ein ungültiger Radius übergeben wird.
398
Sandini Bib
Eine Methode, die den Zugriff auf die Daten kontrolliert, muss dem Programmierer bzw. Anwender das Fehlschlagen der Methode also so mitteilen, dass dieser gezwungen ist das Fehlschlagen zu quittieren. Dafür verwenden Sie eine eigene Ausnahme, die Sie in der Methode zum Schreiben erzeugen. Ich will hier nicht zu tief auf das Thema Ausnahmen eingehen. Sie können eigene Ausnahmeklassen erzeugen, die Sie für Ihre Ausnahmen verwenden können. Ich setze aber einfach die Basisklasse aller Ausnahmen, die Klasse Exception ein. Für den Anfang reicht das vollkommen aus. Ausnahmen in Java Eine Ausnahme erzeugen Sie in Java über die throw-Anweisung. Dieser müssen Sie dabei ein neues Ausnahme-Objekt übergeben. Dazu können Sie ganz einfach die Exception-Klasse verwenden, der Sie im Konstruktor den Fehlertext übergeben können. Der Java-Compiler verlangt, dass Methoden, die Ausnahmen erzeugen, mit dem throws-Schlüsselwort unter Angabe der erzeugten Ausnahmen deklariert werden. Die Methode zum Schreiben des Radius sieht dann optimiert so aus: 07 08 09 10 11 12 13 14 15 16
public void setRadius(double wert) throws Exception { /* Überprüfen des Werts */ if (wert > 0) this.radius = wert; else /* Ausnahme erzeugen */ throw new Exception("Der Radius muss ein Wert " + "größer Null sein");
Im Programm muss diese Ausnahme nun abgefangen werden: /* Radius setzen */ try { kreis1.setRadius(-100); } catch (Exception e) { System.out.println("Radius konnte nicht gesetzt werden: " + e.getMessage()); }
'DWHQNDSVHOXQJ
399
Sandini Bib
Ausnahmen machen Programme fehlerfreier
Über die Methode getMessage erhalten Sie Zugriff auf die Fehlermeldung. Der Anwender wird nun auf jeden Fall darüber informiert, dass beim Setzen des Radius etwas nicht funktioniert hat. Der Programmierer kann nun gegebenenfalls nach einem logischen Fehler im Programm suchen, der den Radius zu klein eingestellt hat, und diesen beseitigen. Sie haben mit Ihrer Klasse dafür gesorgt, dass die Anwendung möglichst einfach und fehlerfrei ist. Ausnahmen in Delphi und Kylix In Delphi und Kylix erzeugen Sie eine Ausnahme ähnlich wie in Java, lediglich mit Hilfe der raise-Anweisung und mit den Object PascalTechniken zur Erzeugung von Objekten. Delphi und Kylix stellen wie Java eine Klasse Exception zur Verfügung, die Sie verwenden können: Das folgende Beispiel demonstriert, wie in Pascal eine Exception ausgelöst wird. Eine der Java Kreisklasse ähnliche Klasse sieht dann in Delphi bzw. Kylix so aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
400
unit CKreis; interface uses SysUtils; type Kreis = class { Private Eigenschaft für den Radius } private radius: double; { Methode zum Setzen des Radius } public procedure setRadius(wert: double); { Methode zum Lesen des Radius } public function getRadius(): double; { Methode zum Lesen des Umfangs } public function getUmfang(): double; end; implementation procedure Kreis.setRadius(wert: double); begin { Überprüfen des Werts } if wert > 0 then self.radius := wert
Sandini Bib
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
else { Ausnahme erzeugen } raise Exception.Create('Der Radius muss ein Wert ' + 'größer Null sein'); end; function Kreis.getRadius(): double; begin result := self.radius; end; function Kreis.getUmfang(): double; begin result := self.radius * 2 * 3.1415927; end; end.
Ein Programm, das diese Klasse verwendet, sollte die Ausnahme nun abfangen: 01 02 03 04 05 06 07 08 09 10 11 12 13
{ Instanz der Klasse Kreis erzeugen } kreis1 := Kreis.Create(); { Radius setzen } try kreis1.setRadius(-100); except on e: Exception do begin writeln('Radius konnte nicht gesetzt werden: ' + e.Message); end; end;
Anders als Java zwingen Delphi und Kylix ein Programm aber nicht dazu, Ausnahmen abzufangen.
6.11.3 Moderne Kapselung Die Kapselung der Daten eines Objekts über Zugriffsmethoden ist in der Praxis bei der Verwendung des Objekts oft etwas mühselig. In Delphi und Kylix können Sie stattdessen auch die moderne Variante verwenden, bei der Eigenschaften nach außen ganz normal aussehen, aber intern Methoden ausrufen.
'DWHQNDSVHOXQJ
401
Sandini Bib
Dazu benötigen Sie zunächst einmal, genau wie bei der klassischen Kapselung, eine private Eigenschaft, die den Wert speichert, und je eine Schreib- und Lesemethode. Diese Zugriffsmethoden werden allerdings nun privat deklariert. Der Public-Block wird um eine spezielle Deklaration erweitert: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
402
unit CKreis; interface uses SysUtils; type Kreis = class { Private Eigenschaft für den Radius } private radiusWert: double; { Methode zum Setzen des Radius } private procedure setRadius(wert: double); { Methode zum Lesen des Radius } private function getRadius(): double; { Methode zum Lesen des Umfangs } public function getUmfang(): double; { Deklaration der speziellen Eigenschaften } public property Radius: double read getRadius write setRadius; public property Umfang: double read getUmfang; end; implementation procedure Kreis.setRadius(wert: double); begin { Überprüfen des Werts } if wert > 0 then self.radius := wert else { Ausnahme erzeugen } raise Exception.Create('Der Radius muss ein Wert ' + 'größer Null sein'); end;
Sandini Bib
41 42 43 44 45 46 47 48 49 50 51
function Kreis.getRadius(): double; begin result := self.radiusWert; end; function Kreis.getUmfang(): double; begin result := self.radiusWert * 2 * 3.1415927; end; end.
Die Deklaration in Zeile 21 und 22 erzeugt eine spezielle Eigenschaft. Die Lesemethode wird über die read-Anweisung zugewiesen, die Schreibmethode über die write-Anweisung. Die Implementierung der Zugriffsmethoden hat sich im Vergleich zum Beispiel bei der einfachen Kapselung nicht wesentlich geändert. Lediglich der Name der privaten Eigenschaft wurde geändert, da die spezielle Eigenschaft bereits Radius heißt. Das Beispiel implementiert auch gleich in Zeile 24 und 25 die Eigenschaft Umfang, der lediglich eine Lesemethode zugewiesen wird. Diese Eigenschaft kann deshalb nur gelesen werden. Das Schreiben verhindert der Compiler. Bei der Verwendung dieser Klasse sehen diese speziellen Eigenschaften nun aus wie normale: { Instanz der Klasse Kreis erzeugen } kreis1 := Kreis.Create(); { Radius setzen } try kreis1.Radius := -100; except on e: Exception do begin writeln('Radius konnte nicht gesetzt werden: ' + e.Message); end; end; { Radius und Umfang ausgeben } writeln('Ein Kreis mit einem Radius von ', kreis1.Radius, ' besitzt einen Umfang von ', kreis1.Umfang);
Ansonsten verhält sich das Programm aber genauso wie bei der klassischen Kapselung.
'DWHQNDSVHOXQJ
403
Sandini Bib
6.12 Vererbungs-Grundlagen Die Vererbung ist ein wichtiges Grundkonzept der OOP, das zwar in eigenen Programmen relativ selten eingesetzt wird. Bei der Verwendung von Bibliotheken werden Sie aber recht häufig damit konfrontiert. Wenn Sie in Java z. B. ein Formular erzeugen, leiten Sie Ihre Formularklasse immer von einer Basis-Formularklasse ab. Bei dieser Art Vererbung erkennen Sie den enormen Vorteil: Ihre neuen Klassen erben die komplette Funktionalität der Basisklasse und müssen diese lediglich um neue Funktionalität erweitern. Für eine umfassende Beschreibung der Vererbung bleibt mir hier leider kein Platz. Deshalb beschreibe ich an einem einfachen JavaBeispiel lediglich die Grundlagen. Die Grundlagen der Vererbung
Die Grundlagen der Vererbung sind einfach: Sie können eine Klasse von einer anderen, der so genannten Basisklasse ableiten. Als (Java-)Beispiel verwende ich eine Person-Klasse: 01 public class Person 02 { 03 /* Eigenschaften */ 04 public String Vorname; 05 public String Nachname; 06 07 /* Konstruktor */ 08 public Person(String vorname, String nachname) 09 { 10 this.Vorname = vorname; 11 this.Nachname = nachname; 12 } 13 14 /* Methode zum Ermitteln des vollen Namens */ 15 public String VollerName() 16 { 17 return this.Vorname + " " + this.Nachname; 18 } 19 }
Von dieser Klasse leite ich eine TitelPerson-Klasse ab, deren Instanzen die Daten von Personen speichern sollen, die einen Titel besitzen: 01 public class TitelPerson extends Person 02 {
404
Sandini Bib
Eine abgeleitete Klasse erbt alle Elemente der Basisklasse. Ein TitelPersonObjekt besitzt also auf jeden Fall die Eigenschaften Vorname und Nachname und die Methode VollerName. Sie können die neue Klasse nun z. B. um eine Eigenschaft erweitern: 03 04
/* Eigenschaft für den Titel */ public String Titel;
Methoden fügen Sie auf dieselbe Weise hinzu. Methoden werden aber häufig auch überschrieben. Dabei versehen Sie eine geerbte Methode mit einer neuen Implementierung. Bei einem TitelPerson-Objekt muss die Methode VollerName z. B. zusätzlich zum Namen auch den Titel ausgeben: 05 06 07 08 09 10
/* Die Methode zum Ermitteln des vollen Namens wird überschrieben */ public String VollerName() { return this.Titel + super.VollerName(); }
Das Beispiel nutzt bereits eine wichtige Technik: Die neue Methode ruft über das super-Schlüsselwort die geerbte Methode auf. So nutzt diese neue Methode den bereits vorhandenen Programmcode und erweitert gleichzeitig die Funktionalität. Der Name des super-Schlüsselworts kommt übrigens daher, dass eine Basisklasse auch als Superklasse bezeichnet wird. Das Hinzufügen der Titel-Eigenschaft und das Überschreiben der VollerName-Methode würde prinzipiell ausreichen, um die TitelPerson-Klasse zu definieren. Leider beginnen hier bereits die Probleme: Die neue Klasse besitzt keinen Konstruktor. Der Compiler erzeugt, wie Sie bereits wissen, automatisch einen Standardkonstruktor. Dieser ruft implizit den geerbten Standardkonstruktor auf. Die Basisklasse besitzt aber keinen solchen, weil ein spezieller Konstruktor implementiert ist. Der Compiler beschwert sich, dass er den Standardkonstruktor der Basisklasse nicht aufrufen kann. Sie müssen der neuen Klasse einen eigenen Konstruktor hinzufügen, der den speziellen Konstruktor der Basisklasse aufruft, wozu Sie wieder das super-Schlüsselwort verwenden: 11 12 13 14 15 16 17
Erste Probleme mit dem Konstruktor
/* Konstruktor */ public TitelPerson(String vorname, String nachname, String titel) { /* Aufruf des geerbten Konstruktors */ super(vorname, nachname); /* Eigene Initialisierungen */
9HUHUEXQJV*UXQGODJHQ
405
Sandini Bib
18 this.Titel = titel; 19 } 20 } TitelPersonObjekte verhalten sich anders als Person-Objekte
Sie können nun Person- und TitelPerson-Objekte erzeugen und im Programm verwenden. Die Methode VollerName verhält sich bei einem TitelPerson-Objekt anders als bei einem Person-Objekt (was bereits zum komplexen Thema Polymorphismus6 gehört ...): /* Instanz der Klasse Person erzeugen */ Person p1 = new Person("Fred-Bogus","Trumper"); // gibt "Fred-Bogus Trumper" aus /* Instanz der Klasse TitelPerson erzeugen */ TitelPerson p2 = new TitelPerson("Dr.", "Jean-Claude","Vigneron"); // gibt "Dr. Jean-Claude Vigneron" aus
Denken Sie jetzt nicht, dass die Vererbung trivial ist. Sie müssen noch einiges mehr beachten, als ich hier dargestellt habe. Im Artikel „OOPGrundlagen“ finden Sie dazu alle notwendigen Informationen. Dort beschreibe ich auch, wie sie in Object Pascal Klassen von Basisklassen ableiten.
6.13 Weitere Möglichkeiten, die nicht besprochen werden Im Rahmen dieses Kapitels konnte ich nicht alle Konzepte der OOP besprechen. Komplexe Konzepte, die in der Praxis eher seltener genutzt werden, blieben außen vor. Dazu gehören die folgenden: • Vererbung in der Tiefe Die Vererbung konnte ich in diesem Kapitel nur anschneiden. Sie müssen noch einiges mehr über die dabei verwendeten Konzepte und die Fallen der Vererbung lernen. • Polymorphismus und virtuelle Methoden Dieses komplexe Konzept, das nur sehr selten eingesetzt wird, will ich hier erst gar nicht versuchen zu erläutern, weil ich allein dazu schon mehrere Seiten benötigen würde ...
6.
406
Vielgestaltigkeit
Sandini Bib
• Schnittstellen Schnittstellen sind ein wichtiges Thema bei der modernen OOP, aber genauso schwierig zu erläutern wie der Polymorphismus. Im Artikel „OOP-Grundlagen“, den Sie auf der Buch-CD finden, werden diese Themen sehr ausführlich besprochen.
6.14 Zusammenfassung In diesem Kapitel haben Sie zunächst erfahren, warum Sie überhaupt objektorientiert programmieren sollten. Sie kennen die wesentlichen Nachteile der strukturierten und die Vorteile der objektorientierten Programmierung. Zur Umsetzung eigener objektorientierter Programme können Sie in Java, Delphi und Kylix einfache Klassen mit Eigenschaften und Methoden programmieren. Sie können diese Klassen über Konstruktoren so ausstatten, dass bei der Erzeugung von Instanzen Initialisierungsdaten übergeben werden können. Daneben kennen Sie das Konzept der Destruktoren und wissen prinzipiell, wofür Sie diese einsetzen, auch wenn Ihnen im Moment noch keine sinnvolle Anwendung einfällt (aber Destruktoren sollten Sie ja auch möglichst vermeiden ...). Um Objekte zu erzeugen, die den Zugriff auf ihre Daten selbst kontrollieren, können Sie das Konzept der Datenkapselung umsetzen. Dabei kennen Sie das klassische Konzept, das in Java verwendet wird, und das moderne, das Sie in Delphi und Kylix verwenden können. Schließlich kennen Sie noch die Grundlagen der Vererbung und können in Java eigene Klassen von vorhandenen Klassen ableiten. Insgesamt sind Sie nun in der Lage, einfache objektorientierte Programme zu schreiben, was aber für die meisten Fälle in der Praxis ausreicht.
6.15 Fragen und Übungen 1. Was ist eine Klasse? 2. Was passiert, wenn Sie eine Objektvariable einer anderen Objekt-
variable zuweisen? 3. Welche Bedeutung besitzt die Referenz self bzw. this? 4. Was ist ein Standardkonstruktor? 5. Welche Daten sollten sinnvollerweise dem Konstruktor einer Klasse
übergeben werden? 6. Was bedeutet Kapselung?
=XVDPPHQIDVVXQJ
407
Sandini Bib
Sandini Bib
7
Daten speichern
Sie lernen in diesem Kapitel:
le
n e rn
• wie Sie eine Liste von Daten in einfachen Arrays speichern, • wie Sie in Java die wesentlich flexibleren Auflistungen prinzipiell zur Verwaltung von Massendaten im Arbeitsspeicher einsetzen und • wie Sie in Java Textdateien lesen und schreiben. Dieses Kapitel beschäftigt sich mit dem sehr wichtigen Speichern von Daten im Arbeitsspeicher und in Dateien. Ich gehe aufgrund der Mächtigkeit der einzelnen Themen nur grundsätzlich darauf ein und zeige, wie Sie Daten prinzipiell speichern, ohne dabei alle Möglichkeiten der einzelnen Sprachen zu beleuchten. Sie lernen zunächst, Massendaten in einfachen Arrays („Listen von Variablen“) zu speichern. Danach zeige ich, wie Sie das wesentlich modernere und flexiblere Konzept der Auflistungen einsetzen. Um das Ganze ein wenig praxisorientiert darzustellen, lernen Sie danach, wie Sie Textdateien einlesen und schreiben. Bis auf die grundlegenden Themen beschreibe ich alle Themen nur für Java. Zwei Programmiersprachen einzubeziehen würde einfach zu viel Platz kosten.
7.1
Speichern im Arbeitsspeicher
Sehr häufig müssen Programme Massendaten im Arbeitsspeicher ablegen, um direkt mit diesen Daten arbeiten zu können. In vielen Fällen werden Daten, die in einer Datei oder Datenbank gespeichert werden, in den Speicher gelesen, um zu erreichen, dass ein Programm möglichst effizient mit den Daten arbeiten kann. Auf den folgenden Seiten zeige ich nun eine klassische und eine moderne Möglichkeit, Massendaten im Arbeitsspeicher zu verwalten.
409
Sandini Bib
7.1.1 Grundlagen Massendaten werden immer listenförmig gespeichert. Dazu verwenden Sie einfache Arrays oder moderne Auflistungen. Beide Varianten speichern zumindest eine Liste von einzelnen Werten. Es kommt aber auch häufig vor, dass die zu speichernden Daten komplexer sind. Wenn Sie z. B. die Daten von Personen speichern wollen, gehören dazu Angaben zum Vornamen, zum Nachnamen, zum Ort und zur Straße. Wenn ich Sie nun frage, wie Sie diese Daten grundsätzlich speichern, kennen Sie die Antwort: in Strukturen oder besser in Objekten. Das Array oder die Auflistung speichert dann einfach eine Liste von Referenzen auf einzelne Objekte. Auch wenn Sie die Daten mehrerer tausend Personen speichern müssen, ist es kein Problem, dazu Objekte zu verwenden, und bringt in der Praxis eigentlich auch nur Vorteile.
7.1.2 Arrays Arrays sind eine einfache Form der listenförmigen Speicherung. Ein Array ist im Prinzip eine Liste von einzelnen Variablen, die zusammenhängend gespeichert werden und die über einen Namen und einen Index gezielt angesprochen werden können. Einfache Arrays in Delphi und Kylix In Object Pascal deklarieren Sie ein einfaches Array wie im folgenden Beispiel: var zahlen: array[0..2] of byte;
Das Array besitzt in diesem Beispiel drei Elemente, die über den Index 0 bis 2 angesprochen werden können. Der Datentyp der einzelnen Elemente ist byte. Im Arbeitsspeicher werden die einzelnen Elemente direkt hintereinander angelegt (Abbildung 7.1). Der Index bezeichnet dann den Teil-Speicherbereich des einzelnen Elements.
00000000 00000000 00000000 0
1
2
Abbildung 7.1: Ein Array aus drei Byte-Werten
In Object Pascal können Sie den Bereich des Index angeben. Sie können das Array beispielsweise auch beim Index 1 beginnen lassen. Dass ich das Array bei Null beginne, hat den Grund, dass viele Programmiersprachen (wie C++, C# und Java) Arrays grundsätzlich und ausschließlich
410
Sandini Bib
bei Null beginnen. Wenn ich Arrays in Object Pascal auf dieselbe Weise verwalte, muss ich in anderen Sprachen nicht umdenken. Über den Index können Sie die einzelnen Variablen nun gezielt ansprechen: zahlen[0] := zahlen[1] := zahlen[2] := ... writeln('Die
1; 2; 4; erste Zahl ist ', zahlen[0]);
Im Arbeitsspeicher sieht das Beispiel-Array dann prinzipiell so aus wie in Abbildung 7.2.
00000001 00000010 00000100 0
1
2
Abbildung 7.2: Ein Byte-Array, das die Zahlen 1, 2 und 4 speichert
Die Arbeit mit einem Array gleicht der mit einfachen Variablen. Der Unterschied ist lediglich, dass Sie den Index mit angeben müssen. Der Vorteil gegenüber einzelnen Variablen ist, dass Sie den Index auch über eine Variable angeben und so z. B. in einer Schleife durch das Array gehen können:
Arrays können in Schleifen bearbeiten werden
for i := 0 to 2 do writeln('Zahl ', i, ' ist ', zahlen[i]);
Per Voreinstellung können Sie in Delphi und Kylix auch auf Arrayelemente zugreifen, die gar nicht existieren, wenn Sie als Index eine Variable einsetzen. So können Sie z. B. das Element mit dem Index 3 beschreiben: i := 3; zahlen[i] = 255;
Delphi und Kylix überprüfen zunächst nicht, ob der Index passt. Da Arrays im Speicher lediglich hintereinander gelegte Variablen sind, wird ein Speicherbereich überschrieben, der hinter dem Array liegt. Das ist aber mit ziemlicher Sicherheit ein Speicherbereich, der zu einer anderen Variablen gehört. Sie überschreiben dann vollkommen andere Daten und produzieren in Ihrem Programm sehr schwer wiegende Fehler.
411
Sandini Bib
Wenn Sie hingegen die Option RANGE CHECKING bzw. BEREICHSÜBERPRÜFUNG in den Compileroptionen einschalten (im Menü PROJECT/ OPTIONS/COMPILER), überprüft das Programm bei jedem Arrayzugriff, ob der Bereich des Arrays überschritten wird, und erzeugt in diesem Fall eine Ausnahme vom Typ ERangeError. Da Ausnahmen im Programm gemeldet werden (sofern sie nicht ignoriert werden), erhalten Sie dann eine Information darüber, dass in Ihrem Programm ein Fehler aufgetreten ist. Schalten Sie diese Option also auf jeden Fall ein. Arrays eignen sich nicht für dynamische Daten
Arrays werden nur noch für spezielle Probleme eingesetzt
Arrays eignen sich bereits recht gut zum Speichern von listenförmigen Daten. Arrays führen aber auch zu Problemen: Bei der Deklaration legen Sie in den meisten Programmiersprachen, wie auch in Object Pascal, die Anzahl der Elemente fest. In vielen Fällen wissen Sie aber bei der Deklaration nicht, wie viele Elemente tatsächlich gespeichert werden müssen. Wenn Sie z. B. Personendaten aus einer Datei in ein Array einlesen, wissen Sie nicht, wie viele Personen tatsächlich in der Datei gespeichert sind. Dieses Problem lösen die ab Seite 416 besprochenen Auflistungen. Arrays werden deshalb in modernen Programmen nur noch zur Lösung spezieller Probleme eingesetzt, bei denen eine Auflistung nicht angebracht ist. Auflistungen sind zwar flexibler, aber beim sequenziellen Durchgehen normalerweise auch immer etwas langsamer als Arrays, weswegen einfache Probleme mit Arrays häufig performanter gelöst werden können. Das folgende Beispiel demonstriert ein solches einfaches Problem. Ein Programm soll Lottozahlen ermitteln. Die einzelnen Zahlen ermittelt das Programm über die Random-Funktion per Zufall. Ab der zweiten Zahl muss jedoch überprüft werden, ob die aktuell gezogene Zahl bereits zuvor gezogen wurde. Zur Lösung dieses Problems setzt das Programm ein Array ein, in dem die gezogenen Zahlen abgelegt werden. Ab der zweiten Ziehung wird das Array durchsucht und damit überprüft, ob die aktuell gezogene Zahl bereits existiert:
412
Sandini Bib
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
program Arrays; {$APPTYPE CONSOLE} uses SysUtils; var zahlen: array[0..6] of integer; var zahlOK: boolean; i, j: integer; begin Randomize; for i := 0 to 6 do begin repeat zahlOK := True; Zahlen[i] := Random(49) + 1; for j := 0 to i - 2 do if zahlen[j] = zahlen[i] then zahlOK := False; until zahlOK = true; end; { Ausgabe der gezogenen Zahlen } for i := 0 to 6 do writeln(zahlen[i]); end.
Das Problem des Ziehens von Lottozahlen ist schon etwas komplexer und kann prinzipiell nur über ein Array gelöst werden. In Zeile 11 wird zunächst der Zufallsgenerator initialisiert. Das ist notwendig, um bei jeder Ausführung des Programms unterschiedliche Zahlen zu erhalten. Würden Sie den Zufallsgenerator nicht initialisieren, würden die gezogenen Zahlen bei jedem Programmstart dieselben sein. Probieren Sie es einfach aus. In Zeile 13 beginnt eine äußere Schleife, die sieben Zahlen ermittelt. Um die Zufallszahl bei Zahlen, die bereits gezogen wurden, wiederholt ermitteln zu können, beginnt in Zeile 15 eine mittlere Schleife. Diese setzt in Zeile 16 zunächst die Variable zahlOK auf true, weil angenommen wird, dass die ermittelte Zahl eindeutig ist. In Zeile 17 wird dann eine Zufallszahl zwischen 1 und 49 ermittelt. Um diese Zahl mit den bereits gezogenen Zahlen zu vergleichen, beginnt in Zeile 18 eine innere Schleife. Über den Vergleich in Zeile 19 entscheidet das Programm, ob die aktuell gezogene Zahl bereits im Array vorkommt. Falls das der Fall ist, wird in Zeile 20 die Variable zahlOK auf false gesetzt. Diese Variable
413
Sandini Bib
wird im Schleifenfuß in Zeile 21 abgefragt. So wird die mittlere Schleife so lange wiederholt ausgeführt, bis die gezogene Zahl eindeutig ist. Arrays in Java In Java ist ein Array keine einfache Speicherstruktur wie in Object Pascal, sondern ein Objekt, das vor der Benutzung wie alle Objekte erzeugt werden muss. Ein Array, das drei Zahlen speichert, wird z. B. so erzeugt: byte[] zahlen = new byte[3];
Der Index eines Arrays beginnt in Java immer bei 0. Der Zugriff auf ein Array sieht aber genauso aus wie in Object Pascal: /* Array beschreiben */ zahlen[0] = 1; zahlen[1] = 2; zahlen[2] = 4; /* Array in Schleife auslesen */ for (int i = 0; i < 3; i++) { System.out.println(zahlen[i]); }
Mehrdimensionale Arrays Die Arrays, die Sie bisher kennen gelernt haben, waren einfache, eindimensionale Arrays, die sich nur in einer Dimension ausbreiteten. Sie können in allen Programmiersprachen aber auch Arrays erzeugen, die aus mehr als einer Dimension bestehen. Obwohl diese Arrays heutzutage kaum noch eingesetzt werden, sollten Sie wissen, worum es sich dabei handelt. Wenn Sie z. B. Personendaten speichern wollen, könnten Sie dazu ein zweidimensionales Array verwenden. Dieses würde schematisch so aussehen wie in Abbildung 7.3. Trumper Overturf Packer
0 Fred-Bogus
Merril Ralf
1 2 0
1
NewYork Wien NewYork 2
Abbildung 7.3: Ein zweidimensionales Array mit drei „Datensätzen“, die aus jeweils drei Feldern bestehen.
414
Sandini Bib
Das Array speichert nun drei „Datensätze“ mit je drei „Feldern“. Solch ein Array deklarieren Sie ganz einfach. In Object Pascal sieht die Deklaration so aus: var personen: array[0..2, 0..2] of string;
In Java wird ein zweidimensionales Array so deklariert: String personen[][] = new String[3][3];
Beim Zugriff auf die Elemente des Arrays geben Sie nun die Indizes aller Dimensionen an. In Object Pascal geben Sie beide Indizes in einer Klammer an: personen[0,0] personen[0,1] personen[0,2] personen[1,0] personen[1,1] personen[1,2] personen[2,0] personen[2,1] personen[2,2]
:= := := := := := := := :=
'Fred-Bogus'; 'Trumper'; 'New York'; 'Merril'; 'Overturf'; 'Wien'; 'Ralf'; 'Packer'; 'New York';
In Java geben Sie die Indizes in separaten Klammern an: personen[0][0] personen[0][1] personen[0][2] personen[1][0] personen[1][1] personen[1][2] personen[2][0] personen[2][1] personen[2][2]
= = = = = = = = =
"Fred-Bogus"; "Trumper"; "New York"; "Merril"; "Overturf"; "Wien"; "Ralf"; "Packer"; "New York";
Sie können natürlich auch wieder schleifengesteuert durch das Array gehen. Die folgende Object Pascal-Schleife schreibt die gespeicherten Daten an die Konsole:
415
Sandini Bib
for i := 0 to 2 do begin for j := 0 to 2 do begin writeln(personen[i,j], ' '); end; writeln; end;
Zwei- und mehrdimensionale Arrays werden, wie bereits gesagt, heutzutage nicht mehr allzu häufig eingesetzt. Wenn Sie strukturierte Daten speichern müssen, eignen sich dazu besser eindimensionale Arrays oder Auflistungen, die einzelne Objekte referenzieren. Da Objekte die unterschiedlichsten Eigenschaften und auch Methoden besitzen können, haben Sie wesentlich mehr Möglichkeiten als mit zweidimensionalen Arrays. Ein großes Problem zweidimensionaler Arrays ist, dass die Datentypen der einzelnen Felder nicht unterschiedlich sein können. In Objekten können Sie aber (natürlich) auch unterschiedliche Daten verwalten. Bei der Besprechung von Auflistungen komme ich darauf zurück. Lediglich im mathematischen Bereich mag es manchmal noch sinnvoll sein, mit zwei- oder mehrdimensionalen Arrays zu arbeiten. Aber diese spezielle Art der Mathematik (die mehrdimensionale) habe ich nie verstanden ...
7.1.3 Auflistungen Auflistungen (englisch „Collections“) sind prinzipiell so etwas wie Arrays, nur wesentlich mächtiger. Auflistungen speichern ebenfalls eine Liste von Daten, allerdings ist diese auf eine Dimension begrenzt. Das ist aber nicht weiter schlimm, denn Auflistungen referenzieren in den meisten Fällen Objekte. Auflistungen sind Objekte
Auflistungen sind selbst aber auch Objekte und besitzen deshalb Methoden und Eigenschaften. Eine sehr hilfreiche Methode ist z. B. die, über die Sie die Liste der gespeicherten Werte bei den meisten Auflistungen in der Laufzeit des Programms dynamisch erweitern können. Andere Methoden erlauben bei einigen Auflistungen das Suchen in den gespeicherten Daten, meist ist auch das Löschen von Elemente über eine Methode möglich. Eine Eigenschaft oder eine weitere Methode gibt bei vielen Auflistungen die Anzahl der aktuell gespeicherten Elemente zurück. In den verschiedenen Programmiersprachen gibt es eine große Anzahl verschiedener Auflistungen, die unterschiedlich spezialisiert sind. Ei-
416
Sandini Bib
nige Auflistungen assoziieren die gespeicherten Daten mit einem Schlüssel, über den der Zugriff auf die Daten sehr einfach und schnell ist. Andere Auflistungen sortieren ihre Liste automatisch, weitere sind spezialisiert auf die Speicherung von Zeichenketten. Ich kann natürlich hier nicht alle Auflistungsarten besprechen. Sie erfahren also nur, wie Sie eine der wichtigsten Arten benutzen, eine Auflistung, die Objekte speichert und diese mit einem Schlüssel assoziiert. Zur Demonstration speichert diese Auflistung einzelne Person-Objekte. Die Klasse dieser Objekte stelle ich hier nicht mehr vor. Ich verwende einfach die Klasse, die wir in Kapitel 6 entworfen haben. Da Sie zurzeit noch keine Dateien oder Datenbanken auslesen können, lege ich die Daten noch im Programm an. Da Sie hier nur das Prinzip lernen sollen, zeige ich lediglich, wie Sie mit einer Java-Auflistung arbeiten. Delphi und Kylix bleiben außen vor. Die Assoziation von Daten mit einem Schlüssel ist in der Programmierung eine weit verbreitete Technik. Besonders in Datenbanken wird dieses Konzept intensiv eingesetzt. Eine (ideal gestaltete) Datenbank, die Kundendaten speichert, assoziiert z. B. jeden Kunden mit einer eindeutigen Kundennummer. Über diesen Schlüssel können Daten sehr schnell gefunden und ausgelesen werden. Um die Daten eines Kunden zu bearbeiten, benötigt ein Mensch oder ein Programm lediglich die Kundennummer. Dieses Konzept dürfte Ihnen nicht fremd sein, weil Ihre Daten sicher in irgendwelchen Datenbanken gespeichert sind: Als Kunde bei verschiedenen Firmen, als Student an einer Universität (wobei die Matrikelnummer der Schlüssel ist) oder einfach nur als Staatsbürger von Deutschland (wobei die Personalausweisnummer der Schlüssel ist). In Datenbanken werden Sie noch öfter mit diesen Schlüsseln konfrontiert werden. Sie können dieses Konzept aber auch direkt in Ihren Programmen umsetzen, indem Sie Massendaten in Auflistungen speichern, die einen Schlüssel für jedes gespeicherte Objekt verwalten. Häufig werden Auflistungen z. B. verwendet, um die in einer Datenbank oder einer Datei gespeicherten Daten in den Arbeitsspeicher zu lesen und dort sehr schnell bearbeiten zu können. Die Java-Hashtable-Klasse Java stellt mit der Klasse Hashtable eine Auflistung zur Verfügung, die beliebige Objekte referenzieren und diese mit einem Schlüssel assoziieren kann.
417
Sandini Bib
Die Beschreibung dieser Klasse finden Sie in der „Java 2 Platform API Specification“, die Sie im Ordner api der Java-Dokumentation finden. Öffnen Sie die Datei index.html und klicken Sie im rechten Fensterbereich auf den Link JAVA.UTIL, um die Dokumentation des Pakets java.util zu öffnen. In der Liste der Klassen finden Sie auch die Klasse Hashtable. Um Daten in einem Hashtable-Objekt speichern zu können, müssen Sie zunächst (natürlich) eine Instanz erzeugen: Hashtable ht = new Hashtable();
Die Hashtable-Klasse wird im Paket java.util verwaltet. Dieses Paket sollten Sie also oben in der Java-Datei importieren: import java.util.*;
Nun können Sie beliebige Objekte an die Liste anfügen. Dazu verwenden Sie die put-Methode: ht.put(Schlüssel, Objekt);
Der Schlüssel ist üblicherweise ein String, kann aber unter Java eigentlich jedes beliebige Objekt sein. Als zu speicherndes Objekt können Sie ebenfalls jedes beliebige Objekt anfügen. Hashcode
Der Name der Hashtable-Klasse kommt daher, dass aus dem Schlüssel intern ein so genannter Hashcode errechnet wird. Ein Hashcode ist normalerweise ein Integer-Wert, der nach einem komplexen Algorithmus berechnet wird und der ein Datum in einer kompakteren Form darstellt. Der Java-Hashcode der Zeichenkette "Die Antwort auf die Frage aller Fragen ist 42" ist z. B. -200667539. Sie können das selbst ausprobieren. Strings sind in Java Objekte, die wie alle Java-Objekte von der Basisklasse Object abgeleitet sind und von dieser Klasse die Methode hashCode erben. Diese Methode errechnet den Hashcode und gibt diesen zurück. Wenn Sie eine String-Variable erzeugen und initialisieren, können Sie den Hashcode ermitteln: /String s = "Die Antwort auf die Frage aller Fragen ist 42"; System.out.println(s.hashCode()); // -200667539
418
Sandini Bib
Hashcodes sind immer eindeutig. Es kann nicht vorkommen, dass zwei unterschiedliche Zeichenketten denselben Hashcode liefern. Ein Hashtable-Objekt speichert nun für jedes Objekt, auf das in der Liste verwiesen wird, nicht den Schlüssel, sondern den Hashcode. Damit wird einfach weniger Speicherplatz benötigt, als wenn der Schlüssel selbst abgelegt wird. Wenn Sie später beim Zugriff auf die einzelnen Objekte den Schlüssel angeben, wird dieser wieder automatisch in seinen Hashcode umgerechnet und mit den gespeicherten Hashcodes verglichen. Weil Hashtable-Objekte den Hashcode des Schlüssels speichern, können Sie neben einfachen Strings auch jedes beliebige Objekt als Schlüssel verwenden. Da alle Objekte von der Basisklasse Object abgeleitet sind, besitzen diese auch eine Methode hashCode. Das Ganze ist aber eher verwirrend als nützlich. In den meisten Fällen werden einfache Zeichenketten oder numerische Werte als Schlüssel verwendet (in Java können Sie allerdings keine einfachen Datentypen als Schlüssel einsetzen, weil diese keine Objekte sind). Die Hashtable-Klasse arbeitet ausschließlich mit Objekten. Einfache Datentypen können Sie nicht in Hashtable-Instanzen speichern (was aber mit anderen Auflistungen durchaus möglich ist). Sie können noch nicht einmal einen einfachen Datentyp als Schlüssel verwenden. Wenn Sie das trotzdem versuchen, erhalten Sie beim Kompilieren den Fehler „Cannot resolve symbol“. Setzen Sie als Schlüssel aber einen String ein, funktioniert das Hinzufügen. Ein String ist in Java nämlich in Wirklichkeit ein Objekt. Sie können nun z. B. Person-Objekte separat erzeugen und hinzufügen: Person p = new Person("Fred-Bogus", "Trumper", "New York"); ht.put("1000", p);
Das Beispiel geht davon aus, dass Sie eine Person-Klasse mit einem Konstruktor zur Verfügung haben, der drei Argumente vom Typ String besitzt. Die put-Methode fügt die Referenz auf das Objekt an die interne Liste an und assoziiert diese mit dem angegebenen Schlüssel. Ein solches Hinzufügen ist sinnvoll, wenn Sie bereits Objekte besitzen, die Sie hinzufügen wollen. Dann müssen Sie das Objekt natürlich nicht – wie im Beispiel – zuvor erzeugen. In den meisten Programmen werden Objekte, die einer Auflistung hinzugefügt werden sollen, aber direkt beim Hinzufügen erzeugt. Die Verwaltung der Objekte ausschließlich in der Auflistung ist
419
Sandini Bib
normalerweise das Ziel solcher Programme. Dann können Sie das Objekt wesentlich direkter erzeugen und hinzufügen: ht.put("1000", new Person("Fred-Bogus", "Trumper", "New York")); ht.put("1001", new Person("Merril", "Overturf", "Wien")); ht.put("1002", new Person("Ralf", "Packer", "New York")); Schlüssel sind eindeutig
Das Beispiel erzeugt drei Person-Objekte und fügt diese direkt der Auflistung hinzu. Die Objekte werden dabei mit den Schlüsseln "1000", "1001" und "1002" assoziiert, was für das Beispiel so etwas wie eine Kundennummer darstellen soll. Beachten Sie, dass Schlüssel (natürlich) immer eindeutig sein müssen. Java-Hashtable-Objekte lassen zwar (anders als manche anderen Auflistungen) scheinbar das Hinzufügen mehrerer Objekte mit gleichen Schlüsseln zu. Beim wiederholten Hinzufügen wird aber die Referenz auf das Objekt mit dem angegebenen Schlüssel durch die neue Referenz ersetzt. Dieses Feature können Sie sehr gut nutzen, um ein Objekt in der Liste gegen ein anderes auszutauschen. Beim Hinzufügen müssen Sie aber auf den Schlüssel achten, sodass Sie nicht versehentlich ein bereits vorhandenes Objekt ersetzen.
Schneller Zugriff über den Schlüssel
Nun können Sie über den Schlüssel sehr schnell auf die gespeicherten Objekte zugreifen. Dazu verwenden Sie die get-Methode, der Sie den Schlüssel übergeben. Diese Methode gibt eine Referenz auf das gespeicherte Objekt zurück. Da die Auflistung nicht weiß, welche Objekte tatsächlich gespeichert sind, wird eine Referenz von Typ Object zurückgegeben. Object ist, wie Sie bereits wissen, die Basisklasse aller JavaObjekte. Eine Referenz vom Typ Object kann jedes Objekt verwalten (weswegen Sie der Auflistung auch beliebige Objekte hinzufügen können). Zu erklären, warum das so ist, ist mir an dieser Stelle leider nicht möglich, weil Sie dazu die komplexen OOP-Konzepte Vererbung und Polymorphismus kennen müssen (für die ich im Buch keinen bzw. nur wenig Platz habe). Lesen Sie gegebenenfalls den Artikel „OOP-Grundlagen“, den Sie auf der Buch-CD finden. Dort beschreibe ich diese Konzepte sehr ausführlich. Das für Sie im Moment Wichtige ist, dass Sie die zurückgegebene Referenz in eine Referenz vom Typ Person konvertieren müssen. Dazu können Sie einfach einen Typecast verwenden. Idealerweise verwenden Sie zum Zugriff auf das Objekt eine Variable: Person p = (Person)ht.get("1002"); System.out.println(p.VollerName());
420
Sandini Bib
Existiert ein Element mit dem angegebenen Schlüssel, gibt get eine Referenz darauf zurück. Diese Referenz wird in eine Referenz vom Typ Person umgewandelt und der Variablen p zugewiesen. Über diese Referenz kann das Objekt dann bearbeitet werden. Das Beispiel geht davon aus, dass die Person-Klasse eine Methode VollerName besitzt, die den vollen Namen der Person zurückgibt. Existiert kein Element mit dem angegebenen Schlüssel, gibt get eine Nullreferenz zurück. Eine Nullreferenz ist eine Referenz, die gar kein Objekt referenziert. Der Wert dieser Referenz ist in Java der Wert null, der eben für solche Nullreferenzen steht. In Object Pascal ist das der Wert nil. Mit einer solchen Referenz können Sie nicht arbeiten. Java lässt zwar die Typumwandlung einer Nullreferenz zu, dabei kommt allerdings wieder eine Nullreferenz heraus.
Nullreferenzen
Sie können die Referenz nun glücklicherweise einfach mit null vergleichen, um festzustellen, ob eine Person mit dem angegebenen Schlüssel gefunden wurde: p = (Person)ht.get("2001"); if (p != null) System.out.println(p.VollerName()); else System.out.println("Es wurde keine Person mit dem " + "Schlüssel '2001' gefunden");
Der Zugriff über den Schlüssel ist normalerweise (bei den meisten Auflistungsarten) ernorm schnell. Das liegt daran, dass Auflistungen die Daten nicht einfach nur listenförmig im Speicher verwalten. Die Daten werden mit teilweise sehr komplexen Techniken gespeichert. Ohne näher darauf einzugehen, will ich wenigstens die Bezeichnungen dieser Techniken nennen, damit Sie u. U. an anderen Stellen nachschlagen können. Meist werden dazu so genannte binäre Bäume oder balancierte binäre Bäume verwendet. Da diese prinzipbedingt schon mit einem Schlüssel arbeiten, ist der Zugriff über diesen sehr performant.
Sehr schneller Zugriff über den Schlüssel
Löschen von Elementen In vielen Programmen müssen Sie nicht nur Daten dynamisch hinzufügen, sondern während der Laufzeit des Programms auch wieder löschen. Stellen Sie sich eine einfache Adressverwaltung vor, bei der Sie die Adressdaten beim Start der Anwendung aus einer Datei in eine Auflistung einlesen und diese dann im Programm bearbeiten. Das beim Einlesen der Daten notwendige Hinzufügen von Objekten beherrschen Sie nun. Sie müssen aber auch Objekte aus der Auflistung entfernen können. Dazu verwenden Sie bei einer Java-Hashtable-Instanz die Methode remove:
421
Sandini Bib
ht.remove("1002");
Falls der Schlüssel nicht existiert, macht diese Methode einfach gar nichts, erzeugt also auch keine Ausnahme. Beachten Sie, dass das Objekt dabei nicht unbedingt wirklich gelöscht wird. Sie entfernen ja nur die Referenz aus der Auflistung. Falls das Objekt noch über eine andere Referenz referenziert wird, bleibt es bestehen, bis die andere Referenz ungültig wird. Über die Auflistung können Sie das Objekt dann aber nicht mehr erreichen. In Object Pascal ist das Ganze noch etwas komplexer, denn dort müssen Sie Objekte ja über deren Free-Methode explizit aus dem Speicher entfernen. Sie müssten also zuerst die Free-Methode für ein in der Auflistung referenziertes Objekt aufrufen und erst danach die nun tote Referenz aus der Auflistung entfernen. Sequenzielles Durchgehen In vielen Programmen müssen Sie eine Auflistung sequenziell (der Reihe nach) durchgehen. In einer einfachen Adressverwaltung müssen Sie z. B. irgendwann die gespeicherten Daten in eine Datei zurückschreiben. Die Technik des sequenziellen Durchgehens unterscheidet sich bei den verschiedenen Auflistungen. In manchen können Sie ähnlich einem Array über einen Integer-Index auf die Elemente zugreifen. Eine Eigenschaft oder Methode der Auflistung liefert Ihnen dazu die Anzahl der aktuell gespeicherten Elemente. Mit einer Java-Hashtable-Instanz ist das Durchgehen aber leider nicht so einfach, wie es eigentlich sein sollte. Sie benötigen dazu eine Variable vom Typ Enumeration. Dieser Variable weisen Sie die Rückgabe der Methode elements des Hashtable-Objekts zu. Eine Enumeration ist ein Objekt. Über dessen Methode hasMoreElements erfahren Sie, ob noch weitere Elemente oder ob überhaupt Elemente gespeichert sind. Die Methode nextElement gibt das nächste Element zurück. Nach dem Aufruf der elements-Methode der Hashtable-Instanz steht das Enumeration-Objekt vor dem ersten Element. nextElement gibt also beim ersten Aufruf die erste gespeicherte Person-Referenz zurück. Das folgende Beispiel sagt wahrscheinlich mehr aus, als diese kurze Beschreibung: Enumeration enum = ht.elements(); while (enum.hasMoreElements()) { p = (Person)enum.nextElement(); System.out.println(p.VollerName()); }
422
Sandini Bib
Sie müssen beachten, dass die Reihenfolge beim sequenziellen Durchgehen nicht der entspricht, die Sie beim Hinzufügen festgelegt haben. Ein Hashtable-Objekt speichert seine Daten, wie bereits gesagt, nicht einfach nur hintereinander. Die Reihenfolge der Daten ist – aus unserer Sicht – mehr oder weniger zufällig. Wenn Sie Daten in einer bestimmten Reihenfolge speichern oder sogar sortieren wollen, müssen Sie eine der anderen Auflistungen wie eine ArrayList-Auflistung (Speicherung in Reihenfolge; Zugriff über einen Integer-Index; Möglichkeit des Einfügens von Elementen mitten in die Liste) verwenden. Nun sind Sie bereits in der Lage, komplexe Daten auf eine sehr einfache Weise im Arbeitsspeicher zu verwalten und diese sogar über einen Schlüssel sehr einfach und schnell zu lokalisieren. Im Moment fehlen Ihnen aber wahrscheinlich noch Ideen, wie Sie dies einsetzen können. Im nächsten Abschnitt beschreibe ich, wie Sie Textdateien lesen und schreiben können. Dann können Sie z. B. schon Adressdaten in einer Datei verwalten und in eine Auflistung einlesen.
7.2
Verwalten von (Text-)Dateien
Bisher haben Sie alle Daten lediglich im Arbeitsspeicher verwaltet. Diese Daten gingen verloren, wenn das Programm beendet wurde. In RealWorld-Programmen müssen Sie Daten aber natürlich auch dauerhaft speichern. Dazu können Sie einfache Dateien verwenden. Die modernere Alternative zu Dateien, nämlich Datenbanken, bespreche ich grundlegend in Kapitel 9.
7.2.1 Welche Daten werden in Dateien verwaltet? Datenbanken sind mittlerweile so einfach und flexibel zu programmieren, dass die Speicherung von Daten in Dateien immer mehr auf dem Rückmarsch ist. Früher wurden hingegen viele Daten in Dateien verwaltet. Dazu wurden so genannte „Dateien mit wahlfreiem Zugriff“ verwendet. Diese Dateien werden auch als Random-1Dateien bezeichnet. Eine solche Datei besteht aus mehreren Datensätzen, die sequenziell hintereinander gespeichert sind. Ein Datensatz besteht aus mehreren Feldern, die die einzelnen Daten speichern. Eine solche Datei, die Personendaten speichert, würde z. B. Datensätze mit den Feldern Vorname, Nachname, Postleitzahl, Straße und Ort verwalten. Das Feld Postleitzahl wäre vom 1.
Random-Dateien
„random“ ist der englische Begriff für „wahlfrei“, aber auch für „zufällig“, „willkürlich“ etc.
423
Sandini Bib
Typ Integer, alle anderen wären String-Felder. Wenn in der Datei zwei Personen gespeichert wären, würde diese zwei Datensätze verwalten. Abbildung 7.4 zeigt das Schema einer solchen Datei. Fred-Bogus
Trumper
NewYork
Merril
Overturf
Wien
Abbildung 7.4: Eine Random-Datei mit zwei Datensätzen
Das „wahlfrei“ in der Bezeichnung solcher Dateien kommt daher, dass ein Programm mit entsprechenden Features der Programmiersprache gezielt auf einzelne Datensätze zugreifen kann. Ein Programm kann also z. B. den zweiten Datensatz auslesen und diesen sogar direkt in der Datei mit neuen Werten beschreiben. Solche Techniken wurden früher angewendet, als der Arbeitsspeicher noch sehr klein war. Heute würden solche Dateien wahrscheinlich eher in einem Rutsch in den Speicher gelesen, dort bearbeitet und in einem Rutsch wieder zurückgeschrieben werden. Dazu würden natürlich Auflistungen verwendet werden. Früher mussten Sie den Umgang mit solchen Dateien noch lernen. Heute übernehmen diese Aufgabe Datenbanken, weswegen Sie sich nicht mehr mit Random-Dateien auseinander setzen müssen. Trotzdem sollten Sie natürlich wissen, worum es sich dabei handelt. Textdateien
Die Arbeit mit Dateien ist aber auch heute noch wichtig. Eine sehr häufige Aufgabe beim Programmieren ist nämlich das Lesen und Schreiben von Textdateien. Textdateien sind, wie Sie ja bereits wissen, Dateien, die einzelne Zeichen sequenziell hintereinander speichern. Einzelne Zeilen sind durch ein Carriage Return2- und ein Line Feed3-Zeichen (die ASCIIZeichen 13 und 10 unter Windows) bzw. nur durch ein Line Feed-Zeichen (unter Linux) getrennt. Alle Programmiersprachen stellen zum Lesen und Schreiben von Textdateien meist einfach anzuwendende Features bereit. Ich zeige die Programmierung am Beispiel von Java.
7.2.2 Textdateien lesen In Java haben Sie einige Möglichkeiten, eine Textdatei zu lesen. Ich zeige hier nur eine, damit Sie das Prinzip verstehen. Die Textdatei, die ich einlesen will, speichert die Namen, die Telefonnummern und die E-Mail-Adresse von einzelnen Personen und sieht so aus:
424
2.
Wagenrücklauf
3.
Zeilenvorschub
Sandini Bib
Donald;Duck;012345-9999;0177-12345678;[email protected] Daisy;Duck;012345-8888;0172-555444;[email protected] Dagobert;Duck;012345-1;0172-123123;[email protected]
Dieses Beispiel ist eine typische Anwendung von Textdateien. Strukturierte Daten werden auch heute noch in einigen Fällen in Dateien verwaltet. Dazu werden aber keine Random-Dateien, sondern eben Textdateien verwendet. Das gilt besonders dann, wenn diese Daten von anderen Systemen stammen, die die gängigen Datenbankformate nicht kennen. Außerdem ist es eine gute Übung zum Lesen von Textdateien, denn die einzelnen „Felder“ müssen nach dem Lesen noch getrennt ermittelt werden. Der nach meinen Recherchen in Java ideale (weil relativ einfache und effiziente Weg) eine Textdatei zu lesen ist, eine Instanz der Klasse BufferedReader zu verwenden. Ein solches Objekt ermöglicht das zeilenweise Einlesen von Textdateien auf eine sehr effiziente Weise.
Lesen über einen BufferedReader
Beim Erzeugen übergeben Sie diesem Objekt eine neue Instanz der Klasse FileReader. Ein FileReader-Objekt wird zum allgemeinen Lesen von Dateien verwendet. Das BufferedReader-Objekt verwendet das FileReaderObjekt zum eigentlichen Lesen der Daten und ermöglicht Ihnen einen vereinfachten Zugriff darauf. Dem FileReader-Objekt übergeben Sie bei der Erzeugung den Dateinamen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
String dateiname = "C:\\Projekte\\Java\\Personen.txt"; /* Eine Instanz von BufferedReader zum zeilenweisen Lesen erzeugen */ BufferedReader in = null; try { in = new BufferedReader(new FileReader(dateiname)); } catch (FileNotFoundException e) { System.out.println("Die Datei '" + dateiname + "' wurde nicht gefunden"); return; }
Da der Dateiname immer absolut angegeben werden muss, habe ich diesen in Zeile 1 auf eine Datei im Ordner C:\Projekte\Java gesetzt. Die doppelten Backslashs bewirken, dass die Sonderbedeutung der Backslashs aufgehoben wird und im Ergebnis nur jeweils ein Backslash herauskommt. Der Dateiname ist hier für Windows angegeben. Unter Linux
425
Sandini Bib
müssen Sie den Dateinamen natürlich anders angeben (/projekte/java/ personen.txt). Die Erzeugung des BufferedReader-Objekts muss in einer Ausnahmebehandlung erfolgen. Deswegen wird das Objekts in Zeile 8 innerhalb eines try-Blocks erzeugt. Die Variable ist außerhalb des Blocks in Zeile 5 deklariert, damit diese unterhalb der Ausnahmebehandlung zur Verfügung steht. Ich habe die Variable mit null initialisiert, da der Compiler ansonsten beim Kompilieren den Fehler meldet, dass diese Variable uninitialisiert sein könnte. In Zeile 10 wird dann noch die beim Öffnen der Datei eventuell auftretende Ausnahme abgefangen. Die Art der abzufangenden Ausnahme meldet der Compiler, wenn Sie das Programm ohne Ausnahmebehandlung versuchen zu kompilieren. Zeilenweise Lesen
426
Nun können Sie die Textdatei zeilenweise einlesen. Dazu verwenden Sie die readLine-Methode des BufferedReader-Objekts. Diese Methode gibt die eingelesene Zeile zurück oder null, wenn keine (weitere) Zeile gelesen werden kann. Sie können die einzelnen Zeilen einfach in einer Schleife einlesen. Das Ganze muss wieder in eine Ausnahmebehandlung eingefügt werden, die auf I/O4-Fehler reagiert: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
try { /* Textdatei zeilenweise lesen */ String zeile; do { zeile = in.readLine(); if (zeile != null) { System.out.println(zeile); } } while (zeile != null);
4.
Input / Output
/* Datei schließen */ in.close(); } catch (IOException e) { System.out.println("Fehler beim Lesen der Daten: " + e.getMessage()); }
Sandini Bib
Das Beispiel gibt in Zeile 25 lediglich die eingelesene Zeile aus. Das Zerlegen der Zeile in die einzelnen Bestandteile zeige ich bei der Besprechung der Beispielanwendung in Kapitel 8. In Zeile 30 wird die Datei schließlich geschlossen. Sie sollten nie vergessen, Dateien, die Sie geöffnet haben, auch wieder zu schließen. So stellen Sie sicher, dass die Dateien nicht versehentlich geöffnet bleiben und so eventuell von anderen Anwendungen nicht gelesen werden können.
7.2.3 Textdateien schreiben Das Schreiben von Textdateien ist ähnlich „einfach“ wie das Lesen. Sie benötigen dazu idealerweise (weil wahrscheinlich am performantesten) eine Instanz der Klasse BufferedWriter, der Sie im Konstruktor eine Instanz der Klasse FileWriter übergeben. In deren Konstruktor übergeben Sie den Dateinamen. Das Ganze muss einmal wieder in eine Ausnahmebehandlung eingefügt werden. Dieses Mal verwende ich lediglich eine globale Ausnahmebehandlung, weil ich keine spezielle Fehlermeldung ausgeben will. Die Systemfehlermeldung, die die getMessage-Methode des Exception-Objekts zurückgibt, reicht mir für dieses Programm vollkommen aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19
String dateiname = "/projekte/java/demo.txt"; // Linux-Dateiname! /* Ermitteln der auf dem aktuellen System gültigen Zeilen-Trennzeichen (nur LF oder CR+LF) */ String zeilenTrennzeichen = System.getProperty("line.separator"); /* Eine Instanz von BufferedWriter zum Schreiben erzeugen */ BufferedWriter out = null; try { out = new BufferedWriter(new FileWriter(dateiname)); /* Zwei Zeilen out.write("Die out.write("ist out.write("Nur out.write("die
schreiben */ Antwort auf die Frage der Fragen "); 42." + zeilenTrennzeichen); leider kennt niemand mehr "); Frage." + zeilenTrennzeichen);
427
Sandini Bib
20 21 22 23 24 25 26 27
/* Datei schließen */ out.close(); } catch (Exception e) { System.out.println("Fehler beim Schreiben der Datei: " + e.getMessage()); }
In Zeile 5 wende ich einen kleinen „Trick“ an. Damit das Programm möglichst auf Windows- und Linux-Systemen läuft, ermittle ich die auf dem aktuellen System gültigen Zeichen für die Trennung von Zeilen in Textdateien. Die Methode getProperty der System-Klasse gibt diese Information (neben einigen anderen System-Eigenschaften) zurück. Diese Lösung habe ich übrigens (natürlich) im Internet gefunden. Nachdem in Zeile 12 dann das BufferedWriter-Objekt erzeugt wurde, schreibt das Beispielprogramm zwei Zeilen in die Textdatei. Dazu nutzen es die write-Methode. Diese Methode erzeugt keinen Zeilenumbruch, weswegen Sie zur Erzeugung eines solchen die aktuellen Zeilentrennzeichen anhängen müssen (was in Zeile 16 und 18 des Quellcodes geschieht). Schließlich müssen Sie die Datei nur noch schließen (Zeile 21). Im folgenden Beispielprogramm nutze ich die hier erlernten Techniken.
7.3
Zusammenfassung
In diesem Kapitel haben Sie zunächst gelernt, mit einfachen Arrays umzugehen. Sie können nun eine Liste von Daten in einem eindimensionalen Array verwalten und kennen auch die Grenzen dieser Art der Speicherung. Um flexibler programmieren zu können, sind Sie in der Lage, eine der Auflistungen Ihrer Programmiersprache zu verwenden. Sie müssen vielleicht noch einmal nachlesen, wie Sie diese verwenden, aber Sie kennen die Vorteile und das Prinzip dieser Art der Speicherung von Massendaten. Sie können sogar Objekte über eine Auflistung verwalten. Um Daten in einer Datei zu verwalten, sind Sie in der Lage unter Java eine Textdatei zu lesen und zu schreiben. Sie kennen das Prinzip der strukturierten Speicherung von Daten in Textdateien und können z. B. die Daten von Personen in einer solchen Datei verwalten.
428
Sandini Bib
7.4
Fragen und Übungen
1. Welche Vorteile bieten Auflistungen gegenüber einfachen Arrays? 2. Welchen Vorteil bieten Arrays gegenüber Auflistungen? 3. Was können Sie alles in einer Auflistung verwalten? 4. Wie finden Sie heraus, wie Sie in Delphi oder Kylix eine Textdatei
lesen und schreiben?
429
Sandini Bib
Sandini Bib
8
Programmieren einer Beispielanwendung
Sie lernen in diesem Kapitel:
le
n e rn
• wie Sie mit dem bisher Gelernten in der Praxis eine Anwendung zur Verwaltung von Adressdaten in einer Textdatei entwickeln. Das Kapitel soll nach der ganzen Theorie zu einem ersten größeren Erfolg führen und Ihnen zeigen, zu was Sie mittlerweile bereits in der Lage sind. Sie entwickeln im Verlauf dieses Kapitels eine Anwendung, die Adressdaten in einer Textdatei verwaltet und diese dem Anwender in einem Formular zur Verfügung stellt. Diese Anwendung beinhaltet sehr viele der bisher gelernten Techniken und führt sogar (wie immer in der Praxis) zu neuen Problemen, die aber natürlich gelöst werden. Das Beispiel soll ein wenig Leben in die graue Theorie bringen und das, was Sie bisher gelernt haben, in der Praxis anwenden. Und es soll auch Spaß machen . Mir ist dabei übrigens bewusst, dass es sich bereits um ein komplexes Programm handelt. Das Nachvollziehen meiner Programmierung ist mit Sicherheit nicht allzu einfach. Aber dieses Programm zeigt sehr viele Techniken, die in der Praxis wichtig sind. Und es zeigt auch, wie Sie das, was Sie bisher gelernt haben, anwenden. Nebenbei erhalten Sie auch noch eine erste einfache Telefonadress-Verwaltung.
-
8.1
Einleitung
Um ein sinnvolles Programm zu erzeugen, soll die Beispielanwendung die Daten der Textdatei aus Kapitel 7 einlesen und in einem Formular darstellen. Der Anwender soll die Möglichkeit haben, schrittweise durch die einzelnen Adressen zu gehen. Um die Daten im Programm sinnvoll zu speichern, verwende ich eine Auflistung, die einzelne Person-Objekte verwaltet.
(LQOHLWXQJ
431
Sandini Bib
Bei der Entwicklung eines solchen, bereits komplexen Programms treten sehr viele Probleme auf, die natürlich gelöst werden müssen. Dabei hilft das Internet. Suchen Sie bei www.google.de, groups.google.com/ advanced_group_search oder auf speziellen Java-Seiten wie dem in Entwicklerkreisen bekannten Java- und Internet Glossary von Mindprod (www.mindprod.com/jgloss.html) nach Lösungsansätzen. So bin auch ich vorgegangen, als ich bei der Entwicklung des Beispielprogramms auf Probleme stieß.
8.2
Vorbereitungen
8.2.1 Das Projekt und das Startformular Erzeugen Sie zunächst in Sun ONE Studio 4 ein neues Projekt, das Sie vielleicht Textdatenbank nennen. Fügen Sie dem Projekt ein Formular vom Typ JFrame hinzu. Stellen Sie das Layout des Formulars auf das NullLayout um und stellen Sie die Eigenschaft FORM SIZE POLICY auf GENERATE RESIZE CODE. Wenn Sie nicht mehr wissen, wie das geht, lesen Sie in Kapitel 2 nach. Wenn Sie unter Sun ONE Studio 4 ein neues Formular erzeugen, erstellen Sie in Wirklichkeit eine neue Klasse. Diese Klasse wird, wenn Sie ein JFrame-Formular benutzen, von der Basisklasse JFrame abgeleitet und erbt deswegen alle Eigenschaften und Methoden dieser Klasse. Das grundsätzliche Verhalten eines Formulars ist bereits in JFrame definiert. Sie müssen lediglich Steuerelemente hinzufügen und Programme schreiben. Alles das, was Sie dem Formular hinzufügen, wird als zusätzliche Eigenschaft (z. B. die Steuerelemente) oder zusätzliche Methode (z. B. die Methoden der Steuerelemente) in Ihrer neuen Klasse gespeichert. Platzieren Sie JLabel-, JTextField- und JButton-Steuerelemente auf dem Formular, bis dieses in etwa so aussieht wie in Abbildung 8.1.
432
Sandini Bib
Abbildung 8.1: Das Formular für das Programm zum Bearbeiten einer Textdatei
Die Steuerelemente habe ich folgendermaßen benannt: • Textfelder: txtVorname, txtNachname, txtTelefon, txtHandy und txtEMail • Schalter unten links: btnErster, btnVorheriger, btnNaechster und btnLetzter • Schalter rechts: btnNeu, btnSpeichern und btnBeenden Die Schalter mit den Zeichen |<, <, > und >| sollen dem Anwender die Möglichkeit geben, durch die einzelnen Adressen zu gehen. Der Schalter NEU soll eine neue Personen erzeugen, der Schalter SPEICHERN soll die aktuell angezeigten Daten (in der Auflistung) speichern.
8.2.2 Eine Klasse für die Personen Zur Speicherung der Personendaten benötigen Sie eine Klasse. Diese Klasse sollte Eigenschaften für die einzelnen Adressbestandteile besitzen. Methoden müssen nicht unbedingt enthalten sein. Eine Methode VollerName, die den vollen Namen der Person zurückgibt, ist aber immer hilfreich. Diese Klasse fügen Sie dem Projekt nun hinzu. Nennen Sie die Klasse vielleicht Person und programmieren Sie sie folgendermaßen:
9RUEHUHLWXQJHQ
433
Sandini Bib
01 public class Person 02 { 03 /* Eigenschaften */ 04 public String Vorname; 05 public String Nachname; 06 public String Telefon; 07 public String Handy; 08 public String EMail; 09 10 /* Methode zur Rückgabe des vollen Namens */ 11 public String VollerName() 12 { 13 return this.Vorname + " " + this.Nachname; 14 } 15 }
Eine Instanz dieser Klasse soll später eine Person aus der Textdatei repräsentieren und besitzt deswegen entsprechende Eigenschaften. Ein Konstruktor ist zurzeit noch nicht notwendig. Die Methode VollerName wird zwar wahrscheinlich ebenfalls nicht benötigt, ist aber sicherlich bei späteren Erweiterungen des Programms hilfreich.
8.2.3 Ein Dialog für Meldungen Ein Programm muss immer wieder Meldungen ausgeben. Unser Beispielprogramm benötigt an einigen Stellen einen Dialog für Meldungen. Für den Fall, dass beim Einlesen der Datei ein Fehler auftritt, muss dieser z. B. gemeldet werden, damit der Anwender informiert ist. Viele Programmiersprachen stellen dazu einen oder mehrere vorgefertigte Dialoge zur Verfügung. In Delphi und Kylix nutzen Sie für einfache Meldungen die Funktion ShowMessage und für komplexere Meldungen (mit verschiedenen Bestätigungsschaltern) die Funktion MessageDlg. Dialoge sind so definiert, dass der Benutzer den Dialog erst schließen muss, bevor das Programm weiterläuft. Solche Dialoge kennen Sie bereits. Schreiben Sie z. B. in einem Editor einen Text und schließen diesen, so fragt das Programm in einem Dialog, ob Sie die Datei vor dem Schließen speichern wollen. Leider besitzt Java scheinbar (nach meinen Recherchen) keinen vorgefertigten Dialog für Meldungen. Sie müssen einen eigenen erzeugen. Aber das ist dann auch gleich eine gute Übung.
434
Sandini Bib
Fügen Sie dem Projekt ein Formular vom Typ JDialog hinzu. Nennen Sie dieses Objekt vielleicht MessageDialog. Platzieren Sie darauf ein JButtonund ein JTextArea-Steuerelement. Ich verwende für die Ausgabe der Meldung kein Label, weil ein Label nicht in der Lage ist mehrere Zeilen Text auszugeben. Geben Sie den Steuerelementen die Namen btnOK und txtMessage. Stellen Sie die Eigenschaften des JTextArea-Steuerelements dann folgendermaßen ein:
Ein JDialog-Objekt wird für Dialoge verwendet
• lineWrap: true. Damit erreichen Sie, dass das Steuerelement den Text automatisch am rechten Rand umbricht • wrapStyleWord: true. Diese Einstellung bewirkt, dass das Steuerelement die Zeilen nicht einfach am letzten darstellbaren Zeichen einer Zeile umbricht, sondern dafür sorgt, dass immer ganze Wörter in die nächste Zeile umbrochen werden • editable: false. Damit erreichen Sie, dass der Anwender den Text nicht editieren kann • opaque: false. Über diese Einstellung erreichen Sie, dass der Hintergrund des Steuerelements transparent wird. Im Ergebnis sieht das Steuerelement nun aus wie ein Label, ist aber in der Lage, mehrere Zeilen Text anzuzeigen. Passen Sie dann noch den Text des Schalters an, bis der Dialog in etwa so aussieht wie in Abbildung 8.2.
Abbildung 8.2: Ein einfacher Dialog für Meldungen mit markiertem JTextArea-Steuerelement
Nun müssen Sie noch dafür sorgen, dass das Programm dem Dialog eine Meldung und idealerweise auch den Titel des Dialogs übergeben kann und dass dieser vom Anwender geschlossen werden kann. Das Programm wird später eine neue Instanz dieser Klasse erzeugen und diese über die show-Methode öffnen. Es wäre also sinnvoll, wenn die Meldung direkt im Konstruktor übergeben werden könnte. Der Konstruktor existiert bereits:
9RUEHUHLWXQJHQ
Meldung und Titel im Konstruktor übergeben
435
Sandini Bib
01 public MessageDialog(java.awt.Frame parent, boolean modal) { 02 super(parent, modal); 03 initComponents(); 04 }
Diesem Konstruktor übergeben Sie bei der Erzeugung eines Dialogs per Voreinstellung ein „Parent“-Formular, das diesen Dialog quasi besitzt. Das bewirkt u. a., dass der Dialog über dem anderen Formular und in dessen Mitte angezeigt wird. Der Datentyp des parent-Arguments ist Frame, weil das die Basisklasse aller Formulare ist. Das zweite Argument entscheidet darüber, ob der Dialog modal oder unmodal ist. Modale Dialoge müssen vom Anwender erst geschlossen werden, bevor das Programm weiterläuft. Unmodale Dialoge werden zwar angezeigt, das Programm läuft aber im Hintergrund weiter. Modale Dialoge sind der Quasi-Standard beim Programmieren, weil es eigentlich immer notwendig ist, das Programm so lange anzuhalten, wie der Dialog geöffnet ist. In Zeile 2 wird der von der Basisklasse JDialog geerbte Konstruktor unter Übergabe der Argumente aufgerufen. Zeile 3 ruft eine private Methode auf, die die Steuerelemente erzeugt, die Sie im Formulareditor auf dem Dialog anlegen. Sie müssen den Konstruktor nun so anpassen, dass eine Meldung und der Titel übergeben werden können: 01 public MessageDialog(java.awt.Frame parent, boolean modal, 02 String meldung, String titel) 03 { 04 super(parent, modal); 05 initComponents(); 06 this.txtMessage.setText(meldung); 07 this.setTitle(titel); 08 }
Achten Sie darauf, dass Sie im Konstruktor eines Formulars immer erst nach dem Aufruf von initComponents auf die Steuerelemente zugreifen. Vor diesem Aufruf sind die Steuerelemente noch nicht erzeugt. Ein Zugriff auf die Steuerelemente würde dann zu einer Ausnahme mit der wenig aussagekräftigen Meldung „null“ führen (weil Sie versuchen, mit einem Nullzeiger zu arbeiten). Der OK-Schalter
436
Nun fehlt nur noch die Methode für die Behandlung der Betätigung des OK-Schalters. Klicken Sie wieder doppelt auf den Schalter, um diese Methode zu erzeugen. Über den Aufruf der hide-Methode können Sie den Dialog schließen:
Sandini Bib
01 private void btnOKActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Dialog schließen */ 03 this.hide(); 04 }
Dummerweise kompiliert der Compiler diese Klasse nun nicht. Die Klasse enthält nämlich eine main-Methode, die eine Instanz der Klasse erzeugt und den alten Konstruktor aufruft. Diese Methode ist für unsere Zwecke vollkommen sinnlos, weil der Dialog nicht als separates Programm gestartet werden soll. Löschen Sie die main-Methode. Nun können Sie den Dialog kompilieren.
Entfernen der main-Methode
8.2.4 Ein Tipp Da Ihr Programm nun recht komplex wird, verlieren Sie schnell die Übersicht und finden zu bearbeitende Methoden u. U. nicht mehr allzu schnell. Sie können die einzelnen Elemente Ihres Programms aber recht einfach über den Explorer erreichen. Öffnen Sie in dessen Projekt-Register den Eintrag, der für das Formular steht. Öffnen Sie danach den Eintrag CLASS Formularname. Dort finden Sie einzelne Bereiche, in denen alle Eigenschaften (die hier als „Fields“ bezeichnet werden), Konstruktoren und Methoden übersichtlich aufgelistet werden. Über einen Doppelklick auf einem Eintrag können Sie sehr effizient zum entsprechenden Quellcode wechseln.
8.3
Einlesen der Daten
Beim Start der Anwendung müssen Sie nun zunächst die Textdatei einlesen. Die Daten sollen in eine Auflistung vom Typ ArrayList gelesen werden, da hier kein Schlüssel verwaltet wird. Eine ArrayList-Auflistung ermöglicht den einfachen Zugriff auf die einzelnen Elemente über einen Integer-Index. Da diese Auflistung so lange leben soll wie das Formular (sie soll ja die eingelesenen Personendaten verwalten), müssen Sie sie als private Eigenschaft der Formularklasse deklarieren. Öffnen Sie den QuellcodeEditor für das Start-Formular über einen Doppelklick auf den Eintrag CLASS STARTFORM im Projekt-Register des Explorers. Da die ArrayList-Klasse dem Paket java.util entstammt, sollten Sie dieses Paket importieren. Das Paket java.io sollten Sie gleich mit importieren, weil Sie es für das Einlesen der Textdatei benötigen:
437
Sandini Bib
import java.util.*; import java.io.*; public class StartForm extends javax.swing.JFrame { /* Private Eigenschaft für die Auflistung */ private ArrayList personen; ... Wo werden die Daten eingelesen?
Nun stellt sich die Frage, wo (bzw. wann) die Daten eingelesen und in der Auflistung gespeichert werden. Die Antwort darauf ist: wenn das Formular geöffnet wird. Dazu müssen Sie auf das Ereignis windowOpened reagieren. Sie könnten statt des Ereignisses windowOpened auch den Konstruktor des Formulars verwenden, um das Formular zu initialisieren. Das Ergebnis ist eigentlich dasselbe (ich kann dabei kaum Unterschiede erkennen). Diese Technik habe ich bereits bei der Erzeugung eines eigenen Dialogs genutzt (siehe Seite 434). Klicken Sie zur Erzeugung einer Ereignisbehandlungsmethode für das windowOpened-Ereignis (das entsprechend dem Namen immer dann aufgerufen wird, wenn das Formular geöffnet wird) mit der rechten Maustaste auf einen freien Bereich des Formulars und wählen Sie den Befehl EVENTS / WINDOW / WINDOWOPENED. Der Editor erzeugt eine passende Methode: private void formWindowOpened(java.awt.event.WindowEvent evt) { // Add your handling code here: }
Als Erstes sollten Sie in dieser Methode nun die Auflistung erzeugen: 01 private void formWindowOpened(java.awt.event.WindowEvent evt) { 02 personen = new ArrayList();
Dann können Sie die Textdatei einlesen. Zur Vereinfachung verwende ich hier nur eine Ausnahmebehandlung: 03 04 05 06 07 08 09 10 11
438
String dateiname = "C:\\Projekte\\Java\\Personen.txt"; BufferedReader in = null; try { in = new BufferedReader(new FileReader(dateiname)); String zeile; do { zeile = in.readLine();
Sandini Bib
Die Angabe des Dateinamens direkt im Programm ist für die Praxis nicht besonders ideal. Normalerweise müssten Sie den Dateinamen in einer separaten Konfigurationsdatei verwalten, die Sie beim Start des Programms einlesen. Alternativ könnten Sie die Datei auch in demselben Ordner erwarten, in dem das Programm gespeichert ist. Für das Beispielprogramm würde eine solche Verwaltung des Dateinamens aber zu weit führen, weil es in Java leider nicht allzu einfach ist, den Ordner zu ermitteln, aus dem heraus ein Programm gestartet wurde. Nach dem Einlesen einer Zeile wird eine private Methode leseZeile aufgerufen (die noch programmiert werden muss), die die Zeile zerlegen und in ein Objekt der Klasse Person schreiben soll. Da dieses Zerlegen auch fehlschlagen kann (z. B. wenn weniger oder mehr als fünf Teilstrings in einer Zeile gespeichert sind), soll diese Methode eine Funktion sein, die true oder false zurückgibt. Beim Aufruf kann dann überprüft werden, ob das Zerlegen eventuell fehlgeschlagen ist: 12 13
if (lesePerson(zeile) == false) {
Für den Fall, dass das Einlesen fehlgeschlagen ist, wird der vorgefertigte Dialog geöffnet: 14 15 16 17 18 19 20
/* Dialog erzeugen */ MessageDialog dialog = new MessageDialog(this, true, "Die Zeile '" + zeile + "' konnte nicht " + "korrekt eingelesen werden. Überprüfen Sie " + "die Datei", "Fehler"); /* Dialog öffnen */ dialog.show();
Das Programm erzeugt den Dialog zunächst in Zeile 15 bis 18, wobei die im Konstruktor erwarteten Argumente übergeben werden. Im ersten Argument wird das Parent-Formular definiert, das ja das aktuelle Formular selbst ist. Das zweite Argument legt fest, dass der Dialog modal geöffnet werden soll. Im dritten und vierten Argument werden dann die Meldung und der Titel des Dialogs übergeben. Im Fehlerfall wird das Programm auch gleich wieder beendet. Dabei sollten Sie die zurzeit noch geöffnete Datei zuvor schließen: 21 22 23
in.close(); System.exit(1); }
439
Sandini Bib
Die exit-Methode der System-Klasse ist eine besondere Methode zum Beenden eines Programms. Diese Methode führt dazu, dass das Programm sofort beendet wird. Für ein normales Schließen eines Formulars ist exit nicht geeignet. Sie sollten diese Methode immer nur im Fehlerfall aufrufen (exit wird allerdings auch intern in der Methode exitForm aufgerufen, damit das Programm beendet wird). Zum normalen Schließen eines Formulars verwenden Sie die dispose-Methode der Formularklasse. Damit stellen Sie sicher, dass alle Ereignismethoden, die nach dem Schließen eines Formulars abgearbeitet werden sollen, auch wirklich aufgerufen werden. Der exit-Methode übergeben Sie einen Integer-Wert. Der hier übergebene Wert kann über eine Umgebungsvariable des Betriebssystems von einem eventuellen Batch- oder Scriptprogramm, das unser Programm gestartet hat, ausgelesen werden. An einem Wert ungleich 0 erkennt dieses Programm dann, dass ein Fehler aufgetreten ist. Schließlich müssen Sie die Schleife noch abschließen und die Datei schließen: 24 25 26
} while (zeile != null); in.close(); }
In der Ausnahmebehandlung wird wieder der vorgefertigte Dialog geöffnet, um eine Fehlermeldung auszugeben. Außerdem wird das Programm auch hier wieder beendet: 27 catch (Exception e) 28 { 29 /* Dialog erzeugen */ 30 MessageDialog dialog = new MessageDialog(this, true, 31 "Fehler beim Lesen der Datei: " + e.getMessage(), "Fehler"); 32 33 /* Programm beenden */ 34 System.exit(1); 35 } 36 }
Damit ist die Programmierung der Ereignisbehandlungsmethode für das windowOpened-Ereignis zunächst abgeschlossen. Die nun zu programmierende Methode lesePerson können Sie einfach in einem freien Bereich der Formular-Klasse programmieren (nur nicht innerhalb einer anderen Methode): 01 private boolean lesePerson(String zeile) 02 {
440
Sandini Bib
Das Problem, das Sie nun lösen müssen, ist, die eingelesene Zeile in einzelne Bestandteile zu zerlegen. Ansätze zur Lösung dieses Problems finden Sie (wie immer) über die Newsgroup- oder die allgemeine Suche von Google. Sie werden schnell herausfinden, dass Sie dazu eine Instanz der Klasse StringTokenizer verwenden. In der Java-Api-Spezifikation, die Sie unter der Datei index.htm im Ordner bin der Java-Dokumentation finden, erfahren Sie mehr über die Konstruktoren und Methoden dieser Klasse. Beim Erzeugen einer Instanz dieser Klasse können Sie den zu zerlegenden String und das Trennzeichen übergeben. Zuvor sollten Sie aber noch überprüfen, ob die Zeile leer ist. Dazu sollten Sie zuvor alle rechts und links in der Zeile eventuell vorhandenen Leerzeichen entfernen. Für diese Aufgabe können Sie eine Methode des Strings verwenden (Strings sind in Java ja auch Objekte). Die Methode trim entfernt alle linken und rechten Leerzeichen: 03
zeile = zeile.trim();
Eine leere Zeile sollte einfach übergangen, aber nicht als Fehler gewertet werden. Dazu müssen Sie die Zeile mit einem leeren String ("") vergleichen. Beim Vergleich mit einem leeren String müssen Sie – wie bei allen Stringvergleichen in Java – aufpassen: Java-Strings sind einzelne Objekte. Wenn Sie zwei Strings miteinander vergleichen, vergleichen Sie die Referenzen, nicht den Inhalt der Objekte. Für Stringvergleiche müssen Sie in Java immer die compareTo-Methode verwenden. Diese Methode gibt 0 zurück, wenn beide Strings gleich sind: 04 05 06 07
if (zeile.compareTo("") == 0) { return true; }
Wenn die Zeile nicht leer ist, können Sie das StringTokenizer-Objekt erzeugen und über die Methode countTokens ermitteln, wie viele einzelne Token der String enthält: 08 09 10 11 12 13
else { StringTokenizer st = new StringTokenizer(zeile, ";"); if (st.countTokens() == 5) {
441
Sandini Bib
Wenn der String genau fünf Token enthält, können Sie diese über die nextToken-Methode auslesen. Diese Methode liest jeweils das nächste Token. Zuvor müssen Sie eine Instanz der Person-Klasse erzeugen, der Sie die einzelnen Token zuweisen: 14 15 16 17 18 19 20 21 22
/* Erzeugen eines neuen Person-Objekts */ Person p = new Person(); /* Schreiben der Eigenschaften */ p.Vorname = st.nextToken(); p.Nachname = st.nextToken(); p.Telefon = st.nextToken(); p.Handy = st.nextToken(); p.EMail = st.nextToken();
Dieses Objekt müssen Sie nun nur noch an die Auflistung anfügen. Eine ArrayList-Auflistung besitzt dazu die Methode add. Dieser Methode übergeben Sie nur das Objekt oder, am zweiten Argument, optional noch den Index, an dem das Objekt angefügt werden soll. Wenn Sie den Index nicht übergeben, wird das Objekt an das Ende angefügt: 23 24
/* Anfügen des Objekts an die Auflistung */ personen.add(p);
Schließlich geben Sie für den Erfolgsfall noch true und für den Fehlerfall false zurück: 25 26 27 28 29 30 31 32 33 } 34 }
/* Erfolg zurückmelden */ return true; } else { /* Nicht die korrekte Anzahl Teilstrings ermittelt */ return false; }
8.4
Test des ersten Entwurfs
Nun sollten Sie das Programm ein erstes Mal testen. Erzeugen Sie das Programm auf jeden Fall komplett über (ª) (Strg) (F11). Durch dieses explizite Erzeugen erreichen Sie, dass alle Ihre Änderungen berücksichtigt werden. In einigen Fällen meint die Entwicklungsumgebung ansonsten, dass einige Dateien nicht kompiliert werden müssen, obwohl Sie diese verändert haben.
442
Sandini Bib
-
Nachdem Sie Ihre Syntaxfehler beseitigt haben ( ) testen Sie das Programm. Sie können zwar noch nicht viel machen, aber die Daten werden (hoffentlich) erfolgreich eingelesen. Sie sollten aber auch Ihre Fehlerbehandlungen testen. Provozieren Sie dazu die erwarteten Fehler, indem Sie eine Zeile der Datei manipulieren und die Datei in einem weiteren Test umbenennen, sodass sie nicht gefunden wird. Das Programm müsste dann den Fehler melden (Abbildung 8.3).
Abbildung 8.3: Fehlermeldung über den eigenen Dialog unter Windows
8.5
Programmierung der eigentlichen Funktionalität des Programms
Nun, da die Textdatei eingelesen ist, programmieren Sie die eigentliche Funktionalität des Programms. Die Grundidee, die hinter dem Programm steckt, ist, die aktuelle Position in der Auflistung über eine Integer-Variable zu verwalten und nach dem Weiterbewegen des „Positionszeigers“ die Daten der aktuellen Person in den Textfeldern darzustellen. Nach dem Start des Programms soll der erste „Datensatz“ automatisch angezeigt werden. Sie müssen aber auch darauf reagieren, dass die Textdatei eventuell leer ist. Dann soll einfach ein neuer Datensatz (eine neue, leere Person) erzeugt und „angezeigt“ werden. Für den Positionszeiger benötigen Sie eine Variable, die während der gesamten Laufzeit des Formulars gültig ist und die in allen Methoden des Formulars verwendet werden kann. Diese Variable deklarieren Sie also wieder auf der Klassenebene: public class StartForm extends javax.swing.JFrame { /* Private Eigenschaft für die Auflistung */ private ArrayList personen; /* Private Eigenschaft für den Positionszeiger */ private int position = 0; ...
443
Sandini Bib
In der Methode, die auf das Ereignis windowOpened (also auf das Öffnen des Formulars) reagiert, programmieren Sie nun die Anzeige der Daten der ersten Person und reagieren darauf, dass noch keine Personen existieren. Diese Programmierung nehmen Sie unten unterhalb der bereits vorhandenen Programmierung vor: 01 private void formWindowOpened(java.awt.event.WindowEvent evt) { 02 personen = new ArrayList(); ... 34 35 36 37 38 39
... /* Programm beenden */ System.exit(1); } /* Die Daten der ersten Person anzeigen oder neue Person erzeugen */
Sie können über die size-Methode der ArrayList-Auflistung ermitteln, wie viele Elemente die Liste speichert. Wenn Personen gespeichert sind, soll die erste angezeigt werden: 40 41 42 43 44
if (personen.size() > 0) { /* Auflistung ist nicht leer: Person anzeigen */ zeigePerson(); }
Das Programm arbeitet mit einer (privaten) Methode zeigePerson. Diese Methode soll die Daten der aktuellen Person in den Textfeldern ausgeben und wird im Programm an einigen Stellen aufgerufen werden. Ich programmiere diese Methode später. Zunächst reagiert das Programm darauf, dass die Textdatei keine Personendaten speichert: 45 else 46 { 47 /* Auflistung ist leer: Neue "leere" Person erzeugen */ 48 personen.add(new Person()); 49 50 /* Dem Anwender eine Meldung übergeben */ 51 MessageDialog dialog = new MessageDialog(this, true, 52 "Die Personen-Datei ist zurzeit noch leer. " + 53 "Das Programm hat automatisch eine neue Person angelegt.", 54 "Information"); 55 dialog.show(); 56 } 57 }
444
Sandini Bib
Für den Fall, dass die Textdatei leer ist, wird zunächst eine neue „leere“ Person erzeugt. Damit der Anwender informiert ist, gibt das Programm (über unsere MessageDialog-Klasse) dann noch eine entsprechende Meldung aus. Das Erzeugen einer neuen Person bei einer leeren Datei ist übrigens ein kleiner Trick. Damit vermeide ich den ansonsten sehr komplizierten Umgang mit dem Sonderfall, dass die Datei leer ist.
8.5.1 Anzeigen der Personendaten Nun müssen Sie eine private Methode programmieren, die die Daten der aktuellen Person anzeigt. Die aktuelle Person wird über den Positionszeiger (die Eigenschaft position) gekennzeichnet. Diese Methode muss also nichts weiter machen, als eine Referenz auf das entsprechende Person-Objekt zu ermitteln, dessen Eigenschaften auszulesen und in die Textfelder zu schreiben. Eine ArrayList-Auflistung ermöglicht über die Methode get den Zugriff über einen Integer-Index. Wie bereits bei der Hashtable-Auflistung müssen Sie die zurückgegebene Referenz in den erwarteten Typ konvertieren: 01 private void zeigePerson() 02 { 03 /* Ermitteln der aktuellen Person */ 04 Person p = (Person)personen.get(this.position); 05 06 /* Ausgeben dieser Daten in den Textfeldern */ 07 this.txtVorname.setText(p.Vorname); 08 this.txtNachname.setText(p.Nachname); 09 this.txtTelefon.setText(p.Telefon); 10 this.txtHandy.setText(p.Handy); 11 this.txtEMail.setText(p.EMail); 12 }
Wenn Sie das Programm nun starten, wird die erste der gespeicherten Personen angezeigt (Abbildung 8.4).
445
Sandini Bib
Abbildung 8.4: Anzeige einer Person im Beispielprogramm unter Linux
8.5.2 Programmieren der Bewegungsschalter Die Programmierung der Schalter zum Bewegen in den Daten ist nun sehr einfach. Sie müssen in den einzelnen Methoden lediglich den Positionszeiger entsprechend umdefinieren und die Methode zeigePerson aufrufen. Bei den Schaltern für den vorherigen und den nächsten Datensatz müssen Sie allerdings aufpassen, dass der Positionszeiger nicht auf einen ungültigen Wert gesetzt wird. Die einzelnen Methoden erzeugen Sie einmal wieder über einen Doppelklick auf den Schaltern. Die entsprechenden Methoden sehen dann so aus: 01 private void btnErsterActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Positionszeiger setzen */ 03 this.position = 0; 04 /* Personendaten anzeigen */ 05 zeigePerson(); 06 } 01 private void btnVorherigerActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Positionszeiger setzen, wenn möglich */ 03 if (this.position > 0) 04 { 05 this.position--; 06 /* Personendaten anzeigen */ 07 zeigePerson(); 08 } 09 } 01 private void btnNaechsterActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Positionszeiger setzen, wenn möglich */ 03 if (this.position < personen.size() -1) 04 {
446
Sandini Bib
05 this.position++; 06 /* Personendaten anzeigen */ 07 zeigePerson(); 08 } 09 } 01 private void btnLetzterActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Positionszeiger setzen */ 03 this.position = personen.size() -1; 04 /* Personendaten anzeigen */ 05 zeigePerson(); 06 }
Der Umgang mit der aktuellen Position ist etwas schwierig. Sie müssen beachten, dass die Position niemals ungültig werden darf. Der Wert der Eigenschaft position darf also nicht kleiner als Null oder größer als die Anzahl der aktuell gespeicherten Elemente - 1 werden. Wenn Sie dies programmiert haben und das Programm testen, können Sie mit den Bewegungs-Schaltern bereits durch die gespeicherten Personen gehen. Nun müssen Sie nur noch ermöglichen, dass die geänderten Daten einer Person gespeichert werden, dass eine neue Person angelegt werden kann und dass die ganze Arbeit wieder in die Textdatei zurückgeschrieben wird.
8.5.3 Speichern der Änderungen des Anwenders Zum Speichern der im Formular geänderten Daten beschreiben Sie einfach das aktuelle Person-Objekt, ähnlich wie Sie beim Lesen das aktuelle Objekt ausgelesen haben (nur eben umgekehrt). Die Programmierung kann komplett in der Ereignisbehandlungsmethode des actionPerformedEreignisses des Schalters btnSpeichern erfolgen: 01 private void btnSpeichernActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Aktuelle Person ermitteln */ 03 Person p = (Person)personen.get(this.position); 04 05 /* Inhalt der Textfelder in diese Person schreiben */ 06 p.Vorname = this.txtVorname.getText(); 07 p.Nachname = this.txtNachname.getText(); 08 p.Telefon = this.txtTelefon.getText(); 09 p.Handy = this.txtHandy.getText(); 10 p.EMail = this.txtEMail.getText(); 11 }
447
Sandini Bib
8.5.4 Neuanlegen einer Person Das Neuanlegen einer Person ist genauso einfach wie das Speichern, was übrigens vorwiegend daran liegt, dass wir objektorientiert programmieren. Sie müssen lediglich ein neues Person-Objekt an die Auflistung anfügen, den Positionszeiger auf dieses neue Element setzen und die Textfelder leeren. Das Leeren der Textfelder übernimmt einfach unsere Methode zeigePerson: 01 private void btnNeuActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Neue Person an die Auflistung anfügen */ 03 personen.add(new Person()); 04 05 /* Positionszeiger auf den neuen Datensatz setzen */ 06 this.position = personen.size() - 1; 07 08 /* Person anzeigen, um die Felder zu leeren */ 09 zeigePerson(); 10 }
8.5.5 Speichern der Daten in der Textdatei Schließlich müssen Sie nur noch den Beenden-Schalter mit Funktionalität versehen und die Daten der Auflistung in die Textdatei zurückschreiben. In der actionPerformed-Ereignisbehandlungsmethode des Beenden-Schalters programmieren Sie nun lediglich einen Aufruf der dispose-Methode: 01 private void btnBeendenActionPerformed(java.awt.event.ActionEvent evt) { 02 /* Formular normal schließen, damit das Closed-Ereignis 03 aufgerufen wird */ 04 this.dispose(); 05 }
Das Speichern der Daten in die Textdatei übernimmt das Ereignis windowClosed des Formulars. Dieses Ereignis wird auch dann aufgerufen, wenn das Formular auf eine andere Weise als über den Beenden-Schalter geschlossen wird.
448
Sandini Bib
Sie sollten sich diese Technik gut merken, denn das ist auch in anderen Programmiersprachen enorm wichtig: Abschließende „Aufräumarbeiten“ sollten Sie in einem Formular immer in einer Methode vornehmen, die auf das Schließen des Formulars reagiert. Den Destruktor sollten Sie dazu übrigens nicht verwenden. Sie wissen nie genau, wann der Garbage Collector den Destruktor aufruft und ob dies überhaupt geschieht. Das Ereignis windowClosed wird hingegen immer und sofort aufgerufen, wenn das Formular geschlossen wird (und das gilt nicht nur für Java). Programmieren Sie das Speichern also in der Methode für das Ereignis windowClosed. Hier gehen Sie einfach die Auflistung durch und erzeugen aus jedem Objekt je eine Textzeile, die Sie in die Datei schreiben. Sie müssen das Schreiben der Datei wieder in eine Ausnahmebehandlung einfügen, aber das kennen Sie ja mittlerweile. Ich denke, der folgende Quellcode spricht für sich: 01 private void formWindowClosed(java.awt.event.WindowEvent evt) { 02 /* Daten in die Textdatei zurückschreiben */ 03 String dateiname = "C:\Projekte\Java\Personen.txt"; 04 05 /* Datei zum Beschreiben öffnen */ 06 try 07 { 08 /* BufferedWriter-Instanz zum Schreiben erzeugen */ 09 BufferedWriter br = new BufferedWriter( 10 new FileWriter(dateiname)); 11 12 /* Aktuelle Zeilen-Trennzeichen ermitteln */ 13 String zeilenTrennzeichen = System.getProperty( 14 "line.separator"); 15 16 /* Die einzelnen Personen durchgehen und in die 17 Datei schreiben */ 18 for (int i = 0; i < personen.size(); i++) 19 { 20 /* Referenz auf die Person holen */ 21 Person p = (Person)personen.get(i); 22 23 /* Eine Textzeile zusammensetzen */ 24 String zeile = p.Vorname + ";" + p.Nachname + ";" + 25 p.Telefon + ";" + p.Handy + ";" + p.EMail; 26 27 /* Diese Zeile in die Datei schreiben */ 28 br.write(zeile + zeilenTrennzeichen); 29 }
449
Sandini Bib
30 31 32 33 34 35 36 37 38 39 40 41 42 }
/* Textdatei schließen */ br.close(); } catch (Exception e) { /* Fehlermeldung anzeigen */ MessageDialog dialog = new MessageDialog(this, true, "Fehler beim Schreiben der Datei: " + e.getMessage(), "Fehler"); dialog.show(); }
8.6
Weitere interessante Features
Nun ist das Beispielprogramm in einer ersten Version fertig. Ich gehe nicht mehr weiter auf dieses Programm ein und überlasse Ihnen die Weiterentwicklung. Interessante Features wären noch: • eine Möglichkeit, Adressen zu suchen, • ein Zwang, beim Ändern und Anfügen mindestens den Vor- und den Nachnamen einzugeben, • eine Möglichkeit für den Anwender, die Daten zwischendurch in die Textdatei zu schreiben, • ein automatisches Speichern beim Wechsel des Datensatzes oder beim Schließen des Formulars, • das Speichern nur gefüllter Adressen in windowClosed (leere Adressen sollten nicht in die Textdatei gespeichert werden), • die Verwaltung der Datei im Ordner der Anwendung und das dynamische Ermitteln dieses Ordners im Programm (was eigenartigerweise in Java sehr kompliziert ist), • die Verwaltung des Dateinamens der Textdatei in einer Konfigurationsdatei (was ebenfalls recht kompliziert zu sein scheint), • oder die Möglichkeit, den Dateinamen beim Aufruf des Programms zu übergeben. Viel Spaß bei der weiteren Programmierung dieses Beispiels
450
-.
Sandini Bib
8.7
Zusammenfassung
In diesem Kapitel haben Sie erfahren, wie Sie eine bereits komplexere Anwendung in der Praxis entwickeln. Sie können nun Anwendungen schreiben, die Daten in Textdateien verwalten und dem Anwender diese Daten zur Bearbeitung zur Verfügung stellen. In diesem Zusammenhang haben Sie die Bedeutung von Auflistungen zur Speicherung von Massendaten erkannt. Sie haben zudem gelernt, dass das Abfangen von Ausnahmen in einer Anwendung enorm wichtig ist. Sie wissen nun, dass das Schreiben eigener Methoden in der Regel zur einer enormen Arbeitserleichterung im weiteren Verlauf der Programmierung führt und Ihr Programm nebenbei auch übersichtlicher macht. Schließlich haben Sie nun etwas mehr Erfahrung mit der ereignisorientierten Programmierung und können auch bereits eigene Dialogformulare gestalten und in Ihren Anwendungen einsetzen.
=XVDPPHQIDVVXQJ
451
Sandini Bib
Sandini Bib
9
Daten in Datenbanken verwalten
Sie lernen in diesem Kapitel:
le
n e rn
• was eine Datenbank ist, • wie Sie Datenbanken mit der Datenbank-Standardsprache SQL erzeugen, • wie Sie in Java auf eine Datenbank zugreifen, • wie Sie Daten abfragen, • wie Sie Daten an eine Datenbank anfügen • und wie Sie Daten in einer Datenbank ändern und löschen. Das Kapitel soll Ihnen einen Einblick in die Welt der Datenbanken verschaffen. Ich kann hier nur absolut grundlegend auf Datenbanken eingehen. Das Thema ist an sich so komplex, das ganze Bücher darüber geschrieben werden. Ich zeige aber, was eine Datenbank prinzipiell ist und wie Sie mit der Standard-Datenbank-Manipulations- und Abfragesprache SQL (Structured Query Language) prinzipiell arbeiten. Da Java einen sehr einfachen Zugriff auf Datenbanken ermöglicht (und weil die Datenbank-Komponenten in der Personal- bzw. Open-Edition von Delphi und Kylix fehlen), verwende ich Java für das Beispielprogramm. Das Prinzip ist aber in anderen Sprachen dasselbe. Sie nutzen lediglich andere Objekte zum Zugriff. Als Datenbank verwende ich MySQL, ein frei verfügbares Datenbanksystem der Firma MySQL AB, das unter Windows und Linux ausgeführt werden kann.
453
Sandini Bib
Die in diesem Kapitel am Beispiel beschriebenen Grundprinzipien des Datenbankdesigns und die verwendeten SQL-Anweisungen kann ich im Einzelfall nicht umfassend erläutern. Ich will aber erreichen, dass Sie wissen, was eine Datenbank ist und wie Sie mit SQL prinzipiell Datenbanken erzeugen, abfragen und manipulieren können. Wenn Sie das alles, was hier beschrieben wird, besser verstehen oder alle Möglichkeiten kennen lernen wollen, müssen Sie noch andere Literatur zu Rate ziehen. Das Buch „SQL lernen“ aus der Lernen-Reihe von Addison-Wesley behandelt z. B. SQL wesentlich ausführlicher, als ich es hier kann.
9.1
Was ist eine Datenbank?
Eine Datenbank speichert Daten (was auch sonst ...). Diese Speicherung erfolgt aber, im Gegensatz zu Dateien, auf eine sehr effiziente und flexible Weise. Moderne Datenbanksysteme haben keine Probleme damit, Millionen von Datensätzen zu verwalten. Über eine spezielle Abfragesprache (meist SQL) können Sie gezielt Daten aus der Datenbank auslesen. Und das wird in der Regel mit einer sehr hohen Geschwindigkeit ausgeführt, auch wenn sehr viele Datensätze gespeichert sind. Daneben ermöglicht eine Datenbank aber auch das einfache Anfügen, Ändern und Löschen von Daten. An eine Datenbank können Sie z. B. ohne Probleme einen einzelnen Datensatz anfügen. Die Arbeit des Abfragens, des Hinzufügens, des Änderns und des Löschens übernimmt die Datenbank für Sie. Wenn Sie hingegen mit einer einfachen Datei zur Speicherung von Daten arbeiten würden, müssten Sie das alles selbst programmieren. OK, in Kapitel 8 erschien dies relativ einfach. Wenn Sie aber mehrere Tausend oder sogar Millionen Datensätze verwalten, können Sie diese nicht mehr so einfach in den Speicher lesen und dort bearbeiten. Dann müssten Sie mit den recht komplexen Random-Dateien arbeiten. Datenbanken erlauben aber noch wesentlich mehr als einfache Dateien. So können Sie die Daten beim Einlesen nach allen denkbaren Kriterien sortieren, die Daten verschiedener Bereiche miteinander in Beziehung setzen und die so in Beziehung stehenden Daten gezielt gemeinsam abfragen.
9.1.1 Datenbanken, Datenbanksysteme und Datenbankmanagementsysteme Eine Datenbank ist im Prinzip eine Datei oder eine Sammlung von Dateien, in denen Daten gespeichert sind. Mit diesen Dateien können Sie direkt nichts anfangen, weil Sie nicht wissen, auf welche Weise die Daten dort verwaltet werden. Ausnahmen bilden Textdatenbanken wie
454
Sandini Bib
die, die Sie in Kapitel 8 kennen gelernt haben. Die dabei verwendeten Dateien können Sie mit den Möglichkeiten der Programmiersprache bearbeiten. Aber solche „unechten“ Datenbanken besitzen nur sehr einfache Möglichkeiten (im Prinzip die, die Sie selbst programmieren). Echte Datenbanken sind hingegen immer Bestandteil von so genannten Datenbanksystemen (DBS). Ein Datenbanksystem besteht aus der Datenbank (also aus den Dateien, die die Daten speichern) und einem so genannten Datenbankmanagementsystem (DBMS). Das DBMS übernimmt das Management der Dateien. Wenn Sie z. B. einen Datensatz hinzufügen, senden Sie dem DBMS einen entsprechenden Befehl. Dieses übernimmt dann das Hinzufügen des Datensatzes in die entsprechende Datei. Ähnliches passiert, wenn Sie Daten abfragen. Ihr Programm sendet dem DBMS einen Befehl, dieses interpretiert den Befehl, liest die Datensätze aus, die zu Ihrer Abfrage passen, und liefert diese zurück.
Programm sendet Befehl zum Hinzufügen
DBMS
fügt Datensätze an
Datenbank
Datenbanksysteme bestehen aus Datenbanken und DBMS
Programm sendet Befehl zum Auslesen
liefert Datensätze zur Bearbeitung
DBMS liest Datensätze aus Datenbank
Abbildung 9.1: Darstellung des Hinzufügens und des Auslesens von Daten in einem DBS
Sie können aber z. B. in einer Artikeldatenbank auch den Befehl senden, die Preise aller Artikel, die der Kategorie „Prozessoren“ angehören, um zehn Prozent zu erhöhen. Das DBMS interpretiert diesen Befehl, sucht alle Artikel heraus, die der angegebenen Bedingung entsprechen, und erhöht deren Feld Preis um zehn Prozent. Es nimmt Ihnen und dem Programm damit eine Menge an Arbeit ab. Moderne Datenbankmanagementsysteme sind selbstständige Programme, die allerdings im Hintergrund (ohne eine Oberfläche) ausgeführt werden (unter Windows normalerweise als Dienst und unter Linux als Daemon). Einfache DBMS liegen aber auch schon einmal le-
:DV LVW HLQH 'DWHQEDQN"
455
Sandini Bib
diglich als Bibliothek vor, die von einem Programm genutzt werden kann. Das hängt ganz von der Art des Datenbanksystems ab. Für Sie als Programmierer spielt das allerdings keine große Rolle, denn Sie nutzen in modernen Programmiersprachen immer dieselben Objekte zum Zugriff. Was im Hintergrund passiert, bekommen Sie gar nicht mit. Lediglich aus der Sicht der Performance ist der Unterschied wichtig: DBMS, die als Programm ausgeführt werden, können Daten effizient im Arbeitsspeicher zwischenspeichern („cachen“) und erhöhen damit bei wiederholten Zugriffen die Performance um ein Vielfaches. Einfache DBMS, die nicht als Programm ausgeführt werden, sind z. B. Access und dBASE. Als Programm werden die Datenbankmanagementsysteme von Oracle, MySQL, vom Microsoft SQL Server und von Borland Interbase ausgeführt.
9.1.2 Objektorientierte Datenbanksysteme Die modernsten Datenbanksysteme sind objektorientiert. Diese Datenbanksysteme sind eigentlich ganz einfach (weil objektorientiert). Die Daten werden in Form von Auflistungen (!) von Objekten verwaltet. Wenn Sie mit einem Programm auf solche Daten zugreifen, fragen Sie Daten ab (z. B. alle Kunden, die in Köln wohnen) und erhalten eine Auflistung von Kundenobjekten zurück, mit der Sie im Programm auf die gewohnte Weise arbeiten können. Die Klassen der Auflistungen und der gespeicherten Objekte sind dabei allerdings speziell und werden vom DBMS zur Verfügung gestellt. Mit einem speziellen Administrationswerkzeug oder über eine spezielle Datenbank-Befehlssprache erzeugen Sie bei der Gestaltung einer Datenbank die entsprechenden Klassen innerhalb der Datenbank. Die Arbeit mit diesen Auflistungen und Objekten unterscheidet sich aber nicht von der mit normalen Objekten. Sie können die Auflistung durchgehen, gezielt über den Schlüssel nach Daten suchen, Objekte auslesen und diese auch verändern. Im Gegensatz zu der Anwendung aus Kapitel 8 werden Ihre Änderungen aber in der Regel sofort und automatisch in die Datenbank geschrieben. Sie haben also nur sehr wenig Arbeit damit. Ein wesentliches Feature von OOPDatenbanken ist, dass einzelne Objekte ohne Probleme direkt mit anderen Objekten in Beziehung gesetzt werden können. In einer Bestelldatenbank können Sie so ein Kunde-Objekt z. B. mit den Bestell-Objekten in Beziehung setzen, die die Bestellungen des Kunden speichern. Die Auflösung dieser Beziehungen im Programm ist dann mehr als einfach. Sie müssen lediglich die Eigenschaft Bestellungen des Kunde-Objekts auslesen. Diese Eigenschaft wird dann auf eine Auflistung verweisen, die Sie ganz einfach durchgehen können und an die Sie genauso einfach neue Datensätze (hier: Bestellungen) anfügen können.
456
Sandini Bib
Leider sind objektorientierte Datenbanksysteme noch lange kein Standard. Die wenigsten Firmen setzen solche Systeme heute schon ein (obwohl es diese seit mehreren Jahren gibt). Deshalb gehe ich nicht weiter darauf ein. Wenn Sie einmal eines der besten OOP-Datenbanksysteme kennen lernen wollen, schauen Sie bei www.cache.de vorbei. Diese Datenbank von InterSystems ist sehr einfach zu programmieren, dabei aber in der Lage sehr große Datenmengen mit einer hohen Geschwindigkeit zu verarbeiten. Viele große Firmen setzen dieses System ein. Als Programmierer erhalten Sie eine kostenlose Entwicklerversion.
9.1.3 Relationale Datenbanksysteme Relationale Datenbanksysteme verwalten keine Objekte, sondern speichern die Daten in logischen Tabellen. Auf diese DBS gehe ich näher ein, weil sie zurzeit noch der aktuelle Standard sind. Ich will relationale Datenbanken direkt am Beispiel erläutern. Eine einfache relationale Datenbank, die Buchdaten speichern soll, besteht aus zwei Tabellen. Eine Tabelle speichert die Daten der Bücher, eine weitere die der Autoren. Abbildung 9.2 stellt diese Datenbank dar.
Abbildung 9.2: Zwei Tabellen einer einfachen relationalen Datenbank
Diese Tabellen stellen lediglich eine logische Sicht auf die Daten dar. Das DBMS speichert die Daten auf eine vollkommen andere Weise, die uns in der Regel aber unbekannt ist (weil es sich dabei meist um ein gut gehütetes Geheimnis des DBMS-Herstellers handelt). Die logische Sicht auf relationale Datenbanken ist aber bei allen relationalen DBMS gleich: Eine Tabelle verwaltet einzelne Datensätze, die aus einzelnen Feldern bestehen. Im Prinzip können Sie sich einen Datensatz als Objekt vorstellen, das ja auch aus einzelnen Feldern (den Eigenschaften) besteht. Leider sind die einzelnen Datensätze in relationalen Datenbanken nicht wirklich Objekte, was die Arbeit mit den Daten ein wenig erschwert.
:DV LVW HLQH 'DWHQEDQN"
457
Sandini Bib
Beziehungen Die beiden Tabellen des Beispiels stehen miteinander in Beziehung. Erkennen Sie diese Beziehung? Jeder Autor besitzt eine eindeutige Id. In der Bücher-Tabelle ist diese Id angegeben. Damit wird jedem Buch ein Autor zugewiesen. Die Daten der Autoren und der Bücher werden separat gespeichert (eben in zwei unterschiedlichen Tabellen). Damit wird erreicht, dass Daten nicht redundant (mehrfach) gespeichert werden. Würde die Datenbank keine Autoren-Tabelle besitzen und die Daten des jeweiligen Autors in der Bücher-Tabelle verwalten, würden Daten mehrfach gespeichert werden. Natürlich können Sie auch solche Datenbanken gestalten. Dann haben Sie aber mit massiven Problemen zu kämpfen, wenn Daten geändert werden müssen. Wenn sich beispielsweise der Nachname eines Autors ändert (was zugegebenermaßen in der Praxis nicht vorkommt), müssten Sie den Namen in allen Buch-Datensätzen ändern, die diesen Autor speichern. Eine Verwaltung der Autoren in einer separaten Tabelle macht das Ändern aber sehr einfach. Da die Bücher-Tabelle nur die Id (den Schlüssel) des Autors speichert, müssen dort gar keine Änderungen vorgenommen werden.
9.1.4 SQL Zur Abfrage und Manipulation relationaler Datenbanken stellen alle DBMS eine spezielle Sprache zur Verfügung. Die meisten verwenden dabei SQL (Structured Query Language1). SQL ist eine ursprünglich von E. F. Codd, einem damaligen Mitglied des IBM Research Laboratory, entwickelte und seit einigen Jahren vom ANSI2-Institut standardisierte Sprache, die speziell für relationale Datenbanksysteme entwickelt wurde. SQL stellt mehrere Befehle zur Verfügung, die Sie in Ihren Programmen über spezielle Objekte an das DBMS senden können. Wenn Sie z. B. in der Beispieldatenbank alle Bücher des Autors mit der Id 1 sortiert nach Titel auslesen wollen, verwenden Sie den folgenden SQL-Befehl: SELECT * FROM Bücher WHERE Autor = 1 ORDER BY Titel;
Einen neuen Autor anfügen können Sie z. B. so: INSERT INTO Autoren (Id, Vorname, Nachname) VALUES (4, 'Peter', 'Hoeg');
Das DBMS empfängt diesen Befehl, interpretiert ihn und führt ihn aus. Im ersten Fall würde das DBMS eine Liste aller Bücher zurückgeben, die im Feld Autor den Wert 1 gespeichert haben. Im zweiten Fall würde es einen neuen Datensatz in der Autoren-Tabelle anlegen.
458
1.
Strukturierte Abfragesprache
2.
American National Standards Institute, www.ansi.org
Sandini Bib
SQL ist sehr mächtig. Ganze Bücher beschäftigen sich mit dieser Sprache. Sie können über SQL nicht einfach nur Daten gezielt und sortiert abfragen, anfügen, verändern oder löschen. Sie können auch in SQLAbfragen direkt arithmetische Berechnungen vornehmen und dabei spezielle SQL-Funktionen (wie z. B. SUM zur Berechnung der Summe eines Feldes) aufrufen. Ein wesentliches Feature ist aber die Möglichkeit, die Beziehungen zwischen Tabellen in einer SQL-Anweisung auflösen zu können. Wenn Sie z. B. alle Bücher auslesen und zu jedem Buch die vollen Autor-Informationen erhalten wollen, verwenden Sie die folgende (anfänglich etwas schwer zu verstehende) SQL-Anweisung:
Auflösen von Beziehungen über SQL
SELECT ISBN, Titel, Vorname, Nachname FROM Bücher INNER JOIN Autoren ON Bücher.Autor = Autoren.AutorId;
Diese Abfrage fragt einige der Felder der Tabellen Bücher und Autoren ab. Die Abfrage setzt die Tabellen dabei direkt über die INNER JOIN-Klausel in Beziehung, wobei die Felder angegeben werden, über die die Beziehung realisiert ist. Das Ergebnis dieser Abfrage ist eine temporäre (nur im Arbeitsspeicher existierende) Tabelle, die die Daten der beiden Datenbanktabellen zusammengefasst darstellt (Abbildung 9.3).
Abbildung 9.3: Das Ergebnis einer Abfrage, die zwei Tabellen kombiniert
Im Ergebnis der Abfrage kommen Daten nun zwar redundant vor. Das ist aber absolut in Ordnung, denn das Ergebnis ist nur temporär. Das Ergebnis kann auf eine sehr einfache Weise im Programm verarbeitet werden. Das Programm kann z. B. eine Liste der Bücher mit den AutorenNamen drucken. Wie das prinzipiell geht, erfahren Sie im weiteren Verlauf dieses Kapitels. Dass die Beziehungen zwischen Tabellen in Abfragen explizit aufgelöst werden müssen, ist übrigens ein großes Problem relationaler Datenbanken. In objektorientierten Datenbanken müssen Sie so etwas nicht machen. In einer solchen Datenbank würden Sie mit einer SQL ähnlichen Sprache einfach eine Auflistung aller Bücher abfragen (die eventuell einem bestimmten Kriterium entsprechen). Die einzelnen BuchObjekte, die Sie damit erhalten, würden einfach eine Eigenschaft Autor besitzen, die auf ein Autor-Objekt verweist. Wenn Sie die Buch-Auflistung durchgehen, können Sie über die Autor-Eigenschaft sofort auch die Daten des Autors auslesen.
:DV LVW HLQH 'DWHQEDQN"
459
Sandini Bib
9.2
Datenbankdesign light: Erzeugen der Beispieldatenbank mit MySQL
Nun wollen wir direkt zur Praxis übergehen. Sie erzeugen dazu mit SQL zunächst die Beispieldatenbank, die ich in Abbildung 9.2 auf Seite 457 vorgestellt habe.
9.2.1 MySQL Als Datenbanksystem verwende ich MySQL. MySQL ist ein DBMS, das nur unter bestimmten Umständen kostenpflichtig ist. In den meisten Fällen können Sie MySQL kostenfrei nutzen. Sie finden MySQL auf der Buch-CD oder im Internet unter der Adresse www.mysql.com. Dort können Sie sich auch die Lizenzbestimmungen anschauen. Die Installation und den Start von MySQL beschreibe ich aus Platzgründen nicht hier, sondern im separaten Artikel „Installation“, den Sie auf der Buch-CD oder unter der Adresse www.juergen-bayer.net/ buecher/programmierenlernen/artikel/installation.html finden. Ich gehe im weiteren Verlauf dieses Kapitels davon aus, dass MySQL installiert und erfolgreich gestartet ist. Die MySQL-Dokumentation finden Sie im Programmordner im Ordner docs. Öffnen Sie dort die Datei index_toc.html. Eine durchsuchbare Version finden Sie im Internet unter der Adresse www.mysql.com/doc/ en/index.html.
9.2.2 Anlegen einer Datenbank über den MySQL-Monitor MySQL beinhaltet mit dem MySQL-Monitor ein einfaches Administrationsprogramm, das Sie an der Konsole aufrufen können, um eine Datenbank zu administrieren. Dieses Programm starten Sie normalerweise unter Angabe eines Benutzernamens und eines Passworts: mysql –u Benutzername Passwort
MySQL ist ein geschütztes Datenbanksystem. Normalerweise müssen Sie sich immer mit einem Benutzernamen und einem Passwort einloggen. Diese Namen und Passwörter werden in der Systemdatenbank mysql (in der Tabelle user) verwaltet und können vom Administrator in der Systemdatenbank oder über spezielle SQL-Anweisungen bearbeitet werden. Nach der MySQL-Installation existiert lediglich ein Benutzer, der root. Dieser Benutzer ist unter MySQL der Super-Administrator, der
460
Sandini Bib
alle verfügbaren Rechte besitzt. Wenn Sie beim Start von mysql keinen Benutzernamen eingeben, werden Sie automatisch als der aktuelle Systembenutzer eingeloggt. Da alle Benutzer des lokalen Systems per Voreinstellung volle Rechte besitzen (was in Systemen, die in Firmen eingesetzt werden, natürlich ein riesengroßes Sicherheitsloch darstellt), funktioniert der Start des SQL-Monitors auch ohne Angabe des Benutzers: mysql
Im nun geöffneten Konsolenprogramm können Sie Befehle eingeben. Neben den SQL-Befehlen sind auch eine Menge spezieller MySQLBefehle möglich. Der Befehl show databases; zeigt z. B. alle aktuell bestehenden Datenbanken an. Nach der Installation sind das die Datenbanken mysql und test.
Abbildung 9.4: Abfrage der aktuell vorhandenen Datenbanken im MySQL-Monitor
In der Datenbank mysql verwaltet MySQL alle Einstellungen des Systems. Die Datenbank test ist eine leere Testdatenbank. Datenbank anlegen Die Datenbank legen Sie nun über eine SQL Anweisung an. Beachten Sie, dass MySQL bei Datenbanknamen Groß- und Kleinschreibung unterscheidet (was bei anderen Namen aber nicht der Fall ist): CREATE DATABASE Buchdaten;
SQL-Anweisungen werden (ähnlich wie Programmanweisungen in Object Pascal und Java) immer mit einem Semikolon abgeschlossen.
461
Sandini Bib
Üblicherweise werden die Schlüsselwörter einer SQL-Anweisung großgeschrieben. Obwohl alle DBMS, die ich kenne, auch mit kleingeschriebenen SQL-Anweisungen zurechtkommen, halte ich mich in diesem Buch an den Brauch. Anweisungen, die nicht zum StandardSQL gehören, sondern eine spezielle Erweiterung von MySQL sind, schreibe ich zur Unterscheidung klein. Meldet das Programm nach der Ausführung dieser Anweisung „OK“, können Sie nun zu dieser Datenbank wechseln: USE Buchdaten;
Das Programm meldet „Database changed“, wenn der Wechsel erfolgreich war. Tabellen werden über CREATE TABLE angelegt
Datenfelder besitzen einen Datentyp
Nun legen Sie die Tabellen an. Dazu verwenden Sie die SQL-Anweisung CREATE TABLE. Tabellen bestehen aus Datensätzen, die wiederum aus einzelnen Feldern bestehen. Ein Feld speichert einen Teil des Datensatzes, der nicht weiter zerlegt werden kann. Eine Autoren-Tabelle besteht z. B. aus den Feldern Id, Vorname und Nachname. Der Vor- und der Nachname werden in separaten Feldern verwaltet. Damit ist sichergestellt, dass die Tabelle mit der maximal möglichen Flexibilität bearbeitet werden kann. So können Sie z. B. beim Abfragen der Daten nach dem Nachnamen sortieren. Oder Sie können alle Autoren abfragen, deren Nachname Adams ist. Würden Sie den Vor- und den Nachnamen hingegen in einem einzigen Feld verwalten, wäre so etwas nicht möglich. Dieses separate Speichern einzelner Informationen gehört übrigens schon zu den wichtigen Prinzipien des Datenbank-Designs. Ein Datenfeld besitzt ähnlich einer Variable in einem Programm einen Datentyp. Die Datentypen von Datenfeldern gleichen den Datentypen eines Programms, werden aber anders benannt. Die Namen sind zwar vom ANSI-Komitee festgelegt, viele Datenbanksysteme verwenden aber vom Standard abweichende Namen. In der Praxis bleibt Ihnen nichts weiter übrig, als in der Dokumentation des DBS nachzuschlagen. In MySQL verwenden Sie für Integer-Felder den Datentyp int und für Textfelder den Datentyp varchar. varchar steht für „variable char“. Ein solches Feld ist in der Datei immer nur so groß wie die darin gespeicherten Daten und belegt deswegen nur den minimal benötigten Speicherplatz. Sie müssen bei der Erzeugung eines solchen Feldes die maximale Größe der späteren Daten angeben. In der Praxis können Sie ruhig sehr große Werte verwenden. Nur dann, wenn Sie erreichen wollen, dass die Tabelle in einem solchen Feld nicht mehr als eine bestimmte Anzahl Zeichnen speichert, sollten Sie die Anzahl niedrig definieren. Stellen Sie aber lieber zu viel als zu wenig mögliche Zeichen ein, dann haben Sie bei
462
Sandini Bib
der Benutzung der Datenbank keine Probleme zu erwarten. MySQL erlaubt in einem solchen Feld maximal 255 Zeichen. Die Autor-Tabelle wird nun folgendermaßen angelegt: CREATE TABLE Autoren ( Id int PRIMARY KEY NOT NULL, Vorname varchar(255) NOT NULL, Nachname varchar(255) NOT NULL);
Zum Anlegen der Tabelle können Sie die einzelnen Zeilen separat eingeben. Der MySQL-Monitor wartet mit der Ausführung des Befehls, bis Sie das abschließende Semikolon eingegeben haben, und ermöglicht nach einer eingegebenen und bestätigten Zeile die Eingabe weiterer Zeilen. Wenn das Programm dann nach der letzten Zeile „Query OK“ meldet, wurde die Tabelle angelegt. Sie können SQL-Anweisungen auch in eine Textdatei ablegen und diese im MySQL-Monitor in einem Rutsch über den Befehl source Dateiname ausführen. Das Feld Id wird im Beispiel mit einer besonderen Bedeutung versehen. Durch die Angabe von PRIMARY KEY wird das Feld zum Primärschlüssel der Tabelle. Ein Primärschlüssel hat die Bedeutung, dass er einen Datensatz eindeutig identifiziert. Über die Autor-Id kann ein Autor eindeutig ermittelt werden, weil die Id (zunächst theoretisch) eindeutig ist. In der Datenbank sorgt die Festlegung, dass das Feld der Primärschlüssel ist, automatisch für diese Eindeutigkeit. Der Primärschlüssel ist ein sehr wichtiges Konzept relationaler Datenbanken. Jede Tabelle sollte einen Primärschlüssel besitzen (was aber niemand erzwingt). Durch das Vorhandensein eines Primärschlüssels vermeiden Sie viele Probleme und erleichtern Programmen den Zugriff auf die Daten.
Der Primärschlüs-
Im Beispiel definiere ich alle Felder zusätzlich über die Angabe von NOT NULL so, dass das jeweilige Feld beim Hinzufügen oder Ändern von Datensätzen nicht leer sein darf. Datenbankfelder, die nicht mit NOT NULL de-
Verhindern, dass
sel identifiziert einen Datensatz eindeutig
Felder leer bleiben
finiert wurden, zwingen den Anwender bzw. das Programm nicht, beim Anlegen oder Ändern eines Datensatzes Daten abzulegen. Das Feld kann in diesem Fall leer bleiben und speichert den speziellen Datenbankwert Null. Diesen Wert sollten Sie nicht mit dem Nullzeiger bei der objektorientierten Programmierung verwechseln. Der Wert Null steht in einer Datenbank dafür, dass ein Feld leer ist. Leere Felder sind in der Praxis meist schwer zu handhaben. Normalerweise sollten Sie dafür sorgen, dass alle Felder auch mit Daten versorgt werden, wenn Datensätze angefügt oder geändert werden. In einigen Fällen ist es aber auch sinnvoll, leere Felder zuzulassen.
463
Sandini Bib
Unter MySQL funktioniert die Definition von NOT NULL nicht so, wie es dem allgemeinen Standard entspricht. MySQL legt nämlich ungefragt für alle Felder eine Defaultwert-Einstellung an (die auch in anderen Datenbanken möglich ist, die Sie dort über DEFAULT Wert für jedes Feld aber explizit festlegen müssen). Bei Zahlfeldern wird 0 als Voreinstellung verwendet, bei Textfeldern eine leere Zeichenkette. Wenn Sie einen Datensatz anfügen, der einige Felder nicht beschreibt, verwendet ein DBMS für die unbeschriebenen Felder die definierten Defaultwerte. Diese Felder sind dann nicht leer. Weil MySQL grundsätzlich Defaultwerte vergibt, erzeugen solche Anweisungen keinen Fehler. Wieder abweichend vom üblicherweise verwendeten Standard verwendet MySQL diesen Defaultwert sogar beim Ändern von Daten, wenn für einzelne Felder der Wert Null übergeben wird. Etwas eigenartig und schwer zu verstehen ist dabei die Tatsache, dass eine leere Zeichenkette nicht Null ist: Eine leere Zeichenkette ist tatsächlich ein normaler Wert (der allerdings leer ist). Sie beginnen hier übrigens damit, die Datenbank so zu gestalten, dass diese möglichst keine ungültigen Daten speichert. Sie legen die dazu notwendigen Regeln direkt in der Datenbank fest. Verletzt ein Programm oder ein Anwender eine dieser Regeln, erzeugt die Datenbank eine Ausnahme und ändert nichts an den Daten. Damit erreichen Sie, dass alle Programme und alle Anwender, die diese Datenbank verwenden, sich an die Regeln halten müssen. Nun legen Sie noch die Bücher-Tabelle an: CREATE TABLE Buecher ( ISBN varchar(255) PRIMARY KEY NOT NULL, Titel varchar(255) NOT NULL, Autor int NOT NULL, Verliehen_an varchar(255));
Der Name von Tabellen und Feldern muss den üblichen Regeln entsprechen. Das Prinzip dabei ist: Beginnen Sie den Namen mit einem Buchstaben. Dann können Zahlen und der Unterstrich folgen. Verwenden Sie möglichst keine Umlaute, weil diese – aufgrund unterschiedlicher Zeichensätze – zu massiven Problemen führen könnten. Die BücherTabelle heißt deswegen Buecher. Die ISBN-Nummer ist in dieser Tabelle der Primärschlüssel. Ich habe für dieses Feld den Datentyp varchar gewählt, weil ISBN-Nummern häufig auch den Buchstaben X beinhalten.
464
Sandini Bib
Das Feld Autor besitzt den Datentyp int. Es ist sehr wichtig, dass dieses Feld, über das ja die Beziehung zur Autorentabelle aufgelöst wird, denselben Datentyp aufweist wie das Primärschlüsselfeld der AutorenTabelle. Für dieses Feld habe ich ebenfalls NOT NULL angegeben. Damit ist gewährleistet, dass für ein Buch auch ein Autor angegeben werden muss. Nun kommt der vielleicht etwas schwierigere (aber auch letzte) Teil. Sie können die Beziehung, zwischen den Tabellen, die zurzeit lediglich theoretisch besteht, nämlich in der Datenbank festlegen (was allerdings leider in MySQL nicht funktioniert, wie ich unten noch erläutere). Dazu ändern Sie die Struktur der Bücher-Tabelle ab und fügen einen so genannten Fremdschlüssel (Foreign Key) hinzu. Ein Fremdschlüssel referenziert den Primärschlüssel einer anderen Tabelle (also in unserem Fall der Autoren-Tabelle). In der Bücher-Tabelle muss dieser Fremdschlüssel auf dem Feld Autor liegen und das Feld Id der Autoren-Tabelle referenzieren. Ein Fremdschlüssel gehört zur Gruppe der Einschränkungen (Constraints). Eine Einschränkung schränkt die Möglichkeiten der Eingabe in einem Feld ein. Ein Fremdschlüssel bewirkt, dass im Feld Autor der Bücher-Tabelle nur Ids von Autoren eingetragen werden können, die in der Autoren-Tabelle existieren. Damit sichern Sie ab, dass die BücherTabelle keine Autor-Id speichert, die nicht existiert. Neben Fremdschlüssel-Einschränkungen existieren noch andere, wie z. B. Überprüfungs-Einschränkungen, die den Wert eines Feldes bedingungsabhängig überprüfen und nur bestimmte Werte zulassen. Auf diese Einschränkungen gehe ich aber nicht weiter ein.
Definieren der Beziehung
Zum Anlegen der Fremdschlüssel-Einschränkung für die Bücher-Tabelle verwenden Sie nun die folgende Anweisung: ALTER TABLE Buecher ADD CONSTRAINT fk_buecher_autoren FOREIGN KEY (Autor) REFERENCES Autoren (Id);
465
Sandini Bib
MySQL ist – anders als viele andere Datenbanksysteme – in der Lage, Tabellen in verschiedenen Datenbankformaten zu speichern. Beim Anlegen einer Tabelle können Sie das Format über die TYPE-Anweisung angeben. Wenn Sie nichts angeben, wird das Standardformat verwendet. Ich will hier nicht näher auf Datenbankformate eingehen. Für Sie ist lediglich wichtig, dass die unterschiedlichen Formate unterschiedliche Features unterstützen. Und dazu gehören leider auch Fremdschlüssel. Diese werden nämlich in MySQL eigenartigerweise (Fremdschlüssel sind ein wichtiges Konzept relationaler Datenbanken) nur im Datenbankformat InnoDB unterstützt. Leider wird dieses Datenbankformat, das einen speziellen Server benötigt, wiederum nicht unter allen Betriebssystemen unterstützt (z. B. nicht unter Windows 2000). Da die Konfiguration zudem recht kompliziert und fehlerträchtig ist, habe ich darauf verzichtet, dieses Format zu verwenden. Sie müssen also damit leben, dass Ihre Datenbank zurzeit noch in der Bücher-Tabelle das Eintragen von Autoren erlaubt, die gar nicht existieren. Ich wollte das wichtige Anlegen eines Fremdschlüsssels – das in allen anderen mir bekannten DBS funktioniert – aber wenigstens zeigen. Überprüfen der Datenbank Zum Überprüfen Ihrer Arbeit können Sie den Befehl show columns from Tabellenname; aufrufen. Der MySQL-Monitor zeigt die Struktur der angegebenen Tabelle in einer übersichtlichen Form an (Abbildung 9.5).
Abbildung 9.5: Anzeige der Struktur der Tabellen der Buchdatenbank unter Windows
466
Sandini Bib
Datenbank löschen Wenn Sie eine Datenbank löschen wollen, weil Sie z. B. beim Anlegen Fehler gemacht haben, verwenden Sie einfach die DROP DATABASE-Anweisung: DROP DATABASE Buchdaten;
Beenden Mit dem Befehl quit können Sie den MySQL-Monitor beenden.
9.3
Daten mit SQL bearbeiten
Damit Sie Ihre Datenbank gleich ein wenig mit Leben füllen, zeige ich nun, wie Sie mit SQL Daten anfügen, abfragen, ändern und löschen. Starten Sie den MySQL-Monitor und wechseln Sie zur Buchdatenbank: USE Buchdaten;
9.3.1 Daten anfügen Über die SQL-Anweisung INSERT INTO können Sie nun Datensätze anfügen. Dabei verwenden Sie das folgende Schema: INSERT INTO Tabellenname (Feldliste) VALUES (Wertliste);
In der Feldliste geben Sie alle Felder der Tabelle an, in die Sie Daten einfügen wollen. Sie müssen nicht in alle Felder Daten einfügen, weswegen die Angabe einer Feldliste sinnvoll ist. Einzelne Feldnamen trennen Sie durch Kommata. In der Wertliste geben Sie dann die Werte der einzelnen Felder an. Dabei verwenden Sie Literale, die Sie von der Programmierung her gewohnt sind: Zahlen werden in der englischen Schreibweise angegeben, Zeichenketten können Sie in einfache Apostrophe oder in Anführungszeichen einschließen. Die Reihenfolge und die Anzahl der Feldwerte muss natürlich der Reihenfolge und Anzahl der in der Feldliste angegebenen Felder entsprechen. So können Sie nun an die Autoren-Tabelle drei Datensätze anfügen: INSERT INTO Autoren (Id, Vorname, Nachname) VALUES (1, "Douglas", "Adams"); INSERT INTO Autoren (Id, Vorname, Nachname) VALUES (2, "John", "Irving"); INSERT INTO Autoren (Id, Vorname, Nachname) VALUES (3, "Matt", "Ruff");
467
Sandini Bib
Und natürlich auch Bücher an die Bücher-Tabelle: INSERT INTO Buecher (ISBN, Titel, Autor) VALUES ("3453209613", "Per Anhalter durch die Galaxis", 1); INSERT INTO Buecher (ISBN, Titel, Autor) VALUES ("3453210727", "Der lange dunkle Fünfuhrtee der Seele", 1); ...
Auf der Buch-CD finden Sie im Ordner \Beispiele\Kapitel 09\B Daten anfügen eine Textdatei, die die entsprechenden Befehle beinhaltet. Beim Anlegen der Bücher verzichte ich auf das Angeben des Wertes für das Feld Verliehen_an, weil dieses Feld zurzeit noch nicht belegt werden soll. Fehler bei der Verletzung des Primärschlüssels
Wenn Sie nun versuchen, einen Autor oder ein Buch mit einem existierenden Primärschlüssel anzulegen, resultiert dies in einem Fehler (Abbildung 9.6).
Abbildung 9.6: MySQL meldet unter Linux einen Fehler beim Anlegen eines vierten Autors mit einer Id, die bereits existiert.
Das DBMS verhindert das Anfügen, weil dadurch die Eindeutigkeit des Primärschlüssels verloren gehen würde. Und das ist auch gut so. In einem Programm würde ein solcher Fehler eine Ausnahme erzeugen, die Sie natürlich abfangen können. Nun, da Sie bereits Daten anfügen können, sollten Sie diese auch abfragen können.
468
Sandini Bib
9.3.2 Daten abfragen Das Abfragen von Daten ist prinzipiell einfach. Die SELECT-Anweisung, die Sie dazu benutzen, ist aber sehr komplex. Dafür können Sie damit schon bei der Abfrage sehr viele Probleme lösen. Ich zeige hier nur eine Möglichkeit, Daten gezielt abzufragen und dabei zu sortieren. Die einzelnen Bestandteile der SELECT-Anweisung sind, bis auf SELECT und FROM, optional. Das (vereinfachte) Schema dieser Anweisung ist das folgende: SELECT Feldliste FROM Tabelle [WHERE Bedingung] [ORDER BY Feldliste];
Zumindest müssen Sie eine Liste abzufragender Felder und den Tabellennamen angeben. Die folgende Anweisung liefert die Felder Vorname und Nachname für alle Datensätze der Autoren-Tabelle: SELECT Vorname, Nachname FROM Autoren;
Wenn Sie alle Felder abfragen wollen, können Sie auch in der Feldliste einen Stern angeben: SELECT * FROM Autoren;
Die Angabe nur einzelner Felder ist effizienter, da das Ergebnis der Abfrage dann kleiner ist. Über ORDER BY können Sie nach beliebigen Feldern sortieren: SELECT ISBN, Titel FROM Buecher ORDER BY Titel;
Das ist bereits ein sehr nettes Feature. Sie haben damit absolut keine Probleme, Daten aus einer Datenbank sortiert abzufragen und auszugeben. Schließlich (aber für die SELECT-Anweisung lange nicht endlich) können Sie Daten noch bedingungsabhängig abfragen. Die Bedingung verwendet logische Ausdrücke, wie Sie diese bereits von der Programmierung her kennen. Die Vergleichsoperatoren sind dieselben wie bei Object Pascal. In der Bedingung beziehen Sie sich auf die einzelnen Felder. So können Sie z. B. gezielt die Bücher des Autors mit der Id 1 abfragen: SELECT ISBN, Titel FROM Buecher WHERE Autor = 1 ORDER BY Titel;
Das Ergebnis kann sich bereits sehen lassen (Abbildung 9.7).
469
Sandini Bib
Abbildung 9.7: Das Ergebnis einer SQL-Abfrage im SQL-Monitor unter Windows Komplexere Anweisungen lösen die Beziehung auf
Über eine komplexere Anweisung können Sie auch die Beziehung zwischen Tabellen auflösen. So können Sie z. B. die ISBN-Nummer aller Bücher gemeinsam mit dem Vor- und Nachnamen des Autors abfragen. Um im Ergebnis klarere Feldnamen zu erhalten, können Sie diese bei der Abfrage mit einem neuen Namen versehen (Abbildung 9.8).
Abbildung 9.8: Abfrage der in Beziehung stehenden Daten zweiter Tabellen
Auf diese Art der Abfrage gehe ich aber nicht weiter ein. Ich wollte diese Möglichkeit nur zeigen, damit Sie wissen, was eine solche Abfrage ist.
9.3.3 Daten ändern Das Ändern von Daten über SQL ist relativ einfach, wenn Sie sich mit der SELECT-Anweisung auskennen. Die zum Ändern verwendete UPDATEAnweisung arbeitet nämlich mit einer identischen WHERE-Klausel. Das Schema dieser Anweisung sieht so aus: UPDATE Tabelle SET Feld1 = Wert [, Feld2 = Wert] [Feld2 = Wert] [...] [WHERE Bedingung]
470
Sandini Bib
Die Angabe der Bedingung ist zwar optional, wenn Sie diese aber nicht angeben, ändern Sie alle Datensätze. Und das ist nur in seltenen Fällen sinnvoll. In der Liste der zu ändernden Felder können Sie ein oder beliebig viele Felder mit neuen Werten versehen. Die Werte geben Sie wieder wie gewohnt an. Zahlen in der englischen Schreibweise, Zeichenketten in Anführungszeichen. So können Sie z. B., wenn Sie alle Bücher von Douglas Adams verliehen haben, das Feld Verliehen_an dieser Bücher aktualisieren: UPDATE Buecher SET Verliehen_an = "Zaphod" WHERE Autor = 1;
Natürlich können Sie auch einzelne Datensätze aktualisieren. Dann beziehen Sie sich auf den Primärschlüssel: UPDATE Buecher SET Verliehen_an = "Trillian" WHERE ISBN = "342312721X";
Sie können über diese Anweisung auch sehr komplexe Aktualisierungen vornehmen, aber ich denke, für den Anfang reicht das, was ich gezeigt habe. Beim eventuellen Ändern des Primärschlüsselwerts einer Tabelle, die in einer anderen Tabelle referenziert wird (also z. B. der Id eines Autors), müssen Sie vorsichtig sein. Wenn diese Id in der anderen Tabelle gespeichert ist, bringen Sie Ihre Datenbank damit durcheinander. Das gilt allerdings nur für MySQL, denn andere Datenbanksysteme unterstützen Fremdschlüssel. Ein Fremdschlüssel verhindert, dass ein in einer anderen Tabelle referenzierter Primärschlüsselwert geändert wird. Der Fremdschlüssel muss dazu natürlich beim Erzeugen der Datenbank (oder später) angelegt worden sein.
9.3.4 Daten löschen Das Löschen von Daten ist in SQL ebenfalls sehr einfach. Dazu verwenden Sie die DELETE-Anweisung, die auch wieder mit einer WHERE-Klausel arbeitet: DELETE FROM Tabelle [WHERE Bedingung];
Die Bedingung ist wieder optional. Wenn Sie diese nicht angeben, löschen Sie alle Datensätze. Gehen Sie mit dieser Anweisung also vorsichtig um. Die Daten werden unwiderruflich gelöscht, Sie können das Löschen nicht rückgängig machen. So könnten Sie z. B. alle Bücher des Autors mit der Id 1 löschen: DELETE FROM Buecher WHERE Autor = 1;
471
Sandini Bib
Beim Löschen von Datensätzen einer Tabelle, die in einer anderen Tabelle referenziert wird, müssen Sie wie beim Aktualisieren vorsichtig sein. Sie können Ihre Datenbank damit in einen inkonsistenten Zustand bringen. So kann es z. B. sein, dass Sie den Autor mit der Id 1 löschen, dieser aber noch in der Bücher-Tabelle referenziert wird. Das gilt einmal wieder nur für MySQL. DBMS, die Fremdschlüssel unterstützen, verhindern, dass solche Datensätze gelöscht werden.
9.4
Daten in Java-Programmen bearbeiten
In Java-Programme können Sie Daten sehr einfach bearbeiten, wozu Sie natürlich SQL einsetzen. Java bietet aber auch eine vereinfachte Möglichkeit, bei der Sie die Features eines Objekts nutzen, das die SQLAnweisungen automatisch erzeugt. Ich zeige im Folgenden nun, wie Sie die Java-Objekte zur Arbeit mit Datenbanken nutzen. Um nicht wieder eine komplexe Beispielanwendung mit grafischer Oberfläche zu erzeugen, die zu einem im Buch ziemlich unübersichtlichen Programm führen würde, zeige ich die Grundlagen lediglich in einer Konsolenanwendung. Ich denke, mit dem Wissen, das Sie bis hier erworben haben, sind Sie in der Lage, eine eigene Anwendung mit einer grafischen Oberfläche zu erzeugen. Eine Abschlussübung des Kapitels wird dann auch sein, ein Programm zur Bearbeitung der Buchdatenbank zu entwickeln . Dieses Programm finden Sie natürlich auch in den Lösungen auf der CD.
-
9.4.1 Der JDBC-Treiber Die Datenbank-Features von Java werden als JDBC (Java Database Connectivity) bezeichnet. JDBC ist eine Bibliothek mit mehreren Klassen, die Sie bei der Programmierung nutzen. Um mit einem Datenbanksystem arbeiten zu können, benötigen Sie zusätzlich zu JDBC einen Treiber. Ein Treiber übernimmt die direkte Kommunikation mit dem DBMS, sorgt also dafür, dass Sie SQL-Anweisungen zum DBMS senden können und das Ergebnis zurückerhalten. In Java selbst ist kein direkter JDBC-Treiber integriert. Java besitzt lediglich einen JDBC-Treiber, der eine Brücke nach ODBC (Open Database Connectivity) darstellt. ODBC ist ein mittlerweile veralteter offener Standard, der Programmen den Zugriff auf Datenbanksysteme über einfache Funktionen erlaubt (nicht über Objekte). Laut Sun soll der JDBCTreiber für ODBC nur zu Experimentierzwecken und dann verwendet werden, wenn kein direkter JDBC-Treiber zur Verfügung steht.
472
Sandini Bib
Die meisten JDBC-Treiber werden von externen Herstellern zur Verfügung gestellt und müssen teilweise gekauft werden. Auf der Seite INDUSTRY.JAVA.SUN.COM/PRODUCTS/JDBC/DRIVERS finden Sie eine Übersicht über die aktuell zur Verfügung stehenden Treiber für verschiedene Datenbanken (zurzeit ca. 160). Für MySQL-Datenbanken können Sie den Treiber von MySQL-AB verwenden, den Sie bereits installiert haben, wenn Sie den Artikel „Installation“ auf der Buch-CD nachvollzogen haben. Ein JDBC-Treiber ist meist nur eine einzige Datei, die lediglich in den Ordner lib\ext der Java-Laufzeitumgebung kopiert werden muss.
9.4.2 Treiber laden und Verbindung aufbauen Um mit einer Datenbank arbeiten zu können, müssen Sie in Java-Programmen zunächst den JDBC-Treiber laden (in anderen Programmiersprachen ist dies nicht notwendig). Dazu verwenden Sie die statische Methode forName der Java-Klasse Class. Dieser Methode übergeben Sie den vollen Namen der Treiber-Klasse (im Fall des MySQL AB-Treibers ist das der Name com.mysql.jdbc.Driver) :
Laden des MySQLTreibers
Class.forName("com.mysql.jdbc.Driver");
Was Sie hier machen, erscheint vielleicht ein wenig eigenartig. Sie laden über diese Anweisung die Klasse Driver in den Speicher. Sie erzeugen noch keine Instanz dieser Klasse. Das Erzeugen einer Instanz übernimmt die DriverManager-Klasse, die Sie im weiteren Verlauf der Programmierung verwenden, um eine Verbindung zur Datenbank aufzubauen. Um Instanzen zu erzeugen, muss die entsprechende Klasse im Speicher verwaltet werden. Wenn Sie selbst im Programm Instanzen erzeugen, erkennt der Compiler an der Deklaration der Variablen, welche Klassen geladen werden müssen, und sorgt dafür, dass diese auch in den Speicher geladen werden. Bei einem JDBC-Treiber werden die Instanzen der Driver-Klasse aber vom Treiber-Manager (der DriverManager-Klasse) erzeugt. Der Compiler kann nicht wissen, welche Klassen geladen werden sollen. Deswegen müssen Sie die Treiberklasse explizit laden. Obwohl der Treiber-Manager die Instanz der Treiber-Klasse implizit erzeugt und intern für den Datenbankzugriff verwendet, sollten Sie beim Laden der Klasse auch eine Instanz erzeugen. Beim Instanzieren kann es nämlich vorkommen, dass Fehler auftreten. Diese Fehler können Sie gezielt abfangen, wenn Sie selbst testweise eine Instanz erzeugen. Dazu verwenden Sie die Methode newInstance des Class-Objekts, das Class.forName zurückgibt (ein Class-Objekt wird u. a. zur Instanzierung dynamisch geladener Klassen verwendet). forName verlangt, dass Sie die Ausnahme ClassNotFoundException abfangen, newInstance verlangt das Abfangen der
473
Sandini Bib
Ausnahmen InstantiationException (Fehler bei der Instanzierung) und IllegalAccessException (Fehler beim Zugriff). Sie müssen diese Ausnahmen also abfangen. Um einigermaßen aussagekräftige Fehlermeldungen auszugeben, aber nicht zu viel programmieren zu müssen, fange ich die ClassNotFoundException-Ausnahme separat und alle anderen Ausnahmen in einer allgemeinen Ausnahmebehandlung ab: 01 import java.sql.*; 02 03 public class DatenBearbeiten 04 { 05 public static void main(String args[]) 06 { 07 /* Treiber laden */ 08 System.out.println("Lade Treiber ..."); 09 try 10 { 11 Class.forName("com.mysql.jdbc.Driver").newInstance(); 12 } 13 catch (ClassNotFoundException e) 14 { 15 System.out.println("Die Treiber-Klasse wurde nicht " + 16 "gefunden. Installieren Sie den MySql-Treiber."); 17 System.exit(1); 18 } 19 catch (Exception e) 20 { 21 System.out.println("Der JDBC-Treiber kann nicht " + 22 "geladen werden: " + e.getMessage()); 23 System.exit(1); 24 } 25
Das Programm importiert in Zeile 1 zunächst alle Klassen des Pakets java.sql, das die Klassen beinhaltet, die Sie zur Arbeit mit Datenbanken einsetzen. In Zeile 11 wird der Treiber geladen. Beim Eintritt einer Ausnahme (Zeilen 15-17 und 21-23) wird das Programm einfach beendet, was wieder über System.exit(1) geschieht. Das funktioniert auch in einer Anwendung mit grafischer Oberfläche und gibt dem System zurück, dass ein Fehler aufgetreten ist (dieser Rückgabewert kann von einem Batch-Programm oder Shell-Script aus einer Systemvariable ausgelesen werden). Aufbau einer Verbindung
474
Wenn der Treiber erfolgreich geladen ist, müssen Sie eine Verbindung zur Datenbank aufbauen. Diese Verbindung verwenden Sie, um Daten abzufragen, anzufügen, zu ändern, zu löschen, um neue Tabellen anzulegen
Sandini Bib
und was sonst noch alles mit SQL möglich ist. Für die Verbindung benötigen Sie eine Variable vom Typ java.sql.Connection. Über die getConnectionMethode des Treiber-Managers erzeugen Sie eine Instanz dieser Klasse. Dabei übergeben Sie einen URL (Uniform Ressource Locator), wie er im Internet verwendet wird (um z. B. die Adresse einer Website anzugeben). Der URL besitzt für eine MySQL-Datenbank das folgende Schema: jdbc:mysql://Rechneradresse/Datenbankname
Die Rechneradresse wird in der im Internet üblichen Form angegeben. Ich will hier nicht näher darauf eingehen, Sie können MySQL-Datenbanken ohne Probleme auch auf anderen Rechnern oder eben sogar über das Internet ansprechen. Läuft die Datenbank im lokalen Netz, geben Sie einfach den Rechnernamen an. Für eine Datenbank, die auf dem Rechner läuft, auf dem das Programm ausgeführt wird, können Sie localhost angeben. Neben dem URL übergeben Sie der getConnection-Methode dann noch einen Benutzernamen und das Passwort des Benutzers. Das Ganze muss natürlich wieder in eine Ausnahmebehandlung eingefügt werden: 26 27 28 29 30 31 32 33 34 35 36 37 38
System.out.println("Baue Verbindung auf ..."); Connection connection = null; try { connection = DriverManager.getConnection( "jdbc:mysql://localhost/Buchdaten", "root", ""); } catch (Exception e) { System.out.println("Fehler beim Öffnen der Verbindung: " + e.getMessage()); System.exit(1); }
Das Beispiel erzeugt die Verbindung in Zeile 30, wobei als Benutzer „root“ ohne Passwort angegeben wird. Das Passwort dieses Benutzers ist nämlich nach einer Neuinstallation noch leer (und sollte natürlich aus Sicherheitsgründen gegebenenfalls geändert werden). Mit dieser Verbindung können Sie dann arbeiten.
9.4.3 Daten abfragen und bearbeiten Das Abfragen von Daten ist nun sehr einfach. Sie benötigen dazu ein Objekt der Klasse Statement. Die executeQuery-Methode dieses Objekts führt eine SELECT-Anweisung aus und gibt ein ResultSet-Objekt zurück, über das Sie die Daten durchgehen und sogar auch bearbeiten können.
475
Sandini Bib
Erzeugen eines Statement-Objekts Sie sollten zuerst das Statement-Objekt erzeugen. Dieses Objekt können Sie für alle SQL-Abfragen verwenden, die Sie auf der geöffneten Datenbank ausführen wollen. Zur Erzeugung verwenden Sie die Methode createStatement des Connection-Objekts: 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
System.out.println("Erzeuge Statement-Objekt ..."); Statement statement = null; try { statement = connection.createStatement(); } catch (Exception e1) { System.out.println("Fehler beim Erzeugen des " + "Statement-Objekts: " + e1.getMessage()); /* Verbindung schließen */ try { connection.close(); } catch (Exception e2) {} System.exit(1); }
Das Erzeugen muss wieder in einer Ausnahmebehandlung erfolgen. Eine Ausnahme beim Erzeugen wird wieder als kritischer Fehler gewertet, weshalb das Programm in Zeile 52 beendet wird. Zuvor muss aber die Verbindung geschlossen werden. Verbindungen sollten Sie immer schließen, bevor Sie ein Programm beenden. In Zeile 51 ruft das Programm deshalb die close-Methode des Connection-Objekts auf. Da diese Methode verlangt, dass die Ausnahme SQLException abgefangen wird, ist das Schließen in eine innere Ausnahmebehandlung eingeschlossen. Diese reagiert aber nicht auf einen eventuellen Fehler, der catch-Block enthält keine Anweisungen. Beim Beenden des Programms sind eventuelle Fehlermeldungen recht sinnlos. Dass das Programm für die Ausnahmebehandlungen zwei Variablen verwendet (e1 und e2), liegt übrigens daran, dass die Variable in der inneren Ausnahmebehandlung anders benannt sein muss als in der äußeren. Abfragen von Daten Nun können Sie Daten abfragen. Dazu verwenden Sie die executeQueryMethode des Statement-Objekts. Dieser Methode übergeben Sie eine SQL-SELECT-Anweisung. executeQuery gibt eine Instanz der Klasse3 Result-
3.
476
In Wirklichkeit ist ResultSet keine Klasse, sondern eine Schnittstelle (englisch „Interface“). Auf dieses schwierige Thema gehe ich im Buch aber nicht ein. Informationen dazu finden Sie im Artikel „OOP-Grundlagen“ auf der Buch-CD.
Sandini Bib
Set zurück. Über dieses Objekt können Sie die abgefragten Daten sehr
flexibel bearbeiten. Ein ResultSet-Objekt ist sehr mächtig und besitzt sehr viele Methoden. Ich kann hier nur die wichtigsten vorstellen. Lesen Sie in der Java-APIDokumentation nach, wenn Sie mehr über diese Klasse erfahren wollen. executeQuery erzeugt im Fehlerfall eine Ausnahme vom Typ SQLException,
die abgefangen werden muss: 55 56 57 58 59 60 61 62 63 64 65 66 67
System.out.println("Frage Daten ab ..."); ResultSet resultSet = null; try { resultSet = statement.executeQuery( "SELECT * FROM Buecher ORDER BY ISBN"); } catch (SQLException e) { System.out.println("Fehler bei der Abfrage der Daten: " + e.getMessage()); }
Daten durchgehen Als SQL-Anweisung können Sie alle SELECT-Anweisungen übergeben, die für die Datenbank möglich sind. Damit können Sie Daten sehr flexibel abfragen. Das ResultSet-Objekt wird mit den abgefragten Datensätzen gefüllt und verwaltet diese intern im Arbeitsspeicher. Sie können nun z. B. sequenziell durchgehen. Das ResultSet-Objekt verwaltet einen internen Datensatzzeiger. Solch ein Zeiger wird im Allgemeinen als Cursor bezeichnet. Nach dem Öffnen einer Abfrage steht dieser vor dem ersten Datensatz (was eigentlich bei solchen Techniken, die in ähnlicher Form auch in anderen Sprachen verwendet werden, unüblich ist, dort steht der Cursor nach dem Öffnen dann auf dem ersten Datensatz). Über die next-Methode bewegen Sie den Cursor um einen Datensatz weiter. Wenn Sie next direkt nach dem Öffnen aufrufen, steht der Cursor also auf dem ersten Datensatz. Wenn Sie die Daten sequenziell durchgehen wollen, müssen Sie überprüfen, ob der Cursor nach dem Aufruf von next nicht hinter dem letzten Datensatz steht. Das ist nämlich nach dem Aufruf von next der Fall, wenn der Cursor zuvor auf dem letzten Datensatz stand. Diese Überprüfung nehmen Sie über die Methode isAfterLast vor. Zudem müssen Sie überprüfen, ob die Abfrage eventuell keine Daten er-
477
Sandini Bib
gab. Das können Sie einfach erledigen, indem Sie die Rückgabe von next überprüfen. next gibt nämlich nur dann true zurück, wenn der Cursor auf einem Datensatz steht. Prinzipiell sieht das Durchgehen dann so aus: if (resultSet.next() == false) { System.out.println("Die Abfrage ergab keine Daten."); } else { do { /* Daten verarbeiten */ ... /* Cursor auf den nächsten Datensatz setzen */ resultSet.next(); } while (resultSet.isAfterLast() == false); } Zugriff auf die Felder über getMethoden
Innerhalb der Schleife können Sie nun auf den aktuellen Datensatz zugreifen. Dazu stehen Ihnen eine Menge verschiedener Methoden zur Verfügung, deren Name immer mit „get“ beginnt. Diese einzelnen Methoden geben den Wert eines Datenfeldes als speziellen Datentyp zurück. getString liefert z. B. einen String, getInt einen int-Wert und getDouble einen double-Wert. Voraussetzung dafür ist natürlich, dass das Datenfeld einen Datentyp besitzt, der identisch ist oder entsprechend konvertiert werden kann. Allen diesen Methoden können Sie den Index des Feldes (bezogen auf die Angabe der Felder in der Abfrage) oder den Namen des Feldes übergeben. Die Übergabe des Index ist in der Praxis nicht zu empfehlen, weil Sie häufig nicht wissen, welchen Index ein Feld besitzt (wenn Sie mit SELECT * ... abfragen) oder der Index aufgrund einer Änderung der Abfrage (was in der Praxis häufiger passiert) sich verschiebt. Fragen Sie die Daten also über den Namen des Feldes ab. Das Ganze muss einmal wieder in eine Ausnahmebehandlung eingefügt werden, die auf die Ausnahme SQLException reagiert. Diese Ausnahme wird z. B. erzeugt, wenn Sie beim Lesen einen nicht existierenden Feldnamen übergeben oder wenn ein Wert nicht konvertiert werden kann: 68 69 70 71
478
try { System.out.println("Gehe Daten durch..."); if (resultSet != null)
Sandini Bib
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
{ /* Auf den ersten Datensatz stellen und gleichzeitig überprüfen, ob die Abfrage eventuell keine Daten ergab */ if (resultSet.next() == false) { System.out.println("Die Abfrage ergab keine Daten."); } else { /* ResultSet in einer Schleife durchgehen */ do { /* Datenfelder auslesen */ System.out.println(resultSet.getString("ISBN")); System.out.println(resultSet.getString("Titel")); System.out.println(resultSet.getInt("Autor")); System.out.println(); /* Cursor auf den nächsten Datensatz stellen */ resultSet.next(); } while (resultSet.isAfterLast() == false); } } } catch (SQLException e) { System.out.println("Fehler beim Durchgehen der Daten: " + e.getMessage()); }
Über die Methoden first, last und previous können Sie den Cursor übrigens auch auf den ersten, den letzten bzw. den vorherigen Datensatz setzen, was bei einer Anwendung mit einem Formular zum Editieren der Daten z. B. notwendig ist. Der Zugriff auf die Daten ist in der Praxis noch etwas komplexer, als ich es hier dargestellt habe. Sie müssen z. B. gegebenenfalls auf leere Felder reagieren. Die get-Methoden geben für leere Felder einen zum Datentyp passenden leeren Wert zurück (bei Zahlen ist das die Null). Nach dem Lesen können Sie über die Methode wasNull überprüfen, ob ein Feld leer war. Außerdem sollten Sie die Ausnahmebehandlung eventuell etwas verfeinern, damit Sie mehr Informationen erhalten, wenn beim Zugriff ein Fehler auftritt. Aber das überlasse ich Ihnen.
479
Sandini Bib
Daten bearbeiten Ein ResultSet-Objekt erlaubt nicht nur das einfache Auslesen der Daten. Sie können diese auch direkt bearbeiten. Zum Ändern eines Datensatzes schreiben Sie einfach Daten über verschiedene Methoden, die mit „update“ beginnen und die ähnlich den get-Methoden verschiedene Datentypen erlauben, in einzelne Felder. Diese Methoden schreiben die übergebenen Daten immer in den aktuellen Datensatz. Über die Methode moveToInsertRow können Sie aber auch einen neuen Datensatz erzeugen. Ich zeige hier nur, wie das prinzipiell funktioniert, ohne die notwendige Ausnahmebehandlung: /* Neuen Datensatz erzeugen */ resultSet.moveToInsertRow(); /* Datensatz beschreiben */ resultSet.updateString("ISBN", "3453099826"); resultSet.updateString("Titel", "Der tiefere Sinn des Labenz"); resultSet.updateInt("Autor", 1); /* Datenbank aktualisieren */ resultSet.insertRow();
Die Methode insertRow generiert aus den hinzugefügten Daten eine passende INSERT INTO-Anweisung und sendet diese an das DBMS. Falls dieses den Datensatz nicht anfügen kann, erzeugt insertRow eine Ausnahme. Auf eine ähnliche Weise können Sie den aktuellen Datensatz aktualisieren: /* Datensatz beschreiben */ resultSet.updateString("Verliehen_an", "Zaphod"); /* Datenbank aktualisieren */ resultSet.updateRow();
Der zu aktualisierende Datensatz muss dazu der aktuelle sein. Sie müssen den Cursor also zuvor auf den zu aktualisierenden Datensatz bewegen. updateRow erzeugt aus den Änderungen eine passende UPDATE-Anweisung. Dabei können natürlich wieder Fehler auftreten, die zu einer Ausnahme führen. Diese Art der Aktualisierung verwenden Sie idealerweise nur in Programme, die dem Benutzer Datensätze zur Bearbeitung in Formularen zur Verfügung stellen (ähnlich der Beispielanwendung aus Kapitel 8). Ansonsten sollten Sie Daten wenn möglich immer direkt über SQL aktualisieren, was aber auch für das Anfügen und Löschen gilt. Zum einen können Sie so gleich mehrere Datensätze in einem Rutsch verändern
480
Sandini Bib
oder löschen. Zum anderen ist die direkte Ausführung von SQL-Anweisungen immer wesentlich schneller, als wenn Sie dazu ein ResultSet-Objekt verwenden. Dabei sollten Sie aber auch bedenken, dass ein solchen Objekt Ihnen das manchmal nicht einfache Zusammensetzen einer SQL-Anweisung abnimmt. Löschen von Daten Als letztes Feature des ResultSet-Objekts zeige ich noch das Löschen. Dazu verwenden Sie die Methode deleteRow. Diese Methode löscht den aktuellen Datensatz (direkt auch in der Datenbank) und setzt den Cursor einen Datensatz weiter. Deshalb müssen Sie nach dem Löschen überprüfen, ob der Cursor nicht hinter dem letzten Datensatz steht (wenn der letzte gelöscht wurde). Dann können Sie den Cursor einfach auf den vorherigen Datensatz stellen. Hinzu kommt, dass Sie dann noch überprüfen müssen, ob das ResultSet-Objekt überhaupt noch Daten enthält. Das macht das Löschen nicht gerade einfach: /* Datensatz löschen */ resultSet.deleteRow(); /* Überprüfen, ob der Cursor nun hinter dem letzten Datensatz steht */ if (resultSet.isAfterLast()) { /* ResultSet zum vorherigen Datensatz bewegen und gleichzeitig überprüfen, ob das ResultSet nun leer ist */ if (resultSet.previous() == false) { System.out.println("Der letzte Datensatz wurde gelöscht"); } }
9.4.4 Daten über SQL direkt anfügen, ändern und löschen Zum Anfügen, Ändern und Löschen von Daten können Sie dem DBMS auch direkt SQL-Anweisungen übergeben. In vielen Fällen ist der direkte Aufruf effizienter und flexibler, als wenn Sie dazu ein ResultSetObjekt verwenden. Wenn Sie beispielsweise lediglich alle Bücher von Douglas Adams als verliehen kennzeichnen wollten, wäre es unsinnig, dafür ein ResultSet-Objekt zu erzeugen, die einzelnen Datensätze durchzugehen und der Reihe nach zu aktualisieren. Das können Sie wesentlich einfacher über eine UPDATE-Anweisung erledigen. Um eine SQLAnweisung an die Datenbank zu übergeben, verwenden Sie die executeUpdate-Methode. Das einzig Schwierige daran ist, die SQL-Anwei-
481
Sandini Bib
sung korrekt zusammenzusetzen. Das gilt besonders dann, wenn die Daten für die Anweisung vom Anwender eingegeben werden. Als Beispiel soll hier das Anfügen eines neuen Buchs dienen: String sql; /* Den Anwender die Daten eingeben lassen */ String isbn = null, titel = null, autor = null; try { java.io.DataInputStream in = new java.io.DataInputStream(System.in); System.out.print("ISBN: "); isbn = in.readLine(); System.out.print("Titel: "); titel = in.readLine(); System.out.print("Autor: "); autor = in.readLine(); sql = "INSERT INTO Buecher (ISBN, Titel, Autor) " + "VALUES ('" + isbn + "', " + "'" + titel + "', " + autor + ")"; statement.executeUpdate(sql); } catch (Exception e1) { System.out.println("Fehler beim Anfügen: " + e1.getMessage()); }
Das Schwierige daran ist, die Eingaben so in die SQL-Anweisung einzubauen, dass diese syntaktisch korrekt ist. Dabei müssen Sie besonders darauf achten, dass Strings innerhalb der Anweisung immer in Anführungszeichen oder Apostrophen eingefügt werden müssen. Ich verwende in Java lieber Apostrophe, weil Anführungszeichen nicht einfach zu handhaben sind (diese müssten in einem String über \" dargestellt werden, was das Ganze noch unübersichtlicher macht, als es bereits ist). Bei Dezimalzahlen müssen Sie darauf achten, dass Sie diese so formatieren, dass die englische Schreibweise verwendet wird. Datumswerte habe ich erst gar nicht berücksichtigt, weil diese immer in einem ganz speziellen Format angegeben werden müssen.
482
Sandini Bib
Was Sie ebenfalls beachten sollten, ist, dass Änderungen, die Sie über SQL direkt vornehmen, in einem gleichzeitig geöffneten ResultSetObjekt nicht sichtbar sind. Das ResultSet-Objekt hat seine Daten ja nur einmal abgefragt und arbeitet mit diesen Daten weiter. Lediglich Änderungen, die über das ResultSet-Objekt ausgeführt werden, sind auch in diesem Objekt sichtbar. In der Praxis ist das sogar ein echtes Problem: Wenn mehrere Anwender gleichzeitig auf eine Datenbank zugreifen (was immer möglich ist), kann es sein, dass ein anderer Anwender einen Datensatz, der in einem ResultSet-Objekt verwaltet wird, zwischenzeitlich ändert oder sogar löscht. Das Handling solcher Probleme ist nicht gerade trivial. Darauf gehe ich aufgrund der Komplexität aber nicht weiter ein.
9.4.5 Verbindung schließen Nach der Arbeit mit der Verbindung sollten Sie diese immer wieder schließen, damit alle (vom DBMS) eventuell belegten Ressourcen wieder freigegeben werden. Verwenden Sie dazu die close-Methode des Connection-Objekts. Das Schließen muss in einer Ausnahmebehandlung erfolgen. Mit dem Schließen der Verbindung beende ich auch das Beispielprogramm: 103 104 105 106 107 108 109 110 111 112 113 } 114 }
System.out.println("Schließe die Verbindung ..."); try { connection.close(); } catch (Exception e) { System.out.println("Fehler beim Schließen der " + "Verbindung: " + e.getMessage()); }
Und schließlich auch das Buch: buch.ende();
9.5
Zusammenfassung
In diesem Kapitel haben Sie zunächst gelernt, was eine Datenbank prinzipiell ist und wie ein Datenbanksystem aufgebaut ist. Sie kennen die grundsätzlichen Aufgaben eines Datenbankmanagementsystems und die wichtigsten Prinzipien von relationalen Datenbanken. Den Begriff
=XVDPPHQIDVVXQJ
483
Sandini Bib
„Tabelle“ bringen Sie in Verbindung mit einer Datenbank und Sie wissen auch, was Beziehungen zwischen Tabellen sind. Objektorientierte Datenbanken sind Ihnen noch fremd, Sie ahnen aber wahrscheinlich schon, dass die Arbeit mit solchen Datenbanken wesentlich einfacher ist als mit relationalen. Um Daten in relationalen Datenbanken erzeugen und bearbeiten zu können, sind Sie in der Lage, mit SQL Datenbanken zu erzeugen und Daten anzufügen, abzufragen, zu aktualisieren und zu löschen. Sie kennen zwar noch nicht alle Möglichkeiten von SQL, aber Sie wissen, worum es sich dabei handelt. Schließlich sind Sie auch in der Lage, eine Datenbank in einem Java-Programm zu bearbeiten, wobei Sie auch einige der erweiterten Features eines ResultSetObjekts kennen.
9.6
Fragen und Übungen
1. Was unterscheidet eine objektorientierte von einer relationalen
Datenbank? 2. Wozu setzen Sie SQL ein? 3. Welche Aufgabe übernimmt ein JDBC-Treiber? 4. Welcher Unterschied besteht zwischen der Arbeit mit einem Result-
Set-Objekt und dem direkten Ausführen von SQL-Anweisungen in
Java? 5. Programmieren Sie eine Anwendung, die dem Anwender erlaubt,
über ein Formular ein neues Buch anzufügen. Mehr soll diese Anwendung nicht bieten. 6. Programmieren Sie eine Anwendung, die es dem Anwender erlaubt,
die gespeicherten Bücher durchzugehen (ähnlich der Beispielanwendung aus Kapitel 8), zu ändern, zu löschen und neue Bücher anzufügen. 7. Erweitern Sie die Anwendung aus Aufgabe 6 so, dass zusätzlich das
Suchen nach Büchern möglich ist, die einen bestimmten Titel tragen. Der Anwender soll dabei nur einen Teil des Buchtitels eingeben können. Lesen Sie in der Dokumentation von MySQL dazu die Syntax des LIKE-Operators nach. 8. Erweitern Sie die Anwendung aus Aufgabe 7 so, dass zusätzlich das
Anfügen von neuen Autoren möglich ist. Viel Spaß dabei.
484
Sandini Bib
N
Nachwort
le
n e rn
Es hat sehr viel Spaß gemacht, dieses Buch zu schreiben. Viele Dinge waren auch für mich neu, besonders unter Java (meine Sprachen sind eher C# und Visual Basic). Ich hoffe, ich habe etwas von diesem Spaß im Buch weitergegeben. Auch wenn die Beispiele, besonders in den letzten beiden Kapiteln, vielleicht schon etwas komplex waren. Sehen Sie das Ganze aber so wie ich: Suchen Sie im Internet nach Lösungen, versuchen Sie nicht, zu tief in die einzelnen Themen einzusteigen und freuen Sie sich über die Erfolge. Viel Spaß also noch bei der weiteren Programmierung.
485
Sandini Bib
Sandini Bib
A
Anhang
le
n e rn
A.1 Lösungen zu den Fragen und Übungen A.1.1 Kapitel 1 1. Warum besteht häufig der Bedarf, Standardanwendungen durch in-
tegrierte Programme zu erweitern? Standardanwendungen sind zwar oft recht mächtig, können aber nie die zum Teil sehr speziellen Bedürfnisse aller Anwender bzw. Firmen abdecken. Besonders Firmen arbeiten häufig mit einer speziellen Geschäftslogik, die mit den eingebauten Möglichkeiten einer Standardanwendung nicht abgebildet werden kann. 2. Sie haben das Problem, eine Datenbank in Java ansprechen zu müs-
sen. Sie kennen Datenbanken, wissen aber nicht, welche Komponenten Sie einsetzen müssen und wie Sie diese programmieren. Wie finden Sie die benötigten Informationen? Wenn Sie ein gutes Buch zur Hand haben, lesen Sie einfach dort nach. Die Informationen in einem Buch (das möglichst nicht vom Hersteller der Programmiersprache stammt) sind meist sehr effizient. Wenn Sie kein Buch besitzen oder die Informationen nicht finden, können Sie auch in der Dokumentation der Programmiersprache nachschlagen, aber das ist oft eher ineffizient. Besser ist in diesem Fall, bei Google in normalen Webseiten und im Newsgroup-Archiv zu suchen. Wenn Sie die Lösung Ihres Problems dann immer noch nicht gefunden haben, können Sie schließlich noch eine Frage in eine Newsgroup mailen, die sich mit Java-Datenbanken beschäftigt.
487
Sandini Bib
A.1.2 Kapitel 2 1. Wie erzeugen Sie aus einem Java-Quelltext (mit der Endung .java) ein
ausführbares Programm? Dazu verwenden Sie den Java-Compiler javac, den Sie an der Konsole unter Übergabe des Namens der Quelltextdatei aufrufen. 2. Wie führen Sie ein Java-Programm (das in einer Datei mit der Endung
.class gespeichert ist) aus? Java-Programme führen Sie über den Java-Interpreter java aus. Rufen Sie diesen dazu an der Konsole unter Übergabe des Programmnamens (allerdings ohne Endung) auf. 3. Welche Vorteile bietet eine Entwicklungsumgebung wie beispielswei-
se Delphi oder Kylix gegenüber dem einfachen Entwickeln in einem Texteditor? Eine Entwicklungsumgebung wie beispielsweise Delphi oder Kylix besitzt gegenüber dem einfachen Entwickeln in einem Texteditor einige Vorteile. Neben einer wesentlich leichter lesbaren Darstellung des Quelltexts und vielen hilfreichen Tools fasst eine Entwicklungsumgebung alle Dateien eines Programms in einem Projekt zusammen, erlaubt das einfache Kompilieren und Testen des Programms und integriert meist auch einen umfangreichen Debugger. 4. Was sollten Sie beachten, wenn Sie ein neues Delphi/Kylix-
Programm speichern? Beim Speichern eines neuen Delphi/Kylix-Programms sollten Sie beachten, dass Sie alle Dateien des Programms in einem separaten Verzeichnis speichern, damit Sie diese ohne Probleme wiederfinden und zuordnen können. 5. Was ist ein Projekt?
Ein Projekt fasst alle Dateien, die zu einem Programm gehören, zusammen. Eine Entwicklungsumgebung erlaubt normalerweise über ein spezielles Projekt-Fenster den schnellen Zugriff auf diese Dateien und das Kompilieren aller Dateien zu einem Programm. 6. Nennen Sie zwei Unterschiede zwischen einer Konsolenanwendung
und einer Anwendung mit grafischer Oberfläche. Die zwei wichtigsten Unterschiede sind: • Eine Konsolenanwendung nimmt Ein- und Ausgaben an der Konsole vor. Eine normale Anwendung nimmt Eingaben in Steuerele-
488
$QKDQJ
Sandini Bib
menten in einem Fenster entgegen und gibt dort auch ihre Ausgaben aus, • Eine Konsolenanwendung startet oben im Quellcode und wird von oben nach unten abgearbeitet. Eine normale Anwendung wartet hingegen darauf, dass der Anwender durch die Betätigung eines Schalters oder durch andere Aktionen das Programm aufruft.
A.1.3 Kapitel 3 1. Warum kann ein Programm mathematische Berechnungen nur mit
einer eingeschränkten Genauigkeit ausführen? Zur Speicherung von Dezimalzahlen steht einem Programm nur eine beschränkte Menge an Bytes zur Verfügung. Die einzelnen Bits müssen nun so aufgeteilt werden, dass alle Vorkommaziffern gespeichert werden können. Für die Nachkommaziffern bleibt dann häufig nicht genügend Platz, sodass diese einfach ab einer bestimmten Stelle abgeschnitten werden. Bei Divisionen resultieren häufig Zahlen mit vielen Nachkommaziffern. Die eingeschränkte Speicherung führt dann immer zu einem etwas ungenauen Ergebnis. 2. Was unterscheidet einen ASCII-Zeichensatz vom Unicode-Zeichen-
satz? ASCII-Zeichen werden in einem Byte gespeichert, Unicode-Zeichen in zwei Byte. ASCII lässt deswegen nur 256 Zeichen zu, was mehrere verschiedene ASCII-Zeichensätze für unterschiedliche Regionen notwendig macht. Unicode kann hingegen 65535 Zeichen verwalten. Damit können fast alle Sprachen dieser Welt in einem einzigen Zeichensatz abgebildet werden. 3. Nennen Sie zwei Unterschiede zwischen einem Quellcode-Interpre-
ter und einem Maschinencode-Compiler. • Ein Quellcode-Interpreter interpretiert die einzelnen QuellcodeAnweisungen eines Quellcode-Programms und führt den so erzeugten Maschinencode direkt aus. Ein Maschinencode-Compiler übersetzt das gesamte Programm in eine ausführbare Datei. • Die von einem Quellcode-Interpreter ausgeführten Programme sind langsamer als in Maschinencode kompilierte Programme, weil das Interpretieren der einzelnen Quellcode-Anweisungen viel Zeit in Anspruch nimmt und auch bei wiederholten Anweisungen immer wieder neu ausgeführt wird. • Ein Quellcode-Interpreter benötigt zur Ausführung den Quellcode des Programms. Ein in Maschinencode kompiliertes Programm kann ohne den Quellcode ausgeführt werden.
489
Sandini Bib
4. Was unterscheidet ein Zwischencode- von einem Maschinencode-
Programm? Ein Maschinencode-Programm wird direkt über das Betriebssystem ausgeführt. Ein Zwischencode-Programm muss hingegen erst noch von einem Zwischencode-Interpreter oder Just-In-Time-Compiler in Maschinencode übersetzt werden. Deshalb kann ein ZwischencodeProgramm aber auch für die CPU des ausführenden Systems optimiert werden und ist deswegen nicht wesentlich langsamer und in einigen Situationen vielleicht auch schneller als ein Maschinencode-Programm. 5. Nennen Sie zwei Vorteile einer verteilten gegenüber einer normalen
Anwendung. • In einer verteilten Anwendung können viele Client-Anwendungen eine Server-Komponente nutzen. Muss die Komponente geändert werden, muss das nur ein einziges Mal ausgeführt werden. Die Clients bleiben im Idealfall unangetastet. • Da in verteilten Anwendungen häufig sehr viel Arbeit auf dem Server ausgeführt wird, können für die Clients einfache, rechenschwache Computer verwendet werden. • Verteilte Anwendungen erlauben eine sehr flexible (weil oft selbst programmierte) Verwaltung von Benutzerrechten. 6. Warum müssen Sie einem Computer absolut exakt mitteilen, was er
zu tun hat, wenn Sie ein Programm schreiben? Ein Computer kann vage Angaben zurzeit noch nicht wie ein Mensch interpretieren, also mit vorhandenem Wissen assoziieren, daraus neue Erkenntnisse gewinnen, durch Experimentieren vertiefen und das neue Wissen speichern. Ansätze dafür gibt es allerdings bereits im Bereich der künstlichen Intelligenz.
A.1.4 Kapitel 4 1. Wie viele verschiedene Speicherbereiche verwendet ein Programm
bei der Ausführung der folgenden Berechnung: i = (1 + 2) * 3;
i ist dabei eine Variable. Das Programm verwendet sechs Speicherbereiche. In drei Speicherbereichen werden die Konstanten verwaltet. Ein Bereich wird für die Addition benötigt, ein weiterer für die Multiplikation (also das Ergebnis). Das Ergebnis wird dann noch in die Variable i geschrieben, die ja auch einen Speicherbereich darstellt.
490
$QKDQJ
Sandini Bib
2. Wieso kann ein Short-Integer-Datentyp lediglich Zahlen im Bereich
von –32768 bis 32767 speichern? Ein Short-Integer-Datentyp belegt 16 Bit im Arbeitsspeicher. Da es sich um eine Zahl mit Vorzeichen handelt, wird das linke Bit für das Vorzeichen verwendet. Damit bleiben 15 Bit für die Speicherung übrig, also können Zahlen im Bereich von -215 (oder anders ausgedrückt 20 + 21 + ... + 214) bis 215-1 gespeichert werden. Die absolut gesehen kleinere positive Zahl resultiert aus der Tatsache, dass Zahlen mit Vorzeichen so gespeichert werden, dass die Zahl 0 nicht zweimal verwaltet wird. 3. Was sollten Sie immer beachten, wenn Sie mit den Datentypen Float
oder Double arbeiten? Diese Datentypen bieten nur eine eingeschränkte Genauigkeit. Float erlaubt nur maximal 6 bis 7 Dezimalziffern, Double 15 bis 16. Bei vielen Vorkommaziffern reduziert sich die Anzahl der möglichen Nachkommaziffern. Berechnungen mit diesen Datentypen sind also immer ungenau, wenn Dezimalstellen vorkommen. 4. Wieso können Sie mit Datumsdatentypen arithmetische Berechnun-
gen ausführen (z. B. einem Datum 100 Tage aufaddieren)? Datumsdatentypen sind eigentlich einfache Zahlen. Bei Java werden die Millisekunden gezählt, die seit dem 1.1.1970 01:00 vergangen sind. Bei Delphi und Kylix werden die Tage gezählt, die seit dem 30.12.1899 vergangen sind. 5. Wann muss ein Datentyp, der einem anderen Datentyp zugewiesen
wird, explizit konvertiert werden? Eine explizite Konvertierung ist dann notwendig, wenn beim Konvertieren Datenverluste auftreten können oder wenn die Konvertierung auch komplett fehlschlagen kann. 6. Warum ergibt die folgende Division in einem Java-Programm nicht
wie erwartet 0,25, sondern 0,0? int zahl1 = 1; int zahl2 = 4; double ergebnis = zahl1 / zahl2;
Werden Integerwerte dividiert, führt Java grundsätzlich eine Ganzzahldivision aus. Lediglich dann, wenn mindestens ein Fließkommawert in der Divisions-Operation vorkommt, dividiert ein JavaProgramm die Zahlen normal.
491
Sandini Bib
7. Welchen Sinn hat die Einteilung der Klassen der Java-Bibliothek in
einzelne Pakete? Die Einteilung in Pakete ermöglicht das einfache Finden einer gesuchten Funktionalität, indem Sie ausgehend von einem Paket, das vom Namen her zu passen scheint, weiter nach unten in der Hierarchie der Pakete suchen.
A.1.5 Kapitel 5 1. Welchen Datentyp besitzen Vergleichsausdrücke?
Vergleichsausdrücke besitzen immer den Typ boolean. 2. Warum ergibt der Vergleich "a" == "a" in Java false?
Wenn Sie in Java Strings miteinander vergleichen, vergleichen Sie in Wirklichkeit zwei Objekte. Ein Objektvergleich vergleicht nicht den Inhalt der Objekte, sondern die Referenzen, die im Programm auf die Objekte zeigen. Der Vergleich zweier Referenzen, die auf zwei separate Objekte zeigen, ergibt immer false. 3. Worauf müssen Sie immer achten, wenn Sie komplexe Vergleichsaus-
drücke formulieren, die mit Und-Verknüpfungen, Oder-Verknüpfungen und/oder Negierungen arbeiten? Sie sollten die einzelnen Teilausdrücke immer klammern. Damit beugen Sie potenziellen Fehlern vor, die durch die Priorität der logischen Operatoren (not vor and vor or) entstehen können. Außerdem vermeiden Sie in Sprachen, bei denen die Operatoren not, and und or eine Doppelbedeutung besitzen, dass der Compiler fälschlicherweise eine Bit-Operation ausführt. 4. Warum sollten Sie die Anweisungen innerhalb einer Schleife und ei-
ner Verzweigung immer etwas nach rechts einrücken? Damit erreichen Sie, dass Ihr Programm wesentlich übersichtlicher wird, als wenn Sie nicht einrücken. Das Verständnis des Programms fällt Ihnen und anderen Programmierern dann leichter. 5. Was unterscheidet eine kopfgesteuerte von einer fußgesteuerten
Schleife? Eine fußgesteuerte Schleife prüft die Schleifenbedingung erst im Fuß und wird deswegen im Gegensatz zur kopfgesteuerten Schleife mindestens einmal durchlaufen.
492
$QKDQJ
Sandini Bib
6. Welchen Wert besitzt die Variable i nach der Ausführung der folgen-
den Schleife? for i := 1 to 10 do begin writeln(i); end;
Nach der Ausführung der Schleife besitzt i den Wert 11. Das Programm hat nach dem letzten Durchlauf (i = 10) diese Variable noch um 1 hochgezählt und dann festgestellt, dass die Bedingung (i <= 10) der Schleife nicht mehr zutrifft. 7. Schreiben Sie eine Java-Konsolenanwendung, bei der Sie eine Einga-
be simulieren, indem Sie eine Variable zahl auf den Wert 7 festlegen. Die Anwendung soll dann für alle Zahlen von 1 bis zu dieser Zahl die Fakultät berechnen und ausgeben. Die Fakultät einer Zahl berechnet sich, indem alle Zahlen von 1 bis zu der Zahl miteinander multipliziert werden. Achten Sie darauf, dass möglichst keine Überläufe entstehen können. Das folgende Beispiel löst das Problem über zwei Schleifen: class Aufgabe07 { public static void main(String args[]) { System.out.println("Aufgabe 7"); /* Festlegung der Zahl */ int zahl = 15; /* Äußere Schleife für alle Zahlen */ for (int i = 1; i <= zahl; i++) { /* Variable für das Ergebnis */ long ergebnis = 1; /* Innere Schleife zur Berechnung der Fakultät */ for (int multiplikant = 2; multiplikant <= i; multiplikant++) { ergebnis *= multiplikant; } /* Ergebnis ausgeben */ System.out.println("Die Fakultät von " + i + " ist " + ergebnis); } } }
493
Sandini Bib
8.
Warum sollten Sie in einer Java-switch-Verzweigung immer alle Blöcke für die einzelnen Fälle mit break abschließen? Java-Programme untersuchen immer jeden einzelnen Fall einer switch-Verzweigung, wenn kein break enthalten ist. Das kann zu fehlerhaften Programmen führen, wenn ein Fall bereits eingetreten ist und ein weiterer ebenfalls zutrifft. Außerdem wird ansonsten der default-Block immer ausgeführt. Über ein break am Ende jedes Falls stellen Sie sicher, dass das Programm die Verzweigung verlässt, wenn ein Fall zutrifft.
9.
Was ist der Unterschied zwischen einer Funktion und einer Prozedur? Eine Funktion gibt immer einen Wert zurück, eine Prozedur nicht.
10. Schreiben Sie eine Java-Funktion, die ermittelt, ob eine Zahl eine
Primzahl ist. Meine Lösung ist recht einfach. Ich nutze dabei die Tatsache, dass die return-Anweisung in Java dazu führt, dass eine Funktion sofort verlassen wird. Damit ist die Funktion automatisch auch gleich so optimiert, dass keine unnötigen Schleifendurchläufe ausgeführt werden: static boolean Primzahl(int zahl) { if (zahl < 2) return false; for (int teiler = 2; teiler < zahl; teiler++) if (zahl % teiler == 0) return false; return true; } 11. Wo sollten Sie die Funktion zur Primzahlenermittlung idealerweise
speichern? Eine Funktion zur Primzahlenermittlung sollte idealerweise in einer Klasse, die für mathematische Berechnungen vorgesehen ist, in einem allgemein zugänglichen Verzeichnis gespeichert werden. Der Pfad zu diesem Verzeichnis sollte in der CLASSPATH-Umgebungsvariablen eingetragen sein, damit der Interpreter die kompilierte Version dieser Datei finden kann. Damit erreichen Sie, dass Sie die Funktion sehr einfach wiederfinden und wiederverwenden können. 12. Wie lange lebt eine Variable, die in einer Funktion deklariert ist?
Eine solche Variable lebt nur, solange die Funktion ausgeführt wird. Nach dem Beenden der Funktion ist der Speicherbereich dieser Variablen freigegeben und damit nicht mehr zugänglich.
494
$QKDQJ
Sandini Bib
13. Warum sollten Sie wenn möglich auf globale Variablen verzichten?
Globale Variablen führen fast zwangsläufig in größeren Projekten zu schweren logischen Fehlern, weil niemand mehr mit Sicherheit sagen kann, welche Funktion, Prozedur oder Methode eine globale Variable benutzt und verändert. Ein fälschliches Manipulieren einer solchen Variablen ist damit fast vorprogrammiert.
A.1.6 Kapitel 6 1. Was ist eine Klasse?
Eine Klasse ist normalerweise eine Bauform für Objekte. Objekte, die mit Hilfe einer Klasse erzeugt werden, besitzen die Eigenschaften und Methoden, die in der Klasse deklariert sind. Klassen können aber auch statische Elemente besitzen, die dann wie normale Funktionen und Variablen verwendet werden können. Eine solche Klasse verhält sich eher wie ein Modul der strukturierten Programmierung. 2. Was passiert, wenn Sie eine Objektvariable einer anderen Objektva-
riable zuweisen? Wenn Sie eine Objektvariable einer anderen Objektvariable zuweisen, wird die Referenz der rechten in die linke Variable kopiert. Die linke Variable verweist dann auf dasselbe Objekt wie die rechte. 3. Welche Bedeutung besitzt die Referenz self bzw. this?
Diese Referenzen erzeugt der Compiler implizit. Innerhalb von Methoden einer Klasse können Sie self bzw. this verwenden, um explizit auf andere Methoden oder Eigenschaften der Klasse zuzugreifen, was besonders dann wichtig ist, wenn innerhalb der Methode gleichnamige lokale Elemente existieren. 4. Was ist ein Standardkonstruktor?
Ein Standardkonstruktor ist ein Konstruktor ohne Argumente. Dieser Konstruktor wird in Delphi, Kylix und Java implizit in neue Klassen integriert. 5. Welche Daten sollten sinnvollerweise dem Konstruktor einer Klasse
übergeben werden? Alle Daten, die direkt bei der Erzeugung eines Objekts definiert werden müssen (also alle Initialisierungsdaten), sollten dem Konstruktor einer Klasse übergeben werden. Damit stellen Sie sicher, dass notwendige Initialisierungen möglichst einfach programmiert werden können.
495
Sandini Bib
6. Was bedeutet Kapselung?
Kapselung bedeutet, dass ein Objekt den Zugriff auf seine Daten kontrolliert und keinen direkten Zugriff zulässt.
A.1.7 Kapitel 7 1. Welche Vorteile bieten Auflistungen gegenüber einfachen Arrays?
Auflistungen können im Programm dynamisch erweitert werden, was mit Arrays nicht möglich ist. Daneben ist auch das Entfernen von Elementen problemlos möglich. Einige Auflistungen ermöglichen zudem die Assoziation der gespeicherten Elemente mit einem Schlüssel, über den der Zugriff auf einzelne Elemente sehr effizient ist. 2. Welchen Vorteil bieten Arrays gegenüber Auflistungen?
Arrays können mehrdimensional gestaltet werden und eignen sich deshalb für die Lösung komplexer mathematischer Probleme. 3. Was können Sie alles in einer Auflistung verwalten?
Alles. Auflistungen erlauben in der Regel die Speicherung von beliebigen Objekten, aber oft auch das Speichern beliebiger einfacher Daten. 4. Wie finden Sie heraus, wie Sie in Delphi oder Kylix eine Textdatei le-
sen und schreiben? Sie suchen im Internet nach einer Lösung.
A.1.8 Kapitel 9 1. Was unterscheidet eine objektorientierte von einer relationalen Da-
tenbank? Eine objektorientierte Datenbank verwaltet die Daten als Objekte in Form von Auflistungen. Eine relationale Datenbank verwaltet ihre Daten in Tabellen. Die Beziehungen zwischen den Daten objektorientierter Datenbanken können direkt aufgelöst werden. Um die Auflösung der Beziehungen zwischen den Tabellen relationaler Datenbanken müssen Sie sich selbst (in Form von geeigneten SQL-Anweisungen) kümmern. 2. Wozu setzen Sie SQL ein?
SQL verwenden Sie zum Erstellen und Manipulieren von Datenbanken, zum Anfügen, zum Abfragen, zum Ändern und zum Löschen von Daten.
496
$QKDQJ
Sandini Bib
3. Welche Aufgabe übernimmt ein JDBC-Treiber?
Ein JDBC-Treiber stellt die physikalische Verbindung zwischen einem DBMS und den JDBC-Objekten her, die Sie in Ihren Programmen verwenden, um auf eine Datenbank zuzugreifen. 4. Welcher Unterschied besteht zwischen der Arbeit mit einem ResultSet-
Objekt und dem direkten Ausführen von SQL-Anweisungen in Java? Mit einem ResultSet-Objekt können Sie Daten durchgehen. Sie können den jeweils aktuellen Datensatz ändern oder löschen und je einen neuen Datensatz anfügen. Mit SQL können Sie hingegen gleich eine Vielzahl von Datensätzen anfügen (wie das geht, habe ich allerdings nicht gezeigt ...), ändern oder löschen. 5. Programmieren Sie eine Anwendung, die dem Anwender erlaubt,
über ein Formular ein neues Buch anzufügen. Die Lösung dieser Aufgabe finden Sie auf der Buch-CD im Ordner Aufgaben-Lösungen\Kapitel 09\Aufgabe 05 oder im Internet an der Adresse www.juergen-bayer.net/buecher/programmierenlernen/loesungen/ kapitel9/aufgabe5/. 6. Programmieren Sie eine Anwendung, die es dem Anwender erlaubt, die
gespeicherten Bücher durchzugehen (ähnlich der Beispielanwendung aus Kapitel 8), zu ändern, zu löschen und neue Bücher anzufügen. Die Lösung dieser Aufgabe finden Sie auf der Buch-CD im Ordner Aufgaben-Lösungen\Kapitel 09\Aufgabe 06 oder im Internet an der Adresse www.juergen-bayer.net/buecher/programmierenlernen/loesungen/ kapitel9/aufgabe6/. 7. Erweitern Sie die Anwendung aus Aufgabe 6 so, dass zusätzlich das
Suchen nach Büchern möglich ist, die einen bestimmten Titel tragen. Der Anwender soll dabei nur einen Teil des Buchtitels eingeben können. Lesen Sie in der Dokumentation von MySQL dazu die Syntax des LIKE-Operator nach. Die Lösung dieser Aufgabe finden Sie auf der Buch-CD im Ordner Aufgaben-Lösungen\Kapitel 09\Aufgabe 07 oder im Internet unter der Adresse www.juergen-bayer.net/buecher/programmierenlernen/loesungen/ kapitel9/aufgabe7/. 8. Erweitern Sie die Anwendung aus Aufgabe 7 so, dass zusätzlich das
Anfügen von neuen Autoren möglich ist. Die Lösung dieser Aufgabe finden Sie auf der Buch-CD im Ordner Aufgaben-Lösungen\Kapitel 09\Aufgabe 08 oder im Internet unter der Adresse www.juergen-bayer.net/buecher/programmierenlernen/loesungen/ kapitel9/aufgabe8/.
497
Sandini Bib
A.2 Glossar .NET: NET ist ein neues Programmierermodell von Microsoft, das eine große Menge an Features beinhaltet. Alle neuen Microsoft-Programmiersprachen setzen auf dieses Modell auf und verwenden die dort definierten Basiselemente. Im .NET-Framework, das auf einem Rechner installiert sein muss, der .NET-Programme ausführen soll, sind z. B. die Datentypen definiert, mit denen die Programmiersprachen dann arbeiten. Aber auch solche komplexen Dinge wie das Erzeugen von Klassen und das Arbeiten mit Objekten sind im .NET-Framework definiert. .NET enthält aber auch noch weitere Features wie z. B. eine sehr einfache Möglichkeit zur Kommunikation einer Anwendung mit anderen Anwendungen oder Objekten (ähnlich COM), die sogar oder Probleme über das Internet ausgeführt werden kann.
Î
Î
Î
.NET-Framework: Das .NET-Framework bildet den Rahmen, der für die Ausführung von .NET-Programmen notwendig ist. Darin enthalten ist u. a. die Laufzeitumgebung, die alle Basisklassen enthält, mit denen der Programmierer in seinen Quellcodes arbeitet. Das Framework enthält aber auch einen Compiler für .NET-Quellcodes und einen JustIn-Time-Compiler für die kompilierten Zwischencode-Programme.
Î
Î
Î
Î
Ableiten: Der Begriff „Ableiten“ wird in der objektorientierten Programmierung verwendet. Wenn Sie eine neue Klasse deklarieren und eine andere Klasse als Basis angeben, leiten Sie die neue Klasse von der Basisklasse ab. Die neue Klasse erbt damit alle Eigenschaften und Methoden der Basisklasse und kann um neue Eigenschaften und Methoden erweitert werden. Sie können sogar die geerbten Eigenschaften und Methoden mit einer neuen Implementierung versehen, so dass diese bei Objekten der neuen Klasse anders reagieren als bei Objekten der alten Klasse. Absolutwert: Der Absolutwert einer Zahl ist deren Wert ohne Berücksichtigung des Vorzeichens. Die Zahl -123 besitzt also denselben Absolutwert wie die Zahl 123. Algorithmus: Ein Algorithmus ist eine Verarbeitungsvorschrift, die so präzise formuliert ist, dass sie von einem Computer durchgeführt werden kann. Da Computer nicht wie Menschen interpretieren oder assoziieren können, muss einem Computer bis auf das letzte Detail mitgeteilt werden, was er zu tun hat. Wenn Sie z. B. eine Funktion schreiben wollen, die die Einkommensteuer aus einem gegebenen Gehalt berechnet, müssen Sie sich vorher Gedanken über den dazu notwendigen Algorithmus machen.
Î
498
$QKDQJ
Sandini Bib
Aufrufer: Bei der Programmierung ist Aufrufer die Bezeichnung für einen Programmteil, der eine Funktion, Prozedur oder Methode aufruft. Der englische Begriff dafür ist „Caller“. Die aufgerufene Funktion, Prozedur oder Methode ist dann im englischen Sprachraum der „Callee“, wofür es wohl keine sinnvolle deutsche Übersetzung gibt (vielleicht „Aufgerufener“). Bezeichner: Ein Bezeichner ist eine Zeichenfolge in einem Programm, über die eine Variable, eine Konstante, eine Funktion, eine Prozedur, eine Methode, eine Klasse, ein benutzerdefinierter Datentyp etc. identifiziert wird. Die Bezeichner eines Programms müssen der Syntax der Programmiersprache entsprechen. In den meisten Programmiersprachen dürfen Bezeichner nur aus Buchstaben, Zahlen und dem Unterstrich zusammengesetzt werden und müssen mit einem Buchstaben beginnen.
Î
Î
Î
Î
Î
Î
Bibliothek: Die Bibliothek einer Programmiersprache enthält meist eine Vielzahl an vorgefertigten Funktionen, Prozeduren und Klassen, über die Sie viele Aufgaben, die bei der täglichen Programmierung anstehen, sehr einfach lösen können. Moderne Bibliotheken liegen meist in Form einer Klassenbibliothek vor.
Î
Î
Î
Î
Cache: Ein Cache ist im Allgemeinen ein schneller Speicherbereich (z. B. im Arbeitsspeicher), in dem ein Programm oder das Betriebssystem Daten, die von einem langsamen Speichermedium (z. B. der Festplatte) gelesen werden, für eine weitere Benutzung temporär zwischenspeichert. Werden diese Daten vom Programm (einfach oder mehrfach) benötigt, können sie aus dem schnelleren Cache gelesen werden. Caches werden hauptsächlich dazu verwendet, Programme zu beschleunigen. Ein Programm, das beispielsweise eine Datenbanktabelle mehrfach durchgehen oder darin suchen muss, sollte diese einmal in den Arbeitsspeicher lesen und dann dort bearbeiten. Oft verwendet auch das Betriebssystem Caches, besonders beim Lesen von Dateien. Eine Datei, die einmal gelesen wurde, wird normalerweise gleich in den Dateicache des Betriebssystems geschrieben, sodass ein zweites Lesen sehr schnell ist. Eine andere Verwendung eines Cache ist die bei CD-Brennern. Ein solcher liest die zu brennenden Daten zuerst in seinen Cache, damit beim Brennen permanent Daten zur Verfügung stehen und der Brennvorgang nicht unterbrochen wird. Während des Brennens wird der Cache immer wieder nachgefüllt. Ohne Cache würden alle Störungen beim Lesen (z. B. wenn das Betriebssystem gerade eine andere rechenintensive Aufgabe erfüllen muss) den Brennvorgang unterbrechen und den CD-Rohling damit unbrauchbar machen. Caller: siehe bei Aufrufer
*ORVVDU
499
Sandini Bib
Carriage Return: Wagenrücklauf. Damit ist das ASCII-Zeichen 13 gemeint. In Textdateien und Zeichenketten wird damit unter Windows in Verbindung mit dem Line Feed-Zeichen (ASCII 10) ein kompletter Zeilenumbruch realisiert. Client/Server-Anwendung: : Eine Client/Server-Anwendung ist eine Anwendung, bei der ein Client (das ist im Prinzip eine normale Anwendung) die Dienste eines Servers verwendet. Der Server ist mindestens in einer anderen Datei gespeichert und läuft in einem anderen Prozess. Häufig läuft der Server-Prozess auch auf einem anderen Rechner. Zur Kommunikation zwischen Client und Server wird häufig das COMund neuerdings auch das .NET-Modell verwendet.
Î
Î
COM: Vereinfacht ausgedrückt liefert das Component Object Model (COM) einen Standard, der ermöglicht, dass Anwendungen mit Objekten kommunizieren, die entweder in anderen Anwendungen oder in speziellen COM-DLL oder -EXE-Dateien gespeichert sind. Diese auch als Komponenten bezeichneten Dateien enthalten in der Regel Klassen, aus denen die benutzende Anwendung Objekte erzeugt. Ein Beispiel für eine solche COM-Komponente ist die „Microsoft ActiveX Data Objects“-Komponente, die Klassen zur Arbeit mit Datenbanken enthält. Jedes Programm, das COM-fähig ist, kann eine solche Komponente verwenden.
Î
Î
Compiler: Ein Compiler übersetzt ein in einer höheren Programmiersprache geschriebenes Programm in die Befehle des Betriebssystems oder in Maschinensprachebefehle und speichert das so übersetzte Programm in eine Datei mit der Endung .exe (executable = ausführbar). Diese .exe-Dateie kann dann direkt über das Betriebssystem ausgeführt werden. Der Quellcode des Programms ist für die Ausführungen nicht mehr notwendig. Component Object Model: siehe unter COM CPU: Central Processing Unit. Dieser Begriff steht für den Prozessor eines Computers. Datenbank: Eine Datenbank speichert zusammengehörige Daten und erlaubt einem Programmierer einen sehr flexiblen Zugriff auf diese Daten. Im Gegensatz zu einer Datei kann eine Datenbank ohne Probleme auch mehrere verschiedene Datenstrukturen speichern. In einem Bereich einer Datenbank für einen Bestell-Shop werden z. B. die Artikel des Shops gespeichert, ein anderer Bereich der Datenbank speichert die Kunden und ein weiterer die Bestellungen.
500
$QKDQJ
Sandini Bib
Datentyp: Ein Datentyp legt den Typ von Daten fest (was auch sonst ...). Jedes Datum (als Einzahl von Daten!) in einem Programm besitzt einen Datentyp. Die möglichen Datentypen unterscheiden sich bei den einzelnen Programmiersprachen. Texte werden meist in einem StringTyp verwaltet. Bei Zahl-Datentypen werden solche für Ganzzahlen (Byte, Integer etc.) und solche für Zahlen mit Dezimalstelle (Single, Double etc.) unterschieden. Der Compiler oder Interpreter kann an Hand der Datentypen erkennen, wie ein Datum ausgewertet werden muss.
Î
Î
Datum: Der Begriff „Datum“ steht nicht nur für ein Kalenderdatum, sondern auch für die Einzahl des Begriffs „Daten“. Debugger: Ein Debugger ist eine Anwendung, mit der Fehler in Programmen lokalisiert werden können. Debugger sind heute meist in die Entwicklungsumgebung einer Programmiersprache integriert, können aber auch als externe Programme vorliegen. Die meisten Debugger sind in der Lage, ein Programm schrittweise auszuführen, dabei den Quellcode des Programms anzuzeigen und die aktuell ausgeführte Anweisung zu markieren. Prinzipiell jeder Debugger bietet dem Programmierer zusätzlich die Möglichkeit, den Wert der im Programm verwendeten Daten ( Variablen etc.) anzuzeigen.
Î
Î
Debugging: Debugging bezeichnet das Suchen und Beseitigen von Fehlern in einem Programm. Üblicherweise wird dabei ein Debugger verwendet.
Î
Deklaration: Eine Deklaration (Anmeldung) ist eine Festlegung, welche Bedeutung ein Bezeichner besitzt. Sie geben an, wie der Compiler diese Bezeichner im nachfolgenden Programmtext behandeln soll. Wird zum Beispiel eine Variable deklariert und der Compiler trifft im Programm auf den Bezeichner der Variable, weiß er, dass es sich um eine Variable (und nicht um eine Prozedur oder Funktion o. Ä.) handelt.
Î
Î
Î
Deklarieren: Deklarieren bedeutet, dass Sie etwas beim Compiler „anmelden“, damit dieser beim Ablauf des Programms daraus eine entsprechende Struktur im Arbeitsspeicher erzeugt. Variablen, die Sie benutzen wollen, müssen Sie beispielsweise bei den meisten Programmiersprachen deklarieren (in einigen speziellen Programmiersprachen, besonders bei Scripting-Sprachen, können Sie auch darauf verzichten), damit der Compiler einen passenden Speicherbereich reserviert. Aber auch das Schreiben von Funktionen, Prozeduren, Klassen, deren Eigenschaften und Methoden wird als Deklaration bezeichet. Eigenschaft: Eine Eigenschaft gehört zu einem Objekt und beschreibt entweder dessen Aussehen oder dessen Verhalten.
*ORVVDU
501
Sandini Bib
Eingabefokus: Der Eingabefokus bezeichnet das Steuerelement in einem Windows- oder Linux-Fenster, das gerade Eingaben empfängt. Wenn Sie z. B. mit der Maus oder der Tab-Taste zu einem Textfeld wechseln, besitzt dieses den Eingabefokus und erhält alle Eingaben. Entwicklungsumgebung: Eine Entwicklungsumgebung vereint alle Tools (Werkzeuge), die man zum Programmieren benötigt, innerhalb einer Anwendung. Die meisten Entwicklungsumgebungen enthalten einen Editor für das Schreiben des Quelltextes, einen Editor für das Entwerfen der Fenster einer (Windows-)Anwendung, einen Debugger für das Finden von Fehlern und den Compiler für die jeweilige Programmiersprache.
Î
Î
Ereignisorientiertes Programm: Ein ereignisorientiertes Programm wartet quasi in einer großen Schleife darauf, dass etwas passiert. Klickt der Anwender z. B. auf einen Schalter in einem Fenster, wird das ClickEreignis dieses Schalters für das Fenster aufgerufen. In den meisten Programmiersprachen werden diese Ereignisse in so genannten Ereignisprozeduren abgefangen. Dort schreibt der Programmierer das Programm, mit dem die Anwendung auf das Ereignis reagieren soll.
Î
Explizit: Im Gegensatz zu implizierten (siehe Implizit) Aussagen und Aktionen sind explizite solche, die ausdrücklich getroffen bzw. ausgeführt werden. Den Text, den Sie in einer Textverarbeitung bearbeiten, können Sie beispielsweise über das entsprechende Symbol der Symbolleiste Ihres Textverarbeitungsprogramms explizit speichern. Haben Sie in den Optionen Ihrer Textverarbeitung das in bestimmten Intervallen automatische Speichern aktiviert, wird dieses dagegen implizit vorgenommen. Formular: Als Formular wird üblicherweise ein Fenster eines Programms mit grafischer Oberfläche bezeichnet. Funktion: Eine Funktion ist eine Folge von zusammenhängenden Anweisungen, die unter dem Namen der Funktion aufgerufen werden können. Funktionen werden wie Prozeduren und Methoden zur Wiederverwendung von Programmcode und zur Strukturierung eines Programms eingesetzt. Im Gegensatz zu einer Prozedur gibt eine Funktion immer einen Wert zurück. Viele Funktionen werden deshalb für komplexe mathematische Berechnungen eingesetzt. Funktionen werden immer in einem Modul gespeichert und gelten global im Programm. Mit Funktionen wird deshalb auf klassische Weise strukturiert programmiert. Das unterscheidet Funktionen auch von Methoden, die in einer Klasse gespeichert werden und deshalb zur objektorientierten Programmierung gehören.
Î
Î
Î
502
$QKDQJ
Î
Sandini Bib
Garbage-Collector: Ein Garbage-Collector („Müllsammler“) ist ein Prozess in einer Anwendung, der immer dann, wenn die Anwendung selbst gerade nicht beschäftigt ist, den Speicher nach unbenutzten Objekten (quasi nach „Müll“) durchsucht und diese dann freigibt. Einige Programmiersprachen, wie Java und die Sprachen des Microsoft-.NET-Konzepts, integrieren einen Garbage-Collector in die mit diesen Sprachen erstellten Programme. Der Programmierer ist damit nicht mehr gezwungen – wie z. B. in C++ und Pascal – benutzte Objekte auch wieder freizugeben. Der Garbage-Collector kümmert sich automatisch um die „Entsorgung“ dieser Objekte. Geschäftslogik: Geschäftslogik ist die Logik, mit der Firmen ihre Geschäfte abwickeln. Jede Firma besitzt ihre eigene Geschäftslogik. Beispielsweise werden Aufträge nur per Fax entgegengenommen, Lieferungen nur per UPS oder DHL ausgesendet, Zahlungen auch in Teilzahlungen akzeptiert und regelmäßig die Rabatte von Kunden, die 3 Monate nicht bestellt haben, um 1 % gekürzt. Diese Geschäftslogik ist in der Praxis meist recht komplex und für Außenstehende schwer zu verstehen. HTML: HTML ist eine Sprache, mit der die Dokumente beschrieben werden, die über das World Wide Web im Internet verteilt werden. Eine Webseite, die Sie sich in einem Internet-Browser anschauen können, besteht mindestens aus Elementen der Sprache HTML, kann aber daneben auch Bilder, Videos und spezielle, in der Programmiersprache Java geschriebene Programme enthalten. Implementierung: Als Implementierung wird das Schreiben des Quellcodes von Funktionen, Prozeduren und Methoden bezeichnet. In den meisten Programmiersprachen werden diese normalerweise direkt mit der Deklaration auch implementiert. Einige Sprachen erlauben bzw. verlangen aber, dass die Funktion, Prozedur oder Methode zunächst nur deklariert und im weiteren Verlauf des Quellcodes dann implementiert wird. Ein Beispiel für diese Technik ist die Sprache Object Pascal, bei der Sie die öffentlichen Prozeduren und Funktionen und die Methoden einer öffentlichen Klasse im Interface-Abschnitt der Unit zunächst deklarieren und diese dann im Implementation-Abschnitt implementieren.
Î Î
Î
Î
Î
Implizit: Implizit bedeutet in der Computerwelt, dass eine Aussage in einer anderen Aussage enthalten ist und deswegen nicht ausdrücklich genannt werden muss. Wenn ich beispielsweise sage, dass ich ein Mensch bin, impliziert dies, dass ich (mehr oder weniger) Verstand besitze (womit ich nicht sagen will, dass Tiere keinen Verstand besitzen!). Implizit bezieht sich oft auch auf Aktionen, die in anderen Aktionen enthalten sind. Wenn Sie beispielsweise ein Auto starten, werden impli-
*ORVVDU
503
Sandini Bib
zit die Zündung und die Benzinpumpe eingeschaltet und der Anlasser mit Strom versorgt. Das Gegenteil vom Implizit ist Explizit (Ausdrücklich).
Î
Interpreter: Ein Interpreter übersetzt ein in einer höheren Programmiersprache übersetztes Programm direkt, Zeile für Zeile in die Befehle des Betriebssystems bzw. in Maschinensprache und sendet diese Befehle an die CPU des Rechners, die diese dann ausführt. Im Gegensatz zu einem Compiler erzeugt ein Interpreter also keine .EXE-Datei und benötigt für die Ausführung immer den Quellcode. Eine Sonderform des Interpreters ist ein Interpreter für Zwischencode-Programme.
Î
Î
JIT: siehe unter Just-In-Time-Compiler Just-In-Time-Compiler: Ein Just-In-Time-Compiler wertet ein in einem Zwischencode vorliegendes Programm so aus, dass der gerade ausgeführte Programmteil „just in time“ kompiliert, für spätere Ausführungen zwischengespeichert und dann ausgeführt wird. Ab dem zweiten Ausführen dieses Programmcodes wird dann der zwischengespeicherte native Programmcode verwendet.
Î Î
Klasse: Eine Klasse wird in der objektorientierten Programmierung verwendet und beschreibt, welche Eigenschaften und Methoden (und Ereignisse) Objekte besitzen, die mit Hilfe dieser Klasse erzeugt werden.
Î
Î
Klassenbibliothek: Eine Klassenbibliothek ist, wie der Name schon sagt, eine Bibliothek, die Klassen enthält. Klassenbibliotheken sind in der Regel vorkompiliert und können in Programme eingebunden werden, damit der Programmierer aus den enthaltenen Klassen Objekte erzeugen und mit diesen Objekten arbeiten kann. Moderne Programmiersprachen liefern sehr viele Klassenbibliotheken mit, die manchmal etwas anders bezeichnet werden. Java verwendet z. B. den Begriff Package.
Î
Klassendeklaration: Eine Klassendeklaration ist die Deklaration einer Klasse. In dieser wird festgelegt, welche Eigenschaften, Methoden und Ereignisse eine Klasse besitzt. Die Deklaration einer Klasse muss noch keine Implementierung der Methoden beinhalten.
Î
Î
Komponente: Eine Komponente ist ein Modul eines Programms. Üblicherweise wird dieser Begriff aber für vorkompilierte Module verwendet. Konsole: Eine Konsole ist eine recht einfache Umgebung für die Ausführung von Programmen, die keine eigene Oberfläche besitzen. Eine Konsole bietet eine so genannte Eingabeaufforderung (den Prompt), an der der Benutzer einfache Text-Eingaben vornehmen kann. Konsoleprogramme laufen in dieser Konsole, nehmen die Eingaben des Benutzers entgegen und können Ausgaben in einfacher Textform vornehmen.
504
$QKDQJ
Sandini Bib
Laufzeitfehler: Ein Laufzeitfehler ist ein Fehler im Programm, der erst dann auftritt, wenn das Programm ausgeführt wird. Ein solcher Fehler kann dadurch verursacht werden, dass ein Programm logische Fehler enthält oder dass eine so genannte Ausnahme auftritt. Wenn ein Programm zum Beispiel eine Datei öffnen will, und diese Datei ist nicht vorhanden, tritt ein Laufzeitfehler auf. Line Feed: Zeilenvorschub. Damit ist das ASCII-Zeichen 10 gemeint. In Textdateien und Zeichenketten wird damit, unter Windows in Verbindung mit dem Carriage Return-Zeichen (ASCII 13), ein kompletter Zeilenumbruch realisiert. Methode: Eine Methode gehört zu einem Objekt. Methoden sind vergleichbar mit Funktionen und Prozeduren, erledigen also irgendeinen Job. Der Unterschied zu diesen ist aber, das Methoden immer zu einem Objekt gehören und folglich immer im Kontext dieses Objekts arbeiten.
Î
Î
Modul: Modul ist ein allgemeiner Begriff für einen Teil eines Programms, der thematisch zusammengehörige Daten, Funktionen, Prozeduren und/oder Methoden enthält. Ein Modul einer Buchhaltungsanwendung kann beispielsweise alles das enthalten, was zum Rechnungsdruck benötigt wird, ein anderes Modul enthält Programmteile, die zur Verwaltung von Bestellungen verwendet werden. Durch eine sinnvolle Aufteilung eines Programms in einzelne Module erreichen Sie, dass diese sehr einfach wiederverwendet und auch sehr einfach gewartet werden können. Dabei spricht man übrigens von „modularer Programmierung“. Bei objektorientierten Programmen enthält ein Modul ausschließlich zusammengehörige Klassen, bei strukturierten Programmen Funktionen und Prozeduren. Ein anderer Begriff für Modul ist „Komponente“. Der Begriff „Komponente“ wird allerdings üblicherweise für vorkompilierte Module verwendet. Motherboard: Ein Motherboard ist eine große Platine mit vielen Schaltkreisen, die definierte Aufgaben übernehmen, und standardisierten Steckplätzen oder Anschlussstellen für die CPU, den Arbeitsspeicher, die Festplatte, die Grafikkarte und andere Erweiterungen des Computers. Alle Bestandteile des Computers sind mehr oder weniger direkt mit dem Motherboard verbunden. Das Motherboard übernimmt die Aufgabe, die Kommunikation zwischen den Computer-Bestandteilen zu ermöglichen und zu steuern. Nativer Code, Native Programme: „Native“ ist ein englischer Begriff, der übersetzt eigentlich so etwas wie „Eingeborener“ bedeutet, aber eigentlich die folgende Bedeutung hat: natürlich, unverändert, im natürlichen Zustand befindlich. Native Programme sind Programme, die mit
*ORVVDU
505
Sandini Bib
den spezifischen Befehlen einer bestimmten Betriebssystem-Familie (z. B. Windows) arbeiten, also auf anderen Betriebssystemen (z. B. Unix) nicht ausgeführt werden können. Im Gegensatz dazu können Programme, die aus einem Zwischencode bestehen, auf verschiedenen Betriebssystemen ausgeführt werden, sind also nicht nativ. Persistent: Persistent (dauerhaft) wird meist in Zusammenhang mit dem Speichern von Daten verwendet. Das persistente Speichern von Daten meint das dauerhafte Speichern, also das Speichern in Form einer Datei, und steht im Gegensatz zum transienten (flüchtigen) Speichern von Daten im Arbeitsspeicher. Protokoll: Ein Protokoll beschreibt (im Computer), wie Daten gesendet und empfangen werden. Einfache Protokolle versehen die gesendeten Daten nur mit einer so genannten Checksumme, die aus den Daten errechnet und an diese hinten angehängt wird. Mit Hilfe dieser Checksumme kann der Empfänger überprüfen, ob die Daten korrekt gesendet wurden. Komplexere Protokolle, die TCP/IP, trennen die gesendeten Daten zusätzlich noch in kleine Pakete auf, die jedes für sich mit einer Checksumme und einer Paketnummer versehen werden. Diese Pakete werden einzeln an den Empfänger gesendet und ermöglichen so auch das Senden von großen Datenmengen über relativ langsame Netze, wie z. B. über das Internet. Über die Checksumme wird die Integrität jedes einzelnen Pakets überprüft, über die Paketnummer kann der Empfänger die einzelnen Pakete wieder in der korrekten Reihenfolge zusammensetzen.
Î
Prozedur: Prozeduren sind wie Funktionen eine Folge von zusammenhängenden Anweisungen, die unter dem Namen der Prozedur aufgerufen werden können. Prozeduren geben aber, im Gegensatz zu Funktionen, keinen Wert zurück. Unter „Funktion“ finden Sie weitere Erläuterungen zur Bedeutung von Prozeduren und zum Unterschied zu Funktionen und Methoden
Î
Quellcode: Wenn Sie ein Programm schreiben, erzeugen Sie zunächst den Quellcode des Programms. Der Quellcode ist also der eigentliche Programmtext. Bei der Kompilierung bzw. beim Interpretieren (siehe Compiler und Interpreter) wird der Quellcode Zeile für Zeile in ausführbaren Code ( Zwischencode oder Maschinencode) übersetzt. Für Programme, die interpretiert werden, muss der Quellcode vorhanden sein. Ist ein Programm kompiliert, wird dieses ohne den Quellcode ausgeführt, in diesem Fall kann der Quellcode also auch fehlen.
Î
Î Î
Î
Î
Referenz: Eine Referenz ist im Prinzip ein Zeiger auf ein Objekt. Die Dereferenzierung einer Referenz ist aber wesentlich einfacher – über den Punkt-Operator – als bei Zeigern. Der Vorteil ist wie bei Zeigern, dass auch gleichzeitig mehrere Referenzen auf ein Objekt zeigen können.
506
$QKDQJ
Sandini Bib
Registrierdatenbank: siehe unter Registry
Î
Registry: Die Registry ist eine Datenbank, in der Windows alle Einstellungen für das System und für die installierten Programme verwaltet. Die Registry wird in zwei Dateien verwaltet, eine Datei enthält die Systemeinstellungen, die für alle Benutzer gelten, eine zweite Datei enthält benutzerspezifische Programm-Einstellungen für jeden in Windows registrierten Benutzer. Schlüsselwort: Als Schlüsselwort wird ein Wort bezeichnet, das in einer Programmiersprache eine festgelegt Bedeutung besitzt. Dazu gehören z. B. die Wörter, mit denen eine Verzweigung eingeleitet wird (meist ist das das Wort if) oder alle Wörter, die die Datentypen der Programmiersprache bezeichnen. SDK: Ein SDK (Software Development Kit) ist eine Sammlung aus Tools, Programmen und Dokumentationen, die bei der Entwicklung von Software hilfreich eingesetzt werden können. Viele Hersteller von Programmiersprachen (besonders Microsoft) bieten meist gleich mehrere SDKs für verschiedene Bereiche an.
Î
Î
Signatur: Der Begriff „Signatur“ steht bei Funktionen, Prozeduren und Methoden für die Argumentliste und den Rückgabedatentyp derselben. Die Übergabearten („By Value“ oder „By Reference“) und die Datentypen der einzelnen Argumente und der Datentyp des Rückgabewerts (falls es sich um eine Funktion handelt) definieren also die Signatur.
Î
Î
SIGSEGV: SIGSEGV steht unter Linux für „Signal Segment Violation“ (Signal einer Speichersegment-Zugriffsverletzung). Damit ist das Signal gemeint, das Linux sendet, wenn ein Programm versucht, auf eine Speicheradresse zuzugreifen, die für das Programm nicht gültig war. Unter Kylix wird ein solches Signal z. B. dann gesendet, wenn ein Programm versucht, ein Objekt zu verwenden, das nicht instanziert ist. SIGWINCH: SIGWINCH steht unter Linux für Signal Window Changed. Dieses Signal sendet Linux an ein Programm, wenn eines seiner Fenster verändert wurde, z. B. weil der Benutzer das Fenster vergrößert oder verkleinert hat. Software Development Kit: siehe unter SDK Subklasse: Wird bei der Vererbung eine Klasse von einer anderen Klasse (einer Superklasse) abgeleitet, bezeichnet man diese als Subklasse.
Î
Superklasse: Als Superklasse wird in der OOP eine Klasse bezeichnet, von der eine so genannte Subklasse über die Vererbung abgeleitet wird. Die Subklasse erbt damit alle Eigenschaften, Methoden und Ereignisse der Superklasse. Eine Superklasse wird auch als Basisklasse bezeichnet.
Î
*ORVVDU
507
Sandini Bib
Syntax: Als „Syntax“ werden die Regeln bezeichnet, nach denen die Worte und Zeichen einer Sprache aneinander gereiht werden müssen, damit ein Compiler oder Interpreter diese versteht. Die Syntax einer Programmiersprache schreibt zum Beispiel vor, dass für arithmetische Berechnungen die Operatoren +, -, * und / verwendet werden und dass diese Berechnungen immer auf der rechten Seite einer Zuweisung stehen.
Î
Î
Syntaxfehler: Ein Syntaxfehler wird durch eine nicht korrekt geschriebene Anweisung im Quelltext verursacht. Die Syntax einer Programmiersprache schreibt vor, wie Anweisungen geschrieben werden müssen. Sind Anweisungen syntaktisch falsch geschrieben, lässt der Compiler oder Interpreter eine Übersetzung des Quellcodes nicht zu und meldet die aufgetretenen Fehler.
Î
Î
Î
Token: Ein Token ist im Allgemeinen ein kleiner Bestandteil einer größeren Einheit. Ein Beispiel dafür sind die einzelnen Zeichen einer Zeichenkette. Aber auch die einzelnen Wörter, aus denen ein Satz oder ein Programm besteht, sind Token. Dieser Begriff wird aber nicht nur bei der Programmierung, sondern auch in anderen Bereichen verwendet. Der Loginname und das Passwort eines Benutzers sind z. B. einzelne „Security Token“, die zu einem Benutzerkonto zusammengefasst werden. Utility: „Utility“ heißt übersetzt „Nützlicher Helfer“, womit bei der Programmierung kleine, aber sehr nützliche Hilfsfunktionen bzw. -klassen gemeint sind. Variable: Variablen werden bei der Programmierung eingesetzt, um Daten zwischenzuspeichern. Das Ergebnis einer Berechnung, das an einer späteren Stelle im Programm weiterverwendet werden soll, wird beispielsweise üblicherweise in einer Variablen gespeichert. Eine Variable besteht prinzipiell aus einem Stück Arbeitsspeicher für die Daten und der Festlegung, wie dieser Speicher ausgewertet werden soll (als Zahl, als Text etc.). Deshalb besitzt eine Variable einen Namen, über den sie angesprochen werden kann, und einen Datentyp.
Î
Vererbung: siehe unter Ableiten. Verteilte Anwendung: Siehe unter Client/Server-Anwendung Windows Scripting Host : Der Windows Scripting Host (WSH) ermöglicht das Ausführen von Scripting-Programmen, die in JavaScript oder VBScript geschrieben sind, direkt auf der Betriebssystemebene. Für ältere Windows-Versionen muss der Windows Scripting Host dazu erst installiert werden, neuere Windows-Versionen haben diesen bereits integriert. Die Anwendung des WSH ist im Prinzip sehr einfach: Sie erzeugen eine Textdatei mit der Endung .JS (JavaScript) oder .VBS
508
$QKDQJ
Sandini Bib
(VBScript) und bringen in dieser Datei ein Scripting-Programm unter. Die Datei können Sie dann z. B. über einen einfachen Doppelklick ausführen. Den WSH können Sie von der Microsoft-Website downloaden. WSH: siehe unter Windows Scripting Host
Î
Zeiger: Ein Zeiger ist eine Variable, die auf einen Speicherbereich zeigt. In diesem Speicherbereich kann ein einfacher Wert, ein Array, eine Struktur oder ein Objekt gespeichert sein (eben alles, was man so speichern kann). Ein Zeiger kann aber auch auf den Startpunkt einer Prozedur oder Funktion zeigen. Zeiger müssen, über spezielle Operatoren, immer dereferenziert werden, damit der Speicherbereich verwendet werden kann. Der Vorteil von Zeigern gegenüber einfachen Variablen ist u. a., dass auch mehrere Zeiger auf denselben Speicherbereich zeigen und diesen damit bearbeiten können. Zeiger sind in der Praxis sehr problematisch und können nur in wenigen Sprachen, wie C++, verwendet werden.
Î
Î
Zwischencode: Programm, das nicht in nativem Maschinencode vorliegt, sondern aus speziellen, CPU-unabhängigen Befehlen besteht. Zwischencode-Programme werden entweder von einem Interpreter oder einem Just-In-Time-Compiler ausgeführt.
Î
Î
A.3 ASCII-Tabelle ISO-8859-1 Abbildung 1.1 zeigt die Zeichen 32 bis 255 der ASCII-Tabelle ISO-88591 in der Windows-Adaption. Beachten Sie, dass die Zeichen 127 bis 159 im ISO-8859-1-Standard nicht darstellbar sind (und deswegen z. B. in HTML-Dokumenten nicht verwendet werden sollten). Windows belegt diese Zeichen allerdings mit einer Sonderbedeutung. Das Euro-Zeichen besitzt z. B. den Code 128. Bei den normalen Zeichen sind die Zeichen 32 und 160 Leerzeichen, wobei dem Zeichen 160 (Non breaking Space) in HTML eine besondere Bedeutung zukommt. Mit diesem Zeichen können Sie in HTML-Dokumenten auch mehrere Leerzeichen hintereinander ausgeben.
509
Sandini Bib
Abbildung A.1: Die ASCII-Tabelle ISO-8859-1 mit Windows-Sonderzeichen
Lesen Sie die Tabelle so, dass Sie in der linken Spalte den Grundwert ermitteln und den Wert dann mit dem Wert in der Überschriftszeile addieren. Die Zeichen 0 bis 31 besitzen eine spezielle Steuerungsfunktion. Die wichtigsten dieser Zeichen zeigt Tabelle A.1. Andere Zeichen wurden früher zur Steuerung des Druckers und werden auch heute noch zur Steuerung der Kommunikation zwischen entfernten Systemen verwendet. Zeichen
Bedeutung
0
Dieser Code steht für „kein Zeichen“. In C und C++ werden z. B. Strings im Speicher mit dem ASCII-Zeichen 0 abgeschlossen.
8
Backspace
9
Tab
10
Das „Line Feed“-Zeichen steht für einen Zeilenumbruch. Dieses Zeichen wird häufig gemeinsam mit dem Zeichen 13 verwendet. Die Reihenfolge dazu ist immer 13, 10.
13
Das „Carriage Return“-Zeichen wird auf einigen Systemen gemeinsam mit dem Zeichen 10 für einen Zeilenumbruch verwendet.
27
Esc
Tabelle A.1: Die wichtigsten Sonderzeichen der ASCII-Tabelle
510
$QKDQJ
Sandini Bib
A.4 Wichtige One Studio 4Tastenkombinationen Projektverwaltung Tasten
Bedeutung
(Strg) (S)
Speichern der aktuellen Datei
(Strg) (2)
Explorer in einem Register anzeigen, in dem dieser nicht sichtbar ist
(Strg) (3)
Quellcode-Editor in einem Register anzeigen, in dem dieser nicht sichtbar ist
Tabelle A.2: Die wichtigen One-Tastenkombinationen zur Verwaltung eines Projekts
Kompilieren und Starten Tasten
Bedeutung
(Strg) (ª) (F9)
Kompilieren des Projekts. Der Compiler kompiliert dabei nur Dateien, die seit dem letzten Kompilieren geändert wurden.
(Strg) (ª) (F11)
Erzeugen des Projekts. Der Compiler kompiliert dabei alle Dateien, auch wenn diese seit dem letzten Kompilieren nicht geändert wurden.
(F9)
Kompilieren der aktuell markierten Quellcodedatei oder aller Dateien in einem verbundenen Dateiordner (ohne die in Unterordnern gespeicherten)
(ª) (F9)
Kompilieren aller Quellcodedateien eines verbundenen Dateiordners inklusive den in Unterverzeichnissen gespeicherten
(F11)
Erzeugen der aktuell markierten Quellcodedatei oder aller Dateien in einem verbundenen Dateiordner (ohne die in Unterordnern gespeicherten)
(ª) (F11)
Erzeugen aller Quellcodedateien eines verbundenen Dateiordners inklusive den in Unterverzeichnissen gespeicherten
(Strg) (ª) (F5)
Ausführen des Projekts inklusive Kompilieren zum Debuggen
(Strg) (ª) (F6)
Ausführen des Projekts inklusive Kompilieren ohne Debuggen
(Alt) (F5)
Ausführen der aktuell ausgewählten Klasse
Tabelle A.3: Die wichtigsten One-Tastenkombinationen zum Kompilieren und Starten eines Programms
511
Sandini Bib
Debuggen Tasten
Bedeutung
(Strg) (ª) (F5)
Ausführen des Projekts inklusive Kompilieren zum Debuggen
(Alt) (F5)
Ausführen der aktuell ausgewählten Klasse
(F8)
Schrittweises Ausführen des Programms. (F8) springt im Gegensatz zu (F7) nicht in aufgerufene Funktionen/Methoden hinein.
(F7)
Schrittweises Ausführen des Programms. (F7) springt im Gegensatz zu (F8) auch in aufgerufene Funktionen/Methoden hinein.
(F4)
Programm weiter bis zur Cursorposition ausführen
(Alt) (ª) (F7)
Schrittweises Ausführen des Programms bis zu der Quellcodezeile, die dem Aufruf der aktuellen Methode folgt (Heraussprung aus einer Methode)
(ª) (F5)
Ausführung des Programms beenden
(Strg) (ª) (F7)
Überwachungsausdruck (Watch) anlegen
(Strg) (F5)
Debugger-Fenster anzeigen
Tabelle A.4: Wichtige Tastenkombinationen beim Debuggen in One
A.5 Wichtige Kylix/DelphiTastenkombinationen Projektverwaltung Tasten
Bedeutung
(Strg) (S) (ª) (Strg) (S)
Speichern der aktuellen Datei Speichern aller Dateien
(Strg) (Alt) (F11)
Projektverwaltung anzeigen
(Strg) (F12)
Units-Fenster anzeigen. Dieses Fenster listet alle Units des Projekts auf . Über einen Doppelklick erreichen Sie eine Unit damit sehr schnell.
(ª) (F12)
Formulare-Fenster anzeigen. Dieses Fenster listet ähnlich dem Units-Fenster alle Formulare des Projekts auf.
(F12)
Umschalten zwischen der Unit- und der Formularansicht bei Formularen
(Strg) (¢)
Wenn der Eingabecursor auf dem Dateinamen einer in einer uses-Anweisung eingebundenen Datei steht: öffnet die Datei im Editor
Tabelle A.5: Die wichtigen Delphi/Kylix-Tastenkombinationen zur Verwaltung eines Projekts
512
$QKDQJ
Sandini Bib
Kompilieren und Starten Tasten
Bedeutung
(Strg) (F9)
Kompilieren der aktuellen Quellcodedatei.
(F9)
Ausführen des Programms
Tabelle A.6: Die wichtigsten Delphi/Kylix-Tastenkombinationen zum Kompilieren und Starten eines Programms
Debuggen Tasten
Bedeutung
(F9)
Ausführen des Programms
(F8)
Schrittweises Ausführen des Programms. (F8) springt im Gegensatz zu (F7) nicht in aufgerufene Funktionen/ Methoden hinein.
(F7)
Schrittweises Ausführen des Programms. (F7) springt im Gegensatz zu (F8) auch in aufgerufene Funktionen/ Methoden hinein.
(ª) (F7)
Schrittweises Ausführen des Programms bis zur nächsten Quellcodezeile.
(F4)
Programm weiter bis zur Cursorposition ausführen
(Strg) (F2)
Ausführung des Programms beenden
(Strg) (F5)
Überwachungsausdruck (Watch) anlegen
(F11)
Objektinspektor anzeigen
(Strg) (Alt) (b)
Haltepunkte-Fenster anzeigen
(Strg) (Alt) (s)
Aufruf-Stack anzeigen
(Strg) (Alt) (w)
Überwachte Ausdrücke anzeigen
Tabelle A.7: Wichtige Delphi/Kylix-Tastenkombinationen beim Debuggen
513
Sandini Bib
Sandini Bib
S
Stichwortverzeichnis
!-Operator 266 &&-Operator 266 ||-Operator 266
Abfragen von Daten (JDBC) 475 Abonnieren einer Newsgroup 39 Addition 222 Algorithmen 166 Ändern von Daten (JDBC) 480 and-Operator 266 Anfügen von Daten (JDBC) 480 Annahme 279 ANSI-Tabelle 135 Anweisungen Blöcke 217 Elementare 213 Endezeichen 215 Kommentare 219 umbrechen 215 Anweisungsblöcke 217 Anwendungen Client/Server 156 Internet 157 Komponentenbasierte 152 Scripting 157 Verteilte 155 Anwendungsarchitekturen 150 Arbeitsspeicher 127 Arithmetische Ausdrücke 222 Arithmetische Operatoren 222 Arrays 410 ASCII-Tabelle 135 ASCII-Zeichenketten 197
le
n e rn
ASP 166 Assembler 121, 142 Auflistungen 416 Aufrufer 312 Ausdrücke Arithmetische 222 Vergleichs- 262 Zuweisungs- 65 Auskommentieren 220 Ausnahmen 66 abfangen 250 debuggen 245 erzeugen 398 Ausnahmen ignorieren in Delphi/Kylix 252
Basisdatum 199 begin-Schlüsselwort 218 Bereichsüberprüfung 207 Betriebssystem 124 Beziehungen (Datenbanken) 465 Bibliotheken 231, 316 in Delphi/Kylix einbinden 320 in ein Programm einbinden 235 in Java einbinden 322 in Sun ONE Studio 4 einbinden 324
Bibliothekspfad 320 Binäre Daten 138 BIOS 123 Bit 130 Boolean-Datentyp 199 Boole, George 199
515
Sandini Bib
break-Anweisung 286, 296 BufferedReader-Klasse 425 BufferedWriter-Klasse 427 Byte 130 Byte-Datentyp 191
C 160 Call By Reference 310 Cannot resolve symbol 322, 323, 390, 419
Cardinal-Datentyp 191 Carriage Return 137 Case-Verzweigung 294 Char-Datentyp 197 Class-Klasse (Java)_ 473 CLASSPATH-Variable 236, 324 Client/Server-Anwendungen 156 Collections siehe Auflistungen Compiler 143 Compilerfehler Cannot resolve symbol 323, 390, 419
Illegal escape character 211 Illegales Zeichen in Eingabedatei 220
Incompatible Types 201, 202, 225, 267
Missing operator or semicolon 210 non-static variable ... 342 Operator not applicable to this operand type 267 operator ... cannot be applied to ... 263, 271
possible loss of precision 202 Undeclared identifier 384 ... has private access ... 386 Connection-Klasse (Java) 475 Constructor-Schlüsselwort 392 Controller 122 CPU 121 Familien 144 Currency-Datentyp 192 Cursor 477 C++ 160
516
6WLFKZRUWYHU]HLFKQLV
DataInputStream-Klasse 326 Date-Datentyp 199 Dateien Begriffsklärung 127 binäre 138 kombinierte 138 mit wahlfreiem Zugriff 423 Textdateien 137 Textdateien lesen 424 Textdateien schreiben 427 verwalten 423 Dateinamenerweiterung Ausblendung abschalten 86 Daten 187 ANSI-Tabelle 135 ASCII-Tabelle 135 Binäre 138 Datentypen 187 in Java abfragen 475 Speichern im Computer 129 Unicode-Tabelle 137 Zahlendarstellung 132 Zeichendarstellung 134 Daten speichern Arrays 410 Auflistungen 416 in Datenbanken 453 Datenbanken Grundlagen 454 Primärschlüssel 463 Datenbanksysteme 454 objektorientierte 456 Relationale 457 Datenkapselung 396 Datensätze in Java abfragen 475 in Java ändern 480 in Java anfügen 480 in Java durchgehen 477 in Java löschen 481 Datentypen 188 Boolean 199 Byte 191 Cardinal 191 Char 197 Currency 192
Sandini Bib
Datumswerte 199 Double 192 Extended 192 Fest- und Fließkomma 192 float 192 Ganzzahl 190 Generische 196 int 191 Integer 190, 191 Int64 191 Konvertierungen 200 Literale für Strings 210 Literale für Zahlen 209 Literale für Zeichen 211 long 191 Longword 191 numerische 190 Shortint 191 Single 192 Smallint 191 String 197 Überläufe 206 Word 191 Zeichenketten 197 Datum 188 Datumswerte 199 DayOf-Funktion 311 DBMS 455 DBS 455 Debugging 49 Geschichte 49 Grundlagen 245 in Delphi und Kylix 249 in Sun ONE Studio 4 248 DecodeDate-Prozedur 311 default-Label 296 Deklarieren von Funktionen 300 DELETE-Anweisung (SQL) 471 deleteRow-Methode (Java) 481 Delphi Arrays 410 Ausnahmen abfangen 251 Ausnahmen ignorieren 252 Automatische Speicherung 71 Case-Verzweigung 294 Debuggen 249 Dokumentation 34
Fest- und Fließkommadatentypen 192
Funktionen deklarieren 300 Globale Variablen 328 If-Verzweigung 289 Integer-Datentypen 190 Klassen deklarieren 356 Kompilieren 57 Konsolenanwendungen entwickeln 51 Logische Operatoren 266 Normale Anwendung entwickeln 69
Prozeduren deklarieren 307 Schleifen 276 String-Datentypen 198 Vergleichsoperatoren 263 destructor-Schlüsselwort 395 Destruktoren 394 Dezimales Zahlensystem 131 Dialog für Meldungen 434 Division 222 Ganzzahl 224, 226 Restwert 226 div-Operator 222 Dokumentation 34 Delphi 34 Java 35 Kylix 34 do-Schleife 282 Double-Datentyp 192 Duales Zahlensystem 131
Eigenschaften bei Steuerelementen 74 in Klassen 357 Eingabeaufforderung 22 Endlosschleife 278 end-Schlüsselwort 218 ERangeError 412 Ereignisbehandlungsmethoden 78 Ereignisorientiertes Programmieren 77 Escape-Sequenzen 211 EVA-Prinzip 120
6WLFKZRUWYHU]HLFKQLV
517
Sandini Bib
Exceptions siehe Ausnahmen executeQuery-Methode (Java) 476 executeUpdate-Methode (Java) 481 Extended-Datentyp 192
GPL 26 Grace Murray Hopper 49
)
Hashcode 418 Hashtable-Klasse 417 Hertz 122 Hexadezimales Zahlensystem 133 Hopper, Grace Murray 49 HTML 158
Fakultät berechnen 217 Fehler logische 67 Syntaxfehler 57 Fehlerbeseitigung in Delphi/Kylix-Programmen 57 in Java-Programmen 90 Festkommazahlen 132, 190, 192 FileWriter-Klasse 427 first-Methode (Java) 479 Fließkommazahlen 133, 190, 192 Float-Datentyp 192 Floating point overflow 208 FloatToStr-Funktion 203 FormatFloat-Funktion 66 forName-Methode (Java) 473 For-Schleife 284 Frederic Vester 29 Fremdschlüssel 465 Funktionen 298 aufrufen 238 Call By Reference 310 Funktionsbibliotheken 231 Fußgesteuerte Schleifen 282
* Ganzzahl-Division 222 Delphi, Kylix 226 Java 224 Ganzzahlen 132 Garbage Collector 368 Genauigkeit 133 General Public License 26 Generische Datentypen 196 George Boole 199 Globale Variablen 328 Google im Newsgroup-Archiv suchen 41 Webseiten suchen 37 Goto 260
518
6WLFKZRUWYHU]HLFKQLV
+
, If-Verzweigung 288 Illegal escape character 211 Illegales Zeichen in Eingabedatei 220 Implementation-Abschnitt 317 Implementierung 317 Importieren von Paketen (Java) 237 Incompatible types 201, 202, 225, 267 Individuelle Programme 27 Infinity 208 Informationen suchen im Internet 36 in der Dokumentation 34 inherited-Schlüsselwort 392 Inkompatible Typen siehe Incompatible types INSERT INTO-Anweisung (SQL) 467 insertRow-Methode (Java) 480 Instanz 234, 342 int-Datentyp 191 Integer-Datentyp 191 Integer-Zahlen 190 interface-Abschnitt 318 Internet Informationen suchen 36 Internetanwendungen 157 Interpreter 145 IntToStr-Funktion 203 Int64-Datentyp 191 Invalid floating point operation 208 isAfterLast-Methode (Java) 477 ISO 135 ISO-Latin-1 135 ISO-8859-1 135
Sandini Bib
Java 212 Arrays 414 Case-Verzweigung 296 Compilerfehler auswerten 90 Debuggen 248 Dokumentation 35 Einfache Programme schreiben 85 Fest- und Fließkommadatentypen 192
Funktionen deklarieren 304 Globale Variablen 334 If-Verzweigung 290 Integer-Datentypen 190 Kapselung 397 Klassen deklarieren 358 Kompilieren unter Linux 89 Kompilieren unter Windows 88 Konsolenanwendungen 85 Konstruktoren 390 Laufzeitumgebung 148 Layout-Manager 110 Logische Operatoren 266 Pakete 236 Programme starten 91 Prozeduren deklarieren 307 Referenzargumente 315 Schleifen 276 Spezielle Zuweisungsoperatoren 230
String-Datentypen 198 Strings vergleichen 265 Vergleichsoperatoren 263 Java-Applets 160 JavaScript 165 JDBC 472 Treiber laden 473 JDialog 435 Just-In-Time-Compiler 149
. Kapselung 396 Klassen Destruktoren 394 Eigenschaften 357 Entwurfs-Prinzipien 370
in Delphi/Kylix deklarieren 356 in Java deklarieren 358 Instanzen erzeugen 360 Instanzen in Delphi/Kylix erzeugen 363 Instanzen in Java erzeugen 366 Kapselung 396 Konstruktoren 389 Methoden 357 Methoden überladen 386 Private und öffentliche Elemente 382
Referenzen 360 Vererbung 404 Klassenbibliotheken 231 Kommentare 219 Kompilieren Delphi und Kylix 57 Java 88 Komponentenbasierte Anwendungen 152 Konsolenanwendungen in Delphi/Kylix 51, 54 in Java 85 Konstanten 188, 209 für Zeichen 211 für Zeichenketten 210 Konstruktoren 389 Kontextsensitive Hilfe 36 Konventionen 20 für Variablennamen 184 Konvertierungen 200 Kylix Arrays 410 Ausnahmen abfangen 251 Ausnahmen ignorieren 252 Automatische Speicherung 71 Case-Verzweigung 294 Debuggen 249 Dokumentation 34 Fest- und Fließkommadatentypen 192
Funktionen deklarieren 300 Globale Variablen 328 If-Verzweigung 289 Integer-Datentypen 190 Klassen deklarieren 356
6WLFKZRUWYHU]HLFKQLV
519
Sandini Bib
Kompilieren 57 Konsolenanwendungen entwickeln 51 Logische Operatoren 266 Normale Anwendung entwickeln 69 Prozeduren deklarieren 307 Schleifen 276 String-Datentypen 198 Strukturen 212 Vergleichsoperatoren 263
/ Label 261 last-Methode (Java) 479 Laufzeitbibliothek 163 Laufzeitfehler 67 Laufzeitfehler 215 207 Laufzeitumgebung 148 Layout-Manager 110 Lebensdauer (Variablen) 334 Lerntheorie 29 Line Feed 137 Literale 209 Strings 210 Zahlen 209 Zeichen 211 Lochkarten 142 Logische Fehler 67, 245 Long Double-Datentyp 192 long-Datentyp 191 Longint-Datentyp 191 Longword-Datentyp 191 Löschen von Daten (JDBC) 481
0 Makros 150 Maschinensprache 121, 142 Meldungen in einem eigenen Dialog ausgeben 434 Methoden aufrufen 238 deklarieren 357 in Klassenbibliotheken 234 überladen 386 überschreiben 405
520
6WLFKZRUWYHU]HLFKQLV
Missing operator or semicolon 210 mod-Operator 222 Module 299 Modulo-Operator 222, 226 MonthOf-Funktion 311 moveToInsertRow-Method (Java) 480 Multiplikation 222 MySQL 460
1 newInstance-Methode (Java)_ 473 new-Operator 234 Newsgroups 39 über Google suchen 41 Nibble 134 nil 363, 421 NoClassDefFoundError 91, 106, 116 non-static variable ... 342 Not-Operator 266, 271 null 363, 421 Nullreferenzen 363, 421 NumberFormat-Klasse 327
2 Object Pascal 163 Object-Basisklasse 367 Objekt 342 Objektorientierte Datenbanksysteme 456 Objektorientierte Programmierung siehe OOP Oder-Verknüpfung 266 OOP 340 Destruktoren 394 Entwurfsrichtlinien 370 Instanzen 360 Kapselung 396 Klassen 356 Konstruktoren 389 Sichtbarkeit 382 Vererbung 404 Vorteile 351 Operanden 222 Operator not applicable to this operand type 267 operator ... cannot be applied to ... 263, 271
Sandini Bib
Operatoren 222 and, && 266 arithmetische 222 für Vergleichsausdrücke 263 logische 266 Modulo 226 not, ! 271 or, || 268 Reihenfolge 229 relationale 263 Zuweisungs- 263 or-Operator 266 overload-Schlüsselwort 388
Projekte Delphi, Kylix 55 externe in One Studio 4 integrieren 106 in One Studio 4 94 Prozeduren aufrufen 238 Call By Reference 310 deklarieren 307 Prozessor 121 Taktrate 122 Pseudocode 168 Public 382
!
"
Pakete (Java) 100, 236 importieren 237 in One Studio 4 105 parse-Methode 327 Path-Variable 25 Performance 286 messen 287 possible loss of precision 202 Postfix-Notation 229 Präfix-Notation 229 previous-Methode (Java) 479 Primärschlüssel 463 Private 382 procedure 307 Programme aus Computersicht 127 CPU-spezifische 144 für das Internet 157 Individuelle 27 komponentenbasierte 152 Scripting 157 Programme starten Delphi, Kylix 58 Java 91 One Studio 4 104 Programmierung ereignisorientierte 77 objektorientierte 339 Spaghetti 262 unstrukturierte 260
Quellcode 48
# raise-Anweisung 400 Random-Dateien 423 readln-Prozedur 63 Real-Datentyp 192 Record 212 Reelle Zahlen 190 Referenzargumente 310 Referenzen bei Objekten 360 bei Referenzargumenten 315 Relationale Datenbanksysteme 457 Relationale Operatoren 263 repeat-Schleife 282 Restwert-Division 222, 226 result-Variable 301, 302 return-Anweisung 304
$ Schleifen 169, 276 abbrechen 286 for- 284 Fußgesteuerte 282 Kopfgesteuerte 276 Scripting-Programme 157 SELECT-Anweisung (SQL) 469 self-Referenz 379 Shell 22
6WLFKZRUWYHU]HLFKQLV
521
Sandini Bib
Short Integer 191 short-Datentyp 191 Shortint-Datentyp 191 Sichtbarkeit 382 Single-Datentyp 192 Small Integer 191 Smallint-Datentyp 191 Software-Architekturen 150 Spaghetti-Programmierung 262 Speicheradressen 140 SQL 458 Beziehungen definieren 465 Daten abfragen 469 Daten ändern 470 Daten löschen 471 Datenbank löschen 467 Datenbanken erzeugen 460 Datensätze anfügen 467 Tabellen anlegen 463 Standardkonstruktor 391 Statement-Klasse (Java) 475 Steuerelemente auf Formularen ablegen 72, 110 Begriffsdefinition 72 Eigenschaften einstellen 74, 111 Stream 326 String-Datentyp 197 Strings als Literal 210 ASCII und Unicode 197 Datentypen 197 Escape-Sequenzen 211 vergleichen 264 zerlegen 441 StrToFloatDef-Funktion 203, 243 StrToFloat-Funktion 203 StrToIntDef-Funktion 203 StrToInt-Funktion 203 Structured Query Language siehe SQL Strukturen 212 Strukturierte Programmierung Funktionen und Prozeduren 231 Probleme 347 Suchen im Internet 36 in der Dokumentation 34 Suchmaschinen 37 Superklasse 405
522
6WLFKZRUWYHU]HLFKQLV
Super-Schlüsselwort 405 switch-Verzweigung 296 Syntax 48 Systemvoraussetzungen Linux 16 Windows 16
% Taktrate 122 Tastenkombinationen in der KDE für Kylix anpassen 50 TDateTime-Datentyp 199 Textdateien 137 lesen 424 schreiben 427 Text-Dokumente 138 this-Referenz 379 Threads bei Newsgroups 39 throw-Anweisung 399 TObject-Basisklasse 367 Treiber 124 Treiber laden (JDBC) 473 Typecast 202 type-Schlüsselwort 357 Typumwandlung siehe Konvertierung
& Überladen von Methoden 386 Überläufe 206, 225 Überlaufprüfung 207 Überschreiben von Methoden 405 Überwachungsausdrücke 248 Umgebungsvariablen 24 Unbekannter Softwarefehler 68 Undeclared identifier 384 Und-Verknüpfung 266 Unicode 137 Unicode-Zeichenketten 197 Unit 231 Unsigned Integer 191 Unsigned Small Integer 191 Unstrukturierte Programmierung 260 UPDATE-Anweisung (SQL) 470 updateRow-Methode (Java) 480 uses-Anweisung 235
Sandini Bib
9
;
Variablen 173 deklarieren 182 Einführung 63 Globale 328 in Java 184 in Modulen 328 Lebensdauer 334 Lokale 328 var-Schlüsselwort 185 VBScript 166 Vererbung 353, 404 Vergleichsausdrücke 262 auflösen 270 Komplexe 266 Verteilte Anwendungen 155 Verzweigungen 170, 288 Case 294 if 288 Vester, Frederic 29 Virtuelle Maschine 148 void 307
X86-CPU-Familie 144
: Wagenrücklauf 137 wasNull-Methode (Java) 479 Watch 248 Webserver 158 while-Schleife 277 Wide Char 198 Wiederverwendung 297 Windows Scripting Host 166 Word-Datentyp 191 WSH 166
< YearOf-Funktion 311
= Zahlen als Literal 209 Duale 130 Fest- und Fließkomma 192 Festkomma 132 Fließkomma 133 Ganze 132, 190 Hexadezimale 133 Zahlendarstellung 132 Zahlensysteme Dezimale 131 Duale 131 Hexadezimale 133 Zeichendarstellung 134 Zeichenketten siehe Strings Zeilenvorschub 137 Zeitmessung 287 Zuweisungen 65, 221 Zuweisungsoperatoren Java und Object Pascal 263 spezielle in Java 230 Zwischencode-Programme 146
6WLFKZRUWYHU]HLFKQLV
523
Sandini Bib
Sandini Bib
Sun Microsystems, Inc. Binary Code License Agreement READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA PACKAGE, YOU AGREE TO THE TERMS OF THIS AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE "DECLINE" BUTTON AT THE END OF THIS AGREEMENT. 1. LICENSE TO USE. Sun grants you a non-exclusive and non-transferable license for the internal use only of the accompanying software and documentation and any error corrections provided by Sun (collectively "Software"), by the number of users and the class of computer hardware for which the corresponding fee has been paid. 2. RESTRICTIONS. Software is confidential and copyrighted. Title to Software and all associated intellectual property rights is retained by Sun and/or its licensors. Except as specifically authorized in any Supplemental License Terms, you may not make copies of Software, other than a single copy of Software for archival purposes. Unless enforcement is prohibited by applicable law, you may not modify, decompile, or reverse engineer Software. You acknowledge that Software is not designed, licensed or intended for use in the design, construction, operation or maintenance of any nuclear facility. Sun disclaims any express or implied warranty of fitness for such uses. No right, title or interest in or to any trademark, service mark, logo or trade name of Sun or its licensors is granted under this Agreement. 3. LIMITED WARRANTY. Sun warrants to you that for a period of ninety (90) days from the date of purchase, as evidenced by a copy of the receipt, the media on which Software is furnished (if any) will be free of defects in materials and workmanship under normal use. Except for the foregoing, Software is provided "AS IS". Your exclusive remedy and Sun's entire liability under this limited warranty will be at Sun's option to replace Software media or refund the fee paid for Software. 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE DISCLAIMERS ARE HELD TO BE LEGALLY INVALID. 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. In no event will Sun's liability to you, whether in contract, tort (including negligence), or otherwise, exceed the amount paid by you for Software under this Agreement. The foregoing limitations will apply even if the above stated warranty fails of its essential purpose. 6. Termination. This Agreement is effective until terminated. You may terminate this Agreement at any time by destroying all copies of Software. This Agreement will terminate immediately without notice from Sun if you fail to comply with any provision of this Agreement. Upon Termination, you must destroy all copies of Software. 7. Export Regulations. All Software and technical data delivered under this Agreement are subject to US export control laws and may be subject to export or import regulations in other countries. You agree to comply strictly with all such laws and regulations and acknowledge that you have the responsibility to obtain such licenses to export, re-export, or import as may be required after delivery to you. 8. U.S. Government Restricted Rights. If Software is being acquired by or on behalf of the U.S. Government or by a U.S. Government prime contractor or subcontractor (at any tier), then the Government's rights in Software and accompanying documentation will be only as set forth in this Agreement; this is in accordance with 48 CFR 227.7201 through 227.7202-4 (for Department of Defense (DOD) acquisitions) and with 48 CFR 2.101 and 12.212 (for non-DOD acquisitions). 9. Governing Law. Any action related to this Agreement will be governed by California law and controlling U.S. federal law. No choice of law rules of any jurisdiction will apply.
525
Sandini Bib
10. Severability. If any provision of this Agreement is held to be unenforceable, this Agreement will remain in effect with the provision omitted, unless omission would frustrate the intent of the parties, in which case this Agreement will immediately terminate. 11. Integration. This Agreement is the entire agreement between you and Sun relating to its subject matter. It supersedes all prior or contemporaneous oral or written communications, proposals, representations and warranties and prevails over any conflicting or additional terms of any quote, order, acknowledgment, or other communication between the parties relating to its subject matter during the term of this Agreement. No modification of this Agreement will be binding, unless in writing and signed by an authorized representative of each party.
JAVATM 2 SOFTWARE DEVELOPMENT KIT (J2SDK), STANDARD EDITION, VERSION 1.4.X SUPPLEMENTAL LICENSE TERMS These supplemental license terms ("Supplemental Terms") add to or modify the terms of the Binary Code License Agreement (collectively, the "Agreement"). Capitalized terms not defined in these Supplemental Terms shall have the same meanings ascribed to them in the Agreement. These Supplemental Terms shall supersede any inconsistent or conflicting terms in the Agreement, or in any license contained within the Software. 1. Software Internal Use and Development License Grant. Subject to the terms and conditions of this Agreement, including, but not limited to Section 4 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a non-exclusive, non-transferable, limited license to reproduce internally and use internally the binary form of the Software complete and unmodified for the sole purpose of designing, developing and testing your Java applets and applications intended to run on the Java platform ("Programs"). 2. License to Distribute Software. Subject to the terms and conditions of this Agreement, including, but not limited to Section 4 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a nonexclusive, non-transferable, limited license to reproduce and distribute the Software, provided that (i) you distribute the Software complete and unmodified (unless otherwise specified in the applicable README file) and only bundled as part of, and for the sole purpose of running, your Programs, (ii) the Programs add significant and primary functionality to the Software, (iii) you do not distribute additional software intended to replace any component(s) of the Software (unless otherwise specified in the applicable README file), (iv) you do not remove or alter any proprietary legends or notices contained in the Software, (v) you only distribute the Software subject to a license agreement that protects Sun's interests consistent with the terms contained in this Agreement, and (vi) you agree to defend and indemnify Sun and its licensors from and against any damages, costs, liabilities, settlement amounts and/or expenses (including attorneys' fees) incurred in connection with any claim, lawsuit or action by any third party that arises or results from the use or distribution of any and all Programs and/or Software. (vi) include the following statement as part of product documentation (whether hard copy or electronic), as a part of a copyright page or proprietary rights notice page, in an "About" box or in any other form reasonably designed to make the statement visible to users of the Software: "This product includes code licensed from RSA Security, Inc.", and (vii) include the statement, "Some portions licensed from IBM are available at http://oss.software.ibm.com/icu4j/". 3. License to Distribute Redistributables. Subject to the terms and conditions of this Agreement, including but not limited to Section 4 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a non-exclusive, non-transferable, limited license to reproduce and distribute those files specifically identified as redistributable in the Software "README" file ("Redistributables") provided that: (i) you distribute the Redistributables complete and unmodified (unless otherwise specified in the applicable README file), and only bundled as part of Programs, (ii) you do not distribute additional software intended to supersede any component(s) of the Redistributables (unless otherwise specified in the applicable README file), (iii) you do not remove or alter any proprietary legends or notices contained in or on the Redistributables, (iv) you only distribute the Redistributables pursuant to a license agreement that protects Sun's interests consistent with the terms contained in the Agreement, (v) you agree to defend and indemnify Sun and its licensors from and against any damages, costs, liabilities, settlement amounts and/or expenses (including attorneys' fees) incurred in connection with any claim, lawsuit or action by any third party that arises or results from the use or distribution of any and all Programs and/or Software, (vi) include the following statement as part of product documentation (whether hard copy or electronic), as a part of a copyright page or proprietary rights notice page, in an "About" box or in any other form reasonably designed to make the statement visible to users of the Software: "This product includes code licensed from RSA Security, Inc.", and (vii) include the statement, "Some portions licensed from IBM are available at http://oss.software.ibm.com/icu4j/".
526
Sandini Bib
4. Java Technology Restrictions. You may not modify the Java Platform Interface ("JPI", identified as classes contained within the "java" package or any subpackages of the "java" package), by creating additional classes within the JPI or otherwise causing the addition to or modification of the classes in the JPI. In the event that you create an additional class and associated API(s) which (i) extends the functionality of the Java platform, and (ii) is exposed to third party software developers for the purpose of developing additional software which invokes such additional API, you must promptly publish broadly an accurate specification for such API for free use by all developers. You may not create, or authorize your licensees to create, additional classes, interfaces, or subpackages that are in any way identified as "java", "javax", "sun" or similar convention as specified by Sun in any naming convention designation. 5. Notice of Automatic Software Updates from Sun. You acknowledge that the Software may automatically download, install, and execute applets, applications, software extensions, and updated versions of the Software from Sun ("Software Updates"), which may require you to accept updated terms and conditions for installation. If additional terms and conditions are not presented on installation, the Software Updates will be considered part of the Software and subject to the terms and conditions of the Agreement. 6. Notice of Automatic Downloads. You acknowledge that, by your use of the Software and/or by requesting services that require use of the Software, the Software may automatically download, install, and execute software applications from sources other than Sun ("Other Software"). Sun makes no representations of a relationship of any kind to licensors of Other Software. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO THE USE OF OR INABILITY TO USE OTHER SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. Trademarks and Logos. You acknowledge and agree as between you and Sun that Sun owns the SUN, SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks and all SUN, SOLARIS, JAVA, JINI, FORTE, and iPLANET-related trademarks, service marks, logos and other brand designations ("Sun Marks"), and you agree to comply with the Sun Trademark and Logo Usage Requirements currently located at http://www.sun.com/ policies/trademarks. Any use you make of the Sun Marks inures to Sun's benefit. 8. Source Code. Software may contain source code that is provided solely for reference purposes pursuant to the terms of this Agreement. Source code may not be redistributed unless expressly provided for in this Agreement. 9. Termination for Infringement. Either party may terminate this Agreement immediately should any Software become, or in either party's opinion be likely to become, the subject of a claim of infringement of any intellectual property right. For inquiries please contact: Sun Microsystems, Inc. 901 San Antonio Road, Palo Alto, California 94303 (LFI#111374/Form ID#011801)
FORTE(TM) FOR JAVA(TM) 4, COMMUNITY EDITION SUPPLEMENTAL LICENSE TERMS These supplemental license terms ("Supplemental Terms") add to or modify the terms of the Binary Code License Agreement (collectively, the "Agreement"). Capitalized terms not defined in these Supplemental Terms shall have the same meanings ascribed to them in the Agreement. These Supplemental Terms shall supersede any inconsistent or conflicting terms in the Agreement, or in any license contained within the Software. 1. Software Internal Use and Development License Grant. Subject to the terms and conditions of this Agreement, including, but not limited to Section 4 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a non-exclusive, non-transferable, limited license to reproduce internally and use internally the binary form of the Software complete and unmodified for the sole purpose of designing, developing and testing your applets and applications ("Programs"). To the extent that you are designing, developing and testing Java applets and applications for a particular version of the Java platform, any executable output generated by a compiler that is contained in the Software must (a) only be compiled from source code that conforms to the corresponding version of the OEM Java Language Specification; (b) be in the class file format defined by the corresponding version of the OEM Java Virtual Machine Specification; and (c) execute properly on a reference runtime, as specified by Sun, associated with such version of the Java platform. 2. License to Distribute Software. Subject to the terms and conditions of this Agreement, including, but not limited to Section 4 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a non-exclusive, non-transferable, limited license to reproduce and distribute the Software in binary code form only, provided that (i) you distribute the Software complete and unmodified, (ii) you do not distribute additional
527
Sandini Bib
software intended to replace any component(s) of the Software, (iii) if you are distributing Java applets and applications for a particular version of the Java platform, any executable output generated by a compiler that is contained in the Software must (a) only be compiled from source code that conforms to the corresponding version of the OEM Java Language Specification; (b) be in the class file format defined by the corresponding version of the OEM Java Virtual Machine Specification; and (c) execute properly on a reference runtime, as specified by Sun, associated with such version of the Java platform, (iv) you do not remove or alter any proprietary legends or notices contained in the Software, (v) you only distribute the Software subject to a license agreement that protects Sun's interests consistent with the terms contained in this Agreement, and (vi) you agree to defend and indemnify Sun and its licensors from and against any damages, costs, liabilities, settlement amounts and/or expenses (including attorneys' fees) incurred in connection with any claim, lawsuit or action by any third party that arises or results from the use or distribution of any and all Programs and/or Software. 3. License to Distribute Redistributables. Subject to the terms and conditions of this Agreement, including but not limited to Section 4 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a non-exclusive, non-transferable, limited license to reproduce and distribute the binary form of those files specifically identified as redistributable in the Software "RELEASE NOTES" file ("Redistributables") provided that: (i) you distribute the Redistributables complete and unmodified (unless otherwise specified in the applicable RELEASE NOTES file), and only bundled as part of Programs, (ii) you do not distribute additional software intended to supersede any component(s) of the Redistributables, (iii) you do not remove or alter any proprietary legends or notices contained in or on the Redistributables, (iv) if you are distributing Java applets and applications for a particular version of the Java platform, any executable output generated by a compiler that is contained in the Software must (a) only be compiled from source code that conforms to the corresponding version of the OEM Java Language Specification; (b) be in the class file format defined by the corresponding version of the OEM Java Virtual Machine Specification; and (c) execute properly on a reference runtime, as specified by Sun, associated with such version of the Java platform, (v) you only distribute the Redistributables pursuant to a license agreement that protects Sun's interests consistent with the terms contained in the Agreement, and (v) you agree to defend and indemnify Sun and its licensors from and against any damages, costs, liabilities, settlement amounts and/or expenses (including attorneys' fees) incurred in connection with any claim, lawsuit or action by any third party that arises or results from the use or distribution of any and all Programs and/or Software. 4. Java Technology Restrictions. You may not modify the Java Platform Interface ("JPI", identified as classes contained within the "java" package or any subpackages of the "java" package), by creating additional classes within the JPI or otherwise causing the addition to or modification of the classes in the JPI. In the event that you create an additional class and associated API(s) which (i) extends the functionality of the Java platform, and (ii) is exposed to third party software developers for the purpose of developing additional software which invokes such additional API, you must promptly publish broadly an accurate specification for such API for free use by all developers. You may not create, or authorize your licensees to create, additional classes, interfaces, or subpackages that are in any way identified as "java", "javax", "sun" or similar convention as specified by Sun in any naming convention designation. 5. Java Runtime Availability. Refer to the appropriate version of the Java Runtime Environment binary code license (currently located at http://www.java.sun.com/jdk/index.html) for the availability of runtime code which may be distributed with Java applets and applications. 6. Trademarks and Logos. You acknowledge and agree as between you and Sun that Sun owns the SUN, SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks and all SUN, SOLARIS, JAVA, JINI, FORTE, and iPLANETrelated trademarks, service marks, logos and other brand designations ("Sun Marks"), and you agree to comply with the Sun Trademark and Logo Usage Requirements currently located at http://www.sun.com/policies/trademarks. Any use you make of the Sun Marks inures to Sun's benefit. 7. Source Code. Software may contain source code that is provided solely for reference purposes pursuant to the terms of this Agreement. Source code may not be redistributed unless expressly provided for in this Agreement. 8. Termination for Infringement. Either party may terminate this Agreement immediately should any Software become, or in either party's opinion be likely to become, the subject of a claim of infringement of any intellectual property right. For inquiries please contact: Sun Microsystems, Inc. 4150 Network Circle, Santa Clara, California 95054. (LFI#112968/Form ID#011801)
528
Sandini Bib
Copyright Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell angebotenen eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook stellen wir lediglich als persönliche Einzelplatz-Lizenz zur Verfügung! Jede andere Verwendung dieses eBooks oder zugehöriger Materialien und Informationen, einschliesslich •
der Reproduktion,
•
der Weitergabe,
•
des Weitervertriebs,
•
der Platzierung im Internet, in Intranets, in Extranets,
•
der Veränderung,
•
des Weiterverkaufs
•
und der Veröffentlichung
bedarf der schriftlichen Genehmigung des Verlags. Insbesondere ist die Entfernung oder Änderung des vom Verlag vergebenen Passwortschutzes ausdrücklich untersagt! Bei Fragen zu diesem Thema wenden Sie sich bitte an: [email protected] Zusatzdaten Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei. Die Zurverfügungstellung dieser Daten auf unseren Websites ist eine freiwillige Leistung des Verlags. Der Rechtsweg ist ausgeschlossen. Hinweis Dieses und viele weitere eBooks können Sie rund um die Uhr und legal auf unserer Website
http://www.informit.de herunterladen