Beuth Hochschule BEUTH HOCHSCHULE FÜR TECHNIK BERLIN University of Applied Sciences WISSENSCHAFTLICHE WEITERBILDUNG Fernstudium Computational Engineering 2. Studienplansemester Vertiefte Grundlagen des CAE Modul 05 / Kurseinheit 123 Elektronische Datenverarbeitung II Prof. Dipl. math. Uwe Stephan 12.2009 3 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 3.1 Die Klasse – der grundlegende Begriff Lernziele: Nach dem Durcharbeiten dieses Kapitels sollen Sie: die grundsätzliche Struktur einer Klasse kennen, öffentliche (public) und private (private) Komponenten einer Klasse sinnvoll einsetzen können, Konstruktoren und einfache Klassenoperationen schreiben können, die Begriffe der ref-Variablen und der non-ref-Variablen und ihre technische Realisierung in Java kennen. 3.1 Die Philosophie der Klassenstruktur Der Autor hat seine Beispiele für diesen Kurs so abgespeichert, dass jedes Beispiel in einem eigenen Verzeichnis abgelegt ist. Diese Verzeichnisse heißen bsp1, bsp2, bsp3 .... und sind alle Unterverzeichnisse eines Verzeichnisses fsi_java: fsi_java +-----------+-----------+-----------+----------bsp1 bsp2 bsp3 Sie können das Verzeichnis fsi_java als Standard-Vorgabe einstellen. Wählen Sie dazu im Menü Configure Options und markieren Sie in der Auswahl der diversen Gebiete „Directories“. Dann können Sie das „Default Project Directory“ einstellen. Erzeugen Sie jetzt (innerhalb WINDOWS) ein neues Verzeichnis bsp3 für das nächste Beispiel und kopieren Sie das komplette Unterverzeichnis simple_io in dieses Verzeichnis. Dann erzeugen Sie im JCreator ein neues Projekt mit dem Menüpunkt File New Project. Geben Sie diesem neuen Projekt den Namen bsp3 (es ist am einfachsten, wenn Projektname und Verzeichnisname identisch sind) und achten Sie darauf, dass das Verzeichnis bsp3 Projektverzeichnis wird (Einstellung im Feld „Location“ während der Erzeugung des Projekts). Jetzt kommen wir zum eigentlichen Thema. Wir wollen Punkte der Ebene verarbeiten, und letztendlich soll das Ziel unseres Programms die Steuerung eines sehr einfachen Roboterarmes sein. Dieses Ziel haben wir nur vor Augen – so weit wird die Realisierung hier nicht gehen. Der Roboterarm ist um eine Achse drehbar, seine Position wird durch einen Winkel phi beschrieben. Dieser Winkel muss aus technischen Gründen stets zwischen -360° und +360° liegen (sonst würden Verbindungskabel überdehnt oder würden reißen). Der Roboterarm kann auch ausgefahren werden, seine Länge r muss stets zwischen 0,5 m und 2,0 m liegen. In einem Programm soll die Lage eines Roboterarmes durch jeweils einen Punkt beschrieben werden. Computational Engineering Elektronische Datenverarbeitung II KE 123 3.2 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut Klassenkonzept: Fehler vermeiden Datenkomponenten schützen 12.2009 Ziel des Klassenkonzepts ist es jetzt, den Programmierer bei der Vermeidung von Fehlern weitgehend zu unterstützen. In den Anfängen des Programmierens versuchte man, Fehler zu vermeiden, indem man viele gute Regeln aufstellte und hoffte, die Programmierer würden sich an diese Regeln halten. Heutzutage setzt man diese Regeln rigoros mit Hilfe passender Sprachen durch. Java ist eine solche Sprache. Unsere Anforderungen hier lauten: Eine Variable, die einen Punkt beschreibt (Typ: CPunkt), enthält zwei Komponenten r und phi. Die Werte dieser Variablen müssen immer zwischen 0.5 und 2.0 bzw. zwischen –360.0 und +360.0 liegen. Die erste Forderung legt nahe, dass der Typ CPunkt eine Verbund-Struktur (englisch: record) hat. Beachten Sie dazu den Abschnitt 5.1 der Kurseinheit 115. Die zweite Forderung hat zur Folge, dass der „normale“ Programmierer keinen Zugriff auf die Komponenten r und phi einer Variablen vom Typ CPunkt hat. Hätte er diesen Zugriff, könnten wir nicht mehr garantieren, dass die Werte nur in den geforderten Intervallen liegen. Ansatz: private Daten Datenkapsel: Verbund (record) Zugriff nur durch Klassenoperationen KE 123 Im Abschnitt 13.5 der Kurseinheit 115 hatten wir bereits den Lösungsansatz der 80-er Jahre des vorigen Jahrhunderts skizziert: Modulare Programmierung und die Verwendung von Paketen. Ein Paket hatte einen privaten und einen öffentlichen Teil. Der „normale“ Programmierer hatte nur Zugriff auf den öffentlichen Teil, die Daten im privaten Teil waren geschützt. Dieses Konzept, das in der Modularen Programmierung für das Paket galt, übertragen wir jetzt auf die einzelne Variable. Der Typ einer Variablen wird jetzt durch eine Klasse (class) beschrieben: Eine Variable eines solchen Klassentyps hat stets eine Verbund-Struktur (record-Struktur), auch wenn sie mal nur eine Komponente haben sollte. Die Daten-Komponenten einer solchen Variablen sind im Allgemeinen als private deklariert, d.h. ein Anwendungsprogrammierer, der Variablen dieses Typs verwendet, kann nicht auf die Komponenten zugreifen. Bei der Erzeugung einer Variablen wird durch ein besonderes Unterprogramm, einen sogenannten Konstruktor, sichergestellt, dass die Komponenten genau definierte, gültige Werte haben. Die Veränderung dieser Werte ist nur durch Aufruf von speziellen Unterprogrammen möglich, die Teil der Klasse sind. Der englische Fachausdruck für diese Unterprogramme ist „member-functions“, der deutsche Ausdruck ist „Klassenoperationen“. Diese Unterprogramme haben Zugriff auf die Komponenten. Eine Bemerkung zur Realität: Wenn bei der Programmierung der Klassenoperationen Fehler gemacht werden, kann die Korrektheit der Daten natürlich auch nicht garantiert werden. Wenn aber einmal (nach Tests und Fehlerkorrektur) die Klassenoperationen fehlerfrei sind, kann der Anwendungsprogrammierer keine Fehler in den Daten mehr verursachen, die die obigen Bedingungen (Werte zwischen 0.5 und 2.0 bzw. zwischen –360.0 und +360.0) verletzen. Elektronische Datenverarbeitung II Computational Engineering 12.2009 3.3 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut Jetzt zur technischen Realisierung in Java. Wir erzeugen mit File New File eine neue Textdatei bsp3.java und beschreiben dort als Erstes nur die Datenstruktur des neuen Typs: class CPunkt { private double r, phi; } Beachten Sie beim Eintippen, wie der Editor Sie unterstützt. Sobald Sie die öffnende Klammer { getippt haben, fügt der Editor automatisch die schließende Klammer } ein. Anschließend definieren wir einen Konstruktor. Konstruktoren haben stets denselben Namen wie die Klasse, sehen aus wie functions in C, haben jedoch keinen return-Typ. Wir nehmen an, dass am Anfang r immer den Wert 0.5 und phi den Wert 0.0 haben soll, sofern nichts weiter gesagt ist. Damit sieht die Klasse so aus: class CPunkt { private double r, phi; Konstruktionen: Name=Klassenname kein return-Typ CPunkt () { // default-constructor r = 0.5 ; phi = 0.0 ; } } Natürlich sollte man die Werte –360.0, +360.0, 0.5 und 2.0 durch Konstanten beschreiben, wir wollen dieses erste Beispiel jedoch nicht überfrachten mit Details. Wir wollen gleich noch eine Klassenoperation definieren, die den Inhalt einer Variablen des Typs CPunkt zeigt. Die gesamte Klasse lautet dann class CPunkt { private double r, phi; CPunkt () { // default-constructor r = 0.5 ; phi = 0.0 ; } void zeigePunkt () { System.out.println( "r = " + FormattedStrings.strOfDouble (r,5,2) + "; phi = " + FormattedStrings.strOfDouble (phi,6,1) ); } } Beim Konstruktor und bei der Klassenoperation zeigePunkt bemerken Sie bereits: Unterprogramm in Java haben dieselbe äußere Struktur wie in C. Eine fehlende Parameterliste wird jedoch nicht durch void, sondern durch eine leere Liste angezeigt. Computational Engineering Elektronische Datenverarbeitung II KE 123 3.4 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 12.2009 3.2 Variablen und Anweisungen in Java Wir wollen gleich zeigen, dass mit dieser Klassendefinition ein neuer Typ definiert wurde. Fügen Sie in dieselbe Quelldatei bsp3.java ein Hauptprogramm ein, indem Sie zunächst die Klasse dafür beschreiben (den Klassennamen bsp3 sollten Sie unbedingt von Hand tippen (nicht kopieren), da der JCreator sich diesen Namen sofort merkt und spätere Änderungen nicht mehr zulässt): import java.io.* ; import simple_io.* ; class bsp3 { // enthält das Hauptprogramm } Danach können Sie die Definition von main aus einer anderen Quelle kopieren. Die gesamte Quelle sollte dann so aussehen: import java.io.* ; import simple_io.* ; class bsp3 { // enthält das Hauptprogramm public static void main ( String[] args ) throws java.io.IOException { System.out.println( "Beispiel 3 beginnt ..." ) ; } } class CPunkt { ......... } Übersetzen Sie das Projekt und führen Sie es aus. Als nächstes wollen wir eine Variable vom Typ CPunkt definieren. Dabei ist zu beachten, dass in Java andere Regeln für den Umgang mit Variablen gelten. Wir fassen diese Regeln kurz zusammen: non-refVariablen KE 123 Es gibt so genannte „non-ref-Variablen“ (Java: „Variables of Primitive Type“). Sie haben dieselbe Bedeutung wie in C, wir können sie uns als Speicherkästchen vorstellen. Atomare Typen haben stets non-refVariablen. Dies sind die folgenden Typen: Elektronische Datenverarbeitung II Computational Engineering 3.5 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 12.2009 Typ-Name Anzahl Bits Wertebereich byte 8 -128 bis 127 short 16 -32768 bis 32767 int 32 -2147483648 bis 2147483647 long 64 ca. –1019 bis +1019 char 16 alle Unicode Zeichen float 32 siehe ANSI/IEEE Standard 754 double 64 siehe ANSI/IEEE Standard 754 bool true false Wir bemerken insbesondere, dass Java im Gegensatz zu C einen eigenen Typ bool für logische Werte hat. Alle anderen Variablen (Klassenvariablen, Felder = arrays) sind so genannte ref-Variablen (Java: „Variables of Reference Type“). Wir erläutern diesen Begriff an einer Variablen vom Typ CPunkt. ref-Variablen Im Hauptprogramm main definieren wir CPunkt p1; p1 ist damit eine ref-Variable vom Typ CPunkt, d.h. p1 ist Zeiger auf eine Variable vom Typ CPunkt. Das „ref“ steht als Abkürzung für „reference“, und reference ist ein anderes Wort für Adresse, Zeiger oder Bezug. Java kennt den Begriff der Adresse (der reference), erlaubt aber nicht, Zeiger (wie in C) vom Programm her zu verändern. Bildlich haben wir nach der obigen Definition also folgende Situation: Die Variable p1 existiert, hat aber keinen Inhalt (genauer: Sie enthält eine „null reference“, einen Zeiger ohne Wert). p1 Wir müssen die eigentliche Variable, also das Objekt vom Typ CPunkt, erst noch erzeugen. Dies geschieht durch Aufruf eines Konstruktors dieser Klasse hinter dem Schlüsselwort new: p1 = new CPunkt (); Computational Engineering Elektronische Datenverarbeitung II KE 123 3.6 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 12.2009 Danach haben wir: p1 Bild 3.1: r 0,5 phi 0,0 Referenz- und Objektvariable Um die beiden Variablen im Bild 3.1 sprachlich unterscheiden zu können, vereinbaren wir: Das kleine Speicherkästchen ist die ref-Variable p1 vom Typ CPunkt. p1 enthält einen Zeiger auf die Objektvariable (sie wird im Bild 3.1 durch das große Kästchen dargestellt). Die Objektvariable wird auch „eine Instanz der Klasse CPunkt“ genannt. Wir lassen uns diese Werte im Programm anzeigen und rufen die Klassenoperation zeigePunkt() für die Variable p1 auf. Dafür (für den Aufruf einer Klassenoperation, die mit einer Variablen dieser Klasse arbeiten soll) gibt es eine besondere Notation, die an den Zugriff auf Komponenten eines Verbundes erinnert (siehe Abschnitt 5.1 der Kurseinheit 115): p1 . zeigePunkt(); Diese Anweisung ist ein Unterprogrammaufruf. p1 ist das aktuelle Objekt dieses Aufrufs, zeigePunkt() ist das aufgerufene Unterprogramm oder die aufgerufene Klassenoperation. 3.3 Weitere Konstruktoren und Klassenoperationen. Eine Klasse kann mehrere Konstruktoren enthalten. Wollen wir (beim Entwerfen und Schreiben der Klasse) dem Anwendungsprogrammierer die Möglichkeit geben, eine Variable des Typs CPunkt gleich am Anfang mit eigenen Werte zu belegen, so können wir einen Konstruktor mit Parametern definieren. In den Anweisungen des Konstruktors müssen wir dafür sorgen, dass zum Schluss gültige Werte in r und phi stehen oder dass die Konstruktorausführung mit einer Fehlermeldung abgebrochen wird. Wir haben uns hier für die schlechtere Lösung, die automatische Korrektur entschieden, da die Fehlerbehandlung in Java erst später im Kurs behandelt wird. CPunkt ( double r_anf, double phi_anf ) { // ein INIT-constructor if ( (0.5 <= r_anf ) && ( r_anf <= 2.0 ) ) r = r_anf ; else r = 0.5 ; if ( (-360.0 <= phi_anf) && (phi_anf<=360.0)) phi = phi_anf; else phi = 0.0 ; } KE 123 Elektronische Datenverarbeitung II Computational Engineering 3.7 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 12.2009 Sie bemerken: Die Syntax der Anweisungen ist wie in C, der Operator && steht für das logische UND. Wir definieren eine zweite Variable p2 und belegen Sie mit Anfangswerten 1.2 und –90°. Das Hauptprogramm sieht z. Z. so aus: public static void main ( String[] args ) throws java.io.IOException { System.out.println( "Beispiel 3 beginnt ..." ) ; CPunkt p1, p2 ; p1 = new CPunkt (); p1 . zeigePunkt(); p2 = new CPunkt ( 1.2 , -90.0 ); p2 . zeigePunkt(); } Beachten Sie: Im Programm müssen wir die konstanten Werte mit einem Dezimalpunkt schreiben, der Java-Compiler erwartet es so. Bei der Eingabe tippen Sie ein Dezimalkomma, sofern Sie das Programm in Deutschland ausführen, da wir in den Routinen von simple_io auf das Land Bezug nehmen, in dem das Programm aktuell ausgeführt wird. Bei der Ausgabe sollten Sie (bei Ausführung in Deutschland) auch Dezimalkommata sehen. Wie kann nun der Compiler die beiden Konstruktoren unterscheiden? In den klassischen Programmiersprachen (also auch in C) gilt, dass Unterprogramme durch ihren Namen identifiziert werden, dass daher alle Namen von Unterprogrammen verschieden sein müssen. In objektorientierten Programmiersprachen (wie C++, Java) gilt, dass ein Unterprogramm durch seine Klassenzugehörigkeit und innerhalb der Klasse durch seine Signatur bestehend aus Name und Liste der Parametertypen identifiziert wird. CPunkt() ist also ein anderer Konstruktor als CPunkt (double, double). Signatur eines Unterprogramms Wir entwerfen jetzt noch zwei Klassenoperation fahre_aus, um den Roboterarm um eine bestimmte Länge auszufahren und drehe_um, um den Arm um einen bestimmten Winkel zu drehen. void fahre_aus ( double delta_r ) { double neu_r = r + delta_r ; if ( ( neu_r < 0.5 ) || (2.0 < neu_r ) ) return ; // Fehlerbehandlung können wir // noch nicht r = neu_r ; } void drehe_um (double delta_phi) { if ( ( delta_phi < -360.0) || ( 360.0 < delta_phi) ) return ; double drehwinkel = delta_phi ; if ( phi + drehwinkel < -360.0 ) drehwinkel = 360.0 + drehwinkel ; if ( phi + drehwinkel > 360.0 ) drehwinkel = drehwinkel - 360.0 ; System.out.println( "Technische Drehung um " + FormattedStrings.strOfDouble (drehwinkel,6,1) ); phi += drehwinkel ; } Computational Engineering Elektronische Datenverarbeitung II KE 123 3.8 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 12.2009 Das Hauptprogramm ergänzen wir so, dass man wiederholt Bewegungsdaten am Bildschirm eingeben kann und die neue Position dann gezeigt wird. public static void main ( String[] args ) throws java.io.IOException { System.out.println( "Beispiel 3 beginnt ..." ) ; CPunkt p1, p2 ; double delta_r, delta_phi ; p1 = new CPunkt (); p1 . zeigePunkt(); p2 = new CPunkt ( 1.2 , -90.0 ); System.out.print ("Roboterarm p2 bei "); p2 . zeigePunkt(); do { delta_r = SimpleInput.readDouble ( "Arm ein/ausfahren um : "); delta_phi = SimpleInput.readDouble ( "Arm drehen um : "); System.out.println (""); p2 . fahre_aus ( delta_r ); p2 . drehe_um ( delta_phi ); System.out.print ("Roboterarm p2 bei "); p2 . zeigePunkt(); } while ( (delta_r != 0.0)||(delta_phi != 0.0) ); } Übersetzen Sie das Programm und führen Sie es aus. Geben Sie im Test dann mehrfach positive Winkel ein und beobachten Sie, wie das System bei zu großem Gesamtwinkel technisch eine negative Drehung durchführt, um den Bereich –360° bis +360° nicht zu verlassen. 3.4 Java und C: Gemeinsamkeiten und Unterschiede. An den obigen Beispielen sehen Sie bereits, dass die Syntax der einzelnen Anweisungen in Java („Programmieren im Kleinen“) der Syntax von C ähnlich ist. Dennoch gibt es einige wesentliche Unterschiede, die in späteren Kapiteln behandelt werden. Wir können die Regeln kurz zusammenfassen: KE 123 Lokale atomare Variablen (also vom Typ byte, short, int, long, char, float, double und bool) werden wie in C definiert (wobei C den Typ bool nicht kennt). Arrays haben in Java eine völlig andere Struktur, die in einem besonderen Kapitel behandelt wird. Kontrollstrukturen (Sequenz, Wiederholungen, Fallunterscheidungen) haben in Java dieselbe Struktur und dieselben Schlüsselwörter wie in C. Java kennt den Begriff der Adresse: Er steckt im Begriff der refVariablen. Java stellt jedoch keine Operatoren zur Veränderung von Adressen zur Verfügung. Elektronische Datenverarbeitung II Computational Engineering 12.2009 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut In Unterprogrammen werden alle Parameter als Vorgabeparameter behandelt, es gibt weder den Adresse-von-Operator (in C: & wie in & v) noch den Zugriff-über-Adresse-Operator (in C: * wie in *p). Parameter atomaren Typs (also vom Typ byte, short, int, long, char, float, double und bool) können daher nie Rückgabeparameter sein. ref-Variablen enthalten Adressen von Objekten. Tritt eine ref-Variable als Parameter auf, so kann das zugehörige Objekt durch den Unterprogrammaufruf verändert werden. In diesem Sinne können auch Java-Unterprogramme Rückgabeparameter haben. Computational Engineering Elektronische Datenverarbeitung II 3.9 KE 123 3.10 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 3.5 12.2009 Übungsaufgaben Aufgabe 1 Nennen Sie die drei Arten von Elementen, die als Teil einer Klasse auftreten können. Aufgabe 2: Wodurch identifiziert der Java-Compiler einzelne Klassenoperationen oder Konstruktoren? Aufgabe 3: Nennen Sie alle atomaren Typen von Java. Aufgabe 4: a) Was sind non-ref-Variablen, was sind ref-Variablen? b) Welche Typen haben non-ref-Variablen, welche haben refVariablen? c) Wie erzeugt man im Programm ein Objekt (eine Instanz) zu einer gegebenen Klasse? KE 123 Elektronische Datenverarbeitung II Computational Engineering 12.2009 © Beuth Hochschule für Technik Berlin – Fernstudieninstitut 3.11 3.6 Lösungen zu den Übungsaufgaben Aufgabe 1: Eine Klasse besteht aus Datenelementen, Konstruktoren und Klassenoperationen. Aufgabe 2: Jede Klassenoperation und jeder Konstruktor wird durch seine Signatur innerhalb der Klasse eindeutig identifiziert. Die Signatur besteht aus dem Namen dieses Unterprogramms und der Liste der Parametertypen. Aufgabe 3: Die atomaren Typen sind byte, short, int, long, char, float, double und bool. Aufgabe 4: a) non-ref-Variablen enthalten stets einen Wert des zugehörigen Typs. ref-Variablen enthalten stets einen null-Zeiger (null reference) oder einen Zeiger auf ein Objekt des zugehörigen Typs. b) Alle atomaren Typen (siehe Aufgabe 3) erzeugen non-ref-Variablen. Alle Klassentypen (ob selbst geschrieben oder aus der JavaBibliothek übernommen) und alle Felder (arrays) erzeugen refVariablen. c) Hinter dem Schlüsselwort new ruft man einen Konstruktor der Klasse auf. Computational Engineering Elektronische Datenverarbeitung II KE 123