Vorlesung Informatik 1 Fachhochschule für Technik Esslingen Studiengang Wirtschaftsinformatik Teil 2: Programmelemente Dr. rer. nat. Andreas Rau http://www.hs-esslingen.de/~rau [email protected] © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #1 Grundbegriffe & Werkzeuge © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #2 Komplexität Pessimisten meinen Ein Computer hilft uns, Probleme zu lösen, die wir ohne ihn nicht hätten... und tatsächlich geht mit Software so manches schief. Allerdings liegt das nicht an der Software an sich sondern an ihrer Komplexität und der Schwierigkeit der beteiligten Menschen, mit dieser Komplexität fertig zu werden. Im allgemeinen bekämpft man Komplexität durch Abstraktion. Allerdings muß man bei der Softwareentwicklung irgendwann wieder konkret werden. Deshalb verwendet man die Methode der hierarchischen Zerlegung bzw. des top-down Entwurfs um ein Problem zu lösen. Man zoomt gewissermaßen von außen in das Problem hinein, indem man zunächst die Grobstruktur des Problems bzw. der Lösung analysiert und sich erst hinterher um die Details kümmert. Hierbei wird das Problem oft in Teilprobleme zerlegt. Das zugehörige Schlagwort hierzu lautet divide and conquer (teilen und herrschen) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #3 Funktion und Daten Laut Niklaus Wirth besteht Softwareentwicklung aus der schrittweisen Verfeinerung von Funktionen und Daten. Es ist wichtig, beide Aspekte im Auge zu behalten. Die Vernachlässigung eines der beiden kann fatale Folgen haben Funktionen können abstrahiert werden über ● Anweisungen ● Funktionen ● Module Daten können abstrahiert werden über ● Einzelne Werte ● Felder (Arrays) und Listen gleichartiger Werte ● Strukturen aus zusammengehörigen Werten verschiedenster Art Wie wir wissen, liegt der Vorteil der Objektorientierung in der Verbindung von Funktionen und Daten zu einem geschlossenen Objekt. Die dadurch erreichte Kapselung stellt sicher, dass nur gültige Operationen auf den Daten möglich sind. Zunächst wollen wir uns jedoch mit den Grundlagen vertraut machen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #4 Verfeinerung von Programm und Daten Sowohl Programm als auch Daten werden schrittweise verfeinert. Dabei Vermeidet man Redundanz ● Kleine, wiederverwendbare Funktionen mit klarem Zweck ● Alles nur einmal speichern (z.B. nicht Geburtsdatum und Alter) ● Bündelt man, was zusammengehört (Kohesion) ● Funktionen in Modulen ● Daten in Datensätzen ● Minimiert man Abhängigkeiten verschiedener Teile (Kopplung) ● Möglichst geschlossene Funktionbereiche ● Vollständige Datensätze ● Letzteres erreicht man durch möglichst minimale, wohldefinierte Schnittstellen. Grundsätzlich gilt, daß Daten nur dort direkt sichtbar sein sollten, wo Sie unbedingt gebraucht werden (Information Hiding). Ansonsten besteht die Gefahr von inkonsistenten Daten aufgrund unkontrollierter Änderungen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #5 Aufbau und Inhalte Bei den Inhalten eines Programms unterscheidet man Syntax und Sematik: Syntax Struktureller Aufbau des Programms (Satzbau) Beispiel: Fussball gerne Hans spielt? ➔Kann vom Compiler/Interpreter geprüft werden (vollständig) Semantik Bedeutung des Programms Beispiel: Der Hund hat in der Drogerie gewonnen. ➔ Muss vom Mensch geprüft werden (unvollständig) Mit anderen Worten: Die Syntax legt fest, wie ein Kochrezept aufgebaut ist. Von der Semantik hängt es ab, ob das Essen schmeckt (Anders als ein menschlicher Koch macht der Computer keine Fehler wenn das Rezept stimmt). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #6 Detaillierungsgrad Die Anforderungen an Syntaktische und Semantische Korrektheit sind bei Programmen viel höher als bei natürlicher Sprache: Ein Mensch der mitdenkt, kann noch so manche unklare Anweisung sinnvoll umsetzen. Ein Computer dagegen streikt schon, wenn eine Klammer fehlt oder macht wieder und immer wieder zuverlässig den vorgeschriebenen Fehler. Als Folge davon muss man beim Programmieren den Arbeitsablauf haarklein und Schritt für Schritt beschreiben. Möglich ist dies u.a. deswegen, weil Programmiersprachen viel exakter definiert sind als natürliche Sprachen (es gibt keine Ausnahmen oder unregelmässige Verben). Mit anderen Worten: Der Rechner erscheint manchmal als zickiger und unflexibler Sklave, der Dienst nach Vorschrift macht ohne mitzudenken. Andererseits ist er zumindest bei syntaktischen Fehlern so freundlich, mit durchaus verständlichen Fehlermeldungen auf das Problem hinzuweisen. Wer (englisch) lesen kann, ist also bei der Fehlersuche klar im Vorteil... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #7 Elemente Jedes Programm besteht irgendwie aus Funktionen und Daten, konkret aus Kontrollstrukturen / Anweisungen (Statements) ⇒ Ablauf steuern ● Sequenz ● Verzweigung ● Auswahl ● Schleife Ausdrücken / Berechnungen (Expressions) ⇒ Werte erzeugen/verändern ● Werte (Variablen und Literale) ● Operatoren Gültigkeitsbereichen (Scopes) ⇒ Sichtbarkeit und Lebensdauer festlegen ● Pakete ● Klassen ● Methoden ● Blöcke © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #8 Programmierwerkzeuge(1) Zur Programmierung werden verschiedene Werkzeuge benötigt Primäre Werkzeuge (Programmierung im Kleinen und im Großen) ●Ein allgemeiner oder spezieller Editor zur Programmerstellung (z.B. Notepad) ●Ein Compiler oder Interpreter zur Programmübersetzung und -Ausführung ●Ein Debugger zur Fehlersuche Sekundäre Werkzeuge (Programmierung im Großen, Projektverwaltung) ● Ein Dokumentationswerkzeug (z.B. OpenOffice) ● Ein Entwurfs- und Modellierungswerkzeug (z.B. ArgoUML) ● Eine Versionsverwaltung (z.B. CVS, SVN) ● Ein Projektplanungswerkzeug (z.B. OpenOffice) Moderne Entwicklungsumgebungen kombinieren viele dieser Werkzeuge unter einer Oberfläche für mehr Komfort und Produktivität. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #9 Programmierwerkzeuge(2) Eigenschaften von Compilern und Interpretern Vorteile von Compilern: Optimierung für bestimmten Prozessor und bestimmtes Betriebssystem; sehr hohe Ausführungsgeschwindigkeit des resultierenden Programms Nachteile von Compilern Zusätzliche Schritte bei der Programmerstellung, insbesondere beim Testen Vorteile von Interpretern: Erhöhte Produktivität durch einfachen Programmtest und interaktives arbeiten Nachteile von Interpretern: Geringe Ausführungsgeschwindigkeit (ständige Interpretation) Wie man sieht, sind die Nachteile des Einen die Vorteile des Anderen... Compiler werden eingesetzt, wenn es auf die Geschwindigkeit bei der Programmausführung ankommt (Programmiersprachen), Interpreter wenn es auf die Geschwindigkeit bei der Programmerstellung ankommt (Skriptsprachen). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #10 Programmierwerkzeuge(3) Ob ein Compiler oder Interpreter verwendet wird richtet sich nach der jeweiligen Programmiersprache (vgl. HTML, "weder Fisch noch Fleisch") Compiler: Quellcode z.B. *.pas, *.c Interpreter: Quellcode z.B. *.bat Objectcode z.B. *.o Compiler Linker Executable z.B. *.exe Interpreter Java kombiniert die Prinzipien von Compiler und Interpreter zur Erreichung der Plattform-Unabhängigkeit. Nur die virtuelle Maschine muss portiert werden. Quellcode Compiler *.java javac.exe Aufruf: X:>javac <Quelldatei(en)> Bytecode *.class Virtuelle Maschine java.exe X:>java <Klassenname> Java kennt keine exe-Dateien; jar-Dateien sind jedoch so ähnlich (später mehr). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #11 Programmierwerkzeuge(4) Im Java Development Kit (JDK) bzw. Java 2 Software Development Kit (J2SDK) wie es heute heisst befinden sich noch eine Reihe weiterer Programmierwerkzeuge. Eines der wichtigsten davon ist der javadoc Compiler. Mit seiner Hilfe ist es möglich, aus speziellen Kommentaren im Quelltext und den (Struktur)Informationen die im Programm selbst enthalten sind automatisch die Programmdokumentation zu generieren. Die generierte Dokumentation entspricht genau dem Aufbau der Online-Hilfe zum Java 2 Application Programming Interface (API). Dies ist kein Zufall, wird doch auch die Dokumentation zum API aus dem Quelltext der Bibliothek generiert. Es ist jedoch möglich über zahlreiche Parameter und Vorlagen (die man unmöglich hier alle beschreiben kann) das aussehen gezielt zu steuern. Quellcode *.java Doku-Generator javadoc.exe Dokumentation *.html Browser Aufruf X:>javadoc -d<Zielverzeichnis> <Quelldatei(en)> © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #12 Elementare Bestandteile © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #13 Rahmenwerk für Beispielprogramme Ab jetzt wirds Ernst, d.h. Sie müssen die Theorie auch praktisch umsetzen. Kleine Sünden strafft der Compiler sofort... Aber keine Angst: Das wird schon. In Java findet alles innerhalb von Klassen und Objekten statt. Daher benötigen wir bereits für unsere ersten Beispielprogramme und Experimente eine echte Klasse als Rahmen. Dabei werden einige Dinge verwendet und vorweggenommen, die Sie jetzt noch nicht verstehen. Für den Augenblick gilt dabei: einfach hinnehmen und machen*. Die Erleuchtung folgt später. Garantiert. // Beispielprogramm; die fett gedruckte Teile sind immer gleich public class KlassenName { // entspricht vorläufig dem Name des Programms // Die (Klassen)Methode mit der jedes Programm beginnt (C läßt grüssen) public static void main( String[] args) { // Hier spielt sich alles ab, u.a. kann man hier Variablen erzeugen // und Berechnungen durchführen. Später werden wir hier auch Objekte // erzeugen und Methoden aufrufen. Aber alles zu seiner Zeit... } } *Kinder lernen genauso © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #14 Zeichenvorrat von Java(1) Wie jede andere Sprache basiert auch eine Programmiersprache auf einem Alphabet, d.h. einer Menge von gültigen Zeichen. Das Alphabet von Java basiert auf dem Unicode Zeichensatz (vgl. www.unicode.org). Im Gegensatz zu den Zeichensätzen ASCII und ISO-Latin-1, die pro Zeichen 1 Byte verwenden, besteht ein Unicode Zeichen aus 2 Bytes. Aus Kompatibilitätsgründen sind jedoch die ASCII Zeichen mit identischer Kodierung im Unicode enthalten. Ziel von Unicode ist die einheitliche Kodierung der Zeichen aller Sprachen. Die meisten Zeichen können direkt über die Tastatur eingegeben werden. Für Sonderzeichen oder Symbole einer Fremdsprache (insbesondere innerhalb von Zeichenketten) besteht die Möglichkeit, den Code anzugeben. Dazu werden sog. Fluchtsequenzen (Escape Sequences) verwendet. Diese können die Form \o1o2o3 (ASCii-Code 0-255) oder \ux1x2x3x4 (Unicode) haben.. Beispiel: (Erklärung zu Zahlensystemen folgt später) \101 (ascii, oktal) = 65 (dezimal) = a \u0041 (unicode, hex) = 65 (dezimal) = a © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #15 Zeichenvorrat von Java(2) Konkret umfasst das Alphabet von Java (=Baumaterial!) folgende Zeichen Buchstaben (inkl. Unterstrich und Dollarzeichen) und Ziffern für Bezeichner A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z _ $ 0 1 2 3 4 5 6 7 8 9 ● Sonderzeichen für Trenner (engl. separators) () {} [] ; , . ● Sonderzeichen für Operatoren = > < ! ? : & | + - * / ^ % ● Sonderzeichen für Ersatzdarstellungen wie \\, \n, \t \ (engl. Backslash) ● unsichtbare(!) Zeichen (engl. Whitespace*) zur Formatierung des Quelltexts das Leerzeichen (engl. Blank), das Zeilenendezeichen (engl. linefeed), den horizontalen Tabulator (engl. tab) und den Seitenvorschub (engl. form feed) ● Begrenzer für Zeichenkonstanten und konstante Zeichenketten (vgl. Literale) das einfache Anführungszeichen '. Eselsbrücke: ein (Anführungs)zeichen das doppelte Anführungszeichen ". Eselsbrücke: mehrere (Anführungs)zeichen ● *im Internet finden sich satirische Beschreibungen von Programmiersprachen, die nur solche Zeichen verwenden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #16 Zeichenvorrat von Java(3) Aus diesem Zeichenvorrat wird der Quellcode und dabei die vorkommenden Literale, reservierten Wörter, und Bezeichner aufgebaut. darin Literale Sind "Werte, die man einfach hinschreiben kann" (siehe spezielle Folie). Reservierte Wörter Sind vordefinierte Namen für Elemente der Programmiersprache, z.B. Kontrollstrukturen und spezielle Operatoren (siehe Buch) . Sie dürfen nicht als Bezeichner verwendet werden. Bezeichner Sind die Namen von Klassen, Methoden und Variablen. Sie werden vom Programmierer vergeben und müssen mit einem Buchstaben oder $/_ beginnen. Java ist case-sensitiv, unterscheidet also zwischen Groß- und Kleinschreibung, . Bezeichner, die sich nur dadurch unterscheiden sind keine gute Idee!!! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #17 Kommentare(1) Als Kommentar bezeichnet man Text, der (in der Regel) vom Rechner ignoriert wird aber dem Menschen (neben gut gewählten Bezeichnern) hilft, das Programm zu verstehen. In Java gibt es mehrere Arten von Kommentaren: Kommentare für einzelne Zeilen (alles nach // wird ignoriert) System.out.println( "Hello, World!"); // gibt "Hello, World!" am Bildschirm aus Kommentare für Zeilenbereiche (alles zwischen /* und */ wird ignoriert) /* * Der Stern vor dieser Zeile ist optional, macht das Ganze aber hübscher */ Neben ihrer Verwendung zur Dokumentation können Kommentare auch bei der Fehlersuche nützlich sein. Hier werden sie genutzt, um Programmteile "auszukommentieren", d.h. durch Kommentarzeichen zu deaktivieren und so als möglichen Übeltäter auszuschließen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #18 Kommentare(2) Daneben gibt es in Java aber auch noch Kommentare, die sowohl für den Menschen als auch den Computer gedacht sind: Kommentare zur Generierung von Dokumentation (später mehr!) /** * Diese Klasse implementiert das berühmte "Hello, World!" Beispiel */ public class HelloWorld { // ... } Sie können an bestimmten Stellen verwendet werden, um Text für die spätere Generierung der Programmdokumentation direkt im Programm zu hinterlegen und sind an dem einleitenden /** (mit 2 Sternchen) erkennbar. Der Vorteil dieser Vorgehensweise liegt darin, daß man Programm und zugehörige Dokumentation stets gemeinsam vorliegen hat und konsistent pflegen kann. So soll verhindert werden, dass Programm und Dokumentation inhaltlich auseinander laufen (weitere Details später). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #19 Konventionen für Bezeichner Bezeichner sind wichtig. Daher gibt es für Bezeichner etablierte Stilvorgaben Bezeichnertyp Klassennamen Methodennamen Datenfeldnamen Variablennamen symbolische Konstanten Konvention 1. Buchstaben groß, Rest klein Kleinbuchstaben Kleinbuchstaben Kleinbuchstaben alle Buchstaben groß Beispiel Datum heute() monat termin FEBRUAR Bei aus mehreren Wörtern zusammengesetzte Namen wird ab dem zweiten Wort jeweils der erste Buchstabe eines Wortes groß geschrieben. Für das erste Wort gelten die obigen Konventionen (Ausnahme: Konstanten). Beispiele: verschiebeTermin() neuerTermin MONATE_PRO_JAHR Methode Variable Konstante Daneben gilt: Ein Name sollte etwas aussagen, und das möglichst eindeutig! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #20 Übung: Richtige und Falsche Bezeichner Welche Bezeichner sind syntaktisch richtig? Wofür stehen sie per Konvention? firstName M31N_N1CKNAM3 §3 2terVorname ALTERSGRENZE _internalValue telefon#2 zins% Konto schuhgröße Kontostand hAaRfaRbe x a1 b4712 haus-nr Buchung $1 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #21 Vorschau: Bestandteile einer Klasse public class BeispielKlasse { Legende static int klassenVariable; static void klassenMethode( int formalerParameter) { // ... } int instanzVariable; int instanzMethode( int formalerParameter) { // ... } public static void main( String[] args) { int lokaleVariable; klassenMethode( lokaleVariable); BeispielKlasse objektReferenz; objektReferenz = new BeispielKlasse(); lokaleVariable = objektReferenz.instanzMethode( lokaleVariable); reservierte Wörter: Sind durch die jeweilige Programmiersprache vorgegeben (müssen genau so lauten) und dürfen nicht für eigene Bezeichner verwendet werden. eigene Bezeichner: Werden vom Programmierer vergeben (könnten auch anders lauten). } } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #22 Vorschau: Bestandteile einer Klasse public class BeispielKlasse { public static void main( String[] args) { Liefert kein Ergebnis } Für Parameter aus dem DOS-Fenster Statisch, stets vorhanden (auch ohne Objekte) gehört zur Klasse } Öffentlich, für jedermann zugänglich © Andreas Rau, 19.11.10 Bezeichner sind normal frei wählbar. Dieser aber ist fest vereinbart! („Rose im Knopfloch“) D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #23 Datentypen © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #24 Datentypen Datentypen Elementare Datentypen boolean Referenztypen = null Numerische Datentypen Ganzzahltypen Gleitkommatypen char float = 0.0 byte = 0 double = 0.0 short = 0 int = 0 Default Typ long = 0 für Literale Default Wert für Variablen © Andreas Rau, 19.11.10 Default Wert für Datenfelder Klassentyp Arrays Schnittstellentyp z.B. String Strings und Arrays sind "magisch“ Sie werden durch eigene Konstrukte in der Sprache besonders unterstützt, z.B. Literale und spezielle Operatoren D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #25 Wertebereiche der Datentypen Jeder Elementare Datentyp in Java hat einen bestimmten Wertebereich: Typ boolean char byte short int long float double © Andreas Rau, 19.11.10 Inhalt Wahrheitswert 16 Bit-Zeichen (Unicode) auch als Zahl interpretierbar 8 Bit-Ganzzahl mit Vorzeichen (Zweierkomplement) 16 Bit-Ganzzahl mit Vorzeichen (Zweierkomplement) 32 Bit-Ganzzahl mit Vorzeichen (Zweierkomplement) 64 Bit-Ganzzahl mit Vorzeichen (Zweierkomplement) 32 Bit-Gleitkommazahl (IEEE 754) mit Vorzeichen, Mantisse, Exponent 64 Bit-Gleitkommazahl (IEEE 754) mit Vorzeichen, Mantisse, Exponent Wertebereich true und false 0 bis 65535 (alle Unicode-Zeichen) 7 bis +2 -1 -2 15 bis +2 -1 -2 31 bis +2 -1 -2 63 bis +2 -1 -2 7 15 31 63 -3.4*1038 bis +3.4*1038 -1.7*10308 bis +1.7*10308 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #26 Wertebereiche der Datentypen Vergleich der Wertebereiche in logarithmischer(!) Darstellung © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #27 Analogie – Datentypen als Eimer feiner größer byte © Andreas Rau, 19.11.10 short int long float double D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #28 Einschub: Speicherorganisation Elementare Bezeichnungen 1 Bit 1 Byte eine einzelne Binärziffer eine einzelne Speicherzelle (8 Bit) 1 Nibble 1 Word eine einzelne Hexziffer (4 Bit oder ein halbes Byte) Registergröße des Prozessors (heute: 32 Bit oder 4 Byte) Dimensionen 1 KByte (KB) 1 MByte (MB) 1 GByte (GB) 1 TByte (TB) 2^10 Byte = 1 024 Byte 2^20 Byte = 1 048 576 Byte 2^30 Byte = 1 073 741 824 Byte 2^40 Byte = ... Byte (Wechseldatenträger) (Festplatten) (Datenbanken) Offizielle Abgrenzung der Binäreinheiten von den physikalischen SI-Einheiten: Kilobyte (kByte) = 1000 Byte Kikibyte (KiByte) = 1024 Byte Wird aber nicht konsequent durchgehalten und führt häufig zu Verwirrung. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #29 Einschub: Speicherbereiche(3) Wie der Stack funktioniert (Last-In, First-Out bzw. First-In, Last-Out) Stack heisst Stapel, und dass kann man durchaus wörtlich nehmen: Was als letztes oben draufgelegt wurde, wird als erstes wieder weggenommen, und was als erstes (also ganz unten) kommt, geht als letztes. Dieser Ablauf ist besonders dazu geeignet, um sich zu merken "wo man herkommt" – z.B. die Rücksprungaddresse beim Aufruf von Unterprogrammen. Das geht natürlich auch über mehrere Schritte. Zusätzlich kann man sich dann noch ein paar Notizen zum Kontext machen. Das sind dann die lokalen Variablen. Exkurs: Sicherheitslücken durch Buffer-Overflow (an der Tafel) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #30 Literale Literale sind Werte, die man „einfach so hinschreiben“ kann, z.B. um Variablen zu initialisieren. Dazu sind für gängige Datentypen feste Schreibweisen definiert. Datentyp boolean char int long float double String* Array Literal true, false 'a', 'b', '\o1o2o3' (ASCII Code oktal), '\ux1x2x3x4' (Unicode) 154 , 012 (Oktal), 0x1a (Hexadezimal) 154L, 012L (Oktal), 0x1aL (Hexadezimal) 1.0f, 12.5e+5f (Expotentialdarstellung = 1250000) 1.0 , 12.5e+5 (Expotentialdarstellung = 1250000) "das ist eine Zeichenkette" (vgl. API Doku) {1, 6, 3, 9} (int-Array mit 4 Elementen) (vgl. API Doku) Für die übrigen Datentypen gibt es keine Literale! Ihre Werte müssen entweder durch Konvertierung der vorhandenen Literale gebildet (elementare Datentypen) oder mit dem new-Operator (kommt noch) erzeugt und initialisiert werden. *identische Stringliterale werden zu einem einzigen String-Objekt zusammengefasst © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #31 Einschub: Zahlensysteme(1) Normale Menschen verwenden im Alltag zur Darstellung von Zahlen das Dezimalsystem (das hat wohl etwas mit unseren Fingern zu tun...). Programmierer verwenden jedoch daneben noch andere Zahlensysteme, v.a. das Dualsystem und das Hexadezimalsystem sowie manchmal das Oktalsystem. Dies hängt mit der Speicherorganisation eines Rechners zusammen. In den genannten Zahlensystem läßt sich der Zustand der Bits leichter ablesen. Dies ist wichtig, weil sich vieles in der Informatik platzsparend in einzelnen Bits kodieren läßt – vor allem in der Steuerungstechnik. Alle genannten Zahlensysteme sind sog. Stellenwertsysteme. Ein Stellenwert weißt jeder Ziffer abhängig von ihrer Position eine Wertigkeit zu, die sich aus dem Exponenten der Basis des Zahlensystems und der Position ergibt (dabei fangen Informatiker wie immer bei Null an zu zählen). Der Ziffernvorrat für jede Stelle ergibt sich aus Basis des Zahlensystems. Der Vorteil von Stellenwertsystemen gegenüber primitiven Zahlensystemen (z.B. 1 = |, 2 = ||, 3 = |||): Man braucht viel weniger Platz für größere Zahlen! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #32 Einschub: Zahlensysteme(2) Dezimalsystem (Dezimal = 10) Ziffern ... Exponent ... 10^3 10^2 10^1 10^0 10^-1 10^-2 10^-3 ... Wertigkeit ... 1000 ... Ziffernvorrat Z3 Z2 100 Z1 Z0 10 1 Z-1 Z-2 1/10 1/100 Z-3 1/1000 ... 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 Die Zahl 6421.32 bedeutet also in Wirklichkeit 6*1000 + 4*100 + 2*10 + 1 + 3*1/10 + 2*1/100 = 6421.32 Beim Dezimalsystem erscheint dies ziemlich Zahlensysteme funktionieren aber genauso... © Andreas Rau, 19.11.10 witzlos. Die anderen D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #33 Einschub: Zahlensysteme(3) Dualsystem (Dual = 2) Ziffern ... Z3 Z2 Z1 Z0 Z-1 Z-2 Z-3 ... Exponent ... 2^3 2^2 2^1 2^0 2^-1 2^-2 2^-3 ... Wertigkeit ... 8 4 2 1 ½ ¼ 1/8 ... Ziffernvorrat 0, 1 Die Zahl 1101.01 bedeutet also in Wirklichkeit 1*8 + 1*4 + 0*2 + 1*1 + 0*1/2 + 1*1/4 = 13,25 Damit kann man also vom Dualsystem ins Dezimalsystem umrechnen... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #34 Einschub: Zahlensysteme(4a) Oktalsystem (Oktopuss = 8 Arme) Ziffern ... Z3 Z2 Z1 Z0 Z-1 Z-2 Z-3 ... Exponent ... 8^3 8^2 8^1 8^0 8^-1 8^-2 2^-3 ... Wertigkeit ... 512 64 8 1 1/8 1/64 1/512 ... Ziffernvorrat 0, 1, 2, 3, 4, 5, 6, 7 Die Zahl 742.36 bedeutet also in Wirklichkeit 7*64 + 4*8 + 2*1 + 3*1/8 + 6*1/64 = 482,46875 Damit kann man also vom Oktalsystem ins Dezimalsystem umrechnen... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #35 Einschub: Zahlensysteme(4b) Hexadezimalsystem (Hexa = 6, Dezimal = 10 ergo Hexadezimal = 16) Ziffern ... Exponent ... 16^3 16^2 16^1 16^0 16^-1 16^-2 16^-3 Wertigkeit ... 4096 Ziffernvorrat Z3 Z2 256 Z1 16 Z0 1 Z-1 Z-2 1/16 1/256 Z-3 1/4096 ... ... ... 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A(10), B(11), C(12), D(13), E(14), F(15) Die Zahl 2E1A.0F bedeutet also in Wirklichkeit 2*4096 + 14*256 + 1*16 + 10*1 + 0*1/16 + 15*1/256 = 11802,059 Damit kann man also vom Hexadezimalsystem ins Dezimalsystem umrechnen... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #36 Einschub: Zahlensysteme(5) Für die Rückwärtsrichtung, d.h. vom Dezimalsystem in ein anderes Zahlensystem, verwendet man den sog. Restwertalgorithmus. Bei diesem Algorithmus teilt man solange durch die Basis des Zahlensystems, bis alle Ziffern ermittelt sind: Beispiel: 112 ins Dualsystem wandeln 112 56 28 14 7 3 1 : : : : : : : 2 2 2 2 2 2 2 Also 112 = 56 Rest 0 = 28 Rest 0 = 14 Rest 0 = 7 Rest 0 = 3 Rest 1 = 1 Rest 1 = 0 Rest 1 = 1110000 = 64 + 32 + 16 = 1*2^6 + 1*2^5 + 1*2^4 + 0*2^3 + 0*2^2 + 0*2^1+ 0*2^0 = (((((((1*2) + 1)*2 + 1)*2 + 0) *2 + 0)* 2 + 0)*2 + 0 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #37 Einschub: Zahlensysteme(6) Der Restwertalgorithmus funktioniert zwar für alle Zahlensysteme, für Oktal und Hexadezimal ist er aber etwas mühsam. Alternativ kann man ausnutzen, dass Hexziffern bzw. Oktalziffern jeweils 4 bzw. 3 Dualziffern entsprechen. Hex 0xF 0xE 0xD 0xC 0xB 0xA 0x9 0x8 Oktal - Dual 1111 1110 1101 1100 1011 1010 1001 1000 Hex 0x7 0x6 0x5 0x4 0x3 0x2 0x1 0x0 Oktal 07 06 05 04 03 02 01 00 Dual 0111 0110 0101 0100 0011 0010 0001 0000 Wichtig: Gruppierung immer von hinten her; Wertigkeit pro Block stets 8, 4, 2, 1 1 6 D 101101101 = (000)1 0110 1101 = 0x16D © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #38 Einschub: Zahlensysteme(7) Restwert Restwert Dezimal = 27 27:8 = 3R3 27:16 = 1R11 3:8 = 1R3 1:16 = 0R 1 Stellenwerte Stellenwerte 3*8+3*1 1*16+11*1 Oktal = 033 Restwert Stellenwerte 27:2 = 13R1 1*16+1*8+0*4 13:2 = 6R1 +1*2+1*1 6:2 = 3R0 3:2 = 1R1 1:2 = 0R1 3 Bits pro Ziffer Hex = 0x1b 4 Bits pro Ziffer Binär = 11011 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #39 Einschub: Darstellung von negativen Ganzzahlen Intern werden alle Werte als „Bits und Bytes“ kodiert gespeichert. Speziell bei Ganzzahlen stellt sich dabei die Frage, wie negative Zahlen dargestellt werden sollen um sie einfach verarbeiten zu können. Gesucht ist also eine Darstellung für -5 damit 5-5 = 0 möglichst einfach berechnet werden kann. Der naive Ansatz, ein eigenes Bit für das Vorzeichen zu spendieren erfordert eine explizite Unterscheidung der Kombinationen ++, +-, -+ und --. Mit Hilfe der Darstellung im sog. Zweierkomplement ist diese Fallunterscheidung nicht mehr nötig. Dabei wird die Tatsache ausgenutzt, dass binär 1+1 = 0(+Überlauf) ergibt. Das Zweierkomplement (ZK) besteht aus zwei Schritten: 1. Bildung des Komplements durch kippen aller* Bits (damit es bei Addition lauter 1er gibt) 2. Addition von 1 (um den Überlauf zu erzwingen – "Domino Effekt / Klippe", "Tachorad") Bsp. Kodierung von -5 5 = 0101 1010 nach Komplement (Hinweis: 0101+1010=1111) 1011 nach Addition von (000)1 (Hinweis: 0101+1011=(1)0000) Das dies wirklich so ist kann man sogar mit einem Programm sichtbar machen (später). *je nach Datentyp gibt es unterschiedlich viele führende Nullen! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #40 Darstellungsgenauigkeit in verschiedenen Zahlensystemen(1) Nicht jede Zahl ist in jedem Zahlensystem gleich gut darstellbar. So ist zum Beispiel im Dezimalsystem die rationale Zahl (d.h. der Bruch) 1/3 nicht exakt darstellbar: 1 / 3 = 0.33... 10 9 10 9 … Ein Dezimalcomputer, der Fließkommazahlen nur mit 5-stelliger Genauigkeit darstellen kann, könnte also folgende Berechnung nicht exakt durchführen 3 * (1/3) = 3 * 0.33333 = 0.99999 Das kann man im Einzelfall durch Rundung oder Umstellung der Berechnung korrigieren: 1 * (3/3) = 1 Im Allgemeinen addieren sich solche Fehler jedoch über die Zeit auf! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #41 Darstellungsgenauigkeit in verschiedenen Zahlensystemen(2) Ähnlich „ungeschickte“ Zahlen gibt es auch im Dualsystem, z.B. 0.1 0.1 = 1/10 (dezimal) = 1 / 1010 (dual) 1 / 1010 = 0.00011001... 10 100 1000 10000 11010 11 01100 1010 1 100 1000 10000 1010 ... © Andreas Rau, 19.11.10 Probe mit Tabellenkalkulation: 1,0000000000000000000000000 0,5000000000000000000000000 0,2500000000000000000000000 0,1250000000000000000000000 0,0625000000000000000000000 0,0312500000000000000000000 0,0156250000000000000000000 0,0078125000000000000000000 0,0039062500000000000000000 0,0019531250000000000000000 0,0009765625000000000000000 0,0004882812500000000000000 0,0002441406250000000000000 0,0001220703125000000000000 0,0000610351562500000000000 0,0000305175781250000000000 0,0000152587890625000000000 0,0000076293945312500000000 0,0000038146972656250000000 0 0 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 Summe 0,0000000000000000000000000 0,0000000000000000000000000 0,0000000000000000000000000 0,0000000000000000000000000 0,0625000000000000000000000 0,0312500000000000000000000 0,0000000000000000000000000 0,0000000000000000000000000 0,0039062500000000000000000 0,0019531250000000000000000 0,0000000000000000000000000 0,0000000000000000000000000 0,0002441406250000000000000 0,0001220703125000000000000 0,0000000000000000000000000 0,0000000000000000000000000 0,0000152587890625000000000 0,0000076293945312500000000 0,0000000000000000000000000 0,0999984741210938000000000 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #42 Darstellungsgenauigkeit in verschiedenen Zahlensystemen(3) Programmbeispiel: Countdown mit Ganzzahlen (OK) public class Countdown1 { } public static void main( String[] args) { int i = 5; System.out.println( i); i = i-1; System.out.println( i); i = i-1; System.out.println( i); i = i-1; System.out.println( i); i = i-1; System.out.println( i); i = i-1; if (i==0) System.out.println( "Boom!"); } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #43 Darstellungsgenauigkeit in verschiedenen Zahlensystemen(4) Programmbeispiel: Countdown mit Kommazahlen (Fehlfunktion!) public class Countdown2 { } public static void main( String[] args) { double d = 0.5; System.out.println( d); d = d - 0.1; System.out.println( d); d = d - 0.1; System.out.println( d); d = d - 0.1; System.out.println( d); d = d - 0.1; System.out.println( d); d = d - 0.1; if (d==0) System.out.println( "Boom!"); // besser: d < 0.01 } Folgerung: Exakte Berechnungen (z.B. bzgl. Geld) immer mit Ganzzahlen durchführen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #44 Variablen © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #45 Variablen(1a) Maschinen verarbeiten Material, Programme verarbeiten Daten. Mit anderen Worten: Daten sind der Rohstoff mit dem Computer arbeiten (daher auch die Bezeichnung elektronische Datenverarbeitungsanlage, kurz EDV). Während man Materialien über Eigenschaften wie Härte oder Dichte charakterisiert, ist bei Daten insbesondere der Datentyp von Bedeutung. Formal umfasst die Beschreibung eines (abstrakten) Datentyps 1) Eine Menge von Werten (z.B. Zahlen) 2) Eine Menge von Operationen auf diesen Werten (z.B. Rechenoperationen) Daten, die mit einem Rechner verarbeitet werden sollen, müssen also auf einen geeigneten Datentyp abgebildet werden. Während dies für Zahlen direkt möglich ist, müssen andere Daten i.d.R. in geeigneter Form als Zahlen kodiert werden. So können z.B. Farben in die Grundfarben RGB zerlegt und deren Anteile numerisch repräsentiert werden. Für andere Daten wie z.B. Wochentage kann eine geeignete Kodierung selbst definiert werden, z.B. Sonntag=0, Montag=1, ... Dabei sind Konstanten bzw. die später vorgestellten Enums nützlich. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #46 Variablen(1b) Seit der imperativen Programmierung werden Daten in Variablen gespeichert und können immer wieder gelesen oder auch verändert werden. Die Verwaltung von Variablen erfolgt bei der Objektorientierung statt über Module über Klassen. Der Name einer Variablen soll nicht nur zur einen Speicherplatz eindeutig identifizieren sondern sollte auch aussagen, was der darin gespeicherte Wert bedeutet. Mühe für gute selbsterklärende Namen erspart Mühe für Kommentare! Neben dem (offensichtlich notwendigen) Namen ist der Datentyp ist die wichtigste Eigenschaft einer Variablen. Er legt fest für welche Art von Werten die Variable stehen kann und was man mit diesen Werten machen kann. Weitere wichtige Eigenschaften von Variablen sind: Lebensdauer (zu welchen Zeitpunkten ist die Variable generell verfügbar) ● Sichtbarkeit / Gültigkeit (wo ist der Name der Variablen bekannt) ● Zugriffsschutz (wo darf die Variable tatsächlich verwendet werden) ● Diese ergeben sich in Java implizit aus dem Ort der Deklaration (siehe Folie "Bestandteile einer Klasse"), der Zugriffsschutz (später) explizit über Modifier. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #47 Variablen(2) In Java müssen alle Variablen vor der ersten Verwendung bekanntgemacht bzw. deklariert werden. Dies ist nicht in allen Programmiersprachen so. Bei der Deklaration wird dem Namen der Variablen ein Datentyp fest zugeordnet. Grundsätzliche Syntax der Deklaration (Erweiterungen später) <Datentyp> <Variablenname>; Beispiel int i; Die Sichtbarkeit und Lebensdauer der Variablen ergibt sich aus dem Ort der Deklaration (innerhalb einer Funktion, innerhalb einer Klasse, ...). Im Unterschied zu älteren Programmiersprachen können Variablen in Java überall im Code deklariert werden. Dies erleichtert das Verständnis von Programmen, da auf diese Art und Weise Deklaration und Verwendung einer Variablen näher beieinander liegen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #48 Variablen(3) Variablen müssen vor ihrer ersten Verwendung zuerst initialisiert werden, d.h. man muss Ihnen einen Wert zuweisen. Die Verwendung von nicht initialisierten Variablen entspricht dem Griff in eine leeren bzw. nicht aufgeräumten Schublade (mit Ungeziefer drin). Dies war eine häufige Fehlerquelle in anderen Programmiersprachen. Um dem vorzubeugen, prüft der Java Compiler vor der Verwendung einer Variablen nach, ob eine Initialisierung erfolgt ist. Ist dies nicht der Fall generiert der Java Compiler einen Fehler. Die Initialisierung kann bereits im Rahmen der Deklaration geschehen: <Datentyp> <Variablenname> = <Startwert>; Die Initialisierung kann aber auch in einem separaten Schritt, d.h. durch Zuweisung eines Werts in einer eigenen Zeile erfolgen. <Datentyp> <Variablenname>; <Variablenname> = <Wert>; Zur Vereinfachung des Initialisierungsaufwands, inbesondere bei Objekten und deren Datenfeldern, gibt es für elementare Datentypen Defaultwerte. Der explizite Initialisierungszwang gilt also nur für lokale Variablen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #49 Beispiele: Verwendung von Variablen Beispiel public class Variables { public static void main(String[] args) { int i; // Eine Variable deklarieren i = 1; // Variable initialisieren char c1, c2; // Zwei Variablen deklarieren c1 = 'a'; // Variable initialisieren mit festem Wert c2 = c1; // Variable initialisieren mit anderer Variablen } int j = 1; // Variable deklarieren und sofort initialisieren j = i; // Wert der Variablen überschreiben } Variablen können mehrfach beschrieben (Zuweisung) und gelesen/verwendet (Ausdrücke, kommt als nächstes) werden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #50 Einschub: Speicherbereiche(1) In Java gibt es unterschiedliche Speicherbereiche: Programmspeicher (text segment/code segment) Inhalt: Programmcode (Methoden), Klassenvariablen (insb. Konstanten) Lebensdauer: dauerhaft; gesamte Programmlaufzeit Heap Inhalt: dynamisch (mit new) erzeugte Objekte inkl. deren Instanzvariablen Lebensdauer: variabel; von der Erzeuger bis zum „Wegwerfen“ der Referenz Stack Inhalt: Funktionsparameter und lokale Variablen, Rücksprungadressen Lebensdauer: begrenzt; bis zum Verlassen der Funktion Anders als in anderen Programmiersprachen muss sich der Programmierer in Java praktischerweise nicht um die Speicherverwaltung kümmern und kann beliebig Objekte anlegen - aufgeräumt werden Sie automatisch! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #51 Stack Funktionsparameter und lokale Variablen; begrenzte Lebensdauer; „viele“ Änderungen Programm Daten Einschub: Speicherbereiche(2) © Andreas Rau, 19.11.10 Heap Objekte inkl. Datenfelder; dynamische Lebensdauer; „wenig“ Änderungen Textsegment Programmcode, und Klassenvariablen; unbegrenzte Lebensdauer; keine Änderungen D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #52 Kopieren von Variablen(1) Kopieren von Variablen mit elementarem Datentyp: 3 int x, y; // Deklaration von 2 int-Variablen x = 1; y = x; y = 3; // Initialisierung von x mit dem Wert 1 // Kopieren von x in y (dannach y = 1) // Änderung von y (dannach y = 3) System.out.println(x); // liefert 1 System.out.println(y); // liefert 3 Der Wert von x wurde echt kopiert. Beide Variablen können unabhängig voneinander verändert werden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #53 Kopieren von Variablen(2) Kopieren von Variablen mit Referenztyp 17.6.1961 Datum d1, d2; d1 = new Datum(17, 6, 1961); // neues Objekt d2 = d1; // Objektreferenz kopieren d2.setzeTag(18); // Objekt durch d2 manipulieren System.out.println(d1); // liefert 18.06.1961 System.out.println(d2); // liefert 18.06.1961 Das Objekt wurde nicht kopiert, sondern nur die Referenz. Manipulationen betreffen beide Variablen – sie zeigen auf dasselbe Objekt. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #54 Kopieren von Variablen(3) Das Kopieren von Variablen ist also analog dem Kopieren von Schubladeninhalten: Bei Variablen mit elementarem Datentyp sind die Werte selbst in der Schublade hinterlegt. Bei Variablen mit Referenztyp ist der Wert auf dem Heap – die Schublade enthält nur eine Referenz. Nach dem Kopieren hat man also kein neues Objekt, sondern nur zwei Referenzen auf das ursprüngliche Objekt. X 1 D1 Y 3 D2 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #55 Analogie – Variablen und Datentypen Schubladen-Analogie: Werte = Teile mit einer bestimmten Form Variablen = Fächer mit einer bestimmten Form Je nach Form kann man verschiedene Dinge mit einem Teil machen (drehen, stapeln, ...). Die Teile passen aber nur in bestimmte Fächer. Manche Teile sind so groß, dass Sie gar nicht in ein Fach passen. In diesem Fall legt man die Teile an einem andern Ort ab und deponiert nur einen Zettel mit einem Verweis im Fach. Schubladendenken kann auch hilfreich sein! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #56 Analogien – Speicherorganisation Analogien stellen Bezüge zwischen der immateriellen Welt der Software und der realen Welt her. Allerdings sollte man sie nicht überbelasten... Nichtsdestotrotz können solche Bilder dem Gehirn helfen, sich besser mit der neuen Materie zurechtzufinden. Nach einiger Zeit braucht man sie dann nicht mehr. In der Vorlesung werden immer wieder Analogien zu einigen wichtigen Konzepten der Programmierung verwendet. Sie werden sicher nicht allen Lesern gleichgut gefallen, und es steht jedermann frei, sich eigene Eselsbrücken zu bauen (die funktionieren meist ohnehin besser als die von anderen Leuten). Wenn wir uns einen Schrank mit Schubladen vorstellen, dann ist eine Variable eine Schublade mit einem Namen und bestimmtem Typ ● ist ein Array eine feste Gruppe numerierter Schubladen mit gleichem Typ ● ist ein Rekord eine feste Gruppe benannter Schubladen mit gemischtem Typ ● In Java gibt es übrigends keine Rekords. Sie sind ein Konzept der imperativen Programmierung. Die Objekte in Java sind eine Erweiterung dieses Konzepts. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #57 Analogien - Sichtbarkeit von Variablen (FIXME) Wir stellen uns eine Welt mit vielen Hotels und Zimmern vor... Eine lokale Variable (in Java: Variable in einer Funktion oder Methode) ist eine Schublade in einem Zimmer. Sie ist nur im Zimmer zugänglich und wenn ein neuer Gast das Zimmer betritt hat das Zimmermädchen den Inhalt weggeräumt. Eine statische lokale Variable (in Java: Nachbildung über eine Instanzvariable) ist eine Schublade in einem Zimmer für Stammgäste. Jedesmal wenn der Gast wieder in sein Zimmer kommt hat Sie wieder ihren alten Inhalt. Eine modulglobale Variable (in Java: Klasse oder Datenelement ohne public) ist eine Schublade an der Rezeption des Hotels. Sie ist nur für Gäste des Hotels bzw. der Hotelkette und deren Tochterfirmen zugänglich und behält ihren Inhalt. Eine absolut globale Variable (in Java: Klasse oder Datenelement mit public) in einem Programm ist eine öffentliche Schublade in der Eingangshalle eines Hotels und für alle Gäste und Besucher zugänglich. Auch sie behält ihren Inhalt. Je besser eine Variable zugänglich ist, desto besser muss koordiniert werden! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #58 Konstanten Konstanten sind Variablen die man nicht ändern kann. Was für einen Sinn hat das, wenn ein Programm auf Zustandstransformationen auf Variablen als Grundprinzip basieren soll? Da Konstanten im Gegensatz zu Werten bzw. Literalen einen Namen haben können sie die Lesbarkeit eines Programms erhöhen: Den Bezeichner MAX_NUMBER_OF_USERS versteht man besser als den Wert 5 (no magic!). Außerdem erhöhen sie die Wartbarkeit eines Programms dadurch, daß man sie an mehreren Stellen referenzieren kann, d.h. man verwendet den Namen anstatt den Wert zu duplizieren. Steht die Konstante für einen einstellbaren Parameter muss man diesen also nur an einer Stelle ändern! Wie oben erwähnt sind Konstanten spezielle Variablen und haben all deren Eigenschaften, insbesondere Name und Datentyp, sind aber durch den Modifier final als Konstanten gekennzeichnet und müssen sofort initialisiert werden. Beispiel final double PI = 3.14; © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #59 Enums(1) In der Praxis braucht man oft Variablen mit endlich vielen Ausprägungen. Enums (Aufzählungstypen) wurden in Java 5.0 eingeführt, um diese endlich symbolisch statt numerisch kodieren zu können (wie true und false) und so die Lesbarkeit und Typsicherheit von Programmen zu verbessern. Hintergrund ist 1. Lesbarkeit (läßt sich durch symbolische Konstanten lösen ) Kein Mensch kann sich merken, dass Sonntag = 0 ist. 2. Typsicherheit (läßt sich nicht durch symbolische Konstanten lösen) Ausdrücke wie "Sonntag + Sommer" eigentlich keinen Sinn machen. Wenn aber Sonntag = 0 und Sommer = 2 ist das für den Computer total in Ordnung! Berechnungen wie Sonntag + 2 erscheinen zwar ganz praktisch (das Resultat wäre in diesem Fall der Dienstag), was aber passiert bei Sonntag + 9? Bei der Definition eines Enums werden nicht nur symbolische Namen festgelegt sondern gleichzeitig ein neuer Datentyp definiert. Dadurch können z.B. Wochentage von Jahreszeiten sauber unterschieden werden. Die Elemente des Enums sind die Literale dieses Datentyps! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #60 Enums(2) Die Definition eines Enums funktioniert in Java folgendermaßen: public enum Season { // in der Datei Season.java Spring, Summer, Autumn, Winter; } Anschließend kann man Variablen mit dem Aufzählungstyp deklarieren Season current = Season.Autumn; // ggf. in einer anderen Datei Die Kodierung der Konstanten im Hintergrund ist zwar immer noch numerisch, jedoch für den Programmierer nicht mehr direkt sichtbar bzw. relevant. Äpfel und Birnen mischen geht jetzt nicht mehr: current = current + 1; // das gibt was auf die Finger! Wie wir später sehen werden, haben Enums auch Ähnlichkeiten mit Klassen. Diese können ausgenutzt werden, um Berechnungen wie auf der vorhergehenden Folie wieder zu ermöglichen – und zwar sicher! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #61 Übung: Kodierung von Daten Welchen Datentyp würden Sie wählen, um folgende Daten zu kodieren? Das Alter einer Person ● Das Baujahr eines Autos ● Die Gültigkeit eines Tickets ● Das Gehalt eines Mitarbeiters ● Das Alter des Universums ● Den Namen einer Firma ● Eine Telefonnummer ● Die Farbe(n) einer Ampel ● Die Farbe eines Pixels auf dem Bildschirm ● Die Lottozahlen ● Das Geburtsdatum eines Studenten ● Hinweis: Neben den in diesem Abschnitt vorgestellten Datentypen enthält die Laufzeitbibliothek eine Reihe von nützlichen Klassen zur Repräsentation von häufig vorkommenden Daten. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #62 Nachtrag: Arrays(1) Arrays gibt es als festen Bestandteil in vielen Programmiersprachen. Auch Java unterstützt Arrays direkt durch bestimmte syntaktische Konstrukte wie ArrayLiterale und den in diesem Kapitel eingeführten Zugriffsoperator. Obwohl Sie sich Arrays in Java bei näherer Betrachtung als waschechte Objekte entpuppen sind sie genau wie Strings „magisch“ in dem Sinne, dass der Java Compiler uns durch spezielle Konstrukte einen einfacheren Umgang mit Ihnen erlaubt. Man merkt also quasi kaum, dass es Objekte sind. Durch die Realisierung von Arrays (wie auch Strings) als Objekte kann man ● typische Fehler (wie Zugriffe jenseits der Array-Grenzen) vermeiden ● eine Reihe von Hilfsfunktionen als Methoden anbieten (vgl. API-Referenz) So findet bei jedem Arrayzugriff (zur Laufzeit) eine Überprüfung anhand der Array-Grenzen statt. Die Größe des Arrays ist über eine öffentliche Konstante abfragbar. Dadurch kann man in Java sicherer Programmieren als z.B. in C. Der Index gibt die Anzahl der übersprungenen Elemente an (zählt ab 0...). Der letzte Index ist Arraygröße-1. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #63 Nachtrag: Arrays(2) Eindimensionale Arrays ia Deklaration int[] ia; // Der Typ von ia ist int-Array Int #0 Int #1 Int #2 Int #3 Initialisierung a) mit Literal int[] ia = {1, 6, 9}; // Länge implizit über Anzahl der Werte b) mit neuem Objekt int[] ia = new int[7]; // explizite Längenangabe Zugriff int i = ia[0]; // auslesen des 1-ten Elements (Index 0) ia[2] = 3; // befüllen des 3-ten Elements (Index 2) Die Felder eines Arrays sind Speicherplätze, also auch LValues! Arrayelemente werden je nach Typ mit den jeweiligen Default-Werten initialisiert. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #64 Nachtrag: Arrays(3) Mehrdimensionale Arrays iaa Deklaration int[][] iaa; // ein int-Array-Array Int[] #0 Int[] #1 Int #0 = 1 Int[] #2 Int #1 = 6 Initialisierung a) mit Literal Int #0 = 9 Int #1 = 2 int[][] iaa = { {1, 6}, {9, 2, 1}, {1}}; b) mit neuem Objekt Int #2 = 1 Int #0 = 1 int[][] iaa = new int[7][]; // explizite Längenangabe, hinten leer Zugriff int i = iaa[0][1]; // 2-tes Element aus 1-tem Array lesen iaa[2][3] = 3; // 4-tes Element aus 3-tem Array befüllen Durch Darstellung von mehrdimensionalen Arrays durch Verschachtelung von eindimensionalen Arrays müssen diese nicht quadratisch sein (Baum statt Tabelle). Die Größe untergeordneter Arrays kann ggf. später festgelegt werden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #65 Nachtrag: Arrays(4) Das gedankliche Modell hinter einem einfachen Array ist Liste/Tabelle. Die Implementierung erfolgt aber als Baum. Das kann man so oder so interpretieren: 1. Zeilenweise (1. Index = Zeile, üblich) 0,0 0,1 0 1,0 1,1 1 0 1 2 0 1 2 0 1 2 2 3 2. Spaltenweise (1. Index = Spalte) 0,0 1,0 0,1 1,1 0 1 2 3 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #66 Zugriff auf Arrays Arrayzugriff Mit Hilfe des Operators [ ] kann man also "in Arrays hineinschauen", entweder um einen Wert zu lesen oder zu schreiben. Dabei gibt die Zahl innerhalb der eckigen Klammern "die Nummer der Schublade" an (weitere Details folgen). Beispiel: // vorgefülltes Array aus Array-Literal erzeugen int[] lottotipp = { 2, 5, 17, 26, 31, 35}; // neues leeres Array mit new Operator erzeugen (nächste Folie) int[] lottozahlen = new int[6]; // Erste Lottozahl erzeugen lottozahlen[0] = Math.random() * (49-1+1) + 1; // Erste Lottozahl ausgeben System.println( "1. Lottozahl=" + lottozahlen[0]); Um die generierten Lottozahlen geschickt mit unserem Tipp vergleichen können fehlt uns an dieser Stelle leider noch das notwendige Handwerkszeug... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #67 Ausdrücke © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #68 Ausdrücke Ausdrücke dienen zur Berechnung von Werten (umgangssprachlich würde man sie wohl als Formeln bezeichnen). Sie werden einerseits in Zuweisungen an Variablen (Zustandsänderungen) und andererseits als Bedingungen in Kontrollstrukturen (Ablaufsteuerung) verwendet. Ausdrücke sind aus 2 Bestandteilen aufgebaut: Operanden ● Operatoren ● Die Operanden sind die Werte in einem Ausdruck, die Operatoren legen fest, was mit diesen Werten passieren soll. Oder andersrum: der Operator operiert auf dem Operand. Die meisten Ausdrücke lassen sich relativ intuitiv hinschreiben bzw. lesen, da die verwendeten Operatoren „alte Bekannte“ (aus der Schulmathematik) sind. Dies gilt insbesondere für arithmetische und logische Berechnungen sowie Vergleiche. Es gibt jedoch auch ein paar Besonderheiten... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #69 Operanden Operanden sind die Werte in einer Berechnung. In Java können dies Literale (feste Werte, z.B. 5) ● Variablen und Konstanten (gespeicherte Werte, z.B. a, Math.PI) ● Funktionen (berechnete Werte, z.B. Math.sin(Math.PI/2)). ● sein. Diese können an beliebiger Stelle in einem Ausdruck stehen. Operanden haben wie Variablen (die ja auch Operanden sind) einen Datentyp. Dieser legt fest, welche Operatoren zu ihrer Verknüpfung gültig sind. So kann man mit Zeichenketten z.B. nicht rechnen. Eine ungültigen Kombination von Operanden und Operatoren quittiert der Compiler mit einer Fehlermeldung die angibt, was er eigentlich erwartet (expected) hat, und was er tatsächlich vorgefunden (found) hat. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #70 Operatoren(1) - Überblick Operatoren stehen für eine Operation, d.h. die Art und Weise in der die Operanden verknüpft werden, um ein Ergebnis zu erhalten. Formal betrachtet sind Operatoren nur eine andere Schreibweise für Funktionen. Operanden entsprechen also Funktionsparametern. Damit könnte man also statt dem gewohnten 2+3 (symbolische Infix Notation) also auch 2 add 3 (Infix Notation) oder add(2,3) (Präfix Notation) oder (2,3,add) (Suffix Notation) schreiben. Die Infix Notation ist kürzer und für Menschen leichter lesbar. Für die maschinelle Auswertung ist jedoch die Präfix oder Suffix Notation praktischer (vgl. UPN Taschenrechner). Tatsächlich kommt jedoch jede dieser Schreibweisen in irgendeiner exotischen Programmiersprache vor... Bei der Betrachtung von Operatoren unterscheidet man folgende Eigenschaften: Stelligkeit (Anzahl der Operanden ~ Parameter) ● Priorität (Auswertungsreihenfolge) ● Assoziativität (Bindungsrichtung) ● Daneben ist es hilfreich, Operatoren nach ihrer Art zu gruppieren (vgl. Buch). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #71 Operatoren(2) - Eigenschaften Stelligkeit Gibt die Anzahl der Operanden an. Es gibt ● einstellige (unäre) Operatoren, z.B. Vorzeichen, logische Negation ● zweistellige (binäre) Operatoren, z.B. Addition (+), Multiplikation (*), ... ● dreistellige (ternäre) Operatoren, z.B. Bedingungsoperator (?:) Funktionen mit 4 und mehr Parametern schreibt man besser als Funktion! Priorität (siehe Tabelle) Bestimmt die Auswertungsreichenfolge (ähnlich wie „Punkt vor Strich“) Assoziativität (~Assoziativgesetz) Bestimmt die Auswertungsrichtung bei gleichwertigen Operanden (siehe Buch) ● Von links nach rechts, z.B. a+b+c ~ ((a+b)+c) ● Von rechts nach links, z.B. a = 2+3 ~ a = (2+3) Art (siehe Tabelle) Gibt die Art des Operators an, z.B. Arithmetisch, Logisch, ... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #72 Operatoren(3) – Übersicht und Demo Aufruf Zugriff Unär [] () . ++ -+ ~ ! (type) new * / % Arithmetik + + << Schieben >> >>> Arrayzugriff Methodenaufruf Komponentenzugriff Prä- oder Postinkrement Prä- oder Postdekrement Vorzeichen (unär) Bitweises Komplement Logische Negation Typ-Umwandlung (cast) Objekterzeugung Multiplikation, Division, Rest Addition, Subtraktion Stringverkettung Linkshift Rechtsshift (mit VZ) Rechtsshift (ohne VZ) < <= > >= Vergleich kleiner/kleiner gleich Vergleich größer/größer gleich Vergleich instanceof Typprüfung für Objekte == Gleichheit != Ungleichheit & Bitweises AND & Logisches AND (Complete) ^ Bitweises XOR ^ Logisches XOR Logik | Bitweises OR | Logisches OR (Complete) && Logisches AND (Shortcut) || Logisches OR (Shortcut) ?: Bedingung Bedingte Auswertung = Einfache Wertzuweisung Zuweisung <op>= Kombinierte Zuweisung Die Prioritäten sind so sinnvoll gewählt, dass es bei komplexen gemischten Ausdrücken keine unliebsamen Überraschungen gibt. Mit anderen Worten: An vielen Stellen sind keine Klammern nötig. Klammern erhöhen aber oftmals die Lesbarkeit und sollten deshalb ggf. trotzdem verwendet werden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #73 Der Zuweisungsoperator Der Zuweisungsoperator hat die niedrigste Priorität (Werte müssen vor der Zuweisung berechnet werden) und ist der einzige Operator, der von rechts nach links ausgewertet wird (wichtig für Kettenzuweisungen). Alle anderen Operatoren werden von links nach rechts, d.h. in Leserichtung ausgewertet. Beispiele int i = i = i = j = i, j, k; 5; // einfache Zuweisung 5*2; // zuerst Berechnung, dann Zuweisung j = k = 2; // Kettenzuweisung 4 * (k = 2); Wie das letzte Beispiel zeigt, kann man Zuweisungen auch als Teil einer Berechnung vornehmen. Dies ist jedoch der Lesbarkeit nicht unbedingt zuträglich und passiert in der Praxis öfter versehentlich als absichtlich! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #74 Kombinierter Zuweisungsoperator Um weniger schreiben zu müssen, kann der Zuweisungsoperator mit einem binären Operator (z.B. +, *, /, %, &, |, <<, >>, ...) kombiniert werden: op1 <op>= op2 entspricht op1 = op1 <op> op2 Beispiele int a a += a *= a %= a |= a &= a <<= = 1; 2; // 8; // 5; // 8; // 8; // 2; // a a a a a a = = = = = = a + a * a % a | a & a << 2 8 5 8 8 2 ==> ==> ==> ==> ==> ==> a a a a a a = 3 = 24 = 4 = 12 = 8 = 32; Aus Gründen der Lesbarkeit ist es allerdings oftmals besser, die ausführliche Schreibweise zu verwenden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #75 Inkrementoperator (und Dekrementoperator) In Java gibt es verschiedene Möglichkeiten zum Hochzählen einer Variablen: a = a + 1; // ~ a_neu = a_alt + 1, d.h. erhöht den Wert von a a += 1; // Abkürzung als kombinierte Zuweisung a++; // Abkürzung mit Inkrement-Operator (nur 1er Schritte) Den Inkrementoperator gibt es in zwei verschiedenen Formen: Präinkrement ( Wert (anders) = neuer Wert; Effekt (gleich) = Erhöhung) int a,b; a = 1; b = ++a; // erst (prä) a=a+1 dann b=a, d.h. b ist 2, a ist 2 Postinkrement ( Wert (anders) = alter Wert; Effekt (gleich) = Erhöhung) int a,b; a = 1; b = a++; // erst b=a dann (post) a=a+1, d.h. b ist 1, a ist 2 Beide haben neben ihrem direkten Ergebnis (neuer bzw. alter Wert von a) noch einen weiteren Effekt (Veränderung von a) und gehen daher nur für LValues! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #76 Inkrementoperator (und Dekrementoperator) Beispiel int a = 5; int b = 7; // a erhöhen ++a; // a=6, b=7 // b = a_alt, a erhöhen b = a++; // a=7, b=6 // b erhöhen, a=b_neu - 2 a = ++b – 2; // a=5, b=7 // b = a_alt*3, a erhöhen b = a++ * 3; // a=6, b=15 // a erhöhen, c=a_neu + b_alt, b erhöhen int c = (++a) + (b++); // a=7, b=16, c=22 // b erhöhen, c = a_alt + b_neu, a erhöhen c = (a++) + (++b); // a=8, b=17, c=24 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #77 Inkrementoperator (und Dekrementoperator) Beispiel i = 5; System.out.println( "Postinkrement"); System.out.println( 4 * i++); // 4 * 5 System.out.println( i); i = 5; System.out.println( "Präinkrement"); System.out.println( 4 * ++i); // 4 * 6 System.out.println( i); © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #78 Inkrementoperator (und Dekrementoperator) Beispiel // schwer verständliche Fälle i = 6; System.out.println( 4 * i++ * i); // 4 * 6 * 7 = 168 System.out.println( i); i= 6; System.out.println( 4 * i * i++); // 4 * 6 * 6 = 144 System.out.println( i); i = 6; System.out.println( System.out.println( i = 6; System.out.println( System.out.println( 4 * ++i * i); // 4 * 7 * 7 = 196 i); 4 * i * ++i); // 4 * 6 * 7 = 168 i); Fazit: In der Praxis lieber in mehreren Schritten (nicht überoptimieren) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #79 Einschub: Zuweisungen – RValues und LValues Zuweisungen sind streng genommen keine Ausdrücke sondern Anweisungen (passen aber ganz gut hierher). Bei Ihnen muss man unterscheiden zwischen LValue = Wert der auf der links vorkommen kann (mit Speicherplatz) ● RValue = Wert, der auf der rechts vorkommen kann (ohne Speicherplatz) ● Eigentlich logisch, denn zum zuweisen(=speichern) braucht man eben einen Speicherplatz. In Java können nur Variablen (inkl. Arrays!) als LValues auftreten. Im rechten Teil einer Zuweisung sind dagegen alle Arten von Operanden erlaubt. Damit gilt für die Syntax von Zuweisungen: <Variable> = <Ausdruck>; Beispiele: a = 5; x = a + 2; v = sin(a); lottozahlen[1] = 49; // !!! © Andreas Rau, 19.11.10 Operanden RValues Literale Funktionen LValues Variablen D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #80 Der Bedingungsoperator ?: Der Bedingungsoperator ist in mehrerer Hinsicht speziell: Er ist der einzige Operator mit mehr als 2 Operanden und besteht aus 2 getrennten Operationsymbolen. Sein Aufbau ist (<Bedingung> ? <Wert1> : <Wert2>) Die Bedingung muss ein Ausdruck vom Typ boolean sein, die Typen von Wert1 und Wert2 sind beliebig, müssen aber gleich sein. Die Klammern sind kein Bestandteil des Bedingungsoperators selbst, sind aber oft notwendig um die Operanden des Bedingungsoperators vor anderen Operatoren zu schützen – er hat nämlich mit die niedrigste Priorität Der Wert eines bedingten Ausdrucks ist Wert1 wenn die Bedingung wahr ist ● Wert2 wenn die Bedingung falsch ist ● Beispiel int abs = ( z >= 0 ? z : -z); // berechnet den Betrag von z © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #81 Arithmetische Operatoren Bei den arithmetischen Operatoren finden wir in erster Linie alte Bekannte aus der Mathematik: Addition + ● Subtraktion ● Division / ● Multiplikation * ● Zu beachten ist jedoch, daß der Divisionsoperator je nach Datentyp eine andere Bedeutung hat: Bei Fließkommazahlen (z.B. double) wird eine echte Division durchgeführt, bei Ganzzahlen (z.B. int) wird nur das Ganzahlergebnis berechnet. Zur Ermittlung des Teilungsrests gibt es einen neuen Operator ● Modulo % Beispiele 5.0 / 2 = 2.5; // Gleitkomma Divisionsoperator 5 / 2 = 2; // Ganzzahl Divisionsoperator (DIV) 5 % 2 = 1; // Ganzzahl Modulo Operator (MOD) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #82 Arithmetische Operatoren und Typkonvertierungen(1) Beim Rechnen mit verschiedenen numerischen Datentypen wird automatisch mit der höchsten Genauigkeit gerechnet, d.h. der genaueste Datentyp in einem Ausdruck bestimmt den Datentyp des Resultats. double > float > long > int > byte > char Alle anderen Werte werden automatisch in diesen Datentyp konvertiert. Diese automatische Typkonvertierung funktioniert nur „von unten nach oben“ und wird auch Beförderung (Promotion) genannt. In Gegenrichtung würde dagegen ein Informationsverlust auftreten. Deswegen funktioniert zwar die Zuweisung von int nach long aber nicht umgekehrt. Hier ist eine manuelle Konvertierung notwendig. long x = 34; // geht int y = 34L; // geht nicht (possible loss of precision) Der Datentyp auf der linken Seite einer Zuweisung wird jedoch bei der automatischen Typkonvertierung bei der Berechnung noch nicht berücksichtigt. float f = 2/3; // rechts nur int, also rechnen mit int. Ergebnis ist gleich Null! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #83 Arithmetische Operatoren und Typkonvertierungen(2) Bei einer manuellen Typkonvertierung muss der Zieldatentyp explizit mit Hilfe des cast-Operators angegeben werden. Der Compiler unterstellt dem Programmierer dann, dass er weiss was er tut, und erspart sich Fehlermeldungen wegen möglichem Informationsverlust. Beispiel: int i = (int)5L; // expliziter cast von long nach int Später werden wir sehen, dass der cast-Operator auch zum konvertieren (genauer: uminterpretieren) von Objektreferenzen verwendet werden kann. So kann man z.B. ein Auto als Fahrzeug interpretieren. Ein solcher upcast (in der Vererbungshierarchie nach oben) ist streng genommen nicht nötig („gelingt immer und klebt nicht“). Anders sieht das bei downcasts aus (in der Vererbungshierarchie). Wenn wir versuchen ein Objekt das wir momentan „durch die Fahrzeug Brille“ betrachten plötzlich als Auto zu interpretieren, es sich in wirklichkeit aber um ein Boot handelt, gibt das einen Laufzeitfehler. Diese Richtung ist also mit Risiken behaftet. Das ist im Prinzip genauso wie bei den Zahlen (up = zum allgemeineren, d.h. größeren Typ). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #84 Arithmetische Operatoren und Typkonvertierungen(3) Beispiele bzgl. „loss of precision“ (Zuweisung „groß an zu klein“) // Überzählige Bits werden abgeschnitten int i = 256; byte b = (byte)i; System.out.println( “b=“ + b); // ergibt 0 00000000 00000000 00000001 00000000 int (4 Byte) 00000000 byte (1 Byte) Nach dem Abschneiden bleibt nichts übrig // Achtung: Das übrige Bit wird als Vorzeichenindikator uminterpretiert int i = 128; byte b = (byte)i; Vorzeichen-Indikator wird 1; Deutung nach 2erKomplement System.out.println( “b=“ + b); // ergibt -128 00000000 00000000 00000000 10000000 int (4 Byte) 10000000 byte (1 Byte) © Andreas Rau, 19.11.10 10000000 = negativ 01111111 Komplement +1 +1 10000000 = 128 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #85 Relationale Operatoren Relationale Operatoren vergleichen zwei Werte und setzen sie somit in Beziehung (Relation). Das Ergebnis ist dabei stets ein Wahrheitswert. Die verwendeten Operatorsymbole unterscheiden sich etwas von den aus der Mathematik gewohnten, sind aber trotzdem einigermaßen verständlich: Beispiele (alle true) int a = 2; System.out.println( System.out.println( System.out.println( System.out.println( System.out.println( System.out.println( a a a a 2 5 < 3); <= 2); > 0); >= 1); == a); != a); // // // // // // kleiner kleiner gleich größer größer gleich gleich (Schubladeninhalt!) ungleich ('!' = Negation) Hinweis: Das Symbol '=' hat bereits die Bedeutung 'Zuweisung'. Daher wird zum direkten Vergleich '==' verwendet. Durch die Umkehr der Operandenreihenfolge kommt es bei Schreibfehlern nicht zu einer unerwünschten Zuweisung. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #86 Logische Operatoren Die logischen Operationen AND und OR stehen in Java in zwei Ausprägungen zur Verfügung: Einer "effizienten" und einer "gründlichen". Die "effiziente" Variante (&& bzw. ||) bricht die Berechnung ab, sobald das Ergebnis feststeht. Dies ist dann der Fall, sobald in einem AND Ausdruck der Wert false bzw. in einem OR Ausdruck der Wert true auftaucht. Weitere Operanden werden dann nicht mehr betrachtet und so unnötiger Rechenaufwand, z.B. zur Auswertung von Teilausdrücken, vermieden. Die "gründliche" Variante (& bzw. |) wertet stets alle Operanden aus. Dieses scheinbar sinnlose Vorgehen ist immer dann wichtig, wenn die Auswertung eines Operanden neben dem Rechenergebnis noch weitere Effekte hat. Diese werden als Seiteneffekte bezeichnet. Solche Seiteneffekte sollten zwar generell vermieden werden, dies ist jedoch nicht immer möglich. Typische Beispiele für solche Seiteneffekte sind Änderungen von Variablen oder Ausgaben auf dem Bildschirm oder in Dateien / Datenbanken. Die Operatoren NOT (!) und XOR (^) gibt es nur in einer Ausprägung (Warum?). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #87 Sinnvolle Prioritäten? Um gemischte Ausdrücke leichter verstehen und manuell berechnen zu können, merke man sich folgende Auswertungsreihenfolge: Arithmetische Operatoren (Rechnen) vor ● Relationalen Operatoren (Vergleichen) vor ● Logischen Operatoren (Verknüpfen) ● Variablen werden vor Beginn der Auswertung eingesetzt, Funktionen werden berechnet, wenn alle ihre Parameter berechnet sind(=verfügbar sind), Zuweisungen erfolgen erst nach kompletter Auswertung des Ausdrucks. Beispiel (mit a=1, b=2, c=3, d=Math.PI) bool b = a+b<5 && c != 0 // = 1+2<5 && 3 != 0 // = 3<5 && 3 != 0 // = true && true // = true // b = true © Andreas Rau, 19.11.10 && && && && Math.sin(d/2) > 0; Math.sin(Math.PI/2) > 0 1 > 0 true Einsetzen Rechnen Vergleichen Verknüpfen Zuweisen D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #88 Bitoperatoren Bitoperationen operieren auf den Bits einer (Ganz)zahl. Um zu verstehen, was eine Bitoperation mit einer Zahl macht muss man sich daher auf die Bitebene begeben. Beispiele 17 & 4 = 10001 & 00100 = 00000 = 0 12 | 13 = 1100 | 1101 = 1101 = 13 (neutrales Element = 1) (neturales Element = 0) Bitoperationen verhalten sich also im Prinzip genauso wie die logischen Operatoren. Statt true und false verknüpfen sie 1 und 0 – und zwar mehrere auf einmal (wieviele hängt wieder von der Anzahl der Bits im Datentyp ab). Dabei bezeichnet man den zweiten Operanden oft als (Bit)maske. Diese wird zur besseren Klarheit i.d.R. in Hex angegeben. Mit Hilfe der Bitoperationen kann man insbesondere einzelne Bits gezielt ein- (ODER mit 1er Maske) und ausschalten (UND mit 0er Maske). Letzteres kann auch dazu verwenden, gezielt einzelne Bits zu testen (Bit war gesetzt, wenn Ergebnis ungleich 0). Beispiele 13 & 0x03 = 1101 & 0100 = 0100 = 4 // Bit 2 (2^2=4) testen 13 | 0x02 = 1101 | 0010 = 1111 = 15 // Bit 1 (2^1=2) setzen 13 & ~0x08 = 1101 | 0111 = 0101 = 5 // Bit 3 (2^3=8) löschen Zum veranschaulichen und üben ist auf der Homepage der BitManipulator verfügbar. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #89 Schiebeoperatoren Schiebeoperatoren arbeiten auf der Binärdarstellung von Ganzzahlen. Aufgrund des Stellenwertsystems werden beim Verschieben nach links bzw. rechts alle Stelle um eine Potenz auf- bzw. abgewertet, d.h. beim Bitschieben gilt ● linksschieben ~ *2 ● rechtsschieben ~ /2 Doch warum gibt es beim Rechtsschieben zwei verschiedene Operatoren? Bei der Anwendung des Zweierkomplements fällt auf, dass für negative Zahlen das MSB (most significant bit – das höchstwertige Bit ganz links) stets 1 ist. Damit ist es geeignet, das Vorzeichen anzuzeigen (ist aber kein Vorzeichenbit!). Um das Vorzeichen zu erhalten, muss das jeweils richtige Bit reingeschoben werden (beim vorzeichenlosen shiften wird links wie rechts 0 reingeschoben). Beispiel +5>>1 == 0101>>1 == 0010 == +2 (Ganzzahldivision durch 2) -5>>1 == 1011>>1 == 1101 == -3 (Mit VZ 1101, ZK: 0010, 0011 = 3!) -5>>>1== 1011>>1 == 0101 == +5 (Ohne VZ, 1011 als 11 interpretiert) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #90 Mathematische Funktionen(1) Zum Rechnen sind neben Operatoren hin und wieder auch mathematische Grundfunktionen wie sin(), cos() oder auch Konstanten wie PI oder e notwendig. Diese stehen auch in Java zur Verfügung. Allerdings sind kein Bestandteil der Sprachsyntax von Java sondern "ganz normal" über eine Klasse implementiert. Diese Klasse heißt Math und ist Teil der Java Bibliothek (vgl. API Doku). Alle in dieser Klasse enthaltenen mathematischen Funktionen und Konstanten sind als Klassenmethoden bzw. Klassenvariabeln realisiert. Der Zugriff darauf ist direkt über die Klasse möglich. Beispiele Math.PI Math.sqrt(2) Das "Math." ist immer notwendig, es gibt keine "freischwebenden" Funktionen! Weitere Funktionen und Konstanten kann man in der Online-Hilfe (die werden wir ab sofort noch öfters brauchen. Kann sich ja kein Mensch alles merken...) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #91 Mathematische Funktionen(2) Erzeugung von Zufallszahlen Zur Erzeugung von Zufallszahlen existiert in der Klasse Math die Funktion random(). Diese liefert eine Gleitkomma Zufallszahl zwischen 0 inkl. und 1 exkl. Um ganzzahlige Zufallszahlen in einem anderen Bereich zu erzeugen, z.B. zwischen 5 und 10, sind weitere Rechenoperationen notwenig: Grundfunktion Math.random() liefert [0...1[ Intervall vergrößern (aufblasen) Math.random()*6 liefert [0...6[ Intervall verschieben Math.random()*6 + 5 liefert [5...11[ Ganzzahlige Werte ausschneiden (int)(Math.random()*6 + 5) liefert [5, 6, 7, 8, 9, 10] Dabei ist 6 = Obergrenze – Untergrenze + 1 und 5 = Untergrenze (Warum nur?) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #92 Erzeugung und Zugriff auf Objekte new-Operator Der new-Operator dient zur Erzeugung und Initialisierung von Objekten. Dazu sind neben dem Operator mindestens der Klassenname nötig (später mehr). Punktoperator Mit Hilfe dieses Operators kann man "in Objekte hineinschauen". Dabei sind zwei Dinge zu beachten: ● ● Objekte sind wie Rekords, d.h. ihre Elemente haben Namen. Also muss hinter dem Punkt ein Name stehen (und davor ein Objekt. Immer!) Objekte können nicht nur Daten sondern auch Methoden enthalten. Um sie verwenden zu können wird zusätzlich der Funktionsaufruf-Operator benötigt. Beispiele Person p = new Person(); // Objekt vom Typ Person erzeugen p.name = "Mustermann"; // name der Person setzen p.vorname = "Max"; // vorname der Person setzen Natürlich geht das in der Praxis meist anders (nämlich über Zugriffsmethoden), da ein direkter Zugriff ja das Prinzip der Kapselung verletzen würde. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #93 Aufruf von Funktionen Funktionsaufruf-Operator Dieser Operator dient, nunja, zum Aufruf von Funktionen. Diese sind in Java grundsätzlich in einem Objekt oder einer Klasse enthalten und werden als Methoden bezeichnet. Daher tritt der Funktionsaufruf meist in Verbindung mit dem Punktoperator auf (Ausnahmeregeln lernen wir später kennen). Beispiel System.out.println( Integer.toString()); // Integer ausgeben Dieses Beispiel demonstriert außerdem die Möglichkeit, Objektzugriffe mit dem Punktoperator zu verketten. Dabei gilt weiterhin, dass links vom Punkt immer ein Objekt stehen muss. Dies bedeutet, dass das Datenfeld der Klasse System System.out ein Objekt sein muss, welches eine Methode namens println() enthält. Dies wird durch einen Blick in die API Doku bestätigt (wird sowieso Zeit, dass wir die kennenlernen – siehe Klassen System und PrintStream). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #94 Vorschau: Kopieren von Werten bei Funktionsaufrufen(1) Ablauf eines Funktionsaufrufs public class Doppler { public static int verdoppeln( int wert) { int dasDoppelte = wert * 2; return dasDoppelte; } } public static void main(String[] args) { int x, y; x = 1; 1. wert = x (aktueller Parameter) y = verdoppeln( x); 2. Funktion ausführen 3. y = dasDoppelte (Rückgabewert) System.out.println( y); } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #95 Vorschau: Kopieren von Werten bei Funktionsaufrufen(2) Für das Kopieren der Parameter und des Rückgabewerts beim Funktionsaufruf gelten dieselben Regeln wie für das Kopieren von Variablen: Primitive Datentypen werden als Wert übergeben. Änderungen des Wertes innerhalb der Funktion haben also außerhalb der Funktion keine Auswirkungen. Referenztypen werden als Referenz übergeben (Call-by-Reference). Änderungen des Objekts innerhalb der Funktion verändern das Originalobjekt, d.h. sie wirken sich auch außerhalb der Funktion aus (die Referenz bleibt jedoch unverändert!). Um unabsichtliche Fehler zu vermeiden ist es wichtig, diesen Unterschied zu kennen. Ungeachtet dessen haben beide Aufrufvarianten ihren Sinn: Kopien von primitiven Datentypen sind "billig", Objekte zu kopieren wäre i.d.R. zu teuer. Daher werden primitive Werte als Kopie übergeben (und ggf. nach Veränderung explizit zurückgegeben) während Objekte direkt verändert werden können. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #96 Nachtrag: Wrapperklassen Auch beim Umgang mit elementaren Datentypen benötigt man gelegentlich die eine oder andere zusätzliche Funktion. Da man diese nicht in den elementaren Typen selbst unterbringen kann, gibt es zu jedem elementaren Datentypen eine zugehörige sog. Wrapperklasse die diese Methoden enthält (analog zu Math). Beispiele (für int / Integer, weitere Wrapper Klassen siehe Buch) int max = Integer.MAX_VALUE; // größten Wert des Typs abfragen int i = Integer.parseInt( "17"); // Zeichenkette "als int deuten" String s = Integer.toString( i); // Zahl in Zeichenkette wandeln Der Name Wrapperklasse kommt daher, dass die Objekte dieser Klassen auch dazu verwendet werden können, um Werte des zugehörigen Typs "als Objekt zu verpacken" - rechnen kann man damit Objekten dann allerdings nicht mehr. Beispiele Integer i = new Integer( 5); // einpacken int j = i.intValue(); // auspacken Ab Java 5.0 erfolgt dieses Ein- und Auspacken automagisch (Autoboxing). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #97 Nachtrag: Wrapperklassen(2) x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x toHexString() x x x x x x x toOctalString() x x x x x x x x x toBinaryString() x toString() valueOf() x x x x x x x x x x x x x x x decode() doubleValue() floatValue() longValue() intValue() shortValue() byteValue() charValue() booleanValue() parseDouble() parseFloat() parseLong() parseInt() parseShort() parseByte() Wrapperklasse Boolean x Character Byte Short Integer Long Float Double parseChar() Datentyp boolean char byte short int long float double parseBoolean() Übersicht über alle Wrapperklassen und deren wichtigste Eigenschaften: x x x Boolean und Character sind speziell; die Wrapperklassen für Zahlen (Numbers) haben viele gemeinsame Eigenschaften. Integer bietet spezielle Unterstützung für verschiedene Zahlensysteme. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #98 Nachtrag: Die Hilfsklasse Arrays Ähnlich wie die Wrapperklassen für primitive Datentypen bietet die Hilfsklasse Arrays einige nützliche Funktionen für den Umgang mit Arrays, z.B. zum durchsuchen und zum sortieren. Einige ausgewählte Funktionen (weitere siehe API Doku) import java.util.Arrays; public class ArrayFunctions { } public static void main( String[] args) { int[] ia = { 1, 6, 3, 8, 4, 9}; System.out.println( Arrays.toString( ia)); Arrays.sort( ia); // sortiert das Array System.out.println( Arrays.toString( ia)); int i = Arrays.binarySearch( ia, 3); // liefert Index 1(!) System.out.println( "Found 3 at index " + i); Arrays.fill( ia, 0); // füllt das Array mit 0 System.out.println( Arrays.toString( ia)); } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #99 Nachtrag: Strings Stringfunktionen kurz vorgestellt Zeichenketten werden in Java als Objekte der Klasse String repräsentiert (und nicht etwa als Array von Zeichen – der Grund dafür ist die Kapselung). Mit diesen Objekten kann man zwar nicht "rechnen" aber doch einige interessante Dinge tun. Die zugehörigen Operationen sind als Funktionen implementiert die in der Klasse String zu finden sind (hier wird der Vorteil der Zusammenfassung von Daten und Methoden zu Klassen deutlich). Einige praktische Stringfunktionen (weitere siehe API Doku) String s = "das ist eine Zeichenkette"; s.length(); // Länge der Zeichenkette ermitteln s.charAt(1); // einzelne Zeichen lesen (schreiben ist nicht!) s.indexOf( 'e'); // Zeichen suchen vorwärts (liefert 8) s.lastIndexOf( 'e'); // Zeichen suchern rückwärts (liefert 24) s.substring(4,7); // Teilstring, hier "ist" s.equals( "eine andere Zeichenkette"); // Vergleich, hier false Anmerkungen: Strings in Java sind nicht veränderbar (immutable). Daher kann der Compiler gefahrlos identische Stringliterale im Speicher zusammenfassen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #100 Beispielprogramme(1) Mit Ausdrücken kann man bereits eine Menge anfangen, z.B. Zahlen konvertieren (Konverter1.java) ● Zahlen konvertieren (Konverter2.java) ● Zahlen konvertieren (Konverter3.java) ● Zahlen konvertieren (Konverter4.java) ● Zahlen konvertieren (Konverter5.java) ● Man kann natürlich auch noch andere Dinge tun ;-). Aber das Beispiel passt zu dem vorher behandelten Lernstoff über Zahlensysteme ● demonstriert die Anwendung verschiedener Operatoren ● zeigt eindrucksvoll: "there's more than one way to do it"* ● Die obigen Beispielprogramme verwenden zur Eingabe von der Tastatur ein Objekt einer Hilfsklasse namens "CommandLine". Diese Klasse kapselt Techniken, die wir erst später kennenlernen werden. *dieser Slogan gehört eigentlich zu einer anderen Programmiersprache, der Skriptsprache Perl © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #101 Kontrollstrukturen © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #102 Kontrollstrukturen(0) Wie wir wissen sind Computer universelle oder zumindest sehr flexible Maschinen. Diese Flexibilität rührt daher, dass sie programmierbar sind, hat also etwas mit den Programmen zu tun. Dies hat im wesentlichen 3 Gründe 1. Man kann verschiedene Programme ablaufen lassen Dies führt offensichtlich zu Flexibilität, weil verschiedene Programme natürlich auch verschiedene Dinge tun können. 2. Ein Programm kann benutzerdefinierte Eingaben verarbeiten Hierzu kann man ein Programm als spezielle Formel interpretieren (die wir mit Hilfe von Ausdrücken implementieren können). Durch verschiedene Eingaben kann diese Formel auch unterschiedliche Ausgaben produzieren. 3. Ein Programm kann verschiedene Berechnungspfade enthalten Anders als die mathematischen Formeln die wir kennen, kann ein Programm Varianten enthalten, z.B. um Sonderfälle zu behandeln. Man kann also ähnliche Formeln in einem Programm zusammenfassen. Die dazu notwendigen Konstrukte werden auf den folgenden Seiten beschrieben. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #103 Kontrollstrukturen(1) Kontrollstrukturen dienen der Ablaufsteuerung. Während Ausdrücke festlegen, was passiert, legen Kontrollstrukturen fest wann bzw. in welcher Reihenfolge. In der Theorie kennt man 3 elementare Arten von Kontrollstrukturen. Dies sind: Die Sequenz – Schritte aufzählen (Blöcke) ● Die Verzweigung - Alternativen auswählen (if-else, switch-case) ● Die Wiederholung – Dinge mehrmals tun (while, for, do-while) ● Praktisch werden diese in Java durch mehr als 3 Konstrukte abgedeckt. Hintergrund dieser elementaren Kontrollstrukturen ist die sog. strukturierte Programmierung. Dabei soll der Begriff strukturiert (das Gegenteil von unstrukturiert) andeuten, dass dem resultierenden Programm eine Struktur zugrundeliegen soll die durchdacht, erkennbar und verständlich ist. Im Gegensatz dazu spricht man von sog. Spaghetti-Code, wenn die Programmstruktur nicht nachvollziehbar ist – ein Nebeneffekt des hemmungslosen Einsatzes von Sprunganweisungen (goto). In diesem Zusammenhang gibt es auch den Begriff des "Kündigungsschutzprogramms"... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #104 Kontrollstrukturen(2) Sequenz / Blöcke Eine Sequenz ist eine Folge von Anweisungen die – wie ein Kochrezept – Schritt für Schritt und ohne wenn und aber abgearbeitet werden sollen und werden in Java werden durch Blöcke realisiert. Eigenschaften von Blöcken ● Blöcke bündeln mehrere Anweisungen zu einer Sequenz und können überall dort stehen, wo eine einzelne Anweisung erwartet wird. Der Inhalt eines Blocks wird dabei durch die beiden geschweiften Klammern begrenzt. Am häufigsten werden sie verwendet um mehrere Anweisungen im Inneren anderer Kontrollstrukturen unterzubringen. ● Es ist möglich, innerhalb eines Blocks Variablen zu deklarieren, die außerhalb des Blocks nicht mehr gültig sind. Blöcke bilden also einen eigenen Gültigkeitsbereich. Somit kann man Hilfsvariablen dort definieren, wo man sie braucht, ohne Platz im Speicher oder im Namensraum zu verschwenden. Gleichzeitig verbessert sich die Lesbarkeit, wenn Deklaration und Verwendungsort von Variablen nahe beeinander liegen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #105 Kontrollstrukturen(3) Noch: Eigenschaften von Blöcken ● Blöcke können verschachtelt werden. Die resultierenden Gültigkeitsbereiche sind dann ebenfalls geschachtelt. Dabei gilt, die inneren Blöcke "sehen" die äußeren Variablen, aber nicht umgekehrt. public static void main(String[] args) { int i; { int j; { int k; // hier sind i, j und k sichtbar } // hier sind nur i und j sichtbar } // hier ist nur i sichtbar } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #106 Kontrollstrukturen(4) Verzweigung / if-else, switch-case Eine Verzweigung ist gewissermaßen eine Weggabelung. Sowas kommt in Kochrezepten eher selten vor. In Programmen erlaubt uns dies, auf verschiedene Gegebenheiten zu reagieren bzw. verschiedene Fälle zu unterscheiden. Verzweigungen werden in Java werden durch if-else- und switchcase-Konstrukte realisiert. Eigenschaften von if-else ● Ein if-else-Konstrukt besteht aus einer Bedingung und einem oder zwei Anweisungszweigen, wobei der zweite optional ist. Ist die Anweisung wahr, wird der erste Zweig durchlaufen, andernfalls der zweite. Mit if-else lassen sich also entweder-oder Entscheidungen darstellen. ● Es ist möglich, if-else-Konstrukte zu verschachteln, indem innerhalb des if oder else-Zweigs weitere if-else-Konstrukte untergebracht werden. Auf diese Weise lassen sich Entscheidungsketten bildet. Damit kann man mehr als zwei Fälle unterscheiden bzw. sich mit immer spezielleren Fragen seinem Ziel nähern. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #107 Kontrollstrukturen(5) public static void main(String[] args) { int i = (int)((Math.random() * 10) + 1); if ( (i%2)==0) { System.out.println("Gerade!"); } else { System.out.println("Ungerade!"); } } Es empfielt sich, den if- und den else-Teil grundsätzlich als Block zu klammern. Dadurch verbessert man die Lesbarkeit und vermeidet außerdem Fehler beim "hinzufügen" von Anweisungen (sog. "dangling else"-Problem) if ( (i%2)==0) System.out.println("Gerade!"); else System.out.println("Ungerade!"); i=0; // das passiert immer, nicht nur bei else! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #108 Kontrollstrukturen(6) Beispiel für beidseitig geschachtelte if- else- Anweisungen. Damit kann man sich einer Diagnose Schritt für Schritt nähern oder einen Ablauf steuern, z. B. einen Benutzerdialog. Hat gegenüber komplexeren Bedingungen den Vorteil der leichteren Verständlichkeit, kommt im Zeitalter der grafischen Oberflächen aber in der Praxis dennoch eher selten vor. Frage 1 ja ja Frage 2a Aktion 3aa © Andreas Rau, 19.11.10 nein nein ja Frage 2b Aktion 3ab Aktion 3ba nein Aktion 3bb if (<Frage1>) { if (<Frage2a>) { <Aktion3aa>; } else { <Aktion3ab>; } } else { if (<Frage2b>) { <Aktion3ba>; } else { <Aktion3bb>; } } D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #109 Kontrollstrukturen(7) Beispiel für einseitig geschachtelte if- else- Anweisungen Damit kann man nahezu beliebige Fallunterscheidungen darstellen. Aktion 4 repräsentiert hierbei den sonst- Fall (keiner der vorigen Fälle traf zu). Oft als Ersatz für switch-case! Theoretisch ist eine einseitige Verschachtelung auch im if- Zweig möglich, diese Version kommt aber in der Praxis sehr selten vor. ja Frage 1 Aktion 1 ja Aktion 2 nein Frage 2 ja Aktion 3 © Andreas Rau, 19.11.10 nein Frage 3 nein if (<Frage1>) { <Aktion1>; } else if (<Frage2>) { <Aktion2>; } else if (<Frage3>) { <Aktion3>; } else { <Aktion4>; } Aktion 4 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #110 Kontrollstrukturen(8) Eigenschaften von switch-case ● Ein switch-case-Konstrukt besteht aus einem Ausdruck, der einen elementaren Datentyp(!) ergibt, und mehreren Fällen, die durch konstante Werte(!) dieses Datentyps charakterisiert sind. Es wird der Fall ausgeführt, dessen Wert mit dem Wert des Ausdrucks übereinstimmt. Daneben gibt es noch einen (optionalen) default-Fall der dann ausgeführt wird, wenn kein anderer passt. Mit switch-case lassen sich also Fallunterscheidungen darstellen. Der Ausdruck ist dabei oftmals der Name einer Variablen, die Werte Literale. Das muss aber nicht so sein. ● ● Nach der Abarbeitung eines Falles wird die Ausführung mit dem nächsten Fall fortgesetzt (sog. "fall through") – es sei denn, der erste Fall endet mit einer break-Anweisung. Praktisch braucht das nahezu niemand und man sollte sich angewöhnen, die break-Anweisung immer hinzuschreiben. Braucht man sie ausnahmsweise mal nicht, ist das mit Sicherheit einen Kommentar wert... Es ist theoretisch möglich, switch-case-Konstrukte zu verschachteln. Allerdings macht das praktisch kein Mensch, weil da niemand mehr durchblickt. Fallunterscheidungen sollten eindimensional sein! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #111 Kontrollstrukturen(9) switch (i-j) { case 0: System.out.println( "Volltreffer"); break; case -1: // fall-through case -2: // fall-through case -3: System.out.println( "Knapp drunter"); break; case +1: // fall-through case +2: // fall-through case +3: System.out.println( "Knapp drüber"); break; default: System.out.println( "Voll daneben"); } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #112 Kontrollstrukturen(10) Vergleich von if-else und switch-case Das switch-case-Konstrukt ist ein Sonderfall einer if-else-Kette. Mit seiner Hilfe lassen sich bestimmte Fallunterscheidungen, nämlich solche die durch exakte Werte eines elementaren Datentyps charakterisiert sind, einfach und übersichtlich darstellen. Im Gegensatz dazu kann man mit Hilfe einer if-else-Kette beliebige Bedingungen mit beliebigen Datentypen und damit beliebige Fälle darstellen, z.B. Bereiche von Zahlen oder Vergleiche von Zeichenketten. Das ifelse-Konstrukt ist also mächtiger als switch-case. Zusätzlich kann man mit einer if-else-Kette durch die Reihenfolge der Bedingungen eine Priorisierung der Fälle ausdrücken. Der häufigste Fall kommt zuerst und wird dann auch am schnellsten abgearbeitet. Zwar kann man auch bei switch-case die Reihenfolge bewußt wählen, einen direkten Einfluß auf die Abarbeitungsreihenfolge hat man aber damit nicht. In beiden Konstrukten kann der else-Zweig bzw. default-Fall verwendet werden, um vergessene Fälle zu entdecken. Auch wenn man glaubt, dass ein solcher Fall nie eintritt, lohnt es sich manchmal, seinen Glauben durch einen else-Zweig mit einem entsprechenden Kommentar der Welt mitzuteilen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #113 Kontrollstrukturen(11) Fallunterscheidung mit einzelnen Werten Fallunterscheidung mit Wertebereichen if (i==0) { System.out.println("Null"); } else if (i==1) { System.out.println("Eins"); } else { System.out.println("???"); } if (0<=i && i<10) { // klein } else if (10<=i && i<100) { // mittel } else if (100<=i && i<1000) { // gross } else { // wow! } switch (i) { case 0: System.out.println("Null"); break; case 1: System.out.println("Eins"); break; default: System.out.println("???"); } © Andreas Rau, 19.11.10 // mit switch nicht möglich D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #114 Kontrollstrukturen(12) Schleifen / while, for, do- while Eine Schleife weist den Rechner an, Dinge mehrmals zu tun und erspart es dem Programmierer, diese Dinge mehrfach hinschreiben zu müssen. Erkauft wird diese Bequemlichkeit durch den Aufwand für die Schleifenverwaltung. In der Echtzeitprogrammierung werden Schleifen deshalb manchmal wieder "ausgerollt" (loop unrolling), d. h. man schreibt doch wieder alles mehrmals hin. Man unterscheidet zwischen fußgesteuerten Schleifen (erst machen, dann prüfen) und kopfgesteuerten Schleifen (erst prüfen, dann machen). Fußgesteuerte Schleifen werden stets mindestens einmal durchlaufen. Bei kopfgesteuerten Schleifen kann es auch vorkommen, dass Sie nie durchlaufen werden. Deswegen werden sie auch abweisende Schleifen genannt. Das abweisende Verhalten von kopfgesteuerten Schleifen ist z. B. dann wichtig, wenn in der Schleife Referenzen benutzt werden, die auch null sein können. Die Bedingung einer abweisenden Schleife dient also oftmals auch zum Schutz vor Fehlern, die im Schleifenrumpf passieren könnten (sog. guard condition). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #115 Kontrollstrukturen(13) Gemeinsame Eigenschaften von Schleifen Eine Schleife besteht im aus drei Bestandteilen: Die Initialisierung stellt den Anfangszustand für die (Variablen in der) Abbruchbedingung her. Dies sollte kurz vor der Schleife passieren, andernfalls ist das Verständnis der Schleife erschwert. Der Schleifenrumpf arbeitet auf das Ziel hin. Er muss die Abbruchbedingung beeinflussen, sonst terminiert die Schleife nie (Endlosschleife). Eine notwendige Bedingung dafür ist, dass der Schleifenrumpf mindestens eine der Variablen verändert, die in der Abbruchbedingung vorkommen. Eine Schleife verfolgt i. d. R. ein bestimmtes Ziel und wird solange ausgeführt, bis dieses Ziel erreicht ist. Dieses Ziel wird durch den Zustand von bestimmten Variablen ausgedrückt und in der Abbruchbedingung überprüft. Bei einer kopfgesteuerten Schleife erfolgt diese Prüfung vor dem Schleifenrumpf, bei einer fußgesteuerten dannach. In Java wird das negierte Ziel angegeben ("weiter solange Ziel nicht erreicht"). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #116 Kontrollstrukturen(14) Eigenschaften von while Die while- Schleife ist eine kopfgesteuerte Schleife. Beispiel // Suche einen Wert in einem Array, // gebe den index aus wenn gefunden, -1 sonst int[] values = { 1,6,3,8,9,4,2}; int value = 2; int result; int index = 0; while (index < values.length && values[ index]!= value) { ++ index; } result = (index < values.length ? index : -1); System.out.println( result); In diesem Beispiel stellt der erste Teil der Abbruchbedingung sicher, dass die Suche bei einem leeren Array gar nicht erst begonnen wird. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #117 Kontrollstrukturen(16) Eigenschaften von do- while Die do- while- Schleife ist eine fußgesteuerte Schleife. Sie wird also mindestens einmal durchlaufen. Beispiel // Ersten Wert suchen wie auf voriger Folie if ( index<values.length) { // Wenn ein Wert gefunden wurde // Suche den nächsten Wert in einem Array ab position idx // liefere den index zurück wenn gefunden, -1 sonst do { ++ index; } while (index < values.length && values[ index]!= value); result = (index < values.length ? Index : -1); } Unter der Annahme, dass idx am Anfang den Index des zuletzt gefunden Werts enthält, stellt diese Schleife sicher, dass mindestens eine Position weitergegangen wird um nicht an diesem Wert "kleben zu bleiben". © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #118 Kontrollstrukturen(15) Eigenschaften von for Die for- Schleife ist ein Sonderfall einer while- Schleife, also kopfgesteuert, und wird hauptsächlich für Zählschleifen eingesetzt: // zähle von start bis end- 1 public void counter( int start, int end) { for ( int i= start; i< end; i++) { System. out. println( i); } } Die äquivalende while- Schleife verdeutlicht die Funktion der for- Schleife: { } int i= start; while ( i< end) { System. out. println( i); i++; } Hinweis: Mit anderen Worten, jede for-Schleife läßt sich als while-Schleife schreiben (aber nicht umgekehrt). Ein kleiner Unterschied besteht bzgl der Sichtbarkeit von i. In der for-Schleife ist das i nur innerhalb des Schleifenrumpfs definiert, für die while-Schleife muss i außerhalb des Schleifenrumpfs definiert werden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #119 Kontrollstrukturen(17) Wie alle Kontrollstrukturen können auch Schleifen geschachtelt werden. Dabei erhöht sich die Anzahl der Durchläufe und damit die Rechenzeit geometrisch: Wird die äußere Schleife n- mal durchlaufen und die innere Schleife m- mal, finden insgesamt n*m Durchläufe statt. Daher ist es ratsam, mit der Verschachtelung von Schleifen vorsichtig umzugehen. for (int i=0; i<100; i++) for (int j=0; j<100; j++) { System.out.println( (i*100+j+1) + "ter Durchlauf"); } } Durch die Verschachtelung der beiden Schleifen mit je 100 Durchläufen werden insgesamt 10.000 Ausgaben erzeugt! Man spricht in diesem Zusammenhang auch von quadratischer Laufzeit. Das ist bei Algorithmen immer schlecht... Bei Zählschleifen werden die Zählvariablen aus alter Fortran Tradition meist mit i, j, k benannt (mehr als 3 Schleifen sollte man aus Gründen der Verständlichkeit und der Rechenzeit nicht schachteln – siehe oben). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #120 Kontrollstrukturen(18) Stellt man in einer Schleife fest, dass das Ziel (vorzeitig) erreicht wurde, kann man die (innerste) Schleife mit Hilfe der break- Anweisung verlassen (wie bei switch- case) oder mit Hilfe der continue- Anweisung den Rest des Schleifenrumpfs zu überspringen und fortzufahren (sofern die Abbruchbedingung noch nicht erfüllt ist). Die break-Anweisung wird manchmal auch in speziellen „Endlosschleifen“ (Event Loop) zur Ereignisverarbeitung eingesetzt (Hinweis: Schleifenabbruch geht nicht mit dem break in switch!): while (true) { if (<Ereignis1> { <Aktion1> } else if (<Ereignis2>) { <Aktion2> } else if (<Ereignis3>) { <Aktion3> } else { break; // Abbruch bei ungültigem Ereignis } } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #121 Kontrollstrukturen(19) Kontrollstrukturen und Datenstrukturen Kontrollstrukturen sollen Daten verarbeiten. Dazu muessen auch die Datenstrukturen geeignet gewählt werden. Dies gilt besonders für Schleifen. Neben irgendwelchen Such- oder Rechenfunktionen, die bestimmte Variablen auf ein Ziel hin optimieren, geht es bei Schleifen oft darum, eine Menge von Daten auf eine bestimmte Art und Weise zu verarbeiten. Aber wie? Die Lösung ist im Grunde naheliegend: Zählschleifen verwenden eine numerische Zählvariable, Arrays können über einen numerischen Index adressiert werden. Damit sind Arrays und Zählschleifen wie füreinander geschaffen. Beispiel // alle Zahlen in einem Array verdoppeln for (int i=0; i<numbers.length; ++i) { numbers[i] = numbers[i]*2; } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #122 Kontrollstrukturen(10) Der enge Zusammenhang von for-Schleifen und Arrays wird in Java 5.0 durch die Einführung einer speziellen Form der for-Schleife unterstrichen mit deren Hilfe sich die Verarbeitung aller Elemente eines Arrays besonders einfach formulieren läßt. Beispiel // alle Zahlen in einem Array ausgeben for (int n : numbers) { // "für jedes n im numbers Array" System.out.println( n); } In anderen Programmiersprachen wird diese Form der Schleife oft als for-each Schleife bezeichnet. Dies hätte jedoch ein neues Schlüsselwort erfordert. Anmerkung: Diese neuartige for-Schleife funktioniert auch mit (den später eingeführten) Collections. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #123 Wie man eine Schleife entwickelt Schleifen sind eigentlich nicht schwer, ehrlich! Aber wie entwickelt man sie? Am einfachsten geht das am Anfang, indem man sie ausschreibt, also z.B. Ziel: 1 2 3 ... n ausgeben Schleifendurchläufe ausschreiben System.out.print( 1 + " "); System.out.print( 2 + " "); System.out.print( 3 + " "); ... System.out.print( n + " "); Wiederkehrende Teile eindampfen und verallgemeinern System.out.print( i + " "); Schleife außenrum ( Initialisierung? Abbruchbedingung? Fortschritt?) for ( int i=1; i<=n; ++i) { System.out.print( i + " "); } Fertig! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #124 Anwendungsbeispiele Natürlich kommen in realen Programmen die Kontrollstrukturen immer in Kombination vor, so z.B. beim Suchen nach einem Wert in einem Array for ( int i : numbers) { if ( i == searchValue) { // Wert verarbeiten break; // wenn keine weiteren Exemplare gesucht } } Eine kleine Abwandlung davon ist die Prüfung, ob ein Wert vorhanden ist boolean found = false; for ( int i : numbers) { if ( i == searchValue) { found = true; break; } } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #125 Struktogramme Mit Hilfe von Struktogrammen (auch: Nassi-Shneidermann Diagramme) kann der gesamte Kontrollfluss eines (Teil)Programms übersichtlich dargestellt werden. Dies kann hilfreich sein, um Teilalgorithmen zu skizzieren oder auch um wichtige Programmteile nachträglich zu dokumentieren. Dabei fällt auf, dass sich die Kontrollstrukturen nur hintereinander setzen oder sauber ineinander verschachteln lassen. Es gibt also keine Sprünge über Kreuz. Insbesondere Schleifen sind stets ineinander verschachtelt und niemals verschränkt. (Bilder siehe Buch) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #126 Anwendung: Einordnung der Kontrollstrukturen Jetzt kennen Sie die Kontrollstrukturen. Schön, und was machen wir nun damit? Die Kontrollstrukturen sind LEGO-Bausteine zum Bau einer Folge von Schritten zur Lösung eines Problems. Jeder Baustein hat einen bestimmten Zweck: Sequenz – Mehrere Schritte nacheinander ausführen ● if-else – Eine ja/nein Entscheidung treffen bzw. Fallunterscheidung vornehmen ● switch-case – Eine Auswahl aus vielen Alternativen bzw. Fällen treffen ● for-Schleife – Zählschleife zur Erzeugung/Bearbeitung von Werten, z.B. Arrays ● for-each-Schleife – Bearbeitungsschleife für Arrays oder Collections ● while-Schleife – Durchführung von Befehlen nach Prüfung einer Bedingung ● do-while-Schleife – Durchführung von Befehlen vor Prüfung einer Bedingung ● Bevor man die Bausteine jedoch zusammensetzt, muss man (a) das Problem verstanden und sich (b) die Lösung ausgedacht haben. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #127 Anwendung: Wie man sich ein Programm ausdenkt Jetzt kennen Sie bereits die wesentlichen Bestandteile eines Programms (alles weitere dient nur der Gliederung) und können womöglich verschiedene Beispielprogramme lesen und verstehen. Aber: Wären Sie auch selber draufgekommen? Wie schreibt man überhaupt ein Programm? Ein Programm ist die Lösung zu einem Problem. Und wie jede Lösung setzt auch ein Programm eine Lösungsidee voraus. Diese Idee muss nicht in der Programmiersprache formuliert sein. Im Gegenteil: Leichter fällt es meistens, die Lösungsidee zunächst in anschaulichen Begriffen oder sog. Pseudocode zu formulieren und erst dannach (Schritt für Schritt) in die Zielsprache zu übersetzen (erinnern Sie sich an das Bild von der Kamera und den Bits?). Hilfreiche Bilder dazu könnten sein (jeder nimmt das, was ihm hilft) ● Daten/Variablen = Schubladen ● If/Else = Schalter, Fallunterscheidung ● Schleifenindex = Zeigefinger ● ... Die Anwendung dieser Bilder kann man auch anhand einiger Beispiele zeigen... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #128 Anwendung: Rezeptvorschläge für if-else Die Kontrollstruktur if-else kann z.B. eingesetzt werden Um Sonderfälle abzuprüfen (z.B. VIP Kunde) ● Um Fehlerfälle abzuprüfen (z.B. Nenner auf 0 prüfen vor Division) ● Um Sachverhalte schrittweise einzugrenzen (Verschachtelung if-else-if) ● Um Fallunterscheidungen vorzunehmen (z.B. Note <= 4,0 sonst durchgefallen) ● Die Bedingung kann dabei einfach oder komplex sein Beispiele für einfache Bedingungen (relationale Operatoren) a==0, b>0, c%2==0 Beispiele für komplexe Bedingungen (logische Operatoren) ('a'<=c && c<='z'), ('a'<=c && c<='z') || ('A'<=c && c<='Z'), a<0 || Math.sqrt(a)>10 © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #129 Anwendung: Rezeptvorschläge für switch-case Aufgrund der Datentyp-Einschränkung dient switch-case i.d.R. Auswahl aus einer Anzahl von Fällen, z.B. Menüpunkte ● Auswahl aus einer Anzahl von Zuständen (über eine Zustandsvariable) ● Damit hat switch-case im Unterschied zu if-else weniger eine Prüffunktion und viel deutlicher eine Auswahlfunktion. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #130 Anwendung: Rezeptvorschläge für for-Schleifen Die for-Schleife kann z.B. eingesetzt werden Eine Anweisungsfolge n-Mal zu wiederholen for ( int i=0; i<n; ++i) { ... } ● Eine Zahlenfolge mit bestimmter Schrittweite zu generieren* for ( int i=1; i<n; i+=3) { ... } ● Elemente in einem Array zu erzeugen, zu bearbeiten oder auszulesen for ( int i=0; i<a.length; ++i) { ... a[i] ... } ● Dabei entspricht eine for-Schleife der Bearbeitung einer Dimension. Durch Verschachtelung von Schleifen können mehrere Dimensionen bearbeitet werden, z.B. mehrdimensionale Arrays. Dabei ist es wichtig, verschiedene Indexvariablen für jede Schleife zu verwenden und diese nicht zu verwechseln. Es wird empfohlen, Indexvariablen grundsätzlich nur einmal zu verwenden um auf das aktuelle Element zuzugreifen und dieses in einer Variablen zu speichern. *man kann auch andere Daten aus Zahlen generieren, z.B. Zeichen via Ascii © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #131 Anwendung: Rezeptvorschläge für for-each-Schleifen Die for-each-Schleife kann eigentlich nur für einen Zweck eingesetzt werden ● Elemente in einem Array oder einer Collection auslesen for ( String name : namen) { ... name ...} Die for-each-Schleife wurde speziel für diesen Zweck eingeführt, um den Schreibaufwand für eine normale for-Schleife einzusparen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #132 Anwendung: Rezeptvorschläge für while-Schleifen Die while-Schleife kann eigentlich alles, wird aber am Besten eingesetzt für Unbestimmte Anzahl von Wiederholungen bis zur Ereichung eines Resultats while ( !fertig) { ... } ● Wiederholung von Schritten wie oben aber mit Vorbehalt while ( kindersicher && !fertig) { ... } ● Bei der while-Schleife kann es also vorkommen, dass sie komplett übersprungen wird weil es nichts zu tun gibt oder nichts getan werden kann, z.B. weil in einem leeren Array nichts gefunden werden kann oder ein nicht vorhandenes Array (null) nicht durchsucht werden kann. Die while-Schleife ist die universellste aller Schleifen (die anderen dienen quasi nur der Bequemlichkeit). Daher gilt: bei Unsicherheit einfach while verwenden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #133 Anwendung: Rezeptvorschläge für do-while-Schleifen Die do-while-Schleife ist die "unsichere Schwester" der while-Schleife für ● mindestens einmalige Wiederholungen bis zur Ereichung eines Resultats do { ... } while ( !fertig) Der Inhalt der do-while Schleife wird also mindestens einmal durchlaufen. Ist dann (noch) nicht das gewünschte Ergebnis erreicht ggf. nochmal. Beispiel: Ziehung einer Lottozahl und Wiederholung falls bereits vorhanden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #134 Unterprogramme (Funktionen) © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #135 Unterprogramme und Schnittstellen(1) Als Unterprogramm bezeichnet man einen Programmteil mit klarem abgegrenztem Zweck, der an mehreren Stellen im Programm aufgerufen und somit wiederverwendet werden kann. 1. Aufruf 2. Aufruf println( ... ) println( ... ) Der Begriff Unterprogramm stammt aus einer Zeit, als Unterprogramme oft nicht viel mehr waren als ein paar Zeilen Code, die man mit Anweisungen wie gosub und return anspringen und wieder verlassen konnte. Der Datenaustausch mit diesen Programmschnipseln war über globale Variablen organisiert. Dies war zwar besser als die Verwendung von goto (ein Sprung ohne Wiederkehr), aber die Verwendung von globalen Variablen verschleierte die Schnittstelle des Unterprogramms und führte aufgrund der gemeinsamen Verwendung dieser Variablen an vielen Stellen im Programm zu Problemen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #136 Unterprogramme und Schnittstellen(2) Zweck von Unterprogrammen Unterprogramme sind aus vielen Gründen sehr nützlich. Die wichtigsten sind: ● ● Erhöhte Lesbarkeit eines Programms. Die Aufteilung eines Programms in Module und Unterprogramme gleicht der Einteilung eines Buchs in Kapitel. Der Name eines Unterprogramms entspricht der Kapitelüberschrift und verdeutlicht die Bedeutung des darin enthaltenen Codes. Wiederverwendung - sowohl innerhalb eines Programms als auch durch Bildung von Bibliotheken. Wiederverwendung dient nicht nur der Einsparung von Tipparbeit sondern hilft auch, Fehler zu vermeiden (Verwendung von erprobtem Code) und die Wartbarkeit zu verbessern (Änderungen sind nur an einer Stelle nötig). Um diese Ziele zu erreichen ist Sorgfalt beim Programmentwurf notwendig. Ein Unterprogramm sollte immer nur einen Zweck verfolgen und entsprechend benannt sein (keine eierlegende Wollmilchsau). Solche Unterprogramme sind kurz und passen idealerweise auf eine Bildschirmseite (Verständlichkeit). © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #137 Unterprogramme und Schnittstellen(3) Die moderne Bezeichnung für ein Unterprogramm lautet Prozedur oder Funktion. In Java spricht man von Methoden, aber die Begriffe Prozedur und Funktion sind dennoch nützlich zur Charakterisierung dieser Methoden. Eine Prozedur verarbeitet ihre Parameter in einer Aktion ohne direkten Rückgabewert. Sie „macht etwas“ und hat i.d.R. Seiteneffekte, z.B. eine Ausgabe am Bildschirm oder die Veränderung globaler Variablen. In Java verändern solche Methoden typischerweise den inneren Zustand des aufgerufenen Objekts. Dies trifft speziell auf set-Methoden zu, die den Wert einer Zustandsvariablen auf einen bestimmten Wert setzen. Eine Funktion verarbeitet ihre Parameter in einer Berechnung zu einem Ergebnis. Sie „berechnet etwas“ und kann zusätzlich Seiteneffekte haben. Der trivialste Vertreter dieser Art von Methoden in Java sind get-Methoden, die den Wert einer Zustandsvariablen zurückliefern. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #138 Unterprogramme und Schnittstellen(4) Methoden werden charakterisiert durch ihren Namen und ihre Schnittstelle. Diese bezeichnet man zusammen als Signatur (=Fingerabdruck) der Methode. Der Name der Methode soll möglichst treffend beschreiben, was die Methode tut. Dazu werden oft Wortkombinationen verwendet, die mit einem Verb beginnen, z.B. getName() oder readLine(). Die Schnittstelle beschreibt den Typ des Rückgabewerts sowie die Anzahl, Positionen und Typen der formalen Parameter. Ihre Namen dienen als Platzhalter zur Formulierung des Methodenrumpfs. Ihre Werte werden beim Aufruf der Funktion durch die Werte der aktuellen Parameter ersetzt. In vielen Sprachen ist eine Funktion bereits durch ihren Namen eindeutig charakterisiert. In Java kann es dagegen mehrere gleichnamige Methoden mit unterschiedlicher Signatur innerhalb eines Gültigkeitsbereichs geben (Beispiel: die Methode println()). Dies nennt man überladen (overloading). Es ermöglicht es, Methoden die im Prinzip das gleiche tun (hier: Ausgabe von Werten) – nur eben mit verschiedenen Daten – auch gleich zu benennen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #139 Kopieren von Werten bei Funktionsaufrufen(1) Ablauf eines Methodenaufrufs public class Doppler { Typ des Rückgabewerts Name + Formale Parameter = Signatur public static int verdoppeln( int wert) { int dasDoppelte = wert * 2; return dasDoppelte; } } public static void main(String[] args) { int x, y; x = 1; 1. wert = x (aktueller Parameter) y = verdoppeln( x); 2. Funktion ausführen 3. y = dasDoppelte (Rückgabewert) System.out.println( y); } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #140 Kopieren von Werten bei Funktionsaufrufen(2) Für das Kopieren der Parameter und des Rückgabewerts beim Methodenaufruf gelten dieselben Regeln wie für das Kopieren von Variablen: Elementare Werte werden als echte Kopie übergeben. Änderungen des Wertes innerhalb der Funktion haben außerhalb der Funktion keine Auswirkungen. Dies wird Call-by-Value genannt. Objekte werden dagegen als Referenz übergeben. Änderungen des Objekts innerhalb der Funktion verändern das Originalobjekt, d.h. sie wirken sich auch außerhalb der Funktion aus. Dies wird Call-by-Reference* genannt. Zum Schutz vor unabsichtlichen Fehlern ist es wichtig, diesen Unterschied zu kennen. Beide Aufrufvarianten haben ihren Sinn! Für primitive Datentypen ist Call-by-Value schnell und billig. Bei der Übergabe von Objekten liegt der Vorteil des Call-by-Reference neben der Durchgriffsmöglichkeit auf den Originalwert auch in einer effizienteren Übergabe, besonders bei großen Objekten. *Bei spitzfindiger Betrachtung stimmt diese Aussage für Java nicht ganz: beim "echten" Call-by-Reference, z.B. in C, kann die ursprüngliche Variable (d.h. der Schubladeninhalt) verändert werden. In Java wäre dies der Verweis auf das Objekt. Tatsächlich kann jedoch nur das Objekt selbst, nicht aber der Verweis geändert werden. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #141 Iteration und Rekursion(1) Es gibt zwei Wege um Dinge mehrfach zu tun: Iteration und Rekursion. Bei der Iteration werden Schleifen verwendet. Diese Variante haben wir bereits kennengelernt. Sie entspricht anschaulich der Wiederholung von Arbeitschritten. Bei der Rekursion ruft sich eine Funktion wieder selbst auf. Diese Variante ist neu. Sie kommt aus der Mathematik und erscheint auf den ersten Blick unverständlich, erlaubt es allerdings die Lösung vieler Probleme elegant darzustellen. Dazu sind wie bei der vollständigen Induktion zwei Schritte nötig: (1) man führt das Problem auf ein kleineres Teilproblem zurück und (2) man löst das kleinste Teilproblem um die Kette der Aufrufe abzubrechen. Die Mächtigkeit von Iteration und Rekursion ist gleich, d.h. jeder iterative Algorithmus kann theoretisch in einen entsprechenden rekursiven Algorithmus umgewandelt werden und umgekehrt (die Lesbarkeit ist dabei ein anderes Thema). Da jedoch die Rekursion mit erheblichem Zusatzaufwand für die vielen Aufrufe verbunden sein kann, hängt die Wahl zwischen Iteration und Rekursion in der Praxis oft von einer Abwägung zwischen Lesbarkeit und Effizienz ab. Dabei gibt es leider keine feste (Daumen)regel... © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #142 Iteration und Rekursion(2) Beispiel: Berechnung der Fakultät (DER Klassiker ;-) ) public class FakExample { // iterative Lösung public static int iterFak( int value) { int result = 1; while (value > 1) { result *= value--; // Achtung: Seiteneffekt! } return result; } } // rekursive Lösung public static int rekFak( int value) { if (value > 1) { return value * rekFak( value-1); // Teilproblem } else { return 1; // Anfangsproblem } } © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #143 Iteration und Rekursion(3) Aufruf: iterFak( 5); value 5 4 3 2 1 result 1 5 20 60 120 Aktion *5 *4 *3 *2 *1 Aufruf: rekFak( 5); value 5 4 3 2 1 © Andreas Rau, 19.11.10 Aktion 5 * rekFak(4) 4 * rekFak(3) 3 * rekFak(2) 2 * rekFak(1) 1 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #144 Iteration und Rekursion(4) Die rekursive Lösung entspricht der math. Definition der Fakultätsfunktion n*(n-1)! falls n > 1 n! = 1 sonst Anderes Beispiel für solche mehrteiligen Definitionen x falls x >= 0 |x| = -x sonst Neben math. rekursiven Funktionen gibt es auch verschiedene rekursive Datenstrukturen bei denen Teilstrukturen der Gesamtstruktur ähneln, z.B. Bäume. Diese lassen sich sehr elegant mit rekursiven Algorithmen bearbeiten. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #145 Zusammenfassung: Ausdrücke und Funktionen Es besteht eine gewisse Ähnlichkeit zwischen Ausdrücken und Anweisungen einerseits und Funktionen und Prozeduren andererseits: Ausdrücke und Funktionen berechnen etwas, d.h. liefern einen Wert zurück. ● Anweisungen und Prozeduren machen etwas, d.h. liefern einen Effekt. ● Demnach kann man Funktionen als eine Abstraktion für (Teil-)ausdrücke und Prozeduren als eine Abstraktion für eine Folge von Anweisungen betrachten. Trotzdem darf natürlich eine Funktion Anweisungen enthalten, z.B. für abschnittsweise definierte Funktionen, und eine Prozedur kann Ausdrücke verwenden. Entscheidend ist, „was hinten rauskommt“ (oder auch nicht). Um einen (Teil)ausdruck als Methode zu implementieren, muss (1) ihr Rückgabewert den Typ des Ergebnisses haben und (2) die Schnittstelle soviele Parameter haben, wie Variablen in dem Teilausdruck vorkommen. Eine Anweisungsfolge als Methode zu implementieren ist viel einfacher – man packt sie einfach rein und übergibt die benötigten Daten als Parameter. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #146 Fazit Die in diesem Teil behandelten Konzepte sind allgemeingültig und lassen sich – mit leichten syntaktischen Abwandlungen – auf fast alle gängigen Programmiersprachen übertragen. Sie stellen gewissermaße die Materialkunde (Datentypen) und handwerkliche Grundausbildung (Ausdrücke und Kontrollstrukturen) zum Bau von Häusern (Programmen) dar. Die unterschiedlichen Arten von Programmiersprachen stellen in diesem Sinne verschiedene Stilrichtungen der Architektur dar. Die Objektorientierung, auf der Java basiert, ist ein solcher Baustil und wird im nächsten Teil behandelt. Schlussbemerkung: Mit den Inhalten aus diesem Abschnitt der Vorlesung sind alle Grundwerkzeuge bekannt. Für Ihre Beherrschung – insbesondere im Zusammenspiel – ist jedoch noch viel Übung notwendig. Wer Pinsel, Farben und Staffelei kennt, ist noch lange kein Picasso – aber auch der hat schließlich als Kind mal mit Strichzeichnungen angefangen. © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #147 Blick über den Tellerrand Das folgende Programm realisiert eine Schleife in JavaScript. Da sowohl Java als auch JavaScript die Kontrollstrukturen von C "geerbt" haben dürfte dieses Programm leicht verständlich sein. <html> <body> <script type="text/javascript"> i = "Das ist ein Test"; document.writeln( i); for (i=0; i<10; i++) { document.writeln( i); } </script> </body> </html> Auffällig ist jedoch, dass die Variable i offenbar keinen (eindeutigen) Datentyp hat. Dies ist für Skriptsprachen typisch aber auch tückisch! © Andreas Rau, 19.11.10 D:\home\ar\fhte\vorlesungen\informatik1\folien\informatik1-theorie-programmelemente.odp #148