3 Entwurf von Algorithmen
3.1 Algorithmen, Programmiersprachen und Pro- gramme
3.2 Systematischer Entwurf von Algorithmen 3.3 Schrittweise Verfeinerung
3.4 Ablaufsteuerung (Kontrollstrukturen) 3.5 Modularität
3.6 Rekursion
3.7 Daten und Datenstrukturen
3.1 Algorithmen, Programmierspra- chen und Programme
Algorithmus: Verfahren zur systematischen, schrittwei- sen Lösung eines Problems
Beschrieben in:
- Umgangssprache - Programmiersprache
- mathematischem Formalismus (z. B. Tu- ring-Maschine)
Church-Turing-These
"All reasonable definitions of 'algorithm' are equivalent."
Algorithmus vs. Programm
Algorithmus
Programmierung
Programm in höherer
Programmiersprache
Übersetzung
Programm in Maschinensprache
Interpretation durch CPU
(Gewünschter Ablauf wird ausgeführt)
Programmentwicklung
• Durchführbarkeitsstudie: Kann das Problem gelöst werden? (Analyse)
• Spezifikation (specification document)
• Design/Software Architektur/Design Pat- terns/Frameworks
• Definition von Algorithmen
• Implementation von Algorithmen
• Code-Test
• Wartung
Beispiele für Algorithmen
Prozess Algorithmus Typische Schritte im Algorithmus
Pullover stricken Strickmuster Linke Masche, rechte Masche stricken
Modellflugzeug bauen Montageanleitung Leime Rippe A an den Holm B
Kleider nähen Schnittmuster Nähe Saum an
Spiele Musik Notenblatt
Kochen Rezept Zutaten (4 Personen)
400 g Tomatensoße 1/2 l Wasser
3 Teelöffel Chilipulver 1 1/2 l Minutenreis
1/2 kg zerstoßene Tortilla-Chips
1/2 kg kleingeschnittenen Cheddarkä- se
Anleitung:
Vermenge Tomatensoße, Wasser, Chili- pulver in einem mittelgroßen Topf und bringe es zum Kochen. Rühre den Reis ein, decke den Topf ab, nehme ihn vom Feuer und lasse ihn 10 Minuten lang ste- hen. Streue Tortilla-Chips und Käse dar- über. Serviere das Essen, wenn ge- wünscht, mit Salatblättern und saurer Sahne.
Algorithmen als fundamentales Konzept
• Unabhängig von der Programmiersprache
• Unabhängig von der Rechner-Hardware
Der Entwurf von Algorithmen ist ein kreativer Pro- zeß, der nicht automatisiert werden kann.
Beispiel
Summiere alle Zahlen zwischen 1 und 100:
Lösung 1
1+2+3+4…+99+100 = 5050 O(n)
Lösung 2
(1+100)+(2+99)+…(50+51) = 50*101 = 5050 O(1)
Wir sehen: Die Effizienz eines Algorithmus ist ein wich- tiges Entwurfskriterium.
Richtlinien
• Ist das Problem lösbar?
• Welche Schritte müssen ausgeführt werden, um einen korrekten Algorithmus zu erhalten?
Korrektheit heißt:
- der Algorithmus erzeugt ein Resultat (ist endlich) - das Resultat ist korrekt
• Ist der Algorithmus effizient?
Beispiele
• Unlösbares Problem Halteproblem
Finde einen Algorithmus, der feststellt, ob ein beliebi- ger Algorithmus in endlicher Zeit irgendein Ergebnis liefert (=anhält).
• Sehr komplexes Problem
Problem des Handlungsreisenden (traveling sales- man)
Für eine beliebige Topologie: Finde den kürzesten Weg, der alle Städte verbindet, die der Handlungsrei- sende aufsuchen soll.
Schwierigkeiten beim Algorithmenentwurf
• Es existiert keine präzise Definition des Problems (Problem der genauen Spezifikation).
• Spezialfälle kommen vor, die nicht betrachtet wurden.
• Es ist schwer zu zeigen, dass der Algorithmus für alle möglichen Eingabedaten korrekt abläuft.
Beispiele für Schwierigkeiten (1)
Beispiel 1: Wegbeschreibung 1. Am Laden rechts abbiegen
2. Geradeaus bis zur nächsten Kreuzung 3. rechts abbiegen
4. dritte Querstraße links
An welchem Laden? (UNPRÄZISE)
Beispiel 2: Finde Süden mit der Uhr 1. Richte kleinen Zeiger auf die Sonne
2. Berechne die Winkelhalbierende zwischen dem klei- nen Zeiger und 12.00 h
3. Winkelhalbierende zeigt nach Süden
Ausrichten des kleinen Zeigers durch Drehen am Stell- knopf oder Drehen der gesamten Uhr (UNPRÄZISE) Funktioniert nicht auf der Südhalbkugel! (Sonderfall ver- gessen)
Beispiele für Schwierigkeiten (2)
Beispiel 3: Repariere Wasserhahn 1. Wasserhahn aufdrehen
2. Warten, bis kein Wasser mehr fließt 3. Hahn abschrauben
4. Dichtung gegen neue Dichtung auswechseln 5. Hahn anschrauben
6. Hahn zudrehen 7. Haupthahn öffnen
Endet nicht; Schritt 2 unendlich; es wurde vergessen, zu Anfang den Haupthahn zuzudrehen.
Folgerung
Der Entwurf von Algorithmen muss auf systematische Weise diszipliniert durchgeführt werden. Methoden zum systematischen Entwurf und zur Analyse von Algo- rithmen (Komplexität und Korrektheit) sind hilfreich.
In einem zweiten Schritt wird der Algorithmus als Com- puterprogramm formuliert. Dieser Schritt wird als Pro- grammierung bezeichnet. Durch die Verwendung einer in Syntax und Semantik wohldefinierten Sprache wird sichergestellt, dass die einzelnen Anweisungen
• unmißverständlich sind
• auf dem Zielrechner ausführbar sind (Mächtigkeit, Detaillierungsgrad).
Formulierungsfehler
Annahme
Es existiert ein Algorithmus, der nicht zu komplex ist.
Welche Fehler können in der Formulierung auftauchen?
3 Klassen von Fehlern:
- Syntaktische Fehler - Semantische Fehler - Logische Fehler
Klassifikation der Programmierfehler
a) Syntaktische FehlerWerden durch den Compiler oder Interpreter gefun- den und als solche gekennzeichnet. Instruktion wird nicht ausgeführt.
Beispiel:
a = b * + c;
b) Semantische Fehler
Werden entweder während der Ausführung gefunden (Java Exception, segmentation fault im Betriebssy- stem) oder schlimmstenfalls nie gefunden.
Beispiel: Feldgrenzenverletzung
der Vektor a hat 100 Elemente i = 101;
a[i] = 17;
Beispiel: Teilen durch 0 r = 0.0;
Klassifikation der Programmierfehler
c) Logische FehlerDas Programm tut nicht, was der Programmierer eigentlich wollte.
Beispiel:
// berechne Kreisumfang
circumference = PI * radius;
// anstatt 2*PI*radius
Viele semantischen und alle logischen Fehler können während des Testens und durch formale Verifikation gefunden werden.
Wichtig
Tests sind im Gegensatz zur formalen Verifikation keine Korrektheitsbeweise für ein Programm!
3.2 Systematischer Entwurf von Algo- rithmen
a) Problemstellung
(a) Ist die Problemstellung verstanden bzw. wie lautet das eigentliche Problem?
• Wenn die Problemstellung nicht verstanden ist, dann weitere Informationen einholen. Wenn der Kern des Problems klar hervorgehoben ist, dann das eigentliche Problem formulieren.
(b) Ist die Problemstellung klar und exakt?
• Wenn nein, dann weitere Informationen zur Pro- blemstellung beschaffen oder Problemstellung selbst präzisieren
(c) Hat das Problem bereits einen Namen?
• Wenn nein, dann dem Problem einen kurzen und treffenden Namen geben.
(d) Was ist bekannt und was ist gegeben?
• Zusammenstellen aller bekannten Eingabegrößen bzw. –objekte. Jedem Eingabeobjekt einen Namen geben und seinen Typ bzw. seine Art beschreiben.
Problemstellung (2)
(e) Was ist unbekannt bzw. was ist gesucht?
• Zusammenstellen aller bekannten Ausgabegrößen bzw. Ausgabeobjekte. Jedem Ausgabeobjekt einen Namen geben und seinen Typ festlegen.
(f) Welche Bedingungen für Ein- und Ausgabeobjekte müssen erfüllt sein bzw. werden gefordert?
• Zusammenstellen dieser Bedingungen (z.B. for- melmäßige Zusammenhänge).
Anmerkung zur Problemstellung
In der Praxis ist nur in den seltensten Fällen das zu lö- sende Problem von vorne herein klar beschrieben (spe- zifiziert).
Gründe
• Derjenige, der das Problem hat, und derjenige, der den Algorithmus entwerfen soll, sind oft nicht iden- tisch.
• Sie haben meist unterschiedliches Vorwissen über die Anwendungsumgebung.
• Sie machen meist unterschiedliche „selbstverständli- che“ Annahmen.
Deshalb:
Spezifikationen sollten gemeinsam erarbeitet werden.
Beispiel für eine unklare Aufgabenstellung (1)
Problemstellung
Berechne die Funktion f(x) = log(x)
Fragen
• Was ist die Basis des Logarithmus?
• Wie ist x gegeben? In welchem Datentyp?
• Wie soll f(x) ausgegeben werden?
• Mit welcher Genauigkeit soll gerechnet werden?
• Welchen Wertebereich hat x?
• Was soll passieren, wenn ein unzulässiges x eingege- ben wird?
Beispiel für eine unklare Aufgabenstellung (2)
Spezifikation f(x) = log10(x)
x, f(x) als IEEE double precision floating point-Werte x: Eingabe
f(x): Ausgabe
Wertebereich: 0.3: 0.3; in allen anderen Fällen Fehler- meldung ausgeben.
Systematischer Entwurf von Algorithmen (1)
b) Lösungsplan(a) Ist dasselbe Problem oder ein ähnliches bzw. ver- gleichbares Problem bekannt?
• Wenn ja, dann versuche man, Kenntnisse über die Lösung zu erhalten.
(b) Ist ein allgemeineres Problem bekannt?
• Wenn ja, dann versuche man, Kenntnisse über die Lösung dieses Problems zu erhalten. Man prüfe, ob das gegebene Problem als Sonderfall des all- gemeinen Problems behandelt werden kann.
Wenn ja, dann wende man die allgemeine Pro- blemlösung an. Wenn sich das Problem verallge- meinern lässt, ohne dass die Lösung erheblich schwerer wird, dann löse man das allgemeinere Problem.
(c) Lässt sich das Problem in ein einfacheres Teilpro- blem oder in mehrere einfachere, in sich geschlos- sene Teilprobleme aufteilen?
• Wenn ja, dann teile man das Problem auf und löse die Teilprobleme.
(d) Man stelle einen in Schritte gegliederten Lösungs- plan auf.
Kommentare zum Lösungsplan
a.) Problem: Zeichne ein Dreieck auf dem Bildschirm.
b.) Allgemeineres Problem: Zeichnen eines Polygons mit n Ecken auf dem Bildschirm. (Dreieck: n = 3, Rechteck: n = 4)
Lösung des allgemeineren Problems erheblich schwe- rer?
Dreieck: Linie von Ecke 1 nach Ecke 2 Linie von Ecke 2 nach Ecke 3 Linie von Ecke 3 nach Ecke 1 Polygon: Linie von Ecke 1 zu Ecke 2
Linie von Ecke 2 nach Ecke 3 ...
Linie von Ecke n nach Ecke 1
c.) Teilproblem: Zeichnen einer Linie zwischen zwei Punkten auf dem Bildschirm.
Was ist ein guter Algorithmus?
Kriterien
• Ausführungszeit (Komplexität)
• Speicherbedarf für Programm und Daten
• Entwicklungszeit. Wird zunehmend wichtiger!
• Gut strukturiertes, wartbares Programm: sehr wichtig!
3.3 Schrittweise Verfeinerung
Zerlegung eines Algorithmus in Teilalgorithmen, von de- nen jeder einzelne einfacher und überschaubarer ist als der ursprüngliche Algorithmus. Dadurch verbessern sich die Chancen, einen insgesamt korrekten Algorithmus zu entwerfen.
Die Verfeinerung wird so lange fortgesetzt, bis die ein- zelnen Schritte unmittelbar auf dem Prozessor ausge- führt werden können: vom Menschen bis hin zur Pro- grammiersprache, vom Compiler bis hin zur Maschinen- sprache.
Beispiel für schrittweise Verfeinerung: (1)
Algorithmus "Koche Kaffee"
1. Koche Wasser
2. Gib Kaffeepulver in die Tasse 3. Fülle Wasser in die Tasse
Beispiel für schrittweise Verfeinerung: (2)
Erste Verfeinerung
(1.1) Fülle Wasserkessel (1.2) Schalte Herdplatte an
(1.3) Warte, bis das Wasser kocht (1.4) Schalte Herdplatte aus
(2.1) Öffne Kaffeeglas
(2.2) Entnehme einen Löffel Kaffee (2.3) Kippe Löffel in die Tasse
(2.4) Schließe Kaffeeglas
(3.1) Gieße Wasser aus dem Kessel in die Tasse, bis die Tasse voll ist
Beispiel für schrittweise Verfeinerung: (3)
Zweite Verfeinerung{Algorithmus zur Zubereitung einer Tasse Kaffee}
{Zuerst Wasser kochen}
(1.1.1.) Stelle Kessel unter Wasserhahn (1.1.2.) Drehe Wasserhahn auf
(1.1.3.) Warte, bis Kessel voll ist (1.1.4.) Drehe Wasserhahn zu (1.2.) Schalte Herdplatte an (1.3.1.) Warte, bis Kessel pfeift (1.4.) Schalte Herdplatte aus {Gib Kaffeepulver in die Tasse}
(2.1.1.) Nehme Kaffeeglas aus dem Fach (2.1.2.) Entferne Deckel vom Kaffeeglas (2.2) Entnehme einen Löffel Kaffee (2.3.) Kippe Löffel in die Tasse
(2.4.1.) Schraube Deckel auf das Kaffeeglas (2.4.2.) Stelle Kaffeeglas in das Fach zurück {Fülle Wasser in die Tasse}
(3.1.) Gieße Wasser aus dem Kessel in die Tasse, bis die Tasse voll ist
3.4 Ablaufsteuerung (Kontrollstruktu- ren)
3.4.1 Sequenz (Folge von Anweisungen)
Der Kaffee-Algorithmus beinhaltet einfache Schritte, die einer nach dem anderen auszuführen sind. Wir sagen, ein solcher Algorithmus ist eine Folge (Sequenz) von Schritten, was bedeutet:
1) Zu einem Zeitpunkt wird nur ein Schritt ausgeführt.
Schritte werden nie parallel ausgeführt.
2) Jeder Schritt wird genau einmal ausgeführt:
keiner wird wiederholt, keiner wird ausgelassen.
3) Die Reihenfolge, in der die Schritte ausgeführt wer- den, ist die gleiche, in der sie niedergeschrieben sind.
4) Mit der Beendigung des letzten Schrittes endet der gesamte Algorithmus.
Sequenz (Folge von Anweisungen)
Die Ausführung eines solchen Algorithmus ist sehr starr.
Im Beispiel des Algorithmus "Koche Kaffee":
• Was passiert, wenn manche Kaffeetrinker Milch und/oder Zucker mögen?
• Lassen sich Teile das Algorithmus auch zum Teeko- chen verwenden?
• Kann das Kaffeepulver in die Tasse gegeben werden, während das Wasser zum Kochen gebracht wird (Par- allelisierung)?
Flussdiagramm der Sequenz
Start
Statement 1
Statement 2
Statement 3
End
Struktogramm
Statement 1
Statement 2
Statement 3
3.4.2 Selektion
Bedingte Ausführung von Anweisungen
a) Einfache Form
Falls Bedingung
dann Anweisung Englisch
if condition
then statement(s)
b) Bedingte Anweisung mit Alternative (allgemeine Form):
Falls Bedingung
dann Anweisung 1 sonst Anweisung 2 Englisch
if condition
then statement 1 else statement 2 Anmerkung:
Die einfache Form ist ein Spezialfall der allgemeinen
Mehrfachauswahl
FallsBedingung 1 dann Anweisung 1 Bedingung 2 dann Anweisung 2 .
. .
andernfalls
Anweisung n + 1
Die Bedingungen 1 bis n müssen sich gegenseitig aus- schließen; d.h. es dürfen nicht zwei Bedingungen
gleichzeitig erfüllt sein.
Beispiele für bedingte Anweisungen
Beispiel 1: Einfache bedingte Anweisung
(2.1.1.) Nehme Kaffeeglas aus dem Fach (2.1.2.) Falls Kaffeeglas leer ist
dann nehme neues Kaffeeglas aus dem Schrank
(2.1.3.) Entferne Deckel vom Kaffeeglas
Beispiel 2: Bedingte Anweisung mit Alternative
max (x,y)
Falls x > y
dann gib x aus sonst gib y aus
Flussdiagramm der bedingten Anweisung
Bedingung
Anweisung 1
Anweisung 2
nächste Anweisung
wahr falsch
Flussdiagramm zur Berechnung des Maximums
x > y
gebe x aus gebe y aus
nächste Anweisung
wahr falsch
Struktogramm der bedingten Anweisung
nächste Anweisung Anweisung
1
Anweisung 2
Bedingung
erfüllt nicht
erfüllt
Struktogramm zur Berechnung des Maximums
nächste Anweisung
gebe x aus gebe y aus x > y
erfüllt nicht
erfüllt
Beispiel
Pseudocode
falls die Note des Studenten größer oder gleich 60 ist dann gebe „passed“ aus.
Java
if ( grade >= 60 ) {
System.out.println( “ passed “ );
}
Pseudocode
falls die Note des Studenten ist größer oder gleich 60 dann
gebe „passed“ aus.
sonst
gebe „failed“ aus.
Java
if ( grade >= 60 ) {
System.out.println( “ passed “ );
}
else {
System.out.println( “ failed “ );
Alternative Formulierung (1)
Java: erste Formulierung if ( grade >= 60 ) {
System.out.println( “ passed “ );
}
else {
System.out.println( “ failed “ );
}
Java: zweite Formulierung
System.out.println( grade >= 60
? “ passed “ : “ failed “ );
Hinweis
Man benutze die zweite Formulierung nur, wenn unbe- dingt notwendig, da sie nicht einfach zu lesen ist. Ihr Gebrauch ist oft gerechtfertigt, wenn sie als Argument einer Methode (Funktion) verwendet wird.
Alternative Formulierung (2)
Beispiel 3: geschachtelte bedingte Anweisung
max (x, y, z) Falls x > y
dann falls x > z
dann wähle x sonst wähle z sonst falls y > z
dann wähle y sonst wähle z
Die Einrückung im Text macht im Pseudocode deutlich, zu welcher Bedingung ein sonst gehört. Ohne Einrük- kung wäre die Zuordnung nicht immer eindeutig!
Beispiel:
falls cond1 dann falls cond1 dann
falls cond2 dann falls cond2 dann
instr1 instr1
sonst sonst
instr2 instr2
Geschachtelte falls/dann-Strukturen
Pseudocodefalls die Note des Studenten ist größer oder gleich 90 ist
dann
drucke “A”
sonst
falls die Note des Studenten ist größer oder gleich 80 ist
dann
drucke “ B “ sonst
falls die Note des Studenten ist größer oder gleich 70 ist
dann
drucke “ C “ sonst
falls die Note des Studenten ist größer oder gleich 90 ist
dann
drucke “ D ” sonst
drucke “ F “
Geschachteltes if/else in Java
if ( grade >= 90 ) {System.out.println( “ A “ );
}
else {
if ( grade >= 80 ) {
System.out.println( “ B “ );
}
else {
if ( grade >= 70 ) {
System.out.println( “ C “ );
}
else {
if ( grade >= 60 ) {
System.out.println( “ D “ );
}
else {
System.out.println( “ F “ );
} } } }
Probleme mit geschachtelten Strukturen
if ( x > 5 ) if ( y > 5 )
System.out.println( “x and y are > 5” );
else
System.out.println( “ x is <= 5” );
FEHLER!!
Java bringt ein else stets mit dem vorherigen if in Ver- bindung. Daher heißt es richtig:
if ( x > 5 ) if ( y > 5 )
System.out.println( “x and y are > 5” );
else
System.out.println( “ y is <= 5” );
Andere häufige Probleme
if ( grade >= 60 ) {System.out.println( “passed” );
}
else {
System.out.println( “failed” );
System.out.println( “You must take this course again” );
}
druckt für Note < 60:
failed
You must take this course again ohne Klammern:
if ( grade >= 60 )
System.out.println( “passeed” );
else
System.out.println( “failed” );
System.out.println( “You must take this couse again” );
druckt für Note >= 60:
passed
You must take this course again
Bemerkungen
• Geschachtelte bedingte Ausdrücke sind schwierig zu testen: Tiefe n bedeutet, dass 2n verschiedene Kom- binationen zu testen sind!
• Solche Strukturen sind auch schwer zu verstehen!
Mehrfachauswahl (1)
Allgemeine Formfalls
Variable = Bedingung 1: Anweisung 1 Variable = Bedingung 2: Anweisung 2
…
andernfalls Anweisung
oder
falls Variable gleich
Bedingung 1: Anweisung 1 Bedingung 2: Anweisung 2
…
andernfalls Anweisung
Mehrfachauswahl (2)
falls
Münze = Fünfer: addiere 5 auf Summe Münze = Zehner: addiere 10 auf Summe Münze = Fünfziger: addiere 50 auf Summe andernfalls:
gebe Münze zurück
Oder in anderer Schreibweise:
falls Münze gleich
Fünfer addiere 5 auf Summe Zehner addiere 10 auf Summe Fünfziger addiere 50 auf Summe andernfalls:
gebe Münze zurück
Flussdiagramm für die Mehrfachauswahl
Bedingung =
Wert 1 Wert 2 Wert 3
Anweisung 1 Anweisung 2 Anweisung 3
Anweisung 4
Mehrfachauswahl für das Münzbeispiel
Münze =
Fünfer Zehner Fünfziger
addiere 5 auf Summe
addiere 10 auf Summe
addiere 50 auf Summe
Anweisung 4
Struktogramm für die Mehrfachauswahl
Variable = 1
2
3
sonst Anweisung 2
Anweisung 1
Anweisung 3
Anweisung 4
Struktogramm für das Münzbeispiel
Münze = Fünfer
Zehner
Fünfziger
sonst addiere 10
auf Summe addiere 5 auf
Summe
addiere 50 auf
Summe
gib Münze zurück
Mehrfachauswahl in Java
switch (Ausdruck) {case Wert1: Anweisung 1 break;
case Wert2: Anweisung 2 break;
...
default: Anweisung n+1;
break;
}
Java-Code für das Münzbeispiel
switch (coin) {
case 5: sum += 5; break;
case 10: sum += 10; break;
case 25: sum += 25; break;
default: giveBack(coin);
}
...
Beispiel: Java-Code für Notenzuweisung
switch ( grade ) {case ‘A’: case ‘a’:
++aCount;
break;
case ‘B’: case ‘b’:
++bCount;
break;
case ‘C’: case ‘c’:
++cCount;
break;
case ‘D’: case ‘d’:
++dCount;
break;
case ‘F’: case ‘f’:
++fCount;
break;
default:
showStatus( “Incorrrect grade.
Try again. “ );
break;
}
3.4.3 Wiederholungsanweisung (Iteration, Schleife)
Wiederholte Ausführung einer Anweisung (oder einer Folge von Anweisungen), bis eine Endbedingung erfüllt ist.
wiederhole
Anweisung(en) bis Bedingung
Beispiele für Schleifen
Beispiel 1
Lies den gesuchten Namen ein.
Hole den ersten Namen aus der Liste.
wiederhole
falls Name der Gesuchte ist dann gib die Adresse aus
sonst
hole den nächsten Namen aus der Liste
bis der gesuchte Name gefunden oder die Namensliste erschöpft ist
Beispiel 2
Summe = 0 wiederhole
addiere 1 auf Summe
bis Summe*Summe < sqrNumber
Flussdiagramm
Anweisung
Abbruchbe - dingung
erfüllt
nicht erfüllt
Flussdiagramm für die Summation
sum = sum + 1
sum * sum
>=
sqrNumber sum = 0
erfüllt
nicht erfüllt
Struktogramm für die Schleife
Abbruchbedingung
Struktogramm für die Summation
sum * sum >= sqrNumber
sum = 0
sum = sum + 1
Pseudocode für Quadratzahlen
{Berechne die Quadratzahlen von 1-10}
setze Wert auf 0 wiederhole
addiere 1 auf Wert
drucke Wert und Wert*Wert bis Wert = 10
Bemerkung:
Die Abbruchbedingung wird nach der Anweisung geprüft.
Spezialfall: Endlosschleife (1)
Wiederhole
Anweisung(en) immer
Beispiel:
Wiederhole
Falls Reaktortemperatur zu hoch dann fahre Bremsstäbe ein
immer
Wiederhole
Schalte Ampeln fort immer
Spezialfall: Endlosschleife (2)
Anmerkung 1
Endlosschleifen sind häufig unbeabsichtigt, weil die Ab- bruchbedingung nicht korrekt formuliert wurde.
Anmerkung 2
Endlose Algorithmen können korrekt sein, wenn sie ein über die Zeit stets wiederkehrendes Problem stets auf's Neue lösen. Sie sind nicht korrekt, wenn sie unendlich lange brauchen, um ein Problem zu lösen.
Beispiel: Newton-Iteration zur Integralberechnung ohne Abbruchschranke
Spezialfall: Endlosschleife (3)
y
= f(x)
x x x x x x x x
1 3 2
4 5 6 7
Approximation eines Integrals Abbruchbedingung: | - | <Σ Σ ε
neu alt
Zweite Form der Iteration
Solange Bedingung führe ausAnweisung(en) // Rumpf der Schleife
Anweisung n + 1 // nicht mehr in der Schleife
Die Anweisungen in der Schleife werden ausgeführt, solange die Bedingung erfüllt ist.
Unterschied zur ersten Form: Die Bedingung wird vor der Ausführung des Schleifenrumpfes geprüft. Deshalb ist diese Form vorzuziehen, wenn damit gerechnet wer- den muss, dass in manchen Fällen bereits beim erstma- ligen Eintritt in die Schleife die Abbruchbedingung erfüllt ist, der Schleifenrumpf also nicht ausgeführt werden soll.
Die beiden Formen der Iteration sind äquivalent, sie las- sen sich (unter Verwendung der bedingten Anweisung) ineinander überführen (⇒ Übungsaufgabe).
Erste Form vs. zweite Form der Iteration
Beispiel: Suche größte Zahl aus einer Liste Algorithmus 1
Setze die erste Zahl der Liste als bislang größte Zahl
Wiederhole
Lese die nächste Zahl der Liste
Falls diese Zahl > bislang größte Zahl dann setze diese Zahl als bislang größte Zahl
bis Liste erschöpft ist
Schreibe die bislang größte Zahl nieder
Dieser Algorithmus ist inkorrekt, falls eine gegebene Li- ste nur eine Zahl enthält! Daher:
Algorithmus 2
Setze die erste Zahl der Liste als bislang größte Zahl
Solange die Liste nicht erschöpft ist führe aus
Lese die nächste Zahl der Liste
Falls diese Zahl > bislang größte Zahl dann setze diese Zahl als bislang
Java-Code für die Summation
sum = 0;do {
sum++;
System.out.println(sum);
} while (sum * sum < 10);
sum = 0;
while (sum * sum < 10) { sum++;
System.out.println(sum);
}
Schleifenbeispiele (1)
yPos = 25;
for (int counter = 1; counter <= 10;
counter++) {
g.drawString(Integer.toString(counter), 25 yPos);
yPos += 15;
}
ist äquivalent zu
for (int counter = 1; counter <= 10;
counter = counter + 1) {
… }
und zu
for (int counter = 1; counter <= 10;
counter += 1) {
… }
und zu
for ( int counter = 1; counter <= 10;
++counter) {
Schleifenbeispiele (2)
x = 2;y = 10;
for (int j = x; j <= 4 * x * y; j += y/x) ist äquivalent zu
for (int j = 2; j <= 80; j += 5)
Schleifenbeispiele (3)
for ( int i = 1; i <= 100; i++ ) for ( int i = 100; i >= 1; i-- ) for ( int i = 7; i <= 77; i += 7 ) for ( int i = 20; i >= 2; i -= 2 ) for ( int j = 2; j <= 20; j += 3 ) for ( int j = 99; j >= 0; j -= 11 ) for ( int number = 2; number <= 100;
sum += number, number += 2 ) for ( int sum = 0, number = 2;
number <= 100;
sum += number, number += 2 )
Äquivalenz von
for-und
while-Schleifen
for ( Ausdruck1; Ausdruck2; Ausdruck3 )Anweisung ist äquivalent zu
=> Übungsaufgabe!
Java-Code für Grafikbeispiel
import java.awt.Graphics;import java.applet.Applet;
public class ContinueTest extends Applet { public void paint(Graphics g)
{
int xPos = 25;
for ( int count = 1; count <= 10;
count++ ) {
if ( count == 5 ) { continue;
}
g.drawString(Integer.toString(count), xPos, 25);
xPos += 10;
} } }
Resultat
1 2 3 4 6 7 8 9 10
Die Anweisung continue dient dazu, das Ausdrucken der 5 zu unterbinden.
Zusammenfassung der Ablaufsteuerung (Kontrollstrukturen)
a) Sequenz (Folge) b) Selektion
c) Iteration
Diese drei Konstrukte genügen, um jeden erdenklichen Algorithmus auszudrücken!
(Beweis: Theoretische Informatik)
3.5 Modularität
3.5.1 Das Konzept der Modularität
FrageIst es möglich, Teile aus einem Algorithmus an anderer Stelle, in demselben oder in einem anderen Algorith- mus, wieder zu verwenden?
Ist es möglich, Teile aus dem Algorithmus so voneinan- der abzugrenzen, dass sie unabhängig voneinander entwickelt (verfeinert) werden können?
⇒ Modularität
Ein Modul ist ein Teilalgorithmus, der ein Teilproblem selbständig löst, ohne auf außerhalb liegende Schritte des Hauptalgorithmus Bezug zu nehmen. Module sind die Bausteine von Algorithmen.
Die Wirkung eines Moduls wird über Parameter gesteu- ert. Beispiele für Module sind Unterprogramme (Proze- duren, Funktionen) in traditionellen Programmierspra- chen und Klassen in Java.
Beispiele für Module (1)
BeispielAlgorithmus "koche Kaffee" und Algorithmus "koche
Tee" benutzen beide das Modul "koche Wasser" mit den Schritten
{ koche_Wasser }
(1.1.1.) Stelle Kessel unter Wasserhahn (1.1.2.) Drehe Wasserhahn auf
(1.1.3.) Warte, bis Kessel voll ist (1.1.4.) Drehe Wasserhahn zu (1.2.) Schalte Kessel an
(1.3.) Warte, bis Kessel pfeift (1.4.) Schalte Kessel aus
Beispiele für Module (2)
{Algorithmus zur Zubereitung einer Tasse Kaffee}
(1.) rufe Modul koche_Wasser auf {Gib Kaffeepulver in die Tasse}
(2.1.1.) Nehme Kaffeeglas aus dem Fach (2.1.2.) Entferne Deckel vom Kaffeeglas (2.2.) Entnehme einen Löffel Kaffee (2.3.) Kippe Löffel in die Tasse
(2.4.1.) Setze Deckel auf das Kaffeeglas (2.4.2.) Stelle Kaffeeglas in das Fach zurück {Fülle Wasser in die Tasse}
(3.1. Gieße Wasser aus dem Kessel in die Tasse, bis die Tasse voll ist
{Algorithmus zur Zubereitung einer Tasse Tee}
(1.) rufe Modul koche_Wasser auf {Gib den Teebeutel in die Tasse}
(2.1) Öffne die Packung mit den Teebeuteln (2.2) Entnimm einen Teebeutel
(2.3) Lege den Teebeutel in die Tasse {Fülle Wasser in die Tasse}
(3.1.) Gieße Wasser aus dem Kessel in die Tasse, bis die Tasse voll ist
Beispiele für Module (3)
Algorithmus "Zeichne zwei Quadrate"Positionieren auf Punkt A
Quadrat der Seitenlänge 10 cm zeichnen Positionieren auf Punkt B
Quadrat der Seitenlänge 20 cm zeichnen
B
A
20 cm 10 cm x
Definition des Moduls
Definiere das Zeichnen eines Quadrates als Modul mit dem Parameter "Seitenlänge".
Annahme: Der Plotter kann die folgenden Befehle un- mittelbar ausführen:
bewege (x) bewege Stift x cm vorwärts
links (x) drehe Stift um x Grad nach links rechts (x) drehe Stift um x Grad nach rechts Stift heben hebe den Stift vom Papier
Stift senken senke den Stift auf das Papier.
Dann kann das Modul "Quadratzeichnen" wie folgt defi- niert werden:
Modul Quadratzeichnen (Größe)
{Zeichnet ein Quadrat mit der Seitenlänge Größe cm.
Das Quadrat wird gegen den Uhrzeigersinn gezeichnet, beginnend mit der aktuellen Position des Zeichenstiftes.
Die erste Kante wird entsprechend der aktuellen Aus- richtung des Zeichenstiftes gezeichnet. Der Zeichenstift wird auf den Ausgangspunkt zurückgesetzt und abge- hoben.}
Stift senken
Wiederhole 4-mal Bewege (Größe) Links (90)
Stift heben
Parameter
Bei der Definition eines Moduls: formale Parameter Beim Aufruf eines Moduls: aktuelle Parameter
Anzahl, Reihenfolge und Datentyp müssen überein- stimmen.
Damit kann der Algorithmus "Zeichne zwei Quadrate"
geschrieben werden als:
(1) Positioniere Stift auf Punkt A (2) Quadratzeichnen (10)
(3) Positioniere Stift auf Punkt B (4) Quadratzeichnen (20)
Modulares Zeichenprogramm
ProgrammablaufB
A A A
nach (1)
nach (2)
nach (3)
nach (4)
A B
Dokumentieren von Modulen
Neben den explizit angegebenen Parametern sollten die impliziten Annahmen, die ein Modul macht, sorgfältig dokumentiert sein! Beispiel: Zeichenrichtung der ersten Kante. Am besten als Kommentar im Kopf (Header) des Moduls. Ebenso müssen Nebeneffekte nach Möglichkeit vermieden oder sorgfältig dokumentiert sein.
Die Schritte (1) und (3) können im Zuge der schrittwei- sen Verfeinerung ebenfalls als Module beschrieben oder direkt in Befehle an den Stift des Plotters aufgelöst wer- den.
3.5.2 Modularität in Java: Klassen und Objekte
Mit einer Klassendeklaration definiert man neue Refe- renztypen und legt gleichzeitig deren Implementation fest. Jede Klasse (außer Object) ist implizit Subklasse der Java-Klasse Object; es gibt also eine gemeinsame Wurzel der Java-Vererbungshierachie. Im Rumpf einer Klasse kann man folgendes deklarieren:• Variablen eines beliebigen Typs, in denen man Ob- jektzustände speichert,
• Methoden, das sind die Operationen, die auf Objekte der Klasse angewendet werden können, die also das Objektverhalten implementieren,
• Konstruktoren, das sind spezielle Methoden, mit de- nen man die Variablen initialisiert,
• Static-Initialisierer, das sind spezielle Anwei-
sungsfolgen, die nach dem Laden der Klasse (eben- falls zum Zweck der Initialisierung) einmal ausgeführt werden, und
• eingebettete Klassen, das sind Klassen, die wieder einen neuen Referenztyp deklarieren; meistens han- delt es sich dabei um Hilfsklassen, die man nur lokal benötigt.
Klassenvariable vs. Instanzvariable
Variablen können als Klassenvariablen deklariert wer- den, die Java einmal pro Klasse anlegt, oder als In- stanzvariablen, die für jedes Objekt neu angelegt wer- den.
Ebenso ist es möglich, eine Methode als Klassenme- thode zu deklarieren, die ohne Objekt aufgerufen wird, oder als Instanzmethode, die immer für ein bestimmtes Objekt aufgerufen wird. Der Unterschied wird durch die Verwendung des Modifizierers static ausgedrückt.
Konstruktoren sind Methoden sehr ähnlich, werden aber nicht mit der für Methoden üblichen Aufrufsyntax aufgerufen.
Klassendeklaration
Eine Klassendeklaration spezifiziert einen neuen Refe- renztypen. Die Deklaration kann aus bis zu sechs Teilen bestehen:
• optionalen Modifizierern, wie public, abstract, final, usw., die spezielle Attribute der Klasse (z.B.
Zugriffsrechte) festlegen,
• dem Schlüsselwort class,
• einem Bezeichner, mit dem die Klasse benannt wird,
• einem optionalen extends mit nachfolgender Angabe einer Superklasse,
• einem optionalen implements mit nachfolgender Li- ste von Interfaces, und
• dem Klassenrumpf, der – in { ... } eingeschlossen – die Deklaration einer beliebigen Anzahl von Variablen, Methoden, Konstruktoren, Initialisierern und einge- betteten Klassen enthält.
Beispiel für eine Klassendeklaration
Beispielsweise wird mit der folgenden Klassendeklarati- on eine Klasse Timer deklariert, die drei Variablen min, sek und tsdSek enthält. Weiterhin sind drei Methoden stelle, tick und zeigeAn deklariert. Sämtliche oben als optional genannten Bestandteile fehlen.
class Timer {
int min, sek, tsdSek;
void stelle(int i, int j, int k) { min = i;
sek = j;
tsdSek = k;
}
void tick() {
if (++tsdSek == 1000) { tsdSek = 0;
if (++sek == 60) { sek = 0;
min++;
} } }
Geltungsbereich von Klassenelementen
Der Geltungsbereich des Namens eines Klassenele- ments – der Code, in dem man das Element einfach durch seinen Bezeichner ansprechen kann – ist der gesamte Rumpf der Klassendeklaration. Man kann die einzelnen Deklarationen der Klassenelemente daher im Rumpf beliebig anordnen.Nach der Deklaration der Timer-Klasse ist es möglich, ihren Namen zur Deklaration von Variablen wie jeden anderen Typnamen (zum Beispiel int, double) zu be- nutzen. Es muß aber beachtet werden, dass es sich um einen Referenztyp handelt, dass also mittels
Timer t;
kein Timer-Objekt erzeugt wird, sondern, daß t lediglich eine Variable ist, die eine Referenz auf ein solches Ob- jekt enthalten kann. Ein Objekt wird erst durch explizites new in einem „Instanzerzeugungs-Ausdruck“, z.B.
t = new Timer();
erzeugt. Nach der Objekterzeugung sind die Instanz- und Klassenvariablen mit Standardwerten initialisiert. Im Beispiel haben alle drei Variablen min, sek und
tsdSek den Wert 0.
Zugriff auf Klassenelemente
Innerhalb der Klassendeklaration kann man auf die Klassenelemente mit ihrem Bezeichner zugreifen. Im Timer-Beispiel wird in allen drei Methoden auf alle In- stanzvariablen zugegriffen.
Auch das Aufrufen einer Methode innerhalb einer ande- ren Methode ist möglich:
class X { void g() { ...
f();
}
void f() { ... } }
Beispiel für den Zugriff auf Klassenelemente
Code in anderen Klassen greift auf Instanzvariablen und Instanzmethoden mit einer Objektreferenz unter Ver- wendung von "." zu (als Interpunktionszeichen, das in diesem Kontext auch als Zugriffs-„Operator“ bezeichnet wird). Zum Beispiel:Timer t1, t2
t1 = new Timer();
t2 = new Timer();
t1.min =10;
t2.sek = 23;
t1.tsdSek = 5;
t2.stelle(10,23,5);
for (int i = 0; i < 1500; i++) t2.tick();
t1 10 23 5
min sek tsdSek
t2 10 24 505
min sek tsdSek
Zugriffsrechte auf Klassenelemente von außer- halb
Ob man, wie im letzten Beispiel, auf die Klassenele-
mente von außerhalb ihrer Klasse zugreifen kann, hängt davon ab, welche Zugriffsrechte vergeben wurden und von wo aus der Zugriff erfolgt. Zur expliziten Festlegung von Zugriffsrechten verwendet man die Modifizierer pu- blic, protected oder private. Üblicherweise spezi- fiziert man Instanzvariablen als private, um die in ih- nen gespeicherten Werte oder die von ihren referen- zierten Objekte zu „kapseln“, also:
Class Timer {
private int min, sek, tsdSek;
...
}
Auf die Variablen kann dann nur noch von Code inner- halb der Timer-Deklaration zugegriffen werden, und im Beispiel müssen die drei Zuweisungen durch einen Auf- ruf t1.stelle(10, 23, 5); ersetzt werden.
Verdeckung von Namen
Der Name einer Instanzvariablen kann durch eine lokale Variable oder einen Methoden- oder Konstruktorpara- meter desselben Namens verdeckt werden. Unter Ver- wendung des Schlüsselworts this kann man dann dennoch auf die verdeckte Variable zugreifen. Dies dient nicht der Verständlichkeit des Codes, ist aber bei Konstruktoren gängige Java-Praxis.
Zum Beispiel wird hier der Wert des Parameters i bzw.
der lokalen Variable i an die Instanzvariable i zugewie- sen:
class X {
private int i;
X(int i) { this.i = i;
}
void f() { int i = 10;
this.i += i;
} }
Instanz- und Klassenvariable
Wie bei lokalen Variablen besteht die Deklaration einer Variablen, die Klassenelement ist, aus optionalen Modi- fizierern, einer Typangabe und einem Bezeichner mit optionalem Initialisierer. Im Unterschied zu lokalen Va- riablen, die mittels Modifizierer final als symbolische Konstante spezifiziert werden können, sind hier auch noch die Modifizierer static und transient sowie einer der Zugriffsrechtspezifizierer public, protected oder private zulässig.
Wenn man eine Variable als static deklariert, wird sie zur Klassenvariablen. Dies bedeutet, daß es genau ei- ne derartige Variable gibt, gleichgültig, wieviele Objekte der Klasse existieren. Klassenvariablen werden beim Laden der Klasse erzeugt.
Instanzvariablen sind Variablen, die nicht als static deklariert wurden. Sie werden für jedes Objekt bei sei- ner Erzeugung angelegt.
Klassenvariable im Timer-Beispiel
Wir erweitern die Klasse Timer um eine Klassenvaria- ble anzTicks, die immer die Anzahl der tick-Aufrufe enthalten soll:
class StatTimer {
static long anzTicks = 0;
private int min, sek, tsdSek;
void stelle(int i, int j, int k,) { .... unverändert }
void tick(){
anzTicks++;
... Rest unverändert }
void zeigeAn() { ... unverändert } }
Wir sehen, dass innerhalb der Klassendeklaration auf eine Klassenvariable wie auf eine Instanzvariable ein- fach mit ihrem Bezeichner zugegriffen werden kann.
Zugriff auf Klassenvariable
Am folgenden Beispiel erkennt man weiterhin, dass von außerhalb der Klasse mittels einer Objektreferenz oder sinnvoller, weil eine Klassenvariable unabhängig von Objekten existiert, auch mit dem Klassennamen an- stelle einer Objektreferenz zugegriffen werden kann:
// Zugriff ohne Objektreferenz
out.println("Anzahl ticks: " + StatTimer.anzTicks);
StatTimer s1 = new StatTimer(), s2 = new StatTimer();
for (int i = 0; i < 21500; i++) s1.tick();
for (int i = 0; i < 821500; i++) s2.tick();
// Zugriff mit Objektreferenz
out.println("Anzahl ticks: " + StatTimer.anzTicks + "\t" + s1.anzTicks + "\t" + s2.anzTicks);
Zugriff auf Klassenvariable per Objektreferenz
Programmierhinweis
Der Zugriff auf eine Klassenvariable über eine Objekt- referent ist zwar erlaubt, aber nicht zu empfehlen, da ir- reführend!
Nach Bearbeitung des Beispiels von der vorigen Seite finden wir im Speicher:
s1 0 21 500
min sek tsdSek
s2 13 41 500
min sek tsdSek
843000
StatTimer.anzTicks
Die Initialisierung von Variablen
Eine Variablendeklaration kann einen Initialisierer ent- halten. Ein Initialisierer ist ein Ausdruck oder – bei
Feldtypen – eine Initialisiererliste. Er folgt nach dem Va- riablenbezeichner und dem Zuweisungsoperator =.
Semantisch wird die Initialisierung wie die Zuweisung des Werts des Ausdrucks an die deklarierte Variable bzw. – bei Feldtypen – der Werte der Elemente der In- itialisiererliste an die Feldkomponenten behandelt. In- itialisiererwerte müssen zuweisbar an die deklarierten Variablen sein. Die Initialisierung von Klassenvariablen (also von als static deklarierten Variablen) wird genau einmal, beim Initialisieren der Klasse nach dem Laden, vorgenommen. Instanzvariablen (also Variablen, die nicht als static deklariert sind) werden jedesmal,
wenn ein Objekt der Klasse dynamisch erzeugt wird, in- itialisiert.
Sind keine Initialisierer angegeben, werden die Stan- dardwerte für den jeweioigen Typ benutzt. Im StatTimer- Beispiel hätten wir also einfach static long anzTicks;
schreiben können, ohne daß sich dadurch etwas ändern würde.
Konstante Klassenelemente
Instanz- und Klassenvariablen können mit dem final- Modifizierer als symbolische Konstanten deklariert werden. In diesem Fall sollte die Deklaration einen In- itialisierer enthalten. Alternativ kann für als final de- klarierte Instanzvariablen in jedem Konstruktor bzw. für als final deklarierte Klassenvariablen in einem sta- tic-Initialisierer genau eine Zuweisung vorgenommen werden. Im letzteren Fall nennt man die Variable wieder
„unspezifiziert final“. Der Wert der final-
“Variablen“ kann nach ihrer Initialisierung bzw. der er- sten Zuweisung nicht mehr verändert werden.
Beispiel
class Rechnung {
static long nr = 0;
final long nummer = ++nr;
final Date datum = new Date();
...
}
Hier sind die Nummer der Rechnung und das Rech- nungsdatum als konstante Instanzvariablen deklariert, die bei der Erzeugung eines Rechnung-Objekts mit der nächsten Nummer bzw. dem aktuellen Datum initialisiert werden.
final
-Variable vom Referenztyp
Es ist zu beachten, dass eine final-Variable eines Referenztyps – wie oben die Instanzvariable datum – immer dasselbe Objekt referenziert, dass es aber mög- lich ist, das Objekt durch Methodenaufrufe zu verän- dern.
Da in Java Felder (arrays) Objekte sind, trifft dies auch auf Felder zu. Wenn eine final-Variable ein Feld refe- renziert, können die Feldkomponenten durch Operatio- nen auf dem Feld modifiziert werden; die Variable ver- weist aber immer auf dasselbe Feld.
Klassenspezifische Konstanten
In vielen Fällen wird man keine objektspezifischen, son- dern klassenspezifische Konstanten benötigen. Dann verwendet man die Modifizierer static und final gemeinsam. Die übliche Reihenfolge bei der Benutzung mehrerer Modifizierer ist public static final bzw.
private static final.
Methoden
Java-Methoden können nur innerhalb von Klassen- rümpfen deklariert werden. Sie spezifizieren ausführba- ren Code, der durch einen Methodenaufruf ausgeführt wird, bei dem eine feste Anzahl von Werten als Argu- mente übergeben werden. Eine Methodendeklaration besteht aus bis zu sechs Teilen:
• optionalen Modifizierern, wie public, abstract, final, usw., die wie gewohnt spezielle Attribute der Methode festlegen,
• dem Typ des Resultats, das bei einem Methodenauf- ruf berechnet wird,
• der Liste der Methodenparameter, mit denen die Werte, die bei einem Aufruf zu übergeben sind, spezi- fiziert werden,
• einer optionalen throws-Klausel, die anzeigt, wel- che Ausnahmen durch einen Aufruf der Methode aus- geworfen werden können, und
• den Methodenrumpf, der – in { ... } eingeschlossen – die Folge der Anweisungen enthält.
Rückgabe von Resultaten
Als Typ des Resultats ist jeder Java-Typ zulässig. Oder es kann mittels void angezeigt werden, dass die Me- thode keinen Wert liefert.
Bei Resultaten eines Feldtyps sind auch Schreibweisen wie int f()[] { ...} als Äquivalent für int [] f() { ... } zuläs- sig.
Die formalen Parameter einer Methode werden (falls vorhanden) durch eine Liste von Typen und Bezeich- nern spezifiziert, die durch Kommas getrennt sind. Die Parameter können als final deklariert werden; dann ist jede Zuweisung an sie im Methodenrumpf ein Fehler.
Methodenaufruf
Ein Methodenaufruf ist, syntaktisch gesehen, ein ele- mentarer Ausdruck. Durch Abschluss mit einem ";"
wird der Aufruf zu einer Anweisung (einer Ausdruck- sanweisung).
Bei jedem Aufruf einer Methode werden die Parameter neu erzeugt und mit den aktuellen Werten initialisiert, bevor mit der Ausführung des Methodenrumpfes be- gonnen wird. Die Parameter haben als Geltungsbereich den gesamten Methodenrumpf und werden ansonsten wie lokale Variable behandelt; sie dürfe nicht durch Re- Deklarationen verdeckt werden.
Die Argumente werden als Werte übergeben und können daher durch den Aufruf nicht modifiziert werden; Java kennt nur einen „call by value“.
Auswirkungen außerhalb der Methode selbst
Ob die Veränderung eines Parameters innerhalb des Methodenrumpfes sich auf die aufrufende Methodeauswirkt, hängt davon ab, ob es sich bei seinem Typ um einen elementaren Typ oder einen Referenztyp han- delt.
Beispiel: Parameter vom Referenztyp
// Parameter.java import java.io.*;
class Parameter {
static void m(int i, Zaehler z) { i++;
z.inkrementiere();
}
public static void main(String[] args) { PrintWriter out = new
PrintWriter(System.out,true);
int a = 2;
Zaehler b = new Zaehler();
b.wert(2);
out.println(a + " " + b.wert());
m(a,b);
out.println(a + " " + b.wert());
}
}
Beim Aufruf von m werde die Parameter i und z er-
zeugt, in i wird der Wert von a, also 2, in z wird die Re- ferenz auf das Zaehler-Objekt b (die Adresse dieses Objekts) kopiert. Das Inkrementieren von i ändert ledig- lich den Parameterwert. Dagegen verweisen der Para-
Speicherinhalt zum Referenztypbeispiel
b a
i z
2
2 Wert
2
Die
return-Anweisung
Eine return-Anweisung beendet die Ausführung einer Methode bzw. eines Konstruktors und springt zu der auf- rufenden Methode bzw. zum aufrufenden Konstruktor zurück. Sie hat die Form
return Ausdruckopt ;
In der Form return; (also ohne Asudruck) darf sie nur in Methoden vorkommen, die mit void als Ergebnistyp deklariert wurden, oder in Konstruktoren. Der Metho- denaufruf liefert dann keinen Wert zurück. Falls eine Methode mit Ergebnistyp void keine return-
Anweisung enthält, fügt der Java-Compiler implizit als letzte Anweisung ein return; ein.
Eine return-Anweisung mit Ausdruck darf nur in einer Methode stehen, die nicht mit void als Typ des Ergeb- nisses deklariert ist. Eine Methode mit einem Ergeb- nistyp ungleich void muss mit einem expliziten return und der Rückgabe eines Werts beendet werden. Bei ei- nem return mit Ausdruck muss der Typ des Ausdrucks zuweisungskompatibel zum Ergebnistyp der Methode
Klassenmethoden
So wie es instanz- und klassenspezifische Variable gibt, gibt es auch instanz- und klassenspezifische Methoden.
Klassenspezifische Methoden werden mit static de- klariert. Die bedeutet, dass die Methode nicht für ein be- stimmtes Objekt, sondern unabhängig von Objektin-
stanzen für die Klasse aufgerufen wird. Wie bei Klas- senvariablen benutzt man dann den Klassennamen an- stelle eines Objektnamens (Instanznamens).
Restriktionen für Klassenmethoden (1)
Für Klassenmethoden gibt es eine Reihen von Restrik- tionen. So können sie nur auf Klassenvariablen ihrer Klasse zugreifen und dürfen das Schlüsselwort this nicht verwenden.Beispiel
class X{
int x;
static int y;
static void f() {x++}; // Fehler: x
// ist nicht static static void g( {y++ } // korrekt: y
// ist static ....
}
Der Grund für diese Einschränkung ist offensichtlich:
Wenn f nicht für ein bestimmtes Objekt aufgerufen wird, ist this undefiniert. In unserem Beispiel wäre bei der Existenz von mehreren Instanzen nicht klar, in welcher Instanz die Instanzvariable x inkrementiert werden soll.
Restriktionen für Klassenmethoden (2)
Klassenmethoden können nur andere Klassenmethoden ihrer Klasse aufrufen.
Beispiel
class X{
int x;
static void f1() {g()} // korrekt:
// g ist static static void f2() {h()} // Fehler:
// h ist nicht static static void g(...}
....
}
Hier setzt Java den Aufruf von g wie X.g(); um. Der Aufruf von h scheitert hier, weil er eine Objektreferenz benötigt, über die eine static-Methode aber nicht verfügt.
Klassenmethoden können darüber hinaus nicht als ab- stract deklariert werden.
Beispiele für vordefinierte Klassenmethoden
Ein Beispiel für eine Klasse, die nur static-Variable und Methoden deklariert, ist Math. Wir haben u.a. die Konstante PI und die Methoden random und sqrt die- ser Klasse benutzt. Aus der Klasse Integer haben wir die Klassenmethoden parseInt und toHexString aufgerufen.in und out sind Klassenvariablen der Klasse System, die wir wiederholt bei der Deklaration von Ein- bzw.
Ausgabeobjekten verwendet haben. Beide haben einen Referenztyp: in hat den Typ InputStream, out den Typ PrintStream.
Schließlich ist main die Klassenmethode, die von der Java-VM für die initiale Klasse aufgerufen wird, mit der also stets die Programmausführung beginnt.
Erzeugen und Vernichten von Objekten
Objekte werden in Java immer dynamisch, also zurLaufzeit im Hauptspeicher erzeugt. Um die Freigabe von Speicher müssen wir uns dennoch nicht kümmern. Dies ist die Aufgabe des Garbage-Collectors, Er sorgt dafür, dass nicht mehr referenzierte Objekte automatisch im Speicher gelöscht werden.
Objekte werden typischerweise mit dem Schlüsselwort new in einem elementaren Ausdruck erzeugt. Diesen Weg haben wir in allen bisher betrachteten Beispielen gewählt und z.B. z = new Z(); geschrieben.
Es ist auch möglich, die völlig analog arbeitende Metho- de newInstance zusammen mit dem entsprechenden Klassenliteral einzusetzen, und statt dessen die Form
z = (Z)Z.class.newInstance();
zu benutzen.
Diese beiden Vorgehensweisen werden als explizite Objekterzeugung bezeichnet.
Implizite Erzeugung von Objekten
Darüber hinaus können Objekte implizit erzeugt wer- den, beispielsweise String-Objekte zur Aufnahme der Werte von Zeichenketten oder im Zusammenhang mit der Auswertung des Operators +, Felder, wenn eine In- itialisiererliste angegeben ist, beliebige Objekt, wenn wir clone aufrufen, usw.
In allen Fällen wird nach der Reservierung des benötig- ten Speicherplatzes und dem Eintragen der Standard- werte in die Instanzvariablen ein Konstruktor der ent- sprechenden Klasse aufgerufen. Dies ist eine spezielle Methode, die den Namen der Klasse trägt und keinen Ergebnistyp deklariert.
Beispiel für einen Konstruktor
Wir versehen die Klasse Timer mit zwei Konstruktoren:
class Timer {
private int min, sek, tsdSek;
Timer() {
min = sek = tsdSek = 0;
}
Timer(int min, int sek, int tsdSek) { this.min = min;
this.sek = sek;
this.tsdSek = tsdSek;
}
... sonst alles unverändert }
Nun wird bei einer Anwendung der Art Timer t1 = new Timer(),
t2 = new Timer (10, 23, 5);
zur Erzeugung des ersten, von t1 referenzierten Ob- jekts der erste Konsruktor Timer() aufgerufen, das zweite Objekt wird mittels Timer(int, int, int) konstruiert.
Der Standardkonstruktor
Wenn wir eine Klasse X ohne Konstruktor deklarieren, erzeugt Java einen Standardkonstruktor, der die Form X() { super(), }
hat. Sofern wir mindestens einen Konstruktor imple- mentieren, entfällt diese implizite Deklaration. Auch ein von uns deklarierter Konstruktor ohne Parameterliste, wie oben der erste Timer-Konstruktor, wird oft als Standardkonstruktor bezeichnet.
Initialisierungsblöcke
Es ist möglich, innerhalb einer Klasse Initialisierungs- blöcke zu deklarieren. Das sind Codeblöcke, die auf das Schlüsselwort static folgen:
static Block;
Initialisierungsblöcke werden genau einmal ausgeführt, wenn die Klasse geladen wird.
Die Initialisierung von Klassenvariablen und das Aus- werten von static-Initialisierungsblöcken wird in der Reihenfolge vorgenommen, in der sie in der Klassende- klaration aufeinander folgen. Man kann static-
Initialisierungsblöcke beispielsweise dazu verwenden, Klassenvariable, die Felder, Mengen oder Listen sind, mit Anfangswerten zu versehen. Auch als unspezifiziert final deklarierte Klassenvariablen müssen auf diese Weise initialisiert werden.