Programmiermethoden - Hochschule RheinMain

Werbung
Programmiermethoden
Sven Eric Panitz
Hochschule RheinMain
Version 451
Generiert von LectureNotes Teaching System
17. April 2016
Dieses Skript ist das kombinierte Skript zum Modul Programmiermethoden des Studiengangs WI und zu einem Teil des Moduls Programmiermethoden und Techniken im Studiengang AI.
2
Inhaltsverzeichnis
1 Iterierbare Objekte
1.1 Eine Schnittstelle zum Iterieren . . . . . . . . . . . . .
1.2 Eine generische Version der Iterationsschnittstelle . . .
1.3 Die Standard-Schnittstelle Iterator . . . . . . . . . .
1.4 Die Schnittstelle Iterable . . . . . . . . . . . . . . . .
1.4.1 Iterator als Innere Klasse . . . . . . . . . . . .
1.4.2 Verknüpfung zweier iterierbarer Objekte . . . .
1.5 Funktionen als Parameter . . . . . . . . . . . . . . . .
1.5.1 Schleifen als Methoden . . . . . . . . . . . . . .
1.5.2 Standard- (default) Methoden in Schnittstellen
1.5.3 Iterable als verzögerte Listen . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
6
7
9
12
14
15
30
31
31
32
2 Hierarchische Strukturen
2.1 Einfach verkettete Listen . . . . . . . . . .
2.1.1 Formale Definition . . . . . . . . .
2.1.2 Implementierung . . . . . . . . . .
2.1.3 Mathematische Definition . . . . .
2.2 Bäume als verallgemeinerte Listen . . . . .
2.2.1 Formale Definition . . . . . . . . .
2.2.2 Implementierung einer Baumklasse
2.3 XML als serialisierte Baumstruktur . . . .
2.4 Das syntaktische XML-Format . . . . . .
2.4.1 Elementknoten . . . . . . . . . . .
2.4.2 Leere Elemente . . . . . . . . . . .
2.4.3 Gemischter Inhalt . . . . . . . . .
2.4.4 XML Dokumente sind Bäume . . .
2.4.5 Chracter Entities . . . . . . . . . .
2.4.6 CData Sections . . . . . . . . . . .
2.4.7 Kommentare . . . . . . . . . . . .
2.4.8 Processing Instructions . . . . . . .
2.4.9 Attribute . . . . . . . . . . . . . .
2.4.10 Zeichencodierungen . . . . . . . . .
2.4.11 DTD . . . . . . . . . . . . . . . . .
2.5 XML Verarbeitung . . . . . . . . . . . . .
2.5.1 Das DOM Api . . . . . . . . . . .
2.5.2 XPath . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
41
41
41
43
44
53
54
54
67
69
69
70
70
71
72
73
73
74
74
75
76
78
78
81
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
Inhaltsverzeichnis
2.5.3
2.5.4
2.5.5
XSLT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Das SAX-API . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Das StAx-API . . . . . . . . . . . . . . . . . . . . . . . . . 105
3 Eine graphische Komponente für Baumstrukturen
4
107
Kapitel 1
Iterierbare Objekte
Eine häufig zu lösende Aufgabe in der Programmierung ist es, durch bestimmte Elemente zu iterieren. Für die Iteration stehen die Schleifenkonstrukte zur Verfügung.
Eine Iteration (lateinisch iteratio, die Wiederholung), soll nacheinander einen bestimmten Code-Block für verschiedene Elemente durchführen. In klassischer Weise
wird zum Iterieren eine Indexvariable in einer Schleife durchgezählt. Mit dem Index
wird dann in einem Schleifenrumpf jeweils etwas gerechnet.
Eine klassische Schleife zum Durchlaufen der geraden Zahlen kleiner 10:
1
2
3
4
5
6
7
package name . p a n i t z . pmt . i t e r a t i o n ; p u b l i c c l a s s I t e r a t i o n 1 {
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( i n t i = 2 ; i <10; i = i +2){
System . out . p r i n t l n ( i ) ;
}
}
}
Listing 1.1: Iteration1.java
Das obige Beispiel ist in keiner Weise ein objektorientiertes Beispiel. Es ist eine
ganz einfache Schleife, wie aus der imperativen Programmierung gewohnt. In einer
objektorientierten Umsetzung wird stets versucht, ein Objekt zu definieren, das
seine Funktionalität kapselt. In diesem Fall ein Objekt, das weiß, wie zu iterieren ist.
Hierzu kann man versuchen alle im Kopf der for-Schleife gebündelten Informationen
in einem Objekt zu kapseln.
In der for-Schleife gibt es zunächst eine Zählvariable i. Dann einen Test, ob die
Schleife noch weiter durchlaufen werden soll, und eine Anweisung, wie sich die
Schleifenvariable verändert, wenn es zum nächsten Durchlauf geht. Packt man diese
drei Informationen in ein Objekt, so erhalten wir die folgende Klasse:
1
2
3
4
5
package name . p a n i t z . pmt . i t e r a t i o n ;
public class GeradeZahlenKleiner10Iterierer {
i n t i ; // d i e S c h l e i f e n v a r i a b l e
GeradeZahlenKleiner10Iterierer ( int i ){
t h i s . i = i ; // I n i t i a l i s i e r u n g d e r S c h l e i f e n v a r i a b l e
5
Kapitel 1 Iterierbare Objekte
}
boolean s c h l e i f e n T e s t ( ) {
r e t u r n i < 1 0 ; // Test u e b e r d i e S c h l e i f e n v a r i a b l e
}
void s c h l e i f e W e i t e r s c h a l t e n ( ) {
i = i + 2 ; // f u e r n a e c h s t e n S c h l e i f e n d u r c h l a u f
}
6
7
8
9
10
11
12
Listing 1.2: GeradeZahlenKleiner10Iterierer.java
Im Schleifenrumpf wird der aktuelle Wert der Iteration benutzt. In der obigen forSchleife ist das die Zahl i. Auch hierfür können wir noch eine Methode vorsehen.
int schleifenWert () {
return i ;
}
13
14
15
Listing 1.3: GeradeZahlenKleiner10Iterierer.java
Mit einem Objekt dieser Klasse können wir jetzt eine Schleife durchlaufen, ebenso
wie in der ersten Schleife oben:
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
for ( GeradeZahlenKleiner10Iterierer i t
= new G e r a d e Z a h l e n K l e i n e r 1 0 I t e r i e r e r ( 0 )
; it . schleifenTest ()
; it . schleifeWeiterschalten () ){
System . out . p r i n t l n ( i t . s c h l e i f e n W e r t ( ) ) ;
}
}
16
17
18
19
20
21
22
23
24
}
Listing 1.4: GeradeZahlenKleiner10Iterierer.java
1.1 Eine Schnittstelle zum Iterieren
Noch ist überhaupt kein Vorteil darin erkennbar, dass wir die Komponenten, die
zur Programmierung einer Schleife benötigt werden (Initialisierung des Iterierungsobjektes, Test, Weiterschaltung, Abfragen des Schleifenelementes) in einem Objekt
gekapselt haben. Der Vorteil der Objektorientierung liegt in zusätzlichen Abstraktionen. Eine der stärksten Abstraktionen in Java sind Schnittstellen. Naheliegend
ist es, die Methoden der obigen Klasse in einer Schnittstelle vorzugeben:
1
2
3
4
package name . p a n i t z . pmt . i t e r a t i o n ;
public interface I n t I t e r i e r e r {
boolean s c h l e i f e n T e s t ( ) ;
void s c h l e i f e W e i t e r s c h a l t e n ( ) ;
6
1.2 Eine generische Version der Iterationsschnittstelle
Integer schleifenWert () ;
5
6
}
Listing 1.5: IntIterierer.java
Die Klasse oben lässt sich nun als Implementierung dieser Schnittstelle schreiben.
Um die Klasse dabei noch ein wenig allgemeiner zu schreiben, übergeben wir jetzt
auch die obere Schranke der Iteration im Konstruktor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package name . p a n i t z . pmt . i t e r a t i o n ;
p u b l i c c l a s s G e r a d e Z a h l e n I t e r i e r e r implements I n t I t e r i e r e r {
i n t from ;
i n t to ;
G e r a d e Z a h l e n I t e r i e r e r ( i n t from , i n t t o ) {
t h i s . from = from ;
t h i s . to = to ;
}
public boolean s c h l e i f e n T e s t ( ) {
r e t u r n from < t o ;
}
public void s c h l e i f e W e i t e r s c h a l t e n ( ) {
from = from + 2 ;
}
public Integer schleifenWert () {
r e t u r n from ;
}
Listing 1.6: GeradeZahlenIterierer.java
Eine Schleife für dieses Klasse sieht jetzt wie folgt aus. Man erkennt, dass die Schleife
nur noch für die Initialisierung des Objektes der Iteration anwendungsspezifischen
Code hat. Ansonsten liegt die Anwendungslogik komplett im Iterations-Objekt.
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( I n t I t e r i e r e r i t = new G e r a d e Z a h l e n I t e r i e r e r ( 0 , 1 0 )
; it . schleifenTest ()
; it . schleifeWeiterschalten () ){
System . out . p r i n t l n ( i t . s c h l e i f e n W e r t ( ) ) ;
}
}
18
19
20
21
22
23
24
25
}
Listing 1.7: GeradeZahlenIterierer.java
1.2 Eine generische Version der Iterationsschnittstelle
Eine weitere Abstraktion, die von heutigen Programmiersprachen angeboten wird,
liegt in der Möglichkeit, Typen variabel zu halten über generische Klassen und
7
Kapitel 1 Iterierbare Objekte
Schnittstellen. Statt eine Iterations-Schnittstelle für ganze Zahlen zu definieren,
lässt sich allgemein eine solche für beliebig aber feste Typen definieren:
1
2
3
4
5
6
package name . p a n i t z . pmt . i t e r a t i o n ;
p u b l i c i n t e r f a c e G e n e r i s c h e r I t e r i e r e r <A> {
boolean s c h l e i f e n T e s t ( ) ;
void s c h l e i f e W e i t e r s c h a l t e n ( ) ;
A schleifenWert () ;
}
Listing 1.8: GenerischerIterierer.java
Beim Implementieren dieser generischen Schnittstelle gilt es jetzt zu entscheiden,
welcher konkrete Typ für die Typvariable eingesetzt werden soll, also was für einen
Typ die Elemente haben, über die iteriert werden soll. In unserem Beispielfall sind
dieses ganze Zahlen. Da für Typvariablen in generischen Typen keine primitiven
Typen eingesetzt werden dürfen, benutzen wir hier die Klasse Integer und vertrauen an mehreren Stellen darauf, dass intern Daten vom Typ int und Integer
gegenseitig konvertiert werden. Dieses wird als automatisches Boxing/Unboxing
bezeichnet.
Die neue Implementierung der Iterationsklasse sieht entsprechend wie folgt aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package name . p a n i t z . pmt . i t e r a t i o n ;
public class GeradeZahlenIterierer2
implements G e n e r i s c h e r I t e r i e r e r <I n t e g e r >{
i n t from ;
i n t to ;
G e r a d e Z a h l e n I t e r i e r e r 2 ( i n t from , i n t t o ) {
t h i s . from = from ;
t h i s . to = to ;
}
public boolean s c h l e i f e n T e s t () {
r e t u r n from < t o ;
}
public void s c h l e i f e W e i t e r s c h a l t e n ( ) {
from = from + 2 ;
}
public Integer schleifenWert () {
r e t u r n from ;
}
Listing 1.9: GeradeZahlenIterierer2.java
An der Benutzung ändert sich in diesem Fall wenig, gegenüber der nicht generischen
Version.
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( G e n e r i s c h e r I t e r i e r e r <I n t e g e r > i t
= new G e r a d e Z a h l e n I t e r i e r e r 2 ( 0 , 1 0 )
19
20
21
8
1.3 Die Standard-Schnittstelle Iterator
; it . schleifenTest ()
; it . schleifeWeiterschalten () ){
int i = i t . schleifenWert () ;
System . out . p r i n t l n ( i ) ;
22
23
24
25
}
26
}
27
28
}
Listing 1.10: GeradeZahlenIterierer2.java
Gewonnen haben wir nun die Möglichkeit, auf gleiche Weise Iteratorobjekte zu
implementieren, deren Elemente andere Typen haben.
1.3 Die Standard-Schnittstelle Iterator
Die
im
vorangegangenen
Abschnitt
entwickelte
Schnittstelle
GenerischerIterierer hat ein Pendant in Javas Standard-API. Im Paket
java.util findet sich dort ebenfalls eine generische Schnittstelle, die beschreibt,
dass ein Objekt zum Iterieren benutzt werden kann, die Schnittstelle Iterator.
Diese ist allerdings ein wenig anders aufgebaut, als unsere Schnittstelle. Sie definiert
drei Methoden:
• boolean hasNext(): diese Methode entspricht ziemlich genau unserer Methode schleifenTest. Sie ergibt true, wenn es noch weitere Elemente zum
Iterieren gibt.
• A next(): diese Methode vereint die zwei Methoden schleifenWert() und
schleifeWeiterschalten unserer Schnittstelle. Es wird dabei das aktuelle
Element zurück gegeben und gleichzeitig intern der Schritt weiter zum nächsten Element vorgenommen. Dieser Schritt kann nicht mehr rückgängig
gemacht werden.
• void remove(): diese Methode ist in der Schnittstelle als optional bezeichnet.
Mit ihr soll es möglich sein, das aktuelle Element aus dem Objekt, über das
iteriert wird, zu löschen. Handelt es sich bei dem Iterator um einen Iterator,
der über die Elemente einer Liste iteriert, soll also das entsprechende Element
aus der Liste gelöscht werden. In unserem Fall der Iteration über einen Bereich
der ganzen Zahlen ergibt diese Operation keinen Sinn. Genau aus diesem
Grund wurde sie in der Dokumentation als optional markiert.
Um unsere Überlegungen zu einer Iterationsschnittstelle mit der Standardschnittstelle Iterator zu verbinden, bietet sich an, eine abstrakte Klasse zu
definieren, die unsere drei Methoden als abstrakte, noch zu implementierende Methoden vorgibt, die Methoden der Standardschnittstelle mit Hilfe der abstrakten Methoden aber bereits implementiert.
Wir definieren zunächst die drei bereits bekannten abstrakten Methoden:
9
Kapitel 1 Iterierbare Objekte
1
2
package name . p a n i t z . pmt . i t e r a t i o n ;
p u b l i c a b s t r a c t c l a s s A b s t r a c t I t e r a t o r <A> implements j a v a . u t i l .
I t e r a t o r <A> {
3
public abstract boolean s c h l e i f e n T e s t ( ) ;
public abstract void s c h l e i f e W e i t e r s c h a l t e n ( ) ;
public abstract A schleifenWert () ;
4
5
6
Listing 1.11: AbstractIterator.java
Nun können wir mit diesen die Methoden der Standardschnittstelle Iterator umsetzen. Beginnen wir mit der Methode next():
p u b l i c A next ( ) {
A result = schleifenWert () ;
schleifeWeiterschalten () ;
return r e s u l t ;
}
7
8
9
10
11
Listing 1.12: AbstractIterator.java
Man sieht genau die Bedeutung der Methode next() der aktuelle Schleifenwert wird
zurück gegeben. zusätzlich wird der Iterator weiter geschaltet, so dass ein nächster
Aufruf das darauffolgende Iterationselement zurück gibt.
Die Methode schleifenTest() entspricht exakt der Methode hasNext() der Standardschnittstelle. Deshalb können wir in der Implementierung unsere Methode direkt aufrufen.
p u b l i c b o o l e a n hasNext ( ) {
return schleifenTest () ;
}
12
13
14
Listing 1.13: AbstractIterator.java
Schließlich sind wir noch gezwungen die Methode remove() zu implementieren. Da
sie optional ist und wir sie in unseren Beispielen nicht sinnvoll umsetzen können,
soll nur eine Laufzeitausnahme geworfen werden.
p u b l i c v o i d remove ( ) {
throw new Unsu pp ortedOperati onExc eptio n ( ) ;
}
15
16
17
18
}
Listing 1.14: AbstractIterator.java
Um wieder an unseren konkreten Iterator über einen bestimmten Zahlenbereich
zu kommen, können wir jetzt eine konkrete Unterklasse dieser abstrakten Klasse
10
1.3 Die Standard-Schnittstelle Iterator
schreiben. Diese implementiert dann automatisch über die geerbten Methoden auch
die Standardschnittstelle Iterator.
Um noch ein wenig flexibler zu werden, sei noch ein drittes Feld der Klasse zugefügt,
dass die Schrittweite beim Weiterschalten des Iterators speichert.
1
2
3
4
5
package name . p a n i t z . pmt . i t e r a t i o n ;
p u b l i c c l a s s I n t e g e r R a n g e I t e r a t o r e x t e n d s A b s t r a c t I t e r a t o r <I n t e g e r >{
i n t from ;
i n t to ;
int step ;
6
I n t e g e r R a n g e I t e r a t o r ( i n t from , i n t to , i n t s t e p ) {
t h i s . from = from ;
t h i s . to = to ;
this . step = step ;
}
public boolean s c h l e i f e n T e s t ( ) {
r e t u r n from <= t o ;
}
public void s c h l e i f e W e i t e r s c h a l t e n ( ) {
from = from + s t e p ;
}
public Integer schleifenWert () {
r e t u r n from ;
}
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Listing 1.15: IntegerRangeIterator.java
Damit können wir jetzt in der standardmäßig in Java empfohlenen Art und Weise
mit dem Iterationsobjekt über die Elemente mit einer for-Schleife iterieren. Der Iterator wird initialisiert und seine Methode hasNext() zum Schleifentest benutzt. Das
Weiterschalten wird nicht im Kopf der for-Schleife vorgenommen, sondern als erster
Befehl im Rumpf, indem dort die Methode next() als erste Anweisung aufgerufen
wird.
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( j a v a . u t i l . I t e r a t o r <I n t e g e r > i t = new I n t e g e r R a n g e I t e r a t o r
(0 ,10 ,2)
; i t . hasNext ( ) ; ) {
i n t i = i t . next ( ) ;
System . out . p r i n t l n ( i ) ;
}
}
21
22
23
24
25
26
27
28
}
Listing 1.16: IntegerRangeIterator.java
11
Kapitel 1 Iterierbare Objekte
1.4 Die Schnittstelle Iterable
Bisher haben wir definiert, was unter Iterator-Objekten zu verstehen ist. In Javas
Standard-API gibt es eine weitere Schnittstelle, die sich mit dem Konzept der Iteration beschäftigt. Es handelt sich dabei um die Schnittstelle Iterable. Diese liegt
nicht im Paket java.util sondern im Standardpaket java.lang. Sie muss also
nicht explizit importiert werden, wenn man sie benutzen will. Auch die Schnittstelle
Iterable ist generisch. Sie soll ausdrücken, dass ein Objekt iterierbar ist, in dem
Sinne, dass es ein Iterator-Objekt gibt, mit dessen Hilfe über die Elemente des
Objekts iteriert werden kann.
Deshalb kommt die Schnittstelle Iterable zunächst (im nächsten Abschnitt werden wir sehen, das dem bald nicht mehr so ist) mit einer einzigen Methode aus.
Die Methode heißt iterator() und ist dazu gedacht, das Iteratorobjekt für die
Klasse zu erfragen. Typische Beispiele sind hierbei natürlich die klassischen Sammlungsklassen, wie Listen und Mengen. Diese implementieren alle die Schnittstelle
Iterable.
Bleiben wir zunächst bei unserem durchgängigen Beispiel. Statt jetzt direkt die
Iteratorklasse für einen Zahlenbereich zu definieren, können wir zunächst eine
Klasse definieren, die nur einen Zahlenbereich beschreibt. Wir lassen sie aber die
Schnittstelle Iterable implementieren. Erst der Aufruf der Methode iterator()
erzeugt dann ein entsprechendes Iterator-Objekt.
1
2
3
4
5
package name . p a n i t z . pmt . i t e r a t i o n ;
p u b l i c c l a s s I n t e g e r R a n g e implements I t e r a b l e <I n t e g e r >{
i n t from ;
i n t to ;
int step ;
6
p u b l i c I n t e g e r R a n g e ( i n t from , i n t to , i n t s t e p ) {
t h i s . from = from ;
t h i s . to = to ;
this . step = step ;
}
7
8
9
10
11
Listing 1.17: IntegerRange.java
Um diese Klasse noch etwas flexibler benutzen zu können, sehen wir zwei weitere Konstruktoren vor. Diese setzen die Schrittweite und die obere Schranke auf
Standardwerte:
p u b l i c I n t e g e r R a n g e ( i n t from , i n t t o ) {
t h i s ( from , to , 1 ) ;
}
p u b l i c I n t e g e r R a n g e ( i n t from ) {
t h i s ( from , I n t e g e r .MAX_VALUE, 1 ) ;
}
12
13
14
15
16
17
12
1.4 Die Schnittstelle Iterable
Listing 1.18: IntegerRange.java
Die Klasse zum Iterieren über einen Zahlenbereich haben wir bereits entwickelt.
Diese kann nun für die Methode iterator() benutzt werden.
p u b l i c j a v a . u t i l . I t e r a t o r <I n t e g e r > i t e r a t o r ( ) {
r e t u r n new I n t e g e r R a n g e I t e r a t o r ( from , to , s t e p ) ;
}
18
19
20
Listing 1.19: IntegerRange.java
Java hat eine syntaktische Besonderheit für Objekte, die die Schnittstelle Iterable
implementieren, eingeführt. Eine besondere Art der Schleife, die als for-each Schleife
bezeichnet wird. Diese hat im Schleifenrumpf zwei Komponenten, die durch einen
Doppelpunkt getrennt werden. Nach dem Doppelpunkt steht das iterierbare Objekt,
vor dem Doppelpunkt wird die Variable definiert, an der in jedem Schleifendurchlauf
das aktuelle Element gebunden ist.
Für unser Beispiel erhalten wir dann die folgende kompakte Schleife:
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
I n t e g e r R a n g e i s = new I n t e g e r R a n g e ( 0 , 1 0 , 2 ) ;
for ( int i : is ){
System . out . p r i n t l n ( i ) ;
}
21
22
23
24
25
Listing 1.20: IntegerRange.java
Gelesen wird dieses Konstrukt als: Für alle Zahlen i aus dem iterierbaren Objekt is
führe den Schleifenrumpf aus.
Im Vergleich hierzu, sei hier noch einmal die entsprechende Schleife ohne Verwendung der for-each Schleife geschrieben. In diesem Fall wird explizit nach dem
Iterator-Objekt gefragt.
f o r ( j a v a . u t i l . I t e r a t o r <I n t e g e r > i t = i s . i t e r a t o r ( ) ; i t . hasNext ( ) ; )
{
i n t i = i t . next ( ) ;
System . out . p r i n t l n ( i ) ;
}
26
27
28
29
}
30
31
}
Listing 1.21: IntegerRange.java
13
Kapitel 1 Iterierbare Objekte
1.4.1 Iterator als Innere Klasse
Da in der Regel die Iteratorklasse fest an der Klasse gebunden ist, über deren
Elemente iteriert werden soll, bietet sich an, die Iteratorklasse zu verstecken. In Java
kann hierfür eine innere Klasse benutzt werden. Mit einem Sichtbarkeitsattribut
private ist dann die Iteratorklasse komplett versteckt:
Wir sehen also zunächst die Klasse mit den notwendigen Feldern für die beschreibung des Iterationsbereichs vor:
32
33
package name . p a n i t z . pmt . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
34
35
36
37
38
p u b l i c c l a s s I n t e g e r R a n g e implements I t e r a b l e <I n t e g e r >{
i n t from ;
i n t to ;
int step ;
39
p u b l i c I n t e g e r R a n g e ( i n t from , i n t to , i n t s t e p ) {
t h i s . from = from ;
t h i s . to = to ;
this . step = step ;
}
40
41
42
43
44
45
p u b l i c I n t e g e r R a n g e ( i n t from , i n t t o ) {
t h i s ( from , to , 1 ) ;
}
p u b l i c I n t e g e r R a n g e ( i n t from ) {
t h i s ( from , I n t e g e r .MAX_VALUE, 1 ) ;
}
46
47
48
49
50
51
Listing 1.22: IntegerRange.java
Für diese Klasse kann ein Iterator-Objekt erzeugt werden:
p u b l i c I t e r a t o r <I n t e g e r > i t e r a t o r ( ) {
r e t u r n new I n t e g e r R a n g e I t e r a t o r ( from , to , s t e p ) ;
}
52
53
54
Listing 1.23: IntegerRange.java
Das Iterator-Objekt ist dabei von einer inneren Klasse, die beschreibt, wie durch
die Elemente iteriert wird:
p r i v a t e s t a t i c c l a s s I n t e g e r R a n g e I t e r a t o r implements I t e r a t o r <
I n t e g e r >{
i n t from ;
i n t to ;
int step ;
55
56
57
58
59
14
1.4 Die Schnittstelle Iterable
I n t e g e r R a n g e I t e r a t o r ( i n t from , i n t to , i n t s t e p ) {
t h i s . from = from ;
t h i s . to = to ;
this . step = step ;
}
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n from < t o ;
}
p u b l i c I n t e g e r next ( ) {
i n t r e s u l t = from ;
from = from + s t e p ;
return r e s u l t ;
}
p u b l i c v o i d remove ( ) {}
60
61
62
63
64
65
66
67
68
69
70
71
72
73
}
74
75
}
Listing 1.24: IntegerRange.java
1.4.2 Verknüpfung zweier iterierbarer Objekte
In diesem Abschnitt werden wir aus zwei iterierbaren Objekten eines machen. Das
neue Objekte soll erst durch die Elemente des einen iterierebaren Objekts und anschließend durch die Elemente des zweiten iterieren. Es ist also ein Aneinanderhängen der beiden Iterables. Fast wie bei Listen, die aneinander gehängt werden.
Hierzu sei die Klasse Append entwickelt. Objekte dieser Klasse sollen aus zwei iterierbaren Objekten ein neues erzeugen. Somit ist sie als generische Klasse, die Iterable
implementiert entworfen:
1
2
package name . p a n i t z . pmt . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
3
4
p u b l i c c l a s s Append<E> implements I t e r a b l e <E> {
Listing 1.25: Append.java
Die Klasse Append soll zwei iterierbare Objekte zu einem neuen verbinden. Hierzu
braucht sie Felder, um die beiden zu verkknüpfenden Objekte zu speichern:
5
6
I t e r a b l e <E> xs ;
I t e r a b l e <E> ys ;
Listing 1.26: Append.java
In gewohnter Weise ist ein Konstruktor vorzusehen, der diese Felder initialisiert.
15
Kapitel 1 Iterierbare Objekte
p u b l i c Append ( I t e r a b l e <E> xs , I t e r a b l e <E> ys ) {
t h i s . xs = xs ;
t h i s . ys = ys ;
}
7
8
9
10
Listing 1.27: Append.java
Es muss schließlich eine Methode geben, die den neuen Iterator zurück gibt, der
durch beide Objekte iteriert.
@Override
p u b l i c I t e r a t o r <E> i t e r a t o r ( ) {
r e t u r n new MyIt ( ) ;
}
11
12
13
14
Listing 1.28: Append.java
Die hier instantiierte Klasse MyIt wird als innere Klasse der Klasse Append realisiert:
p u b l i c c l a s s MyIt implements I t e r a t o r <E> {
15
Listing 1.29: Append.java
Der neue Iterator muss die beiden ursprünglichen Iteratoren kennen. Daher werden
zwei Felder für diese vorgesehen. Initialisiert werden die Felder mit den Iteratoren,
die die beiden zu verknüpfenden iterierbaren Objekte der äußeren Klasse Append
zurück geben:
I t e r a t o r <E> x s I t = xs . i t e r a t o r ( ) ;
I t e r a t o r <E> y s I t = ys . i t e r a t o r ( ) ;
16
17
Listing 1.30: Append.java
Es bleiben die Methoden der Schnittstelle Iterator, die implementiert werden
müssen. Es stellt sich die Methode hasNext() als besonders einfach heraus. Der
neue Iterator hat noch weitere Elemente, wenn einer der beiden ursprünglichen
Iteratoren noch ein nächstes Element hat.
@Override
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n x s I t . hasNext ( ) | | y s I t . hasNext ( ) ;
}
18
19
20
21
Listing 1.31: Append.java
Wo holt der neue Iterator sein nächstes Element her? Wenn es noch weitere Elemente
im ersten Iterator gibt, dann aus diesem, ansonsten aus dem zweiten Iterator.
16
1.4 Die Schnittstelle Iterable
@Override
p u b l i c E next ( ) {
i f ( x s I t . hasNext ( ) ) r e t u r n x s I t . next ( ) ;
r e t u r n y s I t . next ( ) ;
}
22
23
24
25
26
Listing 1.32: Append.java
Auf einer Implementierung der Methode remove() verzichten wir wieder.
@Override p u b l i c v o i d remove ( ) {}
27
}
28
29
}
Listing 1.33: Append.java
In einer kleinen Testklasse soll die Klasse Append an zwei Beispielen ausprobiert
werden.
1
package name . p a n i t z . pmt . u t i l ;
Listing 1.34: AppendTest.java
Einer der beiden Beispielaufrufe soll mit Standardlisten gemacht werden, so dass
diese importiert werden.
2
3
4
5
6
import
import
import
import
import
java . u t i l . ArrayList ;
java . u t i l . I t e r a t o r ;
java . u t i l . LinkedList ;
java . u t i l . List ;
name . p a n i t z . pmt . i t e r a t i o n . I n t e g e r R a n g e ;
Listing 1.35: AppendTest.java
Im ersten Beispielaufruf sollen zwei iterierte Zahlenbereiche nacheinander iteriert
werden:
7
8
9
10
11
p u b l i c c l a s s AppendTest {
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( i n t i : new Append<>(new I n t e g e r R a n g e ( 1 , 1 5 ) , new I n t e g e r R a n g e
(47 ,52) ) ){
System . out . p r i n t l n ( i ) ;
}
Listing 1.36: AppendTest.java
Im zweiten Beispielaufruf werden als iterierbare Objekte zunächst Listen erzeugt:
17
Kapitel 1 Iterierbare Objekte
L i s t <S t r i n g > xs = new L i n k e d L i s t <>() ;
xs . add ( ” Freunde ” ) ;
xs . add ( ” Roemer ” ) ;
xs . add ( ” L a n d s l e u t e ” ) ;
12
13
14
15
16
L i s t <S t r i n g > ys = new A r r a y L i s t <>() ;
ys . add ( ” l e i h t ” ) ;
ys . add ( ” mir ” ) ;
ys . add ( ” Euer ” ) ;
ys . add ( ”Ohr” ) ;
17
18
19
20
21
Listing 1.37: AppendTest.java
Diese beiden Listen lassen sich nun auch zu einem neuen iterierbaren Objekt mit
Append verbinden:
f o r ( S t r i n g s : new Append<>(xs , ys ) ) {
System . out . p r i n t l n ( s . toUpperCase ( ) ) ;
}
22
23
24
}
25
26
}
Listing 1.38: AppendTest.java
Aufgabe 1 In dieser Aufgabe sollen Sie Klassen schreiben, die die Schnittstelle
Iterable implementieren und es ermöglichen über unterschiedliche Daten zu
iterieren. Der Iterator soll dabei jeweils als private innere Klasse realisiert
werden.
a) Schreiben Sie noch einmal, wie bereits in diesem Kapitel vorgemacht, eine
Iterable-Klasse, die es es erlaubt über einen Zahlenbereich zu iterieren.
Nennen Sie die Klasse IntRange und sehen Sie viele verschiedene Konstruktoren vor:
• IntRange(): es wird endlos über alle int Werte beginnend bei 0
iteriert.
• IntRange(int from): es wird endlos über alle int Werte beginnend
bei from iteriert.
• IntRange(int from, int to): es wird beginnend bei from bis
einschließlich to iteriert.
• IntRange(int from, int to, int step): es wird beginnend bei
from bis einschließlich to in step Schritten iteriert.
Lösung
1
2
18
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
1.4 Die Schnittstelle Iterable
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
p u b l i c c l a s s IntRange implements I t e r a b l e <I n t e g e r >{
i n t from ;
i n t to ;
int step ;
boolean i n f i n i t e ;
p u b l i c IntRange ( i n t from ,
i n t to ,
int step ){
t h i s . from = from ;
t h i s . to = to ;
this . step = step ;
infinite = false ;
}
p u b l i c IntRange ( i n t from ,
i n t to ) {
t h i s ( from , to , 1 ) ;
}
p u b l i c IntRange ( i n t from ) {
t h i s ( from , 0 ) ;
i n f i n i t e = true ;
}
p u b l i c IntRange ( ) {
this (1) ;
}
@Override p u b l i c I t e r a t o r <I n t e g e r > i t e r a t o r ( ) {
r e t u r n new I t e r ( from ) ;
}
p r i v a t e c l a s s I t e r implements I t e r a t o r <I n t e g e r >{
i n t from ;
I t e r ( i n t from ) { t h i s . from = from ; }
@Override p u b l i c b o o l e a n hasNext ( ) {
r e t u r n i n f i n i t e | | from<= t o ;
}
@Override p u b l i c I n t e g e r next ( ) {
i n t r e s u l t = from ;
from = from+s t e p ;
return r e s u l t ;
}
}
}
Listing 1.39: IntRange.java
b) Schreiben Sie eine Klasse Fib, die Iterable<Integer> so implementiert,
dass beim Aufruf der Methode next nacheinander die Fibonaccizahlen
zurück gegeben werden.
Lösung Da die Fibonaccizahlen so schnell wachsen, dass sie nach wenigen Zahlen den Bereich des Datentyps Integer sprengen, hier abweichend
von der Aufgabenstellung eine Lösung, die die Klasse BigInteger benutzt
und somit beliebig große Zahlen berechnen kann.
1
package name . p a n i t z . u t i l ;
19
Kapitel 1 Iterierbare Objekte
2
3
4
5
6
7
8
9
10
import j a v a . u t i l . I t e r a t o r ;
import j a v a . math . B i g I n t e g e r ;
p u b l i c c l a s s Fib implements I t e r a b l e <B i g I n t e g e r >{
@Override p u b l i c I t e r a t o r <B i g I n t e g e r > i t e r a t o r ( ) {
r e t u r n new I t e r ( ) ;
}
p r i v a t e c l a s s I t e r implements I t e r a t o r <B i g I n t e g e r >{
B i g I n t e g e r n2 = B i g I n t e g e r . v a l u e O f ( 0 ) ;
B i g I n t e g e r n1 = B i g I n t e g e r . v a l u e O f ( 1 ) ;
11
@Override p u b l i c b o o l e a n hasNext ( ) {
return true ;
}
@Override p u b l i c B i g I n t e g e r next ( ) {
B i g I n t e g e r tmp = n2 . add ( n1 ) ;
n2 = n1 ;
n1 = tmp ;
r e t u r n n2 ;
}
12
13
14
15
16
17
18
19
20
}
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
new Fib ( ) . f o r E a c h ( x−> System . out . p r i n t l n ( x ) ) ;
}
21
22
23
24
25
}
Listing 1.40: Fib.java
c) Schreiben Sie eine Klasse IterableString. Sie soll im Konstruktor ein
String-Objekt erhalten. Die Klasse soll Iterable<Character> so implementieren, dass über die einzelnen Zeichen des Strings iteriert wird.
Lösung Die iterierbare Klasse bekommt einen String im Konstruktor übergeben. Als Iterator wird ein Objekt einer inneren Iteratorklasse
erzeugt:
1
2
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
3
4
5
public class
I t e r a b l e S t r i n g implements I t e r a b l e <Character >{
f i n a l private String str ;
6
7
8
9
public IterableString ( String str ){
this . str = str ;
}
10
11
12
13
p u b l i c I t e r a t o r <Character > i t e r a t o r ( ) {
r e t u r n new I t e r ( ) ;
}
Listing 1.41: IterableString.java
20
1.4 Die Schnittstelle Iterable
Nun wird die innere Iteratorklasse definiert. Diese hat ein int Feld, in
dem der Iterator wich merkt, wie oft schon das nächste Element erfragt
wurde.
p r i v a t e c l a s s I t e r implements I t e r a t o r <Character >{
private int i = 0;
14
15
Listing 1.42: IterableString.java
Wenn bereits alle Zeichen des Strings mit next() erfragt wurden, dann
gibt es kein nächstes Element.
p u b l i c b o o l e a n hasNext ( ) {
return i < str . length () ;
}
16
17
18
Listing 1.43: IterableString.java
Um das nächste Element zurück zu geben, wird das i-te Element des
Strings erfragt. Anschließend wird i um 1 erhöht.
p u b l i c C h a r a c t e r next ( ) {
r e t u r n s t r . charAt ( i ++) ;
}
19
20
21
}
22
23
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( c h a r c : new I t e r a b l e S t r i n g ( ” H e l l o world ! ” ) ) {
System . out . p r i n t l n ( c ) ;
}
}
24
25
26
27
28
29
}
Listing 1.44: IterableString.java
d) Schreiben Sie eine Klasse Lines. Sie soll Iterable<String> so implementieren, dass nacheinander die Zeilen eines im Konstruktor übergebenen Strings bei der Iteration durchlaufen werden.
Lösung
1
2
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
3
4
5
6
7
8
p u b l i c c l a s s L i n e s implements I t e r a b l e <S t r i n g > {
s t a t i c S t r i n g NEW_LINE = System . g e t P r o p e r t y ( ” l i n e . s e p a r a t o r
”) ;
String [ ] l i n e s ;
public Lines ( String s t r ) {
l i n e s = s t r . s p l i t (NEW_LINE) ;
21
Kapitel 1 Iterierbare Objekte
}
9
10
@Override p u b l i c I t e r a t o r <S t r i n g > i t e r a t o r ( ) {
r e t u r n new MyIt ( ) ;
}
11
12
13
14
p u b l i c c l a s s MyIt implements I t e r a t o r <S t r i n g > {
int i = 0;
@Override p u b l i c b o o l e a n hasNext ( ) {
r e t u r n i <l i n e s . l e n g t h ;
}
15
16
17
18
19
20
@Override p u b l i c S t r i n g next ( ) {
r e t u r n l i n e s [ i ++];
}
21
22
23
}
24
25
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
f o r ( S t r i n g s : new L i n e s ( ” h a l l o ”+NEW_LINE+” w e l t ! ”+NEW_LINE
+” wie ”+NEW_LINE+” g e h t ”+NEW_LINE+” e s ” ) )
System . out . p r i n t l n ( s ) ;
}
26
27
28
29
30
}
Listing 1.45: Lines.java
e) Kongruenzgeneratoren sind Algorithmen, mit denen sich zufällig aussehende Zahlenfolgen erzeugen erzeugen lassen. Man spricht dabei auch
von Pseudozufallszahlen. Schreiben Sie eine iterierbare Klasse Random,
die als Iterator einen einfachen Kongruenzgenerator benutzt, um eine
Zahlenfolge zu erzeugen.
f) Schreiben
Sie
eine
Klasse
ReaderIterable.
Sie
soll
Iterable<Character> so implementieren, dass über die einzelnen
Zeichen, die ein im Konstruktor übergebenes Reader-Objekt liefert,
iteriert wird.
Bei der Lösung dieser Aufgabe ergibt sich ein Problem. Ein Reader kann
nur einmal zum Iterieren benutzt werden. Würde man erlauben, das zwei
Iteratoren die Methode desselben Readers benutzen, um das nächste Element zu holen, dann würden beide Iteratoren implizit ein Zeichen weiter
geschaltet werden. Daher werden wir verbieten, dass zweimal die Methode iterator() aufgerufen wird. Beim zweiten Mal wird dann eine Ausnahme geworfen.
Lösung Bei der Lösung dieser Aufgabe ergibt sich ein Problem. Ein
Reader kann nur einmal zum Iterieren benutzt werden. Würde man erlauben, das zwei Iteratoren die Methode desselben Readers benutzen,
22
1.4 Die Schnittstelle Iterable
um das nächste Element zu holen, dann würden beide Iteratoren implizit ein Zeichen weiter geschaltet werden. Daher werden wir verbieten,
dass zweimal die Methode iterator() aufgerufen wird. Beim zweiten
Mal wird dann eine Ausnahme geworfen.
1
2
3
4
5
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
import j a v a . i o . Reader ;
import j a v a . i o . IOException ;
import j a v a . i o . F i l e R e a d e r ;
6
7
8
9
p u b l i c c l a s s R e a d e r I t e r a b l e implements I t e r a b l e <Character >{
f i n a l p r i v a t e Reader i n ;
boolean iteratorGiven = f a l s e ;
10
11
12
13
p u b l i c R e a d e r I t e r a b l e ( Reader i n ) {
this . in = in ;
}
14
15
16
17
18
19
20
21
p u b l i c I t e r a t o r <Character > i t e r a t o r ( ) {
i f ( iteratorGiven )
throw
new RuntimeException ( ” R e a d e r I t e r a b l e can o n l y be
i t e r a t e d once ” ) ;
iteratorGiven = true ;
r e t u r n new I t e r ( ) ;
}
22
23
24
25
26
27
28
29
30
31
32
33
34
p r i v a t e c l a s s I t e r implements I t e r a t o r <Character >{
i n t nextChar ;
Iter () {
try {
nextChar = i n . r e a d ( ) ;
} c a t c h ( IOException ex ) {
throw new j a v a . u t i l . NoSuchElementException ( ) ;
}
}
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n nextChar >= 0 ;
}
35
p u b l i c C h a r a c t e r next ( ) {
c h a r r e s u l t = ( c h a r ) nextChar ;
try {
nextChar = i n . r e a d ( ) ;
} c a t c h ( IOException ex ) {
throw new j a v a . u t i l . NoSuchElementException ( ) ;
}
return r e s u l t ;
}
36
37
38
39
40
41
42
43
44
45
}
46
23
Kapitel 1 Iterierbare Objekte
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n {
f o r ( c h a r c : new R e a d e r I t e r a b l e ( new F i l e R e a d e r ( a r g s [ 0 ] ) ) ) {
System . out . p r i n t l n ( c ) ;
}
}
47
48
49
50
51
52
}
Listing 1.46: ReaderIterable.java
Aufgabe 1 In dieser Aufgabe geht es darum unterschiedliche Iteratorklassen zu
implementieren:
a) Lösen Sie die Aufgabe 1 aus Übungsblatt 11. Vergleichen Sie mit der
Musterlösung.
b) Schreiben Sie eine Klasse ArrayIterator, die im Konstruktor einen Array erhält und bei der Iteratoin über die einzelnen Arrayelemente iteriert.
c) Schreiben Sie eine Klasse Words. Sie soll Iterable<String> so implementieren, dass nacheinander die Wörter eines im Konstruktor übergebenen Strings bei der Iteration durchlaufen werden. Wörter werden durch
Leerzeichen, Zeilenenden und Tabulatorzeichen getrennt. Es soll keine
leeren Wörter geben.
d) Schreiben Sie eine generische Klasse IndexIterator<A>. Im Konstruktor
soll ein Obejekt des Typs java.util.function.Function<Integer,A>
übergeben werden. Beim i-ten Aufruf der Methode next() soll der Iterator diese Funktion benutzen, um das nächste Element für den Index i
zu erzeugen.
e) Benutzen Sie die Klasse aus Teil d) um einen Iterator zu implementieren,
der über die Quadratzahlen iteriert.
f) Schreiben Sie eine generische Klasse GenerationIterator<A>.
Im
Konstruktor
soll
ein
Obejekt
f
des
Typs
java.util.function.Function<A,A> und ein Element a des Typs A
übergeben werden. Beim Aufruf der Methode next() soll der Iterator
die folgende Folge generieren: a, f(a), f(f(a)), f(f(f(a))),....
g) Benutzen Sie die Klasse aus Teil f) um einen Iterator zu implementieren,
der über alle ungeraden Zahlen iteriert.
h) Ergänzen Sie die Klassen IndexIterator und GeneratorIterator um
ein Feld des Typs predicate<A>. Es soll einen überladenen Konstruktor geben, der auch dieses Feld initialisiert. Mit dem Typ des Typs
Predicate<A> soll in der Methode next getestet werden, ob der Iterator noch ein weiteres Element generiert.
Aufgabe 2 In dieser Aufgabe sollen Sie aus einem oder mehreren übergebenen
iterierbaren Objekten neue iterierbare Objekte erzeugen, so wie es die Klasse
Append in diesem Kapitel bereits macht.
24
1.4 Die Schnittstelle Iterable
a) Schreiben Sie eine generische Klasse Cons<E>, die Iterable<E>
implementiert.
Es
soll
folgenden
Konstruktor
geben:
Cons(E head, Iterable<E> tail). Der Iterator zu dieser Klasse
soll beim ersten Aufruf von next() das Element head zurück geben und
anschließend die Elemente des Iterators von tail.
b) Wenn es zwei iterierbare Objekte gibt, dann benötigt man manchmal,
gleichzeitig zusammen durch die beiden Objekte zu iterieren. Das Ergebnis ist dann ein iterierbares Objekt, das über Paare von Objekten iteriert.
Das erste Paar besteht aus den beiden ersten Objekten der Iterationen,
das zweite Paar aus den beiden zweiten usw. Schließlich endet die Iteration, wenn eines der beiden iterierbaren Objekte kein nächstes Element
hat.
Schreiben Sie zunächst eine generische Klasse Pair, die es ermöglicht
Paare von Objekten zu repräsentieren.
Schreiben Sie eine generische Klasse PairIterable<A,B>, die
Iterable<Pair<A,B>> implementiert . Sie soll in einem Konstruktor zwei iterierbare Objekte übergeben bekommen:
public PairIterable(Iterable<A> it1, Iteratable<B> it2)
Beim Iterieren soll dann paarweise durch beide Iterable gelaufen werden.
Lösung
1
2
3
4
5
6
7
8
package name . p a n i t z . u t i l ;
p u b l i c c l a s s Pair<A, B> {
public A f s t ;
p u b l i c B snd ;
p u b l i c P a i r (A a , B b ) {
fst = a;
snd = b ;
}
9
@Override p u b l i c S t r i n g t o S t r i n g ( ) {
r e t u r n ” ( ” + f s t + ” , ” + snd + ” ) ” ;
}
10
11
12
13
}
Listing 1.47: Pair.java
1
2
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
3
4
5
6
p u b l i c c l a s s P a i r I t e r a b l e <A, B> implements I t e r a b l e <Pair<A, B
>> {
f i n a l p r i v a t e j a v a . l a n g . I t e r a b l e <A> a s ;
f i n a l p r i v a t e j a v a . l a n g . I t e r a b l e <B> bs ;
25
Kapitel 1 Iterierbare Objekte
p u b l i c P a i r I t e r a b l e ( j a v a . l a n g . I t e r a b l e <A> as , j a v a . l a n g .
I t e r a b l e <B> bs ) {
t h i s . as = as ;
t h i s . bs = bs ;
}
7
8
9
10
11
p u b l i c I t e r a t o r <Pair<A, B>> i t e r a t o r ( ) {
r e t u r n new I t e r ( ) ;
}
12
13
14
15
p r i v a t e c l a s s I t e r implements I t e r a t o r <Pair<A, B>> {
p r i v a t e I t e r a t o r <A> a I t ;
p r i v a t e I t e r a t o r <B> b I t ;
Iter () {
a I t = as . i t e r a t o r ( ) ;
b I t = bs . i t e r a t o r ( ) ;
}
16
17
18
19
20
21
22
23
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n b I t . hasNext ( ) && a I t . hasNext ( ) ;
}
24
25
26
27
p u b l i c Pair<A, B> next ( ) {
r e t u r n new Pair <>( a I t . next ( ) , b I t . next ( ) ) ;
}
28
29
30
}
31
32
}
Listing 1.48: PairIterable.java
c) Oft braucht man die Situation, dass man über zwei Dimensionen iteriert.
Hierzu hat man für jede Dimension einen eigenen Iterator. Man kann sich
das so vorstellen, dass, wenn man an ein übliches Koordinatensystem
denkt, die eine Dimension die Punkte auf der x-Achse ist, die zweite
Dimension die Punkte auf der y-Achse.
Soll über den zweidimensionalen Raum iteriert werden, so wird ein Iterator benötigt, der über alle möglichen (x,y) Paare iteriert.
Schreiben Sie eine generische Iteratorklasse TwoDimIterable<A,B>. Sie
soll in einem Konstruktor zwei iterierbare Objekte übergeben bekommen:
public TwoDimIterable(Iterable<A> it1, Iterable<B> it2)
Lösung Wir geben zwei Lösungen für die Aufgabe. Die erste ist die
einfachere, wird aber dem Namen der Klasse nicht wirklich gerecht. Sie
funktioniert wie zwei ineinander verschachtelte Schleifen. Die äußere läuft
über den einen Iterator, die innere über den anderen. Der Nachteil ist:
wenn der innere Iterator nie terminiert, sprich unendlich viele Elemente
26
1.4 Die Schnittstelle Iterable
liefert, dann ist nicht mehr gewährleistet, dass jedes Element der Paarmenge irgendwann im Ergebnisiterator erhalten wird.
1
2
3
package name . p a n i t z . u t i l ;
import name . p a n i t z . pmt . i t e r a t i o n . I n t e g e r R a n g e ;
import j a v a . u t i l . I t e r a t o r ;
4
5
6
7
p u b l i c c l a s s D i a g o n a l I t e r a b l e <A, B> implements I t e r a b l e <Pair<
A, B>>{
j a v a . l a n g . I t e r a b l e <A> a s ;
j a v a . l a n g . I t e r a b l e <B> bs ;
8
9
10
11
12
13
14
15
16
17
18
19
p r i v a t e c l a s s I t e r implements I t e r a t o r <Pair<A, B>>{
I t e r a t o r <A> a I t ;
I t e r a t o r <B> b I t ;
A a = null ;
Iter () {
a I t = as . i t e r a t o r ( ) ;
b I t = bs . i t e r a t o r ( ) ;
}
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n b I t . hasNext ( ) | | a I t . hasNext ( ) ;
}
20
p u b l i c Pair<A, B> next ( ) {
i f ( a == n u l l ) a = a I t . next ( ) ;
i f ( b I t . hasNext ( ) )
r e t u r n new Pair <>(a , b I t . next ( ) ) ;
a = a I t . next ( ) ;
b I t = bs . i t e r a t o r ( ) ;
21
22
23
24
25
26
27
r e t u r n next ( ) ;
28
}
29
30
}
31
32
33
34
p u b l i c I t e r a t o r <Pair<A, B>> i t e r a t o r ( ) {
r e t u r n new I t e r ( ) ;
}
35
36
37
38
39
p u b l i c D i a g o n a l I t e r a b l e ( j a v a . l a n g . I t e r a b l e <A> as , j a v a . l a n g .
I t e r a b l e <B> bs ) {
t h i s . as = as ;
t h i s . bs = bs ;
}
40
41
42
43
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
I t e r a b l e <I n t e g e r > a s = new IntRange ( 2 , 2 0 , 2 ) ;
I t e r a b l e <I n t e g e r > bs = new IntRange ( 5 1 , 9 9 , 4 ) ;
44
45
f i n a l I t e r a b l e <Pair<I n t e g e r , I n t e g e r >> ps = new
D i a g o n a l I t e r a b l e <I n t e g e r , I n t e g e r >(as , bs ) ;
46
27
Kapitel 1 Iterierbare Objekte
f o r ( Pair<I n t e g e r , I n t e g e r > p : ps ) {
System . out . p r i n t l n ( p ) ;
}
47
48
49
}
50
51
}
Listing 1.49: DiagonalIterable.java
Die zweite Umsetzung führt tatsächlich eine Diagonalisierung durch. Hierzu werden zwei interne Hilfslisten verwendet. Diese beinhalten immer die bereits von den beiden Iteratoren gesehenen Elemente. Allerdings sind beide in entgegengesetzter Reihenfolge. Nun kann mit dem
PairIterable aus der letzten Aufgabe jeweils der Iterator für eine Diagonale in diesem Zweidimensionalen Raum erzeugt werden.
Etwas kompliziert wird die Lösung, weil die beiden Listen, sobald die
Iteratoren keine neuen Elemente mehr liefern auf korrekte Weise wieder
stückchenweise abgebaut werden müssen.
1
2
3
4
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
import j a v a . u t i l . L i n k e d L i s t ;
import j a v a . u t i l . L i s t ;
5
6
7
8
p u b l i c c l a s s Diagonal<A, B> implements I t e r a b l e <Pair<A, B>> {
f i n a l p r i v a t e j a v a . l a n g . I t e r a b l e <A> a s ;
f i n a l p r i v a t e j a v a . l a n g . I t e r a b l e <B> bs ;
9
10
11
12
13
p u b l i c D i a g o n a l ( j a v a . l a n g . I t e r a b l e <A> as , j a v a . l a n g .
I t e r a b l e <B> bs ) {
t h i s . as = as ;
t h i s . bs = bs ;
}
14
15
16
17
p u b l i c I t e r a t o r <Pair<A, B>> i t e r a t o r ( ) {
r e t u r n new I t e r ( ) ;
}
18
19
20
21
p r i v a t e c l a s s I t e r implements I t e r a t o r <Pair<A, B>> {
p r i v a t e I t e r a t o r <A> a I t = a s . i t e r a t o r ( ) ;
p r i v a t e I t e r a t o r <B> b I t = bs . i t e r a t o r ( ) ;
22
23
24
25
26
27
// d i e f o l g e n d e b e i d e n L i s t e n s p e i c h e r n d i e b e r e i t s
ges eh en d e n i t e r i e r t e n
// Element
// S i e s o l l e n immer zusammen e i n e D i a g o n a l e d a r s t e l l e n
p r i v a t e L i s t <A> xs = new L i n k e d L i s t <>() ;
p r i v a t e L i s t <B> ys = new L i n k e d L i s t <>() ;
28
29
30
28
// d e r Paar−I t e r a t o r g e h t immer u e b e r e i n e D i a g o n a l e
p r i v a t e I t e r a t o r <Pair<A, B>> ps = new P a i r I t e r a b l e <>(xs ,
ys ) . i t e r a t o r ( ) ;
1.4 Die Schnittstelle Iterable
31
@Override
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n ps . hasNext ( ) | | ( a I t . hasNext ( ) && b I t . hasNext ( ) )
| | ( xs . s i z e ( ) >= 1 && b I t . hasNext ( ) )
| | ( ys . s i z e ( ) >= 1 && a I t . hasNext ( ) )
| | ( xs . s i z e ( ) > 1 && xs . s i z e ( ) > 1 ) ;
}
32
33
34
35
36
37
38
39
@Override
p u b l i c Pair<A, B> next ( ) {
i f ( ! ps . hasNext ( ) ) {
b o o l e a n aitHadNext = a I t . hasNext ( ) ;
40
41
42
43
44
// das n a e c h s t e x wird h i n t e n angehaengt
i f ( a I t . hasNext ( ) ) {
xs . add ( a I t . next ( ) ) ;
}
// wenn k e i n ne u e s y mehr kommt , wird x l i s t e vorne
um e i n
// Element g e k u e r z t
i f ( ! b I t . hasNext ( ) ) {
xs = xs . s u b L i s t ( 1 , xs . s i z e ( ) ) ;
}
45
46
47
48
49
50
51
52
53
54
// das n a e c h s t e y wird vorne angehaengt
i f ( b I t . hasNext ( ) ) {
ys . add ( 0 , b I t . next ( ) ) ;
}
// wenn k e i n ne u e s x mehr kam , wird y l i s t e h i n t e n um
55
56
57
58
59
ein
// Element g e k u e r z t
60
61
i f ( ! aitHadNext ) {
ys = ys . s u b L i s t ( 0 , ys . s i z e ( ) − 1 ) ;
}
62
63
64
65
// D i a g o n a l e wurde a u f g e b a u t und kann j e t z t i t e r i e r t
werden
ps = new P a i r I t e r a b l e <>(xs , ys ) . i t e r a t o r ( ) ;
}
r e t u r n ps . next ( ) ;
}
66
67
68
69
70
71
}
72
73
74
75
76
77
78
79
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
L i s t <S t r i n g > xs = new L i n k e d L i s t <>() ;
xs . add ( ” x1 ” ) ;
xs . add ( ” x2 ” ) ;
xs . add ( ” x3 ” ) ;
xs . add ( ” x4 ” ) ;
L i s t <S t r i n g > ys = new L i n k e d L i s t <>() ;
29
Kapitel 1 Iterierbare Objekte
ys . add ( ” y1 ” ) ;
ys . add ( ” y2 ” ) ;
ys . add ( ” y3 ” ) ;
ys . add ( ” y4 ” ) ;
ys . add ( ” y5 ” ) ;
new Diagonal <>(xs , ys ) . f o r E a c h ( p −>System . out . p r i n t l n ( p ) )
;
80
81
82
83
84
85
}
86
87
}
Listing 1.50: Diagonal.java
d) Schreiben Sie eine generische Iterableklasse Interleave<A>. Sie soll in
einem Konstruktor zwei iterierbare Objekte übergeben bekommen. Die
resultierende Iteration, soll abwechselnd aus den beiden Iterable, die Elemente beziehen, bis einer der beiden Iteratoren kein weiteres Element
liefert.
1.5 Funktionen als Parameter
Mit der Version 1.8 von Java sollte die Schnittstelle Iterable zunächst eine
gewaltige Aufwertung erhalten. Dieses führte schließlich dazu, dass für die neuen
Funktionalitäten, die in die Schnittstelle eingebaut wurden, eine komplett neue
Schnittstelle entwickelt wurde, die sich nun in dem Paket java.util.stream befindet. Es ist die Schnittstelle Stream, die in Zukunft eine wesentliche Erweiterung des
Konzeptes für Iteratoren ausdrücken wird. Da diese zum jetzigen Zeitpunkt noch
nicht endgültig definiert ist, werden wir sie in diesem Semester noch nicht betrachten.
Dafür betrachten wir mögliche Erweiterungen der Schnittstelle Iterable. Bisher
hatte die Schnittstelle Iterable nur die einzige Methode iterator(), mit der ein
Iterator-Objekt erzeugt wurde, das zum einmaligen Iterieren durch die Elemente
eines Objekts benutzt werden konnte.
Zwei neue Konzepte bringt Java 1.8 mit, die es ermöglichen, die Schnittstelle
Iterable um wesentliche Funktionalität zu erweitern und ihre eine noch zentralere
Rolle zukommen zu lassen.
• Mit Java 1.8 können Schnittstellen, wie bisher nur von abstrakten Klassen
her bekannt, auch Methoden bereits konkret implementieren, wobei sie die
abstrakten Methoden bereits benutzen können.
• Mit Java 1.8 werden sogenannte Lambda-Ausdrücke, die auch als Closures
bezeichnet werden, sowie auch Methodenobjekte der Sprache Java hinzugefügt.
Mit diesen neuen Sprachkonzepten wird eine bisher in Java nur schwer realisierbare
Programmiermethodik möglich. Die sogenannte Programmierung höherer Ordnung.
30
1.5 Funktionen als Parameter
Dabei werden Methoden oder auch Codeblöcke als Parameter anderen Methoden
übergeben. Dieses ermöglicht eine wesentlich stärkere Abstraktion in der Parameterisierung des Codes. Es kann jetzt über eine ganze Befehlsfolge auf einfache Weise
abstrahiert werden.
Die Schnittstelle Iterable hat weiterhin nur einen abstrakte Methode, die Methode
iterator(), die wir bereits kennen. Zusätzlich sind eine ganze Menge weiterer
Methoden implementiert. Bei diesen handelt es sich zum großen Teil um Methoden
höherer Ordnung.
Um die Beispiele dieses Kapitels zu übersetzen, wird ein Compiler benötigt, der
bereits die entsprechenden Sprachkonstrukte für Java 1.8 unterstützt. Er kann auf
der Webseite des Project Lambda (http://openjdk.java.net/projects/lambda)
herunter geladen werden.
1.5.1 Schleifen als Methoden
Die erste neue Methode höherer Ordnung der Schnittstelle Iterable, die wir betrachten, ist die Methode forEach. Sie hat ein Argument. Dieses ist ein Parameter
vom neuen Typ Block. Es handelt sich dabei um einen belieben Code-Block, der abhängig ist von einer Variablen. Hierfür gibt es eine neue Syntax. Es wird die Variable
(eine in diesem Kontext neu deklarierte Variable) in runde Klammern geschrieben.
Anschließend folgt ein aus einem Minuszeichen und dem Größerzeichen gebildeter
Pfeil. Danach kommt in geschweifte Klammern ein beliebiger Code-Block.
Damit ist es möglich, die for-Schleife komplett als Methodenaufruf darzustellen:
1
2
3
4
package name . p a n i t z . pmt . i t e r a t i o n ;
p u b l i c c l a s s ForEachExample {
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
I n t e g e r R a n g e i s = new I n t e g e r R a n g e ( 0 , 1 0 , 1 ) ;
5
i s . f o r E a c h ( ( x ) −> { System . out . p r i n t l n ( x ) ; } ) ;
6
}
7
8
}
Listing 1.51: ForEachExample.java
Die Zeile is.forEach( (x) -> {System.out.println(x);}) ist wie folgt zu lesen:
Für jedes Element x im iterierbaren Objekt is führe den Code-Block aus, der aus
der einzigen Anweisung println besteht.
1.5.2 Standard- (default) Methoden in Schnittstellen
Mit Java 1.8 ist es jetzt möglich, ähnlich wie in abstrakten Klassen bestimmte Methoden bereits auszuimplementieren und dabei bereits auf die noch abstrakten Methoden zuzugreifen. Ein Beispiel hierfür haben wir bereits gesehen. In der Schnittstelle
31
Kapitel 1 Iterierbare Objekte
Iterable gibt es nun die Methode forEach. Diese ist bereits in der Schnittstelle
konkret implementiert. Beim Implementieren der Schnittstelle brauchten wir diese
nicht selbst zu implementieren. Wir haben sie quasi geschenkt bekommen.
Mit Java 1.8 wird auch in der Schnittstelle Iterator eine Standardmethode eingeführt. Die bisher abstrakte aber in der Dokumentation als optional beschriebene
Methode remove hat von nun an eine Standardimplementierung und braucht beim
Implementieren der Schnittstelle nicht mehr selbst implementiert werden.
Wie man sieht, könnte man mit dieser Methode fast auf die Schleifen in Java
verzichten. Tatsächlich gibt Es Programmiersprachen, die diesen Weg gegangen sind
und keine speziellen Schleifenkonstrukte kennen.
1.5.3 Iterable als verzögerte Listen
Man kann mit den default-Methoden jetzt soweit gehen, dass man gleich die
Schnittstelle Iterable soweit aufmotzt, dass sie bereits eine einfach verkettete Liste
darstellt. Hierzu braucht sie im Prinzip fünf Methoden:
• eine Testmethode empty(), die testet, ob die Liste leer ist.
• eine Methode head(), die das erste Element zurück gibt.
• eine Methode tail(), die die Liste ohne das erste Element zurück gibt.
• eine Methode nil() zum Erzeugen einer leeren Liste.
• eine Methode cons(x,xs), die eine neue Liste erzeugt, bestehend aus einem
neuem ersten Element und einer Restliste.
Wir können tatsächlich eine eigene Schnittstelle Iterable schreiben, die diese fünf
Methoden hat. Hierzu schreiben wir eine Unterschnittstelle von Iterable mit den
entsprechenden dafault-Methoden:
1
2
3
4
5
package name . p a n i t z . u t i l ;
import j a v a . u t i l . I t e r a t o r ;
import j a v a . u t i l . f u n c t i o n . * ;
p u b l i c i n t e r f a c e I t e r a b l e <A> e x t e n d s j a v a . l a n g . I t e r a b l e <A>{
p u b l i c I t e r a t o r <A> i t e r a t o r ( ) ;
Listing 1.52: Iterable.java
Die Testmethode, ob es sich um eine leere Liste handelt, lässt sich einfach implementieren. Man lässt sich einen Iterator geben und fragt diesen, ob er ein nächstes
Element hat.
p u b l i c d e f a u l t b o o l e a n empty ( ) {
r e t u r n ! i t e r a t o r ( ) . hasNext ( ) ;
}
6
7
8
Listing 1.53: Iterable.java
32
1.5 Funktionen als Parameter
Die Methode head ist einfach zu implementieren. Es muss das Iterable-Objekt
nach seinem Iterator gefragt werden und von diesem das erste Element zurück
gegeben werden.
9
10
11
p u b l i c d e f a u l t A head ( ) {
r e t u r n i t e r a t o r ( ) . next ( ) ;
}
Listing 1.54: Iterable.java
Auch die Methode tail ist einfach zu implementieren. Es wird zunächst der Iterator
erfragt. Dieser wird durch einmaliges Aufrufen von next() auf das zweite Element
geschaltet. Nun ist es der Iterator für das neue Iterable.
12
13
14
15
16
17
18
p u b l i c d e f a u l t I t e r a b l e <A> t a i l ( ) {
r e t u r n ( )−>{
I t e r a t o r <A> r e s u l t = i t e r a t o r ( ) ;
r e s u l t . next ( ) ;
return r e s u l t ;
};
}
Listing 1.55: Iterable.java
Aufmerksamen Beobachtern mag aufgefallen sein, dass sich mit den Methoden empty
und tail bereits viele typische Methoden für Listenstrukturen umsetzen lassen. So
zum Beispiel die Berechnung der Länge:
19
20
21
public default int length () {
r e t u r n empty ( ) ? 0 : 1+ t a i l ( ) . l e n g t h ( ) ;
}
Listing 1.56: Iterable.java
cons und nil für Iterables
Wir brauchen die Methode nil() die uns eine Iterable-Objekt erzeugt, dessen
Iterator über kein einziges Element iteriert. Es ist also ein leeres Iterable-Objekt.
Deshalb gibt die Methode hasNext immer false zurück.:
22
23
24
25
26
27
p u b l i c s t a t i c <A> I t e r a b l e <A> n i l ( ) {
r e t u r n ( )−>{
r e t u r n new I t e r a t o r <A>() {
p u b l i c b o o l e a n hasNext ( ) { r e t u r n f a l s e ; }
p u b l i c A next ( ) {
throw new j a v a . u t i l . NoSuchElementException ( ” next f o r empty
iterator ”) ;
33
Kapitel 1 Iterierbare Objekte
}
28
};
29
};
30
}
31
Listing 1.57: Iterable.java
Als Objektmethode schreiben wir als nächstes die Methode cons, diese erhält ein
Element, welches bei der resultierenden Iteration als erstes Element zurück gegeben
werden soll. Es wird also an den Iterationsbereich vorne noch ein Element angehängt.
p u b l i c d e f a u l t I t e r a b l e <A> c o n s ( f i n a l A hd ) {
f i n a l I t e r a b l e <A> t l = t h i s ;
r e t u r n ( ) −> {
f i n a l j a v a . u t i l . I t e r a t o r <A> i t = t l . i t e r a t o r ( ) ;
f i n a l A f i r s t = hd ;
r e t u r n new I t e r a t o r <A>() {
boolean f i r s t C a l l = true ;
p u b l i c b o o l e a n hasNext ( ) { r e t u r n f i r s t C a l l | | i t . hasNext ( ) ; }
p u b l i c A next ( ) {
i f ( firstCall ){
firstCall = false ;
return f i r s t ;
} else {
r e t u r n i t . next ( ) ;
}
}
};
};
}
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Listing 1.58: Iterable.java
Als eine statische Methode wird noch eine Variante dieser Methode implementiert.
Diese Variante erhält das erste Element für die Iteration, sowie ein iterierbares
Objekt für die übrigen Elemente. Dieses zweite iterierbare Objekt wird allerdings
nicht direkt übergeben, sondern über ein Supplier-Objekt. Dieses hat genau eine
Methode namens get, mit der das Objekt erfragt werden kann, das der Supplier
liefert.
p u b l i c s t a t i c <A> I t e r a b l e <A> c o n s (A hd , f i n a l j a v a . u t i l . f u n c t i o n .
S u p p l i e r <I t e r a b l e <A>> t l ) {
r e t u r n ( )−>{
f i n a l A f i r s t = hd ;
r e t u r n new I t e r a t o r <A>() {
j a v a . u t i l . I t e r a t o r <A> i t = n u l l ;
boolean f i r s t C a l l = true ;
51
52
53
54
55
56
57
p u b l i c b o o l e a n hasNext ( ) {
58
34
1.5 Funktionen als Parameter
i f ( f i r s t C a l l ) return true ;
i f ( i t==n u l l ) i t=t l . g e t ( ) . i t e r a t o r ( ) ;
r e t u r n i t . hasNext ( ) ;
59
60
61
}
62
63
p u b l i c A next ( ) {
i f ( firstCall ){
firstCall = false ;
return f i r s t ;
}else {
i f ( i t==n u l l ) i t=t l . g e t ( ) . i t e r a t o r ( ) ;
r e t u r n i t . next ( ) ;
}
}
64
65
66
67
68
69
70
71
72
};
73
};
74
75
}
Listing 1.59: Iterable.java
Filter für die Iteration
Mit den oben geschriebenen Methoden head, tail und cons lassen sich jetzt auf
einfache Weise weitere sinnvolle Methoden für iterierbare Objekte schreiben. Zum
Beispiel ein einfacher Filter. Es wird hierzu ein Prädikat übergeben, dass für ein
einzelnen Element zu wahr oder falsch auswertet. Es soll ein neues iterierbares
Objekt erzeugt werden, das nur noch als den Objekten des ursprünglichen existiert,
für die das Prädikat zu wahr auswertet.
Es gibt drei Fälle:
• für ein leeres iterierbares Objekt ist auch das Ergebnis ein solche leeres Objekt.
• dann ist zu testen, ob das erste Element das Prädikat erfüllt. Wenn ja, dann
wird dieses als erstes Element des Ergebnisses mit cons auf den rekursiven
Aufruf auf den Reste zugefügt.
• ansonsten wird die Methode nur rekursiv auf die übrigen Elemente des Iterationsbereiches angewendet.
76
77
78
79
80
p u b l i c d e f a u l t I t e r a b l e <A> f i l t e r ( P r e d i c a t e <A> p ) {
i f ( empty ( ) ) r e t u r n t h i s ;
i f ( p . t e s t ( head ( ) ) ) r e t u r n c o n s ( head ( ) , ( )−> t a i l ( ) . f i l t e r ( p ) ) ;
return t a i l () . f i l t e r (p) ;
}
Listing 1.60: Iterable.java
35
Kapitel 1 Iterierbare Objekte
Elemente Abbilden
Eine einfache und vielfach in Sprachen, die Lambda-Ausdrücke unterstützen, verwendete Funktion, generiert aus einem Iterationsbereich mit Elementen eines bestimmten Typs einen neuen Iterationsbereich ,der entsteht, indem eine Funktion auf
jedes Element angewendet wird. Es ist also wieder eine Form, der Anwendung für
alle Elemente. Hier wird für jedes Element in ein neues Element mit Hilfe einer
übergebenen Funktion generiert.
p u b l i c d e f a u l t <B> I t e r a b l e <B> map( Function<A, B> f ) {
i f ( empty ( ) ) r e t u r n n i l ( ) ;
r e t u r n c o n s ( f . apply ( head ( ) ) , ( )−> t a i l ( ) . map( f ) ) ;
}
81
82
83
84
Listing 1.61: Iterable.java
Faltungen
Eine mächtige Funktion für Iterationsbereiche sind sogenannte Faltungen (in vielen
Bibliotheken heißen die entsprechenden Funtionen fold). Bei Faltungen geht es
darum, die Elemente alle mit einer Operation zu verknüpfen. Hierzu wird eine Operation benötigt, die zwei Elemente verknüpfen kann. Zusätzlich wird ein Startwert
erwartet, der insbesondere als Ergebnis benutzt wird, wenn keine Elemente in der
Iteration sind. Für eine Funktion ⊗ als Operation in Infix-Schreibweise sei die
Faltung wie folgt definiert:
reduce([x1 , x2 , . . . , xn ], start, ⊗) = start ⊗ x1 ⊗ x2 ⊗ · · · ⊗ xn
Die Implementierung dieser Funktion lässt sich wunderbar per Iteration machen.
Dazu werden die Elemente in einer Schleife nacheinander mit dem Startwert
verknüpft.
p u b l i c d e f a u l t <B> B r e d u c e (B s t a r t , BiFunction<B, A, B> op ) {
f o r (A x : t h i s ) s t a r t = op . apply ( s t a r t , x ) ;
return start ;
}
85
86
87
88
Listing 1.62: Iterable.java
So kurz und einfach diese Funktion wirkt, umso mächtiger ist sie, was auf den ersten
Blick nicht so ersichtlich ist. Ein paar Beispiele für weitere Funktionen, die sich mit
reduce umsetzen lassen. Im Prinzip ist es die Verallgemeinerung einer Iteration, bei
der nacheinander die Elemente der Iteration auf ein Ergebnis akkumuliert werden.
Dabei ist der Parameter start jeweils die Initialisierung der Ergebnisvariablen.
36
1.5 Funktionen als Parameter
So lässt sich zum Beispiel eine Funktion schreiben, die testet, ob eines der Elemente
im Iterationsbereich ein bestimmtes Prädikat erfüllt. Diese Funktion entspricht dem
logischen Existenzquantor. Der Startwert ist in diesem Fall false. Es wird für jedes
Element getestet, ob es das Prädikat erfüllt und schließlich die Teilergebnisse mit
Oder verknüpft.
89
90
91
p u b l i c d e f a u l t b o o l e a n e x i s t s ( P r e d i c a t e <A> p ) {
r e t u r n r e d u c e ( f a l s e , ( x , y )−> x | | p . t e s t ( y ) ) ;
}
Listing 1.63: Iterable.java
Analog funktioniert die Funktion, die testet, ob ein Prädikat für alle Elemente des
Iterationsbereichs zutrifft. Diese Funktion entspricht dem logischen Allquantor.
92
93
94
p u b l i c d e f a u l t b o o l e a n f o r A l l ( P r e d i c a t e <A> p ) {
r e t u r n r e d u c e ( t r u e , ( x , y )−> x && p . t e s t ( y ) ) ;
}
Listing 1.64: Iterable.java
So lässt sich wiederum mit der Funktion exists schnell testen, ob ein bestimmtes
Element in einem Iterationsbereich enthalten ist.
95
96
97
p u b l i c d e f a u l t b o o l e a n c o n t a i n s (A e l ) {
r e t u r n e x i s t s ( x −> x . e q u a l s ( e l ) ) ;
}
Listing 1.65: Iterable.java
Es lassen sich auch wie bei einer Methode toString die Stringdarstellungen aller
Elemente in einem String zusammenfügen:
98
99
100
public default String stringRepresentation () {
r e t u r n r e d u c e ( ” ” , ( x , y ) −> x+” ”+y ) ;
}
Listing 1.66: Iterable.java
Eine entsprechende Funktion, die ein Objekt der Klasse StringBuffer nutzt, um die
Elemente zu einem Gesamtstring zu akkumulieren, lässt sich auch mit Hilfe von
reduce formulieren:
101
102
103
104
105
p u b l i c d e f a u l t S t r i n g show ( ) {
r e t u r n r e d u c e ( new S t r i n g B u f f e r ( ) ,
( buf , y ) −> {
buf . append ( ” ” ) ;
buf . append ( y ) ;
37
Kapitel 1 Iterierbare Objekte
r e t u r n buf ;
}) . t o S t r i n g ( ) ;
106
107
}
108
Listing 1.67: Iterable.java
Auch die Längenberechnung lässt sich mittels reduce formulieren. Als Operation
wird immer die Konstante 1 hinzuaddiert:
public default int length2 () {
r e t u r n r e d u c e ( 0 , ( r e s u l t , x ) −> r e s u l t +1) ;
}
109
110
111
Listing 1.68: Iterable.java
Es lässt sich sogar die Funktion filter aus diesem Abschnitt nun komplett mit Hilfe
von reduce darstellen:
p u b l i c d e f a u l t I t e r a b l e <A> f i l t e r 2 ( P r e d i c a t e <A> p ) {
r e t u r n ( ) −> ( r e d u c e ( new j a v a . u t i l . A r r a y L i s t <A>() ,
( r e s u l t , y ) −> {
i f ( p . t e s t ( y ) ) r e s u l t . add ( y ) ;
return r e s u l t ;
}) . i t e r a t o r ( ) ) ;
}
112
113
114
115
116
117
118
Listing 1.69: Iterable.java
Gleiches gilt auch für die Funktion map, die sich mit Hilfe von reduce formulieren
lässt.
p u b l i c d e f a u l t <B> I t e r a b l e <B> map2 ( Function<A, B> f ) {
r e t u r n ( ) −> ( r e d u c e ( new j a v a . u t i l . A r r a y L i s t <B>() ,
( r e s u l t , y ) −> {
r e s u l t . add ( f . apply ( y ) ) ;
return r e s u l t ;
}) . i t e r a t o r ( ) ) ;
}
119
120
121
122
123
124
125
Listing 1.70: Iterable.java
Ebenso lässt sich für einen größer Vergleich mittels eines Comparator-Objekts mit
reduce das Maximum der Elemente eines Iterationsbereichs ermitteln.
p u b l i c d e f a u l t A max( j a v a . u t i l . Comparator<A> comp ) {
return reduce ( null ,
( r e s u l t , y ) −> {
i f ( n u l l==r e s u l t ) r e t u r n y ;
i f ( comp . compare ( r e s u l t , y ) > 0 ) r e t u r n r e s u l t ;
126
127
128
129
130
38
1.5 Funktionen als Parameter
return y ;
131
}
132
);
133
}
134
135
}
Listing 1.71: Iterable.java
Sieb des Eratosthenes
Mit der obigen Filter-Funktion lässt sich eine iterierbares Objekt schreiben, dass
über alle Primzahlen iteriert. Hierzu kann das bekannte Verfahren des Siebs des Eratosthenes angewendet werden. Gestartet wird mit der Folge von Zahlen beginnend
von 2.
1
2
3
4
5
6
7
package name . p a n i t z . pmt . u t i l ;
import name . p a n i t z . u t i l . I t e r a b l e ;
p u b l i c c l a s s PrimesExample {
p u b l i c s t a t i c I t e r a b l e <I n t e g e r > s i e v e ( f i n a l I t e r a b l e <I n t e g e r > i t ) {
f i n a l i n t f i r s t = i t . head ( ) ;
r e t u r n I t e r a b l e . c o n s ( f i r s t , ( )−>s i e v e ( i t . f i l t e r ( ( x )−> x%f i r s t !=
0 )));
}
8
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
I t e r a b l e <I n t e g e r > i s = ( )−>new I n t e g e r R a n g e ( 2 ) . i t e r a t o r ( ) ;
I t e r a b l e <I n t e g e r > pr im e s = s i e v e ( i s ) ;
p r i m e s . f o r E a c h ( x−>System . out . p r i n t l n ( x ) ) ;
}
9
10
11
12
13
14
}
Listing 1.72: PrimesExample.java
Aufgabe 1 Schreiben Sie in dieser Aufgabe eine Klasse, die eine auf einem Array
als Speicher benutzende generische Liste realisiert. (Eine solche Klasse wurde
bereits im ersten Semester entwickelt.) Sie soll folgende Eigenschaften haben:
a) Es soll das Interface name.panitz.util.Iterable implementiert werden.
b) Es soll eine Methode add zum Hinzufügen neuer Elemente geben.
c) Es soll zwei Konstruktoren geben. Einen Standardkonstruktor ohne Parameter und einen Konstruktor mit einer variablen Parameteranzahl. Im
zweiten Fall sollen die Parameter die Elemente der Liste werden.
d) Die Standardmethode length soll überschrieben werden. Warum ist
dieses sinnvoller als auf die bereits in name.panitz.util.Iterable implementierte Standardmethode zu vertrauen?
39
Kapitel 1 Iterierbare Objekte
Schreiben Sie umfangreiche Tests für alle Methoden.
Aufgabe 2 Schreiben Sie für die Schnittstelle name.panitz.util.Iterable weitere Standardmethoden:
a) Eine Methode zum Anhängen eines weiteren Iterable-Objekts:
public default Iterable<A> append(java.lang.Iterable<A> that)
Sie dürfen dabei die Klasse Append des ersten Übungsblattes verwenden.
b) Schreiben Sie unter Zuhilfenahme der Klasse PairIterable des letzten
Übungsblattes folgende Methode, die das Iterable-Objekt paarweise
mit einem zweiten Iterable-Objekt verbindet:
public default <B> Iterable<Pair<A,B>> zip(java.lang.Iterable<B> that)
c) Schreiben Sie jetzt unter Zuhilfenahme der Klasse TwoDimIterable
folgende Methode, die alle Paare aus den beiden Iterable-Objekten
bereit stellt:
public default <B> Iterable<Pair<A,B>> cross(java.lang.Iterable<B> that)
d) Schreiben Sie eine Standardmethode für die Stringdarstellung eines
Iterable-Objekts.
public default String asString()
Was passiert, wenn Sie versuchen als Standardmethode die Methode
toString zu überschreiben.
Testen Sie mit Hilfe der Klasse AList aus der ersten Aufgabe alle Standardmethoden der Klasse Iterable.
40
Kapitel 2
Hierarchische Strukturen
Von zentraler Bedeutung in der Softwareentwicklung ist es, Daten auf eine hierarchische Art und Weise zu strukturieren. Solche hierarchisch strukturierten Daten sind als Bäume bekannt. Sie sind in der Softwareentwicklung von so zentraler
Bedeutung, dass sich verschiedene Standards durchgesetzt haben, solche Daten in
textueller Form zu serialisieren. Zum einen in Form von XML-Dokumenten, zum
anderen aber auch z.B. durch das JASON-Format.
In diesem Kapitel werden wir uns mit hierarchischen Daten zunächst in allgemeiner
Form und anschließend in Form von XML-Dokumenten beschäftigen.
2.1 Einfach verkettete Listen
Eine der häufigsten Datenstrukturen in der Programmierung sind Sammlungstypen.
In fast jedem nichttrivialen Programm wird es Punkte geben, an denen eine Sammlung mehrerer Daten gleichen Typs anzulegen sind. Eine der einfachsten Strukturen,
um Sammlungen anzulegen, sind Listen. Da Sammlungstypen oft gebraucht werden,
stellt Java entsprechende Klassen als Standardklassen zur Verfügung. Bevor wir uns
aber diesen bereits vorhandenen Klassen zuwenden, wollen wir in diesem Kapitel
Listen selbst spezifizieren und programmieren.
2.1.1 Formale Definition
Wir werden Listen als abstrakten Datentyp formal spezifizieren. Ein abstrakter Datentyp (ADT) wird spezifiziert über eine endliche Menge von Methoden. Hierzu wird
spezifiziert, auf welche Weise Daten eines ADT konstruiert werden können. Dazu
werden entsprechende Konstruktormethoden spezifiziert. Dann wird eine Menge von
Funktionen definiert, die wieder Teile aus den konstruierten Daten selektieren können. Schließlich werden noch Testmethoden spezifiziert, die angeben, mit welchem
Konstruktor ein Datenobjekt erzeugt wurde. Der Zusammenhang zwischen Konstruktoren und Selektoren sowie zwischen den Konstruktoren und den Testmethoden wird in Form von Gleichungen spezifiziert.
41
Kapitel 2 Hierarchische Strukturen
Der Trick, der angewendet wird, um abstrakte Datentypen wie Listen zu spezifizieren, ist die Rekursion. Das Hinzufügen eines weiteren Elements zu einer Liste
wird dabei als das Konstruieren einer neuen Liste aus der Ursprungsliste und einem
weiteren Element betrachtet. Mit dieser Betrachtungsweise haben Listen eine rekursive Struktur: eine Liste besteht aus dem zuletzt vorne angehängten neuen Element,
dem sogenannten Kopf der Liste, und aus der alten Teilliste, an die dieses Element
angehängt wurde, dem sogenannten Schwanz der Liste. Wie bei jeder rekursiven
Struktur bedarf es eines Anfangs der Definition. Im Falle von Listen wird dieses
durch die Konstruktion einer leeren Liste spezifiziert.1
Listenkonstruktoren
Abstrakte Datentypen wie Listen lassen sich durch ihre Konstruktoren spezifizieren.
Die Konstruktoren geben an, wie Daten des entsprechenden Typs konstruiert werden
können. In dem Fall von Listen bedarf es nach den obigen Überlegungen zweier
Konstruktoren:
• einem Konstruktor für neue Listen, die noch leer sind.
• einem Konstruktor, der aus einem Element und einer bereits bestehenden
Liste eine neue Liste konstruiert, indem an die Ursprungsliste das Element
angehängt wird.
Wir benutzen in der Spezifikation eine mathematische Notation der Typen von
Konstruktoren.2 Dem Namen des Konstruktors folgt dabei mit einem Doppelpunkt
abgetrennt der Typ. Der Ergebnistyp wird von den Parametertypen mit einem Pfeil
getrennt.
Somit lassen sich die Typen der zwei Konstruktoren für Listen wie folgt spezifizieren:
• Empty: () → List
• Cons: (Object,List) →List
Listenselektoren
Die Selektoren können wieder auf die einzelnen Bestandteile der Konstruktion
zurückgreifen. Der Konstruktor Cons hat zwei Parameter. Für Cons-Listen werden zwei Selektoren spezifiziert, die jeweils einen dieser beiden Parameter wieder
aus der Liste selektieren. Die Namen dieser beiden Selektoren sind traditioneller
Weise head und tail.
• head: (List) →Object
1
Man vergleiche es mit der Definition der natürlichen Zahlen: die 0 entspricht der leeren Liste,
der Schritt von n nach n + 1 dem Hinzufügen eines neuen Elements zu einer Liste.
2
Entgegen der Notation in Java, in der der Rückgabetyp kurioser Weise vor den Namen der
Methode geschrieben wird.
42
2.1 Einfach verkettete Listen
• tail: (List) →List
Der funktionale Zusammenhang von Selektoren und Konstruktoren läßt sich durch
folgende Gleichungen spezifizieren:
head(Cons(x, xs)) = x
tail(Cons(x, xs)) = xs
Testmethoden für Listen
Um für Listen Algorithmen umzusetzen, ist es notwendig, unterscheiden zu können,
welche Art der beiden Listen vorliegt: die leere Liste oder eine Cons-Liste. Hierzu
bedarf es noch einer Testmethode, die mit einem bool’schen Wert als Ergebnis
angibt, ob es sich bei der Eingabeliste um die leere Liste handelte oder nicht. Wir
wollen diese Testmethode isEmpty nennen. Sie hat folgenden Typ:
• isEmpty: List→ boolean
Das funktionale Verhalten der Testmethode läßt sich durch folgende zwei Gleichungen spezifizieren:
isEmpty(Empty()) = true
isEmpty(Cons(x, xs)) = f alse
Somit ist alles spezifiziert, was eine Listenstruktur ausmacht. Listen können konstruiert werden, die Bestandteile einer Liste wieder einzeln selektiert und Listen
können nach der Art ihrer Konstruktion unterschieden werden.
2.1.2 Implementierung
1
2
3
4
package name . p a n i t z . u t i l ;
p u b l i c c l a s s LiLi <A> implements I t e r a b l e <A>{
p r i v a t e f i n a l A hd ;
p r i v a t e f i n a l LiLi <A> t l ;
5
6
7
8
9
p u b l i c L i L i (A hd , LiLi <A> t l ) {
t h i s . hd = hd ;
this . tl = tl ;
}
10
11
12
13
14
public LiLi () {
t h i s . hd = n u l l ;
this . tl = null ;
}
15
43
Kapitel 2 Hierarchische Strukturen
p u b l i c b o o l e a n isEmpty ( ) {
r e t u r n t l==n u l l && hd==n u l l ;
}
16
17
18
19
p u b l i c A getHead ( ) {
r e t u r n hd ;
}
p u b l i c LiLi <A> g e t T a i l ( ) {
return t l ;
}
20
21
22
23
24
25
Listing 2.1: LiLi.java
Ein Iterator für Listen
Für die so definierten Listen können wir jetzt sofort das gelernte aus dem letzten
Kapitel anwenden, indem wir einen Iterator für Listen schreiben. Listen sind natürlich die Paradeanwendung für iterierbare Objekte.
p u b l i c j a v a . u t i l . I t e r a t o r <A> i t e r a t o r ( ) {
r e t u r n new I t ( t h i s ) ;
}
26
27
28
29
p r i v a t e c l a s s I t implements j a v a . u t i l . I t e r a t o r <A>{
LiLi <A> t h e L i s t ;
I t ( LiLi <A> l i ) { t h e L i s t = l i ; }
p u b l i c b o o l e a n hasNext ( ) {
r e t u r n ! t h e L i s t . isEmpty ( ) ;
}
p u b l i c A next ( ) {
A r e s u l t = t h e L i s t . getHead ( ) ;
theList = theList . getTail () ;
return r e s u l t ;
}
}
30
31
32
33
34
35
36
37
38
39
40
41
Listing 2.2: LiLi.java
2.1.3 Mathematische Definition
Betrachten wir jetzt wieder die Listen von der mathematischen Seite. Die obige
Javaklasse definiert implizit eine induktive Struktur, indem gesagt wird, dass man
eine leere Liste konstruieren kann und aus einer Liste und einem weiteren Element
eine neue Liste konstruieren kann. Etwas formaler und nicht programmiersprachlich
ließen sich Listen wie folgt definieren:
Definition 2.1.1 Sei eine Menge E der Elemente gegeben. Die Menge der Listen
über E im Zeichen ListE sei dann die kleinste Menge, für die gilt:
44
2.1 Einfach verkettete Listen
• Die leere Liste ist eine Liste: Nil() ∈ ListE .
• Sei e ∈ E und sei es ∈ ListE .
Dann ist auch Cons(e, es) ∈ ListE .
Induktive Strukturen
Daten sollen nach Möglichkeit dynamisch wachsen können, d.h. potentiell beliebig
groß werden können. Um einen Datentyp zu definieren, dessen Elemente potentiell beliebig groß werden können, bedient man sich einer strukturell induktiven
Definition. Mit den einfach verketteten Listen haben wir eine induktive Struktur definiert. Dieses Verfahren, Mengen mit zu definieren, ist aus der Mathematik
bekannt. Dort ist die Menge der natürlichen Zahlen ein prominentes Beispiel einer induktiv definierten Menge. Die Zahl 0 ist Element der Menge der natürlichen
Zahlen. Bei unseren Listen entspricht dieses der leeren Liste. Zusätzlich gilt, wenn
eine Zahl n in der Menge der natürlichen Zahlen ist, dass ist auch eine Zahl n + 1
in der Menge der natürlichen Zahlen enthalten. Entsprechend können wir für jede
Liste xs eine Liste, die ein Element mehr enthält, erzeugen.
Um in der Mathematik eine Eigenschaft für alle Elemente der Menge der natürlichen Zahlen zu beweisen, gibt es ein spezielles Beweisverfahren, dass sich an der
strukturellen Definition orientiert: die vollständige Induktion. Diese besteht aus drei
Schritte:
• Induktions-Anfang: zeige, dass die Eigenschaft für die Zahl 0 gilt.
• Induktions-Vorraussetzung: nimm an, dass die Eigenschaft bereits für alle
Zahlen kleiner gleich einer bestimmten Zahl n korrekt bewiesen ist.
• Induktions-Schluss: zeige, dass die Annahme dann auch für die Zahl n + 1
gilt.
Als Beispiel können wir die Gaußsche Summenformel beweisen, die behauptet:
n
∑
i=
i=0
n ∗ (n + 1)
2
• Induktions-Anfang:
0 = (0 ∗ 1)/2
ist korrekt.
• Induktions-Vorraussetzung:
n
∑
i=0
i=
n ∗ (n + 1)
2
ist beweisen für alle Zahlen kleiner oder gleich eines bestimmten n .
45
Kapitel 2 Hierarchische Strukturen
• Induktions-Schluss:
n+1
∑
i=
i=0
(n + 1) +
n
∑
i
i=0
=
=
=
=
=
n ∗ (n + 1)
(n + 1) +
2
2 ∗ (n + 1) n ∗ (n + 1)
+
2
2
2 ∗ (n + 1) + n ∗ (n + 1)
2
(n + 2) ∗ (n + 1)
2
(n + 1) ∗ (n + 2)
2
Rekursive Algorithmen
Ebenso wie der Mathematiker bei einem Beweis per vollständiger Induktion vorgeht, können wir Algorithmen für die induktiv definierte Struktur unserer Listen
definieren. Auch hier gibt es die drei Schritte:
• Induktionsanfang: das Ergebnis der zu entwickelnden Funktion wird für die
leere Liste definiert.
• Induktionsvorraussetzung. wir nehmen an, dass die Funktion bereits für
kürzere Listen funktioniert. Insbesondere für die um ein Element kürzere Liste,
nämlich für die Liste, die wir mit getTail() erhalten.
• Induktionsschritt: aus dem Ergebnis für die Induktionsvorraussetzung wird
das Ergebis für den Gesamtliste definiert.
Für viele einfache Algorithmen ist dieser Weg recht einfach.
Längenberechnung für Listen Ein einfacher Algorithmus für unsere induktive
Listendefinition stellt die Berechnung der Länge der Liste dar, mit Hilfe der wir die
einzelnen Elemente zählen.
public int length () {
42
Listing 2.3: LiLi.java
Zunächst als der Induktionsanfang. Das Ergebnis für die leere List. Dieses ist einfach.
Eine leere Liste hat die Länge 0.
46
2.1 Einfach verkettete Listen
i f ( isEmpty ( ) ) r e t u r n 0 ;
43
Listing 2.4: LiLi.java
Anschließend können wir über die Induktionsvoraussetzung annehmen, dass die
Funktion für die um ein Element kleinere Liste bereits definiert ist und das korrekte Ergebnis berechnet:
f i n a l int tailResult = getTail () . length () ;
44
Listing 2.5: LiLi.java
Schließlich ist zu überlegen, wie von dem Ergebnis der Voraussetzung das
Gesamtergebnis zu definieren ist. Im Falle der Länge ist dieses besonders einfach,
es muss 1 hinzugezählt werden:
final int result = 1 + tailResult ;
45
Listing 2.6: LiLi.java
Damit lässt sich das Ergebnis zurück geben:
return r e s u l t ;
46
47
}
Listing 2.7: LiLi.java
Test auf ein Element Nach dem gleichen Schema lässt sich auch die Funktion
schreiben, die testet, ob in der Liste ein bestimmtes Element enthalten ist:
48
p u b l i c b o o l e a n c o n t a i n s (A a ) {
Listing 2.8: LiLi.java
Zunächst als der Induktionsanfang. Das Ergebnis für die leere List.
49
i f ( isEmpty ( ) ) r e t u r n f a l s e ;
Listing 2.9: LiLi.java
Anschließend können wir über die wieder Induktionsvoraussetzung annehmen, dass
die Funktion für die um ein Element kleinere Liste bereits definiert ist und das
korrekte Ergebnis berechnet:
47
Kapitel 2 Hierarchische Strukturen
f i n a l boolean t a i l R e s u l t = getTail ( ) . contains ( a ) ;
50
Listing 2.10: LiLi.java
Schließlich ist zu überlegen, wie von dem Ergebnis der Voraussetzung das
Gesamtergebnis zu definieren ist. In diesen Falle betrachten wir erst das erste Element und benutzen dann das Ergebnis der Induktionsvoraussetzung.
f i n a l boolean r e s u l t =
getHead ( ) . e q u a l s ( a ) | | t a i l R e s u l t ;
51
52
Listing 2.11: LiLi.java
Damit haben wir das Ergebnis:
return r e s u l t ;
53
}
54
Listing 2.12: LiLi.java
Inhaltlich liegt eine komplett andere Aufgabenstellung als bei der Längenberechnung vor. Strukturell benutzen wir exakt dasselbe Vorgehen, das der vollständigen
Induktion der Mathematik entspricht.
Aneinanderhängen von Listen Nach dem gleichen Schema lässt sich auch die
Funktion schreiben, die eine neue Liste erzeugt durch das Anhängen einer zweiten
Liste.:
p u b l i c LiLi <A> append ( LiLi <A> t h a t ) {
55
Listing 2.13: LiLi.java
Zunächst als der Induktionsanfang. Das Ergebnis für die leere List.
i f ( isEmpty ( ) ) r e t u r n t h a t ;
56
Listing 2.14: LiLi.java
Anschließend können wir über die wieder Induktionsvoraussetzung annehmen, dass
die Funktion für die um ein Element kleinere Liste bereits definiert ist und das
korrekte Ergebnis berechnet:
f i n a l LiLi <A> t a i l R e s u l t = g e t T a i l ( ) . append ( t h a t ) ;
57
Listing 2.15: LiLi.java
48
2.1 Einfach verkettete Listen
Schließlich ist zu überlegen, wie von dem Ergebnis der Voraussetzung das
Gesamtergebnis zu definieren ist. In diesem Fall ist noch das erste Element an die
Ergebnisliste vorne anzuhängen.:
f i n a l LiLi <A> r e s u l t
58
= new LiLi <A>(getHead ( ) , t a i l R e s u l t ) ;
Listing 2.16: LiLi.java
Damit haben wir das Ergebnis:
return r e s u l t ;
59
}
60
Listing 2.17: LiLi.java
Inhaltlich liegt wieder eine komplett andere Aufgabenstellung als bei der Längenberechnung oder dem Test auf ein Element vor. Strukturell benutzen wir aber
wieder exakt dasselbe Vorgehen, das der vollständigen Induktion der Mathematik
entspricht.
61
}
Listing 2.18: LiLi.java
Aufgabe 1 Gegeben sei folgende Schnittstelle:
1
2
3
4
5
6
package name . p a n i t z . u t i l ;
import j a v a . u t i l . f u n c t i o n . Consumer ;
p u b l i c i n t e r f a c e OurList<A> e x t e n d s I t e r a b l e <A> {
b o o l e a n isEmpty ( ) ;
A head ( ) ;
OurList<A> t a i l ( ) ;
7
@Override i n t l e n g t h ( ) ;
b o o l e a n c o n t a i n s (A e l ) ;
v o i d machtAlle ( Consumer<A> c ) ;
OurList<A> add (A e l ) ;
8
9
10
11
12
OurList<A> drop ( i n t i ) ;
OurList<A> t a k e ( i n t i ) ;
OurList<A> s u b l i s t ( i n t s t a r t , i n t l e n g t h ) ;
i n t indexOf (A e l ) ;
A get ( i n t index ) ;
13
14
15
16
17
18
19
}
Listing 2.19: OurList.java
49
Kapitel 2 Hierarchische Strukturen
Sie sollen in dieser Aufgabe folgende noch nicht vollständige Klasse vervollständigen:
1
2
3
package name . p a n i t z . u t i l ;
import j a v a . u t i l . f u n c t i o n . Consumer ;
import j a v a . u t i l . I t e r a t o r ;
4
5
6
7
c l a s s L i s t e <A> implements OurList<A>{
p r i v a t e A head ;
p r i v a t e OurList<A> t a i l ;
8
p u b l i c L i s t e (A head , OurList<A> t a i l ) {
t h i s . head = head ;
this . tail = tail ;
}
9
10
11
12
13
public Liste () { this ( null , null ) ;}
14
15
p u b l i c L i s t e (A . . . xs ) {
f o r (A x : xs ) {
add ( x ) ;
}
}
16
17
18
19
20
21
@Override p u b l i c b o o l e a n isEmpty ( ) {
r e t u r n head == n u l l && t a i l == n u l l ;
}
22
23
24
25
@Override p u b l i c A head ( ) { r e t u r n head ; }
@Override p u b l i c OurList<A> t a i l ( ) { r e t u r n t a i l ; }
26
27
28
}
Dabei sind die einzelnen zu implementierenden Methoden wie folgt spezifiziert.
Schreiben Sie auch mehrere Testaufrufe.
a) @Override int length();
Berechnet die Anzahl der Elemente in der Liste.
b) boolean contains(A el);
Wird wahr, wenn ein Element der Liste gleich dem übergebenen Argument ist.
c) void machtAlle(Consumer<A> c);
Wendet die Methode des Consumer-Objekts auf alle Elemente an.
50
2.1 Einfach verkettete Listen
d) OurList<A> add(A el);
Modifiziert die Liste, indem am Ende das neue Element hinzugefügt wird.
e) OurList<A> drop(int i);
Gibt die Teilliste zurück, die entsteht, wenn die ersten i Elemente von
der Liste genommen werden. Ist i zu groß, so wird eine leere Liste zurück
gegeben.
f) OurList<A> take(int i);
Gibt die Teilliste bestehend aus den ertsen i Elementen zurück.
g) OurList<A> sublist(int start, int length);
Gibt eine Teilliste zurück, die von dem Index start beginnt und die
spezifizierte Länge hat. Ist die Liste zu kurz, wird eine entsprechend
kürzere bis hin zur leeren Liste zurück gegeben.
h) int indexOf(A el);
Gibt an, an welchem Index ein bestimmtes Element liegt. Ist das Element
nicht enthalten, so wird -1 zurück gegeben.
i) A get(int index);
Gibt das Element an einem bestimmten Index zurück. Ist der Index zu
groß, so wird null zurück gegeben.
Aufgabe 2 (Nur für das Modul im Studiengang »Angewandte Informatik«)
Gegeben sei folgende C-Header Datei:
1
#i n c l u d e <s t d b o o l . h>
2
3
4
5
6
s t r u c t Li {
v o i d * head ;
s t r u c t Li * t a i l ;
};
7
8
t y p e d e f s t r u c t Li L i s t ;
9
10
11
12
b o o l isEmpty ( L i s t * t h i s ) ;
L i s t * newEmptyList ( ) ;
L i s t * newConsList ( v o i d * hd , L i s t * t l ) ;
13
14
void d e l e t e L i s t ( L i s t * t h i s ) ;
15
51
Kapitel 2 Hierarchische Strukturen
16
17
unsigned i n t length ( L i s t * t h i s ) ;
L i s t * add ( L i s t * t h i s , v o i d * e l ) ;
18
19
20
21
22
23
24
List *
List *
List *
List *
List *
void *
copyList ( List * thi s ) ;
append ( L i s t * t h i s , L i s t * t h a t ) ;
drop ( L i s t * t h i s , i n t i ) ;
take ( L i s t * this , i n t i ) ;
s u b l i s t ( List * this , int start , int length ) ;
get ( L i s t * this , i n t index ) ;
25
26
v o i d macheFuerAlle ( L i s t * t h i s , v o i d consumer ( v o i d * ) ) ;
Listing 2.20: List.h
Folgende Datei enthält eine rudimentäre Implementierung:
1
2
#i n c l u d e ” L i s t . h”
#i n c l u d e < s t d l i b . h>
3
4
5
6
b o o l isEmpty ( L i s t * t h i s ) {
r e t u r n t h i s −>head==NULL && t h i s −> t a i l==NULL;
}
7
8
9
10
11
12
13
L i s t * newEmptyList ( ) {
L i s t * t h i s = malloc ( s i z e o f ( L i s t ) ) ;
t h i s −>head = NULL;
t h i s −> t a i l = NULL;
return this ;
}
14
15
16
17
18
19
20
L i s t * newConsList ( v o i d * head , L i s t * t a i l ) {
L i s t * t h i s = malloc ( s i z e o f ( L i s t ) ) ;
t h i s −>head = head ;
t h i s −> t a i l = t a i l ;
return this ;
}
21
22
23
24
25
void d e l e t e L i s t ( L i s t * t h i s ) {
i f ( ! isEmpty ( t h i s ) ) d e l e t e L i s t ( t h i s −> t a i l ) ;
free ( this ) ;
}
Listing 2.21: List.c
Implementieren Sie die fehlenden Funktionen entsprechend der Spezifikation
aus Aufgabe 1.
52
2.2 Bäume als verallgemeinerte Listen
2.2 Bäume als verallgemeinerte Listen
In diesem Abschnitt soll jetzt das Prinzip der Listen verallgemeinert werden zu
Bäumen, Listen sind nämlich nur ein Speziellfall einer Baumstruktur.
Bäume sind ein gängiges Konzept um hierarchische Strukturen zu modellieren. Sie
sind bekannt aus jeder Art von Baumdiagramme, wie Stammbäumen oder Firmenhierarchien. In der Informatik sind Bäume allgegenwärtig. Fast alle komplexen
Daten stellen auf die eine oder andere Art einen Baum dar. Beispiele für Bäume
sind mannigfach:
• Dateisystem: Ein gängiges Dateisystem, wie es aus Unix, MacOS und Windows bekannt ist, stellt eine Baumstruktur dar. Es gibt einen ausgezeichneten
Wurzelordner, von dem aus zu jeder Datei einen Pfad existiert.
• Klassenhierarchie: Die Klassen in Java stellen mit ihrer Ableitungsrelation
eine Baumstruktur dar. Die Klasse Object ist die Wurzel dieses Baumes, von
der aus alle anderen Klassen über einen Pfad entlang der Ableitungsrelation
erreicht werden können.
• XML: Die logische Struktur eines XML-Dokuments ist ein Baum. Die Kinder
eines Elements sind jeweils die Elemente, die durch das Element eigeschlossen
sind.
• Parserergebnisse: Ein Parser, der gemäß einer Grammatik prüft, ob ein
bestimmter Satz zu einer Sprache gehört, erzeugt im Erfolgsfall eine Baumstruktur. Im nächsten Kapitel werden wir dieses im Detail kennenlernen.
• Listen: Auch Listen sind Bäume, allerdings eine besondere Art, in denen jeder
Knoten nur maximal ein Kind hat.
• Berechnungsbäume: Zur statischen Analyse von Programmen, stellt man
Bäume auf, in denen die Alternativen eines bedingten Ausdrucks Verzweigungen im Baum darstellen.
• Tableaukalkül: Der Tableaukalkül ist ein Verfahren zum Beweis logischer
Formeln. Die dabei verwendeten Tableaux sind Bäume.
• Spielbäume: Alle möglichen Spielverläufe eines Spiels können als Baum
dargestellt werden. Die Kanten entsprechen dabei einem Spielzug.
• Prozesse: Auch die Prozesse eines Betriebssystems stellen eine Baumstruktur
dar. Die Kinder eines Prozesses sind genau die Prozesse, die von ihm gestartet
wurden.
Wie man sieht, lohnt es sich, sich intensiv mit Bäumen vertraut zu machen, und man
kann davon ausgehen, was immer in der Zukunft neues in der Informatik entwickelt
werden wird, Bäume werden darin in irgendeiner Weise eine Rolle spielen.
Ein Baum besteht aus einer Menge von Knoten die durch gerichtete Kanten verbunden sind. Die Kanten sind eine Relation auf den Knoten des Baumes. Die Kanten
53
Kapitel 2 Hierarchische Strukturen
verbinden jeweils einen Elternknoten mit einem Kinderknoten. Ein Baum hat einen
eindeutigen Wurzelknoten. Dieses ist der einzige Knoten, der keinen Elternknoten
hat, d.h. es gibt keine Kante, die zu diesen Knoten führt. Knoten, die keinen Kinderknoten haben, d.h. von denen keine Kante ausgeht, heißen Blätter.
Die Kinder eines Knotens sind geordnet, d.h. sie stehen in einer definierten Reihenfolge. Eine Folge von Kanten, in der der Endknoten einer Vorgängerkante der
Ausgangsknoten der nächsten Kanten ist, heißt Pfad.
In einem Baum darf es keine Zyklus geben, das heißt, es darf keinen Pfad geben,
auf dem ein Knoten zweimal liegt.
Knoten können in Bäumen markiert sein, z.B. einen Namen haben. Mitunter können
auch Kanten eine Markierung tragen.
Bäume, in denen Jeder Knoten maximal zwei Kinderknoten hat, nennt man Binärbäume. Bäume, in denen jeder Knoten maximal einen Kinderknoten hat, heißen
Listen.
2.2.1 Formale Definition
Auch Bäume werden wir induktiv definieren:
Definition 2.2.1 Sei eine Menge E der Elemente gegeben. Die Menge der Bäume
über E im Zeichen T reeE sei dann die kleinste Menge, für die gilt:
• Der leere Baum ist ein Baum: Empty ∈ TreeE .
• Sei e ∈ E und seien t1 , . . . , tn ∈ TreeE .
Dann ist auch Branch(e, t1 , . . . , tn ) ∈ TreeE .
Der einzige Unterschied zu unserer formalem Definition der Listen ist, dass im zweiten Punkt ein Baum aus n bereits existierenden Bäumen und einen neuem Element
gebaut werden und nicht wie bei Listen aus einer existierenden Liste und einem
Element. Wenn in der Definition der Bäume das n immer nur 1 ist, dann ist die
Definition identisch mit der Definition von Listen.
2.2.2 Implementierung einer Baumklasse
Die Definition eines Baumes war sehr ähnlich der Definition der Listen.
Entsprechend ist auch die Implementierung sehr ähnlich zu unserer Listenimplementierung. Wir schreiben eine Klasse, die generisch über die Elemente, die an den
Baumknoten stehen, ist:
54
2.2 Bäume als verallgemeinerte Listen
1
2
3
4
package name . p a n i t z . u t i l ;
import j a v a . u t i l . L i s t ;
import j a v a . u t i l . A r r a y L i s t ;
import j a v a . u t i l . I t e r a t o r ;
5
6
7
p u b l i c c l a s s Tree<E> implements I t e r a b l e <E>{
Listing 2.22: Tree.java
Ebenso wie bei Listen, benötigen wir ein Feld, für das an dem Baumknoten stehende
Element. Dieses wurde bei Listen meist als head bezeichnet.
8
p u b l i c E element = n u l l ;
Listing 2.23: Tree.java
Eine Listenzelle hatte einen Verweis auf die Restliste. Bei Bäumen gibt es nicht
nur einen Unterbaum, sondern mehrere Unterbäume, die wiederum in einer Liste
gespeichert werden. Hierzu nutzen wir die Standardliste. Also anstatt eines Feldes
LiLi<A> tail wie bei Listen, benötigen wir für Bäume ein Feld, das eine Liste von
Teilbäumen enthält.
9
p u b l i c L i s t <Tree<E>> c h i l d N o d e s = new A r r a y L i s t <>() ;
Listing 2.24: Tree.java
In einem boolschen Flag sei vermerkt, ob es sich um einen leeren Baum, der noch
gar keinen Knoten enthält handelt oder nicht.
10
p u b l i c b o o l e a n isEmptyTree ;
Listing 2.25: Tree.java
Wir verketten unsere Bäume auch rückwärts, von den Kindern zu ihren Elternknoten.
11
p u b l i c Tree<E> p a r e n t = n u l l ;
Listing 2.26: Tree.java
Laut Spezifikation gibt es zwei Konstruktoren. Den ersten zum Erzeugen eines leeren
Baumes:
12
13
14
p u b l i c Tree ( ) {
isEmptyTree = t r u e ;
}
Listing 2.27: Tree.java
55
Kapitel 2 Hierarchische Strukturen
Der zweite erzeugt eine neue Baumwurzel mit einer bestimmten Markierung.
p u b l i c Tree (E e , Tree<E > . . . t s ) {
element = e ;
isEmptyTree = f a l s e ;
f o r ( Tree<E> t : t s ) {
i f ( ! t . isEmptyTree ) {
c h i l d N o d e s . add ( t ) ;
t . parent = t h i s ;
}
}
}
15
16
17
18
19
20
21
22
23
24
Listing 2.28: Tree.java
Hier wird das Konstrukt einer variablen Parameteranzahl verwendet. Die drei Punkte an dem letzten Parameter zeigen an, dass hier 0 bis n Parameter eines Typs
erwartet werden.
Intern wird für diese Parameter ein Array erzeugt, über den wir in diesem Fall mit
einer for-each Schleife iterieren.
Für weitere kleine Tests seien ein paar Beispielbäume bereit gestellt.
static
static
static
static
static
static
static
static
static
static
static
static
25
26
27
28
29
30
31
32
33
34
35
36
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
Tree<S t r i n g >
a1
a2
a3
b1
a4
a5
b2
a6
a7
a8
b3
c1
=
=
=
=
=
=
=
=
=
=
=
=
new
new
new
new
new
new
new
new
new
new
new
new
Tree <>(” a1 ” ) ;
Tree <>(” a2 ” ) ;
Tree <>(” a3 ” ) ;
Tree <>(” b1 ” , a1 ,
Tree <>(” a4 ” ) ;
Tree <>(” a5 ” ) ;
Tree <>(” b2 ” , a4 ,
Tree <>(” a6 ” ) ;
Tree <>(” a7 ” ) ;
Tree <>(” a8 ” ) ;
Tree <>(” b3 ” , a6 ,
Tree <>(” c1 ” , b1 ,
a2 , a3 ) ;
a5 ) ;
a7 , a8 ) ;
b2 , b3 ) ;
Listing 2.29: Tree.java
Als ein weniger abstraktes Beispiel seien die Nachfahren des englische Königs George
VI als ein Stammbaum angegeben:
s t a t i c Tree<S t r i n g > wind sor =
new Tree<>
( ” George ”
, new Tree<>
( ” Elizabeth ”
, new Tree<>
( ” Charles ”
, new Tree <>(” William ” , new Tree <>(” George ” ) )
, new Tree <>(” Henry ” )
37
38
39
40
41
42
43
44
45
56
2.2 Bäume als verallgemeinerte Listen
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
)
, new Tree<>
( ”Andrew”
, new Tree <>(” B e a t r i c e ” )
, new Tree <>(” Eugenie ” )
)
, new Tree<>
( ”Edward”
, new Tree <>(” L o u i s e ” )
, new Tree <>(” James ” )
)
, new Tree<>
( ”Anne”
, new Tree <>(” P e t e r ” )
, new Tree <>(” Zara ” )
)
)
, new Tree<>
( ” Magaret ”
, new Tree<>
( ” David ”
, new Tree <>(” C a r l e s ” )
, new Tree <>(” M a r a g r i t a ” )
)
70
, new Tree <>(” Sarah ” )
)
71
72
73
);
Listing 2.30: Tree.java
Stringdarstellung
Eine der ersten Methoden, die üblicher Weise für Klassen geschrieben wird, ist die
Darstellung des Objektes als ein Text in Form eines Stringobjektes. Hierzu wird
gängiger Weise die Methode toString aus der Klasse Objekt überschrieben. Für
die Baumklassen wäre folgende Umsetzung denkbar, die wir allerdings auf Deutsch
als alsString bezeichnet haben.
74
75
76
77
78
79
80
81
public String alsString () {
i f ( isEmptyTree ) r e t u r n ”Empty ( ) ” ;
S t r i n g r e s u l t = ” Branch ( ”+e l e m e n t+” ) [ ” ;
f o r ( Tree<E> c h i l d : c h i l d N o d e s ) {
r e s u l t = r e s u l t+c h i l d . a l s S t r i n g ( ) ;
}
r e t u r n r e s u l t+” ] ” ;
}
Listing 2.31: Tree.java
57
Kapitel 2 Hierarchische Strukturen
Diese Umsetzung ist rekursiv, indem die Methode für jeden Kindknoten rekursiv aufgerufen wird. Die Teilergebnisse für die Kindbäume werden dabei über die
Stringkonkatenation mit dem eingebauten Operator + aneinander gehängt.
Dieses ist nicht ganz unproblematisch. Stringobjekte sind in Java unveränderbar (unmutable). Ein einmal erzeugtes Stringobjekt wird nie verändert. Der plusOperator erzeugt aus zwei Stringobjekten ein neues Stringobjekt. Dieses geschieht
dadurch, dass ein neues Objekt erzeugt wird und alle Zeichen der beiden Ausgangsobjekte in dieses neue Objekt kopiert werden. Wenn für die Berechnung eines
Stringergebnisses viele Teilsstring mit dem plus-Operator aneinander gehängt werden, dann werden sehr viele Objekte erzeugt und insbesondere sehr viele Zeichen
immer wieder im Speicher von einem Speicherbereich auf einen anderen kopiert..
Dieses werden wir im zweiten Teil der Vorlesung noch direkter sehen, wenn in der
Programmiersprache C, die Operationen des plus-Operators explizit selbst programmiert werden müssen.
Es empfiehlt sich also, bei der Konstruktion großer Texte, nicht gleich alle Teilstrings
mit dem plus-Operator aneinander zu hängen und so permament Teilstrings zu
kopieren.
Strings puffern mit StringBuffer Das Java Standard-API stellt eine Klasse zur
Verfügung, die es ermöglicht eine Folge von Strings zu sammeln, die dann im Endeffekt zu einem großen String konkateniert werden: die Klasse StringBuffer. Diese
hat intern eine Liste der einzelnen aneinander zu hängenden Strings. Erst wenn
alle Strings eingesammelt wurden, dann werden alle Strings zu einem großen String
zusammen kopiert. Somit werden die Zeichen der einzelnen Strings nicht mehrfach
im Speicher kopiert und das Anlegen temporärer Stringobjekte vermieden.
Die Methode toString() kann mit Hilfe der Klasse StringBuffer effizienter umgesetzt werden. Es wird ein StringBuffer-Objekt angelegt. Mit der Methode append
werden die einzelnen Strings dann diesem Objekt angefügt. Ein terminaler Aufruf
der Methode toString auf dem StringBuffer-Objekt führt dann dazu, dass der
Ergebnisstring erzeugt wird:
Zunächst nehmen wir aber den einfachen Fall des leeren Baumes aus:
@Override p u b l i c S t r i n g t o S t r i n g ( ) {
i f ( isEmptyTree ) r e t u r n ”Empty ( ) ” ;
82
83
Listing 2.32: Tree.java
Andernfalls wird ein StringBuffer-Objekt mit dem Anfang des Ergebnisstrings
erzeugt:
S t r i n g B u f f e r r e s u l t = new S t r i n g B u f f e r ( ” Branch ( ” ) ;
84
Listing 2.33: Tree.java
58
2.2 Bäume als verallgemeinerte Listen
Diesem wird zunächst das Element in Stringdarstellung zugefügt:
r e s u l t . append ( e l e m e n t . t o S t r i n g ( ) ) ;
r e s u l t . append ( ” ) [ ” ) ;
85
86
Listing 2.34: Tree.java
Schließlich werden noch die Kinder durchiteriert, um deren String-Darstellung dem
StringBuffer-Objekt zuzufügen:
f o r ( Tree<E> c h i l d : c h i l d N o d e s ) {
r e s u l t . append ( c h i l d . t o S t r i n g ( ) ) ;
}
r e s u l t . append ( ” ] ” ) ;
87
88
89
90
Listing 2.35: Tree.java
Nun sind alle Teilstrings aufgesammelt und das Ergebnisobjekt kann erzeugt werden:
return r e s u l t . toString () ;
91
92
}
Listing 2.36: Tree.java
String schreiben mit einem Writer Wer sich die letzte Lösung der Methode
toString() etwas genauer angeschaut hat, der wird bemerkt haben, dass trotzdem
viele temporäre Teilstrings erzeugt werden. Bei jedem rekursiven Aufruf, wird wieder
ein neues StringBuffer-Objekt erzeugt, welches bei der Rückgabe einen neuen
String erzeugt. Es werden also auch in dieser Umsetzung immer wieder dieselben
Teilstrings im Speicher kopiert. Dieses Problem lässt sich nur umgehen, wenn man
das StringBuffer-Objekt als Parameter an die Methode übergibt.
Eine andere Möglichkeit, die noch flexibler ist, ermöglicht es Strings nicht explizit zu
erzeugen, sondern in eine Datensenke zu schreiben. Hierzu dient die aus dem letzten
Semester bekannte Klasse java.io.Writer. Von dieser gibt es unterschiedliche Implementierungen, die jeweils die Zeichen in unterschiedliche Datensenken schreiben.
Häufig wird diese natürlich eine Datei sein, es kann aber auch über ein Netzwerk an
einen Server geschrieben werden, oder aber auch wieder nur einfach in einen String.
Um in ein Writer-Objekt zu schreiben, muss dieses wie am Ende des letzten Abschnitts erörtert, als Parameter übergeben werde. Zusätzlich ist zu beachten, dass
die IO-Operationen zu Ausnahmen führen können:
93
p u b l i c v o i d w r i t e A s S t r i n g ( j a v a . i o . Writer out ) throws j a v a . i o .
IOException {
Listing 2.37: Tree.java
59
Kapitel 2 Hierarchische Strukturen
Im Falle des leeren Baumes, wird dessen Darstellung geschrieben und die Methode
verlassen:
i f ( isEmptyTree ) {
out . w r i t e ( ”Empty ( ) ” ) ;
return ;
}
94
95
96
97
Listing 2.38: Tree.java
Wenn es sich um einen nichtleeren Baum handelt, können jetzt die einzelnen Teile
nacheinander in den Writer geschrieben werden. Ähnlich wie in der vorherigen
Lösung die einzelnen Teile dem StringBuffer angehängt wurden.
out . w r i t e ( ” Branch ( ” ) ;
out . w r i t e ( e l e m e n t . t o S t r i n g ( ) ) ;
out . w r i t e ( ” ) [ ” ) ;
98
99
100
Listing 2.39: Tree.java
Der Unterschied zu der vorherigen Lösung ist, dass jetzt die rekursiven Aufrufe den
Writer als Parameter übergeben.
f o r ( Tree<E> c h i l d : c h i l d N o d e s ) {
c h i l d . w r i t e A s S t r i n g ( out ) ;
}
out . w r i t e ( ” ] ” ) ;
101
102
103
104
}
105
Listing 2.40: Tree.java
Um diese Implementierung jetzt auf gleiche Weise nutzen zu können wie die Standardmethode toString, kann ein StringWriter-Objekt erzeugt werden, in den der
String geschrieben wird.
public String asString () {
try {
j a v a . i o . S t r i n g W r i t e r out = new j a v a . i o . S t r i n g W r i t e r ( ) ;
w r i t e A s S t r i n g ( out ) ;
r e t u r n out . t o S t r i n g ( ) ;
} c a t c h ( j a v a . i o . IOException e ) {
return ”” ;
}
}
106
107
108
109
110
111
112
113
114
Listing 2.41: Tree.java
Mit dieser Implementierung ist unnötiges Kopieren von Teilstrings komplett vermieden worden. zusätzlich ist sie flexibler, weil die Methode writeAsString nicht
nur zum Erzeugen eines Strings genutzt werden kann, sondern auch dazu, die Stringdarstellung in unterschiedliche Datensenken zu schreiben.
60
2.2 Bäume als verallgemeinerte Listen
Elemente in einer Liste gesammelt
Ist man nicht mehr an der Baumstruktur interessiert, so kann man sich die Elemente
eines Baums in einer Liste speichern.
115
116
117
118
119
p u b l i c j a v a . u t i l . L i s t <E> a s L i s t ( ) {
r e t u r n a s L i s t ( new j a v a . u t i l . L i n k e d L i s t <>() ) ;
}
p u b l i c L i s t <E> a s L i s t ( L i s t <E> r e s u l t ) {
i f ( ! isEmptyTree ) r e s u l t . add ( e l e m e n t ) ;
120
f o r ( Tree<E> c h i l d : c h i l d N o d e s ) c h i l d . a s L i s t ( r e s u l t ) ;
121
122
return r e s u l t ;
123
124
}
Listing 2.42: Tree.java
Ein einfacher Iterator über die Listendarstellung
Mit der Listensammlung aller Elemente eines Baumes lässt sich schließlich leicht
ein Baum über alle seine Elemente iterierbar machen. Es muss nur die Liste erzeugt
werden und über diese iteriert werden.
125
126
127
128
@Override p u b l i c j a v a . u t i l . I t e r a t o r <E> i t e r a t o r ( ) {
return asList () . i t e r a t o r () ;
}
Listing 2.43: Tree.java
Rekursive Methoden mit reduce auf Kindknoten
Mit den Möglichkeiten von Java 8 lassen sich fast alle Algorithmen, die in den
Baum absteigen, um dort Informationen zu sammeln, auch ohne explizite Schleifen
sondern durch Methode höherer Ordnung auf die Kinderliste ausdrücken. Ob das
immer besser ist, sei allerdings dahingestellt.
Betrachten wir zunächst die Größe eines Baumes. Diese lässt sich als ein einziger
Ausdruck formulieren. Zunächst wird durch den Bedingungsoperator der Spezialfall
eines leeren Baumes behandelt. Ansonsten ist es 1 zur Summe der Größen aller
Kinderbäume:
129
130
131
public int size () {
r e t u r n isEmptyTree ? 0 :
1+c h i l d N o d e s
61
Kapitel 2 Hierarchische Strukturen
. parallelStream ()
. r e d u c e ( 0 , ( r e s u l t , c h i l d ) −> r e s u l t+c h i l d . s i z e ( ) , Math : : addExact )
132
133
;
}
134
Listing 2.44: Tree.java
Die Summe der Kinderbäume ist mit der Methode reduce auf Stream gelöst. Diese
entspricht der Methode reduce, wie wir sie selbst in dem eigenen Interface Iterable
umgesetzt haben. Sie bekommt allerdings zwei Funktionsobjekte. Die erste Funktion
beschreibt, wie ein einzelnen Element sein Ergebnis zum Gesamtergebnis hinzuzählt.
In unserem Fall, wird von jedem Kind die Größe zum Gesamtergebnis addiert. Die
zweite Funktion beschreibt, wie Teilergebnisse zu verknüpfen sind. In unserem Fall
durch die Addition. Diese zwei Funktionen werden gebraucht, wenn der Stream, auf
dem gearbeitet wird, parallelisiert wird. Dann wird z.B. vielleicht parallel das Teilergebnis der ersten Hälfte der Kinder und der zweiten Hälfte der Kinder berechnet.
Diese Teilergebnisse sind zu addieren.
Da wir unsere Bäume unser Interface Iterable haben implementieren lassen, können wir aber sehr einfach die dort als default definierte Methode reduce benutzen.
Hier erhalten wir allerdings keine Parallelisierung und erzeugen zusätzlich über die
Methode iterator() der Klasse Tree erst eine Liste aller Elemente.
public int size2 () {
r e t u r n r e d u c e ( 0 , ( r e s u l t , elem ) −> r e s u l t + 1 ) ;
}
135
136
137
Listing 2.45: Tree.java
Sehr sehr ähnlich sieht nun die Lösung für eine gänzlich andere Fragestellung aus.
Was ist die maximale Tiefe des Baumes, also wie viele Generationen hat der Baum.
Statt die Teilergebnisse aufzuaddieren, nehmen wir jetzt immer das Maximum der
Teilergebnisse. Auch diese Aufgabe ist wunderbar parallelisierbar:
public int generations () {
r e t u r n isEmptyTree ? 0 :
1 +
childNodes
. parallelStream ()
. r e d u c e ( 0 , ( r e s u l t , c h i l d )−>Math . max( r e s u l t , c h i l d . g e n e r a t i o n s ( ) ) ,
Math : : max) ;
}
138
139
140
141
142
143
144
145
}
Listing 2.46: Tree.java
Aufgabe 1 Schreiben Sie für die Klasse Tree eine Methode
62
2.2 Bäume als verallgemeinerte Listen
List<E> fringe(),
die die Liste der Blätter des Baumes zurück gibt. Ein Blatt ist dabei ein
Baumknoten, der keine Kinder mehr hat.
Lösung
Listing 2.47: Tree.java
Aufgabe 1 Lösung in der Woche vom 19.5. in der Praktikumsstunde.
Ergänzen Sie Ihre Klasse Tree aus dem letzten Übungsblatt um die folgende
Methode:
public void writeAsXML(java.io.Writer out) throws IOException;
Es soll der Baum serialisiert als ein XML-Dokument in das Writer-Objekt
geschrieben werden.
Aufgabe 2 Schreiben Sie für die Klasse Tree eine Methode
List<E> pathTo(E elem),
die die Elemente auf dem Pfad von der Wurzel zu dem Knoten mit der
Element-Markierung elem zurück gibt. Sollte kein Knoten mit dem Element
elem markiert sein, so ist die leer Liste das Ergebnis.
Aufgabe 2 Lösung in der Woche vom 19.5. in der Praktikumsstunde.
Schreiben Sie für die Klasse Tree eine Methode:
public static Tree<String> readFromXML(java.io.File xmlFile) throws Exception;
Es soll ein neuen Baum erzeugt werden, der aus einer XML-Datei gelesen wird.
Hierbei wird davon ausgegangen, dass die XML-Datei mit der Methode aus
Aufgabe 1 geschrieben wurde. Benutzen Sie hierbei das DOM API.
Aufgabe 3 Schreiben Sie für die Klasse Tree eine Methode
boolean contains(java.util.function.Predicate<E> pred),
die genau dann wahr als Ergebnis hat, wenn für eines der Elemente im Baum
das übergebene Prädikat wahr ist.
63
Kapitel 2 Hierarchische Strukturen
Aufgabe 3 Nur für die Angewandte Informatik. Abgabe dieser Aufgabe
bis Montag 26.5. per Mail an Leiter der Praktikumsstunde.
Gegeben sei folgende Header Datei aus der Vorlesung:
1
2
#i f n d e f BIN_TREE__H
#d e f i n e BIN_TREE__H
3
4
#i n c l u d e <s t d b o o l . h>
5
6
typedef bool l e ( void * , void *) ;
7
8
9
10
11
12
13
14
s t r u c t TR{
/* im p r i n z i p i s t v o i d * s o etwas wie i r g e n d w a s a l s o : Object */
v o i d * head ;
/* l i n k e s Kind ( d a r f NULL s e i n ) */
s t r u c t TR* l e f t ;
/* l i n k e s Kind ( d a r f NULL s e i n ) */
s t r u c t TR* r i g h t ;
15
/* d i e O r d n u n g s r e l a t i o n . s o l l t e im j e d e n Knoten d e r s e l b e Z e i g e r
s e i n */
le * kleinerGleich ;
16
17
18
};
19
20
21
t y p e d e f s t r u c t TR Tree ;
t y p e d e f u n s i g n e d i n t nat ;
22
23
b o o l isEmpty ( Tree * t h i s ) ;
24
25
26
/* mache e i n e l e e r e Menge */
Tree * newEmptyTree ( l e k l e i n e r G l e i c h ) ;
27
28
29
/* mache e i n e e i n e l e m e n t i g e Menge */
Tree * newLeafTree ( l e k l e i n e r G l e i c h , v o i d * elem ) ;
30
31
32
/* l ö s c h e a l l e Baumknoten , a b e r n i c h t d i e Elemente */
v o i d d e l e t e T r e e ( Tree * t h i s ) ;
33
34
/* u n s i g n e d i n t */ nat l e n g t h ( Tree * t h i s ) ;
35
36
37
/* Algorithmus f ü r das Hinzufügen e i n e s Elements :
38
39
40
41
42
43
44
45
64
1 . Schaue ob t h i s l e e r e r Baum : dann t h i s −>head = e l
2. sonst
v e r g l e i c h e : t h i s −>k l e i n e r G l e i c h ( e l , head )
a ) Ja ! dann c h e c k e : t h i s −>k l e i n e r G l e i c h ( head , e l )
i ) Ja ! dann r e t u r n ( ohne w e i t e r e a k t i o n )
i i ) s o n s t add ( t h i s −>l e f t , e l ) ( wenn t h i s −> l e f t n i c h t NULL)
( o d e r t h i s −> l e f t = newLeafTree ( t h i s −>
2.2 Bäume als verallgemeinerte Listen
kleinerGleich , el )
b ) a n a l o g wie oben nur r e c h t e S e i t e r e c h t s
46
47
48
*/
v o i d add ( Tree * t h i s , v o i d * e l ) ;
49
50
51
/* k o p i e r e den Baum, a b e r n i c h t d i e Elemente */
Tree * copyTree ( Tree * t h i s ) ;
52
53
54
55
/* e n t h ä l t d e r Baum e i n bestimmtes Element . H i e r z u wird d i e
O r d n u n g s r e l a t i o n d e s Baumes genommen .
x==y b e d e u t e t : k l e i n e r G l e i c h ( x , y ) && k l e i n e r G l e i c h ( y , x ) .
*/
56
57
b o o l c o n t a i n s ( Tree * t h i s , v o i d * elem ) ;
58
59
60
/* mache etwas f ü r a l l e Elemente , d i e vom Baum r e f e r e n z i e r t
werden . */
v o i d macheFuerAlle ( Tree * t h i s , v o i d consumer ( v o i d * ) ) ;
61
62
/* B e i s p i e l f ü r i n t Z e i g e r :
63
64
65
66
b o o l k l G l ( v o i d * xp , v o i d * yp ) {
r e t u r n * ( ( i n t * ) xp ) <= * ( ( i n t * ) yp ) ;
}
67
68
69
70
void p r i n t I n t ( void * p) {
p r i n t f (”%d ” , * ( ( i n t * ) p ) ) ;
}
71
72
73
74
75
76
i n t main ( ) {
i n t xs [ ] = { 3 4 , 5 , 2 3 , 6 , 5 3 4 5 , − 3 4 , 4 3 , − 4 3 1 , 1 , 4 3 , 5 4 4 5 , − 2 6 5 } ;
Tree * t = newEmptyTree ( k l G l ) ;
int i ;
f o r ( i =0; i < 1 2 ; i ++) add ( t , xs+i ) ;
77
macheFuerAlle ( t , p r i n t I n t ) ;
78
79
deleteTree ( t ) ;
80
81
return 0;
82
83
}
84
85
86
*/
#e n d i f
Listing 2.48: BinTree.h
Schreiben Sie die Implementierung dieser header Datei, so dass sie einen Binärbaum erhalten, der eine Menge realisiert.
1
2
#i n c l u d e ” BinTree . h”
#i n c l u d e < s t d l i b . h>
65
Kapitel 2 Hierarchische Strukturen
3
#i n c l u d e <s t d i o . h>
4
5
6
7
8
9
b o o l isEmpty ( Tree * t h i s ) {
r e t u r n t h i s −>head==NULL
&& t h i s −> l e f t == NULL
&& t h i s −>r i g h t == NULL;
}
10
11
12
13
Tree * newEmptyTree ( l e k l e i n e r G l e i c h ) {
r e t u r n newLeafTree ( k l e i n e r G l e i c h ,NULL) ;
}
14
15
16
17
18
19
20
21
22
23
/* mache e i n e e i n e l e m e n t i g e Menge */
Tree * newLeafTree ( l e k l e i n e r G l e i c h , v o i d * elem ) {
Tree * t h i s = m a l l o c ( s i z e o f ( Tree ) ) ;
t h i s −> l e f t = NULL;
t h i s −> r i g h t = NULL;
t h i s −> k l e i n e r G l e i c h = k l e i n e r G l e i c h ;
t h i s −> head = elem ;
return this ;
}
24
25
26
27
28
29
30
/* l ö s c h e a l l e Baumknoten , a b e r n i c h t d i e Elemente */
v o i d d e l e t e T r e e ( Tree * t h i s ) {
i f (NULL != t h i s −> l e f t ) d e l e t e T r e e ( t h i s −> l e f t ) ;
i f (NULL != t h i s −>r i g h t ) d e l e t e T r e e ( t h i s −>r i g h t ) ;
free ( this ) ;
}
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
v o i d add ( Tree * t h i s , v o i d * e l ) {
i f ( isEmpty ( t h i s ) ) {
t h i s −> head = e l ;
} else {
i f ( t h i s −>k l e i n e r G l e i c h ( e l , t h i s −>head ) ) {
i f ( t h i s −>k l e i n e r G l e i c h ( t h i s −>head , e l ) ) r e t u r n ;
i f (NULL == t h i s −> l e f t ) {
t h i s −> l e f t = newLeafTree ( t h i s −>k l e i n e r G l e i c h , e l ) ;
}else {
add ( t h i s −>l e f t , e l ) ;
}
} else {
i f (NULL == t h i s −> r i g h t ) {
t h i s −> r i g h t = newLeafTree ( t h i s −>k l e i n e r G l e i c h , e l ) ;
}else {
add ( t h i s −>r i g h t , e l ) ;
}
}
}
}
52
53
54
66
v o i d macheFuerAlle ( Tree * t h i s , v o i d consumer ( v o i d * ) ) {
i f ( t h i s == NULL) r e t u r n ;
2.3 XML als serialisierte Baumstruktur
i f ( isEmpty ( t h i s ) ) r e t u r n ;
macheFuerAlle ( t h i s −> l e f t , consumer ) ;
consumer ( t h i s −>head ) ;
macheFuerAlle ( t h i s −> r i g h t , consumer ) ;
55
56
57
58
59
}
60
61
62
63
void p r i n t I n t ( void * p) {
p r i n t f ( ”%d ” , * ( ( i n t * ) p ) ) ;
}
64
65
66
67
68
69
70
71
72
73
b o o l c o n t a i n s ( Tree * t h i s , v o i d * e l ) {
i f ( t h i s == NULL) r e t u r n f a l s e ;
i f ( isEmpty ( t h i s ) ) r e t u r n f a l s e ;
i f ( t h i s −>k l e i n e r G l e i c h ( e l , t h i s −> head )&&t h i s −>k l e i n e r G l e i c h (
t h i s −> head , e l ) )
return true ;
i f ( t h i s −>k l e i n e r G l e i c h ( e l , t h i s −> head ) ) {
r e t u r n c o n t a i n s ( t h i s −> l e f t , e l ) ;
}
r e t u r n c o n t a i n s ( t h i s −> r i g h t , e l ) ;
74
75
}
76
77
78
79
b o o l k l G l ( v o i d * xp , v o i d * yp ) {
r e t u r n * ( ( i n t * ) xp ) <= * ( ( i n t * ) yp ) ;
}
Listing 2.49: BinTree.c
Aufgabe 4 Schreiben Sie für die Klasse Tree eine Methode
boolean contains(E el),
die genau dann wahr als Ergebnis hat, wenn eines der Elemente im Baum
gleich dem übergebenen Element ist.
Aufgabe 5 Schreiben Sie für die Klasse Tree eine Methode
<R> Tree<R> mapTree(java.util.function.Function<E, R> f),
die einen neuen Baum erzeugt, dessen Elemente aus den Elementen des Ursprungsbaums mit Hilfe der übergebenen Funktion erzeugt werden.
2.3 XML als serialisierte Baumstruktur
XML ist eine Sprache, die es erlaubt Dokumente mit einer logischen Struktur zu
beschreiben. Die Grundidee dahinter ist, die logische Struktur eines Dokuments
67
Kapitel 2 Hierarchische Strukturen
von seiner Visualisierung zu trennen. Ein Dokument mit einer bestimmten logischen Struktur kann für verschiedene Medien unterschiedlich visualisiert werden,
z.B. als HTML-Dokument für die Darstellung in einem Webbrowser, als pdf- oder
postscript-Datei für den Druck des Dokuments und das für unterschiedliche Druckformate. Eventuell sollen nicht alle Teile eines Dokuments visualisiert werden. XML
ist zunächst eine Sprache, die logisch strukturierte Dokumente zu schreiben, erlaubt.
Dokumente bestehen hierbei aus den eigentlichen Dokumenttext und zusätzlich
aus Markierungen dieses Textes. Die Markierungen sind in spitzen Klammern
eingeschlossen.
Beispiel 2.3.1 Der eigentliche Text des Dokuments sei:
1
The B e a t l e s White Album
Die einzelnen Bestandteile dieses Textes können markiert werden:
1
2
3
4
<cd>
< a r t i s t >The B e a t l e s </ a r t i s t >
< t i t l e >White Album</ t i t l e >
</cd>
Die XML-Sprache wird durch ein Industriekonsortium definiert, dem W3C. Dieses
ist ein Zusammenschluß vieler Firmen, die ein gemeinsames Interesse eines allgemeinen Standards für eine Markierungssprache haben. Die eigentlichen Standards
des W3C heißen nicht Standard, sondern Empfehlung (recommendation), weil es
sich bei dem W3C nicht um eine staatliche oder überstaatliche Standartisiertungsbehörde handelt. Die aktuelle Empfehlung für XML liegt seit August 2006 als
Empfehlung in der Version 1.1 vor .
XML enstand Ende der 90er Jahre und ist abgeleitet von einer umfangreicheren
Dokumentenbeschreibungssprache: SGML. Der SGML-Standard ist wesentlich komplizierter und krankt daran, daß es extrem schwer ist, Software für die Verarbeitung
von SGML-Dokumenten zu entwickeln. Daher fasste SGML nur Fuß in Bereichen,
wo gut strukturierte, leicht wartbare Dokumente von fundamentaler Bedeutung
waren, so dass die Investition in teure Werkzeuge zur Erzeugung und Pflege von
SGML-Dokumenten sich rentierte. Dies waren z.B. Dokumentationen im Luftfahrtbereich.3
Die Idee bei der Entwicklung von XML war: eine Sprache mit den Vorteilen von
SGML zu Entwickeln, die klein, übersichtlich und leicht zu handhaben ist.
3
Man sagt, ein Pilot brauche den Copiloten, damit dieser die Handbücher für das Flugzeug trägt.
68
2.4 Das syntaktische XML-Format
Wie wir im Laufe des Kapitels sehen werden, ist die logische Struktur eines XMLDokuments auch wieder eine hierarchische Baumstruktur, so dass wir alle unsere
Überlegungen zu Bäumen direkt auf XML-Dokumente anwenden können.
2.4 Das syntaktische XML-Format
Die grundlegendste Empfehlung des W3C legt fest, wann ein Dokument ein gültiges
XML-Dokument ist, die Syntax eines XML-Dokuments. Die nächsten Abschnitte
stellen die wichtigsten Bestandteile eines XML-Dokuments vor.
Jedes Dokument beginnt mit einer Anfangszeile, in dem das Dokument angibt, daß
es ein XML-Dokument nach einer bestimmten Version der XML Empfehlung ist:
1
<?xml v e r s i o n=” 1 . 1 ”?>
Dieses ist die erste Zeile eines XML-Dokuments. Vor dieser Zeile darf kein Leerzeichen stehen. Die derzeitig aktuellste und einzige Version der XML-Empfehlung ist
die Version 1.1. vom 16. August 2006. Nach Aussage eines Mitglieds des W3C ist es
sehr unwahrscheinlich, dass es jemals eine Version 2.0 von XML geben wird. Zuviele
weitere Techniken und Empfehlungen basieren auf XML, so daß die Definition von
dem, was ein XML-Dokument ist kaum mehr in größeren Rahmen zu ändern ist.
2.4.1 Elementknoten
Der Hauptbestandteil eines XML-Dokuments sind die Elemente. Dieses sind mit der
Spitzenklammernotation um Teile des Dokuments gemachte Markierungen. Ein Element hat einen Tagnamen, der ein beliebiges Wort ohne Leerzeichen sein kann. Für
einen Tagnamen name beginnt ein Element mit <name> und endet mit </name>.
Zwischen dieser Start- und Endemarkierung eines Elements kann Text oder auch
weitere Elemente stehen.
Es wird für XML-Dokument verlangt, daß es genau ein einziges oberstes Element
hat.
Beispiel 2.4.1 Somit ist ein einfaches XML-Dokument ein solches Dokument, in
dem der gesammte Text mit einem einzigen Element markiert ist:
1
2
3
4
<?xml v e r s i o n=” 1 . 0 ”?>
<myText>D i e s e s i s t d e r Text d e s Dokuments . Er i s t
mit genau einem Element m a r k i e r t .
</myText>
69
Kapitel 2 Hierarchische Strukturen
Im einführenden Beispiel haben wir schon ein XML-Dokument gesehen, das mehrere
Elemente hat. Dort umschließt das Element <cd> zwei weitere Elemente, die Elemente <artist> und <title>. Die Teile, die ein Element umschließt, werden der
Inhalt des Elements genannt.
Ein Element kann auch keinen, sprich den leeren Inhalt haben. Dann folgt der
öffnenden Markierung direkt die schließende Markierung.
Beispiel 2.4.2 Folgendes Dokument enthält ein Element ohne Inhalt:
1
2
3
4
5
6
<?xml v e r s i o n=” 1 . 0 ”?>
<s k r i p t >
<page>e r s t e S e i t e </page>
<page></page>
<page>d r i t t e S e i t e </page>
</ s k r i p t >
2.4.2 Leere Elemente
Für ein Element mit Tagnamen name, das keinen Inhalt hat, gibt es die abkürzenden
Schreibweise: <name/>
Beispiel 2.4.3 Das vorherige Dokument läßt sich somit auch wie folgt schreiben:
1
2
3
4
5
6
<?xml v e r s i o n=” 1 . 0 ”?>
<s k r i p t >
<page>e r s t e S e i t e </page>
<page/>
<page>d r i t t e S e i t e </page>
</ s k r i p t >
2.4.3 Gemischter Inhalt
Die bisherigen Beispiele haben nur Elemente gehabt, deren Inhalt entweder Elemente oder Text waren, aber nicht beides. Es ist aber auch möglich Elemente mit
Text und Elementen als Inhalt zu schreiben. Man spricht dann vom gemischten
Inhalt (mixed content).
Beispiel 2.4.4 Ein Dokument, in dem das oberste Element einen gemischten Inhalt
hat:
70
2.4 Das syntaktische XML-Format
1
2
3
4
5
6
<?xml v e r s i o n=” 1 . 0 ”?>
<myText>Der <landsmann>I t a l i e n e r </landsmann>
<eigename>Ferdinand C a r u l l i </eigename> war a l s G i t a r r i s t
e b e n s o wie d e r <landsmann>Spanier </landsmann>
<eigename>Fernando Sor</eigename> i n <o r t >Paris </o r t >
a n s ä ß i g .</myText>
Tatsächlich ist gemischter Inhalt nicht bei jedermann beliebt. Es gibt zwei große
Anwendungshintergründe für XML. Zum einen die Dokumentenbeschreibung. In
diesem Kontext ist gemischter ganz natürlich, wie man am obigen Beispiel erkennen
kann. Die zweite Fraktion von Anwendern, die XML nutzt, kommt eher aus dem
Bereich allgemeiner Datenverarbeitung. Hier stehen eher Daten, wie in Datenbanken
im Vordergrund und nicht Textdokumente. Es wird da zum Beispiel XML genutzt
um an einen Webservice Daten zu übermitteln, die dort verarbeitet werden sollen.
In diesem Kontext stört gemischter Inhalt mitunter oder hat zumindest wenig Sinn.
Die Problematik wird sofort ersichtlich, wenn man sich überlegt, dass Leerzeichen
auch Text sind. Unter dieser Prämisse haben mit Sicherheit alle XML-Dokumente
gemischten Inhalt, weil zur optischen besseren Lesbarkeit auch in einem Datendokument, das eigentlich keinen gemischten Inhalt hat, zwischen den einzelnen Elementknoten Leerzeichen in Form von Einrückungen und neuen Zeilen stehen.
2.4.4 XML Dokumente sind Bäume
Die wohl wichtigste Beschränkung für XML-Dokumente ist, dass sie eine hierarchische Struktur darstellen müssen. Zwei Elemente dürfen sich nicht überlappen.
Ein Element darf erst wieder geschlossen werden, wenn alle nach ihm geöffneten
Elemente wieder geschlossen wurden.
Beispiel 2.4.5 Das folgende ist kein gültiges XML-Dokument. Das Element <bf>
wird geschlossen bevor das später geöffnete Element <em> geschlossen worde.
1
2
3
4
5
<?xml v e r s i o n=” 1 . 0 ”?>
<i l l e g a l D o c u m e n t >
<b f >f e t t e S c h r i f t <em>k u r s i v und f e t t </b f >
nur noch k u r s i v </em>.
</i l l e g a l D o c u m e n t >
Das Dokument wäre wie folgt als gültiges XML zu schreiben:
71
Kapitel 2 Hierarchische Strukturen
1
2
3
4
5
<?xml v e r s i o n=” 1 . 0 ”?>
<validDocument>
<b f >f e t t e S c h r i f t <em>k u r s i v und f e t t </em></b f >
<em>nur noch k u r s i v </em>.
</validDocument>
Dieses Dokument hat eine hierarchische Struktur.
Die hierarchische Struktur eines XML-Dokuments lässt sich in der Regel gut erkennen, wenn man das Dokument in einem Web-Browser öffnet. Fast alle Web-Browser
stellen dann die logische Baumstruktur so dar, dass die Kindknoten eines Elementknoten aufgeklappt und zugeklappt werden können.
2.4.5 Chracter Entities
Sobald in einem XML-Dokument eine der spitzen Klammern < oder > auftaucht,
wird dieses als Teil eines Elementtags interpretiert. Sollen diese Zeichen hingegen
als Text und nicht als Teil der Markierung benutzt werden, sind also Bestandteil des
Dokumenttextes, so muss man einen Fluchtmechanismus für diese Zeichen benutzen.
Diese Fluchtmechanismen nennt man character entities. Eine Character Entity beginnt in XML mit dem Zeichen & und endet mit einem Semikolon ;. Dazwischen
steht der Name des Buchstabens. XML kennt die folgenden Character Entities:
Entity Zeichen
Beschreibung
<
<
(less than)
>
>
(greater than)
&
&
(ampersant)
"
"
(quotation mark)
'
'
(apostroph)
Somit lassen sich in XML auch Dokumente schreiben, die diese Zeichen als Text
beinhalten.
Beispiel 2.4.6 Folgendes Dokument benutzt Character Entities um mathematische
Formeln zu schreiben:
1
2
3
4
5
<?xml v e r s i o n=” 1 . 0 ”?>
<g l e i c h u n g e n >
<g l e i c h u n g >x+1&g t ; x</g l e i c h u n g >
<g l e i c h u n g >x * x& l t ; x * x * x f ü r x&g t ;1</ g l e i c h u n g >
</g l e i c h u n g e n >
72
2.4 Das syntaktische XML-Format
2.4.6 CData Sections
Manchmal gibt es große Textabschnitte in denen Zeichen vorkommen, die eigentlich
durch character entities zu umschreiben wären, weil sie in XML eine reservierte
Bedeutung haben. XML bietet die Möglichkeit solche kompletten Abschnitte als
eine sogenannte CData Section zu schreiben. Eine CData section beginnt mit der
Zeichenfolge <![CDATA[ und endet mit der Zeichenfolge: ]]>. Dazwischen können
beliebige Zeichenstehen, die eins zu eins als Text des Dokumentes interpretiert werden.
Beispiel 2.4.7 Die im vorherigen Beispiel mit Character Entities beschriebenen
Formeln lassen sich innerhalb einer CDATA-Section wie folgt schreiben.
1
2
3
4
5
<?xml v e r s i o n=” 1 . 0 ”?>
<formeln ><![CDATA[
x+1>x
x *x<x * x * x f ü r x > 1
]]></ formeln >
Strukturell sind die CDATA-Sektionen auch nur Textknoten im Dokument. Sie unterscheiden sich von anderen Textknoten nur in der Darstellung in der serialisierten
Textform.
2.4.7 Kommentare
XML stellt auch eine Möglichkeit zur Verfügung, bestimmte Texte als Kommentar
einem Dokument zuzufügen. Diese Kommentare werden mit <!-- begonnen und
mit --> beendet. Kommentartexte sind nicht Bestandteil des eigentlichen Dokumenttextes.
Beispiel 2.4.8 Im folgenden Dokument ist ein Kommentar eingefügt:
1
2
3
4
5
6
7
8
9
<?xml v e r s i o n=” 1 . 0 ”?>
<drehbuch>
< t i t e l >Ben Hur</ t i t e l >
<akt >
<s z e ne >Ben Hur am Vorabend d e s Wagenrennens .
<!−−D i e s e Szene muß noch a u s g e a r b e i t e t werden.−−>
</s ze ne >
</a kt >
</drehbuch>
Kommentarknoten werden nicht als eigentliche Knoten der Baumstruktur des Dokuments betrachtet.
73
Kapitel 2 Hierarchische Strukturen
2.4.8 Processing Instructions
In einem XML-Dokument können Anweisung stehen, die angeben, was mit einem
Dokument von einem externen Programm zu tun ist. Solche Anweisungen können z.B. angeben, mit welchen Mitteln das Dokument visualisiert werden soll. Wir
werden hierzu im nächsten Kapitel ein Beispiel sehen. Syntaktisch beginnt eine
processing instruction mit <? und endet mit ?>. Dazwischen stehen wie in der Attributschreibweise Werte für den Typ der Anweisung und eine Referenz auf eine
externe Quelle.
Beispiel 2.4.9 Ausschnitt aus dem XML-Dokument diesen Skripts, in dem auf ein
Stylesheet verwiesen wird, daß das Skript in eine HTML-Darstellung umwandelt:
1
2
3
4
<?xml v e r s i o n=” 1 . 0 ”?>
<?xml−s t y l e s h e e t
t y p e=” t e x t / x s l ”
h r e f=” . . / t r a n s f o r m s k r i p t . x s l ”?>
5
6
7
8
9
10
11
<s k r i p t >
<t i t e l s e i t e >
< t i t e l >Grundlagen d e r D a t e n v e r a r b e i t u n g <w h i t e/>I I </ t i t e l >
<s e m e s t e r >WS 02/03</ s e m e s t e r >
</ t i t e l s e i t e >
</ s k r i p t >
Processing Instruction-knoten werden nicht als eigentliche Knoten der Baumstruktur des Dokuments betrachtet.
2.4.9 Attribute
Die Elemente eines XML-Dokuments können als zusätzliche Information auch noch
Attribute haben. Attribute haben einen Namen und einen Wert. Syntaktisch ist ein
Attribut dargestellt durch den Attributnamen gefolgt von einem Gleichheitszeichen
gefolgt von dem in Anführungszeichen eingeschlossenen Attributwert. Attribute stehen im Starttag eines Elements.
Attribute werden nicht als Bestandteils des eigentlichen Textes eines Dokuments
betrachtet.
Beispiel 2.4.10 Dokument mit einem Attribut für ein Element.
1
2
3
<?xml v e r s i o n=” 1 . 0 ”?>
<t e x t >Mehr I n f o r m a t i o n zu XML f i n d e t man a u f den S e i t e n
d e s < l i n k a d d r e s s=”www. w3c . o r g ”>W3C</ l i n k >.</ t e x t >
74
2.4 Das syntaktische XML-Format
Mit Hilfe der Attribute ist es so möglich eine Abbildung an jeden Elementknoten zu
definieren. Wie wir im letzten Semester gelernt haben, ist eine Abbildung eine Liste
von Paaren. Die Paare sind dabei als Schlüssel/Wert-paare zu verstehen. Ein XMLAttribut stellt ein solches Schlüssel/Wert-Paar dar. Vorr dem Gleichheitszeichen
steht der Schlüssel und eingeschlossen in Anführungsstrichen findet sich der Wert,
auf dem der Schlüssel abgebildet wird.
2.4.10 Zeichencodierungen
Im Zusammenhang mit IO-Operationen wurde im letzten Semester bereits eingeführt, dass Textdokumente in unterschiedlichen Encodings physikalisch auf einem
Datenträger gespeichert werden können.
Auch XML ist ein Dokumentenformat, das nicht auf eine Kultur mit einer bestimmten Schrift beschränkt ist, sondern in der Lage ist, alle im Unicode erfassten Zeichen darzustellen, seien es Zeichen der lateinischen, kyrillischen, arabischen, chinesischen oder sonst einer Schrift bis hin zur keltischen Keilschrift. Jedes Zeichen eines
XML-Dokuments kann potentiell eines dieser mehreren zigtausend Zeichen einer
der vielen Schriften sein. In der Regel benutzt ein XML-Dokument insbesondere
im amerikanischen und europäischen Bereich nur wenige kaum 100 unterschiedliche
Zeichen. Auch ein arabisches Dokument wird mit weniger als 100 verschiedenen
Zeichen auskommen.
Wenn ein Dokument im Computer auf der Festplatte gespeichert wird, so werden
auf der Festplatte keine Zeichen einer Schrift, sondern Zahlen abgespeichert. Diese
Zahlen sind traditionell Zahlen die 8 Bit im Speicher belegen, ein sogenannter Byte
(auch Oktett). Ein Byte ist in der Lage 256 unterschiedliche Zahlen darzustellen.
Damit würde ein Byte ausreichen, alle Buchstaben eines normalen westlichen Dokuments in lateinischer Schrift (oder eines arabischen Dokuments darzustellen). Für
ein Chinesisches Dokument reicht es nicht aus, die Zeichen durch ein Byte allein
auszudrücken, denn es gibt mehr als 10000 verschiedene chinesische Zeichen. Es ist
notwendig, zwei Byte im Speicher zu benutzen, um die vielen chinesischen Zeichen
als Zahlen darzustellen.
Die Codierung eines Dokuments gibt nun an, wie die Zahlen, die der Computer
auf der Festplatte gespeichert hat, als Zeichen interpretiert werden sollen. Eine
Codierung für arabische Texte wird den Zahlen von 0 bis 255 bestimmte arabische
Buchstaben zuordnen, eine Codierung für deutsche Dokumente wird den Zahlen 0
bis 255 lateinische Buchstaben inklusive deutscher Umlaute und dem ß zuordnen.
Für ein chinesisches Dokument wird eine Codierung benötigt, die den 65536 mit
2 Byte darstellbaren Zahlen jeweils chinesische Zeichen zuordnet. Man sieht, dass
es Codierungen geben muss, die für ein Zeichen ein Byte im Speicher belegen, und
solche, die zwei Byte im Speicher belegen. Es gibt darüber hinaus auch eine Reihe
Mischformen, manche Zeichen werden durch ein Byte andere durch 2 oder sogar
durch 3 Byte dargestellt. Im Kopf eines XML-Dokuments kann angegeben werden,
75
Kapitel 2 Hierarchische Strukturen
in welcher Codierung das Dokument abgespeichert ist.
Beispiel 2.4.11 Dieses Skript ist in einer Codierung gespeichert, die für westeuropäische Dokumente gut geeignet ist, da es für die verschiedenen Sonderzeichen
der westeuropäischen Schriften einen Zahlenwert im 8-Bit-Bereich zugeordnet hat.
Die Codierung mit dem Namen: iso-8859-1. Diese wird im Kopf des Dokuments
angegeben:
1
2
<?xml v e r s i o n=” 1 . 0 ” e n c o d i n g=” i s o −8859−1” ?>
<s k r i p t ><k a p i t e l >b l a b l a b l a </ k a p i t e l ></s k r i p t >
Wird keine Codierung im Kopf eines Dokuments angegeben, so wird als Standardcodierung die sogenannte utf-8 Codierung benutzt. In ihr belegen lateinische
Zeichen einen Byte und Zeichen anderer Schriften (oder auch das Euro Symbol)
zwei bis drei Bytes. Eine Codierung, in der alle Zeichen mindestens mit zwei Bytes
dargestellt werden ist: utf-16, die Standardabbildung von Zeichen, wie sie im Unicode definiert ist.
2.4.11 DTD
In der XML-Empfehlung integriert ist die Definition der document type description
language, kurz DTD.
Die DTD ermöglicht es zu formulieren, welche Tags in einem Dokument vorkommen
sollen. DTD ist keine eigens für XML erfundene Sprache, sondern aus SGML geerbt.
DTD-Dokumente sind keine XML-Dokumente, sondern haben eine eigene Syntax.
Allerdings kann ein XML Dokument mit einer DTD beginnen. DTD-Abschnitte
zu Beginn des Dokuments sind ein legaler und normaler Bestandteil eines XML
Dokuments.
Wir stellen im einzelnen diese Syntax vor:
• <!DOCTYPE root-element [ doctype-declaration... ]>
Legt den Namen des top-level Elements fest und enthält die gesammte Definition des erlaubten Inhalts.
• <!ELEMENT element-name content-model>
assoziiert einen content model mit allen Elementen, die diesen Namen haben.
content models können wie folgt gebildet werden.:
– EMPTY: Das Element hat keinen Inhalt.
76
2.4 Das syntaktische XML-Format
– ANY: Das Element hat einen beliebigen Inhalt
– #PCDATA: Zeichenkette, also der eigentliche Text.
– durch einen regulären Ausdruck, der aus den folgenden Komponenten
gebildet werden kann.
* Auswahl von Alternativen (oder): (...|...|...)
* Sequenz von Ausdrücken: (...,...,...)
* Option: ...?
* Ein bis mehrfach Wiederholung: ...*
* Null bis mehrfache Wiederholung: ...+
• <!ATTLIST element-name attr-name attr-type attr-default ...>
Liste, die definiert, was für Attribute ein Element hat:
Attribute werden definiert durch:
– CDATA: beliebiger Text ist erlaubt
– (value|...): Aufzählung erlaubter Werte
Attribut default sind:
– #REQUIRED: Das Attribut muß immer vorhanden sein.
– #IMPLIED: Das Attribut ist optional
– "value": Standardwert, wenn das Attribut fehlt.
– #FIXED "value": Attribut kennt nur diesen Wert.
Beispiel 2.4.12 Ein Beispiel für eine DTD, die ein Format für eine Rezeptsammlung definiert.
1
2
<!DOCTYPE c o l l e c t i o n SYSTEM ” c o l l e c t i o n . d t d ” [
<!ELEMENT c o l l e c t i o n ( d e s c r i p t i o n , r e c i p e *)>
3
4
<!ELEMENT d e s c r i p t i o n ANY>
5
6
7
<!ELEMENT r e c i p e ( t i t l e , i n g r e d i e n t * , p r e p a r a t i o n
, comment ? , n u t r i t i o n )>
8
9
<!ELEMENT t i t l e (#PCDATA)>
10
11
12
13
14
<!ELEMENT i n g r e d i e n t ( i n g r e d i e n t * , p r e p a r a t i o n )?>
<!ATTLIST i n g r e d i e n t name CDATA #REQUIRED
amount CDATA #IMPLIED
u n i t CDATA #IMPLIED>
15
16
<!ELEMENT p r e p a r a t i o n ( s t e p )*>
77
Kapitel 2 Hierarchische Strukturen
17
18
<!ELEMENT s t e p (#PCDATA)>
19
20
<!ELEMENT comment (#PCDATA)>
21
22
23
24
25
<!ELEMENT n u t r i t i o n EMPTY>
<!ATTLIST n u t r i t i o n f a t CDATA #REQUIRED
c a l o r i e s CDATA #REQUIRED
a l c o h o l CDATA #IMPLIED>]>
Es ist auch möglich DTDs als externe Dateien zu haben und zu Beginn eines XMLDokuments aus diese zu referenzieren:
1
<!DOCTYPE n o t e SYSTEM ” c o l l e c t i o n . dtd ”>
2.5 XML Verarbeitung
2.5.1 Das DOM Api
Das DOM-API ist in der Standard-Javaentwicklungsumgebung implementiert. Hierzu findet sich im API das Paket org.w3c.dom. Es fällt auf, dass dieses Paket nur
Schnittstellen und keine Klassen enthält. Der Grund hierfür ist, dass es sich bei
DOM um ein implementierungsunabhängiges API handelt. So soll der Programmierer auch gar keine Klassen kennenlernen, die das API implementieren.
Die wichtigsten Schnittstellen
Die zentrale Schnittstelle in dom ist Node. Sie hat als Unterschnittstellen alle Knotentypen, die es in XML gibt. Folgende Graphik gibt über diese Knotentypen einen
Überblick.
1
2
3
4
5
6
7
8
9
i n t e r f a c e o r g . w3c . dom . Node
|
|−− i n t e r f a c e o r g . w3c . dom . Attr
|−− i n t e r f a c e o r g . w3c . dom . CharacterData
|
|
|
|−− i n t e r f a c e o r g . w3c . dom . Comment
|
|−− i n t e r f a c e o r g . w3c . dom . Text
|
|
|
|−− i n t e r f a c e o r g . w3c . dom . CDATASection
78
2.5 XML Verarbeitung
10
11
12
13
14
15
16
17
18
|
|−− i n t e r f a c e
|−− i n t e r f a c e
|−− i n t e r f a c e
|−− i n t e r f a c e
|−− i n t e r f a c e
|−− i n t e r f a c e
|−− i n t e r f a c e
|−− i n t e r f a c e
o r g . w3c . dom . Document
o r g . w3c . dom . DocumentFragment
o r g . w3c . dom . DocumentType
o r g . w3c . dom . Element
o r g . w3c . dom . E n t i t y
o r g . w3c . dom . E n t i t y R e f e r e n c e
o r g . w3c . dom . N o t a t i o n
o r g . w3c . dom . P r o c e s s i n g I n s t r u c t i o n
Wie man sieht, findet sich die Struktur, die wir in unserem eigenen Versuch,
ein XML-Baum-API zu entwickeln, entworfen haben, wieder. Die allgemeine
Schnittstelle Node und deren beiden Unterschnittstellen Element und Text.
Eine der entscheidenen Methoden der Schnittstelle Node selektiert die Liste der
Kinder eines Knotens:
public NodeList getChildNodes()
Knoten, die keine Kinder haben können (Textknoten, Attribute etc.) geben bei
dieser Methode die leere Liste zurück. Attribute zählen auch wie in unserer Modellierung nicht zu den Kindern eines Knotens. Um an die Attribute zu gelangen, gibt
es eine eigene Methode:
NamedNodeMap getAttributes()
Wie man sieht, benutzt Javas dom Umsetzung keine von Javas Listenklassen
zur Umsetzung einer Knotenliste, sondern nur genau die in DOM spezifizierte
Schnittstelle NodeList. Eine NodeList hat genau zwei Methoden:
1
2
i n t getLe ngt h ( )
Node item ( i n t i n d e x )
Mit diesen beiden Methoden lässt sich über die Elemente einer Liste über den Indexzugriff iterieren. Die Schnittstelle NodeList implementiert nicht die Schnittstelle
Iterable. Dieses ist insofern schade, da somit nicht die neue for-each-Schleife aus
Java für die Knotenliste des DOM benutzt werden kann.
Die Schnittstelle Node enthält ebenso die anderen Methoden, die wir uns vorher in
der eigenen Umsetzung überlegt haben:
1
2
3
S t r i n g getNodeName ( ) ;
S t r i n g getNodeValue ( ) ;
s h o r t getNodeType ( ) ;
79
Kapitel 2 Hierarchische Strukturen
Die Methode getNodeType() gibt keine Konstante einer Aufzählung zurück, sondern eine Zahl des Typs short, die den Knotentyp kodiert. In der Schnittstelle Node
sind Konstanten für die einzelnen Knotentypen definiert. Die anderen beiden Methoden verhalten sich so, wie wir es in unserer Minimalumsetzung auch implementiert
haben.
Die Schnittstelle Node des DOM-APIs ist wesentlich umfangreicher als die kleine
Schnittstelle Node, die wir uns zum Einstieg überlegt haben. Insbesondere gibt es
eine Methode, mit der von einem Knoten auch wieder hoch zu seinem Elternknoten
navigiert werden kann.
1
Node getParentNode ( )
Sofern ein Knoten einen Elternknoten besitzt, kann dieser mit dieser Methode erfragt werden. Ansonsten ist das Ergebnis null.
Parser zur Erzeugung von DOM Objekten
Wir benötigen einen Parser, der uns die Baumstruktur eines XML-Dokuments
erzeugt. In der Javabibliothek ist ein solcher Parser integriert, allerdings nur
über seine Schnittstellenbeschreibung. Im Paket javax.xml.parsers gibt es nur
Schnittstellen. Um einen konkreten Parser zu erlangen, bedient man sich einer Fabrikmethode: In der Schnittstelle DocumentBuilderFactory gibt es eine statische
Methode newInstance und über das DocumentBuilderFactory-Objekt, läßt sich
mit der Methode newDocumentBuilder ein Parser erzeugen.
Wir können so eine statischen Methode zum Parsen eines XML-Dokuments
schreiben:
1
package name . p a n i t z . domtest ;
2
3
4
5
import o r g . w3c . dom . Document ;
import j a v a x . xml . p a r s e r s . * ;
import j a v a . i o . F i l e ;
6
7
8
9
10
11
12
13
14
15
16
p u b l i c c l a s s ParseXML {
p u b l i c s t a t i c Document parseXml ( S t r i n g xmlFileName ) {
try {
return
DocumentBuilderFactory
. newInstance ( )
. newDocumentBuilder ( )
. p a r s e ( new F i l e ( xmlFileName ) ) ;
} c a t c h ( E x c e p t i o n _) { r e t u r n n u l l ; }
}
17
80
2.5 XML Verarbeitung
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
System . out . p r i n t l n ( parseXml ( a r g s [ 0 ] ) ) ;
}
18
19
20
21
}
Listing 2.50: ParseXML.java
Wir können jetzt z.B. den Quelltext dieses Skripts parsen.
sep@linux:~/fh/prog4/examples> java -classpath classes/ name.panitz.domtest.ParseXML ../skript.xml
[#document: null]
Wie man sieht ist die Methode toString in der implementierenden Klasse der
Schnittstelle Document, die unser Parser benutzt nicht sehr aufschlußreich.
Aufgabe 1 Im ersten Semester haben wir eine kleine Klasse für Datumsobjekte
entwickelt. Im Java 8 gibt es es ein neues Paket java.time. Hier finden sich
Klassen um Datums und Uhrzeitobjekte auszudrücken.
a) Machen Sie sich mit dem neuen Api vertraut. Schreiben Sie ein kleines
Tutorial von wenigen Seiten und bereiten einen 5-10 minütigen Vortrag
darüber vor. In der Praktikumsstunde werden Kandidaten ausgelost, die
den Vortrag halten sollen. Die beste Ausarbeitung soll als Kapitel in
diesem Skript integriert werden.
b) Schreiben Sie eine Klasse Agenda. Diese soll Einträge in einem Terminkalender darstellen. Sie ist eine Liste von Terminen. Ein Termin hat
einen Zeitpunkt, eine Beschreibung, eine Dauer und einen Ort. Zusätzlich soll die Klasse die Möglichkeit haben, den Terminkalender als XML
Dokument abzuspeichern und wieder zu lesen.
Aufgabe 1 (Unbewertete Zusatzaufgabe)
Schreiben Sie eine Klasse XMLIter, die die Schnittstelle Iterable implementiert. Sie soll im Konstruktor einen Dom-Knoten erhalten. Bei der Iteration
sollen die Kindknoten des Dom-Knotens durchlaufen werden.
2.5.2 XPath
Für Bäume stellen Pfade ein wichtiges Konzept dar. Entlang der Pfade navigiert
man durch einen Baum. Anhand eines Pfades können Teilbäume selektiert werden.
Deshalb hat das W3C eine Empfehlung heraus gegeben, mit deren Hilfe Pfade in
XML-Dokumenten beschrieben werden können. Die Empfehlung für XPath.
Eines der wichtigsten Konzepten von XPath sind die sogenannten Achsen. Sie
beschreiben bestimmte Arten sich in einem Baum zu bewegen. XPath kennt 12
81
Kapitel 2 Hierarchische Strukturen
Achsen. Eine Achse beschreibt ausgehend von einem Baumknoten eine Liste von
Knoten, die diese Achse definiert. Die am häufigste verwendete Achse ist, die Kinderachse. Sie beschreibt die Liste aller direkten Kinder eines Knotens. Entsprechend
gibt es die Elternachse, die den Elternknoten beschreibt. Diese Liste hat maximal
einen Knoten. Eine sehr simple Achse beschreibt den Knoten selbst. Diese Achse hat
immer eine einelementige Liste als Ergebnis. Achsen für Vorfahren und Nachkommen sind der transitive Abschluss der Eltern- bzw. Kinderachse. Zusätzlich gibt es
noch Geschwisterachsen, eine für die Geschwister, die vor dem aktuellen Knoten
stehen und einmal für die Geschwister, die nach dem aktuellen Knoten stehen. Eine
eigene Achse steht für Attribute. Schließlich gibt es noch eine Achse für Namensraumdeklarationen.
Mit einem Aufzählungstypen, lässt sich in Java einfach ein Typ, der alle 12 Achsen
aufzählt, implementieren.
1
2
3
4
5
package name . p a n i t z . xml . xpath ;
import name . p a n i t z . pmt . i t e r a t i o n . I n t e g e r R a n g e ;
import j a v a . u t i l . L i s t ;
import j a v a . u t i l . A r r a y L i s t ;
import o r g . w3c . dom . * ;
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
p u b l i c enum Axes
{self
, child
, descendant
, descendant_or_self
, parent
, ancestor
, ancestor_or_self
, following_sibling
, following
, preceding_sibling
, preceding
, attribute ;
Listing 2.51: Axes.java
Für jede dieser Achsen soll in der Folge eine Implementierung auf DOM gegeben
werden. Eine Achsenimplementierung entspricht dabei einer Funktion, die für einen
Knoten eine Liste von Knoten zurück gibt.
Da das DOM-API bereits eine eigene Listenimplementierung hat, wir aber bei
den Achsen auf Java-Standardlisten zurückgreifen wollen, sei hier eine simple Konvertierungsmethode von org.w3c.dom.NodeList auf java.util.List gegeben.
22
23
p u b l i c s t a t i c L i s t <Node> n o d e l i s t T o L i s t ( NodeList n l ) {
82
2.5 XML Verarbeitung
L i s t <Node> r e s u l t = new A r r a y L i s t <Node >() ;
f o r ( i n t i : new I n t e g e r R a n g e ( 0 , n l . ge t L en g th ( ) −1) ) {
r e s u l t . add ( n l . item ( i ) ) ;
}
return r e s u l t ;
24
25
26
27
28
29
}
Listing 2.52: Axes.java
Die Achse self
Die einfachste Achse liefert genau den Knoten selbst. Die entsprechende Methode
liefert die einelementige Liste des Eingabeknotens.
Wir geben für jede Achse zwei Methoden, um deren Ergebnisliste zu erhalten. Die
erste Methode erstellt die Ergebnisliste.
30
31
32
p u b l i c s t a t i c L i s t <Node> s e l f ( Node n ) {
r e t u r n s e l f ( n , new A r r a y L i s t <Node >() ) ;
}
Listing 2.53: Axes.java
Die zweite bekommt die Ergebnisliste als Parameter übergeben und fügt in diese
die Knoten ein.
33
34
35
36
p u b l i c s t a t i c L i s t <Node> s e l f ( Node n , L i s t <Node> r e s u l t ) {
r e s u l t . add ( n ) ;
return r e s u l t ;
}
Listing 2.54: Axes.java
Auf diese Weise kann verhindert werden, dass wenn die Vereinigung mehrerer
Achsen berechnet werden soll, oder wenn die Berechnung einer Achse einen rekursive
Methode darstellt, unnötig Zwischenlisten erzeugt werden.
Die Achse child
Die gebräuchlichste Achse beschreibt die Menge aller Kinderknoten. Wir lassen
uns die Kinder des DOM Knotens geben und konvertieren die NodeList zu einer
Javaliste.
Auch hier werden wieder zwei Versionen der Methode definiert. Einmal wird eine
Liste für das ergebnis neu erzeugt, im zweiten Fall wird eine Liste, die die Ergebnisknoten sammelt als Parameter übergeben.
83
Kapitel 2 Hierarchische Strukturen
p u b l i c s t a t i c L i s t <Node> c h i l d ( Node n ) {
r e t u r n c h i l d ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> c h i l d ( Node n , L i s t <Node> r e s u l t ) {
NodeList n l = n . g e t C h i l d N o d e s ( ) ;
f o r ( i n t i : new I n t e g e r R a n g e ( 0 , n l . get Le n gt h ( ) −1) ) {
r e s u l t . add ( n l . item ( i ) ) ;
}
return r e s u l t ;
}
37
38
39
40
41
42
43
44
45
46
Listing 2.55: Axes.java
Die Achse descendant
Die Achse der Nachkommen eines Knotens beschreibt die Liste aller Kinder, Kindeskinder usw. also aller Knoten, die Unterhalb des Ausgangsknotens liegen. Diese
sollen in die Ergebnisliste in preorder eingefügt werden.
p u b l i c s t a t i c L i s t <Node> d e s c e n d a n t ( Node n ) {
r e t u r n d e s c e n d a n t ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> d e s c e n d a n t ( Node n , L i s t <Node> r e s u l t ) {
f o r ( Node c h i l d : c h i l d ( n ) ) {
r e s u l t . add ( c h i l d ) ;
descendant ( child , r e s u l t ) ;
}
return r e s u l t ;
}
47
48
49
50
51
52
53
54
55
56
Listing 2.56: Axes.java
Die Achse descendant-or-self
In der Nachkommenachse taucht der Knoten selbst nicht auf. Diese Achse fügt den
Knoten selbst zusätzlich vorne ans Ergebnis an. Es ist also die Vereinigung der
Achse self mit der Achse descendant.
p u b l i c s t a t i c L i s t <Node> d e s c e n d a n t O r S e l f ( Node n ) {
r e t u r n d e s c e n d a n t O r S e l f ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> d e s c e n d a n t O r S e l f ( Node n , L i s t <Node> r e s u l t )
{
descendant (n , r e s u l t ) ;
r e s u l t . add ( 0 , n ) ;
return r e s u l t ;
}
57
58
59
60
61
62
63
64
84
2.5 XML Verarbeitung
Listing 2.57: Axes.java
Die Achse parent
Die Kinderachse lief in den Baum Richtung Blätter eine Ebene nach unten, die
Elternachse läuft diese eine Ebene Richtung Wurzel nach oben. Die Ergebnisliste
hat maximal ein Element, eventuell 0 Elemente, wenn der Knoten selbst als Elternknoten null zurück gibt.
65
66
67
68
69
70
71
72
73
p u b l i c s t a t i c L i s t <Node> p a r e n t ( Node n ) {
r e t u r n p a r e n t ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> p a r e n t ( Node n , L i s t <Node> r e s u l t ) {
Node pa = n . getParentNode ( ) ;
i f ( pa!= n u l l && pa . getNodeType ( ) !=Node .DOCUMENT_NODE)
r e s u l t . add ( pa ) ;
return r e s u l t ;
}
Listing 2.58: Axes.java
Die Achse ancestor
Die Vorfahrenachse ist der transitive Abschluß über die Elternachse, sie bezeichnet also die Eltern und deren Vorfahren. Diese Achse enthält also auf jeden Fall
den Wurzelknoten. Sie enthält tatsächlich den Pfad von der Wurzel zum Ausgangsknoten, allerdings ohne den Ausgangsknoten. Dieser ist erst in der Achse
ancestor-or-self des nächsten Punktes enthalten.
74
75
76
77
78
79
80
81
82
83
p u b l i c s t a t i c L i s t <Node> a n c e s t o r ( Node n ) {
r e t u r n a n c e s t o r ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> a n c e s t o r ( Node n , L i s t <Node> r e s u l t ) {
f o r ( Node pa : p a r e n t ( n ) ) {
r e s u l t . add ( pa ) ;
a n c e s t o r ( pa , r e s u l t ) ;
}
return r e s u l t ;
}
Listing 2.59: Axes.java
85
Kapitel 2 Hierarchische Strukturen
Die Achse ancestor-or-self
Ebenso wie für die Achse aller Nachkommen ist auch für die Achse aller Vorfahren
eine Version definiert, in der der Ausgangsknoten selbst noch enthalten ist.
p u b l i c s t a t i c L i s t <Node> a n c e s t o r O r S e l f ( Node n ) {
r e t u r n a n c e s t o r O r S e l f ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> a n c e s t o r O r S e l f ( Node n , L i s t <Node> r e s u l t ) {
ancestor (n , r e s u l t ) ;
r e s u l t . add ( n ) ;
return r e s u l t ;
}
84
85
86
87
88
89
90
91
Listing 2.60: Axes.java
Die Achse following-sibling
Auch für die Geschwister eines Knotens sind Achsen definiert. Geschwister sind
Knoten, die denselben Elternknoten haben. Die following-sibling-Achse definiert
alle Geschwister, die in der Kinderliste meines Elternknotens nach mir kommen.
Sozusagen alle jüngeren Geschwister.
p u b l i c s t a t i c L i s t <Node> f o l l o w i n g S i b l i n g ( Node n ) {
r e t u r n f o l l o w i n g S i b l i n g ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> f o l l o w i n g S i b l i n g ( Node n , L i s t <Node> r e s u l t )
{
Node s i b = n . g e t N e x t S i b l i n g ( ) ;
w h i l e ( s i b != n u l l ) {
r e s u l t . add ( s i b ) ;
sib = sib . getNextSibling () ;
}
return r e s u l t ;
}
92
93
94
95
96
97
98
99
100
101
102
Listing 2.61: Axes.java
Die Achse preceding-sibling
Die preceding-sibling-Achse definiert alle Geschwister, die in der Kinderliste meines
Elternknotens vor mir kommen. Sozusagen alle älteren Geschwister.
p u b l i c s t a t i c L i s t <Node> p r e c e d i n g S i b l i n g ( Node n ) {
r e t u r n p r e c e d i n g S i b l i n g ( n , new A r r a y L i s t <Node >() ) ;
}
103
104
105
86
2.5 XML Verarbeitung
106
107
108
109
110
111
112
113
p u b l i c s t a t i c L i s t <Node> p r e c e d i n g S i b l i n g ( Node n , L i s t <Node> r e s u l t )
{
Node s i b = n . g e t P r e v i o u s S i b l i n g ( ) ;
w h i l e ( s i b != n u l l ) {
r e s u l t . add ( s i b ) ;
sib = sib . getPreviousSibling () ;
}
return r e s u l t ;
}
Listing 2.62: Axes.java
Die Achse following
Die Achse following bezieht sich auf die following-sibling-Achse. Sie bezeichnet alle
following-sibling-Knoten und deren Nachkommen, sowie dann noch alle weiteren
following-siblings der Vorfahren und wieder deren Nachkommen. Textuell bezeichnet diese Achse alle Knoten des XML-Baums, die beginnen, nachdem der Ausgangsknoten beendet ist. Daher auch der Name, alle Knoten die folgen. Mathematisch lässt sich die Achse wie folgt beschreiben:
following(n) = {f |a ∈ ancestor-or-self(n), s ∈ following-sibling(a), f ∈ descendant-or-self(s)}
114
115
116
117
118
119
120
121
122
123
p u b l i c s t a t i c L i s t <Node> f o l l o w i n g ( Node n ) {
r e t u r n f o l l o w i n g ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> f o l l o w i n g ( Node n , L i s t <Node> r e s u l t ) {
f o r ( Node a : a n c e s t o r O r S e l f ( n ) )
f o r ( Node s : f o l l o w i n g S i b l i n g ( a ) )
f o r ( Node f : d e s c e n d a n t O r S e l f ( s ) )
r e s u l t . add ( f ) ;
return r e s u l t ;
}
Listing 2.63: Axes.java
Es werden also von allen Vorfahren und dem Knoten selbst die nachfolgenden
Geschwister berechnet und diese und deren Nachkommen genommen.
Die Achse preceding
Entsprechend bezeichnet die Achse preceding von allen Vorfahren die precedingsibling-Knoten und deren Nachkommen. Dieses bedeutet, dass genau die Knoten
beschrieben werden, die in der textuellen Darstellung beendet wurden, bevor der
Ausgangsknoten begonnen hat..
87
Kapitel 2 Hierarchische Strukturen
preceding(n) = {f |a ∈ ancestor-or-self(n), s ∈ preceding-sibling(a), f ∈ descendant-or-self(s)}
Es werden also von allen Vorfahren und dem Knoten selbst die vorhergehenden
Geschwister berechnet und diese und deren Nachkommen genommen.
p u b l i c s t a t i c L i s t <Node> p r e c e d i n g ( Node n ) {
r e t u r n p r e c e d i n g ( n , new A r r a y L i s t <Node >() ) ;
}
p u b l i c s t a t i c L i s t <Node> p r e c e d i n g ( Node n , L i s t <Node> r e s u l t ) {
f o r ( Node a : a n c e s t o r O r S e l f ( n ) )
f o r ( Node s : p r e c e d i n g S i b l i n g ( a ) )
f o r ( Node f : d e s c e n d a n t O r S e l f ( s ) )
r e s u l t . add ( f ) ;
return r e s u l t ;
}
124
125
126
127
128
129
130
131
132
133
Listing 2.64: Axes.java
Damit sind alle Achsen beschrieben, sie sich auf die Baumstruktur des Dokuments
beziehen. Die noch ausbleibende Achse bezieht sich auf die Attribute eines XMLDokuments, die wir nicht als Teil der Baumstruktur gesehen haben. Alle Knoten
eines Dokuments lassen sich durch die Vereinigung der Achsen preceding, ancestor,
self, descendant und followong selektieren. Man kann auch als Faustregel sagen, es
sind die vier Richtungen, die von einem Baumknoten beschritten werden können:
links, über , unter und rechts. preceding sind die Knoten links von mir, ancestor
sind die Knoten über mir, descendant sind die Knoten unter mir und following die
Knoten rechts von mir.
Die Achse attribute
Die bisherigen Achsen selektierten Knoten aus der Baumhierarchie eines XMLDokuments. Die letzten beiden Achsen beziehen sich auf die Attribute. Diese sind
nicht teil der Kindknoten eines Elements. Daher werden über die bisherigen Achsen
niemals Attribute selektiert. Hierfür gibt es die besondere Achse attribute, die die
Liste aller Attribute eines Knoten selektiert.
p u b l i c s t a t i c L i s t <Node> a t t r i b u t e ( Node n ) {
L i s t <Node> r e s u l t = new A r r a y L i s t <Node >() ;
NamedNodeMap n l = n . g e t A t t r i b u t e s ( ) ;
i f ( n u l l != n l ) {
f o r ( i n t i : new I n t e g e r R a n g e ( 0 , n l . g etL e ng t h ( ) −1) ) {
r e s u l t . add ( n l . item ( i ) ) ;
}
}
return r e s u l t ;
134
135
136
137
138
139
140
141
142
88
2.5 XML Verarbeitung
}
143
144
p u b l i c L i s t <Node> s e l e c t ( Node n ) {
switch ( t h i s ) {
case child : return child (n) ;
case descendant : return descendant (n) ;
case descendant_or_self : return descendantOrSelf (n) ;
case parent : return parent (n) ;
case ancestor : return ancestor (n) ;
case ancestor_or_self : return ancestorOrSelf (n) ;
case following_sibling : return followingSibling (n) ;
case preceding_sibling : return precedingSibling (n) ;
case following : return following (n) ;
case preceding : return preceding (n) ;
case attribute : return attribute (n) ;
default : return s e l f (n) ;
}
}
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
}
Listing 2.65: Axes.java
Knotentest
Die Kernausdrücke in XPath sind von der Form:
axis::nodeTest
axis beschreibt dabei eine der 12 Achsen. nodeTest ermöglicht es, aus den durch die
Achse beschriebenen Knoten bestimmte Knoten zu selektieren. Syntaktisch gibt es
8 Arten des Knotentests:
• *: beschreibt Elementknoten mit beliebigen Namen.
• pref:*: beschreibt Elementknoten mit dem Prefix pref und beliebigen weiteren
Namen.
• pref:name: beschreibt Elementknoten mit dem Prefix pref und den weiteren
Namen name. Der Teil vor den Namen kann dabei auch fehlen.
• comment(): beschreibt Kommentarknoten.
• text(): beschreibt Textknoten.
• processing-instruction(): beschreibt Processing-Instruction-Knoten.
• processing-instruction(target): beschreibt Processing-Instruction-Knoten
mit einem bestimmten Ziel.
• node(): beschreibt beliebige Knoten.
Ein XPath-Ausdruck bezieht sich immer auf einen Ausgabgsknoten im Dokument.
Die Bedeutung eines XPath-Ausdrucks
89
Kapitel 2 Hierarchische Strukturen
axisType::nodeTest
ist dann:
Selektiere vom Ausgangsknoten alle Knoten mit der durch axisType angegebenen
Achse. Von dieser Liste selektiere dann alle Knoten, für die der Knotentest erfolgreich ist.
Der folgende Ausdruck selektiert alle Nachkommen des Ausgangsknoten, die den
Elementknoten mit den Tagnamen code sind:
descendant-or-self::code
Dieses sind in dem XML-Dokument, das dieses Skript beschreibt gerade alle Teile,
in denen Java Quelltext steht.
Mehrere XPath-Ausdrücke können laut der XPath Definition nacheinander ausgeführt werden. Hierzu schreibt man zwischen den einzelnen XPath-Audrücken einen
Schrägstrich. Der so zusammengesetzte Ausdruck selektiert wieder eine Liste von
Knoten ausgehend von einem Ausgangsknoten. Die Bedeutung von einer solchen
Verkettung von XPath-Ausdrücken ist:
Selektiere erst mit dem ersten XPath-Teilausdruck ausgehend von einem Startknoten die Liste der Ergebnisknoten. Dann nehme jeden der Knoten in dieser Ergebnisliste als neuen Ausgangsknoten für den zweiten XPath-Ausdruck und vereinige
deren Ergebnissse.
Für das Beispiel des XML-Dokuments dieses Skriptes bezeichnet der Ausdruck
child::kapitel/child::section/descendant::code/child:text()
Den Text, der direkt unterhalb eines code Elements in steht, wobei das code Element Nachfahre eines section Elements ist, welches direktes Kind eines kapitel
Elements ist, welches wiederum ein Kind des Ausgangsknotens ist.
Implementierung Nachdem wir bereits alle Achsen selbst implementiert haben,
sollen auch die Knotentests implementiert werden. Hierzu können wir eine kleine
Schnittstelle vorsehen, die einen Knotentest ausdrückt. Diese ist nichts weiter als
ein für Knoten spezialisiertes Prädikat, so dass wir die allgemeine Schnittstelle
Predicate erweitern können
1
2
3
4
package name . p a n i t z . xml . xpath ;
import o r g . w3c . dom . Node ;
import o r g . w3c . dom . Text ;
import j a v a . u t i l . f u n c t i o n . P r e d i c a t e ;
5
6
p u b l i c i n t e r f a c e NodeTest e x t e n d s P r e d i c a t e <Node>{
Listing 2.66: NodeTest.java
90
2.5 XML Verarbeitung
Die einfachen Tests, ob es sich um Text, Kommentar oder einen beliebigen Knoten
handelt, lassen sich über einen Lambda-Ausdruck definieren.
s t a t i c f i n a l NodeTest commentTest = n−>n . getNodeType ( ) == Node .
COMMENT_NODE;
s t a t i c f i n a l NodeTest t e x t T e s t = n−>n i n s t a n c e o f Text ;
s t a t i c f i n a l NodeTest someNodeTest = n−>t r u e ;
7
8
9
10
}
Listing 2.67: NodeTest.java
Für die weiteren Knotentest können Implementierungen dieser Schnittstelle vorgesehen werden. Die erste Klasse vereinigt die drei Knotentest, die sich auf die Elementknoten beziehen:
1
2
package name . p a n i t z . xml . xpath ;
import o r g . w3c . dom . Node ;
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
p u b l i c c l a s s NameTest implements NodeTest {
String prefix ;
S t r i n g name ;
p u b l i c NameTest ( S t r i n g p r e f i x , S t r i n g name ) {
this . prefix = prefix ;
t h i s . name = name ;
}
p u b l i c NameTest ( S t r i n g name ) {
this . prefix = null ;
t h i s . name = name ;
}
p u b l i c NameTest ( ) {
this . prefix = null ;
t h i s . name = n u l l ;
}
19
p u b l i c b o o l e a n t e s t ( Node n ) {
i f ( n . getNodeType ( ) != Node .ELEMENT_NODE
&& n . getNodeType ( ) != Node .ATTRIBUTE_NODE) r e t u r n f a l s e ;
20
21
22
23
i f ( n u l l ==p r e f i x && n u l l==name ) r e t u r n t r u e ;
i f ( n u l l==p r e f i x ) r e t u r n n . getNodeName ( ) . e q u a l s ( name ) ;
i f ( n u l l==name ) r e t u r n n . g e t P r e f i x ( ) . e q u a l s ( p r e f i x ) ;
r e t u r n n . g e t P r e f i x ( ) . e q u a l s ( p r e f i x ) && n . getLocalName ( ) . e q u a l s (
name ) ;
24
25
26
27
}
@Override p u b l i c S t r i n g t o S t r i n g ( ) {
i f ( n u l l ==p r e f i x && n u l l==name ) r e t u r n ” * ” ;
i f ( n u l l==p r e f i x ) r e t u r n name ;
i f ( n u l l==name ) r e t u r n p r e f i x+” : * ” ;
r e t u r n p r e f i x+” : ”+name ;
}
28
29
30
31
32
33
34
35
}
91
Kapitel 2 Hierarchische Strukturen
Listing 2.68: NameTest.java
Die folgende Klasse realisiert die Knotentests, die sich auf precessing instructions
beziehen.
1
2
3
package name . p a n i t z . xml . xpath ;
import o r g . w3c . dom . Node ;
import o r g . w3c . dom . P r o c e s s i n g I n s t r u c t i o n ;
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
p u b l i c c l a s s P r o c e s s i n g I n s t r u c t i o n T e s t implements NodeTest {
String target ;
public ProcessingInstructionTest () {
target = null ;
}
public ProcessingInstructionTest ( String target ){
this . target = target ;
}
p u b l i c b o o l e a n t e s t ( Node n ) {
i f ( n . getNodeType ( ) != Node .PROCESSING_INSTRUCTION_NODE) r e t u r n
false ;
i f ( n u l l != t a r g e t ) {
return target . equals ( ( ( Pr oc es si n gI ns tr uc t io n )n) . getTarget () ) ;
}
return true ;
}
@Override p u b l i c S t r i n g t o S t r i n g ( ) {
i f ( n u l l==t a r g e t ) r e t u r n ” p r o c e s s i n g −i n s t r u c t i o n ( ) ” ;
r e t u r n ” p r o c e s s i n g −i n s t r u c t i o n ( ”+t a r g e t+” ) ” ;
}
}
Listing 2.69: ProcessingInstructionTest.java
Mit den Achsen von XPath und den Knotentests, lassen sich nun XPath-Ausdrücke
als Paar von einer Achse und einem Test darstellen.
1
2
3
4
package name . p a n i t z . xml . xpath ;
import o r g . w3c . dom . Node ;
import j a v a . u t i l . L i s t ;
import j a v a . u t i l . A r r a y L i s t ;
5
6
7
8
p u b l i c c l a s s XPathExpr{
Axes a x i s ;
NodeTest t e s t ;
9
p u b l i c XPathExpr ( Axes a x i s , NodeTest t e s t ) {
this . axis = axis ;
this . test = test ;
}
10
11
12
13
92
2.5 XML Verarbeitung
p u b l i c L i s t <Node> s e l e c t ( Node n ) {
L i s t <Node> r e s u l t = new A r r a y L i s t <>() ;
f o r ( Node x : a x i s . s e l e c t ( n ) ) {
i f ( t e s t . t e s t ( x ) ) r e s u l t . add ( x ) ;
}
return r e s u l t ;
}
@Override p u b l i c S t r i n g t o S t r i n g ( ) {
r e t u r n a x i s+” : : ”+t e s t ;
}
14
15
16
17
18
19
20
21
22
23
24
}
Listing 2.70: XPathExpr.java
Ein ganzer XPath-Ausdruck stellt eine Folge von einfachen Ausdrücken da, die
nacheinander zur Selektion benutzt werden.
1
2
3
4
package name . p a n i t z . xml . xpath ;
import o r g . w3c . dom . Node ;
import j a v a . u t i l . L i s t ;
import j a v a . u t i l . A r r a y L i s t ;
5
6
7
8
9
10
11
12
13
14
15
16
17
18
p u b l i c c l a s s XPath e x t e n d s A r r a y L i s t <XPathExpr>{
p u b l i c L i s t <Node> s e l e c t ( Node n ) {
L i s t <Node> r e s u l t = new A r r a y L i s t <>() ;
r e s u l t . add ( n ) ;
f o r ( XPathExpr xpath : t h i s ) {
L i s t <Node> r e s u l t 2 = new A r r a y L i s t <>() ;
f o r ( Node x : r e s u l t ) {
r e s u l t 2 . addAll ( xpath . s e l e c t ( x ) ) ;
}
result = result2 ;
}
return r e s u l t ;
}
19
@Override p u b l i c S t r i n g t o S t r i n g ( ) {
S t r i n g B u f f e r r e s u l t = new S t r i n g B u f f e r ( ) ;
boolean f i r s t = true ;
f o r ( XPathExpr xpath : t h i s ) {
i f ( f i r s t ){
first = false ;
}else {
r e s u l t . append ( ” / ” ) ;
}
r e s u l t . append ( xpath . t o S t r i n g ( ) ) ;
}
return r e s u l t . toString () ;
}
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
}
93
Kapitel 2 Hierarchische Strukturen
Listing 2.71: XPath.java
Aufgabe 1 Experimentieren Sie mit der kleinen XPath-Implementierung
dieses
Kapitels.
Benutzen
Sie
dazu
das
http://www.cs.hsrm.de/ panitz/java/skript.xmlXML-Dokument des Quelltextes vom kompletten Java-Skriptes von der Webseite.
Schreiben Sie hierzu ein Testprogramm, das aus den Elementen anhang alle
code Nachkommen und davon alle Text-Nachkommen selektiert und deren
Values schließlich auf der Kommandozeile ausgibt. Es soll also folgender Ausdruck auf das Wurzelelement ausgeführt werden: ./anhang//code//text().
Abkürzende Schreibweise
In obiger Einführung haben wir bereits gesehen, dass XPath den aus dem Dateissystem bekannten Schrägstrich für Pfadangaben benutzt. Betrachten wir XPathAusdrücke als Terme, so stellt der Schrägstrich einen Operator dar. Diesen Operator
gibt es sowohl einstellig wie auch zweistellig.
Die Grundsyntax in XPath sind eine durch Schrägstrich getrennte Folge von Kernausdrücke mit Achsentyp und Knotentest.
Der Ausdruck
child::skript/child::*/descendant::node()/self::code/attribute::class
beschreibt die Attribute mit Attributnamen class, die an einem Elementknoten mit
Tagnamen code hängen, die Nachkommen eines beliebigen Elementknotens sind,
die Kind eines Elementknotens mit Tagnamen skript sind, die Kind des aktuellen
Knotens, auf dem der Ausdruck angewendet werden soll sind.
Vorwärts gelesen ist dieser Ausdruck eine Selektionsanweisung:
Nehme alle skript-Kinder des aktuellen Knotens. Nehme von diesen beliebige
Kinder. Nehme von diesen alle code-Nachkommen. Und nehme von diesen jeweils
alle class-Attribute.
Der einstellige Schrägstrichoperator bezieht sich auf die Dokumentwurzel des aktuellen Knotens.
Aufgabe 1 Angewandte Informatik. Abgabe in der Woche vom 16.6.
Implementieren Sie die Funktionalität der Header-Datei ArrayList.h gemäß
der dokumentierten Spezifikation.
(http://www.cs.hs-rm.de/ panitz//pmt/blatt7/ArrayList.h)
94
2.5 XML Verarbeitung
Eine kleines Programm mit Beispielaufrufen steht Ihnen zur Verfügung:
TestArrayList.c.
(http://www.cs.hs-rm.de/ panitz/pmt/blatt7/TestArrayList.c)
Zusätzlich steht Ihnen die Dokumentation im html-Format zur Verfügung.
(http://www.cs.hs-rm.de/ panitz/pmt/blatt7/html/index.html)
Der doppelte Schrägstrich XPath kennt einen zweiten Pfadoperator //. Auch er
existiert jeweils einmal einstellig und einmal zweistellig. Der doppelte Schrägstrich
ist eine abkürzende Schreibweise, die übersetzt werden kann in Pfade mit einfachen
Schrägstrichoperator.
• //expr: Betrachte beliebige Knoten unterhalt des Dokumentknotens, die
durch expr charakterisiert werden.
• e1 //e2 : Betrachte beliebiege Knoten unterhalb der durch e1 charkterisierten
Knoten.und prüfe diese auf e2 .
Der
Doppelschrägstrich
ist
/descendant-or-self::node()/
eine
abkürzende
Schreibweise
für:
Der Punkt Für die Selbstachse kann als abkürzende Schreibweise ein einfacher
Punkt . gewählt werden, wie er aus den Pfadangaben im Dateisystem bekannt ist.
Der Punkt . ist die abkürzende Schreibweise für: self::node().
Der doppelte Punkt Für die Elternachse kann als abkürzende Schreibweise ein
doppelter Punkt .. gewählt werden, wie er aus den Pfadangaben im Dateisystem
bekannt ist.
Der Punkt .. ist die abkürzende Schreibweise für: parent::node().
Vereinfachte Kindachse Ein Kernausdruck, der Form child::nodeTest kann
abgekürzt werden durch nodeTest. Die Kinderachse ist also der Standardfall.
Vereinfachte Attributachse Auch für die Attributachse gibt es eine abkürzende
Schreibweise: ein Kernausdruck, der Form attribute::pre:name kann abgekürzt
werden durch @pre:name.
Insgesamt läßt sich der obige Ausdruck abkürzen zu: skript/*//code/@class
95
Kapitel 2 Hierarchische Strukturen
XPath-Auswertung in Java
Im Java-API ist eine XPath-Implementierung enthalten. Die dazugehörigen Klassen
befinden sich im Paket javax.xml.xpath. Die Implementierung basiert auf DOM,
es werden also DOM-Knoten als Eingabe und genommen und auch u.a. eine DOMNodelist als Ergebnis berechnet.
Im Folgendem ein Beispiel, wie ein XPath-Ausdruck, der eine Knotenliste als Ergebnis hat, mit dem Java-API für XPath angewendet wird. Wir schreiben eine statische Methode, die einen XPath-Ausdruck als String übergeben bekommt und das
Eingabedokument für diesen Ausdruck als einen Reader.
1
package name . p a n i t z . xml ;
2
3
4
5
import j a v a . i o . FileNotFoundException ;
import j a v a . i o . F i l e R e a d e r ;
import j a v a . i o . Reader ;
6
7
8
9
10
import
import
import
import
j a v a x . xml . xpath . XPath ;
j a v a x . xml . xpath . XPathConstants ;
j a v a x . xml . xpath . XPathExpressionException ;
j a v a x . xml . xpath . XPathFactory ;
11
12
13
import o r g . w3c . dom . NodeList ;
import o r g . xml . sax . I n p u t S o u r c e ;
14
15
16
17
p u b l i c c l a s s XPathTest {
p r i v a t e s t a t i c NodeList evalXPath ( S t r i n g x p a t h E x p r e s s i o n , Reader
reader )
throws XPathExpressionException {
Listing 2.72: XPathTest.java
Zunächst einmal benutzt das XPath-API wieder das Factory-Pattern, so dass ein
XPath-Ausdruck nicht direkt durch einem Konstruktoraufruf erzeugt wird, sondern
durch eine Methode in einer sogenannten Factory-Klasse, ebenso wie auch der DOMParser erzeut wurde.
XPathFactory f a c t = XPathFactory . n e w I n s t a n c e ( ) ;
XPath xpath = f a c t . newXPath ( ) ;
18
19
Listing 2.73: XPathTest.java
Die Schnittstelle XPath kennt eine Methode evaluate, die drei Parameter hat:
• der auszuwertende XPath-Ausdruck als String.
• die Eingabequelle für das XML-Dokuement, auf dem dieser Ausdruck ausgewertet werden soll.
96
2.5 XML Verarbeitung
• eine Konstante, die das erwartete Ergebnis anzeigt. Für die meisten XPathAusdrücke wird ein Objekt des Typs Nodelist als Ergebnis erwartet. Es gibt
aber auch XPath-Ausdrücke, die nur Strings, oder Zahlen als Ergebnis haben.
Das Ergebnis der Methode evaluate ist nur vom Typ Object und muss
entsprechend erst noch mit einer Typzusicherung auf den eigentlichen Ergebnistypen
überprüft werden.
In unserem Beispiel gehen wir davon aus, dass das Ergebnis eine Nodelist ist. Falls
dieses nicht der Fall ist, wird eine Ausnahme geworfen.
NodeList ns = ( NodeList ) xpath . e v a l u a t e ( x p a t h E x p r e s s i o n ,
new I n p u t S o u r c e ( r e a d e r ) , XPathConstants .NODESET) ;
r e t u r n ns ;
20
21
22
}
23
Listing 2.74: XPathTest.java
Abschließend ein kleiner Testaufruf dieser Methode, der aus der XML-Datei, die
diesem Skript zugrunde liegt, alle code-Elemente selektiert:
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws
XPathExpressionException ,
FileNotFoundException {
S t r i n g x p a t h E x p r e s s i o n = ” // code ” ;
Reader r e a d e r = new F i l e R e a d e r ( ” s k r i p t . xml ” ) ;
24
25
26
27
28
NodeList ns = evalXPath ( x p a t h E x p r e s s i o n , r e a d e r ) ;
f o r ( i n t i = 0 ; i < ns . g et L eng t h ( ) ; i ++) {
System . out . p r i n t l n ( ns . item ( i ) . getTextContent ( ) ) ;
}
29
30
31
32
}
33
34
}
Listing 2.75: XPathTest.java
2.5.3 XSLT
XML-Dokumente enthalten keinerlei Information darüber, wie sie visualisiert werden sollen. Hierzu kann man getrennt von seinem XML-Dokument ein sogenanntes
Stylesheet schreiben. XSL ist eine Sprache zum Schreiben von Stylesheets für XMLDokumente. XSL ist in gewisser Weise eine Programmiersprache, deren Programme
eine ganz bestimmte Aufgabe haben: XML Dokumente in andere XML-Dokumente
zu transformieren. Die häufigste Anwendung von XSL dürfte sein, XML-Dokumente
in HTML-Dokumente umzuwandeln.
Wir werden die wichtigsten XSLT-Konstrukte mit folgendem kleinem XML Dokument ausprobieren:
97
Kapitel 2 Hierarchische Strukturen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml v e r s i o n=” 1 . 0 ” e n c o d i n g=” i s o −8859−1” ?>
<?xml−s t y l e s h e e t type=” t e x t / x s l ” h r e f=” cdTable . x s l ”?>
<cds>
<cd>
<a r t i s t >The B e a t l e s </ a r t i s t >
< t i t l e >White Album</ t i t l e >
<l a b e l >Apple</ l a b e l >
</cd>
<cd>
<a r t i s t >The B e a t l e s </ a r t i s t >
< t i t l e >Rubber Soul </ t i t l e >
<l a b e l >Parlophone </ l a b e l >
</cd>
<cd>
<a r t i s t >Duran Duran</ a r t i s t >
< t i t l e >Rio</ t i t l e >
<l a b e l >T r i t e c </ l a b e l >
</cd>
<cd>
<a r t i s t >Depeche Mode</ a r t i s t >
< t i t l e >C o n s t r u c t i o n Time Again</ t i t l e >
<l a b e l >Mute</ l a b e l >
</cd>
<cd>
<a r t i s t >Yazoo</ a r t i s t >
< t i t l e >U p s t a i r s a t E r i c s </ t i t l e >
<l a b e l >Mute</ l a b e l >
</cd>
<cd>
<a r t i s t >Marc Almond</ a r t i s t >
< t i t l e >Absinthe </ t i t l e >
<l a b e l >Some B i z a r r e </ l a b e l >
</cd>
<cd>
<a r t i s t >ABC</ a r t i s t >
< t i t l e >Beauty Stab </ t i t l e >
<l a b e l >Mercury</ l a b e l >
</cd>
</cds>
Gesamtstruktur
XSLT-Skripte sind syntaktisch auch wieder XML-Dokumente. Ein XSLT-Skript hat
feste Tagnamen, die eine Bedeutung für den XSLT-Prozessor haben. Diese Tagnamen haben einen festen definierten Namensraum. Das äußerste Element eines
XSLT-Skripts hat den Tagnamen stylesheet. Damit hat ein XSLT-Skript einen
Rahmen der folgenden Form:
98
2.5 XML Verarbeitung
1
2
3
4
5
<?xml v e r s i o n=” 1 . 0 ” e n c o d i n g=” i s o −8859−1” ?>
<x s l : s t y l e s h e e t
v e r s i o n=” 1 . 0 ”
xmlns : x s l=” h t t p : / /www. w3 . o r g /1999/XSL/ Transform ”>
</ x s l : s t y l e s h e e t >
Wir können mit einer Processing-Instruction am Anfang eines XML-Dokumentes
definieren, mit welchem XSLT Stylesheet es zu bearbeiten ist. Hierzu wird als Referenz im Attribut href die XSLT-Datei angegeben.
1
2
3
4
<?xml v e r s i o n=” 1 . 0 ” e n c o d i n g=” i s o −8859−1” ?>
<?xml−s t y l e s h e e t type=” t e x t / x s l ” h r e f=” cdTable . x s l ”?>
<cds>
<cd > . . . . . . . .
Vorlagen (templates)
Das wichtigste Element in einem XSLT-Skript ist das Element xsl:template. In
ihm wird definiert, wie ein bestimmtes Element transformiert werden soll. Es hat
schematisch folgende Form:
<xsl:template match="Elementname">
zu erzeugender Code
</xsl:template >
Folgendes XSLT-Skript transformiert die XML-Datei mit den CDs in eine HTMLTabelle, in der die CDs tabellarisch aufgelistet sind.
1
2
3
4
<?xml v e r s i o n=” 1 . 0 ” e n c o d i n g=” i s o −8859−1” ?>
<x s l : s t y l e s h e e t
v e r s i o n=” 1 . 0 ”
xmlns : x s l=” h t t p : / /www. w3 . o r g /1999/XSL/ Transform ”>
5
6
7
8
9
10
11
12
<!−− S t a r t r e g e l f ü r das ganze Dokument . −−>
<x s l : t e m p l a t e match=” / ”>
<html><head><t i t l e >CD T a b e l l e </ t i t l e ></head>
<body>
<x s l : apply−t e m p l a t e s/>
</body>
</html>
99
Kapitel 2 Hierarchische Strukturen
13
</ x s l : template >
14
15
16
17
18
19
20
21
22
<!−− Reg el f ü r d i e CD−L i s t e . −−>
<x s l : t e m p l a t e match=” c d s ”>
<t a b l e b o r d e r=” 1 ”>
<t r ><td><b>I n t e r p r e t </b></td><td><b>T i t e l </b></td></t r >
<x s l : apply−t e m p l a t e s/>
</t a b l e >
</ x s l : template >
23
24
25
26
<x s l : t e m p l a t e match=” cd ”>
<t r ><x s l : apply−t e m p l a t e s/></t r >
</ x s l : template >
27
28
29
30
<x s l : t e m p l a t e match=” a r t i s t ”>
<td><x s l : apply−t e m p l a t e s/></td>
</ x s l : template >
31
32
33
34
<x s l : t e m p l a t e match=” t i t l e ”>
<td><x s l : apply−t e m p l a t e s/></td>
</ x s l : template >
35
36
37
38
39
<!−− Reg el f ü r a l l e ü b r i g e n Elemente .
Mit d i e s e n s o l l n i c h t s gemacht werden −−>
<x s l : t e m p l a t e match=” * ”>
</ x s l : template >
40
41
</ x s l : s t y l e s h e e t >
Öffnen wir nun die XML Datei, die unsere CD-Liste enthält im Webbrowser, so
wendet er die Regeln des referenzierten XSLT-Skriptes an und zeigt die so generierte
Webseite als Tabelle an.
Läßt man sich vom Browser hingegen den Quelltest der Seite anzeigen, so wird kein
HTML-Code angezeigt sondern der XML-Code.
Aufgabe 2 Schreiben Sie ein XSL-Skript, das für die in der ersten Aufgabe
angegebenen XML Quelltextdatei ein HTML-Dokument erzeugt, in dem alle
Klassen des Skriptes mit Paketangabe aufgelistet sind. Die Klassen befinden
sich dabei in den code Elementen. Dort gibt es Attribute für die Klasse und
für das Paket.
XSLT in Java
Ebenso wie für XPath gibt es auch eine Implementierung von XSLT in Java. Die
entsprechenden Klassen befinden sich im Paket javax.xml.transform. Auch hier
wird wieder das Factory-Pattern angewendet. Wir geben im Folgendem eine kleine
100
2.5 XML Verarbeitung
Beispielanwendung, in der ein XSLT-Skript auf ein XML-Dokument angewendet
wird.
Interessant ist, dass es auch möglich ist eine Instanz vom Typ Transformer ohne
Angabe eines XSL-Skripts als Quelle zu erzeugen. Dieses entspricht dann der Identität, dass heißt, das transformierte Ausgabeausgabedokument ist gleich dem zu
transformierenden Eingabedokument. Damit kann dieser Identittäts-Transformer
dazu genutzt werden, um einen DOM-Baum wieder als textuelle Datei zu speichern.
1
package name . p a n i t z . xml . x s l t ;
2
3
import o r g . w3c . dom . Node ;
4
5
6
7
8
9
10
11
12
import
import
import
import
import
import
import
import
j a v a x . xml . t r a n s f o r m . T r a n s f o r m e r F a c t o r y ;
j a v a x . xml . t r a n s f o r m . Transformer ;
j a v a x . xml . t r a n s f o r m . OutputKeys ;
j a v a x . xml . t r a n s f o r m . dom . DOMSource ;
j a v a x . xml . t r a n s f o r m . S o u r c e ;
j a v a x . xml . t r a n s f o r m . stream . StreamRes ult ;
j a v a x . xml . t r a n s f o r m . stream . StreamSource ;
j a v a x . xml . t r a n s f o r m . T r a n s f o r m e r E x c e p t i o n ;
13
14
15
import j a v a . i o . S t r i n g W r i t e r ;
import j a v a . i o . F i l e ;
16
17
18
19
20
p u b l i c c l a s s XSLT {
s t a t i c p u b l i c S t r i n g t r a n s f o r m ( F i l e x s l t , F i l e doc ) {
r e t u r n t r a n s f o r m ( new StreamSource ( doc ) , new StreamSource ( doc ) ) ;
}
21
s t a t i c p u b l i c S t r i n g t r a n s f o r m ( S o u r c e x s l t , S o u r c e doc ) {
try {
S t r i n g W r i t e r w r i t e r = new S t r i n g W r i t e r ( ) ;
Transformer t =
TransformerFactory
. newInstance ( )
. newTransformer ( x s l t ) ;
t . t r a n s f o r m ( doc , new StreamR esult ( w r i t e r ) ) ;
return writer . getBuffer () . toString () ;
} c a t c h ( T r a n s f o r m e r E x c e p t i o n _) {
return ”” ;
}
}
22
23
24
25
26
27
28
29
30
31
32
33
34
35
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n {
System . out . p r i n t l n ( t r a n s f o r m ( new F i l e ( a r g s [ 0 ] ) , new F i l e ( a r g s [ 1 ] ) ) )
;
}
36
37
38
39
40
}
101
Kapitel 2 Hierarchische Strukturen
Listing 2.76: XSLT
2.5.4 Das SAX-API
Oft brauchen wir nie das komplette XML-Dokument als Baum im Speicher. Eine
Großzahl der Anwendungen auf XML-Dokumenten geht einmal das Dokument
durch, um irgendwelche Informationen darin zu finden, oder ein Ergebnis zu erzeugen. Hierzu reicht es aus, immer nur einen kleinen Teil des Dokuments zu betrachten. Und tatsächlich hätte diese Vorgehensweise, bai allen bisher geschriebenen
Programmen gereicht. Wir sind nie im Baum hin und her gegangen. Wir sind nie von
einem Knoten zu seinem Elternknoten oder seinen vor ihm liegenden Geschwistern
gegangen.
Ausgehend von dieser Beobachtuung hat eine Gruppe von Programmierern ein API
zur Bearbeitungen von XML-Dokumenten vorgeschlagen, das nie das gesammte
Dokument im Speicher zu halten braucht. Dieses API heißt SAX, für simple api
for xml processing. SAX ist keine Empfehlung des W3C. Es ist außerhalb des W3C
entstanden.
Die Idee von SAX ist ungefähr die, daß uns jemand das Dokument vorliest, einmal
von Anfang bis Ende. Wir können dann auf das gehörte reagieren. Hierzu ist für
einen Parse mit einem SAX-Parser stets mit anzugeben, wie auf das Vorgelesene
reagiert werden soll. Dieses ist ein Objekt der Klasse DefaultHandler. In einem
solchen handler sind Methoden auszuprogrammieren, in denen spezifiziert ist, was
gemacht werden soll, wenn ein Elementstarttag, Elementendtag, Textknoten etc.
vorgelsen wird. Man spricht bei einem SAX-Parser von einem ereignisbasierten Parser. Wir reagieren auf bestimmte Ereignisse des Parses, nämlich dem Starten/Enden
von Elementen und so weiter.
Auch ein SAX-Parser liegt in Java nur als Schnittstelle vor und kann nur über eine
statische Fabrikmethode instanziiert werden.
1
2
3
4
5
package name . p a n i t z . xml . sax ;
import o r g . xml . sax . h e l p e r s . D e f a u l t H a n d l e r ;
import j a v a x . xml . p a r s e r s . * ;
import o r g . xml . sax . * ;
import j a v a . i o . F i l e ;
6
7
8
9
10
11
12
13
p u b l i c c l a s s SaxParse {
public s t a t i c void parse ( F i l e f i l e , DefaultHandler handler )
throws E x c e p t i o n {
SAXParserFactory . n e w I n s t a n c e ( )
. newSAXParser ( )
. parse ( f i l e , handler ) ;
}
102
2.5 XML Verarbeitung
14
}
Listing 2.77: SaxParse.java
Als erstes Beispiel wollen wir unser altbekanntes Zählen der Knoten programmieren.
Hierzu ist ein eigener DefaultHandler zu schreiben, der, sobald beim Vorlesen
ihm der Beginn eines Elements gemeldet wird, darauf reagiert, indem er seinen
Zähler um eins weiterzählt. Wir überschreiben demnach genau eine Methode aus
dem DefaultHandler, nämlich die Methode startElement.
1
package name . p a n i t z . xml . sax ;
2
3
4
5
import o r g . xml . sax . A t t r i b u t e s ;
import o r g . xml . sax . SAXException ;
import o r g . xml . sax . h e l p e r s . D e f a u l t H a n d l e r ;
6
7
8
p u b l i c c l a s s Count e x t e n d s D e f a u l t H a n d l e r {
int result = 0;
9
@Override
p u b l i c v o i d s t a r t E l e m e n t ( S t r i n g u r i , S t r i n g localName , S t r i n g qName ,
A t t r i b u t e s a t t r i b u t e s ) throws SAXException {
r e s u l t ++;
}
10
11
12
13
14
15
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n {
Count c o u n t e r = new Count ( ) ;
SaxParse . p a r s e ( new j a v a . i o . F i l e ( a r g s [ 0 ] ) , c o u n t e r ) ;
System . out . p r i n t l n ( c o u n t e r . r e s u l t ) ;
}
16
17
18
19
20
21
}
Listing 2.78: Count.java
1
package name . p a n i t z . xml . sax ;
2
3
4
import j a v a . u t i l . S e t ;
import j a v a . u t i l . T r e e S e t ;
5
6
7
8
import o r g . xml . sax . A t t r i b u t e s ;
import o r g . xml . sax . SAXException ;
import o r g . xml . sax . h e l p e r s . D e f a u l t H a n d l e r ;
9
10
11
p u b l i c c l a s s GetTagNames e x t e n d s D e f a u l t H a n d l e r {
Set<S t r i n g > r e s u l t = new TreeSet <>() ;
12
13
14
15
16
@Override
p u b l i c v o i d s t a r t E l e m e n t ( S t r i n g u r i , S t r i n g localName , S t r i n g qName ,
A t t r i b u t e s a t t r i b u t e s ) throws SAXException {
r e s u l t . add (qName) ;
103
Kapitel 2 Hierarchische Strukturen
}
17
18
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n {
GetTagNames t a g C o l l e c t o r = new GetTagNames ( ) ;
SaxParse . p a r s e ( new j a v a . i o . F i l e ( a r g s [ 0 ] ) , t a g C o l l e c t o r ) ;
System . out . p r i n t l n ( t a g C o l l e c t o r . r e s u l t ) ;
}
19
20
21
22
23
24
25
}
Listing 2.79: GetTagNames.java
1
package name . p a n i t z . xml . sax ;
2
3
4
5
import o r g . xml . sax . A t t r i b u t e s ;
import o r g . xml . sax . SAXException ;
import o r g . xml . sax . h e l p e r s . D e f a u l t H a n d l e r ;
6
7
8
9
p u b l i c c l a s s GetProgramCode e x t e n d s D e f a u l t H a n d l e r {
S t r i n g B u f f e r r e s u l t = new S t r i n g B u f f e r ( ) ;
b o o l e a n isInCodeElement = f a l s e ;
10
@Override
p u b l i c v o i d s t a r t E l e m e n t ( S t r i n g u r i , S t r i n g localName , S t r i n g qName ,
A t t r i b u t e s a t t r i b u t e s ) throws SAXException {
i f (qName . e q u a l s ( ” code ” ) ) {
isInCodeElement = t r u e ;
}
}
11
12
13
14
15
16
17
18
@Override
p u b l i c v o i d c h a r a c t e r s ( c h a r [ ] ch , i n t s t a r t , i n t l e n g t h )
throws SAXException {
S t r i n g t e x t = new S t r i n g ( ch , s t a r t , l e n g t h ) ;
i f ( isInCodeElement )
r e s u l t . append ( t e x t ) ;
}
19
20
21
22
23
24
25
26
@Override
p u b l i c v o i d endElement ( S t r i n g u r i , S t r i n g localName , S t r i n g qName)
throws SAXException {
i f (qName . e q u a l s ( ” code ” ) ) {
isInCodeElement = f a l s e ;
}
}
27
28
29
30
31
32
33
34
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n {
GetProgramCode c o d e E x t r a c t o r = new GetProgramCode ( ) ;
SaxParse . p a r s e ( new j a v a . i o . F i l e ( a r g s [ 0 ] ) , c o d e E x t r a c t o r ) ;
System . out . p r i n t l n ( c o d e E x t r a c t o r . r e s u l t ) ;
}
35
36
37
38
39
40
}
104
2.5 XML Verarbeitung
Listing 2.80: GetProgramCode.java
2.5.5 Das StAx-API
Wir haben bisher drei methodisch sehr unterschiedliche Herangehensweisen für die
Programmierung von XML Dokumenten kennen gelernt:
• das DOM-API als ein Baum-API.
• das SAX-API als ein Ereignis-basiertes API.
• XPath als spezielle Programmiersprache, zur Beschreibung der Selektion von
Knoten aus der Baumstruktur des Dokuments.
Als Abschluss betrachten wir noch eine vierte methodische Herangehensweise. Dabei
schließen wir wieder den Kreis zum ersten Kapitel, nämlich zu den Iteratoren. Ein
weiteres API zur XML Verarbeitung in Java ist das strombasierte API StAX. Dahinter verbirgt sich wie bei SAX, die Verarbeitung des Dokuments sequentiell in seiner
textuellen Form. Damit steht wieder nicht die reine Baumstruktur direkt im Speicher zur Verfügung. Die sequentielle Verarbeitung wird durch einen Iterator über
die verschiedenen XML-Ereignisse bewerkstelligt.
In Java finden sich die entsprechenden Definitionen als Schnittstellen im Paket
javax.xml.stream. Die entscheidenen beiden Schnittstellen sind dabei:
• XMLEventReader: diese Schnittstelle beschreibt einen Iterator der beim Lesen
eines XML-Dokuments über die einzelnen XML-Komponenten des Dokuments
iteriert. Leider berücksichtigt diese Schnittstelle nicht die generischen Typen
von Java, so dass die Methode next() als Rückgabetyp lediglich Object hat.
• javax.xml.stream.events.XMLEvent: diese Schnittstelle beschreibt allgemein ein XML-Konstrukt, auf dem beim Lesen gestoßen wird. Für die
verschiedenen XML-Anteile gibt es Unterschnittstellen dieser Schnittstelle
wie z.B. StartElement, EndElement oder Characters. In der Schnittstelle
XMLEvent sind boolsche Methoden definiert, die testen, ob ein Objekt von einer dieser Unterschnittstellen ist. Es gibt z.B. die Methode
boolean isAttribute();.
Die Anwendung des StAX-APIs geht sehr analog, zu der Anwendung der anderen
beiden XML APIs.
1
package name . p a n i t z . xml . s t a x ;
2
3
4
5
import j a v a . i o . FileNotFoundException ;
import j a v a . i o . F i l e R e a d e r ;
import j a v a . u t i l . I t e r a t o r ;
6
7
import j a v a x . xml . stream . F a c t o r y C o n f i g u r a t i o n E r r o r ;
105
Kapitel 2 Hierarchische Strukturen
8
9
10
11
import
import
import
import
j a v a x . xml . stream . XMLEventReader ;
j a v a x . xml . stream . XMLInputFactory ;
j a v a x . xml . stream . XMLStreamException ;
j a v a x . xml . stream . e v e n t s . XMLEvent ;
12
13
p u b l i c c l a s s StaxTest {
14
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s )
throws FileNotFoundException
, XMLStreamException
, FactoryConfigurationError {
15
16
17
18
Listing 2.81: StaxTest.java
Ein StAX basierter Parser wird durch eine Factory instanziiert:
f i n a l XMLEventReader s t a x R e a d e r
= XMLInputFactory
. newInstance ( )
. createXMLEventReader ( new F i l e R e a d e r ( a r g s [ 0 ] ) ) ;
19
20
21
22
23
24
i n t r e s u l t =0;
25
Listing 2.82: StaxTest.java
Das so erhaltene XMLEventReader Objekt kann zum Iterieren durch das Dokument
benutzt werden. Leider kennt das StAX-API nicht die Schnittstelle Iterable, so
dass nicht direkt die for-each Schleife zum Iterieren angewendet werden kann. Wir
können uns da mit der Erzeugung eines iterierbaren Objektes durch einen LambdaAusdruck behelfen:
I t e r a b l e <XMLEvent> i t = ( ) −> s t a x R e a d e r ;
f o r (XMLEvent ev : i t ) {
26
27
Listing 2.83: StaxTest.java
Für dieses Beispiel wollen wir die Anzahl der Elementknoten zählen. Ist das iterierte
Objekt der Anfang eines Elements, also ein Start-Tag, dann zählen wir die Ergebnisvariable um eins hoch.
i f ( ev . i s S t a r t E l e m e n t ( ) ) r e s u l t ++;
}
System . out . p r i n t l n ( r e s u l t ) ;
28
29
30
}
31
32
}
Listing 2.84: StaxTest.java
106
Kapitel 3
Eine graphische Komponente für
Baumstrukturen
Zum Abschluss dieses Kapitels über Baumstrukturen, sollen die Bäume auch in einer
graphischen Komponente angezeigt werden können. Fast jede GUI-Bibliothek hat
eine Komponente zur Darstellung von hierarchischen Strukturen, um zum Beispiel
das Dateisystem anzeigen zu können. Im Swing-API ist dieses die Klasse JTree. Die
Klasse JTree hat einen Konstruktor, dem man einen Baumknoten übergeben kann.
Hierzu kennt das API eine Schnittstelle TreeNode, die allgemein die verlangten
Eigenschaften eines Baumknotens in sieben Methoden beschreibt.
Allerdings kann man in der regel nicht davon ausgehen, dass eine Baumstruktur so
entworfen wurde, dass sie diese Schnittstelle implementiert. Die Schnittstelle Node
des DOM-APIs kennt die Schnittstelle TreeNode aus Swing genauso wenig, wie
unsere Klasse Tree.
Deshalb gibt es eine zweite, flexiblere Möglichkeit, um der graphischen Komponente JTree die Baumdaten zu übergeben. Hierzu definiert Swing die Schnittstelle
TreeModel.
1
2
3
4
package name . p a n i t z . u t i l ;
import j a v a x . swing . e v e n t . T r e e M o d e l L i s t e n e r ;
import j a v a x . swing . t r e e . TreeModel ;
import j a v a x . swing . t r e e . TreePath ;
5
6
7
8
9
10
p u b l i c c l a s s MyTreeModel implements TreeModel {
Tree<?> t r e e ;
p u b l i c MyTreeModel ( Tree<?> t r e e ) {
this . tree = tree ;
}
11
12
13
14
15
@Override
p u b l i c Object getRoot ( ) {
return tree ;
}
16
17
18
@Override
p u b l i c Object g e t C h i l d ( Object parent , i n t i n d e x ) {
107
Kapitel 3 Eine graphische Komponente für Baumstrukturen
Tree<?> t = ( Tree <?>) p a r e n t ;
return t . childNodes . get ( index ) ;
19
20
}
21
22
@Override
p u b l i c i n t getChildCount ( Object p a r e n t ) {
Tree<?> t = ( Tree <?>) p a r e n t ;
return t . childNodes . s i z e () ;
}
23
24
25
26
27
28
@Override
p u b l i c b o o l e a n i s L e a f ( Object node ) {
Tree<?> t = ( Tree <?>)node ;
r e t u r n t . c h i l d N o d e s . isEmpty ( ) ;
}
29
30
31
32
33
34
@Override
p u b l i c v o i d valueForPathChanged ( TreePath path , Object newValue ) {
throw new Unsu pp ortedOperati onExc eptio n ( ) ;
}
35
36
37
38
39
@Override
p u b l i c i n t g e t I n d e x O f C h i l d ( Object parent , Object c h i l d ) {
Tree<?> t = ( Tree <?>) p a r e n t ;
int i = 0;
f o r ( Tree<?> c : t . c h i l d N o d e s ) {
i f ( c==c h i l d ) r e t u r n i ;
i ++;
}
r e t u r n −1;
}
40
41
42
43
44
45
46
47
48
49
50
@Override
p u b l i c void addTreeModelListener ( TreeModelListener l ) {
51
52
}
53
@Override
p u b l i c void removeTreeModelListener ( TreeModelListener l ) {
54
55
56
}
}
Listing 3.1: MyTreeModel.java
Bei genauer Betrachtung dieser Umsetzung stellt man fest, dass die Schnittstelle
TreeModel leider kein Gebrauch von generischen Typen macht. Daher sind wir
gezwungen mit Typzusicherungen zu arbeiten. Wir können aber eine abstrakte
Klasse vorsehen, die Gebrauch von generischen Typen macht, und die TreeModel
implementiert. Die Klasse bekommt einen Typparameter für den Typ der Baumknoten.
Die entsprechenden Typzusicherungen werden dann in der abstrakten Klasse
durchgeführt. Zwei abstrakte Methoden bekommen dann eine Signatur, die direkt
Parameter der Baumknoten erhält.
108
1
package name . p a n i t z . u t i l ;
2
3
4
5
import j a v a x . swing . e v e n t . T r e e M o d e l L i s t e n e r ;
import j a v a x . swing . t r e e . TreeModel ;
import j a v a x . swing . t r e e . TreePath ;
6
7
8
p u b l i c a b s t r a c t c l a s s Ab st r a c t T re e M o de l l <T> implements TreeModel {
T tree ;
9
10
11
12
13
p u b l i c A b s t r a c t T r e e M o d e l l (T t r e e ) {
super () ;
this . tree = tree ;
}
14
15
16
17
18
@Override
p u b l i c T getRoot ( ) {
return tree ;
}
19
20
21
22
23
24
@Override
p u b l i c T g e t C h i l d ( Object parent , i n t i n d e x ) {
T t = (T) p a r e n t ;
return getChildAtIndex ( t , index ) ;
}
25
26
a b s t r a c t p u b l i c T g e t C h i l d A t I n d e x (T parent , i n t i n d e x ) ;
27
28
29
30
31
32
33
@Override
p u b l i c i n t getChildCount ( Object p a r e n t ) {
T t = (T) p a r e n t ;
return getChildSize ( t ) ;
}
34
35
a b s t r a c t p u b l i c i n t g e t C h i l d S i z e (T t ) ;
36
37
38
39
40
@Override
p u b l i c b o o l e a n i s L e a f ( Object node ) {
r e t u r n getChildCount ( node ) ==0;
}
41
42
43
44
45
@Override
p u b l i c v o i d valueForPathChanged ( TreePath path , Object newValue ) {
throw new UnsupportedOp erationExcep ti on ( ) ;
}
46
47
48
49
50
51
52
@Override
p u b l i c i n t g e t I n d e x O f C h i l d ( Object parent , Object c h i l d ) {
T t = (T) p a r e n t ;
T c = (T) c h i l d ;
return getChildIndex ( t , c ) ;
}
109
Kapitel 3 Eine graphische Komponente für Baumstrukturen
53
p u b l i c i n t g e t C h i l d I n d e x (T t , T c ) {
f o r ( i n t i =0; i <getChildCount ( t ) ; i ++){
i f ( c == g e t C h i l d ( t , i ) ) r e t u r n i ;
}
r e t u r n −1;
}
54
55
56
57
58
59
60
61
@Override
p u b l i c v o i d a d d T r e e M o d e l L i s t e n e r ( T r e e M o d e l L i s t e n e r l ) {}
62
63
64
@Override
p u b l i c void removeTreeModelListener ( TreeModelListener l ) { }
65
66
67
}
Listing 3.2: AbstractTreeModell.java
Um jetzt für eine Baumklasse ein Modell zu bekommen, reicht es aus, von der
abstrakten Klasse abzuleiten und zwei naheliegende Methoden zu implementieren.
Eine Methode, um die Anzahl der Kinder zu erfragen, eine um ein bestimmtes Kind
zu erhalten.
1
2
3
4
package name . p a n i t z . u t i l ;
import j a v a x . swing . JTree ;
import j a v a x . swing . e v e n t . T r e e S e l e c t i o n E v e n t ;
import j a v a x . swing . e v e n t . T r e e S e l e c t i o n L i s t e n e r ;
5
6
p u b l i c c l a s s TreeTreeModel e x t e n d s A b st ra c t T r e e M o d e l l <Tree<?>>{
7
8
9
10
p u b l i c TreeTreeModel ( Tree<?> t r e e ) {
super ( t r e e ) ;
}
11
12
13
14
@Override p u b l i c Tree<?> g e t C h i l d A t I n d e x ( Tree<?> parent , i n t i n d e x )
{
return parent . childNodes . get ( index ) ;
}
15
16
17
18
@Override p u b l i c i n t g e t C h i l d S i z e ( Tree<?> p a r e n t ) {
return parent . childNodes . s i z e () ;
}
19
20
21
22
23
24
25
26
27
28
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
j a v a x . swing . JFrame f = new j a v a x . swing . JFrame ( ) ;
JTree j t = new JTree ( new TreeTreeModel ( Tree . c1 ) ) ;
j t . a d d T r e e S e l e c t i o n L i s t e n e r ( new T r e e S e l e c t i o n L i s t e n e r ( ) {
@Override
p u b l i c v o i d valueChanged ( T r e e S e l e c t i o n E v e n t e ) {
Object n = e . getPath ( ) . getLastPathComponent ( ) ;
System . out . p r i n t l n ( n ) ;
}
110
}) ;
f . add ( j t ) ;
f . pack ( ) ;
f . s e t V i s i b l e ( true ) ;
29
30
31
32
}
33
34
}
Listing 3.3: TreeTreeModel.java
Aufgabe 1 (Nur für Studiengang WI)
Schreiben sie eine Swing GUI-Komponente DOMTreeView, die eine Unterklasse
von JTree ist und die Elementknoten eines XML-DOM Baumes darstellen
kann. Schreiben Sie hierzu eine Klasse DomTreeModel, die Unterklasse der
Klasse AbstractTreeModel ist.
Aufgabe 1 (Projektblatt für Studiengang WI)
a) Schreiben Sie eine Swing GUI-Komponente XMLViewer, die von
JSplitPane ableitet. Auf der linken Seite des Split-Pane soll mit einem
DOMTreeView ein XML-Dokument in seiner Elementstruktur dargestellt
werden. Klickt man auf einen der Elementknoten, soll in einer JTextArea
auf der rechten Seite der Textinhalt des Elementknotens dargestellt werden.
b) Schreiben Sie eine Swing GUI Applikation XMLWorkbench. Hier soll es
möglich sein eine XML Datei zu laden. Es soll ein XPath-Ausdruck
eingegeben werden, die Ergebnisse der XPath-Auswertung sollen als
XML in einer XMLViewer Komponente angezeigt werden können. Die
Ergebnisse der XPath Berechnung sollen auch abgespeichert werden
können. Zur Auswertung des XPath-Audruckes sollen dabei die Klassen
des Standardpakets javax.xml.xpath verwendet werden.
Sehen Sie in der GUI-Applikation auch eine TextArea vor, in der Log
Information angezeigt werden kann, z.B. wenn Exceptions aufgetreten
sind.
c) Erweitern Sie die XMLWorkbench um die Möglichkeit, dass ein XSL-Skript
in einer JTextArea geladen und auch editiert werden kann. Schließlich
soll es möglich sein, das XSL-Skript auf das geladene XML-Dokument
anzuwenden, das Ergebnis anzuzeigen und abzuspeichern.
111
Herunterladen