Informatik B Objekt-orientierte Programmierung in Java Sommersemester 2003 Fachbereich Mathematik/Informatik, Universität Osnabrück Ute Schmid (Vorlesung) Elmar Ludwig (Übung) und Tutoren Voraussetzungen: (Vorlesung Informatik A “Algorithmen”) Grundkenntnisse in der (prozeduralen) Programmierung mit Java Grundkenntnisse in Algorithmenentwicklung http://www.vorlesungen.uos.de/informatik/b03/ Ute Schmid Fachgebiet Informatik Fachbereich 6, Mathematik/Informatik Universität Osnabrück Albrechtstrasse 28, D-49076 Osnabrück Raum 31/318 Tel.: 0541/969-2558 [email protected] http://www.inf.uos.de/schmid/ Dies ist eine überarbeitete Version der Skripts vom SS 2001 und SS 2002. Vielen Dank an Elmar Ludwig für kritische Durchsicht, Korrekturvorschl äge, Anregungen und Diskussionen. Literatur Objekt-orientierte Programmierung/Spezielle Aspekte Zum Thema ‘Objekt-Orientierte Programmierung’: Timothy Budd, An Introduction to Object-Oriented Programming, 2nd Edition, 1997, Addison-Wesley. Zum Thema ‘Design Patterns’ (und etwas UML): James W. Cooper, Java Design Patterns. Tutorial, 1994, Addison-Wesley. Matthias Felleisen, Daniel P. Friedman, A Little Java, A Few Patterns, 1998, MIT Press. Zum Thema ‘Abstrakte Datentypen’: Timothy Budd, Classic Data Structures in Java, 2001, Addison-Wesley. Zum Thema ‘Nebenläufigkeit’: Doug Lea, Concurrent Programming in Java. Design Principles and Patterns, 2nd Edition, 1999, Sun Books. Lehrbuch, das den Stoff von ‘Algorithmen’ abdeckt mit Programmen in Pseudo-Java (eher prozeduraler Code): Sara Baase, Allen van Gelder, Computer Algorithms – Introduction to Design and Analysis, 3rd Edition, 2000, Addison-Wesley. Überblick über verschiedene Programmierkonzepte: Ravi Sethi, Programming Languages: Concepts & Constructs, 2nd Edition, 1997, Addison-Wesley. Java D. Flanagan, Java in a Nutshell, 3rd Edition, 1999, O’Reilly. D. Flanagan, Java Examples in a Nutshell, 2nd Edition, 2000, O’Reilly. Bruce Eckel, Thinking in Java, 2nd Edition, 1998, Prentice-Hall. Mary Campione, Kathy Walrath, The Java Tutorial, Object-Oriented Programming for the Internet, 2nd Edition, 1997, Addison-Wesley. (auch in deutscher Übersetzung erhältlich) Ken Arnold, James Gosling, The Java Programming Language, 3rd Edition, Addison-Wesley. (auch in deutscher Übersetzung erhältlich) Das Skript bezieht sich auf folgende weitere Quellen: J. R. Anderson, Kognitive Psychologie, 3. Auflage, 2001, Spektrum. (Hierarchisches semantisches Gedächtnis) R. G. Herrtwich und G. Hommel, Nebenl äufige Programme, 2. Auflage, Springer, 1994. Atsushi Igarashi, Benjamin Pierce and Philip Wadler, Featherweight Java: A Minimal Core Calculus for Java and GJ. In Proc. of ACM Conference on Object-Oriented Programing, Systems, Languages, and Applications (OOPSLA), ACM SIGPLAN Notices, 34(10), pp./ 132-146, 1999. Atsushi Igarashi and Benjamin C. Pierce. On Inner Classes. Accepted for publication in Information and Computation as of November 24, 2000. P. Pepper, Grundlagen der Informatik, 2. Auflage, Oldenbourg, 1995. A. T. Schreiner, Vorlesungsskript Programming with Java 2, Universit ät Osnabrück und Rochester Institute of Technology. http://www.cs.rit.edu/˜ats/java-2000-1/ Sperschneider und Hammer, 1996, Theoretische Informatik. Eine problemorientierte Einführung. Springer. Oliver Vornberger und Olaf Müller, Skript Informatik A. Inhaltsverzeichnis 1 Einführung: Programmier-Paradigmen und -Sprachen 1.1 Entwicklung von Paradigmen und Sprachen . . . . . 1.1.1 Überblick . . . . . . . . . . . . . . . . . . . . 1.1.2 Entwicklung von Hochsprachen . . . . . . . . 1.1.3 Motivation: Kenntnis mehrere Paradigmen . . 1.2 Prozeduren und Parameterübergabe-Mechanismen 1.2.1 Programmstruktur bei imperativen Sprachen 1.2.2 Prozeduren . . . . . . . . . . . . . . . . . . . 1.2.3 Parameterübergabe-Methoden . . . . . . . . 1.2.4 Call-by-Value versus Call-by-Reference . . . 1.2.5 Call-by-Value versus Call-by-Name . . . . . . 1.2.6 Call-by-value in Java . . . . . . . . . . . . . . 1.3 Sprach-Implementierung . . . . . . . . . . . . . . . . 1.4 Syntaktische Struktur . . . . . . . . . . . . . . . . . 1.4.1 Formale Sprachen . . . . . . . . . . . . . . . 1.4.2 BNF und EBNF . . . . . . . . . . . . . . . . . 1.5 Semantik und Pragmatik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 1 1 3 4 4 4 6 7 7 8 10 12 12 13 15 2 Java und Objekt-Orientierung 2.1 Grundkonzepte der Objekt-Orientierung . . . . . . . . . . . 2.1.1 Eigenschaften Objekt-Orientierter Sprachen . . . . . 2.1.2 Prinzipien der Objekt-Orientierten Programmierung 2.2 Die Sprache Java . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Entstehungsgeschichte . . . . . . . . . . . . . . . . 2.2.2 Java Buzzwords . . . . . . . . . . . . . . . . . . . . 2.2.3 Sprache, Virtuelle Maschine, Plattform . . . . . . . . 2.3 Das 8-Damen Problem Imperativ und Objekt-Orientiert . . . 2.3.1 Problemstellung . . . . . . . . . . . . . . . . . . . . 2.3.2 Identifikation von Schlagstellungen . . . . . . . . . . 2.3.3 Imperative Lösung . . . . . . . . . . . . . . . . . . . 2.3.4 Objekt-Orientierte Lösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 17 17 18 18 19 19 21 21 22 23 24 3 Klassen und ihre Komponenten 3.1 Klassen in Java . . . . . . . . . . . . . . . . . . . . . 3.2 Members: Felder, Methoden, Klassen . . . . . . . . 3.3 Beispielcode ‘Circle’ . . . . . . . . . . . . . . . . . . 3.4 Klassen- und Instanz-Komponenten . . . . . . . . . 3.4.1 Klassen-Felder . . . . . . . . . . . . . . . . . 3.4.2 Klassen-Methoden . . . . . . . . . . . . . . . 3.4.3 Instanz-Felder . . . . . . . . . . . . . . . . . 3.4.4 Erzeugung und Initialisierung von Objekten . 3.4.5 Instanz-Methoden . . . . . . . . . . . . . . . 3.4.6 Funktionsweise von Instanz-Methoden: ‘this’ 3.4.7 Instanz- oder Klassen-Methode? . . . . . . . 3.5 Referenztypen . . . . . . . . . . . . . . . . . . . . . 3.5.1 Kopieren von Objekten (und Arrays) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 28 28 29 29 29 30 30 31 31 32 32 33 34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.2 Gleichheit von Objekten (und Arrays) . 3.6 Wrapper-Klassen . . . . . . . . . . . . . . . . 3.7 Dokumentation mit ‘javadoc’ . . . . . . . . . . 3.8 Packages und Namespace . . . . . . . . . . 3.8.1 Java API . . . . . . . . . . . . . . . . 3.8.2 Packages und Namespaces in Java . 3.8.3 Namens-Kollisionen . . . . . . . . . . 3.8.4 Verhaltensänderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 37 37 40 40 41 42 43 4 Konstruktoren und Vererbung 4.1 Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Definition von Konstruktoren . . . . . . . . . . . . . . . . 4.1.2 Definition mehrerer Konstruktoren . . . . . . . . . . . . . 4.2 Defaults und Initialisierung für Felder . . . . . . . . . . . . . . . . 4.2.1 Defaults . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.2 Initialisierung von Instanz-Feldern: Konstruktoren . . . . . 4.2.3 Initialisierung von Klassen-Feldern: Initialisierungs-Bl öcke 4.3 Zerstören und Finalisieren von Objekten . . . . . . . . . . . . . . 4.3.1 Garbage Collection . . . . . . . . . . . . . . . . . . . . . . 4.3.2 Anmerkung: Finalization . . . . . . . . . . . . . . . . . . . 4.4 Unterklassen und Vererbung . . . . . . . . . . . . . . . . . . . . 4.4.1 Exkurs: Hierarchische Semantische Netze . . . . . . . . . 4.4.2 Erweiterung von ‘Circle’ . . . . . . . . . . . . . . . . . . . 4.4.3 Erweiterung einer Klasse . . . . . . . . . . . . . . . . . . 4.5 Kapslung und Zugriffskontrolle . . . . . . . . . . . . . . . . . . . 4.5.1 Zugriffs-Kontrolle . . . . . . . . . . . . . . . . . . . . . . . 4.5.2 Vier Ebenen von Zugriffsrechten . . . . . . . . . . . . . . 4.5.3 Zugriffs-Kontrolle und Vererbung . . . . . . . . . . . . . . 4.5.4 Daumenregeln für Sichtbarkeits-Modifikatoren . . . . . . 4.5.5 Daten-Zugriffs-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 45 46 46 46 47 48 49 49 49 50 50 52 52 53 54 54 55 56 56 5 Klassenabhängigkeiten 5.1 Klassenhierarchie . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Finale Klassen . . . . . . . . . . . . . . . . . . . . . . 5.1.2 Die Klasse ‘Object’ . . . . . . . . . . . . . . . . . . . . 5.1.3 Klasse ‘String’ . . . . . . . . . . . . . . . . . . . . . . 5.1.4 Hierarchische Klassenstruktur . . . . . . . . . . . . . 5.2 Ergänzung: Konstruktoren . . . . . . . . . . . . . . . . . . . . 5.2.1 Unterklassen-Konstruktoren . . . . . . . . . . . . . . . 5.2.2 Default-Konstruktoren . . . . . . . . . . . . . . . . . . 5.2.3 Konstruktor-Verkettung . . . . . . . . . . . . . . . . . 5.3 Vererbung: Shadowing und Overriding . . . . . . . . . . . . . 5.3.1 Verdecken von Feldern der Oberklasse . . . . . . . . 5.3.2 Shadowing von Klassen-Feldern . . . . . . . . . . . . 5.3.3 Überschreiben von Instanz-Methoden der Oberklasse 5.3.4 Überschreiben vs. Verdecken . . . . . . . . . . . . . . 5.3.5 Dynamisches ‘Method Lookup’ . . . . . . . . . . . . . 5.3.6 ‘Final’ Methoden und Statisches ‘Method Lookup’ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 58 58 58 58 59 61 61 61 62 62 62 63 64 64 65 65 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.7 Aufruf einer überschriebenen Methode . 5.4 Overloading und Polymorphismus . . . . . . . 5.4.1 Operator-Overloading . . . . . . . . . . 5.4.2 Operator-Overloading in Java . . . . . . 5.4.3 Method-Overloading in Java . . . . . . . 5.4.4 Polymorphismus . . . . . . . . . . . . . 5.4.5 Casting und Polymorphismus . . . . . . 5.4.6 Casting vs. Parameterisierte Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 66 66 67 68 69 69 70 6 Exceptions 6.1 Fehler und Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Vorteile von Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.1 Separierung von Code und Fehlerbehandlung . . . . . . . . 6.2.2 Propagierung von Exceptions . . . . . . . . . . . . . . . . . . 6.3 Exception Handling – ‘try’, ‘catch’, ‘finally’ . . . . . . . . . . . . . . . 6.4 Spezifikation von Exceptions – ‘throws’ . . . . . . . . . . . . . . . . . 6.5 Vererbung und ‘throws’ . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5.1 Gruppierung von Fehler-Typen . . . . . . . . . . . . . . . . . 6.6 Definition eigener Exception-Klassen und Ausl ösen von Exceptions 6.7 Exkurs: UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.7.1 Klassendiagramme in UML . . . . . . . . . . . . . . . . . . . 6.7.2 Klassen-/ Unterklassenbeziehungen in UML . . . . . . . . . 6.7.3 Assoziationen . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.7.4 Kommentare und Annotierung in UML . . . . . . . . . . . . . 6.7.5 UML-Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.8 Exkurs: Design Patterns – Factory Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 72 73 73 74 75 79 79 80 81 82 82 83 84 85 85 86 7 Input/Output 7.1 Ein-/Ausgabe-Ströme . . . . . . . . . . . . . 7.1.1 Klassenstruktur in ‘java.io’ . . . . . . . 7.1.2 Character und Byte Ströme . . . . . . 7.1.3 Wichtige Reader- und Writer-Klassen 7.2 Datei-Ströme . . . . . . . . . . . . . . . . . . 7.3 Puffern von Daten . . . . . . . . . . . . . . . 7.4 Filter-Ströme . . . . . . . . . . . . . . . . . . 7.5 Standard-Ein- und Ausgabe . . . . . . . . . . 7.6 IO-Exceptions . . . . . . . . . . . . . . . . . . 7.7 RandomAccess . . . . . . . . . . . . . . . . . 7.8 Weitere Aspekte von I/O . . . . . . . . . . . . 7.8.1 Tokenizer . . . . . . . . . . . . . . . . 7.8.2 Serializable, Externalizable . . . . . . 7.8.3 Pipe-Ströme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 90 90 91 93 94 95 96 98 99 99 100 100 101 101 8 Vererbung und Typsicherheit 8.1 Formale Modelle für Programmiersprachen 8.2 Featherweight Java . . . . . . . . . . . . . . 8.2.1 Programmbeispiel . . . . . . . . . . 8.2.2 Syntax für FJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 102 103 104 105 . . . . 8.2.3 Subtyping . . . . . . . . . 8.2.4 Hilfsfunktionen . . . . . . 8.3 Typisierung und Reduktion in FJ 8.3.1 Typisierungsregeln . . . . 8.3.2 Reduktionsregeln . . . . . 8.3.3 Veranschaulichung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 106 108 108 108 110 9 Abstrakte Klassen und Interfaces 9.1 Abstrakte Klassen und Methoden . . . . . . . . . . . . . . . . . . . . . . . 9.2 Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2.1 Implementation eines Interfaces . . . . . . . . . . . . . . . . . . . 9.2.2 Interfaces und Konstanten . . . . . . . . . . . . . . . . . . . . . . . 9.2.3 Benutzung von Interfaces . . . . . . . . . . . . . . . . . . . . . . . 9.2.4 Interface vs. Abstrakte Klasse . . . . . . . . . . . . . . . . . . . . . 9.2.5 Implementation mehrerer Interfaces und Erweitern von Interfaces . 9.2.6 Marker-Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.3 Das Enumeration-Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 113 114 115 116 116 117 118 120 120 10 Innere Klassen 10.1 Member Classes . . . . . . . . . . . . . . . . . . . . . . . . . 10.1.1 Anschauliches Beispiel . . . . . . . . . . . . . . . . . 10.1.2 Beispiel ‘Enumerator’ . . . . . . . . . . . . . . . . . . 10.1.3 Eigenschaften von Member-Klassen . . . . . . . . . . 10.1.4 Implementation von Member-Klassen . . . . . . . . . 10.1.5 Member-Klassen und Vererbung . . . . . . . . . . . . 10.2 Static Member Classes . . . . . . . . . . . . . . . . . . . . . 10.2.1 Anschauliches Beispiel . . . . . . . . . . . . . . . . . 10.2.2 Eigenschaften von Static Member Classes . . . . . . 10.2.3 Implementation von statischen Member-Klassen . . . 10.3 Lokale Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . 10.3.1 Anschauliches Beispiel . . . . . . . . . . . . . . . . . 10.3.2 Beispiel: ‘Enumerator’ als lokale Klasse . . . . . . . . 10.3.3 Eigenschaften Lokaler Klassen . . . . . . . . . . . . . 10.3.4 Geltungsbereich Lokaler Klassen . . . . . . . . . . . . 10.4 Anonyme Klassen . . . . . . . . . . . . . . . . . . . . . . . . 10.4.1 Beispiel: ‘Enumerator’ als anonyme Klasse . . . . . . 10.4.2 Eigenschaften von Anonymen Klassen . . . . . . . . . 10.4.3 Implementation von Lokalen und Anonymen Klassen . 10.4.4 Adapter-Klassen als Anonyme Klassen . . . . . . . . 10.4.5 Anwendung und Konventionen . . . . . . . . . . . . . 10.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . 10.6 Beispiel-Code ‘Enumeration’ . . . . . . . . . . . . . . . . . . 10.7 Adapter-Patterns und Java Adapter-Klassen . . . . . . . . . . 10.8 Innere Klassen und Lexical Closures . . . . . . . . . . . . . . 10.8.1 Code as Data . . . . . . . . . . . . . . . . . . . . . . . 10.8.2 Adder-Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 123 123 124 125 126 127 128 128 129 130 130 130 131 132 133 135 135 135 136 136 137 137 138 140 141 142 142 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Abstrakte Datentypen und Collections 11.1 Abstrakte Datentypen . . . . . . . . . . . 11.1.1 Grundlagen . . . . . . . . . . . . . 11.1.2 Funktionalitäten von Collections . 11.2 Implementation von Collections . . . . . . 11.3 Implementation mit Array . . . . . . . . . 11.3.1 MyCollection/Array . . . . . . . . . 11.3.2 Erläuterungen zu ‘MyCollection’ . 11.3.3 Test-Protokoll für ‘MyCollection’ . . 11.3.4 Bag/Array . . . . . . . . . . . . . . 11.3.5 Erläuterungen zu ‘Bag’ . . . . . . 11.3.6 Set/Array . . . . . . . . . . . . . . 11.3.7 Erläuterungen zu Set . . . . . . . 11.3.8 Test-Protokoll für ‘Set’ . . . . . . . 11.3.9 EquivalenceSet/Array . . . . . . . 11.3.10Erläuterungen zu ‘EquivalenceSet’ 11.3.11Test-Protokoll für ‘EquivalenceSet’ 11.4 Implementation mit Offener Hash-Tabelle 11.4.1 Array versus Hash-Tabelle . . . . . 11.4.2 MyCollection/Hash . . . . . . . . . 11.4.3 Erläuterungen zu ‘MyCollection’ . 11.4.4 Bag/Hash . . . . . . . . . . . . . . 11.4.5 Set/Hash . . . . . . . . . . . . . . 11.5 ADT-Test . . . . . . . . . . . . . . . . . . 11.5.1 Anforderungen an ‘Test’ . . . . . . 11.5.2 Test/ADT . . . . . . . . . . . . . . 11.5.3 Erläuterungen zu ‘Test’ . . . . . . 11.6 Implementation mit Liste . . . . . . . . . . 11.6.1 Dynamische Datenstrukturen . . . 11.6.2 Bag/List . . . . . . . . . . . . . . . 11.6.3 Erläuterungen zu ‘Bag’ . . . . . . 11.6.4 Unterklasse ‘Set’ . . . . . . . . . . 11.6.5 Unterklasse ‘EquivalenceSet’ . . . 11.7 Implementation mit Suchbaum . . . . . . 11.7.1 Suchbäume . . . . . . . . . . . . . 11.7.2 Set/Tree . . . . . . . . . . . . . . . 11.7.3 Erläuterungen zu ‘Set’ . . . . . . . 11.8 Visitor . . . . . . . . . . . . . . . . . . . . 11.8.1 Konzept eines Visitor . . . . . . . 11.8.2 ‘Visitor’, ‘Visitable’/ADT . . . . . . 11.8.3 Suchbaum mit Visitor . . . . . . . 11.9 Java Collection Classes . . . . . . . . . . 11.9.1 Grundstruktur . . . . . . . . . . . . 11.9.2 Illustration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 143 143 143 144 145 145 147 147 148 149 149 150 151 151 152 152 153 153 153 156 156 157 157 157 158 160 161 161 163 165 166 166 167 167 168 172 174 174 175 176 176 176 177 12 Reflections 12.1 Methoden des Reflection-API . . . . . . . . . . . . . . . . . . . 12.2 Die Klassen ‘Class’, ‘Method’, ‘Field’ und ‘Constructor’ . . . . . 12.3 Inspektion von Klassen . . . . . . . . . . . . . . . . . . . . . . 12.3.1 Abruf eines ‘Class’ Objekts . . . . . . . . . . . . . . . . 12.3.2 Abruf des Klassen-Namens eines Objekts . . . . . . . . 12.3.3 Abruf von Klassen-Modifikatoren . . . . . . . . . . . . . 12.3.4 Abruf von Oberklassen . . . . . . . . . . . . . . . . . . 12.3.5 Abruf des implementierten Interfaces einer Klasse . . . 12.3.6 Interface oder Klasse? . . . . . . . . . . . . . . . . . . . 12.3.7 Abruf von Klassen-Feldern . . . . . . . . . . . . . . . . 12.3.8 Abruf von Klassen-Konstruktoren . . . . . . . . . . . . . 12.3.9 Abruf von Methoden-Information . . . . . . . . . . . . . 12.4 Manipulation von Objekten zur Laufzeit . . . . . . . . . . . . . 12.4.1 Dynamische Erzeugung von Objekten . . . . . . . . . . 12.4.2 Exceptions beim dynamischen Erzeugen von Objekten 12.4.3 Dynamische Erzeugung mit Konstruktor-Argumenten . 12.4.4 Abruf und Belegung von Feldern . . . . . . . . . . . . . 12.4.5 Method Invocation . . . . . . . . . . . . . . . . . . . . . 12.5 Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 180 180 181 181 182 183 183 184 184 185 186 186 188 188 188 189 190 190 191 13 Multi-Threading – Grundlagen der Nebenl äufigkeit 13.1 Sequentialität, Determinismus, Determiniertheit . 13.1.1 Nebenläufigkeit . . . . . . . . . . . . . . . 13.1.2 Nicht-Determinismus . . . . . . . . . . . . 13.1.3 Nicht-Determiniertheit . . . . . . . . . . . 13.1.4 Verzahnung . . . . . . . . . . . . . . . . . 13.2 Nebenläufigkeit in Java: Threads . . . . . . . . . 13.2.1 Definition von Threads . . . . . . . . . . . 13.2.2 Die Klasse Thread . . . . . . . . . . . . . 13.2.3 Einfaches Beispiel: ‘ThreadDemo’ . . . . 13.2.4 Erläuterungen zu ‘ThreadDemo’ . . . . . 13.2.5 Zustände von Threads . . . . . . . . . . . 13.3 Kooperierende und Konkurrierende Prozesse . . 13.3.1 Kooperierende Prozesse . . . . . . . . . 13.3.2 Konkurrierende Prozesse . . . . . . . . . 13.4 Synchronisation . . . . . . . . . . . . . . . . . . 13.5 Monitore in Java . . . . . . . . . . . . . . . . . . 13.5.1 Synchronized . . . . . . . . . . . . . . . . 13.5.2 Funktion von ‘synchronized’ . . . . . . . . 13.5.3 Warten auf Ereignisse mit Monitoren . . . 13.6 Beispiel: Produzent/Konsument . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 193 193 194 194 195 195 196 196 197 199 200 200 200 201 203 203 203 205 205 206 14 Multi-Threading: Semaphoren und Deadlocks 14.1 Semaphoren . . . . . . . . . . . . . . . . . . . . . 14.1.1 Konzept . . . . . . . . . . . . . . . . . . . . 14.1.2 Klasse ‘Semaphore’ . . . . . . . . . . . . . 14.1.3 Einseitige und Mehrseitige Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 209 209 209 210 14.1.4 Erzeuger-/Verbraucher-Problem mit Semaphoren . . 14.2 Conditional Critical Regions . . . . . . . . . . . . . . . . . . 14.2.1 Monitore, CCRs, Semaphoren . . . . . . . . . . . . 14.3 Deadlocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.3.1 Lösung mit Semaphoren . . . . . . . . . . . . . . . 14.3.2 Dining Philosophers – Lösung mit globaler Kontrolle 14.3.3 Dining Philosophers – Bedingter Zugriff auf Gabel . 14.3.4 Deadlocks durch falsche Anordnung . . . . . . . . . 14.4 Threads: Ergänzungen . . . . . . . . . . . . . . . . . . . . . 14.5 Pipe-Ströme . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Reguläre Ausdrücke und Pattern-Matching 15.1 String Pattern-Matching . . . . . . . . . . . . . . . . 15.1.1 Motivation . . . . . . . . . . . . . . . . . . . . 15.1.2 Straightforward Lösung . . . . . . . . . . . . 15.1.3 String-Matching mit endlichen Automaten . . 15.1.4 Der Knuth-Morris-Pratt (KPM) Algorithmus . 15.1.5 Pattern-Matching mit Regul ären Ausdrücken 15.2 Java 1.4 ‘regex’ . . . . . . . . . . . . . . . . . . . . . 15.2.1 Konstruktion regulärer Ausdrücke . . . . . . . 15.2.2 Die Pattern-Klasse . . . . . . . . . . . . . . . 15.2.3 Die Matcher-Klasse . . . . . . . . . . . . . . 15.2.4 Beispiel: Suche und Ersetze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 212 213 213 213 214 217 219 220 221 . . . . . . . . . . . 225 225 225 225 225 227 229 230 230 231 231 231 16 Assertions 234 16.1 Zusicherungskalkül . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234 16.2 Die ‘assert’-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 17 Ausblick: GUIs und Event Handling 17.1 Java Foundation Classes . . . . . . . . . . . . . . . . . 17.2 Swing-Komponenten . . . . . . . . . . . . . . . . . . . . 17.2.1 Erstes Beispiel ‘HelloWorldSwing’ . . . . . . . . 17.2.2 Properties . . . . . . . . . . . . . . . . . . . . . . 17.2.3 Container . . . . . . . . . . . . . . . . . . . . . . 17.2.4 Layout Management . . . . . . . . . . . . . . . . 17.2.5 Anmerkungen . . . . . . . . . . . . . . . . . . . 17.3 Event-Handling . . . . . . . . . . . . . . . . . . . . . . . 17.3.1 Event-Objekte . . . . . . . . . . . . . . . . . . . 17.3.2 Event Listener . . . . . . . . . . . . . . . . . . . 17.3.3 Event Handling mit Inneren Klassen . . . . . . . 17.4 Applets . . . . . . . . . . . . . . . . . . . . . . . . . . . 17.4.1 Unterschiede zwischen Applets und Applications 17.4.2 Schreiben von Applets . . . . . . . . . . . . . . . 17.4.3 Beispiel . . . . . . . . . . . . . . . . . . . . . . . 17.5 GUIs und Threads . . . . . . . . . . . . . . . . . . . . . 17.6 Beans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 236 236 236 238 238 240 241 242 242 242 243 245 245 245 246 247 248 18 Ausblick: Verteilte Systeme 18.1 Netzwerk-Anwendungen in Java . . . . . . . . . . . 18.2 Grundlagen für Kommunikation im Netz . . . . . . . 18.2.1 Open System Interconncetion (OSI) Model . 18.2.2 TCP und UDP . . . . . . . . . . . . . . . . . 18.2.3 Ports . . . . . . . . . . . . . . . . . . . . . . . 18.2.4 Networking Klassen in Java . . . . . . . . . . 18.3 Die Klasse ‘URL’ . . . . . . . . . . . . . . . . . . . . 18.3.1 Was ist eine URL? . . . . . . . . . . . . . . . 18.3.2 Nutzen der URL Klasse . . . . . . . . . . . . 18.3.3 Beispiel: URLConnection . . . . . . . . . . . 18.4 Sockets für Client/Server Kommunikation . . . . . . 18.4.1 Grundidee der Client/Server Kommunikation 18.4.2 Sockets in Java . . . . . . . . . . . . . . . . . 18.4.3 Beispiel ‘KnockKnockServer’ . . . . . . . . . 18.5 Sicherheit . . . . . . . . . . . . . . . . . . . . . . . . 19 Andere Objekt-Orientierte Sprachen 19.1 Das 8-Damen Problem Revisited 19.2 Lösung in Smalltalk . . . . . . . 19.3 Lösung in Objective-C . . . . . . 19.4 Lösung in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 249 249 249 250 251 251 251 251 252 253 254 254 255 255 258 . . . . 259 259 259 260 261 Informatik B SS 03 1 1 Einführung: Programmier-Paradigmen und -Sprachen 1.1 Entwicklung von Paradigmen und Sprachen 1.1.1 Überblick Zur Zeit existieren vier Programmier-Paradigmen: imperativ, funktional, logisch, objekt-orientiert. Objekt-Orientierung ist das jüngste Paradigma (80er Jahre). Jedem Paradigma lassen sich konkrete Programmiersprachen zuordnen. Java ist eine relativ junge Sprache, die das objekt-orientierte Paradigma unterstützt (1995). Imperative/Prozedurale Sprachen (Fortran, Pascal, Modula, C): Zuweisung von Werten an Variablen durch Befehle, Zustandstransformation. Logische Sprachen (Prolog): Programm als Menge von logischen Klauseln, Interpretation: Beweis der Anfrage bei gegebenem Programm. Funktionale Sprachen (Lisp, ML): Programm als Menge (typisierter) Funktionen, nur call by value keine Seiteneffekte. Objekt-orientierte Sprachen (C++, Smalltalk, Java): Programm als Menge von Objekten, die miteinander interagieren. Is a programming language a tool for instructing machines? A means for communicating between programmers? A vehicle for expressing high-level designs? A notation for algorithms? A way of expressing relationships between concepts? A tool for experimentation? A means for controlling computerized devices? My conclusion is that a general-purpose programming language must be all of those to serve its diverse set of users. The only thing a language cannot be – and survive – is be a mere collection of “neat” features. (Stroustrup, 1994) 1.1.2 Entwicklung von Hochsprachen Die Entwicklung von Hochsprachen wird in Abbildung 1 skizziert: Am Beginn der Entwicklung von Computern wurde mit Maschinensprachen (“erste Generation”) und Assembler-Sprachen (“zweite Generation”) programmiert. Hochsprachen (“dritte Generation”; seit Ende der 50er Jahre) brachten einen wesentlichen Fortschritt: Lesbarkeit, Fehlerprüfung, Maschinenunabhängigkeit, Bibliotheken. strukturierte Programmierung (Dijkstra): Zerlegung eines Programms (Prozeduren, Abstrakte Datentypen, Module) Informatik B SS 03 2 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 Fortran Algol−58 Lisp Algol−60 Cobol Simula I Simula−67 Algol−68 Pascal C Prolog ML Scheme Modula−2 Fortran−77 Smalltalk Ada−83 Common Lisp C++ Standard−ML Objective C CLOS Modula−3 Haskell Fortran−90 Eiffel Ada−95 IMPERATIV Java OBJEKTORIENTIERT FUNKTIONAL LOGISCH Abbildung 1: Entwicklung von höheren Programmiersprachen Hochsprachen werden entweder von einem Compiler in Maschinencode übersetzt oder über einen Interpreter ausgeführt. Dazwischen stehen just-in-time Compiler, bei denen Teile des Programms erst während der Ausführung übersetzt werden. (“vierte Generation”: anwendungsspezifische Sprachen wie SQL, Mathematica) Funktionale und logische Sprachen werden gemeinsam auch als deklarative Sprachen bezeichnet (“fünfte Generation”, KI-Sprachen). Eine Programmiersprache ist eine formale Sprache mit Syntax (Grammatik, die beschreibt, wie die Symbole der Sprache kombiniert werden dürfen), Semantik (Bedeutung der Sprachkonstrukte, ihr Laufzeitverhalten), und Pragmatik (Verwendung von Sprachmerkmalen; Sprachimplementation) Sprachen sind sich mehr oder weniger ähnlich. Sprachen innerhalb desselben Programmierparadigmas basieren auf ähnlichen Grundstrukturen. Natürliche Sprachen – Deutsch, Englisch, Hopi-Indianisch, Japanisch, Esperanto – fallen in verschiedene Sprachfamilien. Indogermanische Sprachen basieren auf anderen Konzepten als indianische Sprachen und damit sind andere Vorstellungen von der Welt verbunden (z. B. Zeit als Pfeil versus Zyklus). Informatik B SS 03 3 1.1.3 Motivation: Kenntnis mehrere Paradigmen Die Grenzen meiner Sprache sind die Grenzen meiner Welt. (L. Wittgenstein) Gründe, warum man mehrere Paradigmen/Sprachen kennen sollte: Größeres Repertoire an Ausdrucksmöglichkeiten – Sprache beschränkt nicht nur was wir formulieren, sondern auch, was wir denken können (Whorf, Language, Thought, and Reality, 1956) – Die Sprache, in der Programme entwickelt werden, beschränkt die möglichen Kontrollstrukturen, Datenstrukturen und Abstraktionen, die verwendet werden können. Damit ist auch die Bandbreite der realisierbaren Algorithmen beschränkt. – Zum Teil können Konzepte einer Sprachklasse in einer anderen simuliert werden. Aber: Es ist immer besser, die Konzepte der Sprache, in der man arbeitet, voll auszunutzen, als nicht gut unterstützte Konzepte zu simulieren (weniger elegant, weniger effizient). Hintergrund für geeignete Sprachwahl – Die Kenntnis einer größeren Menge von Sprachen aus verschiedenen Paradigmen erlaubt es, für ein gegebenes Problem die geignetste Sprache zu wählen. Bessere Voraussetzung um neue Sprachen zu lernen – Programmierer mit wenig formaler Ausbildung lernen häufig nur ein oder zwei Sprachen. Ist die Ausbildung zudem an der Syntax der Sprache und nicht an den Konzepten orientiert, ist es nur schwer möglich, sich schnell in neue Sprachen einzuarbeiten. – Kenntnis des Vokabulars und der Konzepte von Programmiersprachen ist Voraussetzung, um Sprachdokumentationen verstehen zu können. Verständnis von Implementations-Details – Wenn man etwas darüber weiss, wie eine Sprache entworfen und implementiert ist, kann man die Sprache oft intelligenter, ihrem Design entsprechend, nutzen. Man wird ein besserer Programmierer, wenn man versteht, warum bestimmte Konstrukte in einer Sprache realisiert wurden und was die Konsequenzen davon sind. – Verständnis darüber, wie Programme ausgeführt werden, kann die Fehlervermeidung, -erkennung und -beseitigung erleichtern und hilft oft, Programme effizienter zu realisieren. Informatik B SS 03 4 Voraussetzung für Sprachentwicklung – Die Entwicklung neuer Allzweck-Programmiersprachen kommt nicht allzu häufig vor, und es arbeitet nur eine relativ kleine Gruppe von Informatikern in der Sprachentwicklung. – Aber: Viele Programmierer werden ab und zu kleine Sprachen für spezielle Anwendungen entwerfen. Allgemeine Entwicklung von Programmiersprachen – Nicht immer ist die populärste Sprache die beste Sprache. Wenn diejenigen, die sich für die Verwendung einer Sprache entscheiden, besser informiert sind, können sich gute Sprachen vielleicht besser/schneller durchsetzen. 1.2 Prozeduren und Parameterübergabe-Mechanismen 1.2.1 Programmstruktur bei imperativen Sprachen In objekt-orientierten Sprachen sind Klassen/Objekte die kleinsten autonomen Bausteine. In imperativen Sprachen gliedert sich ein Programm dagegen in – einen Deklarationsblock (Typdefinitionen, globale Variablen und Konstanten), – eine Menge von Prozeduren und – ein Hauptprogramm. In Abbildung 2 ist ein Modula-2 Programm abgedruckt. 1.2.2 Prozeduren Prozeduren dienen der Strukturierung des Programms. Wie Methoden in Java sind Prozeduren benannt ( Rekursion als Kontrollstruktur möglich). Prozeduren bestehen aus einem Kopf und einem Rumpf. Der Kopf besteht aus einem Schlüsselwort (z.B. PROCEDURE), einem benutzerdefinierten, möglichst vielsagenden Namen (z.B. ReadMaxIndex), einer Folge von (0 bis beliebig vielen) formalen Parametern, und möglicherweise einem Ergebnistyp (Funktion). (vgl. Java Methoden mit Rückgabetyp bzw. void). Wie bei Java-Methoden können im Rumpf lokale Variablen und Konstanten deklariert werden. Die Berechnungsvorschrift wird als Folge von Anweisungen angegeben. Informatik B SS 03 5 MODULE ggt; FROM InOut IMPORT ReadCard, WriteCard, WriteString, WriteLn; PROCEDURE ReadMaxIndex(VAR maxindex : CARDINAL) : BOOLEAN; BEGIN WriteString("Gib eine ganze, positive Zahl ein : "); ReadCard(maxindex); RETURN (maxindex > 0) (* TRUE wenn maxindex > 0 *) END ReadMaxIndex; PROCEDURE ggT(arg1, arg2 : CARDINAL) : CARDINAL; VAR x, y, z : CARDINAL; (* Hilfsvariablen zur ggT-Bestimmung *) BEGIN x:=arg1; (* ggT(arg1, arg2) bestimmen *) y:=arg2; WHILE y#0 DO z:=x MOD y; x:=y; y:=z END; RETURN x END ggT; PROCEDURE WriteGGTs(maxindex : CARDINAL); VAR line : CARDINAL; (* Zeilenindex *) column : CARDINAL; (* Spaltenindex *) BEGIN WriteString(" "); (* Tabelle *) FOR column:=1 TO maxindex DO WriteCard(column, 3) END; WriteLn; WriteLn; FOR line:=1 TO maxindex DO WriteCard(line, 3); FOR column:=1 TO maxindex DO WriteCard(ggT(line, column), 3) END; WriteLn END END WriteGGTs; VAR maxindex : CARDINAL; (* Zeilenanzahl=Spaltenanzahl der Tabelle *) BEGIN IF ReadMaxIndex(maxindex) THEN WriteGGTs(maxindex) ELSE WriteString("falsche Eingabe"); WriteLn END END ggt. Abbildung 2: Modula-2 Programm zur Berechnung des ggT Informatik B SS 03 6 1.2.3 Parameterübergabe-Methoden Formale Parameter werden im Kopf einer Prozedur angegeben. Im Rumpf tauchen sie in Anweisungen auf. Formale Parameter sind Platzhalter für konkrete Parameter, die beim Prozeduraufruf übergeben werden. Parameterübergabe meint das Matching der konkreten mit den formalen Parametern. Es gibt verschiedene Parameterübergabe-Methoden: – Call-by-value: Argumente werden zunächst ausgewertet. Die resultierenden Werte werden dann übergeben. Zuweisungen an die Parameter innerhalb der Prozedur haben keine Auswirkung auf die im aufrufenden Kontext vorhandenen Variablen. (Man spricht auch von strict evaluation.) Beim Aufruf ggT(line, columnn) in der Prozedur WriteGGTs werden zunächst line und column durch die aktuellen Werte dieser Variablen ersetzt. Mit diesen Werten werden dann die Parameter von ggT – arg1, arg2 – initialisiert. – Call-by-reference: Die Referenz (Adresse) der Argument-Variablen wird an die Prozedur übergeben. Ausführung der Prozedur kann Seiteneffekte auf die Werte der Argumente im aufrufenden Kontext haben. Im Modula-2-Programm in Abb. 2 ist maxindex ein call-by-reference Parameter. maxindex wird in der Prozedur ReadMaxIndex belegt. Wird die Prodzedur WriteGGTs aufgerufen, so hat maxindex den in ReadMaxIndex ermitteltend Wert. – Call-by-name: Argumentausdrücke werden unausgewertet (als Referenz auf Code, der den Wert des Arguments liefert, zusammen mit einer Umgebung für die Werte von freien Variablen) übergeben. (lazy evaluation) Um Mehrfachauswertung zu vermeiden, wird üblicherweise mit Graphreduktionsmethoden gearbeitet (call-by-need). Call-by-name wurde im Rahmen von Algol-60 entwickelt und wird heute vor allem bei funktionalen Sprachen verwendet. Vorteile: Termination bei unendlichen Datenstrukturen (lazy lists) und Rekursion. Informatik B SS 03 7 1.2.4 Call-by-Value versus Call-by-Reference MODULE ParaPass; FROM InOut IMPORT WriteCard, WriteLn; PROCEDURE P(x : CARDINAL; y : CARDINAL; VAR z : CARDINAL); VAR help : CARDINAL; BEGIN help := x; x := 15; z := 2 * x + help; y := 0 END P; VAR a, b, c : CARDINAL; BEGIN a := b := c := P(a, 1; 2; 3; a+b, c); (* (* WriteCard(a); (* WriteCard(b); (* WriteCard(c) (* END ParaPass. call by value: Ausdruecke moeglich *) call by reference: muss Variable sein *) immer noch 1 *) immer noch 2 *) 31 *) Abbildung 3: Call-by-Value und Call-by-Reference in Modula 1.2.5 Call-by-Value versus Call-by-Name (Beispiel in ML) fun sqr(x) : int = x*x; (* uses its argument twice *) fun zero(x :int) = 0; (* ignores its argument *) Auswertung von zero(sqr(sqr(sqr(2)))) mit call-by-value: zero(sqr(sqr(2 2))) zero(sqr(sqr(4))) zero(sqr(sqr(sqr(2)))) ... zero(256) 0 Auswertung von zero(sqr(sqr(sqr(2)))) mit call-by-name: direkt Rückgabe von 0. Informatik B SS 03 8 Aber (ohne Graphreduktion) resultiert unnötige Mehrfachberechnung: sqr(sqr(sqr(2))) ... 1.2.6 Call-by-value in Java In Sprachen wie Modula und ! existieren sowohl call-by-value als auch call-by-reference als Parameterübergabe-Mechanismen. Call-by-reference macht auch dann Sinn, wenn die Parameterwerte nicht innnerhalb der Prozedur geändert werden sollen: Speicherersparnis und Effizienz (z.B. für die Übergabe von Arrays entfällt Kopieren) In Java existiert call-by-value als einziger Parameterübergabe-Mechanismus. Meist liest man Aussagen wie “primitive types are passed by value, but objects are passed by reference”, die Verwirrung stiften können. Auch wenn Objekte natürlich als Referenzen übergeben werden, ist der Parameterübergabe-Mechanismus call-by-value! Das Programm PassByValue “missbraucht” die Klasse Button aus java.awt etwas: genutzt wird nur das Feld label. // Copyright (C) Andrew D. Mackie, 1999. // http://www.javamain.com/ import java.awt.*; public class PassByValue { //Demonstrates that Java parameters are always passed by value public static void main(String[] args){ System.out.println("In main"); //the reference to an object is passed by value Button b = new Button("AAA"); System.out.println("The value of b’s label is " + b.getLabel()); methodX(b); System.out.println("Back in main"); System.out.println("The value of b’s label is " + b.getLabel()); System.out.println(""); //primitives are passed by value as well int i = 5; System.out.println("The value of i is " + i); methodZ(i); System.out.println("Back in main"); Informatik B SS 03 9 System.out.println("The value of i is " + i); System.exit(0); } //the reference to an object is passed by value public static void methodX(Button y){ System.out.println("In methodX"); System.out.println("The value of y’s label is " + y.getLabel()); //update the button object that both y and b refer to y.setLabel("BBB"); System.out.println("The value of y’s label is " + y.getLabel()); //make y reference a different object - doesn’t affect variable b y = new Button("CCC"); System.out.println("The value of y’s label is " + y.getLabel()); //updating button that y now references //has no affect on button referenced by b y.setLabel("DDD"); System.out.println("The value of y’s label is " + y.getLabel()); } //primitives are passed by value as well public static void methodZ(int j){ System.out.println("In methodZ"); System.out.println("The value of j is " + j); //change value of j - doesn’t affect variable i within main j = 6; System.out.println("The value of j is " + j); } } Ausgabe: Back in main In main The value of b’s label is AAA In methodX The value of y’s label is AAA The value of y’s label is BBB The value of b’s label is BBB The value of i is 5 In methodZ The value of j is 5 The value of j is 6 The value of y’s label is CCC The value of y’s label is DDD Back in main The value of i is 5 Informatik B SS 03 10 b b Button Object y Another Button Object Button Object y Abbildung 4: Call-by-Value in Java (siehe Klasse PassByValue) 1.3 Sprach-Implementierung Ein Programm (Code, Quellcode) wird geschrieben, um Aufgaben automatisch von einem Rechner ausführen zu lassen. Üblicherweise wird ein Programm gestartet, erhält eine Eingabe, führt Berechnungen in Abhängigkeit von dieser Eingabe durch, liefert eine Ausgabe und wird beendet. (Programmausführung) Die Laufzeit des Programms meint die Zeit vom Aufruf bis zur Termination; die Compilezeit meint die Zeit, während das Programm in Maschinen- oder Zwischencode überführt wird. In anderem Zusammenhang meint Laufzeit auch die Zeitdauer, die zur Ausführung eines Programms benötigt wird: Diese ist abhängig von der Maschine und insbesondere von der Aufwandsklasse der zugrundeliegenden Algorithmen. (Die Komplexität von Problemen, die gelöst werden sollen, bedingt die untere Schranke des Aufwands von Algorithmen. Thema in der Vorlesung “Theoretische Informatik”) Ein Compiler übersetzt ein Programm in eine Ausgabesprache (häufig Maschinencode). Beispiele für Compilersprachen sind C und Pascal. (siehe Abb. 5) Ein Interpreter ist eine Maschine auf höherer Ebene, die das Programm direkt ausführt. Beispiele für Interpretersprachen sind Common Lisp und Prolog. (siehe Abb. 5) Alternativ gibt es hybride Implementationssysteme: Das Programm wird zunächst in einen Zwischencode (Bytecode) übersetzt, der auf beliebige Maschinen portierbar ist, für die ein entsprechender Bytecode-Interpreter und dazugehöriges Laufzeitsystem existiert. Beispielsweise erzeugt der Informatik B SS 03 11 Quellprogramm ÜBERSETZZEIT Compiler Symbol− tabelle Quellprogramm Quellprogramm Lexikalische Analyse Lexikalische Analyse Syntaxanalyse Syntaxanalyse Zwischencode−Erzeugung Zwischencode−Erzeugung Semantische Analyse (Optimierung) LAUFZEIT Codeerzeugung Eingabe Maschinencode Eingabe Eingabe Maschine Interpreter Ausgabe Ausgabe Zwischencode Interpreter Ausgabe Abbildung 5: Implementations-Systeme: Compiler, Interpreter, Hybrid Java-Compiler Bytecode, der von der Java Virtual Machine (rechnerspezifisch) ausgeführt wird. (siehe Abb. 5) Arbeitsschritte eines Compilers: – Lexikalische Analyse: Zerlegung des Programmtextes in lexikalische Einheiten (Reservierte Worte, Operatoren, Identifier; Kommentare werden ignoriert) – Syntaktische Analyse (Parsierung): Erzeugung eines Parse-Baums, der die syntaktische Struktur des Programms repräsentiert. – Semantische Analyse: üblicherweise Typisierung Zwischencode-Erzeugung (wenn ohne Optimierungsoption, z. B. Assembler) – Optimierung (optional): Methoden der Programmtransformation, partielle Evaluation (Ersetzen von Ausdrücken durch ihr Ergebnis) – Codegenerierung: äquivalentes Maschinencode-Programm – Symboltabelle: Datenbasis für die Compilierung, enthält insbesondere Typen und Eigenschaften der benutzergenerierten Namen im Programm. Vor der Ausführung müssen noch die entsprechenden Programme des Betriebssystems und eventuell Bibliotheksfunktionen mit dem Maschinenprogramm verbunden werden (linking, Einfügen der entsprechenden Adressen in das Maschinencode-Programm). Ausführung: In fetch-execute-Zyklen: Jede Maschineninstruktion muss vom Speicher in den Prozessor übertragen werden und Ergebnisse/Verweise Informatik B SS 03 12 müssen in den Speicher zurückgeschrieben werden. (von-Neumann Flaschenhals) 1.4 Syntaktische Struktur 1.4.1 Formale Sprachen Die Syntax einer Sprache legt fest, wie einzelne Symbole zu größeren Strukturen zusammengefügt werden dürfen. Die Syntax einer Sprache wird – für natürliche wie für formale Sprachen – durch eine Grammatik beschrieben: mit – – – als Menge der Nonterminal-Symbole als Menge der Terminalsymbole als Menge der Grammatikregeln (Produktionen) und als Startsymbol. Beispiel (einfacher Ausschnitt des Englischen) !"#$%&')(&*"+-,.0/1+-2 #&#(&304)( 656" 7 8 (noun phrase) 7 :9; (verbal phrase) (determiner) $% (noun) ')(&<9*"+-,9=/1+=2 # (verb) #(& >9=4( & 5 – mit : Es können syntaktisch korrekte (aber nicht unbedingt semantisch sinnnvolle) Sätze abgeleitet werden, wie “the mouse eats the cat”. ? A@BC>"D=EFHG6" mit : E6@I9JG#C @ GK9JG- C E9E3 beschreibt eine einfache Sprache über dem Alphabet =EFHG . Mit den Regeln aus kann eine unendliche Menge von Worten generiert Beispiel: werden, die zu dieser Sprache gehören. Für natürliche Sprachen geben Grammatiken meist an, wie korrekte Sätze aus Worten geformt werden. Für Programmiersprachen geben Grammatiken an, wie Programme als wohlgeformte Ausdrücke über Schlüsselwörtern und Bezeichnern konstruiert werden. Eine formale Sprache wird als die Menge der wohlgeformten Worte über einem endlichen Alphabet definiert. Worte, die zu obiger Sprache gehören, sind 01, 10, 0101, 0110, 1010, 1001. Die Konstruktion der Wortmenge kann mit einem Ableitungsbaum dargestellt werden (siehe Abb. 6). Informatik B SS 03 13 S 0A 1B 01S 010A 0101S 01 011B 0101 0110 0110S 10 10S 100A 1001 1001S 101B 1010 1010S Abbildung 6: Ableitungsbaum für die einfache reguläre Sprache EFG EFG GHE G#E Die Sprache ist regulär und kann durch den regulären Ausdruck dargestellt werden. bezeichnet , bezeichnet die positive Hülle (Menge aller Konkatenationen von Elementen aus ohne das leere Wort), bezeichnet die Kleene’sche Hülle.) Reguläre Sprachen sind die einfachsten Sprachen in der sogenannten Chomsky Hierarchie. Sie können durch endliche Automaten erkannt werden. Programmiersprachen sind im Wesentlichenn kontextfreie Sprachen. Kontextfreie Sprachen folgen in der Chomsky-Hierarchie auf die regulären. (J ( E3 G& EFG Beispiel: J von genauso vielen G -en. Während Grammatikregeln für reguläre Sprachen links- bzw. rechts-linear sind ( bzw. ), sind bei kontextfreien Sprachen beliebige Kombinationen aus Terminal- und Nonterminalsymbolen erlaubt. beschreibt die Sprache E FG , also 1E -en gefolgt Die Kenntnis formaler Sprachen ist wichtig für die Implementierung von Programmiersprachen. Das Sprachdesign (die Grammatik) bedingt wie aufwendig die Parsierung von Programmen (siehe Phasen der Compilierung) ist! Formale Sprachen sind Gegenstand der Vorlesung “Theoretische Informatik”. 1.4.2 BNF und EBNF Die Syntax von Programmiersprachen wird üblicherweise in Backus-Naur Form (BNF) oder erweiterter Backus-Naur Form (EBNF) dargestellt. BNF und EBNF sind zu kontextfreien Grammatiken äquivalent. BNF und EBNF sind Meta-Sprachen zur Beschreibung von Sprachen. Produktionsregeln haben die Form Kopf ::= Körper. Nichtterminale werden in spitzen Klammern dargestellt, dadurch können auch ganze Wörter statt nur einzelne Symbole als Nonterminale verwendet werden. Alternative rechte Seiten werden durch einen senkrechten Strich getrennt dargestellt. Informatik B SS 03 14 class_declaration ::= { modifier } "class" identifier [ "extends" class_name ] [ "implements" interface_name { "," interface_name } ] "{" { field_declaration } "}" for_statement ::= "for" "(" ( variable_declaration | ( expression ";" ) | ";" ) [ expression ] ";" [ expression ] ";" ")" statement Abbildung 7: Klassen-Definition und For-Statement von Java in EBNF Runde Klammern regeln Zusammenfassung und Vorrang. Alternativen haben den geringsten Vorrang. In der EBNF sind zusätzlich folgende Abkürzungen eingeführt: eckige Klammern ([ ]) umschliessen Symbolfolgen, die null oder einmal auftreten dürfen; geschweifte Klammern ( ) umschliessen Symbolfolgen, die beliebig oft (auch null-mal) vorkommen dürfen. 7 Anschaulich stellt man BNF durch Syntaxdiagramme dar. Java in EBNF findet sich unter: http: //cui.unige.ch/db-research/Enseignement/analyseinfo/JAVA/ Beispiele sind in Abb. 7 angegeben. (Hier werden Terminalsymbole in Anführungszeichen angegeben und Nonterminale erscheinen ohne spitze Klammern.) Die eindeutige und vollständige Beschreibung der Syntax einer Informatik B SS 03 15 S S if E1 then if E2 if S then S1 else S2 E1 if then S else E2 then S1 S2 Abbildung 8: ‘Danglinge-else’ Uneindeutigkeit Programmiersprache ist wichtig für den Programmierer, um syntaktisch korrekte Programme zu schreiben, und Grundlage für den Parser. Ein Parser ist ein Akzeptor für einen Programmtext. Ein syntaktisch korrektes Programm wird ein einen Syntaxbaum überführt. Ein guter Parser gibt informative Fehlermeldungen, falls das Programm nicht der Grammatik der Programmiersprache genügt. Programmiersprachen sollten syntaktisch eindeutig sein. Das heisst, jede Folge von Symbolen entspricht genau einem Parse-Baum. Wenn Uneindeutigkeiten existieren, werden sie durch Konventionen aufgelöst. $% # entspricht den Alternativen: $% $% - Beispiel: “dangling-else ambiguity” Zu welchem if gehört das else? if E1 then if E2 then S1 else S2 (Parse-Bäume siehe Abb. 8) Typische Auflösung: Matche else mit dem am nächsten stehenden noch ungematchten if. Die Kenntnis entsprechender Konventionen sind Voraussetzung für die Vermeidung von Programmierfehlern! 1.5 Semantik und Pragmatik Die formale Beschreibung der Syntax einer Programmiersprache ist relativ leicht nachvollziehbar. Üblicherweise verbinden wir durch die Wahl der Schlüsselworte bereits eine (intuitive und nicht immer zutreffende) Bedeutung mit den Sprachkonstrukten. Aber: Dem Parser ist es völlig egal, ob im Programm “for” oder “bla” steht, solange er die entsprechende Regel in der Grammatik finden kann! Es ist wünschenswert, dass die Semantik einer Sprache genauso eindeutig und präzise beschrieben wird wie die Syntax. Informatik B SS 03 16 Nur für wenige Sprachen ist jedoch eine formale Semantik angegeben (z.B. für die funktionale Sprache ML). Meistens (z.B. bei Java) wird die Bedeutung der Sprachkonstrukte informell natürlichsprachig beschrieben. Für voll durchformalisierte Sprachen ist es möglich, Korrektheitsbeweise wenigstens zum Teil automatisch durchzuführen (Spezifikationssprachen wie ). Verschiedene Möglichkeiten, die Semantik einer Sprache formal anzugeben: (siehe Vorlesung “Theoretische Informatik”, Thema Algebraische Spezifikation) – Operationale Semantik: Regeln, nach denen Ausdrücke ausgewertet werden (Reduktionssemantik). – Denotationale Semantik: Interpretation der Sprachkonstrukte in eine “bekannte” Sprache (z.B. Algebra). – Axiomatische Semantik: Angabe von Gesetzen und Regeln (Hoare Kalkül). Später wird für einen kleinen Ausschnitt von Java (“Featherweight Java”) die formale Semantik dargestellt. Beweis von Typsicherheit Die Pragmatik einer Sprache beschreibt, wie die Konstrukte einer Sprache sinnvoll eingesetzt werden. (Entscheidung, ob eine pre-check-loop (while) oder eine post-check-loop (do-while) verwendet werden soll; Entscheidung zwischen Schleife und rekursiver Lösung.) Um das, was eine Sprache bietet, optimal auszunutzen, ist genaue Kenntnis des realisierten Paradigmas sowie der Implementierung der Sprache wichtig! Informatik B SS 03 17 2 Java und Objekt-Orientierung 2.1 Grundkonzepte der Objekt-Orientierung 2.1.1 Eigenschaften Objekt-Orientierter Sprachen (nach Alan Kay, einem der Smalltalk-Entwickler) Alles ist ein Objekt: Objekte sind “mächtige Daten”. Sie halten Daten und können Operationen auf ihren eigenen Daten ausführen. Der “Datentyp” eines Objekts ist die Klasse mit der es erzeugt wurde. Beispiel: Objekt circle vom Typ Circle kann seinen Flächeninhalt berechnen. Anmerkung: In Java ist im Gegensatz zu Smalltalk nicht alles ein Objekt (primitive Datentypen). Ein Programm ist eine Menge interagierender Objekte: Ein Objekt kann Botschaften an ein anderes Objekt schicken, z.B. eine Aufforderung, eine bestimmte Methode auszuführen. Objekte können zu komplexen Objekten kombiniert werden: Komplexe Objekte können aus einfachen Bausteinen aufgebaut werden, indem Objekte Referenzen auf andere Objekte enthalten. Alle Objekte eines Typs können dieselben Botschaften erhalten: Dies wird vor allem im Hinblick auf Klassenhierarchien interessant. Jedes circle-Objekt kann alle Methoden ausführen, die in der Circle Klasse definiert sind. Ist Circle ein spezieller Shape, so können auch (nicht-überschriebene) Shape-Methoden ausgeführt werden. 2.1.2 Prinzipien der Objekt-Orientierten Programmierung Zwei wesentliche Prinzipien der objekt-orientierten Programmierung sind Kapslung (Encapsulation) und Wiederverwendbarkeit (reuse). Zur Kapslung gehört die Organisation von Daten und Methoden in Klassen sowie das Verstecken von Implementierungsdetails (information hiding). Kapslung ist in mehrfacher Hinsicht guter Programmierstil: – Das Problem wird in kleinere, unabhängige Komponenten gegliedert und ist damit leichter zu überschauen, leichter zu testen und weniger fehleranfällig. – Das Verstecken von Implementationsdetails erlaubt eine bessere Austausch von Code. Beispielsweise ist es notwendig zu wissen, dass die eine Klasse Tree eine Methode insert anbietet, aber nicht, wie insert genau realisiert ist (z.B. wie gewährleistet wird, dass der Baum ausgewogen ist). Informatik B SS 03 18 – Schränkt man zudem die Zugriffsrechte für Felder ein (z.B. private Felder mit public Zugriffsmethoden) kann man unerwünscht Seiteneffekte vermeiden, wie etwa, dass ein zu einer anderen Klasse gehöriges Objekt ein Feld unkontrolliert ändert und möglicherweise Inkonsistenzen erzeugt. – Neben Klassen dienen Pakete zur Kapslung von Information (Zugriffsrechte). Pakete dienen insbesondere der Strukturierung. Wiederverwendbarkeit ist möglich, weil Klassen unabhängig von konkreten Daten definierbar sind. Aussderdem können bereits definierte Klassen durch Vererbung für spezielle Bedürfnisse angepasst werden. Generell sollte Code so geschrieben und dokumentiert werden, dass die Funktionalität der Klasse transparent ist und so Wiederverwendbarkeit (in eigenen Projekten oder Nutzung durch andere) erleichtert wird. Java bietet eine Menge vordefinierter Klassen an (Java APIs). Es ist guter Stil, diese (gut getesteten, bewährten) Klassen zu verwenden. Es ist wichtig, sich einen Überblick über die Java APIs zu verschaffen: http://www-lehre.inf.uos.de/manuals/jdk1.4/docs/ 2.2 Die Sprache Java 2.2.1 Entstehungsgeschichte Start 1990 bei Sun, Gruppe von James Gosling Angelehnt an C++, Elemente aus Smalltalk (Bytecode, Garbage Collection) – via Objective C objekt-orientiert. Bytecode Compilierung und Garbage Collection sind Konzepte, die ursprünglich im Rahmen von Lisp entwickelt wurden. (Guy Steele, der aus dem Bereich Common Lisp bekannt ist, ist bei Sun mitverantwortlich für die Entwicklung der Java Sprachspezifikation!) Ziel: Entwicklung einer Hochsprache für hybride Systeme im Consumer-Electronic Bereich: Steuerung von Waschmaschinen, Telefonanlagen, ... ( ursprünglicher Name “Oak”) Boom des WWW 1993 Einsatz für Internet-Anwendungen Java-Applets: kleine Programme, die in HTML-Seiten eingebunden werden können (Sun Demo Web-Browser HotJava in den 90-ern) Durchbruch 1995: Netscape Navigator 2.0 mit integrierter Java Virtual Machine Java-Versionen, siehe Tabelle 1. Anmerkung: Java 1 (Java 1.0 und 1.1), Java 2 (ab Java 1.2) bezeichnen Versionen mit vielen neuen Features; Java 1.1, 1.2, 1.3, 1.4 bezeichnet Versionen mit einigen neuen Features; von Java 1.2 zu 1.3 vor allem Geschwindigkeitsverbesserungen; Java 1.1.3 und andere dreistellige Nummern bezeichnen in kleineren Details verschiedene Implementierungen einer Version (“minor releases”). Informatik B SS 03 Version 1.0 1.1 1.2 1.3 1.4 # Klassen 212 504 1520 1840 2977 19 # Pakete 8 23 59 76 135 Tabelle 1: Java Versionen Ersch. Anmerkungen Jan. 1996 Feb. 1997 Dez. 1998 auch “Java 2, Release 1.2” Mai 2000 auch ”Java 2, Release 1.3, Standard Edition” Feb. 2002 (1.4.0) 2.2.2 Java Buzzwords Portabel: JVM (virtuelle Maschine ist bewährtes Prinzip); alle Datentypen sind unabhängig von der Implementierung festgelegt (Standards) Objekt-Orientiert: Kapslung in Klassen, Vererbung Multithreaded: Zerlegung in einzelne “Prozesse” (Threads), die unabhängig voneinander ablaufen können Verteilt: Remote Method Invocation (RMI), Netzwerk-Programmierung basierend auf Client-Server Architekturen Robust: kein explizites Arbeiten mit Zeigern, Speicherverwaltung wird von Java gehandhabt (Garbage Collection) Sicher: Plattform erlaubt Laden von unbekanntem Code, der in einer sicheren Umgebung abläuft (kein Lesen und Schreiben von Festplatte, ...) Dynamisch, Erweiterbar: Organisation des Programmcodes in Klassen, in verschiedenen Dateien gespeichert, load when needed Internationalisierung: 16-bit Unicode (statt ASCII) 2.2.3 Sprache, Virtuelle Maschine, Plattform Java, die Programmiersprache: – Sprache, in der Java Programme geschrieben werden. – Compilation in Byte-Code mit javac Dateinamen (inklusive Suffix, .java) – portable Maschinensprache – Ausgabe: *.class Datei(en) – Das Kommando javac erlaubt verschiedene Optionen, wie: -classpath path: Liste von Verzeichnissen, in denen nach weiteren im Quellcode benutzten Klassen gesucht wird. (default: aktuelles Verzeichnis) -d directory: Verzeichnis, in dem die erzeugten class-Dateien abgelegt werden . (default: Verzeichnis der *.java Dateien) Informatik B SS 03 20 -verbose: Ausgaben des Compilers – In einer Datei *.java können mehrere top-level Klassen definiert werden. Maximal eine davon darf public sein. Für jede der Klassen wird bei Übersetzung eine eigene *.class Datei erzeugt. Java Virtual Machine (JVM): – Kann in Hardware oder Software realisiert sein (üblicherw. in Software) – Interpretation und Ausführung des Byte-Codes; JVM für Solaris, Microsoft Windows, Linux, ... – Interpretersprachen sind meist langsam, übliche Technik just-in-time Compilierung, Java: Byte-Code wird in die Maschinensprache der gegebenen Plattform übersetzt (gute Ausführungsgeschwindigkeit für Code, der mehrfach ausgeführt wird) – Ausführung des Bytecodes Klassenname.class mit java Klassenname . – Die entsprechende Klasse muss eine Methode main() mit folgender Signatur enthalten: public static void main(String[] args). Diese Methode ist der Einstiegspunkt ins Programm: hier beginnt der Interpreter die Ausführung. Das Programm läuft so lange, bis die main Methode (und evtl. erzeugte Threads) verlassen werden. – Das Kommando java erlaubt verschiedene Optionen, wie: -classpath path: Liste von Verzeichnissen und JAR Dateien, in denen gesucht wird, wenn eine Klasse geladen wird. (Kurzform: -cp) -Dpropertyname=value: Setzt eine Property mit Namen propertyname auf den Wert value. Im Java-Programm können Properties dann abgefragt werden. Hinter dem Klassennamen können Daten angegeben werden, die an String[] args weitergegeben werden. Java Plattform: – Vordefinierte Menge von Java Klassen, die auf jeder Java Installation vorhanden sind und von allen Java Programmen genutzt werden können – Klassen sind in Paketen organisiert (Input/Output, Graphik, Netzwerkfunktionen, ...) – Auch Java Runtime Environment oder Java APIs (Application Programming Interface) genannt. Informatik B SS 03 21 2.3 Das 8-Damen Problem Imperativ und Objekt-Orientiert 2.3.1 Problemstellung Problem: Plaziere 8 Damen auf einem (8 8) Schachbrett so, dass keine Dame eine andere schlagen kann. Eine Dame kann eine Figur schlagen, die in derselben Reihe oder derselben Spalte oder derselben Diagonale ist. Lösung(en) für das 4-Damen Problem: 4 4 3 3 2 2 1 1 1 Allgemein: 2 . Keine Lösung für 3 4 1 Problem ( Damen auf 2 3 4 Brett) Standard-Beispiel für generate and test Algorithmen (backtracking) Aufwand: – naiv: 8 aus 64 (Binomialkoeffizient über ) 5 5-5 : 4.426.165.368 – in jeder Spalte nur eine Dame: für : 16.777.216 – in keine Spalte die gleiche Position: für : 40320 für Problem mit exponentieller Komplexität! Es kann keinen vollständigen Algorithmus geben, der das Problem effizient löst. Verwendung von heuristischen Suchverfahren bzw. Constraint-Erfüllungstechniken (Künstliche Intelligenz). Komplexität von Problemen wird in der Vorlesung Theoretische Informatik behandelt Aktuelle Literatur zum -Damen Problem: http://www.liacs.nl/˜kosters/nqueens.html Informatik B SS 03 22 aufw. 4 3 testRow testColumn colDif 1 1 1 1 ... 4 4 4 4 1 2 3 4 ... 1 2 3 4 -2 -1 0 1 ... -2 -1 0 1 2 abw. 1 1 2 3 4 upRowDif testRow - row -2 -2 -2 -2 ... 1 1 1 1 downRowDif row - testRow 2 2 2 2 ... -1 -1 -1 -1 Abbildung 9: Bedrohte Diagonalen Sequenz der Anzahl von Lösungen: 1,0,0,2,10,4,40,92,352,724,2680,14200,73712,365596,2279184, 14772512,95815104,666090624,4968057848,39029188884, 314666222712,2691008701644,24233937684440 eine kompakte Formel wurde bisher nicht gefunden (siehe http://mathworld.wolfram.com/QueensProblem.html) 2.3.2 Identifikation von Schlagstellungen Wenn in jede Spalte genau eine Dame gesetzt wird, dann kann eine Dame mit Position (row, column) folgende Stellungen bedrohen: – die Zeile row – die Aufwärtsdiagonale (testColumn – die Abwärtsdiagonale (testColumn column) column) G (testRow - row) (row testRow) G Erläuterung: Diagonalen haben eine Steigung von (aufwärts) bzw. (abwärts). Das heisst, Spaltendifferenz (Waagerechte) und Zeilendifferenz (Senkrechte) sind identisch. Beispiel für row 3 und column 3 in Abb. 9 Bedrohung auf Diagonalen existiert also, wenn: row (testColumn column) testrow oder row (testColumn column) testrow Informatik B SS 03 23 Da beidesmal die Spaltendifferenz benötigt wird, kann diese auch vorab berechnet werden. siehe Methode predecCanAttack() Die von einer Dame bedrohten Positionen können global in entsprechende boolesche Arrays eingetragen werden. Alternativ zum Eintragen und Durchmustern der Arrays können die bedrohten Stellungen auch jeweils für jede Dame geprüft werden. 2.3.3 Imperative Lösung Im folgenden geben wir Programme zum Finden einer gültigen Lösung an. (Alternativ: Finden aller gültigen Lösungen ) Die Anzahl von Queens (MAX) wird als Property über die gesetzt. Default ist 8. Option von java Globale Kontrolle: In der main() Methode wird ein Array queens[] spaltenweise mit einer legalen Zeilenposition für die aktuelle Dame belegt. Existiert für eine Dame keine legale Position wird die bisherige Lösung zurückgenommen (die Vorgängerdame, evtl. deren Vorgänger, etc.) backtracking public class QueensIM { final static int MAX = Integer.getInteger("MAX", 8).intValue(); // Current row for queens at columns 0..MAX-1 final static int[] queens = new int[MAX]; // initialized with 0s // CAUTION: The "real" column is (array index) + 1! // predecCanAttack() depends on the real chessboard positions // therefore we define an array shift and an advance function static int queens(int i) { return queens[i-1]; } static void advance(int i) { queens[i-1]++; } // Test whether a queen (with position row, column) // or any of its predecessors can attack another position static boolean predecCanAttack(int row, int column, int testRow, int testColumn) { // Case 1: current Queen can attack int columnDifference = testColumn - column; if ((row == testRow) || // same row (row + columnDifference == testRow) || // same up-diagonal (row - columnDifference == testRow)) // same down-diagonal return true; Informatik B SS 03 24 // Case 2: can neighbor Queen attack? if (column > 1) // there are queens to the left return predecCanAttack(queens(column-1), column-1, testRow, testColumn); // Case 3: Position cannot attack, is ok return false; } // Prints the position of all queens via loop through the array static void printSolution() { for (int i = 1; i <= MAX; i++) { System.out.println(i + " : " + queens(i)); } } // Global control of positioning of queens. // backtrack if current queen cannnot be positioned without conflict public static void main(String[] args) { int i = 1; while ((i <= MAX) && (i > 0)) { // there are queens to the right // and backtracking does not go beyond leftmost queen advance(i); // advance current queen one row System.out.println("Queen in column " + i + " set on row " + queens(i)); if (i > 1) while ( queens(i) <= MAX && predecCanAttack(queens(i-1),i-1,queens(i),i) ) { advance(i); System.out.println("Advance to row " + queens(i)); } if (queens(i) > MAX) { queens[i-1] = 0; // reset queen, CAUTION: using the array index i--; // backtrack } else i++; } // end while (i <= MAX) && (i > 0) printSolution(); } } 2.3.4 Objekt-Orientierte Lösung Statt einer Klasse mit der Kontrolle in der main() Methode wird nun eine Klasse Queen definiert: Jede Dame kennt ihre eigene Position und kann sich selbst verschieben. Dazu existiert eine Dame, die ihre direkte Nachbarin kennt, als Unterklasse von Queen. Sie kann prüfen, ob sie mit dieser in Konflikt steht, sowie diese “anschubsen”. Informatik B SS 03 25 public class QueensOO { // Number of Queens set as property public final static int MAX = Integer.getInteger("MAX", 8).intValue(); // Loops from first to last Queen // and tries to generate a solution via backtracking. public static void main(String[] args) { // The current Queen. Queen lastQueen = new Queen(1); for (int i = 2; i <= MAX; i++) { lastQueen = new NeighborQueen(i, lastQueen); lastQueen.findSolution(); } lastQueen.printSolution(); } } class Queen { // data fields // row position of a Queen: // can be changed to obtain a conflict free solution protected int row; // column position of a Queen: is unchangable protected final int column; // Initializes a new Queen object. public Queen (int c) { row = 1; // always start in first row column = c; } // A single Queen is always at a conflict free position. public boolean findSolution() { return true; } // Advance one row if possible protected boolean advance() { if (++row <= QueensOO.MAX) return true; row = 1; // start again (backtrack) for the current Queen return false; } // Test whether this Queen can attack another queen protected boolean predecCanAttack(Queen queen) { // current Queen can attack Informatik B SS 03 26 int columnDifference = queen.column - column; return (row == queen.row) || (row + columnDifference == queen.row) || (row - columnDifference == queen.row); // same row // same up-diagonal // same down-diagonal } // Prints the position of the current Queen public void printSolution() { System.out.println(column + " : " + row); } } class NeighborQueen extends Queen { // data fields // immediate left neighbor of a Queen protected final Queen neighbor; // Initializes a new NeighborQueen object public NeighborQueen (int c, Queen n) { super(c); neighbor = n; } // Tries to advance the Queen to a conflict free position. public boolean findSolution() { while (neighbor.predecCanAttack(this)) if (! advance()) return false; return true; } // Advance one row if possible (realizes the backtracking) protected boolean advance() { return super.advance() || // test if neighbor advance and findSolution is ok: neighbor.advance() && neighbor.findSolution(); } // Test whether this Queen // or any of its predecessors can attack another Queen protected boolean predecCanAttack(Queen queen) { // can current Queen or neighbor Queen attack? return super.predecCanAttack(queen) || neighbor.predecCanAttack(queen); } // Prints the position of the current Queen and all predecessors public void printSolution() { neighbor.printSolution(); super.printSolution(); } } Informatik B SS 03 27 1 QueensOO Queen #row: int #column: int +findSolution(): boolean #advance(): boolean #predecCanAttack(queen:Queen): boolean +printSolution() creates +MAX: int +main() creates left neighbor NeighborQueen #neighbor: Queen Abbildung 10: Klassenstruktur von QueensOO Informatik B SS 03 28 3 Klassen und ihre Komponenten 3.1 Klassen in Java Java als objekt-orientierte Sprache Klassen sind die fundamentale Struktur. Jedes Java Programm ist als Klasse definiert; alle Java Programme benutzen Objekte. Eine Klasse ist die kleinste Einheit an Java-Code, die für sich allein stehen kann und vom Java-Compiler und -Interpreter erkannt wird Jede Klasse definiert einen neuen Datentyp! primitiver Datentyp – Wert (int 42) Klasse – Objekt (Circle ‘Kreis mit Radius 2’) Klasse: Sammlung von Attributen (typisierte Platzhalter für Daten) und von Programmcode (gespeichert in benannten Methoden, die auf diesen Daten arbeiten) Klassendefinition: siehe Abb. 7 für die vollständige Spezifikation. Im einfachsten Fall: class Name Members Konvention: Erster Buchstabe des Klassennamens wird gross geschrieben. Die erste Zeile der Klassendefinition repräsentiert die “Klassen-Signatur” (vgl. erste Zeile einer Methodendefinition) Der Java-Interpreter lädt Klassen dynamisch (dynamic loading): Wenn das erste Mal ein Objekt dieser Klasse erzeugt bzw. eine statische Komponente benötigt wird. Wie der Java-Interpreter die Klassen findet, wird im Abschnitt “Packages und Java Namespace” dargestellt. 3.2 Members: Felder, Methoden, Klassen Member (Komponente): Felder, Methoden und (seit Java 1.1) weitere (innere) Klassen Eine Klasse, die nur Felder und keine Methoden definiert, ist lediglich eine Sammlung von Daten, ähnlich einem Record (Modula). Zentral für objekt-orientierte Programmierung ist, dass Nachrichten (Methoden) an Empfängerobjekte (Receiver) gesendet werden, die diese dann ausführen (a.f(): a ist ein Objekt, das die Nachricht “erledige f()” empfängt). zwei Typen von Members: – class members: als static deklariert, assoziiert mit Klasse selbst – instance members: assoziiert mit individuellen Instanzen (Objekten!) der Klasse Informatik B SS 03 29 3.3 Beispielcode ‘Circle’ public class Circle { // A class field public static final double PI = 3.14159; // A useful constant // A class method: just compute a value based on the arguments public static double radiansToDegrees(double rads) { return rads * 180/PI; } // An instance field public double r; // The radius of the circle // Two instance methods: they operate on the instance fields // of an object public double area() { // Compute the area of the circle return PI * r * r; } public double circumference() { // Compute the circumference return 2 * PI * r; // of the circle } } 3.4 Klassen- und Instanz-Komponenten 3.4.1 Klassen-Felder public static final double PI = 3.14159; assoziiert mit Klasse selbst durch static modifier: Klassen-Feld (static field) Es existiert nur eine einzige Kopie eines statischen Feldes! Feld vom Typ double mit Namen PI und zugewiesenem Wert 3.14159 final modifier: Wert des Feldes ändert sich nach erster Zuweisung nicht mehr (Konstante) public modifier: “jeder” kann das Feld benutzen (visibility modifier) lokale Variable/Konstante: innerhalb einer Methode oder eines Blocks Felder: Komponenten einer Klasse! ähnlich einer globalen Variable in einem Programm Innerhalb der Klasse Circle kann PI mit seinem einfachen Namen referenziert werden; ausserhalb: Circle.PI (eindeutige Spezifikation: “das PI, das in Klasse Circle definiert ist”) Informatik B SS 03 30 3.4.2 Klassen-Methoden public static double radiansToDegrees(double rads) 180/PI; return rads * assoziiert mit Klasse selbst durch static modifier: Klassen-Methode (static method) Aufruf (invocation) von ausserhalb der Klasse: Circle.radiansToDegrees(2.0) oft guter Stil Klassennamen auch innerhalb der Klasse mitanzugeben, um klar zu machen, dass eine Klassen-Methode benutzt wird Klassen-Methoden sind “globale Methoden” (in anderen Programmiersprachen sind alle Prozeduren/Funktionen global), “imperativer Programmierstil” kann in Java realisiert werden, wenn nur Klassen-Methoden benutzt werden. radiansToDegree() ist eine “utility”-Methode, die in Circle definiert ist, weil sie beim Arbeiten mit Kreisen nützlich sein kann. benutzt das Klassen-Feld PI Klassen-Methoden können alle anderen Klassen-Komponenten der eigenen Klasse (oder auch anderer Klassen) benutzen. Klassen-Methoden können nicht direkt (mit this) auf Instanz-Felder oder -methoden zugreifen! (weil die Klassen-Methoden nicht mit einer Instanz assoziiert sind) 3.4.3 Instanz-Felder public double r; Jedes Feld, das nicht static deklariert ist, ist ein Instanz-Feld. Instanz-Felder sind mit den Objekten der Klasse assoziiert. Jedes Objekt der Klasse Circle hat seine eigene Kopie des Felds r. Jeder Kreis hat seinen eigenen Radius. Innerhalb von Klassen werden Instanz-Felder durch ihren Namen referenziert. In Code ausserhalb wird der Name über eine Referenz zu dem Objekt, das das Feld enthält, angegeben. Instanz-Felder sind der Kern des objekt-orientierten Programmierens: Mit Instanz-Feldern wird ein Objekt (über seine Eigenschaften) definiert. Die Werte dieser Felder machen Objekte zu unterschiedlichen Identitäten. Circle c = new Circle(); // // c.r = 2.0; // Circle d = new Circle(); // d.r = c.r * 2; // Create a new Circle object store it in variable c Assign a value to its instance field r Create a different Circle object Make this one twice as big Informatik B SS 03 31 Circle PI r + radiansToDegrees() + area() + circumference() circle1: Circle circle2: Circle r=1 r=42 Abbildung 11: Jedes Objekt hat seine eigenen Instanzkomponenten 3.4.4 Erzeugung und Initialisierung von Objekten Erzeugung von Objekten mit new Konstruktor Circle c = new Circle(); Ein Objekt mit Namen name vom Typ des Konstruktors (Klasse oder Unterklasse der Klasse) wird erzeugt und durch Aufruf eines Konstruktors für diese Klasse initialisiert. Konstruktoren können selbst definiert werden. (Kapitel ‘Konstruktoren und Vererbung’) Wenn eine Klasse keinen eigenen Konstruktor definiert, dann bekommt sie automatisch einen Default-Konstruktor ohne Parameter. 3.4.5 Instanz-Methoden public double circumference() return 2 * PI * r; Jede Methode, die nicht static deklariert ist, ist eine Instanz-Methode. Instanz-Methoden arbeiten auf einer Instanz einer Klasse (auf einem Objekt) und nicht auf der Klasse selbst. Instanz-Methoden sind der zweite wesentliche Kern der Objektorientierung. Um eine Instanz-Methode ausserhalb der Klasse, in der sie definiert ist, zu verwenden, muss zunächst ein entsprechendes Objekt erzeugt werden. Objekte – nicht Funktionen – stehen im Mittelpunkt! (a = area(c) vs. a = c.area()) An die Methode area() muss kein Parameter übergeben werden, weil alle Informationen über das Objekt implizit in der Instanz c vorhanden sind. Informatik B SS 03 32 Instanz-Methoden können auf Klassen- und Instanz-Members zugreifen. In circumference() ist r mit dem Wert aus der gerade betrachteten Instanz belegt. Circle c = new Circle(); // // c.r = 2.0; // double a = c.area(); // Create a new Circle object; store it in variable c Assign a value to its instance field r Invoke an instance method of the object 3.4.6 Funktionsweise von Instanz-Methoden: ‘this’ a = c.area(); area() ist scheinbar parameterlos, aber die Methode hat einen impliziten Parameter this. this hat als Wert die Referenz auf das Objekt, über das die Methode aufgerufen wurde. this muss häufig nicht explizit angegeben werden: Wenn eine Methode auf Komponenten zugreift, wird – wenn nichts anderes gesagt wird – angenommen, dass die Komponenten des aktuellen Objekts gemeint sind. this kann/sollte explizit angegeben werden, wenn man klar machen will, dass die Methode auf ihre eigenen Komponenten zugreift. public double area(){ return Circle.PI * this.r * this.r; } this wird explizit benötigt, wenn Parameter oder lokale Variablen einer Methode denselben Namen haben wie Felder. Der Name wird zunächst auf lokale Grössen bezogen! public void setRadius(double r) { this.r = r; //Assign the argument r to the field this.r } r = r ist sinnlos: weist Parameter r seinen eigenen Wert zu. Klassen-Methoden können das this Schlüsselwort nicht benutzen! (weil sie nicht mit einer Instanz, sondern mit der Klasse assoziiert sind) 3.4.7 Instanz- oder Klassen-Methode? Instanz-Methoden sind zentral für objekt-orientierten Programmierstil. Dennoch kann es sinnvoll sein, Klassen-Methoden zu definieren. Informatik B SS 03 33 Design-Entscheidung: Wenn häufig die Fläche eines Kreises berechnet werden soll, aber es ansonsten nicht notwendig ist, dafür extra ein Objekt der Klasse Circle zu erzeugen, sollte area() als Klassen-Methode definiert werden. Die Methode kann zweimal definiert sein (hier: einmal als Klassen-, einmal als Instanz-Methode). Unterscheidung: verschiedene Signaturen! public static double area(double r){ return PI * r * r;} Design-Entscheidung: Wie soll eine Methode realisiert werden, die den grösseren von zwei Kreisen zurückliefert? // Compare the implicit ‘this’ circle to the ‘that’ circle passed // explicitly as an argument and return the bigger one. public Circle bigger(Circle that) { if(this.r > that.r) return this; else return that; } ... Circle biggest = c.bigger(d); // Instance method: // alternatively d.bigger(c) // Compare circle a to circle b and return the one with the larger radius public static Circle bigger(Circle a, Circle b) { if(a.r > b.r) return a; else return b; } ... Circle biggest = Circle.bigger(x,y); // Static method 3.5 Referenztypen Klassen (und Arrays) sind Referenztypen. Objekte haben einen bestimmten Typ: – Circle c = new Circle(); – int[] a = 1,2,3,4,5 ; – String s = "Hello"; c ist ein Objekt vom Typ Circle a ist ein Array-Objekt s ist ein String-Objekt Array und String haben speziellen Status: – In der Object-Hierarchie findet sich keine Unterklasse Array: Es gibt potentiell unendlich viele Arrays (Dimensionalität, Typ der Elemente). Wie bei der Definition und Erzeugung anderer Objekte steht links der Typ des Arrays (Dimensionalität und Typ der Elemente), aber nicht die Länge des Arrays! (siehe Vorlesung Informatik A) Informatik B SS 03 34 int x = 42; int y = x; Circle c = new Circle(); c.r = 2.0; Circle d = c; x 1124 42 c 2411 4711 y 1125 42 d 2412 4711 4711 Object r=2.0 Abbildung 12: Kopieren von Werten/Referenzen – Für String kann ein Objekt direkt initialisiert werden: Text vom Typ String (Zeichen in doppelten Anführungszeichen) kann direkt an eine Variable vom Typ String zugewiesen werden. Primitive Datentypen: feste, bei Deklaration bekannte Grösse (z. B. int bekommt 32 Bit) Klassen und Arrays: sind zusammengesetzte Typen (composite types), für die keine Standardgrösse angegeben werden kann und die häufig mehr Speicherplatz benötigen Manipulation by reference Referenz: fester Wert (Speicheradresse), der auf das Objekt verweist Zuweisung eines Objekts an eine Variable: Variable hält Referenz auf dieses Objekt; Übergabe eines Objekts als Parameter: Methode erhält die Referenz, über die das Objekt manipuliert werden kann. null Referenz: Referenz auf “Nichts”, Abwesenheit einer Referenz; Wert null kann an jede Variable für einen Referenztyp zugewiesen werden. Java erlaubt keine explizite Manipulation von Referenzen. Wichtiger Unterschied zwischen primitiven Datentypen und Referenztypen: Kopieren von Werten und Prüfung von Gleichheit. 3.5.1 Kopieren von Objekten (und Arrays) Für die int-Variablen x und y existieren zwei Kopien des 32-bit Integers 42. Variable d enthält eine Kopie der Referenz, die in Variable c steht. Man spricht hier auch von aliasing. Achtung: Wenn zwei Referenzen a und b auf dasselbe Objekt zeigen, kann ein Methodenaufruf (z.B a.f()) an das eine Objekt den Wert eines Feldes ändern (“calling a method for its side effects”). Das Feld ist dann beim Objekt geändert. Sowohl a.feld als auch b.feld liefern den aktuellen Wert! Eine Kopie des Circle Objekts in der Java VM, aber zwei Kopien der Referenz auf dieses Objekt! Informatik B SS 03 System.out.println(c.r); d.r = 4.0; System.out.println(c.r); 35 // Print out radius of c: 2.0 // Change radius of d // Print out radius of c again: 4.0 Kopieren des Objektes selbst: clone() Objekte müssen zu einer Klasse gehören, die als Cloneable deklariert ist (Implementieren des Cloneable-Interfaces und der clone()-Methode) Arrays sind immer Cloneable clone() erzeugt eine flache (“shallow”) Kopie: alle primitiven Werte und Referenzen werden kopiert. Das heisst, Referenzen werden nicht ge-cloned! rekursives Kopieren muss explizit implementiert werden. Casting ist notwendig (clone() liefert Object) int[] data = {1,2,3,4,5}; // An array int[] copy = (int[]) data.clone(); // A copy of the array Flache Kopie: int[][] data = {{1,2,3},{4,5}}; // Array of 2 refs int[][] copy = (int[][]) data.clone(); // Copy copy[1] = new int[]{7,8,9}; // this does not change data[1] copy[0][0] = 99; // this changes data[0][0] too! Tiefe Kopie: int[][] data = {{1,2,3},{4,5}}; // Array of 2 refs int[][] copy = new int[data.length][]; // new array to hold copied arrays for (int i = 0; i < data.length; i++) copy[i] = (int[]) data[i].clone(); 3.5.2 Gleichheit von Objekten (und Arrays) Bei primitiven Werten prüft der == Operator, ob sie denselben Wert haben (bei ganzzahligen Werten: gleiche Bits). Bei Referenztypen prüft ==, ob zwei Referenzen auf dasselbe Objekt verweisen, aber nicht, ob zwei Objekte denselben Inhalt haben! Prüfung der Gleichheit von Inhalten von Objekten (nicht bei Arrays): equals() Methode ist in Object definiert (default ist ==) Methode in eigener Klasse entsprechend überschreiben (z.B. in String gemacht) Anmerkung: zur Unterscheidung “equals” für Gleichheit von Referenzen und “equivalent” für Gleichheit von Inhalten verschiedener Objekte. Die Benennung der Methode equals ist etwas ungünstig. bei Arrays: java.util.Arrays.equals() (ab Java 1.2) Informatik B SS 03 36 data 1 2 3 4 5 int[ ] data = {1,2,3,4,5} 1 data 4 2 3 5 int[ ][ ] data = {{1,2,3}, {4,5}} data 1 2 4 5 3 copy int[][] copy = (int[][]) data.clone(); data copy 0 1 1 2 4 5 7 8 3 copy[0][]0] 0 1 9 copy[1] = new int[] {7,8,9}; Abbildung 13: Arrays sind Referenztypen Informatik B SS 03 37 // A class method for Circle replacing the instance method equals() in Object public static boolean equals (Circle c, Circle d) { return c.r == d.r; } Circle c = Circle(); c.r = 2.0; Circle d = Circle(); d.r = 2.0; if (c == d) System.out.println("equal"); // but c and d are not equal if (Circle.equals(c,d)) System.out.println("equivalent"); // c and d are equivalent 3.6 Wrapper-Klassen Problem: Array ist ein Container für primitive Typen wie beliebige Objekte. Manchmal möchte man primitive Typen vielleicht in anderen Datenstrukturen speichern (Vector aus den Java Collection Classes in java.util). Diese Klassen sind für Object definiert, aber nicht für primitive Typen. Lösung: Einpacken von primitiven Typen in korrespondierende Klassen (Wrapper) – Boolean, Byte, Short, Integer, Long, Character, Float, Double Dort werden Konstanten und nützliche (statische) Methoden definiert. // java.lang.Integer public Integer(String s) throws NumberFormatException; public Integer(int value); public static int parseInt(String s) throws NumberFormatException; public int compareTo(Integer anotherInteger); String s = "-42"; int i = Integer.parseInt(s); // class method Integer j = new Integer(-50); // create new Integer Object int t = j.compareTo(new Integer(i)); // instance method // >0 if j>i, <0 if j<i, 0 if j=i 3.7 Dokumentation mit ‘javadoc’ Wenn Programm und Kommentar sich widersprechen, sind vermutlich beide falsch. In Java gibt es folgende Kommentare: /* * * * Dies ist ein Kommentar der über mehrere Zeilen gehen kann. H&auml;ufig markiert man die zum Kommentar gehoerigen Zeilen Informatik B SS 03 38 * einleitend ebenfalls mit einem Stern */ int i = 0; // hier ist ein Zeilenkommentar Zwischen /* und */ eingeschlossener Text wird vom Compiler ignoriert. Der Doppel-Slash markiert alles dahinter bis zum Zeilenende als Kommentar. Eine dritte Möglichkeit, Kommentare zu schreiben, hat die Form /** von javadoc extrahierbarer Kommentar */ . Programm javadoc erzeugt API Dokumentation im HTML-Format. javadoc [options] packages | sourcefiles | @lists (Angegeben werden müssen entweder Pakete oder Quelldateien oder Namen von Dateien mit Paket- oder Dateinamen.) Optionen, z.B: -author Information hinter @author wird in die Dokumentation eingetragen -private alle Klassen und Komponenten werden in der Dokumentation berücksichtigt. (meist nicht sinnvoll, information hiding) -classpath Pfad für Klassen- und Sourcefiles -header Text, der oben in jeder doc-Datei erscheint Mit javadoc *.java wird eine Dokumentation für alle Klassen im aktuellen Verzeichnis erstellt. javadoc benutzt den Java-Compiler und integriert alle Dokumentationskommentare. Dokumentationskommentare können HTML-Tags enthalten, z.B. <tt>Classname</tt>. Allerdings sollten keine <A> Tags verwendet werden. (besser den speziellen {@link} Tag). Weitere Schlüsselwörter: @author @version @see empfehlenswert für Methoden: @param und @return. Unter anderem wird eine Datei index.html angelegt, in der eine Übersicht über alle Klassen gegeben wird. siehe zum Beispiel http://www.vorlesungen.uos.de/informatik/ b02/code/shapes/doc/index.html. /** * * * * * * * Simple example class for demonstrating class (static) components and instance components. see Java in a Nutshell, 3rd Edition, chap. 3 @author Ute Schmid @version Vorlesung InfoB SS 02 Informatik B SS 03 39 */ public class Circle { /** A class field for the constant PI */ public static final double PI = 3.14159; /** A class method: compute degrees from radians * @param rads the radians in double * @return rads * 180/PI */ public static double radiansToDegrees(double rads) { return rads * 180/PI; } /** A class method which computes the area of a circle with * given radius. (also exists as instance method) * @param nr radius of a circle * @return PI * nr * nr */ public static double area(double nr) { return PI * nr * nr; } /** An instance field representing the radius of the circle */ public double r; /** An instance method which returns the area of the circle object * @return PI * r * r */ public double area() { // Compute the area of the circle return PI * r * r; // also Circle.Pi, this.r } /** An instance method which returns the circumference of the circle object * @return 2 * PI * r */ public double circumference() { return 2 * PI * r; } /** An instance method which returns the bigger of the current and * another Circle * @param that a Circle object * @return <code>this</code> if the current circle has a larger radius, * <code>that</code> otherwise */ public Circle bigger(Circle that) { if (this.r > that.r) return this; else return that; Informatik B SS 03 40 } } 3.8 Packages und Namespace Paket: Organisation von Klassen; Konzept, um eindeutige Klassennnamen zu garantieren; Achtung: Pakete sind nicht mit Modulen zu verwechseln (Diskussion im Kapitel ‘ADTs’) Namespace: Menge von einzigartigen Namen. Die Organisation von Klassen in Pakete wird vor allem beim Erstellen grösserer Programme interessant (Strukturierung und Kapslung); Thema Software-Engineering (Vorlesung Informatik C) Die von Java zur Verfügung gestellten Klassen sind in Paketen organisiert. Um diese Klassen in eigenen Definitionen zu nutzen, wird ein grundlegendes Verständnis von Paketen und Namensräumen benötigt. Sichtbarkeitsmodifikatoren sind mit dem Paket-Konzept von Java verknüpft. Dieses Thema wird im Kapitel ‘Konstruktoren und Vererbung’ besprochen. 3.8.1 Java API Das API (Application Programming Interface) wird als Teil des JRE (Java Runtime Environment) zur Verfügung gestellt und enthält alle Pakete. z.B. /usr/lib/jdk1.3/jre/lib/rt.jar *.jar ist eine spezielle Art der Archivierung (vgl. *.zip und *.tar). Der Inhalt einer name.jar Datei kann mit jar tvf name.jar angezeigt werden. Auf alle (zugreifbaren) Komponenten von Klassen kann mit dem voll-qualifizierten Klassennamen zugegriffen werden. Beispiel System.out.println("Hello World!"); Durch eine import Anweisung kann mit dem einfachen Klassennamen zugegriffen werden. z.B. import java.io.*;: alle Klassen des I/O-Pakets, import java.io.InputStream;: Klasse InputStream des I/O-Pakets. Warnung: undifferenzierter Import mit .* birgt die Gefahr von ungewollten Kollisionen! java.lang (core API) wird automatisch importiert. System.out.println("Hello World!"); System ist eine Klasse (genauer java.lang.System). out ist ein Klassen-Feld von System (vom Typ java.io.Printstream). System.out verweist auf ein Objekt. Informatik B SS 03 0 1428 11661 155 7193 325 291 1559 113 293 4064 1401 4463 880 740 4398 Wed Wed Wed Wed Wed Wed Wed Wed ... Wed Wed Wed Wed Wed Wed Wed Wed ... 41 Oct Oct Oct Oct Oct Oct Oct Oct 25 25 25 25 25 25 25 25 05:30:36 05:30:34 05:30:34 05:30:34 05:30:34 05:30:34 05:30:34 05:30:34 CEST CEST CEST CEST CEST CEST CEST CEST 2000 2000 2000 2000 2000 2000 2000 2000 java/lang/ java/lang/Object.class java/lang/String.class java/lang/Comparable.class java/lang/Class.class java/lang/CloneNotSupportedException.class java/lang/Exception.class java/lang/Throwable.class Oct Oct Oct Oct Oct Oct Oct Oct 25 25 25 25 25 25 25 25 05:30:36 05:30:36 05:30:36 05:30:36 05:30:36 05:30:36 05:30:36 05:30:36 CEST CEST CEST CEST CEST CEST CEST CEST 2000 2000 2000 2000 2000 2000 2000 2000 java/io/Serializable.class java/io/IOException.class java/io/ObjectStreamField.class java/io/InputStream.class java/io/PrintStream.class java/io/FilterOutputStream.class java/io/OutputStream.class java/io/PrintWriter.class Abbildung 14: Ausschnitt aus ’rt.jar’ Das Objekt hat eine Instanz-Methode println(). Man kann es nicht oft genug sagen: – Eines der wichtigen Konzepte der Objekt-Orientierung ist reuse, also die Nutzung bereits vorhandener (getesteter) Klassen bei der Entwicklung von neuen Klassen. – Es ist empfehlenswert, sich einen Überblick über die vom API zur Verfügung gestellten Klassen zu verschaffen, und diese vordefinierten Klassen auch zu verwenden! – Beispielsweise muss man sich einen Stack nicht selber schreiben, sondern kann die entsprechende Klasse aus java.util verwenden. – Am besten orientiert man sich über die “Java 2 Platform API Specification” (z.B. http://www-lehre.inf.uos.de/manuals/jdk1.4/docs/ api/overview-summary.html) 3.8.2 Packages und Namespaces in Java Jede Klasse hat einen einfachen Namen, z.B. Circle oder InputStream, sowie einen voll-qualifizierten Namen, z.B. java.io.InputStream. Indem man als erstes die Anweisung package name ; in einer Datei schreibt, bestimmt man, dass alle Klassen in dieser Datei zum Paket name gehören. Informatik B SS 03 42 Beispielsweise gehört die Klasse InputStream zum Paket java.io. Eine Datei, in der eine Klasse definiert wird, kann also vor der Klassendefinition noch eine Paket-Definition (als aller erstes, ausser Kommentar) sowie beliebig viele Import-Anweisungen enthalten: package mypackage; import java.io.InputStream; import java.util.Stack; public class MyClass { // definition } Gibt man für eine Datei keine Paket-Anweisung an, so gehören alle dort definierten Klassen zu einem default (unbenannten) Paket. Im Allgemeinen bestehen Pakete aus mehreren Klassen. Die Klassen eines Pakets stehen in einem gemeinsamen Verzeichnis. Das Verzeichnis muss denselben Namen wie das Paket haben. Vorteile: – Mehr Übersicht und Ordnung in den Dateien. – Einzigartige, voll-qualifizierte Klassennamen! Durch Java-Konvention: Der erste Teil des Paket-Namens ist der umgedrehte Internet-Domain-Name des Nutzers, der die Klasse erzeugt. Wenn ein Java-Programm läuft und eine Klasse (*.class) geladen werden soll (dynamic loading), wird der Paket-Name in einen Verzeichnisnamen (Pfad) aufgelöst. Um die entsprechenden absoluten Pfade zu finden, schaut der Java-Interpreter in der Variable CLASSPATH nach und beginnt in den dort gegebenen Verzeichnissen mit der Suche. – Diese Variable wird üblicherweise über das Betriebssystem gesetzt. – Es ist nützlich, eigene Makefiles zu schreiben, in denen die konkret benötigten Klassenpfade angegeben sind (Übung!) 3.8.3 Namens-Kollisionen Was passiert, wenn zwei Pakete importiert werden, die Klassen mit demselben Namen enthalten? import java.util.*; // enthaelt Vector import mycollection.*; // enthaelt Vector Informatik B SS 03 43 Zunächst: nur potentielle Kollision. Der Compiler beschwert sich nicht, solange die Klasse Vektor nicht genutzt wird. Erst wenn versucht wird, ein Objekt der Klasse Vector zu erzeugen, meldet der Compiler das Problem. Lösung: java.util.Vector v = new java.util.Vector(); kennzeichnet eindeutig, welche Klasse gemeint ist. Anmerkung: Der Compiler ersetzt alle Klassennamen durch ihre voll-qualifizierten Namen. 3.8.4 Verhaltensänderungen Achtung: Wenn via import Klassen genutzt werden, so kann das eigene Programm sein Verhalten verändern, wenn die Definitionen einer importierten Klasse geändert wurden. Wenn die importierten Klassen nicht selbstgeschrieben sind, kann dies, wenn es dumm läuft, unbeabsichtigt sein und zu Fehlern in der eigenen Klasse führen! Guter Stil ist es, dass solche Klassen und Komponenten, die öffentlich zur Verfügung gestellt werden, eine klar definierte und dokumentierte Schnittstelle (Art und Anzahl der Parameter, Rückgabewert) haben und dass die Funktionalität/Semantik der öffentlich zur Verfügung gestellten Klassen nicht (bzw. nicht ohne alle Nutzer zu informieren) geändert wird. Thema: Software-Engineering Verfolgen, welche Klassen für ein Programm geladen werden mit java -verbose CircleTest (kleiner Ausschnitt in Abb. 15) 44 Informatik B SS 03 $ java -verbose CircleTest [Opened /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.lang.Object from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.io.Serializable from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.lang.Comparable from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.lang.String from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.lang.Class from /usr/lib/jdk1.3.1/jre/lib/rt.jar] ... [Loaded java.util.Dictionary from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.util.Map from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.util.Hashtable from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.util.Properties from /usr/lib/jdk1.3.1/jre/lib/rt.jar] ... [Loaded java.io.ObjectStreamField from /usr/lib/jdk1.3.1/jre/lib/rt.jar] ... [Loaded java.security.AccessController from /usr/lib/jdk1.3.1/jre/lib/rt.jar] ... [Loaded sun.io.ByteToCharConverter from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded sun.io.Converters from /usr/lib/jdk1.3.1/jre/lib/rt.jar] ... [Loaded sun.misc.Launcher from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.net.URLStreamHandlerFactory from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded sun.misc.Launcher$Factory from /usr/lib/jdk1.3.1/jre/lib/rt.jar] ... [Loaded com.sun.rsajca.Provider from /usr/lib/jdk1.3.1/jre/lib/sunrsasign.jar] ... [Loaded CircleTest] [Loaded Circle] [Loaded java.lang.FloatingDecimal from /usr/lib/jdk1.3.1/jre/lib/rt.jar] [Loaded java.lang.Math from /usr/lib/jdk1.3.1/jre/lib/rt.jar] Circle c Radius: 2.0 Circle d Radius: 4.0 Radius of bigger of c and d: 4.0 Circle c Radius: 6.0 [Loaded java.lang.Shutdown$Lock from /usr/lib/jdk1.3.1/jre/lib/rt.jar] Abbildung 15: Der Java-Interpreter lädt die core API-Klassen, weitere Klassen werden dynamisch dazugeladen Informatik B SS 03 45 4 Konstruktoren und Vererbung 4.1 Konstruktoren Im Allgemeinen sind Konstruktoren spezielle Funktionen zur Erzeugung von Objekten. In der funktionalen Programmierung und in der Typtheorie ist ein Konstruktor ein Symbol, mit dem ein Objekt eines algebraischen Datentyps erzeugt wird. – Beispiel in ML: datatype ’a list = nil | :: of ’a * ’a list – Ausdrücke über Konstruktoren werden nicht reduziert (durch Ergebnis der Funktionsanwendung ersetzt); sie sind bereits in Normalform. – Beispiele für ML-Listen mit Typvariable ’a als Integer: nil, 1::nil, 2::(1::nil) – Algebraische Datentypen heissen abstrakte Datentypen, wenn sie ihre Konstruktoren nicht “nach aussen” weitergeben. Objekte eines abstrakten Datentyps können nur mit speziellen Funktionen, die für diesen ADT (im selben “Modul”) definiert wurden, manipuliert werden. In der objekt-orientieren Programmierung wurde der Begriff “Konstruktor” mit C++ eingeführt: – Ein Konstruktor wird von einer Klasse zur Verfügung gestellt und dient der Initialisierung eines Objekts. – In C++ und Java haben Konstruktoren denselben Namen wie die Klasse. – Da ein neues Objekt fast immer über einen Konstruktoraufruf erzeugt wird, kann die Initialisierung nicht vergessen werden. – Als Gegenstück könnnen Destruktoren definiert werden, die die vom Objekte gehaltenen Ressourcen freigeben. – Java hat das Konstruktor-Konzept übernommen. Destruktoren sind kaum notwendig, da Java mit Garbage Collector arbeitet. 4.1.1 Definition von Konstruktoren Wird für eine Klasse kein Konstruktor definiert, so wird ein Default Konstruktor (auch “no-arg constructor”) angelegt. Der Default Konstruktor hat denselben Namen wie die Klasse, keine Argumente und keinen Rückgabetyp (auch nicht void). Alle Konstruktoren haben implizit eine Referenz zum neu erzeugten Objekt (this) als Argument. Informatik B SS 03 46 Im Konstruktor-Körper werden die Initialisierungen des this-Objekts vorgenommen. Beispiel: Radius eines Circle-Objekts kann bei der Erzeugung initialisiert werden public Circle(double r) { this.r = r; } ... Circle c = new Circle(2.0); ' Die Variable ist vom Typ Circle. Sie wird mit einem neuen Circle-Objekt mit Radius 2 belegt (genauer mit dem Verweis auf dieses Objekt). 4.1.2 Definition mehrerer Konstruktoren Möglichkeit, Objekt auf verschiedene Art zu initialisieren. Beispiel: Initialisierung eines Circle-Objekts mit spezifischem Radius oder mit Default-Wert. public Circle() { r = 1.0; } public Circle(double r) { this.r = r; } Wie bei Methoden gilt overloading: gleicher Name aber unterschiedliche Signaturen (Anzahl und Typ der Argumente). (Overloading wird im Kapitel ‘Klassenabhängigkeiten’ besprochen) Ein Konstruktor kann andere Konstruktoren aufrufen: this() als Konstruktoraufruf; welcher Konstruktor aktiviert wird, hängt wieder von Anzahl und Typ der Argumente ab. Verwendung von this() ist eine gute Strategie, wenn die Konstruktoren Teile der Initialisierung gemeinsam haben this() darf nur als erste Anweisung in einem Konstruktor vorkommen. Grund: automatischer Aufruf der Konstruktoren der Oberklasse (Kapitel ‘Klassenabhängigkeiten’) // This is the basic constructor: initialize the radius public Circle(double r) { this.r = r; } // This constructor uses this() to invoke the constructor above public Circle() { this(1.0); } 4.2 Defaults und Initialisierung für Felder 4.2.1 Defaults Lokale Variablen (innerhalb von Methoden definiert) haben keine Default-Werte. Werden lokale Variablen nicht vor ihrer Verwendung initialisiert, liefert der Java-Compiler eine Fehlermeldlung (der C-Compiler nur eine Warnung). Informatik B SS 03 47 Tabelle 2: Default-Werte für Felder Typ boolean char byte, short, int, long float, double reference Default false ‘ u0000’ 0 0.0 null (Klassen- und Instanz-) Felder sind automatisch mit Default-Werten initialisiert. Übliche Deklaration mit Zuweisung eines initialen Wertes ist ebenfalls möglich. public static final double PI = 3.14159; public double r = 1.0; 4.2.2 Initialisierung von Instanz-Feldern: Konstruktoren Der Java Compiler erzeugt Initialisierungscode für Instanz-Felder und fügt sie in den Konstruktor (oder die Konstruktoren) der Klasse ein. Reihenfolge: die im Programm angegebene (Nutzung von bereits initialisierten Feldern bei der Initialisierung weiterer Felder möglich). Wenn ein Konstruktor mit this() Anweisung beginnt, dann wird in diesen Konstruktor die Initialisierung nicht eingefügt, sondern in denjenigen Konstruktor, der durch this() aktiviert wird. ab Java 1.1: Initialisierungsblöcke für Instanz-Felder: ... , die an beliebiger Stelle (an der Komponenten stehen können) in die Klasse eingefügt werden können; üblich: direkt nach Feld; benutzt vor allem für anonyme innere Klassen (kommt später) public class TestClass { public int len = 10; public int[] table = new int[len]; public TestClass(){ for (int i = 0; i < len; i++) table[i] = i; } } Ist äquivalent zu public class TestClass { public int len; Informatik B SS 03 48 public int[] table; public TestClass() { len = 10; table = new int[len]; for (int i = 0; i < len; i++) table[i] = i; } } 4.2.3 Initialisierung von Klassen-Feldern: Initialisierungs-Blöcke Klassen-Felder existieren auch dann, wenn kein Objekt erzeugt wird. Initialisierung vor Konstruktoraufruf notwendig. Java Compiler erzeugt automatisch eine Klassen-Initialisierungs-Methode für jede Klasse (interne, versteckte Methode clinit ), in der alle Klassen-Felder initialisiert werden. Diese Methode wird genau einmal ausgewertet, nämlich wenn die Klasse das erstemal benutzt (geladen) wird. Initialisierung wieder in der im Programm angegebenen Reihenfolge. Explizite Initialisierung von Klassen-Feldern mit static initializer Block: static ... , der an jeder Stelle der Klasse stehen kann, wo Komponenten stehen können. Es kann mehrere solche Initialisierungs-Blöcke geben. Initialisierungs-Blöcke werden vom Compiler in die Klassen-Initialisierungs-Methode integriert. Statische Initialisierung ist wie eine Klassen-Methode, also keine Verwendung von this möglich, keine Nutzung von Instanz-Komponenten möglich. // We can draw the outline of a circle using trigonometric functions // Trigonometry is slow, though, so we precompute a bunch of values public class TrigCircle { // Here are our static lookup tables and their own simple initializers private static final int NUMPTS = 500; private static double sines[] = new double[NUMPTS]; private static double cosines[] = new double[NUMPTS]; // Here’s a static initializer that fills in the arrays static { double x = 0.0; double delta_x = (Circle.PI/2)/(NUMPTS-1); for (int i = 0; i < NUMPTS; i++, x += delta_x) { sines[i] = Math.sin(x); cosines[i] = Math.cos(x); } } } Informatik B SS 03 49 4.3 Zerstören und Finalisieren von Objekten 4.3.1 Garbage Collection Mit new werden neue Objekte erzeugt (und dynamisch, zur Laufzeit auf dem heap abgelegt) Anmerkung: Werte primitiver Datentypen (bei lokalen Variablen) werden dagegen statisch, auf dem Stack abgelegt, dessen Grösse bereits zur Compilezeit bestimmt wird. Wenn ein Objekt nicht länger benutzt wird, wird der Speicherplatz automatisch freigegeben (garbage collection, alte Technik, ursprünglich in Lisp entwickelt) Der Java Interpreter weiss, welche Objekte und Arrays er angelegt (allocated) hat, und kann ermitteln, welche Objekte und lokale Variablen auf andere Objekte verweisen. Wenn kein Verweis auf ein Objekt existiert, kann es zerstört werden; dito für nicht mehr referenzierte Verweis-Zyklen. Der Garbage Collector läuft immer im Hintergrund als low priority thread (Multi-Threading wird später behandelt); wird im Normalfall immer aktiv, wenn nichts Wichtiges passiert (z.B. beim Warten auf Input), ausser: wenn kaum noch freier Speicher vorhanden ist. Garbage Collection kann nie so effizient sein wie gute selbstgeschriebene Speicherverwaltung (free(), delete); aber es verhindert Fehler (z.B. memory leaks) und erlaubt schnellere Entwicklung von Code. Memory leaks können in Java so gut wie nicht passieren (Ausnahmen: lokale Variablen in Methoden, die lange Ausführungszeiten haben und dabei nicht auf diese Variablen zugreifen; Objekte mit Referenzen in Hash-Tabellen werden erst zerstört, wenn die Hash-Tabelle selbst zerstört wird). 4.3.2 Anmerkung: Finalization Freigabe von bestimmten Resourcen, die ein Objekt benutzt, wird nicht vom Garbage Collector erledigt (z.B. temporäre Dateien löschen) Finalizer ist Instanz-Methode, Gegenstück zu Konstruktor (“Destruktor”); wird vom Garbage Collector aufgerufen; keine Argumente, kein Rückgabewert Es darf nur einen Finalizer pro Klasse geben. protected void finalize() Selbstgeschriebene Klassen benötigen selten explizite Finalizer (Ausnahme native finalize für Schnittstellen zu Code, der nicht unter Kontrolle des Garbage Collectors ist) Informatik B SS 03 50 Animal has skin can move around eats breathes has wings can fly has feathers Bird can sing Canary Ostrich is yellow has thin long legs is tall can’t fly Fish has fins can swim has gills can bite Shark Salmon is dangerous is pink is edible swims upriver to lay eggs Abbildung 16: Illustration eines hierarchischen semantischen Netwerks 4.4 Unterklassen und Vererbung 4.4.1 Exkurs: Hierarchische Semantische Netze Der Teachable Language Comprehender (TLC) von Collins und Quillian (1969) ist ein frühes (implementiertes) kognitives Modell zum semantischen Gedächtnis. (siehe Abb. 16) Psychologische Experimente: Antwortzeiten bei Entscheidungsfragen “A canary eats?” dauert länger als “A canary is yellow?” Idee: Wissen ist in hierarchischem Netz mit Vererbung organisiert (“kognitive Ökonomie”) Antworten dauern um so länger, je weiter man “nach oben” (zu Oberklassen) suchen muss, um eine Eigenschaft zu verifizieren. “Flache” logische Repräsentation: Klassen und Objekte werden mehrfach repräsentiert! Verifikation über logische Inferenzregeln (Transitivität) “natürlicher”: Jede Klasse existiert genau einmal, Ober-/Unterklassen-Beziehungen werden für jede Klasse angegeben. Informatik B SS 03 51 /* Fakten */ isa(canary, bird). isa(ostrich, bird). isa(bird, animal). isa(shark, fish). isa(salmon, fish). isa(fish, animal). has(skin, animal). does(eat, animal). ... /* Inferenzregeln */ is_a(A,B) :- isa(A,B). /* R1: direkter Fall isa is_a(A,C) :- isa(A,B), is_a(B,C). /* R2: Transitivitaet von isa /* analog fuer has, does, ... */ */ */ Abbildung 17: Flache Prolog-Realisierung des TLC class Animal { boolean hasSkin = true; boolean canEat = true; } class Bird extends Animal { boolean hasWings = true; boolean canFly = true; // default, gilt nicht fuer alle Voegel } class Ostrich extends Bird { Ostrich() { canFly = false; } } Abbildung 18: Hierarchie und Vererbung sind natürliche Konzepte in der OO-Programmierung Informatik B SS 03 52 4.4.2 Erweiterung von ‘Circle’ Spezielle Kreise: haben Radius und Position in der Ebene PlaneCircle als Unterklasse von Circle Funktionale Erweiterung von Klassen durch Unterklassenbildung ist zentral für objekt-orientierte Programmierung class Name extends SName ... Felder und Methoden der Oberklasse werden automatisch vererbt, Konstruktoren nicht! (In anderen Sprachen werden sinnvollerweise auch die Konstruktoren vererbt. Dafür kann es dann keine automatisch generierten Default-Konstruktoren geben.) Unterklassen-Konstruktor kann Konstruktor der Oberklasse durch super() aufrufen (analog zu this()) public class PlaneCircle extends Circle { // We autmatically inherit the fields and methods of Circle, // so we only have to put the new stuff here. // New instance fields that store the center point of the circle public double cx, cy; // A new constructor method to initialize the new fields // It uses a special syntax to invoke the Circle() constructor public PlaneCircle(double r, double x, double y) { super(r); // Invoke constructor of the superclass this.cx = x; // Initialize instance fields this.cy = y; // using ‘this’ is not necessary here } // The area() and circumference() methods are inherited from Circle // A new instance method that checks whether a point is inside the circle // Note that it uses the inherited instance field r public boolean isInside(double x, double y) { double dx = x - cx, dy = y - cy; // Distance from center double distance = Math.sqrt(dx*dx + dy*dy); // Pythagorean theorem return (distance < r); // Returns true or false } } 4.4.3 Erweiterung einer Klasse Vererbung von Feldern (z.B. r) Vererbung von Methoden: Methoden der Oberklasse können für Objekte der Unterklasse genutzt werden. Zusätzliche Felder und Methoden können für die Unterklasse definiert werden. Informatik B SS 03 53 Circle PI r + radiansToDegrees + area + circumference PlaneCircle cx cy + isInside Abbildung 19: Ein Circle und seine Unterklasse Jedes Objekt der Unterklasse besitzt alle Instanz-Felder und -Methoden der Oberklasse. Typkonversion zwischen Unter- und Oberklassen: von Unterklasse zu Oberklasse (upcasting), Objekt wird allgemeiner (verliert Zugriff auf spezielle Felder und Methoden) ohne Casting; von Oberklasse zu Unterklasse (downcasting): Casting notwendig (und Prüfung zur Laufzeit durch die VM) (vergleiche widening und narrowing bei primitiven Datentypen) PlaneCircle pc = new PlaneCircle(2.0, 5.0, 5.0); double ratio = pc.circumference() / pc.area(); Circle c = pc; // no access to positioning PlaneCircle pc2 = (PlaneCircle) c; boolean origininside = ((PlaneCircle) c).isInside(0.0, 0.0); 4.5 Kapslung und Zugriffskontrolle Klassen als Sammlung von Daten und Methoden. Wichtige objekt-orientierte Technik: Information-Hiding, Encapsulation (Einkapslung): Daten nur über Methoden zugänglich machen; Daten und interne (private) Methoden sind sicher in der Klasse eingeschlossen und können nur von vertrauenswürdigen Nutzern (also über ordentlich definierte öffentliche Methoden der Klasse) benutzt werden. Informatik B SS 03 54 Schutz der Klasse gegen absichtliche oder unabsichtliche Eingriffe. z.B. konsistente Belegung von voneinander abhängigen Felder (Sicherung über sorgfältig definierte Methoden) Verstecken interner Implementations-Details. Ändern der Implementation, ohne dass genutzter Code dieser Klasse betroffen ist. (vgl. Theorie der Modularen Programmierung) Öffentlich sichtbare Felder und zu viele Methoden machen API unübersichtlich und schwer zu verstehen. 4.5.1 Zugriffs-Kontrolle (access control) Paket-Zugriff: Nicht Teil von Java selbst (Lesbarkeit von Dateien, Verzeichnissen) Paket: üblicherweise in einem Verzeichnis; wenn kein explizites Paket angegeben wird, wird ein unbenanntes Default-Paket generiert. Klassen-Zugriff: Default ist, dass top-level Klassen (etwas anderes kennen wir noch nicht) paket-weit zugreifbar sind. public deklarierte Klassen sind überall (wo das Paket zugreifbar ist) zugreifbar. Zugriff auf Komponenten einer Klasse: Komponenten sind in jedem Fall in der Klasse selbst zugreifbar; Default: paket-weiter Zugriff; Alle Klassen, die zum selben Paket gehören, dürfen zugreifen. Bei unbenanntem Paket typischerweise alle Klassen im selben Verzeichnis (implementationsabhängig). Zugriffsmodifikatoren: public, protected, private für Felder und Methoden. 4.5.2 Vier Ebenen von Zugriffsrechten Für Klassen-Komponenten: public: von überall (wo Paket zugreifbar ist) zugreifbar protected: paket-weit und aus allen Unterklassen (egal, in welchem Paket sie definiert sind) zugreifbar default: paket-weit zugreifbar (wenn kein Modifikator angegeben) private: nur in der Klasse selbst zugreifbar Informatik B SS 03 55 Tabelle 3: Zugriffsmodifkatoren Accessible to Defining class Class in same package Subclass in different package Non-subclass in different package public yes yes yes yes protected yes yes yes no ‘package’ yes yes no no private yes no no no 4.5.3 Zugriffs-Kontrolle und Vererbung Unterklasse erbt alle Instanz-Felder und -Methoden der Oberklasse. Manche Komponenten sind aufgrund der Einschränkung der Sichtbarkeit nicht zugreifbar. Wenn Ober- und Unterklasse im selben Paket: Zugriff auf alle nicht-privaten Felder und Methoden. Wenn Ober- und Unterklasse in verschiedenen Paketen: Zugriff auf alle public und protected Felder und Methoden. private Komponenten und Konstruktoren können nie ausserhalb der Klasse zugegriffen werden. In Tabelle 3 ist eine Übersicht über die Zugriffsmodifikatoren in Java angegeben. Die Semantik ist leicht verschiedenen zu anderen objekt-orientieren Sprachen (C++). Hinter der einfachen Struktur der Tabelle sind subtile Probleme versteckt (z.B. wie verhält sich protected, wenn Klasse A und Unterklasse B in verschiedenen Paketen stehen und Klasse B in Klasse A genutzt wird? Wie spielen Casting und Zugriffsmodifikatoren zusammen?) Beispiel: Circle mit protected r. PlaneCircle (ist Unterklasse) in anderem Paket. Methode in PlaneCircle: public boolean isBigger (Circle c) { return (this.r > c.r); } Compiler-Fehler: Zugriff auf this.r ist erlaubt, da das Feld r von der Unterklasse PlaneCircle geerbt wird. Aber: Zugriff auf c.r ist in PlaneCircle nicht erlaubt! Wäre erlaubt, wenn PlaneCircle c anstelle von Circle c, oder wenn Circle und PlaneCircle im selben Paket definiert wären. (“Java-Feature”) Informatik B SS 03 56 In Tabelle 3 fehlt die Information über das Zusammenspiel der Rechte mit dem Typ der Referenz. (siehe Übung) In der Regel darf man auf protected/private Komponenten nur über eine Referenz der Klasse zugreifen, in der der Zugriff erfolgt. 4.5.4 Daumenregeln für Sichtbarkeits-Modifikatoren Benutze public nur für Methoden und Konstanten, die den öffentlichen Teil des API der Klasse darstellen sollen. Kapsle Felder: als privat deklarieren und Zugriff über public Methoden Benutze protected für Komponenten, die bei der Erzeugung von Unterklassen notwendig sein könnten. Achtung: Änderung von protected Komponenten kann im Code der Klasse Inkonsistenzen erzeugen. Benutze default Sichtbarkeit für Komponenten, die interne Implementation realisieren und von Klassen im selben Paket genutzt werden sollen. Nutzung der package Anweisung, um miteinander kooperierende Klassen in ein Paket zu bündeln. Sonst benutze private. Besser zunächst möglichst restriktive Rechte vergeben und erst wenn nötig lockern. 4.5.5 Daten-Zugriffs-Methoden package myshapes; // Specify a package for the class public class Circle { // The class is still public // This is a generally useful constant, so we keep it public public static final double PI = 3.14159; protected double r; // Radius is hidden, but visible to subclasses // A method to enforce the restriction of the radius // This is an implementation detail that may be of // interest to subclasses protected void checkRadius(double radius) { if (radius < 0.0) throw new IllegalArgumentException("radius may not be negative."); } // The constructor method public Circle(double r) { checkRadius(r); this.r = r; } Informatik B SS 03 57 // Public data accessor methods public double getRadius() { return r; } public void setRadius(double r) { checkRadius(r); this.r = r; } // Methods to operate on the instance field public double area() { return PI * r * r; } public double circumference() { return 2 * PI * r; } } Klasse ist einem Paket zugeordnet (Verzeichnisname gleich dem Paketnamen). Methoden sind um Fehlerprüfung erweitert (explizite checkXX() Methoden). Zugriff auf Werte von Instanz-Feldern erfolgt über Methoden (setXX(), getXX()). Felder, auf die mit set- und get-Methoden zugegriffen wird, werden auch Properties genannt. Man spricht von “Getter”- und “Setter”-Methoden. Informatik B SS 03 58 5 Klassenabhängigkeiten 5.1 Klassenhierarchie 5.1.1 Finale Klassen Als final deklarierte Klassen können nicht erweitert werden. z.B. java.lang.System Verhindern ungewünschter Erweiterungen (Compiler kann einige Optimierungen vornehmen, siehe später) 5.1.2 Die Klasse ‘Object’ Jede Klasse, die definiert wird, hat eine Oberklasse. Wenn keine Oberklasse (spezifiziert nach extends) angegeben wird, dann wird als Oberklasse java.lang.Object vergeben. Object ist eine Klasse mit speziellem Status: einzige Klasse, die keine Superklasse hat; alle Java Klassen erben die Methoden von Object. Vorteil einer solchen “single rooted hierarchy”: – Bei jedem Objekt ist garantiert, dass es vom Typ Object ist. Damit können Klassen/Methoden definiert werden, die auf allen Objekten arbeiten (siehe Collection Klassen) und bestimmte Methoden können für alle Objekte angewendet werden (z.B. toString()). – Für alle Objekte ist via Object garantiert, dass sie sinnvoll mit dem Laufzeit-System interagieren (z.B. Garbage Collection) 5.1.3 Klasse ‘String’ Spezieller Status (wie Array; mit spezieller Syntax). Text in doppelten Anführungszeichen kann direkt an eine Variable vom Typ String zugewiesen werden. Escape-Sequenzen können verwendet werden. Strings im Quelltext dürfen nicht über Zeilen umgebrochen werden. (Lösung: Aufteilen und konkatenieren; Konkatenation konstanter Strings bei Compilation, nicht zur Laufzeit) String name = "David"; System.out.println("Hello\t" + name); String s = "This is a test of the" + "emergency broadcast system"; Informatik B SS 03 59 5.1.4 Hierarchische Klassenstruktur In Java kann eine Klasse nur genau eine andere Klasse als Oberklasse haben. Im Gegensatz zu C++ gibt es keine Mehrfachvererbung Klassenbaum (nicht Klassenverband)! Datenstruktur: Problem bei Mehrfachvererbung: Existiert dieselbe Komponente in mehr als einer Klasse, von der geerbt wird, muss geregelt werden, welche dieser Komponenten in die Unterklasse übernommen wird! Beispiel: java.lang.Object ist die Wurzel des Baums. Number stammt von Object ab; Integer von Number. In Abb. 20 (Quelle: Charles L. Perkins, siehe auch http: //www.cs.rit.edu/˜ats/java/html/skript/A_Classes.htmld/ zeigen die durchgezogenen Linien Klassenabhängigkeiten, die gestrichelten Linien zeigen die Implementierung von Schnittstellen (Kapitel ‘Abstrakte Klassen und Interfaces’). – Boxen mit runden Ecken: Interfaces (Kapitel ‘Abstrakte Klassen und Interfaces’) – weisse Boxen: Klassen – hellgraue Boxen: Final – dunkelgraue Boxen: abstrakte Klassen (Kapitel ‘Abstrakte Klassen und Interfaces’) Informatik B SS 03 60 java.lang Boolean 1995-7, Charles L. Perkins http://rendezvous.com/java Character FDBigInt local to package Class Cloneable ClassLoader Compiler Serializable Byte Double java.io-objects Math Float Number Integer Process Long Object Runtime Short FloatingDecimal SecurityManager local to package String NullSecurityManager StringBuffer local to package System Runnable Thread ThreadDeath ThreadGroup Error java.lang-errors Throwable Exception Void java.lang-exceptions Abbildung 20: Hierarchische Organisation von ‘java.lang’ Informatik B SS 03 61 5.2 Ergänzung: Konstruktoren 5.2.1 Unterklassen-Konstruktoren PlaneCircle extends Circle // A new constructor method to initialize the new fields public PlaneCircle(double r, double x, double y) { super(r); // Invoke constructor of the superclass this.cx = x; // Initialize instance fields this.cy = y; } super(): Aufruf eines Oberklassen-Konstruktors (analog zu this(): Aufruf eines Klassen-Konstruktors) super() kann nur innerhalb einer Konstruktor-Methode benutzt werden Aufruf des Oberklassen-Konstruktors muss an erster Stelle in einem Konstruktor stehen (sogar vor der Deklaration lokaler Variablen) Argumente, die an super() übergeben werden, müssen mit der Signatur eines Oberklassen-Konstruktors übereinstimmen. Der entsprechende Konstruktor wird dann ausgewählt. 5.2.2 Default-Konstruktoren Java garantiert, dass immer, wenn eine Instanz einer Klasse erzeugt wird, die (bzw. eine) Konstruktormethode der Klasse aufgerufen wird. Wenn die Klasse eine Unterklasse ist, ist ebenfalls garantiert, dass der Konstruktor der Oberklasse aufgerufen wird. Java muss sicherstellen, dass jede Konstruktormethode eine Konstruktormethode der Oberklasse aufruft. Wenn kein expliziert Aufruf angegeben ist, wird der Aufruf des Default Konstruktors super() eingefügt. Achtung: Wenn die Oberklasse keinen null-stelligen Konstruktor anbietet, erfolgt ein Compiler-Fehler! Wenn eine Klasse gar keinen Konstruktor definiert, wird ein null-stelliger Default-Konstruktor erzeugt: public Klassen erhalten public Konstruktoren; alle anderen Klassen erhalten den default Konstruktor ohne Sichtbarkeits-Modifikator. Um zu verhindern, dass ein public Konstruktor eingefügt wird, sollte mindestens ein nicht-public Konstruktor definiert werden. Für Klassen, die nicht instantiiert werden sollen, sollte ein private Konstruktor definiert werden (kann nicht von ausserhalb der Klasse aufgerufen werden, verhindert automatische Einführung eines default-Konstruktors.) (später: abstrakte Klassen) Informatik B SS 03 62 5.2.3 Konstruktor-Verkettung (constructor chaining) Wenn eine neue Instanz eines Objekts zur Klasse erzeugt wird, wird der entsprechende Konstruktor aufgerufen. Dieser ruft explizit oder implizt den Konstruktor der unmittelbaren Oberklasse auf usw., solange bis der Konstruktor der Klasse Object aufgerufen wird. Die Ausführung ist in umgekehrter Reihenfolge zum Aufruf, also zuerst wird Object() ausgeführt. Wann immer der Körper eines Konstruktors ausgeführt wird, ist sichergestellt, dass alle Felder der Oberklasse bereits initialisiert sind! Anmerkung finalizer chaining: finalize() Methoden werden nicht automatisch verkettet. super.finalize() 5.3 Vererbung: Shadowing und Overriding 5.3.1 Verdecken von Feldern der Oberklasse (Shadowing) Ein etwas konstruiertes Beispiel: Weiteres Instanz-Feld in Klasse PlaneCircle, das die Distanz zwischen dem Kreismittelpunkt und dem Ursprung (0, 0) angibt, das r genannt wird, also dieselbe Bezeichnung hat, wie das Feld r (für Radius) in der Oberklasse Circle. class PlaneCircle extends Circle { public double r; ... PlaneCircle(...) { // Pythagorean Theorem this.r = Math.sqrt(cx * cx + cy * cy); } } Zugriff auf Felder der aktuellen Klasse und der Oberklasse: r this.r super.r // Feld der aktuellen Klasse // dito // Feld der Oberklasse Alternativ: Cast des Objekts zur entsprechenden Oberklasse. ((Circle) this).r Klammerung beachten: erst Cast, dann Zugriff auf Feld! Informatik B SS 03 63 Casting ist vor allem hilfreich, wenn auf ein Feld referenziert werden soll, das nicht in der direkten Oberklasse definiert ist. Beispiel: class C extends B, class B extends A und alle drei Klassen haben ein Feld x; Innerhalb von C: x this.x super.x ((B)this).x ((A)this).x super.super.x // // // // // // Field x dito Field x dito Field x Illegal in class C in class B in class A Syntax Zugriff auf x einer Instanz von C C c = new C(); c.x ((B)c).x ((A)c).x // Field x of class C // Field x of class B // Field x of class A (Häufig sinnvoller: andere Variablennamen verwenden.) 5.3.2 Shadowing von Klassen-Feldern Klassen-Felder können ebenfalls überdeckt werden Beispiel: exakteres PI in PlaneCircle public static final double PI = 3.14159265358979323846; Innerhalb von PlaneCircle: PI PlaneCircle.PI super.PI Circle.PI // // // // Field PI in class PlaneCircle dito Field PI in class Circle dito Achtung: Innerhalb der Methoden area() und circumference(), die in Circle definiert sind, wird immer auf Circle.PI referenziert, auch wenn diese Methoden innerhalb von PlaneCircle oder von einem PlaneCircle-Objekt benutzt werden! Informatik B SS 03 64 5.3.3 Überschreiben von Instanz-Methoden der Oberklasse (Overriding) Wenn in einer Klasse eine Instanz-Methode definiert wird, die dieselbe Signatur (Name, Parameter) hat wie eine Methode der Oberklasse, wird die Methode der Oberklasse überschrieben: Wenn die Methode bei einem Objekt der Klasse aufgerufen wird, dann wird die neue Methodendefinition aktiviert. Achtung: Bei gleicher Signatur darf kein anderer Rückgabetyp verwendet werden, Sichtbarkeitsmodifikatoren dürfen nur gelockert werden (sonst Compiler-Fehler). Überschreiben von Methoden ist eine wichtige Technik der objekt-orientierten Programmierung. Klassen-Methoden werden nicht überschrieben sondern nur überdeckt. Klasse . methode (), OberKlasse . methode () haben in jedem Fall verschiedene Namen Etwas konstruierte Erweiterung des Circle-Beispiels: Ellipse als Unterklasse von Circle mit neuer Definition von area() und circumference(). Es ist wichtig, dass für ein Objekt der Klasse Ellipse immer die neuen Methoden zur Berechnung verwendet werden! 5.3.4 Überschreiben vs. Verdecken class A { int i = 1; int f() { return i; } static char g() { return ’A’; } } class B extends A { int i = 2; int f() { return -i; } static char g() {return ’B’; } } // // // // Define a class named A An instance field An instance method A class method // // // // Define a subclass of A Shadows field i in class A Overrides instance method f in A Shadows class method g in A public class OverrideTest { public static void main (String[] B b = new B(); // System.out.println(b.i); // System.out.println(b.f()); // System.out.println(b.g()); // System.out.println(B.g()); // A a = (A)b; System.out.println(a.i); args) { Creates new object of type B Refers to B.i; prints 2 Refers to B.f(); prints -2 Refers to B.g(); prints B This is a better way to invoke B.g() // Casts b to a reference to class A // Now refers to A.i; prints 1 Informatik B SS 03 65 System.out.println(a.f()); // Still refers to B.f(); prints -2 System.out.println(a.g()); // Refers to A.g(); prints A System.out.println(A.g()); // This is a better way to invoke A.g() } } Unterschied zwischen Überschreiben von Instanz-Methoden und Überlagern von Feldern (und Klassen-Methoden) macht Sinn: Für ein Objekt sollen auf jeden Fall seine spezifischen Methoden angewendet werden. (Beispiel: Array von Circle-Objekten, von denen manche Circle- und manche Ellipse-Objekte sind. Methoden zur Berechnung von Flächeninhalt und Umfang sollen auf jeden Fall die der spezifischen Klasse sein!) 5.3.5 Dynamisches ‘Method Lookup’ Woher weiss der Compiler, ob er die Methode zur Oberklasse A oder zur Unterklasse B aufrufen soll, wenn beispielsweise ein Array von A Objekten definiert wurde, in dem manche Objekte zur Klasse B gehören können? Kann er nicht wissen: Code, der dynamisches Method Lookup zur Laufzeit benutzt: Interpreter prüft Typ eines Objektes und ruft die entsprechende Methode auf. auch als Virtual Method Invocation bezeichnet (in C++) 5.3.6 ‘Final’ Methoden und Statisches ‘Method Lookup’ Schneller, wenn kein dynamic method lookup zur Laufzeit benötigt wird. Wenn eine Methode mit dem final Modifikator deklariert ist, heisst das, dass die Methode nicht von einer Unterklassen-Methode überschrieben werden darf. Der Compiler weiss bereits, welche Version der Methode gemeint ist, und dynamisches Lookup ist damit unnötig. Für bestimmte Methoden kann das Java-Laufzeitsystem dynamic method lookup vermeiden: – Alle Methoden einer final deklarierten Klasse sind final: also ist bekannt, für welche Klasse der Aufruf erfolgt. – Alle private Methoden können generell nur in der Klasse selbst aufgerufen werden: damit ist ebenfalls bekannnt, für welche Klasse der Aufruf erfolgt. private Methoden sind implizit final und können nicht überschrieben werden. – static (Klassen)-Methoden werden generell nicht überschrieben (sondern überdeckt). Informatik B SS 03 66 5.3.7 Aufruf einer überschriebenen Methode Aufruf überschriebener Methoden ist syntaktisch ähnlich zu Zugriff auf überdeckte Felder: super. methode () Aufruf einer überschriebenen Methode kann nicht mit Casting (((A)this).f()) realisiert werden! Modifizierte Form von dynamischem Method Lookup bei super: Gehe zur direkten Oberklasse derjenigen Klasse, innerhalb derer super aufgerufen wird. Wenn die Methode dort definiert ist, verwende sie, ansonsten gehe zur direkten Oberklasse dieser Klasse etc. super spricht die Methode an, die unmittelbar überschrieben wurde. super bezieht sich immer auf die unmittelbare Oberklasse der Klasse, in der der Aufruf steht. Beispiel: super.f() in OverrideTest bezieht sich auf die Klasse Object! Überdeckte Klassen-Methoden können ebenfalls durch super angesprochen werden. (Hier erfolgt generell kein dynamic lookup.) class A { // Define a class named A int i = 1; // An instance field int f() { return i; } // An instance method static char g() { return ’A’; } // A class method } class B extends A { // Define a subclass of A int i; // Shadows field i in class A int f() { // Overrides instance method f in A i = super.i + 1; // It can retrieve A.i like this return super.f() + i; // It can invoke A.f() like this }} 5.4 Overloading und Polymorphismus 5.4.1 Operator-Overloading Nicht zu verwechseln: Überschreiben und Überladen! Überladen (operator overloading): Verwendung desselben Symbols, um Operatoren mit verschiedenen Signaturen zu bezeichnen. Beispiel: monadisches und diadisches -; + für Integers, Floats, Strings Overloading wird auch als “ad hoc Polymorphismus” bezeichnet. Java erlaubt dem Benutzer kein Überladen von primitiven Operatoren, aber Überladen von Methoden (und Konstruktoren). Vorteil von Overloading: Bedeutung eines Symbols im Kontext spart die Einführung zusätzlicher Symbole. Beispiel: Definition von für komplexe Zahlen. Vergleiche natürliche Sprache (das vermisste Buch finden, sein Glück finden). Informatik B SS 03 67 Nachteil von Overloading: Es ist nicht mehr ohneweiteres nachvollziehbar, was ein Operator wirklich tut (unklare Semantik), wenn primitive Operatoren vom Benutzer überladen werden dürfen (Kritik an C++). Beispiel: a b – Intuitiv, wenn a und b vom gleichen primitiven numerischen Typ sind: int int int, float float float. – Verschiedene Optionen für char char: liefert den String aus den beiden Zeichen, liefert die Summe der Ordnungszahl der Zeichen, ... – Häufig sinnvoll: wenn verschiedene numerische Typen beteiligt sind, wird zunächst auf den “größeren” Typ ge-castet: int float float. – Was tun bei char short? (in Java haben beide 16 Bit)? – Eventuell Verlust der Kommutativität: Vorrang des ersten Arguments, also könnte char short einen anderen Ergebnistyp liefern als short char. – Was ist die Intuition für HashMap HashMap oder Stack Vector? Zulässigkeit, Einschränkungen und Techniken für Overloading sind eine wichtige Frage für das Design einer Programmiersprache! 5.4.2 Operator-Overloading in Java Arithmetische Operatoren ( , überladen. , , , ) und Vergleichsoperatoren in Java sind Arithmetische Operatoren und Vergleichsoperatoren sind für numerische Typen definiert (alle primitiven Typen ausser boolean, siehe auch die entsprechenden Wrapper-Klassen in Abb. 20) Es ist zulässig, dass ein Operator Argumente verschiedenen Typs miteinander verknüpft. Dabei erfolgt ein implizites Casting zu dem größeren Typ, mindestens zu int (widening, siehe Tab. 4). Rückgabetyp bei arithmetischen Operatoren: double wenn mindestens ein Argument double, float wenn mindestens ein Argument float, long wenn mindestens ein Argument long, int sonst (auch, wenn beide Argumente byte, short oder char sind). Dabei werden zuerst die impliziten Casts auf den Operanden durchgeführt und dann der Operator angewendet. Der Operator + (und +=) ist zusätzlich für String-Objekte definiert. Wenn mindestens eines der Argumente String ist, wird das andere Argument zu String konvertiert. Informatik B SS 03 68 Tabelle 4: Typumwandlung in Java Von Nach boolean byte short char int long float double boolean N N N N N N N byte N Y C Y Y Y Y short N C C Y Y Y Y char N C C Y Y Y Y int N C C C Y Y* Y long N C C C C Y* Y* float N C C C C C Y double N C C C C C C N(o), Y(es), explicit C(asting), Y*(yes, automatic widening, möglicher Verlust) Klammern sind häufig notwendig: System.out.println("Total: " + 3 + 4); // Total: 34 nicht 7 (Klammern (3 + 4): erst Addition auf Zahlen) Für String Object wird Object in String umgewandelt, indem die toString()-Methode des Objekts angewendet wird. Achtung: Die für Object definierte toString() Methode liefert die Referenz-Adresse des Objekts als String. Falls andere Information gewünscht wird, muss diese Methode überschrieben werden. 5.4.3 Method-Overloading in Java In Java musste Method-Overloading zugelassen werden, da Konstruktoren-Namen durch den Klassennamen bestimmt werden (automatische Erzeugung von default-Konstruktoren), und da es möglich sein sollte, mehr als einen Konstruktor für eine Klasse zu definieren. Überladene Methoden müssen sich eindeutig durch ihre Signatur unterscheiden: Anzahl, Reihenfolge und Typ der Argumente. Eine blosse Unterscheidung durch verschiedene Rückgabe-Typen ist unzulässig: void f() {} int f() {} Könnte nur klappen, wenn der Compiler eindeutig aus dem Kontext bestimmen kann, welche Methode gemeint ist, z.B. int x = f(); Auch bei Methoden mit unterschiedlichen Signaturen kann es Probleme geben, die “gemeinte” Methode eindeutig zu identifizieren. In diesem Fall liefert der Compiler eine Fehlermeldung. (siehe Abb. 21) Informatik B SS 03 69 public class Overloading { public static String f(String s, Object o) { return s + o; } public static Object f(Object o, String s) { return (Object) (o + s); } public static void main(String[] args) { System.out.println(f("Die Zahl ist ", (Object)"17")); System.out.println(f((Object)"17", " ist eine Zahl")); // System.out.println(f("Hello", "World")); // ambiguous!!! } } Abbildung 21: Method-Overloading und Eindeutigkeit 5.4.4 Polymorphismus Polymorphismus wurde ursprünglich von Christopher Strachey (1967) als Konzept benannt und dann im Rahmen der funktionalen Programmierung weiterentwickelt. Polymorphismus bei Funktionen (parametrischer P.) meint, dass Funktionen über Parameter mit Typvariablen definiert werden können: length : ’a list -> int ist eine generische Funktion, die die Länge von Listen mit Elementen beliebigen (aber einheitlichen) Typs liefert. ML war 1976 die erste Programmiersprache, die polymorphe Typisierung (zusammen mit starker Typisierung) eingeführt hat. Parametrischer Polymorphismus erlaubt denselben Code, um Argumente verschiedenen Typs zu behandeln; Overloading meint dagegen die Wiederverwendung desselben syntaktischen Symbols und verlangt verschiedenen Code, um verschiedene Typen zu behandeln! In der objekt-orientierten Programmierung meint Polymorphismus, dass Variablen deklariert werden können, die zur Laufzeit auf Objekte verschiedener Klassen verweisen können. 5.4.5 Casting und Polymorphismus Upcasting in der Klassenhierarchie ist immer zulässig. Wird ein Circle-Objekt zu einem Shape-Objekt gecasted, stehen für den Zugriff nur noch die Methoden und Felder der Klasse Shape zur Verfügung. Downcasting ist nur in speziellen Fällen möglich: Jeder Circle ist ein Shape, aber nicht jeder Shape ist ein Circle. Informatik B SS 03 70 Shape Upcasting + setColor Shape s; s.setColor(); Circle Rectangle Triangle + area + circumference + diameter + area + circumference + isSquare + area + circumference + ... Abbildung 22: Polymorphismus Unerlaubtes Downcasting führt zu einem Laufzeit-Fehler (RuntimeException), genauer zu einer ClassCastException (siehe Kapitel ‘Exceptions’). Mit dem booleschen Operator objectname instanceof Classname kann vor dem Casting explizit geprüft werden, ob das Objekt zur gewünschten Klasse gehört oder die Klasse des Objekts von der gewünschten Klasse abgeleitet ist! (gilt auch für Interfaces) void checkPos(Circle c, double x, double y) { if (c instanceof PlaneCircle) ((PlaneCircle)c).isInside(x,y); Vorteil von Polymorphismus: Eine Methode setColor() kann für beliebige Shape-Objekte definiert werden. Egal, ob die Methode zur Laufzeit zu einem Circle- oder einem Rectangle-Objekt gehört, sie kann auf jeden Fall angewendet werden. Dadurch wird Duplizierung von Code vermieden! Zudem ist keine Kenntnis nötig, welche Unterklassen von Shape konkret existieren. Wird eine Methode f() in der Oberklasse definiert, so kann sie in der Unterklasse überschrieben werden. Für ein Shape-Objekt, das ein Circle ist, wird dann seine spezifische Methode verwendet. Polymorphismus kann nur zusammen mit dynamic binding (auch late binding) realisiert werden. Viele imperative Sprachen verwenden early binding, d.h., Methodenaufrufe werden schon zur Compilezeit mit dem Methodenkörper verbunden. 5.4.6 Casting vs. Parameterisierte Klassen Unzulässiges Downcasting kann erst zur Laufzeit erkannt werden. Informatik B SS 03 71 Downcasting wird in Java immer dann verwendet, wenn Methoden/Datenstrukturen benutzt werden, die für recht allgemeine Klassen definiert werden. Werden Objekte zurückgeliefert, müssen sie häufig wieder auf ihren speziellen Typ gecasted werden. Beispiel: Collection-Klassen verwalten Objekte vom Typ Object. Alternative Idee: generische Klassen, Templates (C++). Templates sind parametrisierte Klassen, die für spezielle Typen konkretisiert werden können. Dies entspricht parametrisierten, polymorphen Typen, wie sie in der funktionalen Programmierung verwendet werden. Beispiel: Statt einer Stack-Klasse, die Objekte vom Typ Object verwaltet, kann eine Stack(T)-Klasse definiert werden. Parameter T kann dann verschieden belegt werden (zu Integer, Shape, etc.). Arrays in Java sind eine Art generische Klasse: Ausgehend von einem “allgemeinen Array-Typ” werden nach Bedarf Objekte zu spezifischen Element-Typen erzeugt. vgl. int[] vs. [](int) oder Circle[] vs. [](Circle) Informatik B SS 03 72 6 Exceptions 6.1 Fehler und Ausnahmen Exception exceptional event (aussergewöhnliches Ereignis). Eine Exception ist ein Ereignis, das während der Ausführung eines Programms auftritt und den normalen Ablauf unterbricht. In Java werden “Fehlerereignisse” als Objekte (vom Typ Exception) repräsentiert. Exception und Error erweitern Throwable. Error-Unterklassen betreffen schwerwiegende Fehler (z.B. VirtualMachineError) und sollten nicht vom Programmierer behandelt werden. Exceptions sollten dagegen behandelt werden. Exceptions treten typischerweise innerhalb von Methoden auf. Erkennt eine Methode eine Fehlerbedingung, wird ein entsprechendes Exception-Objekt erzeugt und “geworfen”. Wenn eine Exception auftritt (z. B. FileNotFoundException wenn eine Datei, die geöffnet werden soll, nicht gefunden wird), wird ein Exception Objekt erzeugt und an das Laufzeitsystem gegeben. throwing an exception Dieses Objekt enthält Information über die Art der Exception und über den Zustand des Programms (Aufrufstack) zum Zeitpunkt zu dem die Exception auftrat. Das Laufzeitsystem ist verantwortlich, Code zu finden, der den Fehler behandelt. Die Behandlung kann in der Methode, in der der Fehler aufgetreten ist, selbst oder in einer der diese Methode aufrufenden Methoden (Aufrufstack) erfolgen. Beispiel: h() -----------------> Exception tritt auf g() (ruft h() auf) f() (ruft g() auf) Behandlung der Exception Exception Handler (catch-Block): Für eine aufgetretene Exception ist derjenige Handler angemessen, der den entsprechenden Exception-Typ (Klasse oder Oberklasse des geworfenen Exceptionen-Objects) behandelt. Wird eine aufgetretene Exception nicht behandelt, terminiert das Laufzeitsystem und damit das Programm. Informatik B SS 03 73 6.2 Vorteile von Exceptions Separierung von regulärem Code und Fehlerbehandlung: Transparenz, Strukturiertheit Propagierung von Fehlern möglich Gruppierung von Fehler-Typen, Fehlerdifferenzierung readFile { open the file; determine its size; allocate that much memory; read the file into memory; close the file; } // // // // // // was ist, wenn file nicht geoeffnet werden kann? Laenge nicht bestimmt werden kann? nicht genug Speicher belegt werden kann? beim Lesen ein Fehler auftritt? file nicht geschlossen werden kann? 6.2.1 Separierung von Code und Fehlerbehandlung Abfangen mit expliziten bedingten Anweisungen im Code: errorCodeType readFile { initialize errorCode = 0; open the file; if (theFileIsOpen) { determine the length of the file; if (gotTheFileLength) { allocate that much memory; if (gotEnoughMemory) { read the file into memory; if (readFailed) { errorCode = -1; } } else { errorCode = -2; } } else { errorCode = -3; } close the file; if (theFileDidntClose && errorCode == 0) { errorCode = -4; } else { errorCode = errorCode and -4; } } else { errorCode = -5; } return errorCode; } Informatik B SS 03 74 Trennung von “normalem” Code und Fehlerbehandlung: readFile { { // Block mit Anweisungen, bei denen Exceptions auftreten koennten open the file; determine its size; allocate that much memory; read the file into memory; close the file; } handle fileOpenFailed { doSomething; } handle sizeDeterminationFailed { doSomething; } handle memoryAllocationFailed { doSomething; } handle readFailed { doSomething; } handle fileCloseFailed { doSomething; } } 6.2.2 Propagierung von Exceptions method1 { call method2; } method2 { call method3; } method3 { call readFile; } Nur method1 sei an Fehlern, die in readFile() auftreten können interessiert. Fehler-Propagierung “zu Fuss”: method1 { errorCodeType error; error = call method2; if (error) doErrorProcessing; else proceed; } errorCodeType method2 { errorCodeType error; error = call method3; if (error) Informatik B SS 03 75 return error; else proceed; } errorCodeType method3 { errorCodeType error; error = call readFile; if (error) return error; else proceed; } In Java ist es zulässig, dass eine Methode den Fehler nicht selbst fängt (“duck” a thrown exception), sondern weitere nach oben durchreicht. method1 { try { call method2; } catch (exception) { doErrorProcessing; } } method2 throws exception { call method3; } method3 throws exception { call readFile; } Achtung: Wenn in einer Methode (oder einem Konstruktur) Exceptions auftreten können, müssen diese entweder spezifiziert (throws) oder behandelt werden! InputFile.java:11: Exception java.io.FileNotFoundException must be caught, or it must be declared in the throws clause of this method. in = new FileReader(filename); ˆ 6.3 Exception Handling – ‘try’, ‘catch’, ‘finally’ // Note: This class won’t compile by design! // See ListOfNumbersDeclared.java or ListOfNumbers.java // for a version of this class that will compile. import java.io.PrintWriter; import java.io.FileWriter; import java.util.Vector; public class ListOfNumbers { Informatik B SS 03 76 private Vector vec; private static final int SIZE = 10; public ListOfNumbers () { vec = new Vector(SIZE); for (int i = 0; i < SIZE; i++) vec.addElement(new Integer(i)); } public void writeList() { PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt")); for (int i = 0; i < SIZE; i++) out.println("Value at: " + i + " = " + vec.elementAt(i)); out.close(); } } Mögliche IOException: Konstruktor FileWriter() kann angegebene Datei nicht öffnen. Mögliche RunTimeException: ArrayIndexOutOfBoundsException bei vec.elementAt(i). Es macht Sinn, dass verlangt wird, dass alle Fehler ausser RunTimeExceptions vom Programmierer behandelt werden. Der Compiler/das Laufzeitsystem können nicht hellsehen, auf welche Art eine Exception behandelt werden soll. Beispiel FileNotFoundException: Programm beenden, andere Datei einlesen, File dieses Namens erzeugen. Beispiele für RunTimeExceptions: arithmetic exceptions (division by zero), pointer exceptions (access object by null reference), indexing exceptions. Checked Exceptions: Exception ausser RunTimeExceptions, die vom Compiler geprüft werden (ob sie behandelt oder spezifiziert werden). import import import import java.io.PrintWriter; java.io.FileWriter; java.io.IOException; java.util.Vector; public class ListOfNumbers { private Vector vec; private static final int SIZE = 10; public ListOfNumbers () { vec = new Vector(SIZE); for (int i = 0; i < SIZE; i++) Informatik B SS 03 77 vec.addElement(new Integer(i)); } public void writeList() { PrintWriter out = null; try { System.out.println("Entering try statement"); out = new PrintWriter(new FileWriter("OutFile.txt")); for (int i = 0; i < SIZE; i++) out.println("Value at: " + i + " = " + vec.elementAt(i)); } catch (ArrayIndexOutOfBoundsException e) { System.err.println("Caught ArrayIndexOutOfBoundsException: " + e.getMessage()); } catch (IOException e) { System.err.println("Caught IOException: " + e.getMessage()); } finally { if (out != null) { System.out.println("Closing PrintWriter"); out.close(); } else { System.out.println("PrintWriter not open"); } } } } Erläuterungen: Es könnte alternativ zu System.err.println() auch System.out.println() verwendet werden, um Meldungen auszugeben. Üblicherweise geht der Fehler-Strom aber auf das Terminal, während der Ausgabestrom beispielsweise in eine Datei gehen kann (in der man nicht die Fehlermeldungen haben will). try-catch-finally ist eine Kontrollstruktur. Der try-Block kann ohne folgende catch-Blöcke oder ohne folgenden finally-Block stehen. (try alleine wird vom Compiler angemeckert.) Der try-Block ohne catch-Blöcke bewirkt, dass hier keine Fehlerbehandlung erfolg. Der try-Block wird verlassen, sobald eine Exception darin auftrat. Wenn bestimmte Dinge auf jeden Fall erledigt werden sollen (z.B. Schliessen einer Datei), so kann dies in einem finally-Block spezifiziert werden. Es kann maximal einen finally-Block geben, in dem abgesichert werden kann, dass bestimmte Dinge auf jeden Fall ausgeführt werden (wenn vorher kein System.exit() erfolgt), auch wenn etwas schief geht. Exception-Objekte haben die Methode getMessage(), die den Fehlertext der Exception liefert. Informatik B SS 03 78 Falls die Datei OutFile.txt nicht zum Schreiben geöffnet werden kann, reagiert der FileWriter-Konstruktor und erzeugt und wirft das entsprechende Exception-Objekt. Die ArrayIndexOutOfBoundsException müsste nicht abgefangen werden. Falls ArrayIndexOutOfBoundsException und IOException in einem einzigen catch-Block behandelt werden sollen, muss die gemeinsame Oberklasse dieser Exceptions gefangen werden. Die ist nur Exception, was üblicherweise zu allgemein wäre! finally kann Code-Duplizierung vermeiden: try { . . . out.close(); // don’t do this; it duplicates code } catch (ArrayIndexOutOfBoundsException e) { out.close(); // don’t do this; it duplicates code System.err.println("Caught ArrayIndexOutOfBoundsException: " + e.getMessage()); } catch (IOException e) { System.err.println("Caught IOException: " + e.getMessage()); } Was passiert, wenn sowohl catch als auch finally eine return-Anweisung enthalten? (Übung!) Drei Möglichkeiten des Programmablaufs: new FileWriter() geht schief, IOException: Entering try statement Caught IOException: OutFile.txt PrintWriter not open ArrayIndexOutOfBoundsException: Entering try statement Caught ArrayIndexOutOfBoundsException: 10 >= 10 Closing PrintWriter try-Block wird ohne Exception verlassen Entering try statement Closing PrintWriter Informatik B SS 03 79 6.4 Spezifikation von Exceptions – ‘throws’ import import import import java.io.PrintWriter; java.io.FileWriter; java.io.IOException; java.util.Vector; public class ListOfNumbersDeclared { private Vector vec; private static final int SIZE = 10; public ListOfNumbersDeclared () { vec = new Vector(SIZE); for (int i = 0; i < SIZE; i++) vec.addElement(new Integer(i)); } public void writeList() throws IOException, ArrayIndexOutOfBoundsException { PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt")); for (int i = 0; i < SIZE; i++) out.println("Value at: " + i + " = " + vec.elementAt(i)); out.close(); } } throws spezifiziert mögliche Fehler und verschiebt deren Behandlung zu aufrufenden Methoden. Dies macht Sinn, wenn es übergeordnete Strukturen gibt, bei denen erst klar ist, welche Fehler wie abgefangen werden sollen. Es macht wenig Sinn, dass die main-Methode (letzte Methode im Aufrufstack) Fehler spezifiziert und nicht behandelt. Programmbenutzer wird dann mit den java-Fehlermeldungen konfrontiert. throws kann nur für Methoden (und Konstruktoren) deklariert werden. 6.5 Vererbung und ‘throws’ Es ist nicht erlaubt, einer Methode beim Überschreiben weitere throws-Klauseln hinzuzufügen! Ansonsten wäre keine Zuweisungskompatibilität mehr gegeben: In allen Methoden, die diese Methode verwenden, müsste eine Spezifikation bzw. Behandlung der neu hinzugekommenen Exceptions erfolgen, was durch Casting zur Oberklasse umgangen werden könnte. Weglassen eines Teils oder einer kompletten throws-Klausel ist erlaubt. Informatik B SS 03 80 java.lang-exceptions ClassNotFoundException 1995-7, Charles L. Perkins http://rendezvous.com/java Object CloneNotSupportedException java.lang IllegalAccessException Throwable java.lang Exception InstantiationException InterruptedException NoSuchFieldException ArithmeticException ArrayStoreException ClassCastException IllegalThreadStateException NoSuchMethodException IllegalArgumentException NumberFormatException IllegalMonitorStateException IllegalStateException RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException StringIndexOutOfBoundsException NegativeArraySizeException NullPointerException Serializable java.io-objects SecurityException Abbildung 23: Java Language Exception Klassen 6.5.1 Gruppierung von Fehler-Typen Exception-Objekte sind – wie andere Java Objekte – in einer Klassenhierarchie organisiert. Ganze Gruppen von Exceptions können mit einem einzigen catch behandelt werden, wenn eine entsprechende Oberklasse verwendet wird. Wenn mehrere catch-Blöcke den Typ der Exception behandeln, so wird nur der erste (Reihenfolge im Code) passende ausgeführt. Falls mehrere Exceptions, die in einer Unterklassenbeziehung zu einander stehen, abgefangen werden sollen, so müssen diese sequentiell von der speziellsten zur allgemeinsten behandelt werden! Achtung: Exception-Handler, die zu allgemein sind, können Code wieder fehleranfällig machen! Es können dadurch Exceptions gefangen werden, die nicht vorhergesehen wurden und entsprechend nicht korrekt behandelt werden. Informatik B SS 03 81 6.6 Definition eigener Exception-Klassen und Auslösen von Exceptions public class IllegalRadiusException extends Exception { private double invalidRad; public IllegalRadiusException (String msg, double rad) { super(msg); invalidRad = rad; } public String getMessage() { return super.getMessage() + ": " + invalidRad; } } public class CheckedCircle { public static final double PI = 3.14159; protected double r; protected void checkRadius(double radius) throws IllegalRadiusException { if (radius < 0.0) throw new IllegalRadiusException ("radius may not be negative", radius); } public CheckedCircle(double r) throws IllegalRadiusException { checkRadius(r); this.r = r; } public double getRadius() { return r; } public void setRadius(double r) throws IllegalRadiusException { checkRadius(r); this.r = r; } } public static void main (String[] args) { try { CheckedCircle c = new CheckedCircle(1.0); c.setRadius(-1.0); } catch (IllegalRadiusException e) { System.out.println( e.getMessage() ); } } Nicht verwechseln: throws – Spezifizieren einer Exception, und throw – Auslösen einer Exception. In den API’s sind Exceptions auf dieselbe Art realisiert wie hier gezeigt. Alle checked exceptions werden von Methoden ausgelöst. Laufzeitfehler werden dagegen aus dem Laufzeitsystem heraus erzeugt. Im Programmbeispiel CheckedCircle muss nun in alle Methoden, in denen die IllegalRadiusException auftreten könnte, entweder die Exception spezifiziert (geworfen) oder behandelt werden. Alternativ hätte IllegalRadiusException von RunTimeException abgeleitet werden können. Dann wäre die Fehlerbehandlung nicht verpflichtend. Informatik B SS 03 82 6.7 Exkurs: UML Im Skript werden Klassen manchmal graphisch als Kästen und Beziehungen zwischen Klassen mit Pfeilen veranschaulicht. (z.B. Abb. 19) Eine standardisierte Notation zur Repräsentation von Klassen und ihren Beziehungen sind UML-Diagramme. UML (“Unified Modeling Language”) ist eine auf Diagrammen basierende Beschreibungssprache für den objekt-orientierten Entwurf und die objekt-ortientierte Analyse. Standardisierung durch: Booch, Jacobson, Rumbaugh (“Die drei Amigos”). Ein Meta-Modell legt fest, wie diese Sprache benutzt werden soll. In vielen Dokumentationen wird auf diesen Aspekt kaum eingegangen. Vergleiche: BNF als Meta-Sprache zur Repräsentation von Grammatiken für Programmiersprachen; Meta-Modell als Sprache zur Repräsentation von Grammatiken für UML-Diagramme. aktuelle Forschung: Beschreibung von UML-Diagrammen mit Graph-Grammatiken (Parsierung, syntaktische Korrektheit, ...) Objekt-orientierter Entwurf meint die Konzeption von Klassenstrukturen und Abhängigkeiten bei der Systementwicklung. Vorteile einer standardisierten Sprache: CASE-Tools (Computer Assisted Software Engineering) zur Erzeugung von Code Austausch von Entwürfen UML wird in der Vorlesung Informatik C behandelt. Im folgenden werden die wesentlichen Komponenten kurz illustriert. 6.7.1 Klassendiagramme in UML Abbildung 24 zeigt den wesentlichen Aufbau von Klassendiagrammen. UML-Diagramme sollen übersichtlich sein! Immer soviel Information für die Klassen angeben, wie notwendig ist. Optionale Angabe von Typ-Information bei Feldern und Methoden Angabe ausgewählter Komponenten (beispielsweise: nur Methoden) public abstract class Person { protected String personName; private int age; public Person (String name) { personName = name; } Informatik B SS 03 83 Person −age #personName +Person +getAge getJob +makeJob −splitNames Kasten aus drei Teilen: Klassen-Namen (fett), Felder, Methoden Sichtbarkeits-Modifikatoren: public, private, protected Kursiv: Abstrakte Klassen/Methoden Unterstrichen: Klassen-Methoden, -Felder Abbildung 24: Darstellung der Klasse ‘Person’ in UML Person −age: Integer #personName: String Person +Person(String) +getAge(): Integer getJob(): String +makeJob(): String −splitNames() +getAge getJob +makeJob −splitNames Abbildung 25: Darstellungsvarianten für Klassen in UML static public String makeJob () {return "hired";} public int getAge () {return age;} private void splitNames () {} abstract String getJob (); } 6.7.2 Klassen-/ Unterklassenbeziehungen in UML Der Klasse/Oberklasse-Pfeil repräsentiert eine Generalisierung (Employee ist Unterklasse von Person). Pfeil mit durchgezogener Linie und hohler Pfeilspitze von der Unterklasse zur Oberklasse bei Interfaces: gestrichelte Linie, über dem Interface-Namen steht <<interface>> Informatik B SS 03 84 public class Employee extends Person { public Employee (String name) { super(name); } public String getJob() { return "Research Staff"; } } Person Employee +getAge getJob +makeJob −splitNames +Employee +getJob Abbildung 26: Vererbung in UML 6.7.3 Assoziationen Beziehungen zwischen Klassen/Objekten werden als Assoziationen bezeichnet. Generalisierung ist eine spezielle Assoziation. Eine Assoziation bezeichnet eine “Rolle”, die eine Klasse bezüglich der anderen einnimmt. vergleiche: Casus-Strukturen, Fillmore, 1968, zur Repräsentation der Tiefenstruktur natürlichsprachiger Sätze. Bei Generalisierung: “Ein Employee ist eine Person.” Ein Employee arbeitet für eine Company. Assoziationen werden allgemein durch Linien zwischen Klassen (ohne Pfeil) dargestellt. Zusätzlich können Zahlenbereiche angegeben werden, die anzeigen, wieviele Instanzen von Objekten einer Klasse mit einer anderen Klasse in Beziehung stehen können. – gibt an, dass null bis beliebig viele Instanzen einer Klasse mit einer anderen assoziiert sein können. – 1 gibt an, dass genau eine Instanz mit einer Klasse/Objekt assoziiert ist (entspricht 1..1). Assoziationen einer Klasse mit sich selbst heissen rekursiv. Eine spezielle Art von Assoziation ist die Teil-Ganzes-Beziehung: Aggregation. (“Ein Auto hat Räder.”) Informatik B SS 03 85 public class Company { Employee empt1; Person per1; public Company() {} } Person Employee public class Company1 { Employee[] emp1; public Company1() {} } 0..1 per1 Company 0..1 emp1 Employee * emp1 Company1 Abbildung 27: Assoziationen in UML Komposition ist wiederum eine spezielle Aggregation, bei der die Teile ohne das Ganze nicht existieren können. Aggregationen werden durch eine Linie mit einer Raute im Ursprung dargestellt. Bei Komposition ist die Raute gefüllt. Die Klasse NeighborQueen (siehe Kapitel 2) definiert, dass eine NeighborQueen eine Queen als linke Nachbarin hat. Diese Assoziation ist eher keine Aggregation: Wir formulieren “Eine NeighborQueen hat eine Queen als Nachbarin”, aber nicht “Eine NeighborQueen hat als Teil/besteht aus einer Queen.” 6.7.4 Kommentare und Annotierung in UML Kommentare als eigene Boxen mit umgeklappter Ecke Pfeile mit durchgezogenen Linien und vollen Köpfen, um anzuzeigen, welche Klasse eine Methode welcher anderen Klasse aufruft. 6.7.5 UML-Tools Zur Kommunikation, zum Entwurf: Papier und Bleistift, beliebiges Graphik-Programm Informatik B SS 03 86 Spezielle Tools: Erzeugung von UML aus Graphik-Bausteinen, Erzeugung von UML aus Code, Erzeugung von Code aus UML (spezielle syntaktische Konventionen einzuhalten!) Beispiele: Rational Rose, Together, argouml 6.8 Exkurs: Design Patterns – Factory Pattern Design Patterns: (abstrakter) Code, der wiederverwendet werden kann (bewährte Standardlösungen von Experten) Beispiele: Factory Pattern, Adapter Pattern, Proxy Pattern, Patterns für GUI Cognitive Science: Abstraktion als Ergebnis des Lernens beim Analogen Problemlösen! Ein Simple Factory Pattern liefert eine Instanz für eine oder mehrere Klassen in Abhängigkeit von den gelieferten Daten. Üblicherweise haben alle Klassen, für die die Factory Objekte erzeugen kann, eine gemeinsame Oberklasse und gemeinsame Methoden. Die Klassen unterscheiden sich dadurch, dass sie für verschiedene Arten von Daten optimiert sind. Beispiele: Temperatur kann als Celsius oder Fahrenheit aufgefasst werden, ein Nutzerdialog kann in verschiedenen Sprachen erfolgen (Internationalisierung), eine Zeichenkette kann als numerischer Wert verschiedenen Typs aufgefasst werden, ... Informatik B SS 03 87 X XFactory +XFactory() +do_X() +newObj() : X XY XZ ... +do_X() +do_X() Abbildung 28: Das Factory-Pattern X als Basis-Klasse, von der XY und XZ abgeleitet sind. Klasse XFactory entscheidet, Instanzen welcher Unterklasse zurückgeliefert werden, in Abhängigkeit davon, welche Argumente übergeben werden. Die newObj()-Methode erhält einen Wert und liefert Instanz der “entsprechenden” Klasse. Welche Klasse zurückgeliefert wird, ist dem Programmierer egal, da alle dieselben Methoden (aber in unterschiedlichen Implementationen) haben. Beispiel: Eine Zahl wird der Factory als String übergeben. Je nach “Form” wird das Objekt einer passenden Wrapper-Klasse zurückgeliefert. package number; public class NumberFactory { public Number newNumber (String value) throws NumberFormatException { try { return new Byte(value); // first try Byte } catch (NumberFormatException e1) { Informatik B SS 03 88 java.lang Number NumberFactory intValue() newNumber(String) : Number Integer intValue() ... Double intValue() Abbildung 29: Eine Number-Factory try { return new Short(value); // ... then Short } catch (NumberFormatException e2) { try { return new Integer(value); // .. then Integer } catch (NumberFormatException e3) { try { return new Long(value); // ... then Long } catch (NumberFormatException e4) { // ‘new Float(value)’ will return an infinite value // if the number cannot be represented as a float // (not a NumberFormatException), so check for this return new Float(value).isInfinite() ? (Number) new Double(value) : (Number) new Float(value); } } Informatik B SS 03 89 } } } } import number.NumberFactory; public class NumberFactoryTest { // print object’s class name and value public static void printNumber (Number number) { System.out.println(number.getClass().getName() + ": " + number); } public static void main (String args[]) { NumberFactory factory = new NumberFactory(); // the factory try { printNumber(factory.newNumber("123")); printNumber(factory.newNumber("1234")); printNumber(factory.newNumber("123456")); printNumber(factory.newNumber("1234567890123")); printNumber(factory.newNumber("3.14159")); printNumber(factory.newNumber("1e100")); printNumber(factory.newNumber("abcd")); } catch (NumberFormatException e) { System.err.println(e); } } } // // // // // // // Byte Short Integer Long Float Double Exception // print error message Informatik B SS 03 90 open a stream while more information read information close the stream open a stream while more information write information close the stream Abbildung 30: Input und Output Stream 7 Input/Output 7.1 Ein-/Ausgabe-Ströme Anwendung: Einlesen von Information aus einer externen Quelle (source) oder Ausgabe zu einer externen Destination (sink). Information kann von verschiedenen Quellen stammen: Tastatur, Datei, Speicher, Internet, anderes Programm, ... Information kann verschiedener Art sein: Zeichen, Objekte, Bilder, ... Information kann zu verschiedenen Ausgaben gehen: Bildschirm, Datei, Drucker, Speicher, Internet, anderes Programm, ... Klassen in java.io: sehr viele Klassen, im folgenden werden nur ausgewählte Aspekte dargestellt Information wird generell sequentiell gelesen/geschrieben. 7.1.1 Klassenstruktur in ‘java.io’ Die wichtigsten Klassen zum Lesen und Schreiben von Daten sind Reader/Writer für Zeichen-Ströme (ab Java 1.1) sowie Informatik B SS 03 91 InputStream/OutputStream für Byte-Ströme (ab Java 1.0) und deren Unterklassen (nächster Abschnitt). Die Klasse RandomAccessFile erlaubt Lesen und Schreiben von Bytes, Text und primitiven Datentypen von oder in spezifische(n) Positionen einer Datei. Die Klasse StreamTokenizer liefert eine einfache lexikalische Analyse für einen Eingabestrom und zerlegt ihn in “Tokens” (wichtig für Parser-Konstruktion). Die Klasse File unterstützt plattform-unabhängige Definition von Datei- und Verzeichnisnamen. Sie liefert Methoden zum Auflisten von Verzeichnissen, Prüfen der Schreib-Lese-Rechte und weitere typische Operationen auf Dateien und Verzeichnissen. Ein File-Objekt bezeichnet einen Datei-Strom aus dem gelesen/in den geschrieben werden kann. Da Daten zum Lesen und Schreiben seriell verarbeitet werden, müssen Klassen das Serializable-Interface implementieren, damit entsprechende Objekte verarbeitet werden können. Serializable ist ein sogenanntes “Marker”-Interface, das keine Methoden oder Konstanten definiert. 7.1.2 Character und Byte Ströme Lesen/Schreiben von 16-Bit (unicode) Zeichen: Abstrakte Reader/Writer Klasse und entsprechende Unterklassen (ab Java 1.1). Lesen/Schreiben von 8-Bit Bytes: Abstrakte InputStream/OutputStream Klasse und entsprechende Unterklassen (ab Java 1.0). Ähnliche APIs (Methoden mit gleichem Namen und äquivalenter Signatur) für verschiedene Character- und Byte-Ströme. Für Byte-Ströme werden Byte-Arrays, für Character-Ströme Character-Arrays verarbeitet: int read() int read(char cbuf[]) int read(char cbuf[], int offset, int length) int write(int c) int write(char cbuf[]) int write(char cbuf[], int offset, int length) read() und write() können IOExceptions werfen. read() liefert int zurück: 0 255 für Bytes oder 0 65535 (0x00-0xffff) für Characters und -1 für Ende des Stroms. Informatik B SS 03 92 Reader Abstrakte Klassen zum Lesen und Schreiben von Character−Strömen Writer InputStream Abstrakte Klassen zum Lesen und Schreiben von Byte−Strömen Object OutputStream RandomAccessFile Lesen und Schreiben an beliebigen Stellen in einer Datei StreamTokenizer Zerlegung von Input in Tokens File Plattform−unabhängige Definition von Datei− und Verzeichnisnamen <<interface>> Serializable <<interface>> Externizable Abbildung 31: Auswahl von Klassen in java.io Informatik B SS 03 InputStreamReader Reader 93 FileInputStream FileReader BufferedInputStream InputStream BufferedReader FilterInputStream FilterReader OutputStreamReader Writer FileOutputStream FileWriter BufferedWriter OutputStream FilterWriter Abbildung 32: Unterklassen OutputStream BufferedOutputStream FilterOutputStream von Reader/Writer sowie InputStream und Überladene Methoden: – Lesen/Schreiben eines Zeichens/Bytes. – Lesen/Schreiben in/aus Array von Zeichen/Bytes. – Lesen/Schreiben von length Zeichen/Bytes in/aus Array ab Index offset. Bei den Methoden, die Arrays verarbeiten, wird die Anzahl der verarbeiteten Zeichen/Bytes zurückgeliefert bzw. -1 für Ede des Stroms. 7.1.3 Wichtige Reader- und Writer-Klassen Um komplexe Funktionalität zu erhalten, werden gerade im I/O-Bereich häufig Objekte komponiert/ineinander verschachtelt. Viele Konstruktoren und Methoden in java.io werfen Exceptions! Die Klasse InputStreamReader bietet eine Reader-Schnittstelle zu einem InputStream (analog für OutputStreamWriter): ein InputStream-Objekt wird in ein InputStreamReader-Objekt eingebettet. public class InputStreamReader extends Reader { // public constructor public InputStreamReader(java.io.InputStream in); // ... } Schreiben in eine Datei und Lesen aus einer Datei kann mit FileWriter und FileReader erledigt werden. Informatik B SS 03 94 Die Angabe von Dateien kann über File-Objekte oder (plattform-spezifische) Dateinamen als Strings erfolgen. Konstruktoren: public class FileReader extends InputStreamReader { // public constructor public FileReader(File file) throws FileNotFoundException; public FileReader(String fileName) throws FileNotFoundException; // ... } Puffern von Daten bringt Effizienz: Daten werden (in Array) gesammelt und dann weiterverarbeitet. Die Klasse BufferedReader hat eine readLine()-Methode. Die abstrakten Filter-Klassen FilterReader und FilterWriter erlauben Zusatzfunktionen während des Lesens/Schreibens: z.B. Zählen von Zeichen, Umwandlung von Zeichen etc. 7.2 Datei-Ströme import java.io.*; public class Copy { public static void main(String[] args) throws IOException { File inputFile = new File(".", "farrago.txt"); File outputFile = new File("outagain.txt"); // File Reader/Writer fuer Character-weises Lesen/Schreiben FileReader in = new FileReader(inputFile); FileWriter out = new FileWriter(outputFile); // Alternativ fuer Bytes // FileInputStream in = new FileInputStream(inputFile); // FileOutputStream out = new FileOutputStream(outputFile); int c; while ((c = in.read()) != -1) out.write(c); in.close(); out.close(); } } Klasse File: Konstruktor erzeugt File-Objekt aus String. Achtung bei Pfadnamen: Pfadseparatoren sind vom Betriebssystem abhängig Informatik B SS 03 95 Alternativ zum obigen Code könnte der Dateiname direkt als String an FileReader übergeben werden, z. B. FileReader(farrago.txt"); FileReader ist ein Datei-Strom. FileReader ist Unterklasse von InputStreamReader: Eine Datei ist eigentlich als Folge von Bytes und nicht von Unicode-Zeichen repräsentiert. Die Klasse FileReader realisiert die Anwendung eines InputStreamReader auf ein InputStream-Objekt! mit new FileReader(...) etc. wird die Datei automatisch geöffnet. Dateien können (sollten) explizit geschlossen werden, damit keine Ausgabe im I/O-Puffer verbleibt (flush). Im Prinzip werden Dateien auch vom Garbage Collector geschlossen, wenn nicht mehr auf sie zugegriffen wird. In dem Programm könnte noch eine Abfrage eingebaut werden, ob der Name der Zieldatei verschieden vom Namen der Quelldatei ist (equals() Methode der Klasse File). 7.3 Puffern von Daten Beliebige Reader-Objekte können in einen BufferedReader gesteckt werden, also z.B. FileReader-Objekte. (analog für Writer, analog für Byte-Ströme) Im zweiten Teil von BufferDemo wird gezeigt, wie Daten aus dem Standard-Eingabestrom (z.B. der Tastatur) gepuffert verarbeitet werden können. System.in ist ein InputStream-Objekt! Analog zu BufferedReader in = new BufferedReader(new FileReader(inputFile)); kann natürlich geschrieben werden: FileReader fr = new FileReader(inputFile); BufferedReader in = new BufferedReader(fr); public class BufferDemo { public static void main(String[] args) throws IOException { File inputFile = new File("farrago.txt"); File outputFile = new File("outagain.txt"); BufferedReader in = new BufferedReader(new FileReader(inputFile)); BufferedWriter out = new BufferedWriter(new FileWriter(outputFile)); String s; while ((s = in.readLine()) != null) { out.write(s); out.write(’\n’); Informatik B SS 03 96 } in.close(); out.close(); // probieren Sie, was passiert, wenn Sie out nicht // schliessen // analog von der Standard-Eingabe (System.in) BufferedReader console = new BufferedReader (new InputStreamReader(System.in)); System.out.print("What is your name: "); try { String name = console.readLine(); System.out.println("Hello " + name); } catch (IOException e) { System.err.println(e); } } } Achtung: bei allen gepufferten Ausgabe-Strömen muss die flush-Methode angewendet werden, damit der Puffer “geleert”, also damit wirklich geschrieben wird. close() führt automatisch ein flushing aus. 7.4 Filter-Ströme Die abstrakten Filter-Klassen von java.io erlauben es, beim Lesen und Schreiben zusätzliche Operationen durchzuführen. Die default-Implementation der Filter-Klassen leiten die Methodenaufrufe an das bei Konstruktion übergebene Objekt weiter, z.B. übliches read(): Null-Filter. Wird eine Unterklasse einer Filter-Klasse wie FilterInputStream definiert, so können genau die Methoden überschrieben werden, von denen man zusätzliche Funktionalität haben möchte. Die Filter-Klassen entsprechen dem Decorator-Pattern. Der Name kommt ursprünglich aus der GUI-Programmierung (z.B. Fenster mit verschiedener Dekoration), ist aber analog für nicht-visuelle Bereiche definiert. import java.io.*; public class CaseFilter extends FilterReader { public CaseFilter(Reader f) { super(f); } public int read() throws IOException { Informatik B SS 03 97 int ch = super.read(); if (Character.isLowerCase((char) ch)) return Character.toUpperCase((char) ch); else return ch; } } Die CaseFilter-Klasse implementiert eine erweiterte Funktionalität für read(): Beim Einlesen werden Kleinbuchstaben in Grossbuchstaben umgewandelt. In der Klasse DecoStream wird nun das read() eines CaseFilter-Objekts verwendet. Direkt beim Einlesen wird der Text nun umformatiert! public class DecoStream { private String readNormal(String fl) { StringBuffer s = new StringBuffer(); try { FileReader fread = new FileReader(fl); int c; while ((c = fread.read()) != -1) s.append((char) c); fread.close (); } catch(IOException e) { System.err.println(e); } return s.toString(); } private String readFilter(String fl) { StringBuffer s = new StringBuffer(); try { FileReader fread = new FileReader(fl); CaseFilter ff = new CaseFilter(fread); // CaseFilter class // provides read() with Uppercase int c; while ((c = ff.read()) != -1) s.append((char) c); ff.close (); } catch(IOException e) { System.err.println(e); } return s.toString(); } static public void main(String[] argv) { DecoStream d = new DecoStream(); String s = d.readNormal("note.txt"); System.out.println(s); Informatik B SS 03 98 s = d.readFilter("note.txt"); System.out.println(s); } } Anmerkung: Die Verwendung von StringBuffer statt String ist hier effizienter: Bei s = s + (char) c; muss jeweils eine Kopie des Strings angelegt werden. StringBuffer besitzt die Methode append(), mit der das neue Zeichen direkt angefügt werden kann. 7.5 Standard-Ein- und Ausgabe Auf Betriebssystem-Ebene existieren für jedes laufende Programm drei Ströme (siehe Übung): Standard Input, Standard Output und Standard Error. In Java existieren entsprechende Objekte in der System-Klasse: – System.in: ist ein InputStream – System.out und Systen.err sind bereits in einen PrintStream gepackt (statt OutputStream). Defaultmässig ist Standard Input die Tastatur, Standard Output und Error der Monitor. Die Defaults können über das Betriebssystem, aber auch innerhalb eines Programms (z.B. Java) umdefininert werden. Statische Methoden in der System-Klasse und die Default-Belegung: – setIn(InputStream in) – setOut(PrintStream out) – setErr(PrintStream err) Im Programmbeispiel BufferDemo wurde gezeigt, wie ein System.in-Objekt in ein Reader-Objekt und dann in ein BufferedReader-Objekt gepackt werden kann. Bereits bekannt ist die Verwendung der Methoden System.out.print() und System.out.println(). Beispiel für die Umleitung des Ausgabe-Stroms: import java.io.*; public class Redirecting { public static void main (String[] args) throws IOException { BufferedInputStream in = new BufferedInputStream( new FileInputStream( "Redirecting.java")); PrintStream out = new PrintStream ( Informatik B SS 03 99 CharConversionException EOFException java.lang InvalidClassException java.io FileNotFoundException InvalidObjectException Exception IOException InterruptedIOException NotActiveException ObjectStreamException NotSerializableException SyncFailedException OptionalDataException UnsupportedEncodingException StreamCorruptedException UTFDataFormatException WriteAbortedException Abbildung 33: IO-Exceptions new BufferedOutputStream( new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out); // wenn auskommentiert: // Fehlermeldungen auf Monitor BufferedReader br = new BufferedReader( new InputStreamReader(System.in)); String s; while ((s = br.readLine()) != null) System.out.println(s); out.close(); // nicht vergessen! sonst wird nicht geflushed } } 7.6 IO-Exceptions Bei der Ein- und Ausgabe können zahlreiche Exceptions auftreten: Dateien können nicht vorhanden oder nicht zugreifbar sein, Eingaben können vom falschen Typ sein, etc. 7.7 RandomAccess Die Klasse RandomAccessFile ist fast völlig isoliert vom Rest der Klassen in java.io. RandomAccessFile stammt direkt von Object ab und implementiert die Interfaces DataInput und DataOutput. Informatik B SS 03 100 Object RandomAccessFile + read() + seek() + write() <<interface>> DataInput <<interface>> DataOutput Abbildung 34: Die Klasse RandomAccessFile Die Klasse ermöglicht Lesen und Schreiben sowie das Vorwärts- und Rückwärtsgehen in einer Datei. Sie kann nicht in einen BufferedInputStream o.Ä. gepackt werden. Im Prinzip arbeitet die Klasse wie ein kombinierter DataInputStream und DataOutputStream. Typische Anwendung: Effizientes Lesen aus ZIP-Archiv – Sequentieller Zugriff: Öffnen des ZIP-Archivs Sequentielle Suche bis das gewünschte File gefunden ist Extraktion des Files Schliessen des ZIP-Archivs – Aufwand: im Mittel halbe Länge des ZIP-Files – mit Random Access: Öffnen des ZIP-Archivs Suche des dir-entry und lokalisiere den Eintrag für das gewünschte File Suche rückwärts zur Position des Files Extraktion des Files Schliessen des ZIP-Archivs 7.8 Weitere Aspekte von I/O 7.8.1 Tokenizer Manchmal kann es nützlich sein, eine Eingabe in einzelne Tokens (Worte) zu zerlegen. Informatik B SS 03 101 StreamTokenizer führt eine lexikalische Analyse des Eingabestroms durch. (Anwendung: Parser-Konstruktion). whitespaceChars() spezifiziert die Worttrenner (z.B. Leerzeichen), ordinaryChars() spezifiziert die in Worten erlaubten Zeichen. Verwandt zum StreamTokenizer ist die Klasse StringTokenizer aus java.util. Verwendung in späteren Kapiteln. 7.8.2 Serializable, Externalizable Über Streams werden Daten sequentiell übertragen. Objekte sind keine linearen Gebilde Serialisierung (Implementation des Marker-Interface Serializable). Um Objekte (z.B. für verteilte Anwendungen übers Netz) zu verschicken, kann die Serialisierung explizit kontrolliert werden: interface Externalizable extends Serializable. Hierfür müssen dann Methoden readExternal() und writeExternal() implementiert werden. Daten, die nicht serialisiert werden sollen/können, können als transient markiert werden. Solche Felder werden nicht serialisiert/deserialisiert. Genaueres im Zusammenhang mit Netzwerkprogrammierung/Verteilten Anwendungen (Vorlesung Informatik C). 7.8.3 Pipe-Ströme Zur Kommunikation zwischen Threads (siehe Kapitel ‘Multi-Threading’) werden Pipe-Ströme verwendet. Wieder gibt es Klassen für Byte- und Unicode-Verarbeitung. Informatik B SS 03 102 8 Vererbung und Typsicherheit 8.1 Formale Modelle für Programmiersprachen Ein formales Modell dient dazu, einen Gegenstandsbereich (etwa eine Programmiersprache) präzise zu beschreiben. Durch das Auflisten und Beweisen von Eigenschaften werden häufig Aspekte offengelegt, die beim Entwurf übersehen wurden (Lücken, Widersprüche). Kompromiss zwischen Vollständigkeit und Kompaktheit: Beispielsweise ist es kaum möglich, eine komplexe Sprache wie Java voll zu formalisieren. Stattdessen sollten ausgewählte Aspekte formalisiert werden. Im folgenden: Operationale Semantik für einen Java-Kern – “Featherweight Java” (FJ), Arbeit von Igarashi et al., 1999. Grundidee: Es soll gezeigt werden, dass Java (im Kern) typsicher ist. – Die Syntax von Java wird auf FJ reduziert. – Es werden formale Regeln für die Typisierung angegeben. – Es werden Reduktionsregeln für Ausdrücke angegeben. Mittels der Reduktionsregeln wird bewiesen, dass FJ typsicher ist. Motivation: Eine Operationale Semantik beschreibt in Form von Reduktionsregeln, wie ein Ausdruck (expression) einer Programmiersprache zu einem einfacheren Ausdruck ausgewertet wird: Solche Regeln können auch auf Teile eines Ausdrucks angewendet werden: Es werden (in beliebiger Reihenfolge) solange Regeln angewendet, bis keine Regel mehr anwendbar ist. Der Ausdruck ist dann entweder in Normalform (repräsentiert eine “konstanten Wert”) oder die Auswertung ist nicht vollständig möglich (Fehler). Reduktionsregeln entsprechen Termersetzungsregeln (rewrite rules). Ein einfaches Beispiel (Kaffeedosen-Problem) ist in Abb. 35 angegeben. Eine Reihe schwarzer und weisser Bohnen kann verkürzt werden, indem nach festen Aktion Paare benachbarter Bohnen durch Regeln der Form Bedingung eine einzelne Bohne ersetzt werden. Informatik B SS 03 103 Gegeben ist eine Kaffeedose, in der schwarze (S) und weiße (W) Bohnen in einer festen Reihenfolge angeordnet sind, beispielsweise: W W S S W W S S. Gegeben sind folgende Regeln: SW S WS S SS W WWSSWWSS WWSSWSS WWSSSS WSSSS SSSS WSS SS W Das Ziel ist, am Ende möglichst wenige Bohnen zu haben. Die Konfliktlösungs-Strategie sei, immer die oberste anwendbare Regel auszuwählen. Abbildung 35: Lösung des “Kaffeedose” Problems mit einer Menge von Ersetzungsregeln Häufig haben Reduktionsregeln Anwendungsbedingungen: Es muss nicht nur ein bestimmter Unter-Ausdruck in einem Ausdruck existieren, damit eine Regel angewendet werden darf, sondern es müssen noch weitere Bedingungen erfüllt sein. Solche Regeln werden häufig so geschrieben, dass die Anwendungsbedingungen über einer Linie stehen und die eigentliche Regel unter einer Linie: * Lies: Wenn eine Klasse N Felder mit Typen besitzt, dann kann der Ausdruck zu ausgewertet werden. (Zugriff auf das Feld ergibt den Wert des entsprechenden Feldes – unter der Randbedingung, dass der Konstruktor alle Felder mit den übergebenen Werten füllt.) 8.2 Featherweight Java Reduktion von Java auf einen minimalen Kern: Funktionaler Kern und Casting. Vererbung Funktionaler Kern (Seiteneffekt-Freiheit, vgl. Lambda-Kalkül): – Alle Felder und Parameter sind implizit final. – Felder werden im Konstruktor initialisiert und danach nicht mehr verändert. – Jede Methode besteht aus einer einzigen return-Anweisung. Abgedeckt werden alle Aspekte, die für Polymorphismus relevant sind: – Objekt-Erzeugung – Wechselseitig rekursive Klassen-Definitionen – Zugriff auf Felder und Aufruf von Methoden Informatik B SS 03 104 – Überschreiben von Methoden – Unterklassen (subtyping) – Casting Um Regularität in den Klassendefinitionen zu haben, wird zu jeder Klasse ihr Obertyp explizit angegeben (auch wenn es Object ist), wird immer ein Konstruktor explizit definiert, wird immer der Empfänger bei Feld-Zugriffen oder Methoden-Aufrufen angegeben (auch wenn es this ist). 8.2.1 Programmbeispiel class A extends Object { A() { super(); } } class B extends Object { B() { super(); } } class Pair extends Object { Object fst; // first element of a pair Object snd; // second element of a pair Pair(Object fst, Object snd) { super(); this.fst = fst; this.snd = snd; } Pair setfst(Object newfst) { return new Pair(newfst, this.snd); } } Fünf Arten von Ausdrücken: ( sind Platzhalter für Ausdrücke) 1. Object constructors: new A(), new B(), new Pair(e1, e2) 2. Method invocation: e3.setfst(e4) 3. Field access: this.snd 4. Variable: newfst, this (this wird in FJ als Variable aufgefasst) 5. Cast: (Pair) e5 Da (ausser in Konstruktoren) keine Zuweisung erlaubt ist, wird auf das Schlüsselwort final verzichtet: Felder werden nur einmal belegt, Methoden-Parameter werden im Körper nicht manipuliert. Im Kontext der Klassen-Definitionen A, B und Pair können Ausdrücke ausgewertet werden: new Pair(new A(), new B()).setfst(new B()) evaluiert zu new Pair(new B(), new B()) ((Pair) new Pair(new Pair(new A(), new B()), new A()).fst).snd evaluiert zu new B() Dabei bezeichnet new X() ein Objekt vom Typ X. Die Auswertung (Reduktion) wird im folgenden formal gefaßt. Informatik B SS 03 105 ::= class extends ; ) super( ::= ( , ); this. = ; ::= ( ) return ; ::= | . | . ( ) | new ( ) | ( ) // class declarations // constructor declaration // method declaration // expression declaration Platzhalter für Klassennamen: A, B, C, D, E Platzhalter für Felder: f, g Platzhalter für Methodennamen: m Platzhalter für Variablen: x Platzhalter für Ausdrücke: d, e Sequenzen: für etc. (bei Leere Sequenz: , Länge von Sequenzen ohne Kommata), "$# %' & für ! Abbildung 36: Syntax für FJ 8.2.2 Syntax für FJ Die Syntax von FJ kann abstrakt angegeben werden (siehe Abb. 36): hier eine Art EBNF mit Zusatzsyntax. Klassendeklaration: Neue Klasse mit Oberklasse , Felder )(*( (keine primitiven Typen, Instanzvariablen erweitern die Menge der in den Oberklassen deklarierten Variablen – Namen müssen echt verschieden sein, kein shadowing), ein Konstruktor + und Methodendeklarationen , ,.- . Konstruktordeklaration: Es müssen alle Felder der Klasse explizit initialisiert werden, der Konstruktor muss genauso viele Parameter erhalten, wie es Felder gibt. Der Oberklassenkonstruktor muss aufgerufen werden. Methodendeklaration: immer mit Rückgabetyp, Variablen Ausdruck gebunden. / und this sind im 8.2.3 Subtyping Definition einer Klassentabelle CT: Abbildung von Klassennamen Klassendeklarationen 0 . auf Tabelle CT hat als Domain (dom) eine Menge von Klassennamen . Zur Vereinfachung wird jeweils eine feste Tabelle angenommen. Ein Programm ist ein Paar (CT, e) einer Klassentabelle und eines Ausdrucks. (siehe Programmbeispiel weiter oben) Klasse Object wird speziell behandelt: erscheint nicht in CT, hat keine Felder und Methoden (Vereinfachung gegenüber Java) Untertyp-Relationen können über CT ermittelt werden. Untertyp-Regeln sind in Abb. 37 angegeben. Informatik B SS 03 106 class C extends D C <: D C <: C C <: D D <: E C <: E ... // Untertyp // Reflexivität // Transitivität Abbildung 37: Subtyping Regeln für FJ Jede Klassendefinition legt eine direkte Unterklassenbeziehung fest. Die beiden weiteren Regeln ermöglichen es, die reflexive und transitive Hülle der Unterklassenbeziehungen zu ermitteln. CT muss einige Korrektheitsbedingungen (sanity conditions) erfüllen: I*"+-/ – CT(C) = class C ... für jedes – Object *"+-/ . . – Für alle Klassennamen I*"+-/ (ausser Object) in CT, gilt – Es existieren keine Zyklen in der Subtyp-Relation, die durch CT induziert ist, d.h. die Relation <: ist antisymmetrisch. @ Typen dürfen rekursiv (und wechselseitig rekursiv) sein: Die Definition einer Klasse darf Methoden und Instanzvariablen, in denen vorkommt, besitzen. @ 8.2.4 Hilfsfunktionen Zur Definition der Typisierungs- und Reduktionsregeln werden einige Hilfsfunktionen benötigt, die in Abb. 38 angegeben sind. / , : Definition von Methode nicht vor. / kommt in den Methodendeklarationen fields(C) ist eine Sequenz von Paaren Klasse und ihrer Oberklassen. , : Typen und Namen aller Felder der mtype(m, C) liefert die Signatur einer Methode als Resultattyp). C C (Argumenttypen und mbody(m, C) liefert einen Ausdruck und die darin gebundenen Variablen / Beispiel: Der Ausdruck new Pair(newfst, this.snd) enthält die Variablen / newfst, this. < mtype() und mbody() sind partielle Funktionen (undefiniert für Object). Informatik B SS 03 107 Abbildung 38: Hilfsfunktionen für FJ Informatik B SS 03 108 8.3 Typisierung und Reduktion in FJ 8.3.1 Typisierungsregeln Die Typisierungsregeln sind in Abb. 39 angegeben. Sie definieren, unter welchen Bedingungen (über dem Strich angegeben) ein Ausdruck zu einem bestimmten Typ auswertet bzw. ob Klassen und Methoden korrekt definiert wurden. Es gibt Typisierungsregeln für Klassen, Methoden und Ausdrücke. Typisierung erfolgt bzgl. eines Environments , das eine endliche Abbildung von Variablen auf Typen / festlegt. Typisierungs-Aussagen für Ausdrücke haben die Form: Environment hat Ausdruck den Typ ”. , lies “in Es gibt für jede der fünf syntaktischen Arten von Ausdrücken eine Typisierungsregel – nur für Casts gibt es drei Regeln. Für Casts werden Upcast, Downcast und “stupid cast” betrachtet. Der “stupid cast” behandelt Fälle, in denen die Zielklasse bzgl. der Klassenhierarchie unverbunden mit der Klasse des aktuellen Objekts ist. “stupid casts” werden im Formalismus benötigt, weil während der Anwendung von Reduktionsregeln aus zulässigen Casts “stupid casts” entstehen können: (Beispielprogramm von oben) (A) (Object)new B() (A)new B() (Object) new B() wird zunächst zu new B() reduziert: Der Upcast ist zulässig, aber es handelt sich um ein Objekt vom Typ B. Der Cast zur unverbundenen Klasse A ist unzulässig. Die Typisierungsregeln (bis auf “stupid cast”) entsprechen der Semantik der Typisierung in Java. Typisierungs-Aussagen für Methoden-Deklarationen haben die Form M OK IN C – “Methoden-Deklaration M ist ok, wenn sie in Klasse C auftaucht”. Für Overriding gilt: Die Methode der Unterklasse muss denselben Typ haben wie die der Oberklasse. Typisierungs-Aussagen für Klassen-Deklarationen: Es muss gelten, dass der Konstruktoraufruf korrekt ist (Aufruf von super() mit den Feldern der Oberklasse und Initialisierung der Klassenfelder) und dass jede Methodendeklaration ok ist. Über die Hilfsfunktionen kann jede Methode unabhängig von den anderen Methoden geprüft werden. 8.3.2 Reduktionsregeln Die Reduktionsregeln sind (zusammen mit Kongruenzregeln) in Abb. 40 angegeben. Informatik B SS 03 109 Abbildung 39: Typisierungsregeln für FJ Informatik B SS 03 110 Reduktionsregeln beschreiben, wie ein Ausdruck ausgewertet wird (computation). Anwendung einer Reduktionsregel auf einen gegebenen Ausdruck: Prüfen, ob Anwendungsbedingungen (über dem Strich) gelten, Finden eines Unterausdrucks, der mit der linken Seite der Reduktionsregel matched (gleich ist, wenn Variablen entsprechend substitutiert werden können), ersetzen dieses Unterausdrucks durch die rechte Regelseite (in der Variablen durch substituiert wurden). Anmerkungen: Mit Variablen sind hier Platzhalter in den Reduktionsregeln gemeint. Beispielsweise steht für den Namen eines Feldes. Dieses könnnte beispielsweise für die Klasse Pair durch fst substituiert werden und meint dann den Wert dieses Feldes, z.B. new B(). Sie haben die Form (ein Ableitungsschritt). Ganze Ketten von Auswertungen werden mit notiert (reflexive und transitive Hülle) Es gibt drei Reduktionsregeln: für Feldzugriff, für Methodenaufruf und für Casting. * / Variable meint: in Ausdruck werden Variablen durch Ausdruck ersetzt. / durch Ausdrücke * und Die Kongruenzregeln beschreiben, wie mit Teilausdrücken umgegangen wird, z.B.: wenn dann auch . Die Beziehung zwischen Typisierung und Auswertung sollte in Java (FJ) typkorrekt sein. Das heißt, für korrekt typisierte Ausdrücke sollte die Auswertung solcher Ausdrücke wieder typkorrekt sein! Dies kann für FJ formal bewiesen werden: Wenn ein wohltypisierter Term reduziert wird, dann ist das Resultat entweder von einem Untertyp des Typs des Originalterms oder ein Ausdruck, bei dem ein unzulässiger Downcast versucht und abgewiesen wird. Ist keine Regel anwendbar, wird die Berechnung gestoppt (run time error). 8.3.3 Veranschaulichung Die Beweise sind in dem entsprechenden Artikel von Igarashi et al. nachzulesen. Beweis-Idee: Für beliebige Ausdrücke von FJ wird gezeigt, dass die Anwendung der Reduktionsregeln auf korrekt typisierte Ausdrücke dann wieder zu korrekt typisierten Ausdrücken führt, wenn nur upcasting vorkommt. Anstelle der Beweise wird im folgenden veranschaulicht, wie mit dem formalen Kalkül von FJ, das die Semantik der Auswertung von Ausdrücken beschriebt, “gerechnet” werden kann. Informatik B SS 03 111 Abbildung 40: Reduktionsregeln für FJ Informatik B SS 03 112 Feldzugriff: Anwendung von Regel R-FIELD new Pair(new A(), new B()).snd new B() Methoden-Aufruf: Anwendung von Regel R-INVK new Pair(new A(), new B()).setfst(new B()) new B()/newfst new Pair(new A(), new B())/this new Pair(newfst, this.snd) new Pair(new B(), new Pair(new A(), new B()).snd) Casting: ((Pair) new Pair(new Pair(new A(), new B()), new A()).fst).snd ((Pair) new Pair(new A(), new B())).snd new Pair(new A(), new B()).snd new B() Der Adressat des Cast wird zunächst zu einem Objekt (z. B. new A()) reduziert. Wenn dieses Objekt zu einer Unterklasse des Target, gehört wird der Cast entfernt, anderenfalls ergibt sich ein Laufzeitfehler, z. B. für (A) new(B). Drei Möglichkeiten, dass Berechnung scheitert: Versuch, auf ein Feld zuzugreifen, das nicht zur Klasse gehört. Passiert nicht in wohl-typisierten Programmen (Beweis) Versuch, eine Methode aufzurufen, die nicht zur Klasse gehört. (dito) Versuch, etwas zu einer Klasse zu casten, die nicht Oberklasse ist. Passiert nicht in wohl-typisierten Programmen, die keine Downcasts (“narrowing”) enthalten. Informatik B SS 03 113 9 Abstrakte Klassen und Interfaces 9.1 Abstrakte Klassen und Methoden Beispiel Circle (ist inzwischen Teil eines Pakets myshapes2). Erweiterung: Implementation einer Vielzahl verschiedener Shape-Klassen: Circle, Rectangle, Square, Ellipse, Triangle, ... Alle Shape-Klassen sollen area()- und circumference()-Methoden haben. Arbeiten mit einem Array von Shape-Objekten: Es wäre günstig, wenn eine gemeinsame Oberklasse Shape existiert, die alle Komponenten definiert, die allen geometrischen Formen gemeinsam sind. Was ist mit den Methoden area() und circumference()? (Ohne konkrete geometrische Gestalt ist Berechnungsvorschrift unbekannt.) abstrakte Methoden! Abstrakte Methode: Definition ohne Implementation (Körper); Modifikator abstract; Methodenkopf abgeschlossen durch Semikolon. Jede Klasse, die eine abstrakte Methode enthält, ist selbst abstrakt und muss als abstract deklariert werden. Eine abstrakte Klasse kann nicht instantiiert werden (keine Objekt-Erzeugung mit new möglich). Eine Unterklasse einer abstrakten Klasse kann nur instantiiert werden, wenn alle abstrakten Methoden der Oberklasse implementiert werden. “konkrete” Unterklasse Eine Unterklasse, die nicht alle abstrakten Methoden implementiert, ist selbst abstrakt. static- und final-Methoden können nicht abstrakt sein, da diese nicht von einer Unterklasse überschrieben werden können. private Methoden sind implizit final. Ebenso können final Klassen keine abstrakten Methoden enthalten. Klassen können abstract deklariert werden, auch wenn sie keine abstrakten Methoden enthalten. Hinweis, dass Methoden unvollständig sind und dass die Methode als Oberklasse für konkrete Unterklassen gedacht ist. (Abstrakte Klassen können generell nicht instantiiert werden.) Objekte von Unterklassen können direkt (ohne Cast) an Variablen (z. B. Elemente eines Arrays von Shapes) der Oberklasse zugewiesen werden. Abstrakte Methoden der Oberklasse können für jedes Objekt einer konkreten Unterklasse aufgerufen werden (dynamic method lookup). Informatik B SS 03 114 public abstract class Shape { public abstract double area(); // Abstract methods: note public abstract double circumference(); // semicolon instead of body } public class Circle extends Shape { public static final double PI = 3.14159265358979323846; protected double r; // Radius is hidden, but visible to subclasses < code omitted ... > // Methods to operate on the instance field // Implementation of abstract shape methods public double area() { return PI * r * r; } public double circumference() { return 2 * PI * r; } } public class Rectangle extends Shape { protected double w, h; // Instance fields // width and height < code omitted ... > // Instance methods // implementation of abstract methods public double area() { return w * h; } public double circumference() { return 2 * (w + h); } } Shape[] shapes = new Shape[3]; // Create an Array to hold shapes shapes[0] = new Circle(2.0); // Fill in the array shapes[1] = new Rectangle(1.0, 3.0); shapes[2] = new Rectangle(4.0, 2.0); double total_area = 0; for (int i = 0; i < shapes.length; i++) total_area += shapes[i].area(); // Compute area of shapes 9.2 Interfaces Nächste Erweiterung des Shapes-Beispiels: Nicht nur Grösse, auch Position der geometrischen Objekte in der Ebene. Erste Idee: weitere abstrakte Klasse CenteredShape mit Unterklassen CenteredCircle, CenteredRectangle, ... CenteredCircle soll natürlich auch die Methoden von Circle erben. Problem: Java erlaubt nicht, dass eine Klasse mehr als eine Oberklasse hat! (Mehrfachvererbung) Informatik B SS 03 115 Java Lösung: Interfaces (Schnittstellen) Eine Klasse kann beliebig viele Interfaces implementieren. Ein Interface ist ein Referenztyp sehr ähnlich einer Klasse: Definiert wird eine Funktionalität und nicht eine Implementation (Realisierung), ein Interface gibt Signaturen – Namen und Typen von Methoden (und Konstanten) – vor. Ein Interface wird mit dem Schlüsselwort interface deklariert. Ein Interface enthält keinerlei Methoden-Implementation. Alle Methoden sind implizit abstrakt, auch wenn ohne diesen Modifikator deklariert. Ein Interface kann nur Instanz-Methoden enthalten. Ein Interface ist ohne Sichtbarkeitsmodifikator paketsichtbar. Als einziger Sichtbarkeitsmodifikator darf public angegeben werden. Alle Methoden sind implizit public, auch wenn der Modifikator nicht explizit angegeben ist. Es ist ein Fehler, protected oder private Methoden in einem Interface zu deklarieren! Ein Interface kann keine Instanz-Felder definieren, aber als static und final deklarierte Konstanten. Da ein Interface nicht instantiiert werden kann, definiert es keinen Konstruktor. Interfaces sind reine Spezifikationen! 9.2.1 Implementation eines Interfaces Unterklasse extends Oberklasse Klasse implements Interface Schlüsselwort implements folgt nach extends (falls Oberklasse angegeben); wird von einem oder mehreren (durch Komma getrennte) Namen von Interfaces gefolgt. implements bedeutet, dass in der implementierenden Klasse die Körper für Methoden des Interfaces definiert werden. Werden nicht alle Methoden implementiert, so ist die Klasse abstrakt und muss als solche deklariert werden. (Alle Methoden des Interfaces werden Teil der Klasse.) public interface Centered { public void setCenter(double x, double y); public double getCenterX(); public double getCenterY(); } public class CenteredRectangle extends Rectangle implements Centered { private double cx, cy; // New instance fields public CenteredRectangle(double cx, double cy, double w, double h) { super(w, h); Informatik B SS 03 116 this.cx = cx; this.cy = cy; } // We inherit all the methods of Rectangle, but must // provide implementations of all the Centered methods. public void setCenter(double x, double y) { cx = x; cy = y; } public double getCenterX() { return cx; } public double getCenterY() { return cy; } } 9.2.2 Interfaces und Konstanten Konstanten dürfen in Interface-Deklarationen vorkommen. Alle Felder, die in einem Interface deklariert werden, werden implizit als static und final aufgefasst, auch wenn nicht explizit so deklariert. Es ist jedoch guter Stil, diese Modifikatoren explizit anzugeben! Jede Klasse, die das Interface implementiert, erbt die Konstanten und kann sie benutzen, als wären sie direkt in der Klasse selbst deklariert (keine Voranstellung des Interface-Namens vor den Konstanten-Namen notwendig). Konstanten müssen nicht unbedingt mit festen Werten initialisiert werden: public interface RandVals { int rint = (int) (Math.random() * 10); long rlong = (long) (Math.random() * 10); float rfloat = (float) (Math.random() * 10); double rdouble = Math.random() * 10; } Manchmal nützlich: Interface, das nur Konstanten enthält. Konstanten, die von mehreren Klassen benutzt werden (wie Port-Nummern, die von Client und Server benutzt werden). Beispiel: java.io.ObjectStreamConstants (Konstanten für Javas Serialisierungs-Mechanismus) 9.2.3 Benutzung von Interfaces Wenn eine Klasse ein Interface implementiert, können Objekte dieser Klasse an eine Variable vom Typ des Interfaces zugewiesen werden. Object instanceof Interface/Klasse liefert Wahrheitswert. Shape[] shapes = new Shape[3]; // Create an array to hold shapes // Create some centered shapes, and store them in the Shape[] Informatik B SS 03 Shape 117 Circle CenteredCircle Rectangle CenteredRectangle Square CenteredSquare <<interface>> Centered Abbildung 41: Struktur der Shape-Klassen // No cast necessary shapes[0] = new CenteredCircle(1.0, 1.0, 1.0); shapes[1] = new CenteredSquare(2.5, 2, 3); shapes[2] = new CenteredRectangle(2.3, 4.5, 3, 4); // Compute average area of the shapes and average distance from the origin double totalArea = 0; double totalDistance = 0; for (int i = 0; i < shapes.length; i++) { totalArea += shapes[i].area(); // Compute the area of the shapes if (shapes[i] instanceof Centered) { // The shape is a Centered shape // Note the required cast from Shape to Centered // No cast would be required to go from CenteredSquare to Centered etc. Centered c = (Centered) shapes[i]; // Assign it to a Centered variable double cx = c.getCenterX(); double cy = c.getCenterY(); totalDistance += Math.sqrt(cx*cx + cy*cy); } } System.out.println("Average area: " + totalArea/shapes.length); System.out.println("Average distance: " + totalDistance/shapes.length); 9.2.4 Interface vs. Abstrakte Klasse Entwurfsentscheidung zwischen abstrakter Klasse und Interface. Interface: Jede Klasse kann es implementieren. Zwei nicht verwandte Klassen können dasselbe Interface implementieren. Abstrakte Klasse: Nur eine Klasse kann Oberklasse einer anderen Klasse sein. Interface: Nur abstrakte Methoden; wenn Methoden für viele Klassen gleich sind, so müssen sie immer neu implementiert werden. Informatik B SS 03 118 Abstrakte Klasse: Kann Default-Implementation für typische Methoden liefern. Kompatibilität: Wenn Interface zum public API hinzugefügt wird und später das Interface um eine Methode erweitert wird, so sind alle Klassen, die das Interface implementieren, “kaputt”. Zu abstrakten Klassen können implementierte Methoden gefahrlos hinzugefügt werden. Manchmal nützlich: Abstrakte Klasse, die ein Interface implementiert und Default-Implementationen für Methoden der Unterklassen liefert. (Adapter-Klasse) Alternativ: Delegate-Pattern (Support-Klasse), die Methoden eines Interface implementieren. Eigene Klasse, die das Interface implementiert, kann als Komponente ein Objekt einer Support-Klasse (die dasselbe Interface implementiert) erzeugen (siehe Vorlesung Infomatik C). // Here is a basic interface. It represents a shape that fits inside // a rectangular bounding box. Any class that wants to serve as a // RectangularShape can implement these methods from scratch. public interface RectangularShape { public void setSize(double width, double height); public void setPosition(double x, double y); public void translate(double dx, double dy); public double area(); } // Here is a partial implementation of that interface. // Many implementations may find this a useful starting point. public abstract class AbstractRectangularShape implements RectangularShape { // The position and size of the shape protected double x, y, w, h; // Default implementations of some of the interface methods public void setSize(double width, double height) { w = width; h = height; } public void setPosition(double x, double y) { this.x = x; this.y =y; } public void translate(double dx, double dy) { x += dx; y += dy; } } 9.2.5 Implementation mehrerer Interfaces und Erweitern von Interfaces Wenn die Klasse, die mehrere Interfaces implementiert, nicht abstrakt sein soll, so müssen die Methoden aller Interfaces implementiert werden. public class SuperDuperSquare extends Shape implements Centered, Scalable { // Methods omitted } Informatik B SS 03 119 Wie Klassen Unterklassen haben können, so können Interfaces Unter-Interfaces haben. Bei Interfaces dürfen hinter extends mehrere andere Interfaces stehen. public interface Positionable extends Centered { public void setUpperRightCorner(double x, double y); public double getUpperRightX(); public double getUpperRightY(); } public interface Transformable extends Scalable, Translatable, Rotatable {} public interface SuperShape extends Positionable, Transformable {} Hier kann man sich das Problem der Mehrfachvererbung teilweise doch einhandeln. Es gelten folgende Regeln, wenn Methoden gleichen Namens in verschiedenen (zu implementierenden, zu erweiternden) Interfaces deklariert werden: – Methoden mit gleichem Namen und gleicher Signatur werden einmal aufgenommen. – Methoden mit gleichem Namen und verschiedenen Signaturen sind überladen. – Methoden mit gleichem Namen, gleichen Parametern und verschiedenem Rückgabetyp führen zu Übersetungs-Fehler. – Bei Methoden mit gleicher Signatur und verschiedenen spezifizierten Exceptions muss die “Schnittmenge” dieser Exceptions oder eine Teilmenge davon spezifiziert werden. interface X { void setup() throws SomeException; } interface Y { void setup(); } class Z implements X, Y { public void setup() { // ... } } Folgender Code führt zu einem Übersetzungs-Fehler: interface X { Informatik B SS 03 120 void setup() throws FileNotFoundException; } interface Y { void setup() throws IOException; } // Schnittmenge ist FileNotFoundException class Z implements X, Y { public void setup() throws IOException { // ... } } 9.2.6 Marker-Interfaces Manchmal ist es nützlich, ein leeres Interface zu definieren. Klasse, die dieses Interface implementiert, muss keine Methoden implementieren. Jede Instanz der Klasse ist zulässige “Instanz” des Interfaces. Prüfbar mit instanceof Beispiele: Cloneable und java.io.Serializable MyClass o; // Initialized elsewhere MyClass copy; if (o instanceof Cloneable) copy = o.clone(); else copy = null; 9.3 Das Enumeration-Interface Idee: Für eine Datenstruktur, die Elemente hält, sollen diese Elemente aufgezählt werden. Typisch für Collection-Klassen (siehe Kapitel ‘Collection-Klassen’), in denen Objekte gehalten werden. Beispiele: Stack, LinkedList Vordefiniertes Enumeration-Interface mit zwei Methoden: – boolean hasMoreElements(): true, wenn die Aufzählung noch weitere Elemente enthält, false sonst – Object nextElement(): liefert das nächste Element, falls es existiert, sonst wird eine Exception ausgelöst. Beispiel: Collection ist einfache Array-Liste Objekte werden in einem Array abgelegt (Methode add()) Informatik B SS 03 121 Wenn der Array gefüllt ist, werden keine weiteren Elemente mehr angenommen. Es fehlen Methoden zum Löschen, zum Prüfen, ob ein Element enthalten ist (mit entsprechender equals()-Methode), evtl. eine Methode zum Vergrößern des Arrays, etc. public class MyList { public static final int MAX = 100; protected Object[] list = new Object[MAX]; protected int numOfEls = 0; public boolean add(Object o) { if (numOfEls >= MAX) return false; else { list[numOfEls++] = o; return true; } } public Object get (int i) { return list[i]; } public int size() { return numOfEls; } } Eine unelegante Art, den Inhalt einer Liste aufzuzählen, ist es, mit einer Schleife über die Liste zu laufen: MyList intlist = new MyList(); for (int i=0; i < 10; i++) { intlist.add(new Integer(i)); } // enumerate elements by index for (int i=0; i < intlist.size(); i++) { System.out.println(intlist.get(i)); Aufzählen über Index geht nur für Collection-Klassen, die Index-Zugriff erlauben. Alternativ: Erzeugen eines Enumerators für MyList-Objekte. Ein Enumerator für MyList muss das Enumeration-Interface implementieren: Informatik B SS 03 122 public class ListEnumerator implements Enumeration { protected int current = 0; protected MyList collection; public ListEnumerator(MyList c) { collection = c; } public boolean hasMoreElements() { return current < collection.numOfEls; } public Object nextElement() { if (! hasMoreElements()) throw new java.util.NoSuchElementException(); return collection.list[current++]; } } Jetzt kann der Inhalt der Liste folgendermaßen ausgegeben werdenn: ListEnumerator enum = new ListEnumerator(intlist); while (enum.hasMoreElements()) System.out.println(enum.nextElement()); Informatik B SS 03 123 10 Innere Klassen Bisher: “top-level” Klassen (direkte Mitglieder eines Pakets). Seit Java 1.1: Innere Klassen: definiert innerhalb einer anderen Klasse (Komponente einer Klasse, ähnlich Felder und Methoden) Vier Arten von inneren Klassen: – Member Classes (“echte” innere Klasse) – Static Member Classes (Nested Top-Level Classes) – Local Classes – Anonymous Classes 10.1 Member Classes 10.1.1 Anschauliches Beispiel Ein Auto besteht aus vielen Teilen – Motor, Gangschaltung, Auspuff, etc. Manche Teile bilden sinnvollerweise eine eigene Klasse, aber können dennoch nicht unabhängig vom Auto existieren. Beispiel: Klimaanlage – Interaktion zwischen Klimaanlage und Auto ist notwendig. – Leistung der Klimaanlage ist abhängig von Geschwindigkeit des Autos. Je langsamer das Auto fährt, desto mehr Energie muss die Klimaanlage zum Kühlen aufbringen. Lösung: Member Klasse /** The general AirConditioner class */ class AirConditioner { ... public float getTemperatureMin() { }; public float getTemperatureMax() { }; } /** The Automobile class which has an inner class which is an AC */ class Automobile { private Engine engine; private GearBox gearBox; ... private class AutoAirConditioner extends AirConditioner { private float default = ...; private float factor = ...; ... public float getTargetTemperature () { Informatik B SS 03 124 float temperature = default - factor * engine.getSpeed(); ... } } public AirConditioner getAirConditioner () { return new AutoAirConditioner(); } } Der AutoAirConditioner ist abhängig von Parametern eines bestimmten Autos. Beachte: Ein Objekt der inneren Klasse kann nur zusammen mit einem Objekt der umschließenden Klasse existieren. Erst neues Automobil erzeugen, dann die AutoAirCondition! Es ist nicht möglich, ein Objekt vom Typ AutoAirCondition zu erzeugen, ohne dass ein Auto, zu dem diese Klimaanlage gehört existiert. Innere Klasse ist privat: – Andere Klassen/Objekte können nur auf das öffentliche Inferface (AirConditioner) zugreifen. – Die umschließende Klasse hat eine Methode, um ein “Handle” (Referenz) auf die öffentlichen Teile des Objekts der inneren Klasse zu liefern (getAirConditioner). – Beachte: return new AutoAirConditioner() ist explizit: return this.new AutoAirConditioner() Alternatives Beispiel: Organe können nicht ohne Körper existieren. Weitere Beispiele: Enumerator (bzw. Iterator) Im Kontext von Auto: z.B. Aufzählen aller Schrauben (mit unterschiedlichen Typen, aber gemeinsamer Oberklasse), um z.B. mittlere Größe, mittlere Kosten zu bestimmen. 10.1.2 Beispiel ‘Enumerator’ public class MyListMC { public static final int MAX = 100; protected Object[] list = new Object[MAX]; protected int numOfEls = 0; public boolean add(Object o) { if (numOfEls >= MAX) return false; else { list[numOfEls++] = o; Informatik B SS 03 125 return true; } } public int size() { return numOfEls; } public java.util.Enumeration enumerate() { return new Enumerator(); } protected class Enumerator implements java.util.Enumeration { protected int current = 0; // constructor not necessary public Enumerator() { current = 0; } public boolean hasMoreElements() { return current < numOfEls; } public Object nextElement() { if (! hasMoreElements()) throw new java.util.NoSuchElementException(); return list[current++]; } } } Erzeugen und Anwenden des Enumerators: MyListMC intlist = new MyListMC(); for (int i=0; i < 10; i++) { intlist.add(new Integer(i)); } java.util.Enumeration enum = intlist.enumerate(); while (enum.hasMoreElements()) System.out.println(enum.nextElement()); 10.1.3 Eigenschaften von Member-Klassen Member-Klassen sind die typischen, “echten” inneren Klassen. Member-Klassen sind wie Instanz-Felder und -Methoden mit einer Instanz der Klasse, in der sie definiert sind, assoziiert. Also: Zugriff auf alle Komponenten der umschliessenden Klasse. Member-Klassen können beliebig tief geschachtelt werden. D. h., eine innere Klasse kann weitere innere Klassen enthalten. Informatik B SS 03 126 Eine Member-Klasse kann mit allen Sichtbarkeits-Modifikatoren deklariert werden. Name muss verschieden vom Namen der umschliessenden Klasse sein. Member-Klassen dürfen keine statischen Komponenten enthalten. Ausnahme: static und final deklarierte Konstanten. Interfaces können nicht als Member-Klassen definiert werden, da Interfaces keine Instanz-Variablen besitzen dürfen (also kein this-Verweis möglich). Wichtigstes Merkmal: Zugriff auf Instanz-Felder und -Methoden der umschliessenden Klasse. current < numOfEls Wie funktioniert explizite Referenz? this.current < this.numOfEls Problem: this.numOfEls ist nicht zulässig (this bezieht sich auf Enumerator-Objekt) Erweiterte Syntax: this.current < MyListMC.this.numOfEls Diese Zugriffsform ist dann notwendig, wenn man sich auf eine Komponente einer äusseren Klasse beziehen will, die denselben Namen hat wie eine Komponente der inneren Klasse. Analoge Erweiterung der super-Syntax (Zugriff auf eine überdeckte oder überschriebene Komponente der Oberklasse der umschliessenden Klasse): Klassenname .super. feld Klassenname .super. methode Ausführung des Member-Klassen Konstruktors bewirkt, dass die neue Instanz mit dem this Objekt der umschliessenden Klasse assoziiert wird. Gleichbedeutende Schreibweisen: public Enumeration enumerate() public Enumeration enumerate() return new Enumerator(); return this.new Enumerator(); Anstelle der Definition von enumerator() könnte eine Enumeration auch so erzeugt werden: MyListMC intlist = new MyListMC(); // Create empty list Enumeration enum = intlist.new Enumerator(); // Create Enum for it Da die umschliessende Instanz implizit den Namen der umschliessenden Klasse spezifiziert, ist die explizite Angabe der Klasse ein Syntaxfehler: Enumeration e = intlist.new MyListMC.Enumerator(); // Syntax error 10.1.4 Implementation von Member-Klassen Innere Klassen seit Java 1.1. Informatik B SS 03 127 Erweiterung der Sprache (“syntactic sugar”) aber nicht der JVM: Java Compiler wandelt Repräsentation von inneren Klassen entsprechend um. (Disassemblierung mit javap, um zu sehen, welche Tricks der Compiler benutzt.) Compilation in eigene top-level-Datei. Compiler muss Code so manipulieren, dass Zugriff auf Komponenten zwischen innerer und äusserer Klasse funktioniert. this$0 Feld für jede Member-Klasse (Assoziation mit Instanz der umschliessenden Klasse; Abspeichern der entsprechenden Referenz). Für weitere Referenzen zu umschliessenden Klassen wird entsprechend weitergezählt (this$1, etc.). Jeder Member-Klassen Konstruktor erhält einen zusätzlichen Parameter, um dieses Feld zu initialisieren. protected Member-Klassen werden public; private Member-Klassen werden default-sichtbar. 10.1.5 Member-Klassen und Vererbung Es ist erlaubt, dass eine top-level Klasse als Unterklasse einer Member-Klasse definiert wird. Damit hat die Unterklasse keine umschliessende Klasse, aber ihre Oberklase! Wegen unklarer Semantik argumentieren einige dafür, dass diese Art der Vererbung verboten werden soll. Atsushi Igarashi and Benjamin C. Pierce (2001). On inner classes. Information and Control. Die Autoren haben bei der Definition einer Reduktions-Semantik für innere Klassen und Vererbung Unterspezifikationen der Sprache Java aufgedeckt. // A top-level class that extends a member class class SpecialEnumerator extends MyListMC.Enumerator { // The constructor must explicitely specify a containing instance // when invoking the superclass constructor public SpecialEnumerator(MyListMC l) { l.super(); } // Rest of class omitted } (Igarashi and Pierce, 2001) class C { void who(){ System.out.println("I’m a C object"); } class D extends C { void m(){ C.this.who(); } void who(){ System.out.println("I’m a C.D object"); } Informatik B SS 03 128 } public static void main(String[] args){ new C().new D().m(); } } Qualified this: Für eine innere Klasse C1.C2...Ci...Cn denotiert Ci.this die -te direkt umschliessende Instanz; aber was ist, wenn Ci Superklasse von Cn ist? Compiliert mit JDK 1.1.7: I’m a C.D object (Compilerfehler), compiliert mit JDK 1.2.2: I’m a C object Zwei hierarchische Strukturen: Klassenhierarchie und Enthaltensein-Hierarchie (Containment) Es können Namenskonflikte zwischen vererbten Komponenten (Oberklasse) und Komponenten der umschliessenden Klasse auftreten. class A { int x; } class B { int x; class C extends A { x; // inherited field this.x; // inherited field B.this.x; // field of containing class } } 10.2 Static Member Classes 10.2.1 Anschauliches Beispiel Ein Autoradio gehört als Teil zum Auto, seine Eigenschaften sind aber unabhängig vom Auto selbst. Autoradios sind Objekte, die unabhängig von einem konkreten Auto existieren können: diese Klassen benötigen keinen Zugriff auf Instanz-Felder und/oder -Methoden der Automobil-Klasse. Statische innere Klassen dienen vor allem der Strukturierung von Programmcode. interface Radio { void setVolume (); ... Informatik B SS 03 129 } /** The Automobile class which has a static inner class */ class Automobile { private Engine engine; private GearBox gearBox; ..... private static class AutoRadio extends Radio { private int channel = ...; private float volume = ...; ... public void setVolume () { ... } } public Radio getRadio () { return new AutoRadio(); } } 10.2.2 Eigenschaften von Static Member Classes Während Member-Klassen analog zu Instanz-Feldern und -Methoden zu sehen sind, sind static member classes ähnlich wie Klassen-Felder und -Methoden zu verstehen. (“class class”) Sie haben Zugriff auf alle statischen Komponenten der umschliessenden Klasse. Static member classes werden auch als nested top-level classes bezeichnet. Interfaces dürfen nur als static members definiert werden. Static Klassen können in einem Interface deklariert werden. Static member classes (und Interfaces) werden wie top-level Klassen behandelt. Sie sind nicht mit einer Instanz der umschliessenden Klasse assoziiert (also: kein umschliessendes this-Objekt). Deklaration mit Zugriffsmodifikator genau wie für andere Komponenten. Name muss verschieden vom Namen der umschliessenden Klasse sein. (unqualifizierter) Zugriff auf alle (auch privaten) statischen Komponenten der umschliessenden Klasse (inklusiver weiterer static member classes). Methoden der umschliessenden Klasse haben Zugriff auf alle Komponenten der Member-Klasse. Zugriff von externen Klassen: mit qualifiziertem Namen. Automobile.AutoRadio Vorteil: Strukturierung, paket-ähnliche Organisation für Klassen innerhalb einer Datei. Informatik B SS 03 130 Merke: Member-Klassen (“echte” wie static deklarierte) sollen immer dann verwendet werden, wenn eine Referenz “von innen nach aussen” benötigt wird. Auf der Modellierungsebene heisst das: Ein Objekt ist aus anderen (inneren) Objekten aufgebaut, es kann nicht ohne diese inneren Objekte existieren, und die inneren Objekte benötigen Information über das umschliessende Objekt. Bei Member-Klassen werden Informationen der umschliessenden Instanz benötigt. Bei static deklarierten inneren Klassen werden statische bzw. keine Komponenten der umschliessenden Klasse benötigt, aber die umschliessene Klasse kann auf Komponenten der inneren Klasse zugreifen. 10.2.3 Implementation von statischen Member-Klassen Compiler generiert zwei Klassen-Dateien, z.B. Automobile.class und Automobile$AutoRadio.class (Innere Klasse AutoRadio wird zu top-level Klasse). Compiler qualifiziert Ausdrücke, die auf statische Komponenten der umschliessenden Klasse zugreifen, mit dem Klassennamen. Da auch auf private Komponenten zugegriffen werden darf: Automatische Generierung von nicht-privaten Zugriffsmethoden (mit Default-Zugriffsrechten, paket-weit) und Umwandlung der entsprechenden Ausdrücke. 10.3 Lokale Klassen 10.3.1 Anschauliches Beispiel Alternative Modellierungsidee für die Automobil-Klasse Da die Klasse AutoAirConditioner nur einmal, innerhalb der Methode getAutoAirConditioner(), benötigt wird, kann die Klasse lokal definiert werden. /** The general AirConditioner class */ class AirConditioner { ... public float getTemperatureMin() { }; public float getTemperatureMax() { }; } /** The Automobile class which has an inner class which is an AC */ class Automobile { private Engine engine; private GearBox gearBox; ... public AirConditioner getAirConditioner () { class AutoAirConditioner extends AirConditioner { private float default = ...; Informatik B SS 03 131 private float factor = ...; ... public float getTargetTemperature () { float temperature = default - factor * engine.getSpeed(); ... } } return new AutoAirConditioner(); } } 10.3.2 Beispiel: ‘Enumerator’ als lokale Klasse public class MyListLC { public static final int MAX = 100; protected Object[] list = new Object[MAX]; protected int numOfEls = 0; public boolean add(Object o) { if (numOfEls >= MAX) return false; else { list[numOfEls++] = o; return true; } } public int size() { return numOfEls; } public java.util.Enumeration enumerate() { class Enumerator implements java.util.Enumeration { protected int current = 0; public boolean hasMoreElements() { return current < numOfEls; } public Object nextElement() { if (! hasMoreElements()) throw new java.util.NoSuchElementException(); return list[current++]; } } return new Enumerator(); } } Informatik B SS 03 132 10.3.3 Eigenschaften Lokaler Klassen Nicht Komponente einer Klasse, sondern innerhalb eines Blocks definiert. Typischerweise innerhalb einer Methode, auch innerhalb von Initialisierungsblöcken. Analogie: Lokale Variable – lokale Klasse; Instanz-Feld – Member-Klasse Geltungsbereich: Innerhalb des Blocks Java ist eine lexically scoped Sprache: Geltungsbereich von Variablen ist durch ihre Position im Code definiert: innerhalb der geschweiften Klammern, in die sie eingeschlossen sind. Wenn eine Member-Klasse nur innerhalb einer einzigen Methode der umschliessenden Klasse genutzt wird, kann sie ebenso gut als lokale Klasse definiert werden. Name muss verschieden vom Namen der umschliessenden Klasse sein. Interfaces können nicht lokal deklariert werden. Wie Member-Klassen: Zugriff auf alle Komponenten der umschliessenden Klasse. Zusätzlich auf alle im Block sichtbaren final Parameter und Variablen. Keine Sichtbarkeits-Modifikatoren erlaubt. Keine statischen Felder erlaubt (Ausnahme: static und final deklarierte Konstanten. (wie Member-Klassen) Zugriff auf sichtbare Variablen und Parameter nur, wenn diese final deklariert sind, weil die Lebensdauer einer Instanz einer lokalen Klasse länger sein kann als die Ausführung der Methode, in der sie definiert ist. D.h., die lokale Klasse benötigt eine private Kopie aller lokalen Variablen, die sie verwendet (automatisch vom Compiler erzeugt). Einzige Möglichkeit, Konsistenz zu garantieren (lokale Variablen und deren Kopie bleiben identisch): final. public class A { int f() { int i = 5; class B { int j = i; } i = i - 1; return new B().j; } public static void main (String[] args) { A a = new A(); int value1 = a.f(); int value2 = a.f(); // should be same value ! } } Informatik B SS 03 133 Erweiterung der Java-Syntax: final-Modifikator darf nicht nur für lokale Variablen, sondern auch für Parameter von Methoden und Exception Parameter im catch-Statement angegeben werden. Wie Member-Klassen haben lokale Klassen Zugriff auf die Instanz der umschliessenden Klasse, falls sie nicht in einer Klassenmethode vereinbart werden (qualifiziertes this um auf Komponenten der umschliessenden Klasse zuzugreifen). 10.3.4 Geltungsbereich Lokaler Klassen class A { protected char a = ’a’; } class B { protected char b = ’b’; } public class C extends A { private char c = ’c’; // visible to local class public static char d = ’d’; public void createLocalObject(final char e) { final char f = ’f’; int i = 0; // not final, not usable by local class class Local extends B { char g = ’g’; public void printVars(){ // All of these fields and variable are accessible System.out.println(g); // this.g, field of Local System.out.println(f); // final local variable System.out.println(e); // final local parameter System.out.println(d); // C.this.d, field of containing class System.out.println(c); // C.this.c System.out.println(b); // inherited by Local System.out.println(a); // inherited by containing class } } Local l = new Local(); // Create instance of Local l.printVars(); // call its method } } public class Weird { // A static member interface used below public static interface IntHolder { public int getValue(); } public static void main(String[] args) { IntHolder[] holders = new IntHolder[10]; // for (int i = 0; i < 10; i++) { // final int fi = i; // final local var, a class MyIntHolder implements IntHolder { public int getValue() { return fi; } An array to hold 10 objs Loop to fill array new one for each iteration // local class Informatik B SS 03 134 } holders[i] = new MyIntHolder(); // Instantiate local class } // The local class is now out of scope, so we can’t use its name. // But we’ve got 10 valid instances of that class in our array. // The local variable fi is not in our scope here, but each of the // 10 objects still has access to its local copy for use in // the getValue() method. // So call getValue() for each object and print it out. // This prints the digits 0 to 9. for(int i = 0; i < 10; i++) System.out.println(holders[i].getValue()); } } Die Klasse Test kann nicht kompiliert werden: Die lokale Variable value in foo() ist nicht final deklariert. Veranschaulichung für mögliche Inkonsistenz: Die beiden in der lokalen Klasse Local erzeugten Objekte benutzen die gleiche lokale Variable value. Nach Verlassen der Methode foo() existiert diese Variable nicht mehr. Jedes Local Objekt hat eine lokale Kopie. Wenn der Wert einer solchen Variable verändert werden dürfte, könnte diese Variable bei verschiedenen Objekten verschiedene Werte annehmen! public class Test { public static interface Value { public int getValue (); public void setValue (int i); } public Value[] foo () { int value = 0; class Local implements Value { public int getValue () { return value; } public void setValue (int i) { value = i; } }; return new Value[] { new Local(), new Local() }; } public static void main (String args[]) { Informatik B SS 03 135 Test test = new Test(); Value[] v = test.foo(); // two value objects v[0].setValue(42); v[1].setValue(24); System.out.println(v[0].getValue()); // ??? } } 10.4 Anonyme Klassen Namenlose lokale Klassen Kombination der Syntax von Klassen-Definition und Objekt-Erzeugung wie lokale Klassen innerhalb eines Ausdrucks definiert. 10.4.1 Beispiel: ‘Enumerator’ als anonyme Klasse public java.util.Enumeration enumerate() { // The anonymous class is defined as part of the // return statement return new java.util.Enumeration() { protected int current = 0; public boolean hasMoreElements() { return current < numOfEls; } public Object nextElement() { if (! hasMoreElements()) throw new java.util.NoSuchElementException(); return list[current++]; } }; // semicolon required to finish return statement } 10.4.2 Eigenschaften von Anonymen Klassen Lokale Klasse ohne Namen. Definition und Instantiierung in einem einzigen Ausdruck (new Operator). Zwei Formen: new class-name ( [ argument-list ] ) class-body Konstruktoraufruf der Oberklasse (evtl. auch Default-Konstruktor), Erzeugung eines Objekts der anonymen Unterklasse. new interface-name ( ) class-body Informatik B SS 03 136 Default-Konstruktoraufruf für ein Interface, Erzeugung einer anonymen Unterklasse von Object, die das Interface implementiert. Lokale Klasse ist Anweisung in einem Block, anonyme Klasse Ausdruck als Teil eines grösseren Ausdrucks (z.B. Methodenaufruf). Verwendung: lokale Klasse, die nur einmal benutzt wird. (Definition und Nutzung genau dort, wo verwendet; weniger “clutter” im Code) Typische Anwendung: Implementation von Adapter-Klassen. Definition von Code, der von anderen Objekten aufgerufen wird. Beschränkungen wie für lokale Klassen: keine statischen Komponenten, ausser static final Konstanten; nicht als public, private, protected, static deklarierbar. Da namenlos: keine Konstruktor-Definition möglich. Erbt – ausnahmsweise – die Konstruktoren der Oberklasse. Im Fall eines Interfaces wird ein Default-Konstruktor eingefügt. Wenn eigener/anderer Konstruktor notwendig, als lokale Klasse definieren. Alternative: Instanz-Initialisierer (Initialisierungsblöcke für Instanzen wurden genau für anonyme Klassen eingeführt), Initialisierungsblock wird in die geerbten Konstruktoren/den Default-Konstruktor eingefügt. 10.4.3 Implementation von Lokalen und Anonymen Klassen Zusätzlich zu den Zugriffsrechten von Member-Klassen, Zugriff auf final deklarierte lokale Variablen im Geltungsbereich des Blocks, in dem sie definiert sind. Compiler gibt der inneren Klasse private Instanzfelder, um Kopien der lokalen Variablen zu halten. Compiler fügt versteckte Parameter für jeden Konstruktor einer lokalen Klasse ein, um diese private Felder zu initalisieren. Lokale Klasse hat nicht wirklich Zugriff auf die lokalen Variablen, sondern auf eine private Kopie dieser Variablen. final garantiert Konsistenz! “Hoch”-Compilation von anonymen Klassen: Vergabe von Nummern, z.B. MyListAC$1.class. 10.4.4 Adapter-Klassen als Anonyme Klassen File f = new File("/src"); // The directory to list // Now call the list() method with a single FilenameFilter argument // Define and instantiate an anonymous implementation of FilenameFilter // as part of the method invocation expression. String[] filelist = f.list(new FilenameFilter() { public boolean accept(File f, String s) {return s.endsWith(".java"); } Informatik B SS 03 137 }); // Don’t forget the parenthesis and semicolon that end the method call! Methode list() aus java.io.File hat als Argument ein Objekt vom Typ des Interfaces FilenameFilter, das die Dateinamen des zu listenden Verzeichnisses filtert. Anonyme Klasse implementiert Interface FilenameFilter aus java.io: Adaptation der accept()-Methode an konkrete Anforderung!. Kein extends oder implements kann für anonyme Klassen spezifiziert werden! siehe Abschnitt “Adapter-Patterns” 10.4.5 Anwendung und Konventionen Anonyme Klasse statt lokaler Klasse, wenn Klasse kleinen Körper hat nur eine Instanz der Klasse benötigt wird die Klasse direkt nach ihrer Definition benutzt wird Name für die Klasse Code nicht leichter verständlich macht. Layout-Empfehlungen von Sun: Öffnende geschweifte Klammer in selber Zeile wie new, und new in selber Zeile wie der Ausdruck, zu dem die anonyme Klasse gehört. Einrücken des Körpers relativ zu der Zeile mit new. Schliessende geschweifte Klammer in selber Zeile wie Ende des umschliessenden Ausdrucks. (z.B. Semikolon als Abschluss einer return-Anweisung) 10.5 Zusammenfassung Bisher: “top-level” Klassen (direkte Mitglieder eines Pakets). Seit Java 1.1: Innere Klassen: definiert innerhalb einer anderen Klasse (Komponente einer Klasse, ähnlich Felder und Methoden) Vier Arten von inneren Klassen: – Static Member Classes (Nested Top-Level Classes): “class class” (vgl. Klassen-Feld, Klassen-Methode) Verhält sich ähnlich wie top-level Klasse, hat Zugriff auf die statischen Komponenten der umschliessenden Klasse. Interfaces dürfen nur als static member classes, nicht als non-static definiert werden. Informatik B SS 03 138 – Member Classes (“echte” innere Klasse): Analog zu Instanz-Feldern und Methoden, Zugriff auf alle Felder der umschliessenden Klasse. Spezielle Syntax zum Zugriff auf “umschliessende Instanz”. – Local Classes: definiert innerhalb eines Blocks von Java-Code (innerhalb einer Methode), ähnlich zu lokalen Variablen. Sichtbarkeit: nur innerhalb des Blocks. Zugriffsrechte wie Member-Klassen, zusätzlich Zugriff auf alle final lokalen Parameter und Variablen, die im Block sichtbar sind. – Anonymous Classes: Namenlose lokale Klasse; Kombination der Syntax von Klassen-Definition und Objekt-Instantiierung; definiert innerhalb eines Ausdrucks. 10.6 Beispiel-Code ‘Enumeration’ // A class that implements a stack as a linked list public class LinkedStack { // This static member interface defines how objects are linked public static interface Linkable { public Linkable getNext(); public void setNext(Linkable node); } // The head of the list is a Linkable Object Linkable head; // Methods public void push(Linkable node){ node.setNext(head); head = node; } public Object pop(){ if (head != null) { Linkable oldtop = head; head = head.getNext(); return oldtop; } else return "error"; } } // This method returns an Enumeration object for this LinkedStack public java.util.Enumeration enumerate() { return new Enumerator(); } // short for this.new Enumerator(); // Here is the implementation of the Enumeration interface Informatik B SS 03 139 // defined as a member class // alternative realization as local class or anonymous class // see Java in a Nutshell protected class Enumerator implements java.util.Enumeration { Linkable current; // The constructor uses the private head field of the containing class public Enumerator() { current = head; } // explicit: this.current = LinkedStack.this.head; public boolean hasMoreElements() {return (current != null); } public Object nextElement() { if (current == null) throw new java.util.NoSuchElementException(); Object value = current; current = current.getNext(); return value; } } } // This class implements the static member interface class LinkableInteger implements LinkedStack.Linkable { // Here’s the node’s data and constructor int i; public LinkableInteger(int i) { this.i = i; } // Here are the data and methods required to implement the interface LinkedStack.Linkable next; public LinkedStack.Linkable getNext() { return next; } public void setNext(LinkedStack.Linkable node) { next = node; } } public class IntStack { public static void main (String[] args) { LinkedStack intstack = new LinkedStack(); LinkableInteger li = new LinkableInteger(1); System.out.println("Push! New Integer-Element: " + li.i); intstack.push(li); System.out.println ("Top of Stack is " + ((LinkableInteger)(intstack.head)).i); LinkableInteger li2 = new LinkableInteger(2); System.out.println("Push! New Integer-Element: " + li2.i); intstack.push(li2); System.out.println ("Top of Stack is " + ((LinkableInteger)(intstack.head)).i); intstack.pop(); System.out.println("pop!"); System.out.println ("Top of Stack is " + ((LinkableInteger)(intstack.head)).i); Informatik B SS 03 140 intstack.push(li2); // Enumerator java.util.Enumeration intstackenum = intstack.enumerate(); while (intstackenum.hasMoreElements()) {System.out.println( ((LinkableInteger)(intstackenum.nextElement())).i);} } } 10.7 Adapter-Patterns und Java Adapter-Klassen Adapter: Konvertierung eines API einer Klasse in das API einer anderen. Anwendung: Zusammenarbeit unverbundener Klassen in einem Programm. Konzept: Schreibe Klasse mit dem gewünschten Interface und lasse diese Klasse mit der Klasse kommunizieren, die ein anderes Interface hat. Zwei Möglichkeiten zur Realisierung: – Klassen-Adapter: Ableitung einer neuen Klasse von der nicht-angepassten Klasse und Hinzufügen von Methoden so, dass die neue Klasse dem gewünschten Interface genügt. class A { // a class which "nearly" meets specification B } interface B { // ... } class AdA extends A implements B { // implement methods of B using A } – Objekt-Adapter: Einbetten eines Objekts der ursprünglichen Klasse in die neue Klasse und Definition von Methoden, um Aufrufe in der neuen Klasse entsprechend zu übersetzen. class AdA implements B { A a; // ... } siehe Cooper, Java Design Patterns, Kap. 9. In Java wird der Begriff “Adapter” für Klassen im GUI-Bereich verwendet. Informatik B SS 03 141 Hier sind Adapter-Klassen Klassen, die nur Methoden mit leerem Körper zur Verfügung stellen. Anwendung: Erzeugung eines entsprechenden Objekts und Überschreiben der benötigten Methoden. Beispiel: ‘WindowAdapter’ // illustrates using the WindowAdapter class // make an extended window adapter. // This class closes the frame // when the closing event is received class MyWindowAdapter extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } } public class Closer { public Closer() { MyWindowAdapter win = new MyWindowAdapter(); Frame f = new Frame(); f.addWindowListener(win); f.setSize(new Dimension(100,100)); f.setVisible(true); } public static void main(String[] args) { new Closer(); } } Kompaktere Realisierung mit anonymer Klasse: // create window listener for window close addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {System.exit(0);} }); 10.8 Innere Klassen und Lexical Closures Lexical Closure: Funktion, die ihren Kontext erinnert und freie Variablen darüber bindet. Konzept aus der funktionalen Programmierung. Siehe, z.B. Steele, Common Lisp, Kap. 7.1. Brent W. Benson (1999), Java Reflections: Inner Classes – Closures for the masses, ACM SIG-Plan Notices, 34(2), 32-35. Informatik B SS 03 142 [1]> (defun adder (n) (function (lambda (m) (+ m n)))) ADDER [2]> (setq add3 (adder 3)) #<CLOSURE :LAMBDA (M) (+ M N)> [3]> (funcall add3 4) 7 Für die innere, namenlose Funktion (lambda-Ausdruck) ist n eine freie Variable Ergebnis von (adder 3) ist eine Funktion, die die Zahl 3 zu ihrem Argument (m) addiert. 10.8.1 Code as Data Behandlung von Code als Daten ist mächtige Eigenschaft einer Programmiersprache. Common Lisp, Scheme, C: Funktionen als “First Class”-Objects, die in Variablen gespeichert, manipuliert, und auf Daten angewendet werden können. In Java: Objekte als “First Class” Objects (Funktionen/Methoden sind untergeordnet). Fähigkeit, dass ein Stück Code den Kontext erinnert, in dem es erzeugt wurde (Closure). 10.8.2 Adder-Beispiel interface Adder{ int add(int m); } public Adder makeAdder(final int n) { return new Adder() { public int add (int m) {return m + n;} }; } In makeAdder wird das Adder-Interface für eine spezielle Anforderung adaptiert! Die anonyme innere Klasse realisiert das Konzept der lexical closure. Informatik B SS 03 143 11 Abstrakte Datentypen und Collections 11.1 Abstrakte Datentypen 11.1.1 Grundlagen Abstrakter Datentyp (ADT): Sammlung von Objekten eines Typs, die in einer festen Struktur organisiert sind. Konstruktoren repräsentieren die Struktur des ADT (kleinstes Element und Aufbau einer Struktur durch Einfügen), empty-Test, Selektoren, weitere Operatoren. Alle Operatoren werden abstrakt definiert. Ihre Funktionalität wird über Axiome beschrieben. Beispiele: Liste, Stack, Menge, Bag, Binärbaum. ADT sind abstrakt, weil nur die Struktur, nicht aber die konkrete Realisierung festgelegt wird. Die Realisierung (Implementation) bedingt die Performanz: Speicheraufwand und Aufwand für Suchen, Einfügen, Löschen von Objekten. Abstrakte Datentypen und konkrete Implementationen in Java: Collections (java.util). Funktionalität in Java: Interface Signatur der Operationen. Axiome als natürlichsprachiger Kommentar. Realisierung: Implementation des Interface. 11.1.2 Funktionalitäten von Collections In den folgenden Abschnitten: Beispielhafte Implementation von Collections. Ziel: Vertiefung der bisher vermittelten Konzepte der objekt-orientieren Programmierung mit Java an einem etwas komplexeren Beispiel (von A.T. Scheiner, Skript zur Java-Vorlesung) Wir betrachten einen ADT als Sammlung (Collection) von Objekten. Funktionalitäten: Einfügen (add()), Löschen (sub()), Finden (find()) von Objekten, Anzahl von Objekten (count()). Weitere Funktionalitäten: Aufzählen (Enumeration), ... Charakterisierung der Basis-Funktionalitäten: – Man kann nur Objekte mit find() entdecken, die irgendwann vorher mit add() eingefügt wurden. – Man kann nur ein Objekt mir sub() löschen, das irgendwann vorher mit add() eingefügt wurde. – count() wird von add() und sub() beeinflusst. Charakterisierung der abstrakten Struktur von Objekten: – Kann das gleiche Objekt mehr als einmal hinzugefügt werden? nein Set, ja Bag Informatik B SS 03 144 – Sind die Objekte geordnet? nein Set, ja List – Kann nur von einer Seite hinzugefügt und gelöscht werden? nein Liste, ja Queue oder Stack – Sind die Objekte über einen Index zugreifbar? Set oder List, ja Array nein Weitere Aspekte: – Definition von Gleichheit: Identität oder Äquivalenz – Status von null: als Objekt (das eingefügt, gelöscht, gefunden werden kann) oder nicht. 11.2 Implementation von Collections Im Prinzip kann die Funktionalität eines Arrays über eine Listen-Implementation realisiert werden. Dies ist aber wohl der Performanz nicht zuträglich. Frage der Implementation: welche Funktionalitäten werden wie am besten realisiert? Im Folgenden: Set und Bag als Array, als doppelt verkettete Liste, als Binärbaum. // adt/MyCollection.java // A.T. Schreiner package adt; /** adt: rudimentary collection; an implementation<ul> <li>may use equality or identity <li>may or may not accept null <li>may return original objects or objects from the container <li>may throw RuntimeException for unsuitable objects, overflow, etc. </ul> */ public interface MyCollection { /** insert an object into the collection. */ Object add (Object x); /** locate an object in the collection. */ Object find (Object x); /** remove an object from the collection. */ Object sub (Object x); /** @return number of distinct objects in the collection; zero, if the collection is empty. */ int count (); } Informatik B SS 03 adt MyCollection <<interface>>> 145 adt.array MyCollection Bag Set EquivalenceSet add(x: Object): Object find(x: Object): Object sub(x: Object): Object count(): Integer Abbildung 42: Struktur von adt.array 11.3 Implementation mit Array 11.3.1 MyCollection/Array Grundlegende Funktionalität: Array fester Länge, erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte mehrfach ein, löscht immer nur ein Objekt. // adt/array/MyCollection.java // A.T. Schreiner package adt.array; /** fixed size, array based container with multiple insertion. */ public class MyCollection implements adt.MyCollection { protected Object[] element; protected int count; /** define capacity of the collection. @throws RuntimeException if capacity < 0. */ public MyCollection (int capacity) { element = new Object[capacity]; } /** default capacity 128; permit package to use newInstance(). */ public MyCollection () { this(128); } /** insert an object into the collection, do not check if already present. @param x object to be added, may be null. @return x. @throws ArrayIndexOutOfBoundsException for overflow. */ public Object add (Object x) { element[count] = x; ++ count; return x; } /** locate an object in the collection. @param x object to be found. @return object from collection, as determined by locate(). 146 Informatik B SS 03 @throws ArrayIndexOutOfBoundsException if x cannot be found. */ public Object find (Object x) { return element[locate(x)]; } /** remove an object from the collection. @param x object to be removed. @return object from collection, as determined by locate(). @throws ArrayIndexOutOfBoundsException if x cannot be found. */ public Object sub (Object x) { int n = locate(x); x = element[n]; if (count > n+1) System.arraycopy(element, n+1, element, n, count-(n+1)); -- count; return x; } /** @return number of distinct objects in the collection; zero, if the collection is empty. */ public int count () { return count; } /** locate an object in the collection. @param x object to be found. @return (last) position if present, based on identity. @throws ArrayIndexOutOfBoundsException if x cannot be found. */ protected int locate (Object x) throws ArrayIndexOutOfBoundsException { int n = count; while (element[--n] != x) ; return n; } /** permit symbolic dump. */ public String toString () { StringBuffer buf = new StringBuffer(super.toString()); buf.append(" count ").append(count); for (int n = 0; n < count; ++ n) buf.append(’\n’).append(element[n]); return buf.toString(); } /** test: . display, -word remove, else add. */ public static void main (String[] args) { if (args != null) new MyCollection().test(args); } protected void test (String[] args) { for (int a = 0; a < args.length; ++ a) try { Informatik B SS 03 147 if (args[a].equals(".")) System.out.println(this); else if (args[a].equals("null")) add(null); else if (!args[a].startsWith("-")) add(args[a]); else if (args[a].equals("-null")) sub(null); else sub(args[a].substring(1)); } catch (RuntimeException e) { System.err.println(e); } } } 11.3.2 Erläuterungen zu ‘MyCollection’ Länge des Arrays kann über Konstruktor gesetzt werden. Beim Löschen eines Elements, das nicht an der letzten Indexposition (count) steht, muss der Sub-Array rechts von diesem Index um eins nach links verschoben werden. Dies wird durch die Methode System.arraycopy() realisiert. sub() und find() müssen ein Objekt lokalisieren. Die Methode locate() ist bisher so realisiert, dass Gleichheit Identität meint (==). Alternativ: equals() (ist z.B. für String-Objekte definiert). locate() ist protected deklariert: verstecktes Implementationsdetail. Wenn null als Objekt wie andere Werte eingefügt werden kann, müssen erfolglose Versuche, ein Objekt zu lokalisieren (in sub(), find()) als Exception behandelt werden. ArrayIndexOutOfBoundsException ist von RuntimeException abgeleitet und muss deshalb nicht im Kopf der Methode angegeben werden. Durch die Deklaration wird explizit darauf hingewiesen, dass diese Exception auftritt, wenn Objekt x nicht gefunden wird. (Hier wird die ArrayIndexOutOfBoundsException etwas “missbraucht”. Besser könnte eine NoSuchElementException() verwendet werden.) Um die Collection zu testen, können wir den Inhalt des Arrays mit toString() sichtbar machen. Die Oberklasse von MyContainer ist Object und für Object existiert eine Methode toString(), die wir benutzen können. Die return-Anweisung am Ende von toString() liefert den Inhalt des StringBuffers als String zurück. (System.out.println() arbeitet auf mit toString() umgewandelten Objekten.) Die Methode test() reagiert auf folgende Kommandozeilen-Argumente: . zeigt Inhalt des Arrays an; -wort löscht, sonst wird hinzugefügt. null wird als Null-Referenz und nicht als Wort "null" aufgefasst. 11.3.3 Test-Protokoll für ‘MyCollection’ $ javac -classpath ../.. MyCollection.java $ java -classpath ../.. adt.array.MyCollection null ’ ’ axel null . -null -axel . 148 Informatik B SS 03 adt.array.MyCollection@65f57 count 4 null axel null java.lang.ArrayIndexOutOfBoundsException: -1 adt.array.MyCollection@65f57 count 3 null axel 11.3.4 Bag/Array Funktionalität: Array fester Länge, der bei Bedarf verlängert (aber nie verkürzt) wird; erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte mehrfach ein, löscht immer nur ein Objekt. Unterklasse von adt.array.MyCollection, Überschreiben von add(): Bei Bedarf, Umkopieren des Arrays in einen Array mit mehr Indexplätzen. // adt/array/Bag.java // A.T.Schreiner package adt.array; /** array based collection with multiple insertion. */ public class Bag extends MyCollection { /** define initial capacity of the collection. @throws RuntimeException if capacity < 0. */ public Bag (int capacity) { super(capacity); } /** default initial capacity determined by superclass. */ public Bag () { } /** insert an object into the collection, do not check if already present; dynamically extend element array by 1k. @param x object to be added, may be null. @return x. */ public Object add (Object x) { try { return super.add(x); } catch (ArrayIndexOutOfBoundsException e) { Object[] ne = new Object[element.length + 1024]; System.arraycopy(element, 0, ne, 0, element.length); element = ne; return super.add(x); } } } Informatik B SS 03 149 11.3.5 Erläuterungen zu ‘Bag’ Als Unterklasse hat Bag (mindestens) die gleichen Klassen- und Instanzvariablen wie seine Oberklasse MyCollection. Die Methoden der Oberklasse können problemlos auf die Objekte der Unterklasse angewendet werde. Die Unterklasse erbt (die für sie sichtbaren) Methoden der Oberklasse. Man kann aber Methoden überschreiben (auch mit schwächeren Sichtbarkeitsrestriktionen versehen). Über super. methodenname hat man in den Methoden immer noch Zugriff auf die Methoden der Oberklasse. In add() überlässt man das Einfügen selbst der Methode der Oberklasse und stellt nur bei Bedarf einen längeren Array zur Verfügung. Weil Konstruktoren nicht vererbt werden, müssen auch für die Unterklasse Konstruktoren definiert werden. Konstruktoren sind verkettet und werden von “oben” (Object) bis zur endgültigen Klasse hin ausgeführt. Als allererste Anweisung in einem Konstruktor muss entweder ein anderer eigener Konstruktor (this()) oder ein Oberklassen-Konstruktor (super()) aufgerufen werden. Ansonsten erfolgt implizit der Aufruf eines parameterlosen Konstruktors der Oberklasse, der existieren muss. Wird kein expliziter Konstruktor definiert, so wird automatisch ein parameterloser Konstruktor generiert. Im leeren Konstruktor Bag() wird automatisch der Aufruf des Oberklassenkonstruktors eingefügt. 11.3.6 Set/Array Funktionalität: Array fester Länge, der bei Bedarf verlängert (aber nie verkürzt) wird; erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte nur einmal ein, löscht immer nur ein Objekt. Unterklasse zu adt.array.Bag, Überschreiben von add(): Ist ein identisches Objekt schon im Array, so wird es nicht noch einmal eingefügt. // adt/array/Set.java // A.T. Schreiner package adt.array; /** array based collection with unique identity insertion. */ public class Set extends Bag { /** define initial capacity of the collection. @throws RuntimeException if capacity < 0. */ public Set (int capacity) { super(capacity); } Informatik B SS 03 150 /** default initial capacity determined by superclass. */ public Set () { } /** insert an object into the collection, unless present. @param x object to be added, may be null. @return object from collection. @throws RuntimeException for overflow. */ public Object add (Object x) { try { return super.find(x); // avoid inadvertent override } catch (ArrayIndexOutOfBoundsException e) { } return super.add(x); } /** test: . display, -word remove, else add. */ public static void main (String[] args) { if (args != null) new Set().test(args); } } 11.3.7 Erläuterungen zu Set Es genügt, add() noch einmal zu überschreiben. Es wird find() in add() verwendet: Vorsicht! Wenn in einer weiteren Unterklasse find() ersetzt würde, so könnte die Funktionsweise von add() verändert werden. Denn jede Methode wird in der Klasse ihres Empfängers (‘Receiver’) gesucht, unabhängig davon, von wo aus ihr Aufruf erfolgt. Mit super. startet die Suche in der Oberklasse der Klasse, in der der Aufruf mit super. steht. Dadurch kann add() nicht mehr durch ein Ersetzen von find beeinflusst werden! Eine Funktion wird unmittelbar nach Ausführung einer return Anweisung verlassen. Wird also ein Objekt im Array gefunden, so wird das Resultat von super.find(x) zurückgeliefert. Wird das Objekt nicht gefunden, so liefert find() wegen der Benutzung von locate() eine ArrayIndexOutOfBoundsException und als Rückgabe erfolgt super.add(x) (das Objekt wird also eingefügt). Achtung: Der Java-Compiler verlangt, dass garantiert eine Return-Anweisung erreicht wird. Würden wir return super.add(x) in den catch Block schreiben, könnte der Compiler nicht feststellen, dass eine der beiden return-Anweisungen auf jeden Fall ausgeführt wird, und würde mit einer Fehlermeldung reagieren. Man kann sich in solchen Fällen mit der Programmzeile return null; // never reached Informatik B SS 03 151 behelfen. 11.3.8 Test-Protokoll für ‘Set’ $ javac -classpath ../.. Set.java $ java -classpath ../.. adt.array.Set axel null ’ ’ null . -null -axel . adt.array.Set@65f55 count 3 axel null java.lang.ArrayIndexOutOfBoundsException: -1 adt.array.Set@65f55 count 2 axel 11.3.9 EquivalenceSet/Array Funktionalität: Array fester Länge, der bei Bedarf verlängert (aber nie verkürzt) wird; erlaubt ‘null’ als Objekt, vergleicht auf Äquivalenz, trägt gleiche Werte nur einmal ein, löscht immer nur ein Objekt. Unterklasse von adt.array.Set, Überschreiben von locate(): Objektvergleich mit equals(). // adt/array/EquivalenceSet.java // A.T. Schreiner package adt.array; /** array based collection with unique insertion based on equals(). */ public class EquivalenceSet extends Set { /** define initial capacity of the collection. @throws RuntimeException if capacity < 0. */ public EquivalenceSet (int capacity) { super(capacity); } /** default initial capacity determined by superclass. */ public EquivalenceSet () { } /** locate an object in the collection. @param x object to be found, may be null. @return (last) position if present, based on equals(). @throws ArrayIndexOutOfBoundsException if x cannot be found. */ protected int locate (Object x) throws ArrayIndexOutOfBoundsException { int n = count; while (!equals(x, element[--n])) ; return n; } Informatik B SS 03 152 /** @returns x.equals(y), avoids NullPointerException. */ public static boolean equals (Object x, Object y) { return x == null ? y == null : x.equals(y); } /** test: . display, -word remove, else add. */ public static void main (String[] args) { if (args != null) new EquivalenceSet().test(args); } } 11.3.10 Erläuterungen zu ‘EquivalenceSet’ Alle Klassen erben von Object. In Object ist eine Methode equals() definiert, deren Empfänger sich selbst mit einem Argument vergleicht. Die Methode ist dort für Identität implementiert. Neue Klassen können die Methode überschreiben. In java.lang.String wird true zurückgeliefert, wenn zwei String-Objekte aus denselben Character-Folgen bestehen. Die Methode ist symmetrisch, wennn / und Strings sind: x.equals(y) == y.equals(x). In EquivalenceSet werden zwei equals()-Methoden benutzt, die sich durch ihre Signatur unterscheiden: Die Methode equals() mit zwei Argumenten benötigt keinen Empfänger, ist also eine Klassenmethode (static). Die für Object definierte und von String überschriebene Methode mit einem Argument benötigt einen von null verschiedenen Empfänger. return-Statement in equals(): Wenn / null ist, dann liefere true, wenn y ebenfalls null ist, sonst rufe x.equals(y) auf. Falls x und y String sind wird auf Äquivalenz verglichen. Bisher wurde Duplizierung von Code sorgfältig vermieden. Alle Vergleiche sind in locate() gekapselt (sogar, als add() überschrieben wurde). D.h., es ist ausreichend, locate() in einer Unterklasse zu überschreiben, wobei null als Spezialfall behandelt wird. 11.3.11 Test-Protokoll für ‘EquivalenceSet’ Set und EquivalenceSet fügen nur Objekte ein, die nicht bereits im Array vorhanden sind. Set arbeitet mit Identität, EquivalenceSet arbeitet mit equals(). $ java adt.array.Set null ’ ’ axel null . -null -axel . adt.array.Set@65f55 count 3 null Informatik B SS 03 153 axel java.lang.ArrayIndexOutofBoundsException: -1 adt.array.Set@65f55 count 2 axel $ java adt.array.EquivalenceSet null ’ ’ axel null . -null -axel . adt.array.EquivalenceSet@65f54 count 3 null axel adt.array.EquivalenceSet@65f54 count 1 11.4 Implementation mit Offener Hash-Tabelle 11.4.1 Array versus Hash-Tabelle Lineare Suche im Array wird um so teurer, je länger der Array ist (Zahl der Objekte). Offene Hash-Tabelle: Feste Länge, Eintrag über Hash-Code. Kollisionsauflösung: jeder Indexplatz enthält eine Collection (üblicherweise Liste). Hash-Code muss möglichst eindeutigen Wert liefern. hashCode() berechnet (möglichst) eindeutigen int-Wert für Empfänger-Objekt. Wenn equals() true liefert, muss hashCode() denselben Wert für beide Objekte (Empfänger und Argument von equals()) liefern. Wenn equals() überschrieben wird, muss man daran denken, auch hashCode() zu überschreiben! “Gute” Hash-Funktion streut gleichmässig (keine Clusterbildung!). Üblich: Key/Data-Paare, Hash-Code wird über Key berechnet. Im Folgenden: nur Daten, Key wird über Daten direkt berechnet. 11.4.2 MyCollection/Hash Grundlegende Funktionalität: Offene Hash-Tabelle für Daten als Array (bucket), bei dem jeder Indexplatz auf eine Collection verweist. In welcher Collection ein Datum abgelegt wird, wird über hashCode() ermittelt. Die Collections können verschieden realisiert werden, z.B. als Array oder als Liste. // adt/hash/MyCollection.java // A.T. Schreiner package adt.hash; /** base class for open hash based collections. */ Informatik B SS 03 154 Hash−Tabelle Daten Abbildung 43: Offene Hash-Tabelle adt.hash adt MyCollection <<interface>>> MyCollection add(x: Object): Object find(x: Object): Object sub(x: Object): Object count(): Integer Abbildung 44: Struktur von adt.hash Bag Set Informatik B SS 03 155 public abstract class MyCollection implements adt.MyCollection { protected final adt.MyCollection[] bucket; /** define bucket table. @throws RuntimeException if capacity < 0. */ public MyCollection (int capacity) { bucket = new adt.MyCollection[capacity]; } /** default bucket capacity is 16. Should use this(16), but JDK 1.1.6 complains about final. */ public MyCollection () { bucket = new adt.MyCollection[16]; } /** insert an object into the collection. */ public Object add (Object x) { return bucket(x, true).add(x); } /** locate an object in the collection. @throws RuntimeException if not found. */ public Object find (Object x) { return bucket(x, false).find(x); } /** remove an object from the collection. @throws RuntimeException if not found. */ public Object sub (Object x) { return bucket(x, false).sub(x); } /** @return number of distinct objects in the collection; zero, if the collection is empty. */ public int count () { int result = 0; for (int n = 0; n < bucket.length; ++ n) if (bucket[n] != null) result += bucket[n].count(); return result; } /** access function for bucket[]. @param x object to hash, may be null. @param create true to create a missing bucket. @return bucket collection or null if create is false. */ protected adt.MyCollection bucket (Object x, boolean create) { int n = x == null ? 0 : Math.abs(x.hashCode() % bucket.length); if (create && bucket[n] == null) bucket[n] = newBucket(); return bucket[n]; } /** factory method for bucket collections, decides class chracteristics. */ protected abstract adt.MyCollection newBucket (); /** permit symbolic dump. */ Informatik B SS 03 156 public String toString() { StringBuffer buf = new StringBuffer(super.toString()); buf.append(" count ").append(count()); for (int n = 0; n < bucket.length; ++ n) buf.append(’\n’).append(bucket[n]); return buf.toString(); } } } 11.4.3 Erläuterungen zu ‘MyCollection’ Anders als üblich bei Hash-Tabellen, werden nicht Key/Data Assoziationen, sondern nur Daten eingetragen (vgl. HashSet in java.util.Collections). adt.hash.MyCollection benutzt einen Array fester Länge mit adt.MyCollection-Objekten als Elemente (bucket). Alle Nachrichten an ein MyCollection-Objekt werden an die entsprechende von hashCode() ausgewählte Collection weitergeleitet. (Methode bucket()) Alle Operationen (add(), sub(), find()) werden über eine Methode bucket() realisiert, die das entsprechende Element erzeugt oder findet. Zugriffsmethode (Access-Method) für Array bucket[]. Elemente können “spät” erzeugt werden, Zugriffskontrolle, ... Konkrete Auswahl eines Collection-Elements und damit das Verhalten von MyCollection wird über newBucket() realisiert: abstrakte Methode abstrakte Klasse, newBucket() regelt Erzeugung von Objekten und wird in Unterklassen durch konkrete Methoden überschrieben: Factory-Pattern! Weil Array-Indizes positiv sein müssen, wird Math.abs() verwendet. Für null kann man keine Methode, also auch nicht hashCode() aufrufen. 11.4.4 Bag/Hash Funktionalität: Array fester Länge, der bei Bedarf verlängert (aber nie verkürzt) wird; erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte mehrmals einmal ein, löscht immer nur ein Objekt. newBucket() liefert ein adt.array.Bag Objekt // adt/hash/Bag.java // A.T. Schreiner package adt.hash; /** open hash based container with multiple insertion. */ public class Bag extends MyCollection { /** define bucket table. Informatik B SS 03 157 @throws RuntimeException if capacity < 0. */ public Bag (int capacity) { super(capacity); } /** default initial capacity determined by superclass. */ public Bag () { } /** factory method for bucket collections, decides class chracteristics. @return adt.array.Bag. */ protected adt.MyCollection newBucket () { return new adt.array.Bag(); } } 11.4.5 Set/Hash Funktionalität: Array fester Länge, der bei Bedarf verlängert (aber nie verkürzt) wird; erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte nur einmal ein, löscht immer nur ein Objekt. newBucket() liefert ein adt.list.Set Objekt // adt/hash/Set.java // A.T. Schreiner package adt.hash; /** open hash based collection with unique identity insertion. */ public class Set extends MyCollection { /** define bucket table. @throws RuntimeException if capacity < 0. */ public Set (int capacity) { super(capacity); } /** default initial capacity determined by superclass. */ public Set () { } /** factory method for bucket collections, decides class chracteristics. @return adt.list.Set. */ protected adt.MyCollection newBucket () { return new adt.list.Set(); } } 11.5 ADT-Test 11.5.1 Anforderungen an ‘Test’ Da adt.array.MyCollection und adt.hash.MyCollection nur Object als gemeinsame Oberklasse haben, müsste die für array definierte Methode test() kopiert werden. Informatik B SS 03 158 Definition einer allgemeinen Test-Klasse adt/Test.java, mit der beliebige Realisationen von Bag, Set und EquivalenceSet als array, hash, list oder tree getestet werden können! Funktionalität von adt/Test: . # word toString() count() sub() add() Ausgabe der Collection auf Standard-Output. Anzeige der Anzahl von Objekten in der Collection. Entferne nächstes Wort. Trage Wort ein. Das Wort “null” wird als null-Referenz eingefügt/gelöscht. Test soll die zu testende Collection als Kommandozeilen-Argument (z.B. adt.hash.Set) übergeben bekommen. intern() erlaubt, dass Strings mit identischen Zeichenfolgen als ein- und dasselbe Objekt behandelt werden. Mit -i soll dies in Test unterbunden werden. $ wc -w Test.java 376 Test.java $ export CLASSPSTH=.. $ { cat Test.java; echo ’#’; } | java adt.Test adt.hash.Set 179 $ { cat Test.java; echo ’#’; } | java adt.Test -i adt.hash.Set 376 Das Unix-Commando wc liefert die Anzahl der Worte (durch Leerzeichen separierte Zeichenfolgen) in einer Datei. Die tatsächlichen Kommandos (Einfügen/Löschen von Worten, ...) sollen als Kommandozeilen-Argumente (wie bisher) oder von Standard-Input oder aus einer Datei gelesen werden können (abstrakte Methode next()). Zentrale Methode in Test: test() intern ja oder nein; Manipulation einer Collection (Einfügen/Löschen von Worten). 11.5.2 Test/ADT // adt/Test.java // A.T. Schreiner package adt; import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; /** test collections using strings with or without intern(). */ public abstract class Test { /** @return next input word, maybe null. Informatik B SS 03 159 @throws EOFException if no more input. @throws IOException if input error. */ public abstract String next () throws IOException; /** tests collections with words delivered by next() until exception.<dl> <dt>.<dd>display collection on stdout <dt>#<dd>display count on stderr <dt>-<dd>remove next word <dt>null<dd>represents <tt>null</tt> <dt>word<dd>add word </dl> @param collection to be tested. @param intern if true, intern() is used on each word. @throws EOFException if no more input. @throws IOException if input error. */ public void test (MyCollection collection, boolean intern) throws IOException { for (;;) { String s = next(); if (s == null) collection.add(null); else { String i = s.intern(); if (i == ".") System.out.println(collection); else if (i == "#") System.err.println(collection.count()); else if (i == "-") try { s = next(); if (s == null || s.equals("null")) collection.sub(null); else collection.sub(intern ? s.intern() : s); } catch (RuntimeException e) { System.out.println("sub "+s+" not found"); } else if (i == "null") collection.add(null); else collection.add(intern ? i : s); } } } /** run test(), either on arguments or on words from standard input. Optional argument <tt>-i</tt> suppresses use of intern(). First argument is full class name. */ public static void main (final String[] args) { if (args == null) return; int a = 0; boolean intern = true; if (a >= args.length) return; Informatik B SS 03 160 if (args[a].equals("-i")) { intern = false; ++ a; } MyCollection collection; if (a >= args.length) return; try { collection = (MyCollection)Class.forName(args[a]).newInstance(); ++ a; } catch (ClassNotFoundException e) { System.err.println(e); return; } catch (IllegalAccessException e) { System.err.println(e); return; } catch (InstantiationException e) { System.err.println(e); return; } // generate local Test object and run test() try { Test test; if (a < args.length) { final int n = a; test = new Test() { int a = n; public String next () throws IOException { if (a < args.length) return args[a++]; throw new EOFException(); } }; } else test = new Test() { StreamTokenizer st = new StreamTokenizer(new InputStreamReader(System.in)); { st.resetSyntax(); st.wordChars(’\0’, ’˜’); st.whitespaceChars(’\0’, ’ ’); } public String next () throws IOException { if (st.nextToken() == st.TT_WORD) return st.sval; throw new EOFException(); } }; test.test(collection, intern); } catch (EOFException e) { } catch (IOException e) { System.err.println(e); } } } 11.5.3 Erläuterungen zu ‘Test’ Für String-Konstanten kann mit intern() erzwungen werden, dass identische Zeichenketten mit einem Objekt identifiziert werden. Es wird generell MyCollection.intern() benutzt, um Eingaben wie “null” oder “#” durch einfache Vergleichsoperationen entdecken zu können. Informatik B SS 03 161 Durch Verwendung von intern() prüft == auf Äquivalenz! Identitätsprüfung wie vorher realisiert: durch Schalter -i (boolean intern in test()). Abstrakte Methode next() kann null liefern, also muss in test() zunächst geprüft werden, ob dies der Fall ist, bevor intern() oder equals() aufgerufen wird. In test() wird Wort “null” als null-Referenz behandelt. main() prüft zunächst, ob -i als Option für Test angegeben wurde. Danach wird der Name der Klasse von der Kommandozeile gelesen und ein entsprechendes Collection-Objekt erzeugt. java.lang.Class ist die Klasse der Klassenbeschreibungen in Java. Jede Klasse hat ein eindeutiges Class-Objekt, das folgendermassen angesprochen werden kann: someObject.getClass() SomeClass.class int.class float[].class Die Klassen-Methode forName() erzeugt ein Class-Objekt für einen vollständigen Klassennamen wie adt.array.Set, das dann die Klasse adt.array.Set repräsentiert (wenn sie gefunden wird!). newInstance() kann an ein Class-Objekt gesendet werden um ein neues Objekt zu erzeugen (wenn ein paramterloser Konstruktor existiert!). (siehe Kapitel “Reflections”) Beim Erzeugen eines Objekts auf diese Art können zahlreiche Exceptions auftreten! Dafür ist Test aber sehr flexibel. Es soll möglich sein, dass die tatsächlichen Manipulationen der Collection über weitere Kommandozeilen-Argumente oder von der Standard-Eingabe (Tastatur) kommen. Entsprechend muss in main() in Abhängigkeit von den weiteren Argumenten auf der Kommandozeile ein Test-Objekt erzeugt werden. Dies wird mit zwei anonymen Klassen realisiert. System.in liefert Standard-Input als Byte-Folge. Ein InputStreamReader erlaubt, Bytes in Characters umzuwandeln. Ein StreamTokenizer konstruiert Worte aus Zeichen. Hier wurde als Trennung (whitespaceChars) zwischen Worten (Tokens) Leerzeichen definiert (Initializer-Block, da anonyme Klasse keinen expliziten Konstruktor definieren kann!). 11.6 Implementation mit Liste 11.6.1 Dynamische Datenstrukturen Ein Array hat eine fest vorgegebene Länge. Wächst die Anzahl der Elemente, muss der Array evtl. in einen längeren Array umkopiert werden. Beim Löschen Informatik B SS 03 162 leere Liste: Einfügen von ‘x’: prev next prev dummy info Bag prev x dummy next next null p.next Löschen von ‘a’: prev p next prev a next n next prev n.prev Abbildung 45: Doppelt-verkettete Liste mit Dummy-Element adt.list adt MyCollection <<interface>>> Bag Set EquivalenceSet add(x: Object): Object find(x: Object): Object sub(x: Object): Object count(): Integer Abbildung 46: Struktur von adt.list müssen Elemente verschoben werden, um “Löcher” zu vermeiden. Dynamische Datentypen (Listen, Bäume): haben keine fest vorgegebene Kapazität. Doppelt-verkettete Liste: jedes Element verweist auf seinen Nachfolger und seinen Vorgänger. Organisation der Liste als Ring mit ausgezeichnetem, zusätzlichem “Dummy”-Element (das nicht gelöscht werden kann). Dadurch wird Einfügen und Löschen sehr einfach. Realisation von Bag als Implementation von adt/MyCollection. Danach: Set als Unterklasse von Bag und EquivalenceSet als Unterklasse von Set. Informatik B SS 03 163 11.6.2 Bag/List Funktionalität: Doppelt-verkettete Liste mit Dummy-Element; erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte mehrfach ein, löscht immer nur ein Objekt. // adt/list/Bag.java // A.T. Schreiner package adt.list; import adt.MyCollection; /** doubly linked list based collection with multiple insertion. */ public class Bag implements MyCollection { /** element structure. */ protected static class Element { public final Object info; public Element prev, next; /** create and doubly link. */ public Element (Object info, Element prev, Element next) { this.info = info; this.prev = prev; prev.next = this; this.next = next; next.prev = this; } /** create dummy: null information, linked to itself. */ public Element () { info = null; prev = next = this; } /** detach from within a list. */ public void unlink () { prev.next = next; next.prev = prev; } /** permit symbolic dump. */ public String toString () { return id()+" prev "+prev.id()+" next "+next.id()+" info "+info; } protected String id () { String result = super.toString(); return result.substring(result.lastIndexOf(’.’)+1); } } /** list header, used as dummy element. */ protected Element list = new Element(); protected int count; /** insert an object into the collection, do not check if already present. The object is added at list.next. @param x object to be added, may be null. @return x. */ 164 Informatik B SS 03 public Object add (Object x) { new Element(x, list, list.next); ++ count; return x; } /** locate an object in the collection. @param x object to be found. @return object from collection, as determined by locate(). @throws RuntimeException if x cannot be found. */ public Object find (Object x) throws RuntimeException { return locate(x).info; } /** remove an object from the collection. @param x object to be removed. @return object from collection, as determined by locate(). @throws RuntimeException if x cannot be found. */ public Object sub (Object x) { Element e = locate(x); e.unlink(); -- count; return e.info; } /** @return number of distinct objects in the collection; zero, if the collection is empty. */ public int count () { return count; } /** locate an object in the collection. @param x object to be found. @return (last) element if present, based on identity. @throws RuntimeException if x cannot be found. */ protected Element locate (Object x) { for (Element e = list.next; e != list; e = e.next) if (e.info == x) return e; throw new RuntimeException(x+": not found"); } /** permit symbolic dump. */ public String toString() { StringBuffer buf = new StringBuffer(super.toString()); buf.append(" count ").append(count); Element e = list; do buf.append(’\n’).append(e); while ((e = e.next) != list); return buf.toString(); } } Informatik B SS 03 165 11.6.3 Erläuterungen zu ‘Bag’ Innere Klasse Element: ist nested top-level (static). Kein Zugriff von Element auf Instanz-Felder der umschliessenden Klasse Bag notwendig. Inhalt eines Elements ist vom Typ Object und final! Konstruktor mit drei Argumenten: Einhängen eines neuen Elements mit Inhalt info und Verkettung mit Vorgänger prev und Nachfolger next. Konstruktor ohne Argumente: Erzeugung des Dummy-Element mit sich selbst als Vorgänger und Nachfolger. unlink() veranlasst ein Element, sich selbst aus einer Liste auszuklinken, indem es einfach seine Nachbarn aufeinander verweisen lässt. Ausgabe: id() liefert Objekt-Referenz als String, wobei der volle Name abgeschnitten wird. (super.toString() ist Aufruf der Methode der Oberklasse Object). Element-Konstruktoren, unlink() und toString() sind public, damit sie in Unterklassen von Bag benutzt werden können. Der Struktur-Zugriff von der äußeren Klasse Bag auf Komponenten der Innneren Klasse Element macht Implementierung effizienter (keine speziellen Accessor-Methoden). Es wäre nicht nötig gewesen, die Instanzvariablen von Element public zu deklarieren, da in Java die äußere Klasse auf alle Komponenten der inneren Klasse Zugriff hat. Ausserhalb von adt.list.Bag ist Element unsichtbar! (Element ist nur via Bag erreichbar. Element ist protected, also nur in Unterklassen von Bag zugreifbar.) Das Innenleben von Bag geht niemand etwas an, Bag ist eine Struktur, in der Elemente aufbewahrt werden. Die MyCollection-Operationen (add(), sub(), find() und count()) werden auf Element-Operationen zurückgespielt. Nach dem bewährten Schema muss eine einzige Methode – locate() – bemüht werden, um das Element zu finden, in dem ein Object gespeichert ist. Wird ein Objekt nicht gefunden, wird eine RunTimeExeception geworfen. (besser wäre spezifische Exception) Suche beginnt bei next vom Ring-Element list (Dummy) und darf list nicht erreichen. Eine leere Liste besteht nur aus list, das dann mit sich selbst verkettet ist. Bei der symbolischen Darstellung muss man ein bisschen aufpassen: Bag fordert jedes Element – inklusive list – auf, sich selbst darzustellen. Ein Element darf aber seine Nachbarn nur nach id() und nicht nach ihrer Darstellung fragen, sonst wird das Ganze im Ring herum rekursiv. Zum Testen wird adt.Test verwendet. Ohne Schalter werden String-Objekte mit denselben Zeichenketten als identische Objekte behandelt! Informatik B SS 03 166 Als erstes Element wird immer das Dummy-Element ausgegeben. $ java adt.Test adt.list.Bag axel null ’ adt.list.Bag@65fcc count 4 Bag$Element@65fca prev Bag$Element@65fbf Bag$Element@65fbc prev Bag$Element@65fca Bag$Element@65fbd prev Bag$Element@65fbc Bag$Element@65fbe prev Bag$Element@65fbd Bag$Element@65fbf prev Bag$Element@65fbe adt.list.Bag@65fcc count 2 Bag$Element@65fca prev Bag$Element@65fbe Bag$Element@65fbd prev Bag$Element@65fca Bag$Element@65fbe prev Bag$Element@65fbd ’ null . - null - axel . next next next next next Bag$Element@65fbc Bag$Element@65fbd Bag$Element@65fbe Bag$Element@65fbf Bag$Element@65fca info info info info info null null null axel next Bag$Element@65fbd info null next Bag$Element@65fbe info next Bag$Element@65fca info null 11.6.4 Unterklasse ‘Set’ Funktionalität: Doppelt-verkettete Liste mit Dummy-Element; erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte nur einmal ein, löscht immer nur ein Objekt. Unterschied zu Bag: Methode add() kontrolliert, ob sein Argument schon vorhanden ist. // adt/list/Set.java // A.T. Schreiner package adt.list; /** doubly linked list based collection with unique identity insertion. */ public class Set extends Bag { /** insert an object into the collection, unless present. @param x object to be added, may be null. @return object from collection. */ public Object add (Object x) { try { return super.find(x); // avoid inadvertent override } catch (RuntimeException e) { } return super.add(x); } } Implementierung analog zu adt.array.Set.java. Kein Reuse, da eine Klasse nur eine Oberklasse erweitern kann. 11.6.5 Unterklasse ‘EquivalenceSet’ Funktionalität: Doppelt-verkettete Liste mit Dummy-Element; erlaubt ‘null’ als Objekt, vergleicht auf Äquivalenz, trägt gleiche Werte nur einmal ein, löscht immer nur ein Objekt. Unterklasse zu Set, Überschreiben von locate(). Informatik B SS 03 167 // adt/list/EquivalenceSet.java // A.T. Schreiner package adt.list; /** doubly linked list based collection with unique insertion based on equals(). */ public class EquivalenceSet extends Set { /** locate an object in the collection. @param x object to be found. @return (last) element if present, based on equals(). @throws RuntimeException if x cannot be found. */ protected Element locate (Object x) { for (Element e = list.next; e != list; e = e.next) if (adt.array.EquivalenceSet.equals(x, e.info)) return e; throw new RuntimeException(x+": not found"); } } Aufruf der public definierten Klassenmethode von adt.array.EquivalenceSet! 11.7 Implementation mit Suchbaum 11.7.1 Suchbäume Suchbaum: dynamische Datenstruktur mit Ordnung. Suche nach Objekten wird effizienter ( und Listen ( ). +#, ) als lineares Suchen in Arrays Aber: Einfügen und Löschen wird aufwendiger, da die Ordnung im Baum erhalten bleiben muss. Häufig: Binärer Baum mit key/data-Paaren in den Knoten. Invariante Eigenschaft: alle keys im linken Unterbaum sind kleiner als der key des aktuellen Knotens, alle keys im rechten Unterbaum grösser. Um Suche effizient zu halten, sollte der Baum möglichst ausgewogen sein (Höhen-Balance, Gewichts-Balance für Elemente mit annähernd gleichen Zugriffswahrscheinlichkeiten). Im Folgenden: Eingetragen werden nicht key/data-Paare sondern Elemente. Die Elemente müssen Comparable sein! (vgl. Marker-Interface). Das Interface Comparable deklariert die Methode compareTo(), die anti-symmetrisch ist (Ordnungsrelation!) und angibt, ob ihr Empfänger kleiner (Resultat ), gleich (Resultat ) oder grösser (Resultat ) als das Argument-Objekt ist. (Gleichheit muss verträglich mit equals() sein.) E E E Informatik B SS 03 168 Set 4 sub 2 1 6 3 5 root left 7 info right 4 left info right 2 right left info 6 Abbildung 47: Suchbaum: Klassische Darstellung (links) und als adt.tree.set 11.7.2 Set/Tree Funktionalität: Speichern eines von add() gelieferten Comparable Wertes in einem Element, das rekursiv auf kleinere und grössere Werte verweist, die ebenfalls mit Element-Objekten gespeichert sind. erlaubt ‘null’ als Objekt, vergleicht auf Identität, trägt gleiche Werte nur einmal ein, löscht immer nur ein Objekt. Grundgedanke: Es wird wieder eine Hilfsfunktion locate() verwendet, um ein Objekt in der Collection aufzufinden, die von add(), find() und sub() verwendet wird. locate() realisiert die Traversierung des Baums. Funktionalität von locate() im Suchbaum: – Ist der Wert des gesuchten Objekts gleich dem aktuellen Element, dann liefere das Element zurück. – Ist der Wert des gesuchten Objekts kleiner dem aktuellen Element, dann Suche im linken Unterbaum weiter. – Ist der Wert des gesuchten Objekts grösser dem aktuellen Element, dann Suche im rechten Unterbaum weiter. Spezialbehandlung des null-Objekts ist wieder notwendig: null ist kleinstes Element, also “ganz links unten” im Suchbaum. Wenn locate() das gesuchte Objekt nicht gefunden hat, so sind wir bei der Traverse aber an der Stelle gelandet, wo es stehen sollte. Informatik B SS 03 169 MyCollection <<interface>> Set NotFound Result <<interface>> at: Integer add(x: Object): Object ... locate(x: Object): Result Found find(): Object sub(): Object at: Integer kennt ... Set.Element Abbildung 48: Objekt-Orientierter Entwurf von adt.tree.set Problem: add(), sub() und find() benötigen die Position im Baum, an der das gesuchte Objekt steht/eingetragen werden soll. Beim Array war dies einfach: (1) Suche liefert Indexplatz, (2) Methode springt an den entsprechenden Platz. Um weiterhin die Lokalisation unabhängig von den MyCollection-Methoden zu halten, muss man sich die Position im Baum merken, um sie in diesen Methoden nutzen zu können. Objekt-Orientierte Lösung: “Einfrieren” des Ergebnisses der Traverse in einem Objekt! (das das Interface Result) implementiert. Spezielle Realisierung der Datenstruktur “Suchbaum”: Element ist selbst Suchbaum, besteht also aus Eintrag und Verweis auf linken und rechten Unterbaum. // adt/tree/Set.java // A.T. Schreiner package adt.tree; import adt.MyCollection; import adt.Visitable; import adt.Visitor; /** binary tree based collection for <tt>Comparable</tt> with unique insertion. */ public class Set implements MyCollection, Visitable { protected Element[] sub = { null }; /** element structure, <b>not</b> limited to <tt>Comparable</tt>. */ protected static class Element extends Set { public final Object info; /** create. */ public Element (Object info) { this(info, null, null); } 170 Informatik B SS 03 /** create and link. */ public Element (Object info, Element left, Element right) { this.info = info; sub = new Element[] { left, right }; } /** let this disappear. @return combined subtrees as a tree. */ public Element unroot () { if (sub[0] == null) return sub[1]; // nothing at left... right if (sub[1] == null) return sub[0]; // nothing at right... left Element e = sub[0]; // else start at left... while (e.sub[1] != null) e = e.sub[1]; // ...move to it’s bottom right e.sub[1] = sub[1]; // ...and attach my right there return sub[0]; // ...all is below left } /** adjust count by current element. */ public int count () { return super.count()+1; } /** permit symbolic dump. */ public String toString () { return info+""; } /** receive a visitor. */ public boolean visit (Visitor v) { return visit(0, v) && v.visit(info) && visit(1, v); } } /** inefficient, unless the collection is empty. @return number of distinct objects in the collection; zero, if the collection is empty. */ public int count () { int result = 0; for (int n = 0; n < sub.length; ++ n) if (sub[n] != null) result += sub[n].count(); return result; } /** insert an object into the collection. @throws ClassCastException if x is not <tt>Comparable</tt>. */ public Object add (Object x) throws ClassCastException { return locate(x).add(x); } /** locate an object in the collection. @throws RuntimeException if not found. @throws ClassCastException if x is not <tt>Comparable</tt>. */ Informatik B SS 03 171 public Object find (Object x) throws ClassCastException { return locate(x).find(); } /** remove an object from the collection. @throws RuntimeException if not found. @throws ClassCastException if x is not <tt>Comparable</tt>. */ public Object sub (Object x) throws ClassCastException { return locate(x).sub(); } /** operations on comparison state of locate(). */ protected interface Result { Object add (Object x); Object find (); Object sub (); } /** locate an object in the collection. @param x object to be found. @return comparison state for further processing. */ protected Result locate (Object x) throws ClassCastException { Comparable info = (Comparable)x; // can still be null Set s = this; int at = 0; for (;;) { if (s.sub[at] == null) return s.new NotFound(at); // cannot involve null in compareTo... int c; // ...make null less than anything if (s.sub[at].info == null) c = info == null ? 0 : 1; else c = info == null ? -1 : info.compareTo(s.sub[at].info); if (c == 0) return s.new Found(at); s = s.sub[at]; at = c < 0 ? 0 : 1; } } /** this.sub[at] is null and should be Element(x). */ protected class NotFound implements Result { protected final int at; public NotFound (int at) { this.at = at; } public Object add (Object x) { sub[at] = new Element(x); return x; } public Object find () { throw new RuntimeException("not found"); } public Object sub () { throw new RuntimeException("not found"); } } /** this.sub[at].info is x. */ protected class Found implements Result { protected final int at; public Found (int at) { this.at = at; } Informatik B SS 03 172 public Object add (Object x) { return sub[at].info; } public Object find () { return sub[at].info; } public Object sub () { Object result = sub[at].info; sub[at] = sub[at].unroot(); return result; } } /** receive a visitor. */ public boolean visit (Visitor v) { return visit(0, v); } protected boolean visit (int at, Visitor v) { return sub[at] != null ? sub[at].visit(v) : true; } /** permit symbolic dump. */ public String toString () { class Dumper implements Visitor { protected StringBuffer buf = new StringBuffer(); public boolean visit (Object o) { buf.append(’\n’).append(o); return true; } public String toString () { return buf.toString(); } } Dumper d = new Dumper(); visit(d); return super.toString()+" count "+count()+d.toString(); } } 11.7.3 Erläuterungen zu ‘Set’ Visitor, Visitable: Interfaces in adt (vgl. Visitor Pattern, Cooper, Kap. 26). siehe weiter unten Instanzvariable sub als Array von Element-en. Idee: Bei Set hat sub ein Element, nämlich den kompletten Suchbaum. Bei Element hat sub zwei Elemente, mit sub[0] als linkem (kleinere Elemente) und sub[1] als rechtem (grössere Elemente) Unterbaum. Wieder Element als statische innere Klasse, aber: stammt von Set ab und erbt deshalb die Instanzvariable sub. Die Modellierung des ADT Suchbaum über Element als Unterklasse des Suchbaums selbst ist nicht Standard! (vgl. Realisierung in Vorlesung Informatik A) Bei der hier gezeigten Implementierung werden dafür objekt-orientierte Techniken sehr schön verdeutlicht. info-Objekte sind hier allgemein als vom Typ Object deklariert und nicht auf Comparable beschränkt. (Platz für eigene Definitionen von Vergleichen) Analog zur Liste gibt es zwei Konstruktoren: Wurzel mit zwei Unterbäumen oder Blatt. Informatik B SS 03 173 Tabelle 5: Verhalten von locate() Gesucht null = anderes null anderes ? Eintrag null null anderes anderes Wert 0 1 -1 compareTo() weiter fertig rechts links depends Analog zu unlink() ist unroot() realisiert: Hat das aktuelle Element keinen oder nur einen Unterbaum wird dieser (null wenn kein Unterbaum) zurückgeliefert. Komplizierterer Fall: Element hat zwei Unterbäume. An das rechteste Element im linken Unterbaum (grösstes Element vor dem aktuellen) wird der rechte Unterbaum angehängt. count() wird mit super auf Set zurückgespielt und muss zusätzlich das Element selbst zählen, da super.count() die Anzahl der Elemente der Unterbäume aufaddiert. add(), find() und sub() werden wieder über locate() realisiert. locate() muss entweder ein vorhandenes Element im Suchbaum finden oder eine geeignete Position liefern, an der ein Element eingefügt werden soll. Neue Realisation: locate() liefert ein Objekt, das man mit Einfügen, Finden oder Löschen beauftragen kann. In diesem Objekt wird der Zustand der aktuellen Baumtraverse eingefroren (Verweis auf aktuellen Knoten Element). Startet mit s beim Set und Index at bei 0. Das Argument (Object x) muss Comparable sein. Ist sub[at] null, so gibt es das gesuchte Element nicht. Es hätte aber an diese Stelle gehört. Spezielle Behandlung von null-Referenz: Entweder null ist an Position at eingetragen, dann wurde der Wert gefunden; anderenfalls wird -1 zurückgeliefert (null ist kleiner als alle anderen Werte). Das endgültige Vergleichsresultat hängt vom weiteren Verlauf der Suche ab: Entweder der Vergleich stimmt, dann wurde eine Position für den gesuchten Wert gefunden, oder die Suche geht im linken oder rechten Unterbaum weiter. Ein Wert wird bei s.sub[at] gefunden oder er müsste dort gespeichert werden. Member-Klassen NotFound und Found: zur Weiterverarbeitung des von locate() gelieferten Result-Objekts. Sie haben Zugriff auf Instanz-Komponenten der umschliessenden Klasse, für die sie erzeugt wurden – hier speziell auf Komponenten von s. Informatik B SS 03 174 Ein NotFound- oder Found-Objekt kennt also durch seine Konstruktion das Element oder Set s, das bei der Suche entdeckt wurde und hat damit Zugriff auf dessen sub. Es muss sich nur den Index at explizit merken. Durch die Definition des Result-Interfaces und der inneren Klassen NotFound und Found lassen sich add(), find() und sub() wesentlich einfacher realisieren als durch explizite if-Abfragen zur Fallunterscheidung in diesen Methoden selbst. Der einzig komplizierte Fall – das Löschen eines Wertes – kann mithilfe von unroot() leicht bewerkstelligt werden. Nested top-level Klassen sind weniger aufwendig als echte Member-Klassen. Man sollte echte Member-Klassen nur dann verwenden, wenn man den impliziten Verweis auf die umschliessende Instanz benötigt. Häufig wird als Erzeuger für die innere Instanz this (vor new) verwendet. Eine Konstruktion wie die NotFound- und Found-Objekte in adt.tree.Set wird auch als Closure bezeichnet: Bei der Konstruktion wird eine bestimmte Situation eingefangen, die später durch Nachricht an ein solches Objekt weiterverarbeitet werden kann (vgl. auch die anonymen Klassen in adt.Test). 11.8 Visitor 11.8.1 Konzept eines Visitor Ganz allgemein kann man von einer Collection verlangen, dass einem Besucher (Visitor) alle Insassen (Elemente) genau einmal vorgestellt werden. Interface Visitable: Methode zum Anliefern eines Visitor bei der Collection. Ein beliebiges Objekt ist ein Visitor, wenn ihm Objekte vorgeführt werden können. Interface Visitor: Methode, mit der die Collection dem Visitor ihre Insassen vorstellt. Eine Collection ist Visitable, wenn sie Besucher empfangen kann. Ausbaumöglichkeit: visit() in Visitor könnte mit verschiedenen Signaturen verschiedene Arten von Insassen unterscheiden. Beim Suchbaum hat man die schöne Möglichkeit, Einträge sortiert auszugeben. Beispielsweise kann adt.Test mit adt.tree.Set Wörter einlesen (String ist Comparable) und sortiert ausgeben. Ein Suchbaum sollte Visitable sein, um die Sortierung ausnutzen zu können. Implementiert man einen Visitor, der die Insassen in einen StringBuffer abbildet, kann man den Baum sehr elegant darstellen lassen. (Methoden visit() und toString() in adt.tree.Set) Informatik B SS 03 175 Visitable <<interface>> Visitor <<interface>> visit(v: Visitor): Boolean visit(x: Object): Boolean Set visit(v: Visitor): Boolean visit(at: Integer, v: Visitor): Boolean toString(): String Dumper visit(x: Object): Boolean Set.Element visit(v: Visitor): Boolean Abbildung 49: Realisierung des Visitor-Patterns in adt.tree.set Die visit()-Methoden haben als Resultat-Wert boolean. Über das Resultat der Methode in Visitor kann der Besucher steuern, ob die Traverse fortgesetzt werden soll (Besucher sagt: “weiter” oder “ich will nicht mehr”). 11.8.2 ‘Visitor’, ‘Visitable’/ADT // adt/Visitable.java // A.T. Schreiner package adt; /** framework for visitor pattern: ability to be visited. */ public interface Visitable { /** receive a visitor, manage the visit. @return true if the visitor always replies true. */ boolean visit (Visitor v); } // adt/Visitor.java // A.T. Schreiner package adt; /** framework for visitor pattern: visiting object. */ Informatik B SS 03 176 public interface Visitor { /** visit an object. @return true to continue visiting. */ boolean visit (Object x); } 11.8.3 Suchbaum mit Visitor In Set wird eine inorder Traversierung realisiert. (Drei Möglichkeiten, einen Baum zu traversieren: Inorder, Präorder, Postorder). visit()-Methoden in Set: visit() mit einem Argument schickt Visitor zu sub[0], visit() mit zwei Argumenten schickt Visitor zu Position at. visit()-Methoden in Element: visit() mit einem Argument realisiert Inorder, visit() mit zwei Argumenten wird von Set geerbt. Für adt.tree.Set implementiert man toString() mit einem Dumper-Objekt, das jeden Insassen in seinen StringBuffer einträgt und diesen bei toString() (von Dumper) als seine eigene Darstellung abliefert. Dumper ist ein Beispiel für eine lokale Klasse (Es hätte auch eine anonyme Klasse genügt). Eine lokale Klasse kann jedoch mehr als ein Interface auf einmal implementieren. Innerhalb der lokalen Klasse wird wieder eine visit()-Methode implementiert. In toString() wird die visit()-Methode von Set aufgerufen, der ein Dumper-Objekt übergeben wird. Im zwei-parametrigen visit() wird geprüft, ob an der aktuellen Position noch ein Unterbaum existiert. Wenn ja, wird die visit()-Methode für diesen Unterbaum an Position at aufgerufen. Die eigentliche Arbeit erledigt die visit()-Methode von Element: Hier wird die Inorder-Traverse realisiert. Das Objekt info wird an die visit()-Methode des Visitors übergeben. 11.9 Java Collection Classes 11.9.1 Grundstruktur Java collection framework in java.util: wichtige Klassen und Interfaces, um mit Collections zu arbeiten. Bis Java 1.1: nur Vector (jetzt ArrayList) und HashTable (jetzt HashMap). Zwei Grundtypen von Collections: Informatik B SS 03 177 1. Collection (Interface): Gruppe von Objekten mit Set (Interface) als Collections ohne Dublikate und List (Interface) als Collection mit geordneten Elementen 2. Map (Interface): Menge von Assoziationen (Mappings) zwischen Objekten. weitere Interfaces: Iterator, ListIterator. Ähnlich zum Enumeration-Interface: public interface Iterator boolean hasNext(); // Object next(); // void remove(); // } { vgl. hasMoreElements() in Enumeration vgl. nextElement() in Enumeration zusaetzlich remove() erlaubt sicheres Löschen von Elementen während der Iteration. Gelöscht wird das als letztes von next() gelieferte Element. Modifikation der Collection während ihrer Aufzählung ist mit remove() (und nur mit remove()) möglich! Für Objekte eigener Klassen, die in Collections gespeichert werden sollen, sollten die Methoden equals() und hashcode() entsprechend der gewünschten Funktionalität überschrieben werden. Die Klasse Collections liefert statische Methoden und Konstanten, die beim Arbeiten mit Collections nützlich sind. 11.9.2 Illustration Set s = new HashSet(); s.add("test"); boolean b = s.contains("test2"); s.remove("test"); // // // // Implementation based on a hash table Add a String object to the set Check whether a set contains an obj Remove a member from a set Set ss = new TreeSet(); // TreeSet implements SortedSet ss.add("b"); // Add some elements ss.add("a"); // Now iterate through the elements (in sorted order) and print them for(Iterator i = ss.iterator(); i.hasNext();) System.out.println(i.next()); List l = new LinkedList(); l = new ArrayList(); Vector v = new Vector(); l.addAll(ss); l.addAll(1, ss); Object o = l.get(1); l.set(3, "new element"); // // // // // // // LinkedList implements a doubly linked list ArrayList is more efficient, usually Vector is an alternative in Java 1.1/1.0 Append some elements to it Insert elements again at index 1 Get the second element Set the fourth element Informatik B SS 03 178 java.lang java.util Object AbstractCollection AbstractList AbstractSet Collection <<interface>> Map <<interface>> ArrayList C S Vector C S HashSet C S TreeSet C S LinkedList C S Stack List <<interface>> Set <<interface>> AbstractMap AbstractSequentialList HashMap C S TreeMap C S SortedSet <<interface>> Comparator <<interface>> Iterator <<interface>> ListIterator <<interface>> SortedMap <<interface>> Abbildung 50: Java Collection Classes (S: implements Serializable, C: implements Clonable) Informatik B SS 03 179 l.add("test"); // Append a new element to the end l.add(0, "test2"); // Insert a new element at the start l.remove(1); // Remove the second element l.remove("a"); // Remove the element "a" l.removeAll(ss); // Remove elements from this set if(!l.isEmpty()) // If list is not empty System.out.println(l.size()); // print out the number of elements in it boolean b1 = l.contains("a"); // Does it contain this value? booelan b2 = l.containsAll(ss); // Does it contain all these values? List sublist = l.subList(1,3); // A sublist of the 2nd and 3rd elements Object[] elements = l.toArray(); // Convert it to an array l.clear(); // Delete all elements Map m = new HashMap(); m.put("key", new Integer(42)); Object value = m.get("key"); m.remove("key"); Set keys = m.keySet(); // // // // // Hashtable an alternative in Java 1.1./1.0 Associate a value object with key object Look up the value association from the Map Remove association from the Map Get the set of keys held by the Map Arrays und Collections können wechselseitig konvertiert werden: Object[] Object[] Object[] Object[] members = set.toArray(); items = list.toArray(); keys = map.keySet().toArray(); values = map.values().toArray(); // // // // Get Get Get Get set elements as an array list elements as an array map key objects as an array map value objects as an array List l = Arrays.asList(elements); // View array as ungrowable list List l = new ArrayList(Arrays.asList(elements)); // Make a growable copy of it So wie java.util.Arrays Methoden zur Manipulation von Arrays anbietet, definiert java.util.Collections Methoden zum Umgang mit Collections. Beispiele: Collections.sort(list); // Sort a list int pos = Collections.binarySearch(list, "key"); // List must be sorted first Collections.max(c); // Find largest element in Collection c Collections.min(c); // Find smallest element in Collection c Collections.reverse(list); // Reverse list Collections.shuffle(list); // Mix up list Informatik B SS 03 180 12 Reflections 12.1 Methoden des Reflection-API Das Reflection-API repräsentiert (reflektiert) Klassen, Schnittstellen und Objekte in der aktuellem JVM. Anwendung: vor allem für Debugger, Class-Browser, GUI-Builder. Dale E. Parson (2000). Using Java Reflection to Automate Extension Language Parsing. ACM SIGPLAN Notices, 35(1), pp. 67–80. Methodenübersicht: Bestimmung der Klasse eines Objekts. Information über Modifikatoren, Felder, Methoden, Konstruktoren, Oberklassen einer Klasse. Information, welche Konstanten und Methoden-Deklarationen zu einem Interface gehören. Erzeugung einer Instanz einer Klasse, deren Namen erst zur Laufzeit bekannt ist. Zugriff und Belegung eines Objekt-Feldes, auch wenn der Name des Feldes erst zur Laufzeit bekannt ist. Aufruf (Invocation) einer Methode eines Objekts, auch wenn die Methode erst zur Laufzeit bekannt ist. Erzeugen eines neuen Arrays, dessen Grösse und Komponenten-Typ erst zur Laufzeit bekannt sind, und Modifikation von Komponenten. 12.2 Die Klassen ‘Class’, ‘Method’, ‘Field’ und ‘Constructor’ Die Klasse Class repräsentiert einen Java-Typ (Klasse, Interface, primitiver Typ). Für jede von der JVM geladene Klasse existiert genau ein Class-Objekt. Dieses Objekt kann durch Aufruf der getClass()-Methode für jede beliebige Instanz besorgt werden. Wie man Klassen mit Class-Methoden inspizieren kann, wird im nächsten Abschnitt dargestellt. public final class Class extends Object implements Serializable { public static Class forName(String name) throws ClassNotFoundException; public Field[] getFields() throws SecurityException; public Method[] getMethods() throws SecurityException; public Constructor[] getConstructors() throws SecurityException; // many more } Informatik B SS 03 181 Die java.lang.reflect-Klassen Method, Field und Constructor repräsentieren Methoden, Felder und Konstruktoren einer Klasse. Entsprechende Objekte werden von entsprechenden get-Methoden eines Class-Objekts zurückgeliefert. Diese Klassen sind Unterklassen von AccessibleObject und implementieren das Member-Interface. Wie Klassenkomponenten inspiziert werden können, wird im nächsten Abschnitt dargestellt. Mit newInstance() können zur Laufzeit neue Objekte erzeugt werden (übernächster Abschnitt). public final class Method extends AccessibleObject implements Member { public String getName(); public Class[] getParameterTypes(); public Class getReturnType(); public Object invoke(Object obj, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; // many more } public final class Field extends AccessibleObject implements Member { public String getName(); public Class getType(); public boolean equals(Object obj); public int hashCode(); // many more } public final class Constructor extends AccessibleObject implements Member { public String getName(); public Class[] getParameterTypes(); public Class[] getExceptionTypes(); public Object newInstance(Object[] initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException; // many more } 12.3 Inspektion von Klassen Das Laufzeitsystem hält für jede Klasse/Schnittstelle ein nicht-änderbares Class-Objekt, das Informationen über die Klasse/Schnittstelle enthält. 12.3.1 Abruf eines ‘Class’ Objekts object .getClass() Class class .getSuperClass() Class Informatik B SS 03 182 Class.forName( String ) class .class Class Class Wenn eine Instanz einer Klasse verfügbar ist, kann dessen Klasse abgefragt werden: Class c = mystery.getClass(); Abruf der Oberklasse: (Circle-Objekt mit Oberklasse Shape) Circle k = new Circle(); Class c = k.getClass(); Class s = c.getSuperClass(); Wenn der Name einer Klasse zur Compile-Zeit bekannt ist, kann das Klassenobjekt abgerufen werden, indem .class an den Namen angehängt wird. Class c = java.awt.Button.class; Wenn der Klassen-Name erst zur Laufzeit verfügbar ist, kann das Klassen-Objekt über den Namen (als String repräsentiert) erzeugt werden. Class c = Class.forName(strg); 12.3.2 Abruf des Klassen-Namens eines Objekts class .getName() String import java.awt.Button; class SampleName { public static void main(String[] args) { Button b = new Button(); printName(b); } static void printName(Object o) { Class c = o.getClass(); String s = c.getName(); System.out.println(s); } } Rückgabe des voll qualifizierten Namens der Klasse als String: java.awt.Button Informatik B SS 03 183 12.3.3 Abruf von Klassen-Modifikatoren class .getModifiers() int Modifier.isPublic( intcode ) bool Modifier.isAbstract( intcode ) bool Modifier.isFinal( intcode ) bool import java.lang.reflect.Modifier; class SampleModifier { public static void main(String[] args) { String s = new String(); printModifiers(s); } public static void printModifiers(Object o) { Class c = o.getClass(); int m = c.getModifiers(); if (Modifier.isPublic(m)) System.out.println("public"); if (Modifier.isAbstract(m)) System.out.println("abstract"); if (Modifier.isFinal(m)) System.out.println("final"); } } Ausgabe: Modifikatoren von String sind: public final 12.3.4 Abruf von Oberklassen class .getSuperclass() Class import java.awt.Button; class SampleSuper { public static void main(String[] args) { Button b = new Button(); printSuperclasses(b); } static void printSuperclasses(Object o) { Class subclass = o.getClass(); Class superclass = subclass.getSuperclass(); Informatik B SS 03 184 } } while (superclass != null) { String className = superclass.getName(); System.out.println(className); subclass = superclass; superclass = subclass.getSuperclass(); } Die Oberklasse von Button ist Component und deren Oberklasse ist Object. java.awt.Component java.lang.Object 12.3.5 Abruf des implementierten Interfaces einer Klasse class .getInterfaces() Class[] import java.io.RandomAccessFile; import java.io.IOException; class SampleInterface { public static void main(String[] args) { try { RandomAccessFile r = new RandomAccessFile("myfile", "r"); printInterfaceNames(r); } catch (IOException e) { System.err.println(e); } } } static void printInterfaceNames(Object o) { Class c = o.getClass(); Class[] theInterfaces = c.getInterfaces(); for (int i = 0; i < theInterfaces.length; i++) { String interfaceName = theInterfaces[i].getName(); System.out.println(interfaceName); } } Die Klasse RandomAccessFile implementiert die Interfaces DataOutput und DataInput: java.io.DataOutput java.io.DataInput 12.3.6 Interface oder Klasse? class .isInterface() bool import java.util.Observer; import java.util.Observable; Informatik B SS 03 185 class SampleCheckInterface { public static void main(String[] args) { Class observer = Observer.class; Class observable = Observable.class; verifyInterface(observer); verifyInterface(observable); } } static void verifyInterface(Class c) { String name = c.getName(); if (c.isInterface()) { System.out.println(name + " is an interface."); } else { System.out.println(name + " is a class."); } } Ausgabe: java.util.Observer is an interface. java.util.Observable is a class. 12.3.7 Abruf von Klassen-Feldern class .getFields() Field[] field .getType() Class import java.lang.reflect.Field; import java.awt.GridBagConstraints; class SampleField { public static void main(String[] args) { GridBagConstraints g = new GridBagConstraints(); printFieldNames(g); } } static void printFieldNames(Object o) { Class c = o.getClass(); Field[] publicFields = c.getFields(); for (int i = 0; i < publicFields.length; i++) { String fieldName = publicFields[i].getName(); Class typeClass = publicFields[i].getType(); String fieldType = typeClass.getName(); System.out.println("Name: " + fieldName + ", Type: " + fieldType); } } Beginn der Ausgabe: Informatik B SS 03 186 Name: Name: Name: Name: Name: Name: RELATIVE, Type: int REMAINDER, Type: int NONE, Type: int BOTH, Type: int HORIZONTAL, Type: int VERTICAL, Type: int ... 12.3.8 Abruf von Klassen-Konstruktoren class .getConstructors() Constructors[] constructor .getParameterTypes() Class[] import java.lang.reflect.Constructor; import java.awt.Rectangle; class SampleConstructor { public static void main(String[] args) { Rectangle r = new Rectangle(); showConstructors(r); } static void showConstructors(Object o) { Class c = o.getClass(); Constructor[] theConstructors = c.getConstructors(); for (int i = 0; i < theConstructors.length; i++) { System.out.print("( "); Class[] parameterTypes = theConstructors[i].getParameterTypes(); for (int k = 0; k < parameterTypes.length; k ++) { String parameterString = parameterTypes[k].getName(); System.out.print(parameterString + " "); } System.out.println(")"); } } } ( ( ( ( ( ( ( ) int int ) int int int int ) java.awt.Dimension ) java.awt.Point ) java.awt.Point java.awt.Dimension ) java.awt.Rectangle ) 12.3.9 Abruf von Methoden-Information class .getMethods() Method[] method .getReturnType() Class Informatik B SS 03 method .getParameterTypes() 187 Class[] import java.lang.reflect.Method; import java.awt.Polygon; class SampleMethod { public static void main(String[] args) { Polygon p = new Polygon(); showMethods(p); } static void showMethods(Object o) { Class c = o.getClass(); Method[] theMethods = c.getMethods(); for (int i = 0; i < theMethods.length; i++) { String methodString = theMethods[i].getName(); System.out.println("Name: " + methodString); String returnString = theMethods[i].getReturnType().getName(); System.out.println(" Return Type: " + returnString); Class[] parameterTypes = theMethods[i].getParameterTypes(); System.out.print(" Parameter Types:"); for (int k = 0; k < parameterTypes.length; k ++) { String parameterString = parameterTypes[k].getName(); System.out.print(" " + parameterString); } System.out.println(); } } } Name: equals Return Type: boolean Parameter Types: java.lang.Object Name: getClass Return Type: java.lang.Class Parameter Types: Name: hashCode Return Type: int Parameter Types: . . Name: intersects Return Type: boolean Parameter Types: double double double double Name: intersects Return Type: boolean Parameter Types: java.awt.geom.Rectangle2D Informatik B SS 03 188 Name: translate Return Type: void Parameter Types: int int 12.4 Manipulation von Objekten zur Laufzeit Erzeugung und Manipulation von Objekten, deren Klassennamen erst zur Laufzeit bekannt sind. 12.4.1 Dynamische Erzeugung von Objekten Der new-Operator kann nicht auf Variablen angewendet werden. Stattdessen: newInstance(). import java.awt.Rectangle; class SampleNoArg { public static void main(String[] args) { Rectangle r = (Rectangle) createObject("java.awt.Rectangle"); System.out.println(r); } static Object createObject(String className) { Object object = null; try { Class classDefinition = Class.forName(className); object = classDefinition.newInstance(); } catch (InstantiationException e) { System.err.println(e); } catch (IllegalAccessException e) { System.err.println(e); } catch (ClassNotFoundException e) { System.err.println(e); } return object; } } Ausgabe: java.awt.Rectangle[x=0,y=0,width=0,height=0] 12.4.2 Exceptions beim dynamischen Erzeugen von Objekten ClassNotFoundException: Es wird versucht, ein Objekt einer Klasse zu erzeugen, die nicht (auf dem aktuellen Pfad) existiert. IllegalAccessException: Klasse darf nicht zugegriffen werden (z. B. nicht public) InstantiationException: Fehler bei der Objekterzeugung (z.B. es gibt keinen Konstruktor ohne Argumente) IllegalArgumentException: beim Aufruf von Konstruktor/Methode mit Argumenten. (RuntimeException) InvocationTargetException: “Meta”-Exception; es tritt Exception innerhalb des aufgerufenen Konstruktors (bzw. der aufgerufenen Methode) auf. Informatik B SS 03 189 12.4.3 Dynamische Erzeugung mit Konstruktor-Argumenten import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.awt.Rectangle; class SampleInstance { public static void main(String[] args) { Rectangle rectangle; Class rectangleDefinition; Class[] intArgsClass = new Class[] {int.class, int.class}; Integer height = new Integer(12); Integer width = new Integer(34); Object[] intArgs = new Object[] {height, width}; Constructor intArgsConstructor; try { rectangleDefinition = Class.forName("java.awt.Rectangle"); intArgsConstructor = rectangleDefinition.getConstructor(intArgsClass); rectangle = (Rectangle) createObject(intArgsConstructor, intArgs); } catch (ClassNotFoundException e) { System.err.println(e); } catch (NoSuchMethodException e) { System.err.println(e); } } public static Object createObject(Constructor constructor, Object[] arguments) { System.out.println ("Constructor: " + constructor); Object object = null; try { object = constructor.newInstance(arguments); System.out.println ("Object: " + object); return object; } catch (InstantiationException e) { System.err.println(e); } catch (IllegalAccessException e) { System.err.println(e); } catch (IllegalArgumentException e) { System.err.println(e); } catch (InvocationTargetException e) { Informatik B SS 03 190 System.err.println(e); } return object; } } Ausgabe: Constructor: public java.awt.Rectangle(int,int) Object: java.awt.Rectangle[x=0,y=0,width=12,height=34] 12.4.4 Abruf und Belegung von Feldern class .getField( string ) Field field .set( object , object ) field .get( object ) import java.lang.reflect.Field; import java.lang.reflect.NoSuchFieldException; import java.awt.Rectangle; class SampleSet { public static void main(String[] args) { Rectangle r = new Rectangle(100, 20); System.out.println("original: " + r.toString()); modifyWidth(r, new Integer(300)); System.out.println("modified: " + r.toString()); } static void modifyWidth(Rectangle r, Integer widthParam ) { Field widthField; Integer widthValue; Class c = r.getClass(); try { widthField = c.getField("width"); widthField.set(r, widthParam); } catch (NoSuchFieldException e) { System.err.println(e); } catch (IllegalAccessException e) { System.err.println(e); } } } Ausgabe: original: java.awt.Rectangle[x=0,y=0,width=100,height=20] modified: java.awt.Rectangle[x=0,y=0,width=300,height=20] 12.4.5 Method Invocation class .getMethod( string , parTypes) Method method .invoke( object , arguments[] ) Informatik B SS 03 191 import java.lang.reflect.Method; import java.lang.reflect.NoSuchMethodException; import java.lang.reflect.InvocationTargetException; class SampleInvoke { public static void main(String[] args) { String firstWord = "Hello "; String secondWord = "everybody."; String bothWords = append(firstWord, secondWord); System.out.println(bothWords); } public static String append(String firstWord, String secondWord) { String result = null; Class c = String.class; Class[] parameterTypes = new Class[] {String.class}; Method concatMethod; Object[] arguments = new Object[] {secondWord}; try { concatMethod = c.getMethod("concat", parameterTypes); result = (String) concatMethod.invoke(firstWord, arguments); } catch (NoSuchMethodException e) { System.err.println(e); } catch (IllegalAccessException e) { System.err.println(e); } catch (InvocationTargetException e) { System.err.println(e); } return result; } Ausgabe: Hello everybody. 12.5 Bemerkungen Reflections erlauben es, Entscheidungen auf die Laufzeit zu verschieben, die man zur Compile-Zeit nicht treffen will oder kann (z.B. Erzeugung von Objekten von zur Compile-Zeit unbekannten Klassen). Dadurch müssen bestimmte Arbeiten, die normalerweise der Compiler leistet, zur Laufzeit erledigt werden (z.B. NoSuchMethodException). Man sollte nicht unbedingt das Reflection API verwenden, wenn sich andere Möglichkeiten ergeben. Beispiel: Anstatt Method-Objekte zu benutzen, ist es meist sinnvoller, ein Interface zu definieren und in der Klasse, die eine bestimmte Methode haben soll, zu implementieren. 192 Informatik B SS 03 Bei dynamischer Erzeugung und Nutzung von Objekten (Methoden) können viele Exceptions auftreten. Dynamisches Laden von Klassen kann einfach mit Class realisiert werden: Class c = Class.forName(classname) (siehe z.B. Kapitel “Collections”, Test) Alternativen: java.lang.ClassLoader oder java.net.URLClassLoader. Informatik B SS 03 193 13 Multi-Threading – Grundlagen der Nebenläufigkeit 13.1 Sequentialität, Determinismus, Determiniertheit Sequentielles Programm: Es gibt genau einen nächsten Ausführungsschritt, und alle Schritte werden nacheinander ausgeführt. v = x; w = x; x = y; und erhalten den Wert von / , danach erhält / den Wert von Deterministisches Programm: Programm, dessen Ablauf eindeutig vorherbestimmt ist; vgl. deterministische Automaten: bei gegebener Eingabe gibt es in jedem Zustand genau einen klar definierten Folgezustand. Determiniertes Programm: Jede Ausführung liefert bei gleichen Ausgangswerten das identische Ergebnis. (Ein deterministisches Programm ist immer determiniert, der Umkehrschluss gilt nicht zwangsläufig.) Darstellung mit Vor- und Nachbedingungen (vgl. Hoare-Kalkül): {x = i, y = j} v = x; w = x; x = y; {v = w = i, x = y = j} 13.1.1 Nebenläufigkeit Verzicht auf Sequentialität: Nebenläufigkeit (concurrency) – Anweisungen können parallel ausgeführt werden (mehrere Prozessoren oder Threads). – Anweisungen können sequentiell in beliebiger Reihenfolge ausgeführt werden. Sequentielle Programme lassen nicht explizit erkennen, ob die sequentielle Ordnung zur Lösung eines gegebenen Problems tatsächlich erforderlich ist. Es ist schwierig, in einem sequentiellen Programm die potentiell parallelisierbaren Aktionen herauszufinden. Nebenläufigkeit explizit sichtbar machen: {x = i, y = j} conc w = x || v = x end conc; x = y; {v = w = i, x = y = j} Informatik B SS 03 194 willkürliche Sequentialisierung: w = x; v = x; x = y; v = x; w = x; x = y; Im Bereich KI-Planung werden (bei partial order Planern) unabhängig lösbare Teilprobleme automatisch beim Planaufbau erkannt (z.B. NOAH, Graphplan). 13.1.2 Nicht-Determinismus Manchmal sind nicht-deterministische Programme sinnvoll: Vermeidung von Überspezifikation bei Gewährleistung von Determiniertheit. Beispiel: Maximum zweier Zahlen if (a > b) max = a; else max = b; 4 ( 4 ( 4 Dieser Algorithmuus ist bzgl. der Problemstellung überspezifiziert: wenn ist, dann wird zurückgeliefert, obwohl es egal ist, ob oder zurückgeliefert wird. Nicht-deterministische Auswahl mit Wächtern: select a >= b -> max = a [] a <= b -> max = b end select [] “oder wenn” Wächter (guard) regelt, ob eine Alternative prinzipiell ausgewählt werden kann. Alternativen sind nicht notwendigerweise disjunkt. Auswertung der Wächter in willkürlicher Reihenfolge. 13.1.3 Nicht-Determiniertheit In der Regel möchte man – auch bei nicht-deterministischen Programmen – determinierte Ergebnisse. Ergebnisse sind determiniert genau dann, wenn keine Schreib-/Schreib- und keine Schreib-/Lese-Konflikte auftreten (Veränderung von Variablen-Werten). Erwünschte Nicht-Determiniertheit: Beispiel: Bestimme den kürzesten Weg in einem Graphen. Wenn es mehrere kürzeste Wege gibt, ist es egal, welcher davon zurückgeliefert wird. Beispiel: {x = i, y = j} conc w = x || v = x || x = y; end conc; {v = ?, w = ?, x = y = j} Informatik B SS 03 195 willkürliche Sequentialisierung: w = x; v = x; x = y; v = x; x = y; w = x; x = y; w = x; v = x; ... ist nicht-determiniert. 13.1.4 Verzahnung conc w = x; v = w+1 || y = x; z = y end conc Mögliche Sequentialisierungen (1) w = x; (2) y = x; v = w+1; z = y; y = x; w = x; z = y; v = w+1; v z v z z v z v Weitere Verzahnungen (3) (4) (5) (6) w w y y = = = = x; x; x; x; y y w w = = = = x; x; x; x; = = = = w+1; y; w+1; y; = = = = y; w+1; y; w+1; Bei der Sequentialisierung nebenläufiger Anweisungsfolgen muss nur die in jeder Anweisungsfolge vorgegebene Sequenz beachtet werden. Ansonsten kann beliebig verzahnt werden (interleaving). Hier ist das Ergebnis trotz nicht-deterministischer Auswahl determiniert, da keine Konflike auftreten und die vorgeschriebene Sequentialisierung erhalten bleibt. Bei jeder Programmausführung kann eine andere Verzahnung auftreten. Die Programmausführung ist nicht reproduzierbar. Fehler können sich je nach Verzahnung nur gelegentlich bemerkbar machen. nebenläufige Programm sind schwerer zu testen (validieren) und zu verifizieren als sequentielle! Vielfalt möglicher Zustandsübergänge bei der Programmausführung. 13.2 Nebenläufigkeit in Java: Threads Greifen verschiedene (nebenläufige) Aktivitäten auf verschiedene Adressräume zu, so spricht man von (parallelen) Prozessen. Findet Nebenläufigkeit im selben Adressraum statt (z.B. eine Java VM), so spricht man von Threads (“Programmfäden”). (Addressraum: Menge aller zugreifbaren Speicherbereiche.) Informatik B SS 03 196 Thread Runnable <<interface>> MAX_PRIORITY ... run() sleep() run() start() join() ... ErsterThread run() ZweiterThread runs run() Abbildung 51: Definition von Threads In Java sind Threads integraler Bestandteil der Sprache und nahtlos in das Konzept der Objekt-Orientierung eingebaut (Threads sind Objekte). Probleme: Synchronisation, Deadlocks 13.2.1 Definition von Threads Zwei Möglichkeiten: Als Unterklasse der Klasse Thread. Durch Implementieren der Runnable-Schnittstelle (immer dann, wenn Klasse bereits eine andere Klasse erweitert; z.B. bei Applets) 13.2.2 Die Klasse Thread Die run()-Methode definiert, was ein Thread (quasi gleichzeitig mit anderen Threads) ausführen möchte (analog zu main()). Wie bei allen Methoden kann auch die run()-Methode auf Methoden und Felder anderer Objekte zugreifen. Informatik B SS 03 197 Wichtige Methoden: – start(): Starten des Threads (Aufruf der run()-Methode beim Runnable durch das System). – join(): Warten auf das Zuendegehen eines Threads. – sleep(long): Schlafenlegen eines Threads. – yield(): Pausieren, um anderen Threads eine Chance zu geben. Prioritäten: setPriority(int) / getPriority() MIN_PRIORITY = 1 MAX_PRIORITY = 10 NORM_PRIORITY = 5 Konventionen: 10 7-9 4-6 2-3 1 Crisis Management Interactive, event-driven IO-bound Background computation Run only, if nothing else can Deprecated (Termination bzw. Unterbrechung in inkonsistenten Zuständen möglich): stop(), suspend(), resume(). (Anmerkung: Später wird eine sichere Realisierung des Unterbrechens von Threads über interrupt() dargestellt.) // Set a thread t to lower than normal priority t.setPriority(Thread.NORM_PRIORITY-1); // Set a thread to lower priority than the current thread t.setPriority(Thread.currentThread().getPriority()-1); // Threads that don’t pause for I/O should explicitely yield the CPU // to give other threads with the same priority a chance to run. Thread t = new Thread(new Runnable() { public void run() { for(int i = 0; i < data.length; i++) { // Loop through a bunch of data process(data[i]); // Process it if (i % 10 == 0) // But after every 10 iterations Thread.yield(); // Let other threads run. } } }); 13.2.3 Einfaches Beispiel: ‘ThreadDemo’ class ErsterThread extends Thread { public void run () { for (int i = 0; i < 10; i++) Informatik B SS 03 198 try { Thread.sleep( Math.round (1000 * Math.random ()) ); System.out.println (this + " " + i); } catch (InterruptedException e) { System.err.println (e); } } } class ZweiterThread implements Runnable { public void run () { for (int i = 0; i < 10; i++) try { Thread.sleep (Math.round (1000 * Math.random ())); System.out.println (Thread.currentThread().toString () + " " + i); } catch (InterruptedException e) { System.err.println (e); } } } public class ThreadDemo { static public void main (String args[]) { ErsterThread thread1 = new ErsterThread (); thread1.start (); Thread thread2 = new Thread(new ZweiterThread ()); thread2.start (); try { thread1.join (); thread2.join (); } catch (InterruptedException e) { System.err.println (e); } } } Thread[Thread-5,5,main] Thread[Thread-5,5,main] Thread[Thread-4,5,main] Thread[Thread-5,5,main] Thread[Thread-5,5,main] Thread[Thread-4,5,main] Thread[Thread-5,5,main] Thread[Thread-4,5,main] Thread[Thread-5,5,main] Thread[Thread-4,5,main] Thread[Thread-5,5,main] Thread[Thread-4,5,main] Thread[Thread-5,5,main] Thread[Thread-5,5,main] Thread[Thread-5,5,main] 0 1 0 2 3 1 4 2 5 3 6 4 7 8 9 Informatik B SS 03 199 start thread2 start thread1 join thread1 join thread2 main thread1 thread2 Zeit Abbildung 52: Zusammenspiel von Threads in ThreadDemo Thread[Thread-4,5,main] Thread[Thread-4,5,main] Thread[Thread-4,5,main] Thread[Thread-4,5,main] Thread[Thread-4,5,main] 5 6 7 8 9 13.2.4 Erläuterungen zu ‘ThreadDemo’ Klasse Thread implementiert Runnable. Dort ist eine Methode run() spezifiziert. Mit der Methode start() wird ein Thread gestartet, indem run() aufgerufen wird. Methode join() wartet auf Beendigung des Thread. ErsterThread ist Unterklasse von Thread. Die run-Methode von Thread wird überschrieben. sleep() legt einen Thread für eine spezifizierte Zeit (in Millisekunden) schlafen. Hier wird zufällig eine Zeit zwischen 0 und 1000 ms gewartet und dann der Wert des Schleifenzählers ausgegeben. Mit toString() wird die Information des aktuellen Thread-Objekts zurückgeliefert: sein Name, seine Priorität (zwischen 1 und 10), und sein erzeugender Thread. ZweiterThread implementiert Runnable. Hier muss explizit eine Instanz von Thread erzeugt werden. Die Instanz von ZweiterThread wird einem Thread-Objekt übergeben, der die run()-Methode “betreibt”. ErsterThread kann sich selber schlafen legen. ZweiterThread legt die benutzte Instanz von Thread schlafen. Bei Programmstart existiert immer automatisch ein erster Thread, dessen Ausführung in der main()-Methode beginnt. Dieser main-Thread startet nun zwei weitere Threads (fork) und wartet (join()) auf das Ende dieser Threads. Bei jedem Ablauf ergibt sich nicht-deterministisch eine andere Reihenfolge der Ausgaben. Informatik B SS 03 200 vorhanden bereit laufend fertig wartend (blockiert) vorhanden: mit new als Instanz erzeugt. bereit: ablaufbereit, aber die Ablaufsteuerung hat alle CPUs an andere Threads vergeben. laufend: hat eine CPU des Systems. wartend: es fehlen Betriebsmittel (z. B. File ist nicht zum Lesen/Schreiben freigegeben), sleep() oder Warten auf Signal eines anderen Threads. fertig: Ende der run()-Methode erreicht. (Anmerkung: Diese Abbildung wird später für Monitore verfeinert.) Abbildung 53: Zustände von Threads 13.2.5 Zustände von Threads Threads haben verschiedene Zustände, die sie zum Teil selbst beeinflussen können, die aber auch von ihrer Umgebung manipulierbar sind (siehe Abb. 53). 13.3 Kooperierende und Konkurrierende Prozesse 13.3.1 Kooperierende Prozesse Nebenläufige Prozesse, die Koordination erfordern, heissen voneinander abhängig. Grundlegende Kommunikationsformen (siehe Abb. 54): – Erzeuger/Verbraucher Muster (producer/consumer): Ein Prozess nimmt Daten auf, die ein anderer erzeugt hat. Beispiel: Buchungssystem im Supermarkt – Auftraggeber/Auftragnehmer Muster (client/server) Beispiel: Verkehrsleitzentrale. Beispiel: Buchungssystem im Supermarkt Strichcode-Leser, Buchungsprozessor, Drucker Jedem Gerät wird ein Prozess zugeordnet (wichtiges Programm-Strukturierungs-Prinzip) Leseprozess erfasst über Strichcodeleser die Kennung des Artikels Information wird an Buchungsprozess weitergegeben, der Bezeichung und Preis des Artikels feststellt Informatik B SS 03 201 Produzent Auftraggeber 1 Konsument 2 Auftragnehmer Abbildung 54: Arten der Kooperation Butter Kaffee ... Lesen Buchen 2 1,29 1 7,99 Drucken Abbildung 55: Beispiel: Buchungssystem im Supermarkt Werte werden an Druckprozess weitergegeben, der sie auf Drucker ausgibt. Prozesse sind nebenläufig: Während der Buchungsprozess alte Eingabedaten verarbeitet, kann der Leseprozess neue Daten lesen. Prozesse sind abhängig: Buchungsprozess kann erst arbeiten, wenn er Daten vom Leseprozess erhalten hat. Beispiel: Verkehrsleitzentrale gibt Auskunft über aktuellen Straßenzustand und schlägt Fahrstrecken vor. Prozess im Bordcomputer (Client) stellt Anfrage an Auskunftsprozess in der Zentrale (Server). Auskunftsprozess gibt die Information erst, wenn nach ihr gefragt wird. Prozess im Bordcomputer wartet, bis er die gewünschte Information erhält. Prozesse sind wechselseitig abhängig. 13.3.2 Konkurrierende Prozesse Abhängigkeit nebenläufiger Prozesse kann auch aus Konkurrenz der Prozesse (um gemeinsame Ressourcen) resultieren. Die Aktivität eines Prozesses behindert einen anderen Prozess. Beispiel: Eingleisige Teilstrecke im Eisenbahnverkehr Informatik B SS 03 202 Beispiel: Drucker im Mehrbenutzersystem Schreib-/Schreib-Konflikt: Beispiel conc x = x + 1 || x = x + 1 end conc; kann zu ein- oder zweimaliger Inkrementierung von / führen. Folgende Verzahnung führt zur einmaligen Inkrementierung: (Notation in Pseudo-Assembler mit P1 P2 LOAD x, r1 LOAD x, r2 INCR r1 INCR r2 STORE r1,x STORE r2,x x i i i i i i+1 i+1 als Prozesse und r1 ? i i i+1 i+1 i+1 i+1 als Register) r2 ? ? i i i+1 i+1 i+1 (“STORE r1,x direkt nach “INCR r1” resultiert in zweimaliger Inkrementierung.) Schreib-/Lese-Konflikt: Beispiel int schecks; int gesamt; void einnahme(boolean zahlungsart, int betrag) { // zahlungsart false = bar, true = scheck if (zahlungsart) schecks = schecks + betrag; gesamt = gesamt + betrag; } void kassensturz () { System.out.println("Einnahmen: " + gesamt); if (gesamt > 0) System.out.println("Davon prozentual als Scheckzahlung: " + 100 * schecks/gesamt); } Gegebener Kontostand sei 100,- Euro. Wenn einnahme(1, 200) und kassensturz() nebenläufig abgearbeitet werden, kann es zu inkonsistenten Informationen kommen: P1 P2 schecks+200 kassensturz gesamt+200 gesamt 100 100 100 300 scheck 0 200 200 200 Output 200% Informatik B SS 03 203 13.4 Synchronisation Die gezielte Sequentialisierung nebenläufiger Prozesse heisst Synchronisation. Synchronisierte Prozesse müssen Information austauschen, also kommunizieren. Kommunikation kann realisiert werden durch: (1) gemeinsamen Datenbereich, auf den mehrere Prozesse zugreifen können. (2) Operationen, die Daten vom Datenbereich eines Prozesses in den des anderen transportieren. Anstelle ganzer Prozesse können auch kritische Abschnitte (Anweisungsblöcke) synchronisiert werden. @ @ Arten von Synchronisation: ( und seien verschiedenen Prozessen zugeordnete Aktivitäten) @ @ @ @ Kausale Abhängigkeit (Produzent/Konsument): Transitiver Abschluss (partielle Ordnung der Aktivitäten ) Einseitige Synchronisation (blockiert wird höchstens ) erzwingt Reihenfolge. (vgl. Supermarkt-Buchungssystem) @ @ @ Ausschluss der Nebenläufigkeit (Schreib-/Schreib- und Schreib-/Lese-Konflikte): Symmetrie von “nicht zusammen mit” Mehrseitige Synchronisation: Bei gleicher Priorität ist keine Reihenfolge festgelegt. (vgl. eingleisiges Eisenbahnstück) 13.5 Monitore in Java 13.5.1 Synchronized Wird mehr als ein Thread verwendet, so tritt häufig Synchronisations-Bedarf auf. Zugriff auf kritische Daten muss kontrolliert werden. Hierzu wird ein Monitor-Objekt eingesetzt, das den Zugang von Threads zu den Daten steuert. (Jedes beliebige Objekt kann als Monitor verwendet werden.) In Java wird synchronized verwendet, um einen kritischen Bereich zu definieren und diesem Bereich ein Monitor-Objekt zuzuordnen. synchronized(monitor) für Blöcke (kritische Abschnitte) Wenn sich ein synchronized(this)-Block auf eine komplette Methode bezieht, kann alternativ der Modifikator synchronized für diese Methode angegeben werden. Der Modifikator synchronized kann auch für Klassen-Methoden angegeben werden. Hier wird das Klassen-Objekt zum Monitor. Informatik B SS 03 204 Monitor Zugang zum Monitor geschützte Daten blockiert für alle anderen Threads Abbildung 56: Monitor Kommunikation bei Monitoren: – wait() (Warte und gib den Monitor frei), – notify() (Benachrichtige einen auf den Monitor wartenden Thread) und – notifyAll() (Benachrichtige alle auf den Monitor wartenden Threads). Diese Methoden gehören nicht zur Klasse Thread, sondern zu einem Monitor-Objekt. synchronized void einnahme(boolean zahlungsart, int betrag) { if (zahlungsart) schecks = schecks + betrag; gesamt = gesamt + betrag; } synchronized void kassensturz () { System.out.println("Einnahmen: " + gesamt); if (gesamt > 0) System.out.println("Davon prozentual als Scheckzahlung: " + 100 * schecks/gesamt); } // This method swaps two array elements in a synchronized block public static void swap(Object[] array, int index1, int index2) { synchronized(array) { Object tmp = array[index1]; array[index1] = array[index2]; array[index2] = tmp; } } Informatik B SS 03 vorhanden 205 bereit laufend wartend wenn Zutritt zum Monitor fertig wait() notify() eines anderen Threads blockiert Monitor durch anderen Thread besetzt Abbildung 57: Zustände von Threads (bzgl. Monitor-Objekt) 13.5.2 Funktion von ‘synchronized’ Wenn in einer Klasse eine Methode mit dem Modifikator synchronized angegeben ist – also this als Monitor definiert wird – so verwenden verschiedene Aufrufe bei verschiedenen Instanzen dieser Klasse verschiedene Monitore. Monitore können “Wettrennen” (race-conditions) zwischen Threads verhindern. Zu jedem Zeitpunkt kann nur eine der synchronized()-Aktivitäten für dasselbe Monitor-Objekt aufgerufen werden. Beispiel Produzent/Konsument bzw. Schreib-/Lese-Konflikt: Falls derjenige Thread, der die Daten ausliest, unterbrochen wird und ein zweiter Thread mit Schreibzugriffen aktiv wird, so muss der zweite Thread warten, bis der erste Thread (Leser) den Monitor verlassen hat. Java-Monitore sind “re-entrant”: Ein Thread, der im Monitor ist, wird nicht durch sich selbst dadurch blockiert, dass er eine andere auf denselben Monitor synchronisierte Methode aufruft. 13.5.3 Warten auf Ereignisse mit Monitoren Beispiel (Produzent/Konsument): Thread wartet auf Daten, die von einem anderen Thread geliefert werden müssen. Aktives Warten blockiert CPU unnötig: Schleife, die ständig eine Variable abfragt. Alternativ: wait() Thread verbraucht keine CPU-Zeit, verlässt den Monitor und “schläft”. Ein mit notify() bzw. notifyAll() aufgeweckter Thread wartet wieder auf Zugang zum Monitor (ist blockiert, solange bis Monitor freigegeben ist). wait() versus sleep() Informatik B SS 03 206 – sleep() ist eine Thread-Methode während wait() eine Monitor-Methode ist. – Wenn ein Thread im Besitz des Monitors sich schlafen legt, so bleibt er in Besitz des Monitors. Ein wartender Thread gibt dagegen den Monitor ab. – Wenn ein wartender Thread aufwacht, kann es sein, dass er nicht ausgeführt werden kann, weil er den Monitor nicht (sofort) wieder belegen kann. – Ein wartender Thread kann für immer warten, wenn nie ein notify erfolgt. – Sowohl wait() als auch sleep() können durch interrupt() unterbrochen werden. (Einbettung in try{ ... } catch (InterruptedException e) { ... } ) 13.6 Beispiel: Produzent/Konsument public class ConsumerProducer { public static void main (String[] args) { Object[] d = { null }; Producer p = new Producer(d, 0); Producer p2 = new Producer(d, 100); Consumer c = new Consumer(d); p.start(); p2.start(); c.start(); try { p.join(); p2.join(); c.join(); } catch (InterruptedException e) { System.err.println(e); } } public static class Producer extends Thread { protected Object[] data; protected int count; protected int offset; public Producer (Object[] d, int i) { data = d; offset = i; } public void run () { Informatik B SS 03 } 207 Integer o; while (count < 8) { o = new Integer (count++ + offset); while (!store(o)); report(o); } protected boolean store (Object o) { if (data[0] == null) { try { sleep(500); } catch (InterruptedException e) { System.err.println(e); } data[0] = o; return true; } else return false; } protected void report(Object o) { System.out.println(this + " produced " + o); } } // end Producer public static class Consumer extends Thread { protected Object[] data; protected int count; public Consumer (Object[] d) { data = d; } public void run () { while (count < 16) { Object o; o = fetch(); if (o != null) { report(o); emptyStore(); count++; } } } protected Object fetch() { return data[0]; } protected void report(Object o) { System.out.println(this + " consumed " + o); Informatik B SS 03 208 } protected void emptyStore() { data[0] = null; } } // end Consumer } Anmerkung: Producer und Consumer sind als nested top-level Klassen in derselben Datei wie ConsumerProducer. Producer und Consumer sind Unterklassen von Thread, die run-Methoden werden also nebenläufig ausgeführt. Der Consumer soll ein Objekt abholen, der Producer soll ein Objekt ablegen. Problem: Mehrere Threads wollen lesend bzw. schreibend auf data zugreifen. Klassischer Schreib-Lese-Konflikt! synchronized für die run()-Methoden macht keinen Sinn: this würde als Monitor verwendet, und jeder Thread hätte damit seinen eigenen Monitor! Synchronisationsblöcke in den jeweiligen run-Methoden mit data als Monitor verhindern zwar gleichzeitigen Zugriff, bergen aber die Gefahr von deadlocks. Lösung: Conditional Critical Region (siehe Kapitel “Semaphoren und Deadlocks”). Informatik B SS 03 209 14 Multi-Threading: Semaphoren und Deadlocks 14.1 Semaphoren 14.1.1 Konzept Von Dijkstra zur Synchronisation nebenläufiger Prozesse eingeführt. Begriff aus der Seefahrt: Optisches Signal zum Passeeren/passer (Passieren) und Vrijgeven/verlaat (Freigeben). Idee: Prozess wird im Synchronisationsfall blockiert und in Warteschlange eingeordnet. Semaphoren sind abstrakte Datentypen: – Objekte bestehen aus Zähler und Warteschlange. – Operation P: Zähler wird um eins erniedrigt. Wenn negativer Wert, dann ist Prozess blockiert. (Warten auf das Eintreten einer Bedingung) – Operation V: Zähler wird um eins erhöht. (Signalisieren des Eintretens einer Bedingung). Die Warteschlange muss in Java nicht explizit definiert werden. Sie existiert implizit als Menge der blockierten Prozesse. Durch notify() wird ein Prozess aktiviert (nicht unbedingt der, der am längsten wartet). 14.1.2 Klasse ‘Semaphore’ /** A class for the classical semaphore -- A.T. Schreiner */ public class Semaphore { /** the value, nonnegative. */ protected int n; /** set initial value. */ public Semaphore (int n) { this.n = n; } /** passer: decrement; may block until decrementing is possible. */ public synchronized void P () { while (n <= 0) try { wait(); // blockiert } catch (InterruptedException e) { } -- n; } /** verlaat: increment; inform if necessary. */ public synchronized void V () { if (++ n > 0) Informatik B SS 03 210 notify(); // nur _ein_ wartender Prozess wird aus Blockierung entlassen } } 14.1.3 Einseitige und Mehrseitige Synchronisation Einseitige Synchronisation Jeder Synchronisationsbedingung wird eine Semaphorvariable zugeordnet. -Operation in einem Prozess wartet auf Prozess. -Operation in einem anderen Semaphore s = new Semaphore(0); void process1 () { // ... s.V(); // Ereignis signalisieren // ... } void process2 () { // ... s.P(); // Ereignis abwarten // ... } Mehrseitige Synchronisation Kritischer Abschnitt (critical region): Bereich, zu dem nur eine Aktivität zu einer Zeit Zugang hat. Initialwert des Semaphorzählers legt die maximale Anzahl von Prozessen fest, die den kritischen Abschnitt betreten dürfen. und umschliessen kritischen Abschnitt. Semaphore s = new Semaphore(1); void process1 () { // ... s.P(); // ... kritischer Abschnitt s.V(); // ... } void process2 () { // ... s.P(); // ... kritischer Abschnitt s.V(); // ... } 14.1.4 Erzeuger-/Verbraucher-Problem mit Semaphoren /** semaphore demonstration with member class pattern. */ public class ProdConsDemo { /** controls access to data. */ protected Semaphore available = new Semaphore(0); /** signals that data has been copied. */ protected Semaphore copied = new Semaphore(1); Informatik B SS 03 211 /** "global" buffer. */ protected String data; /** producer: has command line copied by several threads in consumer. */ public static void main (String[] args) { if (args != null) { int nt = args.length+1 / 2; // divide by 2 ProdConsDemo producer = new ProdConsDemo(); // owns semaphores for (int n = 0; n < nt; ++ n) producer.new Consumer(""+n).start(); // performs copy // int as String producer.produce (args); producer.terminateConsumers (nt); } } /** producing some data */ protected void produce (String[] args) { for (int n = 0; n < args.length; ++ n) { copied.P(); // critical section data = args[n]; // writes information available.V(); } } /** terminate consumer threads */ protected void terminateConsumers (int nt) { for (int n = 0; n < nt; ++ n) { // done: inform all copied.P(); data = null; // termination marker available.V(); } } /** consumer thread: copies data to standard output until data == null. */ protected class Consumer extends Thread { /** save name. */ public Consumer (String name) { super(name); } /** performs copy. */ public void run () { String copy; do { Informatik B SS 03 212 available.P(); copy = data; copied.V(); // critical section if (copy != null) System.out.println(getName()+" "+copy); } while (copy != null); } } } $ 1 1 0 0 0 0 0 0 0 1 java ProdConsDemo a b c d e f g h i j b c a e f g h i j d Erläuterungen: Das Produzent/Konsument Muster ist hier mit Member-Klasse realisiert: Im main-Thread wird produziert (ein Produzent). Es werden halb soviele Consumer-Threads generiert, wie Argumente über die Eingabezeile angegeben werden. Über das Feld data werden String-Objekte ausgetauscht. Zwei Semaphoren: copied schützt Schreibzugriff auf data; available schützt Lesezugriff. 14.2 Conditional Critical Regions Konzept von Hoare, allgemeiner als Semaphoren. Es existiert ein kritischer Abschnitt, wobei der Zugang durch eine Bedingung geschützt wird. Typischerweise mit while-Schleife realisiert. Würde man if anstelle von while verwenden, so würden aufgeweckte Threads nicht merken, dass ein anderer Thread die condition verändert hat. (Während man schläft können andere arbeiten.) Typisches Muster: synchronized(o) { while (!condition ) { // ... synchronized(o) { // condition == false // tu was Informatik B SS 03 o.wait(); // ... } 213 condition = true; o.notifyAll(); } // tu was // evtl. condition = false } 14.2.1 Monitore, CCRs, Semaphoren Monitor: Ein Java-Objekt, das den Zugang zu synchronisierten (Instanz)-Methoden/Blöcken kontrolliert. Conditional Critical Region: kritischer Abschnitt (in einem synchronisierten Block), bei dem auf Zugang gewartet wird (wait()), solange bis eine Bedingung erfüllt ist. Ein anderer Block macht die Bedingung wahr und gibt den kritischen Abschnitt frei (notify(), notifyAll()). Semaphoren: Sperren und Freigeben, realisiert durch Zähler und Warteschlange. Spezielle Technik, um CCRs zu realisieren. 14.3 Deadlocks Eine Situation in der zwei oder mehr Prozesse/Threads nicht weiterarbeiten können, weil jeder darauf wartet, dass mindestens ein anderer etwas bestimmtes erledigt, heisst Deadlock. Standardbeispiel: Dining Philosophers Fünf Philosophen sitzen um einen runden Tisch. Vor jedem Philosoph steht ein Teller, zwischen jedem Teller-Paar liegt eine Gabel. Es existieren also fünf Gabeln, aber um zu essen braucht jeder Philosoph zwei Gabeln (die zu seiner rechten und zu seiner linken Seite). Wenn alle fünf Philosophen zur Gabel zu ihrer Rechten greifen, entsteht ein Deadlock! Zweites Problem: Aushungern eines Philosophen (die anderen sind immer schneller beim Zugreifen). Kann mit Semaphoren oder kritischen Abschnitten alleine nicht verhindert werden. Java erlaubt immer einem beliebigen Thread, dass er zum Zug kommt. 14.3.1 Lösung mit Semaphoren Nicht verklemmungsfrei: // Anzahl von Gabeln ist 5. // Jede Gabel sei eine Semaphore, die mit 1 initialisiert wird. // // Für einen Philosophen i: Informatik B SS 03 214 Abbildung 58: Dining Philosophers while(true) { think(); gabel[i].P(); gabel[(i+1)%anzahl].P(); eat(); gabel[i].V(); gabel[(i+1)%anzahl].V(); } // wg. i+1 größer 4 Verklemmungsfreie Lösung mit Semaphoren? Eine globale Semaphore “table”, die jeweils nur einem Philosoph erlaubt, zu prüfen, ob seine linke und rechte Gabel frei sind und diese dann aufzunehmen. Deadlocks können auftreten, wenn Exklusive Belegung: Betriebsmittel sind entweder von genau einem Prozess belegt oder frei. (eine Gabel) Belegen und Warten: Prozesse belegen Betriebsmittel und warten während der Belegung auf die Zuteilung weiterer Betriebsmittel. (linke und rechte Gabel) Kein zwangsweises Freigeben: Betriebsmittel können nicht entzogen werden, sondern müssen vom Prozess zurückgegeben werden (Hinlegen einer Gabel) Zyklische Wartebedingung: Es muss einen Ring aus zwei oder mehr Prozessen bestehen, bei der jeder Prozess auf ein von einem anderen Prozess aus der Kette belegtes Betriebsmittel wartet. (5 Philosophen) 14.3.2 Dining Philosophers – Lösung mit globaler Kontrolle (nach Jobst, Programmieren in Java, Hanser) Conditional Critical Region Konzept: takeForks() und putForks() Informatik B SS 03 215 Aushungern theoretisch möglich. Erweiterung: Änderung von Prioritäten, z.B. “hungrig” und “satt” als Eigenschaft der Philosophen (Threads). Fairness! // Der Manager handelt nach der Philosophie: gib nur dann Gabeln an einen // Philosophen, wenn ALLE benoetigten Gabeln frei sind. Damit werden // Deadlocks vermieden (vgl. Literaturangaben) // public class Manager { public final static int N = 5; // fuenf Philosophen private static int phils[] = new int [N]; // Zustaende der Philosophen private final static int NOTHING = 0; private final static int EATING = 1; private int left (int i) { return (i-1+N) % N; } // Linker Nachbar private int right (int i) { return (i+1) % N; } // Rechter Nachbar synchronized public void takeForks (int no) { while (phils[left(no)] == EATING || // Wenn hoechstens einer phils[right(no)] == EATING) { // der Nachbarn isst: try { wait (); // Warte bis fertig. } catch (InterruptedException e) { System.err.println(e); } } phils[no] = EATING; } synchronized public void putForks (int no) { phils[no] = NOTHING; // Markiere frei notifyAll (); // Nachricht an Wartende } synchronized public void display () { StringBuffer s = new StringBuffer("Philosophen : "); for (int i = 0; i < N; i++) if (phils[i] == EATING) s.append(" " + i); System.out.println (s); } public static void main (String[] args) { Manager m = new Manager (); // Manager zuerst installieren Thread p[] = new Thread [N]; for (int i = 0; i < p.length; i++) p[i] = new Philosopher (i, m); for (int i = 0; i < p.length; i++) p[i].start (); for (int i = 0; i < p.length; i++) { Informatik B SS 03 216 try { p[i].join (); } catch (InterruptedException e) { System.err.println (e); } } } } class Philosopher extends Thread { protected int no; // Die (unpersoenliche) Nummer des Philosophen protected Manager m; // Referenz zum Manager public Philosopher (int no, Manager m) { super ("Phil. " + no); this.no = no; this.m = m; } protected void eat () { m.display (); try { sleep (Math.round (1000 * Math.random ())); } catch (InterruptedException e) {} } protected void think () { try { sleep (Math.round (1000 * Math.random ())); } catch (InterruptedException e) {} } public void run () { for (int j = 0; j < 5; j++) { think (); // Denken .... m.takeForks (no); // Auf Zugang warten eat (); // Essen m.putForks (no); // Zugang fuer die Kollegen ermoeglichen } } } Zu einem Zeitpunkt können höchstens zwei, nicht nebeneinander sitzende Philosophen essen: Philosophen : Philosophen : 4 3 Informatik B SS 03 Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen Philosophen : : : : : : : : : : : : : : : : : : : : : : : 0 1 1 0 0 1 1 2 0 0 4 1 2 0 2 1 0 0 2 2 1 1 2 217 3 3 3 3 2 4 4 4 2 3 4 4 2 4 4 3 3 4 3 3 14.3.3 Dining Philosophers – Bedingter Zugriff auf Gabel // Philosopher A.T. Schreiner import java.util.Random; /** the Dining Philosophers. */ public class Philosopher2 extends Thread { /** the fork between two philosophers. */ protected static class Fork { protected int me; // number for trace protected boolean inUse; // true if fork is in use public Fork (int me) { this.me = me; } /** returns true if fork is obtained, false if not. */ public synchronized boolean get (int who) { System.err.println(who+(inUse ? " misses " : " grabs ")+me); return inUse ? false : (inUse = true); } /** drops the fork. */ public synchronized void put (int who) { Informatik B SS 03 218 System.err.println(who+" drops "+me); inUse = false; notify(); } /** returns once fork is obtained. */ public synchronized void waitFor (int who) { while (! get(who)) try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } /** make one diner. */ public Philosopher2 (int me, Fork left, Fork right) { this.me = me; this.left = left; this.right = right; } protected static Random random = new Random(); // randomize protected int me; // number for trace protected Fork left, right; // my forks /** philosopher’s body: think and eat 5 times. */ public void run () { for (int n = 1; n <= 5; ++ n) { System.out.println(me+" thinks"); try { Thread.sleep((long)(random.nextFloat()*1000)); } catch (InterruptedException e) { e.printStackTrace(); } for (;;) try { left.waitFor(me); if (right.get(me)) { System.out.println(me+" eats"); try { Thread.sleep((long)(random.nextFloat()*1000)); } catch (InterruptedException e) { e.printStackTrace(); } right.put(me); break; } } finally { left.put(me); } } System.out.println(me+" leaves"); } /** sets up for 5 philosophers. */ public static void main (String args []) { Informatik B SS 03 219 Fork f[] = new Fork[5]; for (int n = 0; n < 5; ++ n) f[n] = new Fork(n); Philosopher2 p[] = new Philosopher2[5]; p[0] = new Philosopher2(0, f[4], f[0]); for (int n = 1; n < 5; ++ n) p[n] = new Philosopher2(n, f[n-1], f[n]); for (int n = 0; n < 5; ++ n) p[n].start(); } } Lösung führt zum Aushungern von 2 durch 1 und 3!!! 0 thinks 1 thinks 2 thinks 3 thinks 4 thinks 1 grabs 0 1 grabs 1 1 eats 3 grabs 2 3 grabs 3 3 eats 1 drops 1 1 drops 0 1 thinks 2 grabs 1 2 misses 2 2 drops 1 2 grabs 1 2 misses 2 2 drops 1 2 grabs 1 ... 14.3.4 Deadlocks durch falsche Anordnung // When two threads try to lock two objects, deadlock can occur unless // they always request the locks in the same order. final Object resource1 = new Object(); // Here are two objects to lock final Object resource2 = new Object(); Thread t1 = new Thread(new Runnable() { // Locks resource1 then resource2 public void run() { synchronized(resource1) { synchronized(resource2) { compute(); } } } }); Thread t2 = new Thread(new Runnable() { // Locks resource2 then resource1 Informatik B SS 03 220 public void run() { synchronized(resource2) { synchronized(resource1) { compute(); } } } }); t1.start(); // Locks resource1 t2.start(); // Locks resource2 and now neither thread can progress! 14.4 Threads: Ergänzungen Threads können durch setDaemon(true) zu Dämon-Threads gemacht werden: Der Interpreter wird beendet, wenn alle Nicht-Dämon-Threads beendet sind. Threads können Thread-Gruppen zugeordnet werden und dann gemeinsam behandelt werden. Ohne explizite Angabe einer Gruppe gehört ein Thread zur Gruppe ‘System’. Vordefinierte Klassen, die Collection, Set, List oder Map implementieren, haben in der Regel keine synchronized()-Methoden (z.B. ArrayList). Es können synchronisierte Wrapper-Objekte erzeugt werden: List synclist = Collections.synchronizedList(list); Map syncmap = Collections.synchronizedMap(map); Ordentliches Unterbrechen eines Threads durch interrupt(). wait() und sleep() erlauben einen Interrupt, also kann ein Thread nicht “mitten beim Arbeiten” angehalten werden. class T extends Thread { public void run() { while(true) { // This thread runs until asked to stop process(); // Do something try { Thread.sleep(1000); } // Wait 1000 millisecs catch (InterruptedException e) { // Handle the interrupt return; } } } public stopThread() { interrupt(); } } Informatik B SS 03 221 Reverse List of words Sort List of reversed words Reverse List of words Sort Reverse List of reversed sorted words List of rhyming words Reverse List of rhyming words Abbildung 59: Kommunikation ohne/mit PipeStreams 14.5 Pipe-Ströme Pipes werden benutzt, um von einem Thread erzeugte Daten einem anderen Thread zur Verfügung zu stellen. Klassen PipedReader/PipedWriter (Character-Stream) bzw. PipedInputStream/PipedOutputStream Beispiel: Klasse, die verschiedene String-Manipulationen durchführt (Sortieren, Umkehren von Text). Ermittlung von reimenden Worten: Jedes Wort einer Liste umdrehen, sortieren, wieder umdrehen. Ohne Pipe-Ströme müssen die Operationen Umdrehen, Sortieren, Umdrehen sequentiell abgearbeitet werden, da jedes Zwischenergebnis explizit, z. B. in einer Datei, gespeichert werden muss. Mit Pipe-Strömen kann der Output einer Operation direkt an die nachfolgende Operation übergeben werden. In der main-Methode wird ein Reader-Objekt erzeugt. Die Ermittlung der reimenden Worte wird über den verschachtelten Methodenaufruf reverse(sort(reverse(words))) realisiert. Die Methoden reverse() und sort() arbeiten mit Threads und kommunizieren über Pipe-Ströme (siehe Abb. 60): Informatik B SS 03 222 Abbildung 60: Direkte Kommunikation mit PipeStream – Ein PipeReader wird “auf” einem PipeWriter erzeugt: Was auf den PipeWriter geschrieben wird, kann vom PipeReader gelesen werden! – Dazu gibt es einen BufferedReader, der die Eingabe (aus einer Quelle, z.B. der Pipe) erhält, und einen PrintWriter, der die Ausgabe in eine Sink (z.B. Pipe) schreibt. Anmerkung: Im Prinzip kann mit Pipes nebenläufig gearbeitet werden. Z.B. kann ein Thread bereits konsumieren (lesen und verarbeiten), während ein anderer noch produziert. Die Methode sort() kann jedoch erst arbeiten, wenn alle Worte von reverse() umgedreht wurden. Ein Beispiel für “parallele” Verarbeitung mit Pipe-Strömen ist im begleitenden Code zur Vorlesung angegeben (SuffixExtract.java). import java.io.*; public class RhymingWords { public static void main(String[] args) throws IOException { FileReader words = new FileReader("words.txt"); // do the reversing and sorting Reader rhymedWords = reverse(sort(reverse(words))); // write new list to standard out BufferedReader in = new BufferedReader(rhymedWords); String input; while ((input = in.readLine()) != null) System.out.println(input); in.close(); } public static Reader reverse(Reader source) throws IOException { // BufferedReader in = new BufferedReader(source); PipedWriter pipeOut = new PipedWriter(); PipedReader pipeIn = new PipedReader(pipeOut); Informatik B SS 03 223 // PrintWriter out = new PrintWriter(pipeOut); // new ReverseThread(out, in).start(); new ReverseThread(pipeOut, source).start(); return pipeIn; } public static Reader sort(Reader source) throws IOException { // BufferedReader in = new BufferedReader(source); PipedWriter pipeOut = new PipedWriter(); PipedReader pipeIn = new PipedReader(pipeOut); // PrintWriter out = new PrintWriter(pipeOut); // new SortThread(out, in).start(); new SortThread(pipeOut, source).start(); return pipeIn; } } import java.io.*; public class ReverseThread extends Thread { private PrintWriter out = null; private BufferedReader in = null; public ReverseThread(Writer out, Reader in) { this.out = new PrintWriter(out); this.in = new BufferedReader(in); } public void run() { if (out != null && in != null) { try { String input; // for every line you read, reverse it while ((input = in.readLine()) != null) { out.println(new StringBuffer(input).reverse().toString()); out.flush(); } out.close(); } catch (IOException e) { System.err.println("ReverseThread run: " + e); } } } Informatik B SS 03 224 } import java.io.*; import java.util.Arrays; public class SortThread extends Thread { private PrintWriter out = null; private BufferedReader in = null; public SortThread(Writer out, Reader in) { this.out = new PrintWriter(out); this.in = new BufferedReader(in); } public void run() { int MAXWORDS = 50; if (out != null && in != null) { try { String[] listOfWords = new String[MAXWORDS]; int numwords = 0; // first read all while ((listOfWords[numwords] = in.readLine()) != null) numwords++; // then sort it Arrays.sort(listOfWords, 0, numwords); for (int i = 0; i < numwords; i++) out.println(listOfWords[i]); out.close(); } catch (IOException e) { System.err.println("SortThread run: " + e); } } } } Informatik B SS 03 225 15 Reguläre Ausdrücke und Pattern-Matching Achtung: Dieses Kapitel wird noch überarbeitet und erweitert! 15.1 String Pattern-Matching Zu den Klassen von Algorithmen, die jeder Informatiker kennen sollte, gehören neben Such-, Sortier-, und Graphalgorithmen auch Pattern-Matching Algorithmen. Im folgenden werden die Grundlagen für String Pattern-Matching eingeführt (siehe Baase & van Gelder, Kap. 11). Der Schwerpunkt liegt auf den Algorithmen, nicht auf der Java-Implementierung. Entsprechend werden die Algorithmen in Pseudo-Code angegeben, 15.1.1 Motivation Problem: Finden eines Teilstrings (Muster/Pattern) in einem anderen String (Text). Anwendungsbereiche: Textverarbeitung, Information-Retrieval, Suche über Verzeichnisbäume, Bioinformatik, etc. 15.1.2 Straightforward Lösung Algorithmische Idee: – Starte beim ersten Zeichen im Text und prüfe, ob die Zeichenfolge des Patterns mit der anfänglichen Zeichenfolge im Text matched. – Wenn ja: Erfolg, Wenn nein: Starte neu beim zweiten Zeichen im Text Nicht gerade effizient: Anzahl von Zeichen-Vergleichen ist O( Länge des Patterns und Länge des Texts! P: ABABC vvvvv T: ABABABCCA ABABC vvvvv ABABABCCA / ) für / ABABC vvvvv ABABABCCA 15.1.3 String-Matching mit endlichen Automaten Idee: Repräsentation eines Patterns als endlichen Automaten (siehe Kapitel 1.4.1 “Formale Sprachen”). Text als Input in den Automaten, Termination in Endzustand, wenn Pattern entdeckt. Informatik B SS 03 226 Tabelle 6: Einfaches String Pattern-Matching int simpleScan(char[] P, char[] T, int m) int match; // value to return int i; // current guess where P begins in T int j; // index of current char in T int k; // index of current char in P match = -1; i = j = 1; k = 1; while (!endText(T, j)) if (k m) match = i; // match found break; if (T[j] == P[k]) j++; k++; else // Back up over matched chars int backup = k-1; j = j-backup; k = k-backup; // Slide pattern forward, start over j++; i = j; // Continue loop return match; Informatik B SS 03 227 B B, C A Start A 1 A 2 3 B C 4 * B, C C Abbildung 61: Endlicher Automat für P = AABC Vorteil: Jeder Buchstabe im Text muss nur einmal angeschaut werden: O( ); allerdings gilt dieser Vorteil nur, wenn die automatische Konstruktion des endlichen Automaten aus dem Pattern effizient durchgeführt werden kann. In Abb. 61 ist ein endlicher Automat für das Pattern “AAABC” angegeben. “*” markiert einen Endzustand, in allen anderen Zuständen wird das nächste Zeichen auf dem Band gelesen und der Schreib-Lese-Kopf um ein Zeichen nach rechts verschoben. Leider ist das Erstellen des endlichen Automaten (die entsprechende Übergangstabelle) recht aufwendig. der Knuth-Morris-Pratt Algorithmus arbeitet mit einer einfacheren Variante zum endlichen Automat – einer sogenannten Verschiebetabelle, die in linearer Zeit in der Pattern-Länge konstruiert werden kann! 15.1.4 Der Knuth-Morris-Pratt (KPM) Algorithmus KPM-Idee: Verschiebetabelle statt Übergangstabelle Grundidee: Wenn in einem Text nach einem Pattern gesucht wird, und bereits der Anfang des Patterns (Präfix) matched, 12345678 P: ABABABCB T: ...ABABABx.... i=1 dann soll bei Miss-Match das Pattern so (nach rechts) über dem Text verschoben werden, dass die nächstmögliche Position des Patterns im Text gefunden werden kann. Möglicher nächster Beginn des Patterns im Text ist das größte Präfix des Patterns, das mit einem Suffix (Endstück) des bisher verarbeiteten Texts übereinstimmt: Informatik B SS 03 228 Tabelle 7: Konstruktion der Verschiebetabelle void kmpSetup(char[] P, int m, int[] fail) fail[1] = 0; k = 0; // Position des Anfangsabschnitts for (int q = 2; q while ((k m; q++) 0) && (P[k+1] P[q])) k = fail[k]; if (P[k+1] == P[q]) k++; fail[q] = k; 12345678 P: ABABABCB T: ...ABABABx.... i=1 Problem: Die Verschiebetablle soll nur bezüglich des Patterns, also unabhängig von einem konkreten Text, konstruiert werden. Trick: Man weiss etwas über den bisher verarbeiteten Text. Das bisher gematchte Präfix des Patterns muss das Suffix des Texts sein! Berechnung der Rücksprünge: i 12345678 P ABABABCB fail 00123400 Rücksprung ist jeweils derjenige Indexplatz im Pattern, der das größte Präfix für das bisher verarbeitete Pattern darstellt. Beispiel: 12345678910 T: ...ABABABABCB... T[1] A ok P[1] T[2] B ok P[2] T[3] A ok P[3] T[4] B ok P[4] T[5] A ok P[5] T[6] B ok P[6] T[7] A missmatch --> gehe zu P[4] (nimm ABAB als gematched an) T[7] A ok P[5] T[8] B ok P[6] T[9] C ok P[7] T[10]B ok P[8] --> output 10-8 = 2 Erläuterung: Berechnung der Verschiebetabelle Informatik B SS 03 229 Tabelle 8: Der KPM-Algorithmus int kpmScan(char[] P, char[] T, int m, int[] fail) int q = 0; // Zahl der Pos. in denen T und P übereinstimmen for (int i = 0; i while ((q n; i++) 0) && (P[q+1] T[i])) q = fail[q]; if (P[q+1] = T[i]) q++; if (q == m) output (i-m) Tabelle 9: Illustration KPM-Algorithmus: Pattern “ABABABCB” und Text “ABABABABCB” Konstruktion der Verschiebetabelle für “ABABABCB” q 2 3 4 5 6 7 8 k 0 0 1 2 3 4 2 0 0 P[k+1] A A B A B A A A A P[q] B A B A B C C C B k’ 1 2 3 4 2 0 - i 1 2 3 4 5 6 7 q P[q+1] 0 A 1 B 2 A 3 B 4 A 5 B 6 C 4 A 8 5 B 9 6 C 10 7 B output: 10-8 fail[q] 0 1 2 3 4 0 0 T[i] A B A B A B A A B C B q’ 1 2 3 4 5 6 4 5 6 7 8 Bestimmung der Verschiebungen: Ausgehend von fail[1] = 0 lassen sich die weiteren Verschiebungen mithilfe der bereits errechneten bestimmen. Für die Pattern-Position ist immer maximal gewählt, so daß gilt: . Daraus ergibt sich fail[q] = k + 1. G A5 G 5 G 5 / 5 Aufwand: For-Schleife (Index ) geht über Länge des Patterns ; Die -Werte können innerhalb der while-Schleife maximal den Wert annehmen. Die maximale Anzahl von Änderungen des -Werts kann auf abgeschätzt werden. Damit gilt für die Berechnung der Verschiebetabelle . Da der Matching-Algorithmus linear in der Länge des Textes ist, ergibt sich ein . Gesamtaufwand von 5 / / G / 15.1.5 Pattern-Matching mit Regulären Ausdrücken Statt konstanter Strings will man häufig nach allgemeineren Muster suchen. Beispiele: Informatik B SS 03 230 – Finde alle Folgen aus drei Zeichen, die mit “T” beginnen und mit “r” enden: T.r (“.” matched jedes Zeichen ausser “newline”) – Finde alle Worte, die mit “F” beginnen und mit “l” enden: F.*l (Der Kleene-Stern definiert eine Folge aus 0 bis n Zeichen der angegebenen Menge.) – Finde Jahreszahlen zwischen 1970 und 1999 19[789][0-9] (Eckige Klammern geben eine Zeichen-Klasse/Menge an. Mit x-y kann man Bereiche für geordnete Zeichenmengen definieren.) Auch für Pattern-Matching mit regulären Ausdrücken existieren Pattern-Matcher. Diese Matcher basieren typischerweise auf endlichen Automaten. Das grep-Kommando implementiert einen Matcher für reguläre Ausdrücke. Die Programmiersprache Perl ist im wesentlichen ein Matcher für reguläre Ausdrücke Natürlich können Matcher für reguläre Ausdrücke auch String-Pattern-Matching – konstante Strings sind spezielle reguläre Ausdrücke. 15.2 Java 1.4 ‘regex’ Vor Java 1.4: Pattern-Matching konnte mithilfe der StringTokenizer und charAt()-Methoden nur sehr umständlich realisiert werden. Neu: java.util.regex Flanagan nominiert regular expressions als Nummer 2 der Top-Ten Liste von “cool new features in Java 1.4”. Die Syntax für reguläre Ausdrücke in Java ist sehr ähnlich zu Perl. 15.2.1 Konstruktion regulärer Ausdrücke Neben den standardmässig zur Beschreibung regulärer Ausdrücke verwendbaren Konstrukte werden einige Zusatzkonstrukte erlaubt, die es einfacher machen reguläre Ausdrücke aufzuschreiben. Im Tabelle 10 wird ein kurzer Überblick gegeben (weitere Information ist in der Dokumentation nachzulesen). Die Symbole für special characters (wie $ ˆ . * +) und die Syntax für Zeichenklassen sind Standard für die Notation von regulären Ausdrücken. Weitere Notationen, insbesondere die Möglichkeit von Kurznotationen für vordefinierte Zeichenklassen sind spezifisch für Java bzw. Perl. Achtung bei greedy-Quantoren: a.*i würde im String gadji beri bin blassa glassala laula lonni cadorsu sassala bim bis zu dem letzten ‘i’ in ‘bim’ matchen! (Zeile aus dem Gedicht ’Gadji beri bimba’ von Hugo Ball) Informatik B SS 03 231 Achtung: Da das Pattern als Java-String angegeben wird, müssen Zeichen, die wörtlich gematched werden sollen, mit zweifachem Backslash eingeleitet werden, z. B. \\.. 15.2.2 Die Pattern-Klasse Die Pattern-Klasse repräsentiert einen regulären Ausdruck, der als String spezifiziert wurde. Mit der Klassenmethode Pattern.compile(string) wird das Pattern in eine effiziente interne Repräsentation umgewandelt. Pattern p = Pattern.compile("[,\\s]+"); erzeugt ein Pattern für Trennung durch Komma oder Whitespace Weitere Methoden: – split(): Teilt den gegebenen Input bezüuglich der Matches zum Pattern String[] result = p.split("one,two, three four , five "); – matcher(): erzeugt einen Matcher, der gegebenen Input gegen das Pattern vergleicht Matcher m = p.matcher("onetwothree four five,six"); 15.2.3 Die Matcher-Klasse Die Eingabe an einen Matcher muss dem Interface CharSequence genügen. Im obigen Beispiel wurde ein String-Objekt übergeben. Beispielsweise implementieren String, StringBuffer und CharBuffer dieses Interface. Wichtige Methoden: – matches(): Vergleicht den gesamten Input gegen das Pattern und liefert true, bei Übereinstimmung, false sonst. – find(): Scanned die eingegebene Zeichenfolge und sucht die nächste Teilfolge, die mit dem Pattern übereinstimmt. – appendReplacement(), appendTail(): Sammeln des Ergebnisstrings in einem StringBuffer – replaceAll(): liefert String, in dem jedes vorkommen des Patterns durch alternatives Pattern ersetzt wird. 15.2.4 Beispiel: Suche und Ersetze import java.util.regex.*; public class Replacement { public static void main(String[] args) throws Exception { Informatik B SS 03 232 Tabelle 10: Konstruktion regulärer Ausdrücke x \\ \t \n \cx ... Zeichen Zeichen x Backslash Tab Newline Control-Zeichen x ... Zeichenklassen einfache Klasse Negation inklusiver Bereich Subtraktion Subtraktion mit inkl. Bereich [abc] [ˆabc] [a-zA-Z] [a-z-[bc]] [a-z-[m-p]] [a-z-[ˆdef]] Vordefinierte Zeichenklassen . beliebiges Zeichen \d eine Ziffer \D keine Ziffer \s Trenner (whitespace) \S kein Trenner \w Textzeichen \W kein Textzeichen Begrenzer ˆ Zeilenanfang $ Zeilenende \b Wortgrenze \B keine Wortgrenze ... ... Greedy Quantoren * 0 bis n des vorangestellten Zeichens + 1 bis n des vorangestellten Zeichens ? das vorangestellte Zeichen 0- oder 1-mal ... ... Weitere Quantoren ... ... Quotation \ markiert das folgende Zeichen als “wörtlich zu nehmen” XY X | Y Logische Operatoren Sequenz Oder (a, b, oder c) (ein beliebiges Zeichen ausser a, b, oder c) (alle Gross- und Kleinbuchstaben) (a bis Z ausser b und c) (a bis z ausser m bis p) (d, e, oder f) (evtl. exklusive Zeilenendzeichen) [0-9] [ˆ0-9] [ \t\n\x0B\f\r] [a-zA-Z_0-9] weitere Verwendung Negation Kleene-Stern (X gefolgt von Y) (vgl. BNF) Informatik B SS 03 // Create Pattern p // Create Matcher m 233 a = a = pattern to match cat Pattern.compile("cat"); matcher with an input string p.matcher("one cat," + " two cats in the yard"); StringBuffer sb = new StringBuffer(); boolean result = m.find(); // Loop through and create a new String // with the replacements while(result) { m.appendReplacement(sb, "dog"); result = m.find(); } // Add the last segment of input to // the new String m.appendTail(sb); System.out.println(sb.toString()); } } Allgemein können reguläre Ausdrücke durch Strings über Variablen ersetzt werden. Hierzu werden im regulären Ausdruck Gruppen gebildet: \[([a-z])\.([A-Z])\] kann durch (\1,\2) ersetzt werden. Beispiele: [a.B] durch (a,B) [x.Y] durch (x,Y) Gruppen werden durch runde Klammern eingeschlossen. Wie genau Variablen notiert werden, hängt vom verwendeten Programm ab. Informatik B SS 03 234 16 Assertions Achtung: Dieses Kapitel wird noch überarbeitet und erweitert! 16.1 Zusicherungskalkül Im Kapitel “Typsicherheit” wurde gezeigt, wie im Prinzip die Korrektheit einer Programmiersprache bewiesen werden kann. Schreibt man Programme, so möchte man gegeben die Semantik der entsprechenden Implementierungssprache, zusichern, dass diese korrekt sind. Verifikation: Das Programm erfüllt seine Spezifikation (Beweis der partiellen Korrektheit). Validierung: Das Programm tut, was es tun soll (Testen, siehe Vorlesung “Informatik C”). In der Vorlesung “Informatik A” wurde das Zusicherungskalkül (Hoare-Kalkül) eingeführt. Dieses Kalkül ist eine formale Methode zum Korrektheitsbeweis von Programmen, das bis zu halbautomatischen Beweisern ausgebaut werden kannn. Das Zustandskalkül ermöglicht die Angabe von Vor- und Nachbedingungen für Programmanweisungen. Für alle Typen von Anweisungen (Zuweisung, bedingte Anweisung, ...) und Schleifen sind Axiome vorgegeben. Von besonderer Bedeutung sind Schleifeninvarianten. = 4#-@= ! Beispiel: Aus folgt K while 4 loop @ end 4 . Typischerweise gibt man Zusicherungen als Kommentare vor. Die Beweise folgen als “symbolische Auswertung” per Hand. /* P */ while b { /* P && b */ ... /* P */ } /* P && !b */ Seit Java 1.4 gibt es die assert-Anweisung – von Flanagan als das “Number one cool feature of Java 1.4” nominiert. Zusicherungen enthalten boolesche Ausdrücke, von denen der Programmierer annimmt, dass sie an der entsprechenden Stelle gelten. Informatik B SS 03 235 Arbeiten mit Zusicherungen ist ein schneller und effizienter Weg um Fehler im Programm zu erkennen und korrigieren. Sie dienen gleichzeitig als Dokumentation der Funktionalität des Programms und erhöhen damit seine Wartbarkeit! 16.2 Die ‘assert’-Anweisung Ergänzung folgt Informatik B SS 03 236 17 Ausblick: GUIs und Event Handling im Detail in “Informatik C: Oberflächenprogrammierung” 17.1 Java Foundation Classes Java Foundation Classes (JFC): Sammlung von Standard Java APIs für Graphik und GUIs. “Foundation”: weil die meisten Java (client-seitigen) Applikationen darauf aufgebaut sind. Abstract Windowing Toolkit (AWT): bis Java 1.1, rudimentär und “heavyweight” (auf native GUI Komponenten aufgebaut). Durch native look and feel ergeben sich viele kleine Unterschiede in der Darstellung zwischen verschiedenen Plattformen. Erweiterung Java2D: unterstützt das Erstellen zweidimensionaler Graphiken, Erstellung von Zeichenprogrammen und Bildeditoren. Swing: state-of-the-art GUI toolkit, das vollständig in Java geschrieben ist (“lightweight”). Swing unterstützt pluggable look and feel, das heisst, dass das Aussehen der GUI dynamisch an verschiedene Plattformen und Betriebssysteme angepasst werden kann. Aber Achtung: Swing-Klassen sind auf AWT-Klassen aufgebaut. Applikationen: Java-Programme mit graphischer Oberfläche, Ausgabe von Graphik und Text auf Bildschirm oder Drucker, Daten-Transfer via drag-and-drop. Applets: Kleine Applikationen, die in einem Web-Browser (z.B. Netscape) laufen. GUI-Builder: sind visuelle Programmierumgebungen, die automatisch Code für graphisch spezifizierte Oberflächen erzeugen. Dadurch kann sich die Entwicklungszeit verringern und es besteht eine einfache Möglichkeit, verschiedene Design auszuprobieren. 17.2 Swing-Komponenten 17.2.1 Erstes Beispiel ‘HelloWorldSwing’ import javax.swing.*; public class HelloWorldSwing { public static void main(String[] args) { JFrame frame = new JFrame("HelloWorldSwing"); final JLabel label = new JLabel("Hello World"); frame.getContentPane().add(label); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); } } Informatik B SS 03 237 Paket-Präfix javax: Swing kann als Erweiterung (eXtension) für Java 1.1 benutzt werden. Für Java 2 gehört Swing zu den core-packages. Klasse JFrame: ist ein “top-level” Container Hierarchie, siehe Abb. 62 Methode getContentPane() liefert Container und Container haben Methode add(). pack() ist Methode von Window: macht das Fenster Displayable, Berechnung der Grösse für das präferierte Layout und die Grössen der enthaltenen Komponenten. setVisible(): Methode einer Component, Zeigen oder Verstecken einer Component. (alternativ show()) Bei Dialog-Fenstern kann es Sinn machen, dass bei Beenden des Dialogs das Fenster nicht zerstört, sondern nur unsichtbar gemacht wird (setVisible(false)). Generelles Prinzip: immer zunächst einen top-level Container erzeugen. GUI ist aus Bausteinen aufgebaut: push buttons, scrollbars, pull-down menus, ... Jeder Baustein ist eine Komponente (Component). Swing hat alle Komponenten von AWT und weitere. Swing-Komponenten beginnen mit ‘J’. Swing Komponenten sind “light-weight” (plattform-unabhängig). JComponent ist Unterklasse von awt.Component. JWindow, JFrame und JDialog sind top-level Komponenten. Typischerweise verwendet man JFrame als Fenster- und JDialog als Dialogfenster-Klasse. JWindow ist ein Fenster ohne “Dekoration”. JPanel ist einn typischer Container (siehe unten). Weitere wichtige Komponenten sind JButton (ein push-button), JLabel (Display-Feld für Text oder Graphik), JTextField (Darstellen und Editieren einer Textzeile). Informatik B SS 03 238 Dialaog JDialog Window Component Container JWindow Frame JFrame JButton JComponent JLabel JPanel JTextField java.awt javax.swing Abbildung 62: Wichtige Swing und AWT Komponenten 17.2.2 Properties Für jede Component kann ihr Aussehen und Verhalten angepasst werden (customization). Dies geschieht durch die Spezifikation von Werten für die Properties einer Komponente. Properties werden über Accesor-Methoden abgefragt und gesetzt: getprop(), setprop(). Beispiel: setVisible(true). Für Properties mit Typ boolean wird anstelle von get is verwendet: isVisible() liefert true oder false. 17.2.3 Container Komponenten müssen innerhalb eines Container gepackt werden. Ein Container ist eine Component, die andere Components enthalten kann. Alle speziellen Container sind Unterklassen von awt.Container. Typische Container: Fenster und Dialog-Boxen. Häufig werden Container in andere Container verschachtelt. Achtung: JFrame, JWindow und JDialog können nur als “äussertster” Container verwendet werden! (siehe unten) Manche Container stellen Information auf spezielle Weise dar, manche haben Restriktionen bzgl. Zahl und Art der Komponenten, die sie enthalten können, manche sind generisch (beliebig konfigurierbar). Informatik B SS 03 239 http://java.sun.com/docs/books/tutorial/uiswing/components/components_pics. html Abbildung 63: Beispiele für Komponenten Informatik B SS 03 240 Mit der Window-Methode pack() wird ein Fenster ge-rendert (rendering), also in eine graphische Darstellung gebracht. Dabei wird die Größe des Fensters – in Abhängigkeit vom Layout der Unter-Komponenten – ermittelt. Jedes Fenster ist mit einem LayoutManager (ein Interface in java.awt) verbunden. Dieser wird mit pack() aktiviert. Typische Layout-Manager sind BorderLayout (Unterteilung eines Containers in fünf Bereiche, siehe unten) oder ScrollPaneLayout (Default für JScrollPane). Typische Schritte zum Erzeugen einer GUI: 1. Erzeugung der Container 2. Erzeugen der Components 3. Hinzufügen der Components zum Container add() JFrame frame = new JFrame("HelloWorldSwing"); // Container final JLabel label = new JLabel("Hello World"); // Component frame.getContentPane().add(label); Spezielles Verhalten der top-level Container JFrame, JWindow, JDialog: – Wenn solche Container erzeugt werden, erzeugen diese automatisch eine Unterklasse JRootPane, die eine Unterklasse von JComponent ist und das Interface RootPaneContainer implementiert. – Alle Komponenten werden in JRootPane eingefügt. Die Methode getContentPane() liefert denjenigen Container, in den die Komponenten eingefügt werden sollen. 17.2.4 Layout Management JFrame und JDialog sind generische Komponenten. Sie benutzen JPanel als default content pane und spezifizieren kein vordefiniertes Layout der Komponenten. Hier muss ein LayoutManager definiert werden, um die Komponenten im Container anzuordnen. Default-Layout ist vorgegeben (d.h. entsprechendes Objekt existiert). Z.B. für JFrame: BorderLayout. BorderLayout erlaubt bis zu 5 Komponenten mit den Positionen North, South, East, West, Center. import java.awt.*; import javax.swing.*; public class BorderWindow extends JFrame { Informatik B SS 03 241 public BorderWindow() { Container contentPane = getContentPane(); //Use the content pane’s default BorderLayout. //contentPane.setLayout(new BorderLayout()); //unnecessary contentPane.add(new JButton("Button 1 (NORTH)"), BorderLayout.NORTH); contentPane.add(new JButton("2 (CENTER)"), BorderLayout.CENTER); contentPane.add(new JButton("Button 3 (WEST)"), BorderLayout.WEST); contentPane.add(new JButton("Long-Named Button 4 (SOUTH)"), BorderLayout.SOUTH); contentPane.add(new JButton("Button 5 (EAST)"), BorderLayout.EAST); } public static void main(String args[]) { BorderWindow window = new BorderWindow(); window.setTitle("BorderLayout"); window.pack(); window.setVisible(true); } } 17.2.5 Anmerkungen Bei der Gestaltung der Oberfläche ist es meist sinnvoll, sich an bereits eingeführte Standards zu halten (z.B. bei der Menu-Organisation, Anordnung, Aussehen): Benutzer haben sich bereits an eingeführte Oberflächen gewöhnt (Adaptationseffekte), auch wenn diese ergonomisch keineswegs optimal sind. principle of least astonishment Es ist kein guter Stil, eine schicke Oberfläche allein zu verkaufen (d.h. mit “niy” hinter allen interessanten Menüpunkten und einigen Standardalgorithmen hinter Informatik B SS 03 242 den anderen Menüpunkten). Software-Ergonomie ist ein wichtiges Forschungsgebiet, zu dem Kognitionswissenschaftler viel beitragen können. Empirische Studien von grundlegenden psychophysischen Faktoren (Kontrast, Grösse, Farbe von Schrift) bis zur Nutzerführung. Eine noch so schicke GUI bringt gar nichts, wenn dahinter nicht sorgfältiger Code steht! (“aussen GUI innen pfui”). Für die Entwicklung komplexer Projekte kann es jedoch sinnvoll sein, vorab oder parallel zum Code bereits die Oberfläche zu entwerfen, um einen Überblick über alle gewünschten Funktionalitäten und deren Abhängigkeiten zu bekommen. 17.3 Event-Handling Bisher: Hübsche Oberflächen, die aber nichts tun. GUI-Komponenten sollen auf Benutzer-Eingaben reagieren können. Event: Nutzeraktion, wie Mausklick auf Button oder Tastatureingabe. Objekte, die auf Event reagieren. event listener 17.3.1 Event-Objekte Basis-Klasse: java.util.EventObject Swing: javax.swing.event-Paket enthält Unterklassen von EventObject und AWTEvent. EventObject-Methode getSource(): liefert Objekt, das den Event ausgelöst hat AWTEvent-Methode getID(): Unterscheidung von verschiedenen Events einer Klasse WindowEvent-Methode: getNewState() liefert den neuen Zustand eines Fensters ( NORMAL, ICONIFIED, MAXIMIZED_VERT ...) Typische Event-Klassen: WindowEvent, MouseEvent 17.3.2 Event Listener Ein Objekt, das einen Event erzeugt heisst event source. Ein Objekt, das auf einen Event reagieren soll heisst event listener. Event Source Objekte halten eine Liste von listeners, die informiert (notified) werden wollen und bietet Methoden zum Einfügen und Löschen von Listener-Objekten. Informatik B SS 03 243 Alle Komponenten sind Event-Sources und definieren entsprechende add() und remove() Methoden, die per Konvention mit Listener enden. z.B. addWindowListener(), addActionListener() Zu jeder Art von Event Objekt existiert ein korrespondierender Event Listener, z.B. ActionListener. Alle Listener erweitern das Marker-Interface java.util.EventListener. Vordefinierte Listener wie ActionListener sind selbst Interfaces und geben eine Methode actionPerformed() vor. Event-Adapter können alternativ verwendet werden: statt alle Methoden eines ListenerInterfaces zu implementieren kann eine Unterklasse zu einer entsprechenden Adapterklasse erzeugt werden, in der dann die gewünschte(n) Methode(n) überschieben wird. Beispiel: Realisieren eines WindowListener über einen WindowAdapter (siehe unten) 17.3.3 Event Handling mit Inneren Klassen Um über einen Event informiert zu werden, muss ein entsprechendes EventListener-Interface implementiert werden. Manchmal kann dies direkt in der Haupt-Klasse der Applikation geschehen. Typisch ist, anonyme innere Klassen zu verwenden. (Listener waren die Hauptmotivation für Innere Klassen) // create window listener for window close click addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {System.exit(0);} }); /* JFC import import import import in a Nutshell, Flanagan java.awt.*; javax.swing.*; javax.swing.border.*; java.awt.event.*; */ // // // // AWT classes Swing components and classes Borders for Swing components Basic event handling public class DisplayMessage { public static void main(String[] args) { /* * Step 1: Create the components */ JLabel msgLabel = new JLabel(); // Component to display the question JButton yesButton = new JButton(); // Button for an affirmative response JButton noButton = new JButton(); // Button for a negative response /* 244 Informatik B SS 03 * Step 2: Set properties of the components */ msgLabel.setText(args[0]); msgLabel.setBorder(new EmptyBorder(10,10,10,10)); yesButton.setText((args.length >= 2)?args[1]:"Yes"); noButton.setText((args.length >= 3)?args[2]:"No"); // // // // The msg to display A 10-pixel margin Text for Yes for no button /* * Step 3: Create containers to hold the components */ JFrame win = new JFrame("Message"); // The main application window JPanel buttonbox = new JPanel(); // A container for the two buttons /* * Step 4: Specify LayoutManagers to arrange components in the containers */ win.getContentPane().setLayout(new BorderLayout()); // layout on borders buttonbox.setLayout(new FlowLayout()); // layout left-to-right /* * Step 5: Add components to containers, with optional layout constraints */ buttonbox.add(yesButton); // add yes button to the panel buttonbox.add(noButton); // add no button to the panel // add JLabel to window, telling the BorderLayout to put it in the middle win.getContentPane().add(msgLabel, "Center"); // add panel to window, telling the BorderLayout to put it at the bottom win.getContentPane().add(buttonbox, "South"); /* * Step 6: Arrange to handle events in the user interface. */ yesButton.addActionListener(new ActionListener() { // Note: inner class // This method is called when the Yes button is clicked. public void actionPerformed(ActionEvent e) { System.exit(0); } }); noButton.addActionListener(new ActionListener() { // Note: inner class // This method is called when the No button is clicked. public void actionPerformed(ActionEvent e) { System.exit(1); } }); /* * Step 7: Display the GUI to the user */ win.pack(); // Set the size of the window based its children’s sizes. Informatik B SS 03 win.show(); 245 // Make the window visible. } } 17.4 Applets Mini-Applikation, die über Netz von einer (untrusted) Quelle geladen werden kann, und die in einem Web-Browser oder einer andere Applet-Viewer Anwendung ausgeführt werden kann. Mächtige Möglichkeit, um Java Programme an Endbenutzer zu liefern. Gerade die Applets machten Java populär. 17.4.1 Unterschiede zwischen Applets und Applications Ein Applet hat keine main() Methode. Ein Applet wird nicht über die Kommandozeile aufgerufen. Es ist in ein HTML-File – APPLET -tag – eingebettet und erhält seine Argumente über PARAM tags im HTML-File. Applets unterliegen einigen Sicherheitsbeschränkungen, die verhindern sollen, dass auf dem Host unsichere und möglicherweise bösartige Applets ausgeführt werden. 17.4.2 Schreiben von Applets Unterklasse von java.applet.Applet (Unterklasse von java.awt.Component <-- java.awt.Panel) und Überschreiben von Standard-Methoden. (Alternativ JApplet) Applets haben keine Kontrolle über den Execution-Thread (anders als Programme mit main()). Deshalb dürfen sie keine zeitaufwendigen Berechnungen durchführen – ausser sie erzeugen ihren eigenen Thread. Auswahl von Methoden der Applet-Klasse: Informatik B SS 03 246 init(): Wird beim Laden des Applets ausgeführt – anstelle eines Konstruktors. Hier werden typischerweise GUI-Komponenten erzeugt. destroy(): Gegenstück zu init(): Applet wird aus dem Browser entfernt und sollte alle Resourcen freigeben. start(): Wird aufgerufen, wenn das Applet sichtbar wird. stop(): Temporäres nicht-sichtbar machen, stoppen der Animation/Berechnung. getImage(): Laden eines Image-Files vom Netz. getCodeBase(): URL, von der das Applet-Klassen-File geladen wurde. Auswahl von Methoden der Component-Klasse: paint(): Zeichne Dich selbst als wichtigste Methode. 17.4.3 Beispiel /* JFC in a Nutshell, Flanagan */ import java.applet.*; import java.awt.*; public class MessageApplet extends Applet { protected String message; // The text to display protected Font font; // The font to display it in // One-time initialization for the applet public void init() { message = this.getParameter("message"); font = new Font("Helvetica", Font.BOLD, 48); } // Draw the applet whenever necessary. public void paint(Graphics g) { // The pink oval g.setColor(Color.pink); g.fillOval(10, 10, 330, 100); // The red outline. The browser may not support Java2D, so we // try to simulate a 4-pixel wide line by drawing four ovals. g.setColor(Color.red); g.drawOval(10,10, 330, 100); g.drawOval(9, 9, 332, 102); g.drawOval(8, 8, 334, 104); g.drawOval(7, 7, 336, 106); // The text g.setColor(Color.black); g.setFont(font); Informatik B SS 03 g.drawString(message, 40, 75); } } HTML-File: <APPLET code="MessageApplet.class" width=350 height=125> <PARAM name="message" value="Hello World"> </APPLET> Aufruf: > appletviewer MessageApplet.html 17.5 GUIs und Threads Jede GUI-Applikation hat einen event dispatch thread: Thread, der darauf wartet, dass Ereignisse eintreten und diese an die entsprechenden Event-Handler ausliefert. Alle event listener Methoden werden vom event dispatch thread aufgerufen (invoke). alle GUI Manipulationen, die über event listener ausgeführt werden, sind sicher. actionPerformed() und paint() werdem im event-dispatching thread ausgeführt. So wird beispielsweise während eine actionPerformed() Methode ausgeführt wird, die GUI “eingefroren” (kein re-paint, keine Reaktion auf Maus-Klicks, ...) 247 Informatik B SS 03 248 Swing Komponenten sind nicht thread-safe: Es muss darauf geachtet werden, dass nur der event-dispatch thread auf solche Komponenten zugreift. Übliche Lösung: “Single-Thread Rule”: Wenn eine Swing-Komponente realisiert wurde, sollte der gesamte Code, der diese Komponenten beeinflusst oder von ihr abhängt im event dispatch thread ausgeführt werden. Code in Event-Handlern sollte schnell ausgeführt werden (sonst schlechte Performanz) 17.6 Beans Bean: Wiederverwendbare Software-Komponente, die in einem Builder-Tool visuell manipuliert werden kann. Beispiel: BDK (Java Beans Development Kit). Typisch für graphische Benutzeroberflächen. Schreiben von Beans: z.B. neue graphische Komponenten sollten über Properties konfigurierbar sein und entsprechende get- und set-Methoden anbieten. Nutzen von Beans: Zusammenstecken und Konfigurieren von Komponenten und mit Code verbinden. Informatik B SS 03 249 18 Ausblick: Verteilte Systeme 18.1 Netzwerk-Anwendungen in Java Im Pakete java.net werden Klassen definiert, mit denen es recht einfach ist, Netzwerk Anwendungen zu schreiben. Beispiel: Klasse URL, die einen uniform resource locator definiert. Unter anderem werden die folgenden Protokolle unterstützt: http: (HyperText Transfer Protocol), ftp: (File Transfer Protocol), ... Beispiel: Klasse Socket, um mit einem Server zu kommunizieren. Unix-Kommando netstat zeigt mit -r die IP Routing Tabelle und mit -a die aktiven Internet-Verbindungen (Sockets). z.B. > netstat -rn Kernel IP routing table Destination Gateway 131.173.13.0 0.0.0.0 Genmask 255.255.255.0 Flags U MSS Window 40 0 irtt Iface 0 eth0 Unix-Kommando ifconfig zeigt localhost und Verbindung(en) nach Aussen. 18.2 Grundlagen für Kommunikation im Netz 18.2.1 Open System Interconncetion (OSI) Model 1. Physikalisch: Hardware Übermittlung von Binärdaten-Sequenzen durch elektromagnetische Signale (Kabel, Glasfaser, Radiowellen). Beispiele: ISDN, Ethernet 2. Data Link: Übermittlung von Daten zwischen physikalisch direkt verbundenen Knoten. zwischen Routern, Hosts (Wide Area Network, WAN; Local Area Network, LAN) 3. Netzwerk: Übermittlung von Daten zwischen Computern in einem bestimmten Netzwerk. Beispiel: IP (Internet Protocol), weltweit eindeutige Adresse für jeden Rechner im Netz 4. Transport: Unterste Ebene, auf der Nachrichten (statt Daten-Paketen) bearbeitet werden. Nachrichten werden an Ports adressiert, die mit Prozessen assoziiert sind. Beispiele: TCP, UDP (später etwas genauer) 5. Session: Fehlererkennung und automatic recovery 6. Presentation: Übermittlung von Daten in in eine Rechner-unabhängige Repräsentation. Beispiel: CORBA Informatik B SS 03 250 7. Application: Protokolle, die (meist) ein Interface zu einem Service definieren; Kommunikations-Anforderungen für spezifische Applikationen.; Beispiele: HTTP, FTP 18.2.2 TCP und UDP In der Praxis sind die obersten drei Schichten nicht sauber getrennt. Schreibt man eigene Anwendungen (oberste Ebene im OSI Modell) in Java, muss man kaum etwas über die darunterliegenden Schichten wissen. Um aber entscheiden zu können, welche Klassen aus java.net man benutzen sollte, muss man die Unterschiede zwischen TCP und UDP kennen. TCP (Transmission Control Protocol): – Verbindungsbasiertes (conncetion-based) Protokoll, das einen zuverlässigen Datenfluss zwischen zwei Rechnern realisiert. – Analogie: Telefonleitung Aufbau einer Verbindung zwischen zwei Parteien und Datenaustausch über diese Verbindung. – Garantiert, dass die Daten in der selben Reihenfolge ankommen, in der sie gesendet wurden. – HTTP, FTP, Telnet erfordern solche zuverlässigen Kommunikations-Kanäle. Wenn z.B. eine URL mit HTTP gelesen wird, so müssen die Daten in der Reihenfolge empfangen werden, in der sie gesendet wurden, sonst hat man beispielsweise eine unsinnige HTML Datei oder ein unbrauchbares (korruptes) zip-File. UDP (User Datagramm Protocol): – Nicht verbindungsbasiertes Protokoll, das unabhängige Daten-Pakete (Datagramme), von einem Rechner zu einem anderen schickt, ohne zu garantieren, das die Daten ankommen. – Analogie: Briefe per Post verschicken Reihenfolge der Auslieferung ist unwichtig und nicht garantiert, Nachrichten sind unabhängig voneinander. – Es gibt Nachrichten, bei denen Reihenfolge und Zuverlässigkeit essentiell ist. Manchmal genügen aber schwächere Anforderungen, die dafür wenig Overhead benötigen und schneller sind. – Beispiel: Uhrzeit-Service Clock-Server schickt aktuelle Zeit an Client. Falls der Client ein Paket nicht erhält, macht es keinen Sinn, die Daten nochmal zu übermitteln, weil die aktuelle Uhrzeit inzwischen eine andere ist. Informatik B SS 03 251 – Beispiel: ping Kann gar nicht zuverlässigem Service realisiert werden, da ja gerade die Zahl von verlorenen Paketen und/oder Pakten in falscher Reihenfolge benötigt wird, um die Güte der Verbindung anzugeben. 18.2.3 Ports Ein Rechner hat eine einzige physikalische Verbindung zum Netzwerk, auf der alle Daten ankommen. Ports werden benutzt, um ankommende Daten einem bestimmten Prozess zuzuordnen. Datenübertragung im Internet wird mit Adressen (IPs) realisiert: 32 bit für Rechner und 16 bit für Port. Portnummern von 0 bis 1023 sind für “well-known” Dienste (wie HTTP, FTP) reserviert. Eigene Anwendungen sollten nicht an solche Portnummern gebunden werden. Genauere Information, in allen Lehrbüchern zum Thema “Verteilte Systeme”, siehe auch Vorlesungen “Verteilte Systeme” und “Informatik C” 18.2.4 Networking Klassen in Java Über die Klassen in java.net können Java Programme TCP oder UDP verwenden, um über das Internet zu kommunizieren. TCP-Klassen: URL, URLConnection, Socket, ServerSocket UDP-Klassen: DatagramPacket, DatagramSocket, MulticastSocket 18.3 Die Klasse ‘URL’ 18.3.1 Was ist eine URL? URL (Uniform Resource Locator) ist eine Referenz (Adresse) zu einer Resource im Internet. Eine URL-Adresse hat zwei Haupt-Komponenten: Protokoll-Identifier: http, ftp, file, news, ... Resource Name: vollständige Adresse deren Format vom benutzten Protokoll abhängt Beispiel: http://java.sun.com Format für HTTP Resourcen: – Host Name (Rechner), z.B.: http://java.sun.com, http://www.informatik.uni-osnabrueck.de Informatik B SS 03 252 – Filename (Pfad zur Datei auf dem Host), z.B. /schmid/research.html oft nur Verzeichnis und Server lädt index.html, kann auch ein script, ein gif, etc. sein – Port Number (optional), default ist 80 – Reference (optional): markierter Ort in einer Datei, z.B. HTML name = ... in 18.3.2 Nutzen der URL Klasse import java.net.* import java.io.* // Create some URL objects URL url=null, url2 = null, url3 = null; try{ url = new URL("http://www.oreilly.com"); // An absolute URL url2 = new URL(url, "catalog/books/javanut3/"); // A relative URL url3 = new URL("http:", "www.oreilly.com", "index.html"); // protocoll host name file name } catch (MalformedURLException e) { /* Ignore this exception */ } // Read the content of a URL from an input stream InputStream in = url.openStream(); // For more control over the reading process, get a URLConnection objecy URLConnection conn = url.openConnection(); // Now get some information about the URL String type = conn.getContentType(); String encoding = conn.getContentEncoding(); java.util.Date lastModified = new java.util.Date(conn.getLastModified()); int len = conn.getContentLength(); // If necessary, read the contents of the URL using this stream InputStream in = conn.getInputStream(); Komponenten von URLs können nach der Erzeugung des URL Objekts nicht mehr verändert werden. (set Methode ist nicht public) Parsierung von URLs: getProtocol(), getHost(), getPort(), getFile(), getRef(). Nicht alle URLs haben diese Komponenten (URL Klasse ist etwas “HTTP”-Zentriert). Erinnerung zum Umgang mit Streams: BufferedReader myin = new BufferedReader(new InputStreamReader(in)); // ‘in’ als bereits definierter InputStream, z.B. ueber url.openStream() Informatik B SS 03 253 Beispiel für das Auslesen des Inhalts einer HTML-Datei als String: /Vl24/URLReader openConnection() initialisiert eine Kommunikationsverbindung zwischen dem Java Programm und der URL über das Netz. Auch URLConnection Objekte haben Methoden zum Lesen. Vorteil: auch Methoden zum Schreiben, Abfrage von Eigenschaften. Abfrage von Eigenschaften: die im Header-File (vom Server erzeugt) mitgeliefert werden. 18.3.3 Beispiel: URLConnection import java.net.*; import java.io.*; public class URLInfo { public static void main(String[] args) throws Exception { URL yahoo = new URL("http://www.yahoo.com/"); URLConnection conn = yahoo.openConnection(); // Now get some information about the URL String type = conn.getContentType(); String encoding = conn.getContentEncoding(); java.util.Date lastModified = new java.util.Date(conn.getLastModified()); int len = conn.getContentLength(); System.out.println("ContentType: " + type); System.out.println("ContentEncoding: " + encoding); System.out.println("lastModified: " + lastModified); System.out.println("ContentLength: " + len); } // Read the contents of the URL using this stream BufferedReader in = new BufferedReader( new InputStreamReader( conn.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) System.out.println(inputLine); in.close(); } Anfang der Ausgabe: ContentType: text/html ContentEncoding: null lastModified: Thu Jan 01 01:00:00 CET 1970 ContentLength: 16178 Informatik B SS 03 254 Abbildung 64: Client-Server Kommunikation über Sockets 18.4 Sockets für Client/Server Kommunikation 18.4.1 Grundidee der Client/Server Kommunikation URLs sind “high-level” Verbindungen zum Web. Die Implementation baut zum Teil auf Sockets auf. Ein Socket ist ein Endpunkt in einer zweiseitigen Kommunikationsverbindung (link) zwischen zwei Programmen, die auf dem Netz laufen. Ein Socket ist an eine Port-Nummer gebunden, so dass die TCP Schicht die Applikation identifizieren kann, zu/von der die Daten gesendet werden sollen. Server-Seite: Ein Server läuft auf einem Rechner (IP-Adresse) und hat ein Socket, das an eine spezifische Port-Nummer gebunden ist. Der Server wartet und hört dem Socket zu, ob ein Client eine Verbindung anfordert. Client-Seite: Ein Client muss den Hostnamen (IP-Adresse) des Rechners, auf dem der Server läuft, und die Portnummer, mit der der Server verbunden ist, kennen. Um eine Verbinung anzufordern schickt der Client eine Anfrage an den Server. Verbindungsaufbau/Server-Seite: Wenn der Server die Verbindung akzeptiert, erhält der Server ein neues Socket, das an einen neuen Port gebunden ist. Das neue Socket dient der Kommunikation mit dem Clienten. Das Original-Socket bleibt bereit für neue Anfragen. Verbindungsaufbau/Client-Seite: Auf dem Client-Rechner wird ebenfalls ein Socket erzeugt und an eine lokale Portnummer gebunden (dies ist nicht die Portnummer, die bei der ursprünglichen Anfage an den Server verwendet wurde). Kommunikation: Wenn Server und Client erfolgreich verbunden sind, kann durch Schreiben/Lesen von den jeweiligen Sockets komminiziert werden. Informatik B SS 03 255 18.4.2 Sockets in Java im java.net Paket ServerSocket: – Warten auf Client-Anfragen (ServerSocket ssocket = new ServerSocket( portnumber );) (Port-Nummer grösser 1023) – Aufbau einer Verbindung (Socket clientSocket = ssocket.accept();) Socket: Erlaubt plattform-unabhängige Kommunikation, die Klasse ist über plattform-abhängige Implementation definiert und versteckt die spezifischen Details. – Aufbau einer Verbindung zum Server: Socket mySocket = new Socket( hostname , portnumber ) Vollqualifizierter hostname (IP als Zahlencode oder mit Namen), und Portnummer (muss dem Client bekannt sein) – Öffnen von Schreiber und Leser: PrintWriter über mySocket.getOutputStream() BufferedReader über mySocket.getInputStream() 18.4.3 Beispiel ‘KnockKnockServer’ Quelle: Sun-Tutorial Idee: Knock Knock Witze, die nach festem Frage-Antwort-Muster Ablaufen. Protokoll legt Reaktionen des Servers fest. Client-Eingaben über Tastatur. Beispiel: Server: Knock knock! Client: Who’s there! (vom Nutzer einzugeben) Server: Turnip. Client: Turnip who? Server: Turnip the heat, it’s cold in here! Want another (y/n)? Programm-Ausführung: Starte KnockKnockServer (z.B. auf drako) Starte KnockKnockClient (Rechner im gleichen lokalen Netz) Austausch über KnockKnockProtokoll, das die Konventionen von Frage und Antwort festlegt. 256 Informatik B SS 03 import java.net.*; import java.io.*; public class KnockKnockServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(12345); } catch (IOException e) { System.err.println("Could not listen on port: 12345."); System.exit(1); } Socket clientSocket = null; try { clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("Accept failed."); System.exit(1); } PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); String inputLine, outputLine; KnockKnockProtocol kkp = new KnockKnockProtocol(); outputLine = kkp.processInput(null); out.println(outputLine); while ((inputLine = in.readLine()) != null) { outputLine = kkp.processInput(inputLine); out.println(outputLine); if (outputLine.equals("Bye.")) break; } out.close(); in.close(); clientSocket.close(); serverSocket.close(); } } import java.io.*; import java.net.*; public class KnockKnockClient { public static void main(String[] args) throws IOException { Socket kkSocket = null; PrintWriter out = null; BufferedReader in = null; try { kkSocket = new Socket("suleika", 12345); out = new PrintWriter(kkSocket.getOutputStream(), true); in = new BufferedReader( new InputStreamReader(kkSocket.getInputStream())); Informatik B SS 03 257 } catch (UnknownHostException e) { System.err.println("Don’t know about host: suleika."); System.exit(1); } catch (IOException e) { System.err.println("Couldn’t get I/O for the connection to:" + "suleika."); System.exit(1); } BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); String fromServer; String fromUser; while ((fromServer = in.readLine()) != null) { System.out.println("Server: " + fromServer); if (fromServer.equals("Bye.")) break; fromUser = stdIn.readLine(); if (fromUser != null) { System.out.println("Client: " + fromUser); out.println(fromUser); } } out.close(); in.close(); stdIn.close(); kkSocket.close(); } } Erläuterungen zu KnockKnockServer Es wird festgelegt, dass der Server auf Port 12345 auf Anfragen wartet. (Irgendeine Portnummer grösser 1023) Nach Anfrage durch einen potentiellen Clienten wird auf der Server-Seite ein neues Socket für diesen Clienten erzeugt. Die Kommunikation wird aufgebaut. Das Kommunikationsprotokoll regelt die Konversation: – Hole den Input- und Output-Stream des Sockets – Initialisierung der Kommunkation mit dem Client, indem zum Socket geschrieben wird. – Kommunizierte mit dem Clienten (while-Schleife). Der Server ist “ordentlich” und räumt am Ende alles ab. Erläuterungen zu KnockKnockProtocol (Code nicht im Skript, aber im Code-Verzeichnis zur Vorlesung) Erläuterungen zu KnockKnockClient Es muss ein Socket mit Host (IP des Servers) und Port (Port, auf dem der KnockKnockServer von aussen angesprochen werden kann) erzeugt werden. Informatik B SS 03 258 Analog zum Server werden der Input- und der Output-Stream des Sockets geöffnet. In der while-Schleife wird die Kommunikation geregelt. Das Programm kann einfach so erweitert werden, dass mehrere Clienten unterstützt werden. 18.5 Sicherheit Java Programme können dynamisch Klassen aus verschiedenen Quellen laden (Stärke von Java), inklusive “untrusted sources”, z.B. Web-Sites über unsichere Netz-Verbindungen. Gutes Sicherheitskonzept notwendig! Sicherheits-Risiko Applets: Wenn beliebige Applets ausgeführt werden dürften, so könnten diese unauthorisiert in das System des Nutzers eingreifen (Dateien löschen, emails versenden, Information stehlen, ...) Java’s Sicherheitskonzept heisst Zugriffskontrolle: untrusted Code darf nicht auf dem Gast-System lesen, schreiben, löschen und darf nur mit dem Web-Server kommunizieren, von dem er geladen wurde. Java 1.0 Sandbox: Die Installation des java.lang.SecurityManager Objekts, legt die “Sandkiste” fest, in der der fremde Code lesen darf. checkRead() verbietet zum Beispiel, dass die Datei etc/passwd gelesen wird. Java 1.1 Digitally Signed Cases: Zusätzlich java.security Paket. Authentizierung von Code (man weiss, woher der Code kommt). Java 1.2 Permissions und Policies: Feinabgestimmte Vergabe von Schreib-/Leserechten. Verschlüsselung und Entschlüsselung javax.crypto. Informatik B SS 03 259 19 Andere Objekt-Orientierte Sprachen Simula: von Ole-Johan Dahl und Kristen Nygaard(Oslo, Norwegen, 1962-1967). Einführung wichtiger OO-Konzepte wie Klassen, Objekte, Vererbung, dynamische Bindung. (v.a. für diskrete Ereignissimulation) http://java.sun.com/people/jag/SimulaHistory.html Smalltalk (1972, Alan Kay, XEROX). Standardisierung 1980, erste Publikation 1981 in Byte. Eiffel: Bertrand Meyer und Jean Marc Nerson (aus Frankreich, 1985). rein objektorientiert, Entwicklung zuverlässiger Software. C++: Stroustrup (Bell Labs, 1986) Objective-C: (Cox 86) Object Pascal (1985 Apple; 1995 Delphi) Java (C# “C-Sharp”, Java-Clone) 19.1 Das 8-Damen Problem Revisited Das 8-Damen Problem und eine objektorientierte Lösung in Java wurden bereits zu Beginn (siehe Kapitel “Java und Objektorientierung”) besprochen. Im Folgenden: Lösung in anderen OO-Sprachen. Es wird ersichtlich, dass sich die verschiedenen OO-Sprachen recht ähnlich sind (ähnliche Grundkonzepte). siehe: Timothy Budd, 1997, An Introduction to Object-Oriented Programming. 19.2 Lösung in Smalltalk Erstellung von Programmen über ein User-Interface: Smalltalk Browser mit Point-and-Click Editor. Klassen als Typen. Variablen müssen nicht mit Typ deklariert werden. # kennzeichnet Symbol. (Korrespondenz zwischen Name und Wert!) Trennung von Instanz- und Klassenvariablen. Object subclass: #Queen instanceVariableNames: ’column row neighbor’ Variablen können Objekte beliebiger Klassen zugewiesen werden: lastQueen kann mit Objekten der Klasse Queen und anderer Klassen belegt werden. (Beispiel: linkeste Dame als spezieller “Wächter” ohne Nachbar) Informatik B SS 03 260 Methoden können Namen aus mehreren Komponenten haben. setColumn: aNumber neighbor: aQueen " initialize the data fields " column <- aNumber. neighbor <- aQueen. " find first solution " row <- 1. Zuweisung als Unterstrich oder Pfeil nach links; Separator: Punkt (statt Semikolon) Auf Instanzvariablen darf nur über Accessor-Methoden zugegriffen werden. Lokale Variablen werden durch senkrechte Striche markiert, Blöcke durch eckige Klammern. Return durch “Dach” oder Pfeil nach oben. Operatoren werden auch als Methoden betrachtet. Explizite ifTrue und ifFalse Ausdrücke. Bedingungen als Nachrichten. canAttack: testRow column: testColumn | columnDifference | columnDifference <- testColumn - column. (((row = testRow) or: [ row + columnDifference = testRow]) or: [ row - columnDifference = testRow]) ifTrue: [ ˆ true ]. ˆ neighbor canAttack: testRow column: testColumn Erzeugung einer Instanz: lastQueen <- (Queen new) 19.3 Lösung in Objective-C Ähnlich zu Smalltalk. Trennung von Interface und Implementation. Typ id erlaubt, dass Variable mit irgendeinem Objekt belegt wird (nicht typ-geprüft). (alternativ: Object *; typ-geprüft, es dürfen nur Methoden von Object aufgerufen werden.) Objekt-Referenzen (Zeiger wie in C) entsprechen Referenzen in Java (Objective-C: Queen * entspricht Java: Queen) Instanz-Methoden werden durch -, Klassen-Methoden durch + markiert. Aktuelle Instanz: self. @interface Queen : Object { /* data fields */ int row; Informatik B SS 03 261 int column; id neighbor; } /* instance methods */ - (void) initialize: (int) c neighbor: ngh; - (int) advance; /* ... */ @end @implementation Queen : Object /* ... */ - (int) advance { if (row < 8) { row = row + 1; return [ self findSolution ]; } if ( ! [ neighbor advance ] ) return 0; row = 1; return [ self findSolution ]; } 19.4 Lösung in C++ Erlaubt Mehrfachvererbung. Möglichkeit zur Definition von Templates (Generische Klassen), d.h. Klassen mit Parametern (vgl. Datentypen in ML). template<class T> class List { public: void addElement (T newValue); T firstElement (); ListIterator<T> iterator(); // Iterator liefert Objekt // der Klasse ListIterator private: Link<T> * firstLink; // firstLink ist ein Zeiger auf einen Knoten }; Erlaubt Operator-Overloading. Wie in allen genannten OO-Sprachen sind Objekte dynamische Werte und werden über Pointer repräsentiert. Queen * lastQueen = 0; vgl. null in Java und nil in Smalltalk Es gibt lokale Objekte (nicht dynamisch, auf Stack). Sichtbarkeit: Trennung von privaten und öffentlichen Komponenten (wie bei den meisten Sprachen, z.B. Objective-C, Java). Konstruktoren (fast wie Java). 262 Informatik B SS 03 class queen { public: // constructor queen (int, queen *); // find and print solutions bool findSolution(); bool advance(); void print(); private: // data fields int row; const int column; queen * neighbor; // internal method bool canAttack (int, int); }; queen::queen(int col, queen * ngh) : column(col), neighbor(ngh) { row = 1; } bool queen::canAttack (int testRow, int testColumn) { // test rows if (row == testRow) return true; // test diagonals int columnDifference = testColumn - column; if ((row + columnDifference == testRow) || (row - columnDifference == testRow)) return true; // try neighbor return neighbor && neighbor->canAttack(testRow, testColumn); } // ... hier fehlt noch Eiffel