Informatik B Objekt-orientierte Programmierung in Java

Werbung
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ä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
Herunterladen