Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Sprachbeschreibung
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Mathematisches
6 Eigene Klassen schreiben
7 Angewandte Objektorientierung
8 Exceptions
9 Die Funktionsbibliothek
10 Threads und nebenläufige Programmierung
11 Raum und Zeit
12 Datenstrukturen und Algorithmen
13 Dateien und Datenströme
14 Die eXtensible Markup Language (XML)
15 Grafische Oberflächen mit Swing
16 Grafikprogrammierung
17 Netzwerkprogrammierung
18 Verteilte Programmierung mit RMI und Web-Services
19 JavaServer Pages und Servlets
20 Applets
21 Midlets und die Java ME
22 Datenbankmanagement mit JDBC
23 Reflection und Annotationen
24 Logging und Monitoring
25 Sicherheitskonzepte
26 Java Native Interface (JNI)
27 Dienstprogramme für die Java-Umgebung
A Die Begleit-DVD
Stichwort

Download:
- ZIP, ca. 12,5 MB
Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren mit der Java Standard Edition Version 6
Buch: Java ist auch eine Insel

Java ist auch eine Insel
7., aktualisierte Auflage
geb., mit DVD (November 2007)
1.492 S., 49,90 Euro
Galileo Computing
ISBN 978-3-8362-1146-8
Pfeil 2 Sprachbeschreibung
Pfeil 2.1 Elemente der Programmiersprache Java
Pfeil 2.1.1 Textkodierung durch Unicode-Zeichen
Pfeil 2.1.2 Literale
Pfeil 2.1.3 Bezeichner
Pfeil 2.1.4 Reservierte Schlüsselwörter
Pfeil 2.1.5 Token
Pfeil 2.1.6 Kommentare
Pfeil 2.2 Anweisungen formen Programme
Pfeil 2.2.1 Anweisungen
Pfeil 2.2.2 Eine Klasse bildet den Rahmen
Pfeil 2.2.3 Die Reise beginnt am main()
Pfeil 2.2.4 Programme übersetzen und starten
Pfeil 2.2.5 Funktionsaufrufe als Ausdrücke und Anweisungen
Pfeil 2.2.6 print(), println() und printf() für Bildschirmausgaben
Pfeil 2.2.7 Modifizierer
Pfeil 2.2.8 Anweisungen und Blöcke
Pfeil 2.3 Datentypen
Pfeil 2.3.1 Primitive Datentypen im Überblick
Pfeil 2.3.2 Wahrheitswerte
Pfeil 2.3.3 Variablendeklarationen
Pfeil 2.3.4 Ganzzahlige Datentypen
Pfeil 2.3.5 Die Fließkommazahlen float und double
Pfeil 2.3.6 Alphanumerische Zeichen
Pfeil 2.4 Ausdrücke, Operanden und Operatoren
Pfeil 2.4.1 Zuweisungsoperator
Pfeil 2.4.2 Arithmetische Operatoren
Pfeil 2.4.3 Unäres Minus und Plus
Pfeil 2.4.4 Zuweisung mit Operation
Pfeil 2.4.5 Präfix- oder Postfix-Inkrement und -Dekrement
Pfeil 2.4.6 Die relationalen Operatoren und die Gleichheitsoperatoren
Pfeil 2.4.7 Logische Operatoren Und, Oder, Xor, Nicht
Pfeil 2.4.8 Rang der Operatoren in der Auswertungsreihenfolge
Pfeil 2.4.9 Die Typanpassung (das Casting)
Pfeil 2.4.10 Überladenes Plus für Strings
Pfeil 2.4.11 Was C(++)-Programmierer vermissen könnten
Pfeil 2.5 Bedingte Anweisungen oder Fallunterscheidungen
Pfeil 2.5.1 Die if-Anweisung
Pfeil 2.5.2 Die Alternative mit einer if/else-Anweisung wählen
Pfeil 2.5.3 Die switch-Anweisung bietet die Alternative
Pfeil 2.6 Schleifen
Pfeil 2.6.1 Die while-Schleife
Pfeil 2.6.2 Schleifenbedingungen und Vergleiche mit ==
Pfeil 2.6.3 Die do-while-Schleife
Pfeil 2.6.4 Die for-Schleife
Pfeil 2.6.5 Ausbruch planen mit break und Wiedereinstieg mit continue
Pfeil 2.6.6 break und continue mit Sprungmarken
Pfeil 2.7 Methoden einer Klasse
Pfeil 2.7.1 Bestandteil einer Funktion
Pfeil 2.7.2 Beschreibungen in der Java-API
Pfeil 2.7.3 Aufruf einer Methode
Pfeil 2.7.4 Methoden ohne Parameter
Pfeil 2.7.5 Statische Funktionen (Klassenmethoden)
Pfeil 2.7.6 Parameter, Argument und Wertübergabe
Pfeil 2.7.7 Methoden vorzeitig mit return beenden
Pfeil 2.7.8 Nicht erreichbarer Quellcode bei Funktionen
Pfeil 2.7.9 Rückgabewerte
Pfeil 2.7.10 Methoden überladen
Pfeil 2.7.11 Vorgegebener Wert für nicht aufgeführte Argumente
Pfeil 2.7.12 Finale lokale Variablen
Pfeil 2.7.13 Rekursive Funktionen
Pfeil 2.7.14 Die Türme von Hanoi
Pfeil 2.8 Weitere Operatoren
Pfeil 2.8.1 Bits und Bytes
Pfeil 2.8.2 Operationen auf Bit-Ebene
Pfeil 2.8.3 Die Verschiebeoperatoren
Pfeil 2.8.4 Ein Bit setzen, löschen, umdrehen und testen
Pfeil 2.8.5 Bit-Funktionen der Integer- und Long-Klasse
Pfeil 2.8.6 Der Bedingungsoperator
Pfeil 2.9 Einfache Benutzereingaben
Pfeil 2.10 Zum Weiterlesen


Galileo Computing - Zum Seitenanfang

2.7 Methoden einer Klasse Zur nächsten ÜberschriftZur vorigen Überschrift

In objektorientierten Programmen interagieren zur Laufzeit Objekte miteinander und senden sich gegenseitig Nachrichten als Aufforderung, etwas zu machen. Diese Aufforderungen resultieren in einem Methodenaufruf, in dem Anweisungen stehen, die dann ausgeführt werden. Das Angebot eines Objekts, also das, was es »kann«, wird in Java durch Methoden ausgedrückt. Die Begriffe »Methode« und »Funktion« wollen wir in diesem Tutorial gleichwertig benutzen.

Wir haben schon mindestens eine Methode kennen gelernt: println(). Sie ist eine Methode vom out-Objekt. Ein anderes Programmstück schickt nun eine Nachricht an das out-Objekt, die println()-Methode auszuführen. Im Folgenden werden wir den aktiven Teil des Nachrichtenversendens nicht mehr so genau betrachten, sondern wir sagen nur noch, dass eine Methode aufgerufen wird.

Zur Deklaration von Funktionen gibt es drei Gründe:

  • Wiederkehrende Programmteile sollen nicht immer wieder programmiert, sondern an einer Stelle angeboten werden. Änderungen an der Funktionalität lassen sich dann leichter durchführen, wenn der Code lokal zusammengefasst ist.
  • Komplexe Programme werden in kleine Teilprogramme zerlegt, damit die Komplexität des Programms heruntergebrochen wird. Damit ist der Kontrollfluss leichter zu erkennen. In klassischen Programmen heißen die Methoden daher auch Unterprogramme. Dieses Wort wollen wir hier allerdings nicht benutzen.
  • Die Operationen einer Klasse, also das Angebot eines Objekts, sind ein Grund für Funktionsdeklarationen in einer objektorientierten Programmiersprache. Daneben gibt es aber noch weitere Gründe, die für Methoden sprechen. Sie werden im Folgenden erläutert.

Galileo Computing - Zum Seitenanfang

2.7.1 Bestandteil einer Funktion Zur nächsten ÜberschriftZur vorigen Überschrift

Eine Funktion besteht aus mehreren Bestandteilen. Dazu gehören der Methodenkopf (kurz Kopf) und der Methodenrumpf (kurz Rumpf). Der Kopf besteht aus einem Rückgabetyp (auch Ergebnistyp genannt), dem Funktionsnamen und einer optionalen Parameterliste.


Beispiel Beispiel An der bekannten main()-Funktion lassen sich die Bestandteile ablesen:

public static void main( String[] args ) 
{ 
  System.out.println( "Im Rumpf" ); 
}

Die Methode liefert keine Rückgabe, daher ist der »Rückgabetyp« void. (An dieser Stelle sollte bemerkt werden, dass void in Java kein Typ ist.) Der Funktionsname ist main und die Parameterliste String[] args. Der Rumpf besteht nur aus der Bildschirmausgabe.


Signaturen einer Methode

Der Methodenname, die Parameter und die Typen der Parameter bestimmen die Signatur einer Methode – der Rückgabetyp gehört nicht dazu. Pro Klasse darf es nur eine Methode mit derselben Signatur geben, sonst meldet der Compiler einen Fehler.


Beispiel Beispiel Die Funktionen

void main( String[] args )

und

String main( String[] arguments )

haben die Signatur (main, String[]). Der Compiler sieht sie als gleich an und so können sie nicht zusammen in einer Klasse deklariert werden. Die Namen der Parameter spielen keine Rolle.



Galileo Computing - Zum Seitenanfang

2.7.2 Beschreibungen in der Java-API Zur nächsten ÜberschriftZur vorigen Überschrift

In der Java-Dokumentation sind alle Funktionen mit ihren Rückgaben und Parametern relativ genau definiert. Betrachten wir die Dokumentation der Funktion max der Klasse Math:

Abbildung 2.5 Die online API-Dokumentation für Math.max()

Die Hilfe gibt Informationen über die komplette Signatur der Methode. Der Rückgabetyp ist ein double, die Funktion heißt max(), und sie erwartet genau zwei double-Zahlen. Verschwiegen haben wir die Schlüsselwörter public static, die so genannten Modifizierer. public gibt die Sichtbarkeit an und sagt, wer diese Funktion nutzen kann. Im Fall von public bedeutet es, dass jeder diese Funktion verwenden kann. Das Gegenteil ist private. Dann kann nur das Objekt selbst diese Funktion nutzen. Das ist sinnvoll in dem Fall, wenn Funktionen benutzt werden, um die Komplexität zu verkleinern und Teilprobleme zu lösen. Private Funktionen werden in der Regel nicht in der Hilfe angezeigt. Das Schlüsselwort static zeigt an, dass sich die Funktion mit dem Klassennamen nutzen lässt, also kein Exemplar eines Objekts nötig ist.

Es gibt Funktionen, die noch andere Modifizierer und eine erweiterte Signatur besitzen. Ein weiteres Beispiel aus der API:

Abbildung 2.6 Ausschnitt aus der API-Dokumentation für die Klasse ServerSocket

Die Sichtbarkeit dieser Funktion ist protected. Das bedeutet: Nur abgeleitete Klassen und Klassen im gleichen Verzeichnis (Paket) können diese Funktion nutzen. Ein zusätzlicher Modifizierer ist final, der in einer Vererbung der Unterklasse nicht erlaubt, die Funktion zu überschreiben und ihr neuen Programmcode zu geben. Zum Schluss folgt hinter dem Schlüsselwort throws eine Ausnahme. Diese sagt etwas darüber aus, welche Fehler die Funktion verursachen kann und worum sich der Programmierer kümmern muss. Im Zusammenhang mit der Vererbung werden wir noch über protected und final sprechen. Dem Ausnahmezustand widmen wir das Kapitel 8. Die Dokumentation zeigt mit dem »Since: JDK 1.1« an, dass es die Funktion seit Java 1.1 gibt. Die Information kann auch an der Klasse festgemacht sein.


Galileo Computing - Zum Seitenanfang

2.7.3 Aufruf einer Methode Zur nächsten ÜberschriftZur vorigen Überschrift

Da eine Funktion immer zu einer Klasse gehört, muss deutlich sein, zu wem die Methode gehört. Im Fall von System.out.println() ist println() eine Methode vom out-Objekt. Wenn wir das Maximum zweier Gleitkommazahlen mit Math.max(a, b) bilden, dann ist max() eine Funktion der Klasse Math. Für den Aufrufer ist damit immer ersichtlich, wer diese Methode anbietet, also auch, wer diese Nachricht entgegennimmt. Was der Aufrufer nicht sieht, ist die Arbeitsweise der Funktion. Der Funktionsaufruf verzweigt in den Programmcode, aber der Aufrufer weiß nicht, was dort geschieht. Er betrachtet nur das Ergebnis.

Die aufgerufene Funktion wird mit ihrem Namen genannt. Die Parameterliste wird durch ein Klammerpaar umschlossen. Diese Klammern müssen auch dann gesetzt werden, wenn die Methode keine Parameter enthält. Eine Funktion wie System.out.println() gibt nichts als Ergebnis einer Berechnung zurück. Anders ist die Funktion max(); sie liefert ein Ergebnis. Damit ergeben sich vier unterschiedliche Typen von Funktionen:


Tabelle 2.8 Funktionen mit Rückgabewerten und Parametern
Funktion Ohne Rückgabewert Mit Rückgabewert

ohne Parameter

System.out.println()

System.currentTimeMillis()

mit Parameter

System.out.println(4)

Math.max(12, 33)


Die Methode System.currentTimeMillis() gibt die Anzahl der verstrichenen Millisekunden ab dem 1.1.1970 als long zurück.


Galileo Computing - Zum Seitenanfang

2.7.4 Methoden ohne Parameter Zur nächsten ÜberschriftZur vorigen Überschrift

Die einfachste Funktion besitzt keinen Rückgabewert und keine Parameter. Im mathematischen Sinn ist dann vielleicht auch der Name »Funktion« falsch, wenn sie keinen Wert zurückliefert, aber das soll uns nicht kümmern. Im klassischen Sinn ist dieser Typ von Funktion unter dem Namen »Prozedur« bekannt, die von der Aufgabe abstrahiert, indem sie Funktionalität hinter einem Namen verbirgt. Der Begriff »Prozedur« ist jedoch in der Objektwelt nicht anzutreffen.

Der Funktionscode steht in geschweiften Klammern hinter dem Kopf und bildet damit den Körper der Methode. Gibt eine Funktion nichts zurück (das mathematische Dilemma), dann wird void vor dem Funktionsnamen geschrieben. Falls die Funktion etwas zurückgibt, wird der Typ der Rückgabe anstelle von void geschrieben.


Beispiel Beispiel Eine Funktion ohne Rückgabe und Parameter, die etwas auf dem Bildschirm ausgibt.

Listing 2.17 SimpleFunction.java

class SimpleFunction 
{ 
  static void output() 
  { 
    System.out.println( "Toll hier im Java-Land" ); 
  } 
 
  public static void main( String[] args ) 
  { 
    output(); 
  } 
}

Am Aufruf der Funktion lässt sich ablesen, dass hier kein Objekt gefordert ist, das mit der Methode verbunden werden soll. Das ist möglich, denn die Funktion ist als static deklariert, und innerhalb der Klasse lassen sich alle Funktionen einfach mit ihrem Namen nutzen.


Tipp Tipp Die Vergabe eines Funktionsnamens ist gar nicht so einfach. Nehmen wir zum Beispiel an, wir wollen eine Funktion schreiben, die eine Datei kopiert. Spontan kommen uns zwei Wörter in den Sinn, die zu einem Funktionsnamen verbunden werden wollen: »file« und »copy«. Doch in welcher Kombination? Soll die Funktion copyFile() oder fileCopy() heißen? Wenn dieser Konflikt entsteht, sollte das Verb die Aktion anführen, unsere Wahl also auf copyFile() fallen. Funktionsnamen sollten immer das Tu-Wort vorne haben und das Was, das Objekt, an zweiter Stelle.


Eclipse
Eine gedrückte Keyboard Strg -Taste und ein Mausklick auf einen Bezeichner lässt Eclipse zur Deklaration springen. Ein Druck auf Keyboard F3 hat den gleichen Effekt. Steht der Cursor in unserem Beispiel auf dem Methodenaufruf output() und wird Keyboard F3 gedrückt, dann springt Eclipse zur Definition in Zeile 3 und hebt den Funktionsnamen hervor.


Galileo Computing - Zum Seitenanfang

2.7.5 Statische Funktionen (Klassenmethoden) Zur nächsten ÜberschriftZur vorigen Überschrift

Bisher arbeiten wir nur mit statischen Funktionen (auch Klassenmethoden genannt), also mit Methoden, die ohne ein erzeugtes Objekt auskommen. Leicht fällt das Schlüsselwort static unter den Tisch, denn static muss nicht zwingend vor der Methodendeklaration stehen – sondern nur dann, wenn wir eine Methode aus einer anderen statischen Funktion wie main() nutzen wollen. Fehlt static, so erzeugt der Compiler etwa folgende Fehlermeldung: »non-static method hui() cannot be referenced from a static context«.


Galileo Computing - Zum Seitenanfang

2.7.6 Parameter, Argument und Wertübergabe Zur nächsten ÜberschriftZur vorigen Überschrift

Einer Funktion können Werte übergeben werden, die sie dann in ihre Arbeitsweise einbeziehen kann. Der Funktion println(2001) ist zum Beispiel ein Wert übergeben worden. Sie wird damit zur parametrisierten Funktion.


Beispiel Beispiel Werfen wir einen Blick auf die Funktionsdeklaration maxOut() für Gleitkommazahlen. Die Methode soll später den größeren Wert auf dem Bildschirm ausgeben.

static void maxOut( double a, double b ) 
{ 
  // Hier kommt die Implementierung hinein. 
}

Der Bezeichner, der innerhalb der Methode verwendet wird, um den übergebenen Wert anzusprechen, heißt formaler Parameter. a und b sind in unserem Beispiel die formalen Parameter. Sie werden durch ein Komma getrennt aufgelistet. Für jeden Parameter muss ein Typ angegeben sein, und eine Abkürzung wie bei der Variablendeklaration Typ V1,V2 ist nicht möglich. Jeder Parameter muss mit seinem eigenen Typ aufgelistet werden. Mehrere Bezeichner dürfen nicht den gleichen Namen tragen, andernfalls ergibt sich ein Übersetzungsfehler.

Der Aufrufer der Funktion gibt für jeden Parameter ein Argument an. Rufen wir unsere Methode maxOut() etwa mit maxOut(10, y) auf, so ist das Literal 10 und die Variable y ein Argument der Funktion beziehungsweise ein aktueller Parameter der Funktion. Die Argumente müssen vom Typ her passen. Die Ganzzahl 10 kann auf ein double konvertiert werden, und y muss ebenfalls automatisch angepasst werden können. Für die Typanpassung gelten die bekannten Regeln.

Wertübergabe: Call by value

Wenn eine Funktion aufgerufen wird, dann gibt es in Java ein bestimmtes Verfahren, in dem jedes Argument einem Parameter übergeben wird. Diese Technik heißt im Allgemeinen Parameterübergabemechanismus, und die meisten Programmiersprachen verfügen über eine ganze Reihe von verwirrenden Möglichkeiten dazu. Java kennt nur den Mechanismus der Wertübergabe (engl. call by value beziehungsweise auch copy by value). Der in der Methode deklarierte Parameter wird als lokale Variable betrachtet, die zum Zeitpunkt des Aufrufs mit dem Argument initialisiert ist. Das Ende des Blocks bedeutet dann auch das Ende für die Parametervariable.


Beispiel Beispiel Die Funktion maxOut() gibt die größere der beiden Zahlen aus:

static void maxOut( double a, double b ) 
{ 
  if ( a > b ) 
    System.out.println( a ); 
  else 
    System.out.println( b ); 
}

Innerhalb des Funktionskörpers nutzen wir einfach die übergebenen Werte über die neuen lokalen Variablen. Beim Aufruf der Methode kopiert die Laufzeitumgebung die Werte des Arguments in die Variablen.


Beispiel Beispiel Aufruf der Funktion maxOut():

int i = 2; 
maxOut( 10, i );

Der Wert von 10 gelangt in die Variable a, und der Inhalt von i wird ausgelesen und der Variable b in der Methode zugänglich gemacht. Eine Typanpassung nimmt der Compiler automatisch vor, und die beiden Ganzzahlen werden in double für die Funktion konvertiert.

Da der Wert der Variable übergeben wird, heißt das insbesondere, dass es keine Übereinstimmung der Variablennamen geben muss. Die Variable i muss nicht b heißen. Wegen dieser Aufrufart kommt auch der Name »copy by value« zustande. Lediglich der Wert wird übergeben und kein Verweis auf die Variable, wie dies Referenzen in C++ tun.

Auswertung der Argumentenliste von links nach rechts

Bei einem Methodenaufruf werden erst alle Argumente ausgewertet und anschließend der Methode übergeben. Dies bedeutet im Besonderen, dass Unterfunktionen ausgewertet und Zuweisungen gemacht werden können. Fehler führen dann zu einem Abbruch des Funktionsaufrufs. Bis zum Fehler werden alle Ausdrücke ausgewertet.


Galileo Computing - Zum Seitenanfang

2.7.7 Methoden vorzeitig mit return beenden Zur nächsten ÜberschriftZur vorigen Überschrift

Läuft eine Methode bis zum Ende durch, dann ist die Methode damit beendet, und es geht zurück zum Aufrufer. In Abhängigkeit von einer Bedingung kann eine Methode jedoch vor dem Ende des Ablaufs mit einer return-Anweisung beendet werden. Das ist nützlich bei Methoden, die in Abhängigkeit von Parametern vorzeitig aussteigen wollen. Wir können uns vorstellen, dass vor dem Ende der Funktion automatisch ein verstecktes return steht.


Beispiel Beispiel Eine Methode soll die Wurzel einer Zahl auf dem Bildschirm ausgeben. Bei Zahlen kleiner null erscheint eine Meldung, und die Methode wird verlassen. Andernfalls wird die Wurzelberechnung durchgeführt:

static void sqrt( double d ) 
{ 
  if ( d < 0 ) 
  { 
    System.out.println( "Keine Wurzel aus negativen Zahlen!" ); 
    return; 
  } 
  System.out.println( Math.sqrt( d ) ); 
}

Die Realisierung wäre auch mit einer else-Anweisung möglich gewesen.


Eigene Methoden können natürlich wie Standardfunktionen heißen, da sie zu unterschiedlichen Klassen gehören.


Galileo Computing - Zum Seitenanfang

2.7.8 Nicht erreichbarer Quellcode bei Funktionen Zur nächsten ÜberschriftZur vorigen Überschrift

Folgt direkt hinter einer return-Anweisung Quellcode, so ist dieser nicht erreichbar – im Sinne von nicht ausführbar. return beendet also immer die Methode und kehrt zum Aufrufer zurück. Folgt nach dem return noch Quelltext, meldet der Compiler einen Fehler. In manchen Fällen ist das jedoch gewollt. Soll etwa eine Methode in der Testphase nicht komplett durchlaufen, sondern in der Mitte beendet werden, so lässt sich Folgendes nicht schreiben:

public static void main( String[] args ) 
{ 
  int i = 1; 
  return; 
  i = 2;              // Fehler! 
}

Reduzieren wir eine Anweisung bis auf das Nötigste, das Semikolon, so führt dies bisweilen zu amüsanten Ergebnissen:

public static void main( String[] args ) 
{ 
  ;return;; 
}

Das Beispiel enthält zwei Null-Anweisungen: eine vor dem return und eine dahinter. Doch das zweite Semikolon hinter dem return ist unzulässig, da es nicht erreichbarer Code ist.


Galileo Computing - Zum Seitenanfang

2.7.9 Rückgabewerte Zur nächsten ÜberschriftZur vorigen Überschrift

Funktionen wie Math.max() liefern in Abhängigkeit von den Argumenten ein Ergebnis zurück. Für den Aufrufer ist die Implementierung egal; er abstrahiert und nutzt lediglich die Methode statt eines Ausdrucks. Damit Funktionen Rückgabewerte an den Aufrufer liefern können, müssen zwei Dinge gelten:

  • Eine Methodendeklaration bekommt einen Rückgabetyp ungleich void.
  • Eine return-Anweisung gibt einen Wert zurück.

Beispiel Beispiel Eine Methode bildet den Mittelwert und gibt diesen zurück.

static double avg( double x, double y ) 
{ 
  return (x + y) / 2; 
}

Fehlt der Ausdruck und ist es nur ein einfaches return, meldet der Compiler einen Programmfehler.


Hinweis Hinweis Obwohl einige Programmierer den Ausdruck gerne klammern, ist das nicht nötig. Klammern sollen lediglich komplexe Ausdrücke besser lesbar machen. Geklammerte Ausdrücke erinnern sonst nur an einen Funktionsaufruf, und diese Verwechslungsmöglichkeit sollte bei Rückgabewerten nicht bestehen.


Der Rückgabewert muss an der Aufrufstelle jedoch nicht zwingend benutzt werden. Berechnet unsere Methode jedoch den Durchschnitt zweier Zahlen, ist es unsinnig, den Rückgabewert nicht zu verwenden.

Eclipse
Eclipse erkennt, ob ein Rückgabetyp fehlt, und schlägt über Keyboard Ctrl + Keyboard 1 einen passenden Typ vor.

Ist es ein Schaltjahr oder nicht?

Die nächste Funktion isLeapYear() stellt nach der Methode CMI fest, ob es sich bei einem Jahr um ein Schaltjahr handelt. (Die Klasse GregorianCalendar bietet diese Funktion auch schon an.) Ein Schaltjahr ist alle vier Jahre fällig. Die Funktion arbeitet mit dem gregorianischen Kalender und gibt nur eine korrekte Antwort, wenn das Jahr zwischen 1583 und 20000 liegt. [Das dürfte relativ zukunftssicher sein. ]

Listing 2.18 LeapYear.java

class LeapYear 
{ 
  /** 
   * Checks, if the given year is a leap year. 
   * 
   * @param year  Year to check. 
   * @return      {@code true} if {@code year} is a leap year, 
   *              {@code false} otherwise. 
   */ 
  static boolean isLeapYear( int year ) 
  { 
     return ( (year & 3) == 0 ) && ( year % 100 != 0 || year % 400 == 0 ); 
  } 
 
  public static void main( String[] args ) 
  { 
    System.out.println( isLeapYear(2000) ); 
  } 
}

Alle 100 Jahre fällt das Schaltjahr aus, wenn nicht gerade 400 Jahre um sind. Damit ergeben sich 97 Schaltjahre in 400 Jahren. Die Anzahl der Tage pro Jahr nähert dieser einfache Algorithmus mit 365 + 97/400 = 365,2425 ganz gut an den tatsächlichen Wert 365,24219 an. Die jährliche Verschiebung von etwa 25 Sekunden macht auf 10 000 Jahre aber »nur« drei Tage aus, was in Kauf genommen wird.

Diese Funktion soll didaktisch zeigen, dass ein Wahrheitswert direkt zurückgegeben werden kann und dann Anweisungen wie if (Schaltjahr-Ausdruck) return true; else return false; oder – noch kreativer – return Schaltjahr-Ausdruck ? true : false; unnötig sind.


Hinweis Hinweis Oft findet sich in diesem Algorithmus ein % 4, wo ich hier & 3 schreibe. Der Grund ist einfach: Eine Restwert-Operation ist – wenn sie der Compiler nicht optimiert – teuer und kann für Zweierpotenzen leicht in ein arithmetisches Und umgeschrieben werden. Bitoperatoren finden später in diesem Kapitel Platz.


Mehrere Ausstiegspunke mit return

Für Methoden mit Rückgabewert gilt ebenso wie für void-Methoden, dass es mehr als ein return geben kann. Nach der Abarbeitung von return geht es im Programmcode des Aufrufers wie bei den normalen void-Methoden weiter.


Beispiel Beispiel In if-Anweisungen mit weiteren else-if-Alternativen und Rücksprung ist die Semantik oft die gleiche, wenn das else-if durch ein einfaches if ersetzt wird. Der nachfolgende Programmcode zeigt das:

if ( a == 1 ) 
  return 0; 
else if ( a == 2 )   // mit else 
  return 1;

Äquivalent ist:

if ( a == 1 ) 
  return 0; 
 
if ( a == 2 )        // ohne else 
  return 1;

Ist die erste Bedingung wahr, so endet die Funktion, und das nachfolgende if würde sowieso nicht beachtet.


Wichtig ist nur, dass jeder denkbare Programmfluss mit einem return beendet wird. Der Compiler verfügt über ein scharfes Auge und merkt, wenn es einen Programmpfad gibt, der nicht mit einem return-Ausdruck beendet wird.


Beispiel Beispiel Die Funktion isLastBitSet() soll 0 zurückgeben, wenn das letzte Bit einer Ganzzahl nicht gesetzt ist, und 1, wenn es gesetzt ist. Den Bit-Test erledigt der Und-Operator.

static int isLastBitSet( int i ) 
{ 
  switch ( i & 1 ) { 
    case 0: return 0; 
    case 1: return 1; 
  } 
}

Die Funktion lässt sich nicht übersetzen, obwohl ein Bit nur gesetzt oder nicht gesetzt sein kann – dazwischen gibt es nichts.


Bei den Dingen, die für den Benutzer meistens offensichtlich sind, muss der Compiler passen, da er nicht hinter die Bedeutung sehen kann. Ähnliches würde für eine Wochen-Funktion gelten, die mit einem Ganzzahl-Argument (0 bis 6) einen Wochentag als String zurückgibt. Wenn wir die Fälle 0 = Montag bis 6 = Sonntag beachten, dann kann in unseren Augen ein Wochentag nicht 99 sein. Der Compiler kennt aber die Funktion nicht und weiß nicht, dass der Wertebereich beschränkt ist. Das Problem ließe sich mit einem default leicht beheben.


Beispiel Beispiel Die Funktion posOrNeg() soll eine Zeichenkette mit der Information liefern, ob die übergebene Gleitkommazahl positiv oder negativ ist.

static String posOrNeg( double d ) 
{ 
  if ( d >= 0 ) 
    return "pos"; 
 
  if ( d < 0 ) 
    return "neg"; 
}

Überraschenderweise ist dieser Programmcode ebenfalls fehlerhaft. Denn obwohl er offensichtlich für positive oder negative Zahlen den passenden String zurückgibt, gibt es einen Fall, den diese Funktion nicht abdeckt. Wieder gilt, dass der Compiler nicht erkennen kann, dass der zweite Ausdruck eine Negation des ersten sein soll. Es gibt aber noch einen zweiten Grund, der damit zu tun hat, dass es in Java spezielle Werte gibt, die keine Zahlen sind. Denn die Zahl d kann auch eine NaN (Not-a-Number) aus einer negativen Wurzel sein. Diesen speziellen Wert überprüft posOrNeg() nicht. Als Lösung für den einfachen Fall ohne NaN reicht es, aus dem zweiten if einfach ein else zu machen.

Methoden, die einen Fehlerwert wie 1 zurückliefern, sind häufig so implementiert, dass am Ende immer automatisch der Fehlerwert zurückgeliefert und dann in der Mitte die Methode bei passendem Ende verlassen wird.

Fallunterscheidungen mit Ausschlussprinzip

Eine Methode between(x, a, b) soll testen, ob ein Wert x zwischen a (untere Schranke) und b (obere Schranke) liegt. Bei Funktionen dieser Art ist es immer sehr wichtig, darauf zu achten und zu dokumentieren, ob der Test auf echt kleiner (<) oder kleiner gleich (<=) durchgeführt werden soll. Wir wollen hier auch die Gleichheit betrachten.

In der Implementierung gibt es zwei Lösungen, wobei die meisten Programmierer zur ersten Lösung neigen. Die erste Lösungsidee zeigt sich in einer mathematischen Gleichung. Wir möchten gerne a <= x <= b schreiben, doch ist dies in Java nicht erlaubt. [... im Gegensatz zur Programmiersprache Python. ] So müssen wir einen Und-Vergleich anstellen, der etwa so lautet: Ist a <= x && x <= b, dann liefere true zurück.

Die zweite Methode zeigt, dass sich das Problem auch ohne Und-Vergleich durch das Ausschlussprinzip lösen lässt:

static boolean between( int x, int a, int b ) 
{ 
  if ( x < a ) 
    return false; 
 
  if ( x <= b ) 
    return true; 
 
  return false; 
}

Mit geschachtelten Anfragen sieht das dann so aus:

static boolean between( int x, int a, int b ) 
{ 
  if ( a <= x ) 
    if ( x <= b ) 
      return true; 
 
  return false; 
}

Galileo Computing - Zum Seitenanfang

2.7.10 Methoden überladen Zur nächsten ÜberschriftZur vorigen Überschrift

Eine Funktion ist gekennzeichnet durch Rückgabewert, Name, Parameter und unter Umständen durch Ausnahmefehler, die sie auslösen kann. Java erlaubt es, den Namen der Funktion gleich zu lassen, aber andere Parameter einzusetzen. Eine überladene Methode ist eine Funktion mit dem gleichen Namen wie eine andere Funktion, aber einer unterschiedlichen Parameterliste. Das ist auf zwei Arten möglich:

  • Eine Funktion akzeptiert eine unterschiedliche Anzahl von Parametern.
  • Eine Funktion lautet gleich, hat aber für den Compiler unterscheidbare Typen.

Anwendungen für den ersten Fall gibt es viele. Der Name einer Funktion soll ihre Aufgabe beschreiben, aber nicht die Typen der Parameter, mit denen sie arbeitet, extra erwähnen. Das ist bei anderen Sprachen üblich, doch nicht in Java. Sehen wir uns als Beispiel die in der Mathe-Klasse Math angebotene Funktion max() an. Sie ist mit den Parametertypen int, long, float und double deklariert – das ist viel schöner, als etwa separate Funktionen maxInt() und maxDouble().


Beispiel Beispiel Eine unterschiedliche Anzahl von Parametern ist ebenfalls eine sinnvolle Angelegenheit. Die Funktion avg() könnten wir so für zwei und drei Parameter deklarieren:

static double avg( double x, double y ) { 
  return (x + y) / 2; 
} 
static double avg( double x, double y, double z ) { 
  return (x + y + z) / 3; 
}

Kommen wir nun zu zwei weiteren Beispielen, die etwas komplizierter sind und übersprungen werden können.

print() und println() sind überladen

Das bekannte print() und println() sind überladene Funktionen, die etwa wie folgt deklariert sind:

class PrintStream 
{ 
  void print( Object arg ) { ... } 
  void print( String arg ) { ... } 
  void print( char[] arg ) { ... } 
  ... 
}

Wird nun die Funktion print() mit irgendeinem Typ aufgerufen, dann wird die am besten passende Funktion herausgesucht. Versucht der Programmierer beispielsweise die Ausgabe eines Objekts Date, dann stellt sich die Frage, welche Methode sich darum kümmert. Glücklicherweise ist die Antwort nicht schwierig, denn es existiert auf jeden Fall eine print()-Methode, die Objekte ausgibt. Und da Date, wie auch alle anderen Klassen, eine Unterklasse von Object ist, wird print(Object) gewählt. (Natürlich kann nicht erwartet werden, dass das Datum in einem bestimmten Format – etwa nur das Jahr – ausgegeben wird, jedoch wird eine Ausgabe auf dem Schirm sichtbar.) Denn jedes Objekt kann sich durch den Namen identifizieren, und dieser würde in dem Fall ausgegeben. Obwohl es sich so anhört, als ob immer die Funktion mit dem Parametertyp Object aufgerufen wird, wenn der Datentyp nicht angepasst werden kann, ist dies nicht ganz richtig. Wenn der Compiler keine passende Klasse findet, dann wird die nächste Oberklasse im Ableitungsbaum gesucht, für die in unserem Fall eine Ausgabefunktion existiert.

Negative Beispiele und schlaue Leute

Oft verfolgt auch die Java-Bibliothek die Strategie mit gleichen Namen und unterschiedlichen Typen. Es gibt allerdings einige Ausnahmen. In der Grafik-Bibliothek finden sich die folgenden drei Funktionen:

  • drawString( String str, int x, int y )
  • drawChars( char[] data, int offset, int length, int x, int y )
  • drawBytes( byte[] data, int offset, int length, int x, int y )

Das ist äußerst hässlich und schlechter Stil.

Ein anderes Beispiel findet sich in der Klasse DataOutputStream. Hier heißen die Methoden etwa writeInt(), writeChar() und so weiter. Obwohl wir dies auf den ersten Blick verteufeln würden, ist diese Namensgebung sinnvoll. Ein Objekt vom Typ DataOutputStream dient zum Schreiben von primitiven Werten in einen Datenstrom, und davon gibt es bekannterweise einige, mit unterschiedlichen Längen. Gäbe es in DataOutputStream die überladenen Methoden write(byte), write(short), write(int), write(long) und write(char) und würden wir sie mit write(21) füttern, dann hätten wir das Problem, dass eine Typkonvertierung die Daten automatisch anpassen und der Datenstrom mehr Daten beinhalten würde, als wir wünschen. Denn write(21) ruft nicht etwa write(short) auf und schreibt zwei Bytes, sondern write(int) und schreibt somit vier Bytes. Um also die Übersicht über die geschriebenen Bytes zu behalten, ist eine ausdrückliche Kennzeichnung der Datentypen in manchen Fällen gar nicht so dumm.


Galileo Computing - Zum Seitenanfang

2.7.11 Vorgegebener Wert für nicht aufgeführte Argumente Zur nächsten ÜberschriftZur vorigen Überschrift

Überladene Funktionen lassen sich gut verwenden, wenn vorinitialisierte Werte bei nicht vorhandenen Argumenten genutzt werden sollten. Ist also ein Parameter nicht belegt, soll ein Standardwert eingesetzt werden. Um das zu erreichen, überladen wir einfach die Funktion und rufen die andere Funktion mit dem Standardwert passend auf. (Die Sprache C++ definiert in der Sprachgrammatik eine Möglichkeit, die wir in Java nicht haben.)


Beispiel Beispiel Zwei überladene Funktionen, tax(double d, double rate) und tax(double d), sollen die Steuer berechnen. Wir möchten, dass der Steuersatz automatisch 19 ist, wenn die Funktion tax(double d) aufgerufen wird und der Steuersatz nicht explizit gegeben ist; im anderen Fall können wir rate beliebig wählen.

static double tax( double d, double rate ) 
{ 
  // Berechnung 
} 
static double tax( double d ) 
{ 
  return tax( d, 19.0 ); 
}


Galileo Computing - Zum Seitenanfang

2.7.12 Finale lokale Variablen Zur nächsten ÜberschriftZur vorigen Überschrift

In einer Methode können Parameter oder lokale Variablen mit dem Modifizierer final deklariert werden. Dieses zusätzliche Schlüsselwort verbietet nochmalige Zuweisungen an diese Variable, sodass sie nicht mehr verändert werden kann.

static void foo( final int a ) 
{ 
    int i = 2; 
    final int j = 3; 
    i = 3; 
//  j = 4;       führt zu einem Fehler 
//  a = 2;       führt zu einem Fehler 
}

Aufgeschobene Initialisierung

Java erlaubt bei finalen Werten eine aufgeschobene Initialisierung. Das heißt, dass nicht zwingend zum Zeitpunkt der Variablendeklaration ein Wert zugewiesen werden muss. Dies kann auch genau einmal im Programmcode geschehen. Folgendes ist gültig:

final int a; 
a = 2;

Obwohl auch Objektvariablen und Klassenvariablen final sein können, gibt es dort nur beschränkt eine aufgeschobene Initialisierung. Bei der Deklaration müssen wir die Variablen entweder direkt belegen oder im Konstruktor zuweisen. Wir werden uns dies später noch einmal genauer ansehen. Werden finale Variablen vererbt, so können Unterklassen diesen Wert auch nicht mehr überschreiben. (Das wäre ein Problem, aber vielleicht auch ein Vorteil für manche Konstanten.)

Final deklarierte Referenz-Parameter und das fehlende const

Wir haben gesehen, dass finale Variablen dem Programmierer vorgeben, dass er Variablen nicht beschreiben darf. Das heißt, Zuweisungen sind tabu. Dabei ist es egal, ob die Parametervariable vom primitiven Typ oder vom Referenztyp ist. Bei einer Funktionsdeklaration der folgenden Art wäre also eine Zuweisung an i und auch an s verboten:

public void foo( final int i, final String s )

Ist die Parametervariable ein Referenztyp (und nicht final), so würden wir mit einer Zuweisung den Verweis auf das ursprüngliche Objekt verlieren, und das wäre wenig sinnvoll.

public void foo( String s ) 
{ 
  s = "Hallo"; 
}

Halten wir fest: Ist ein Parameter mit final deklariert, sind keine Zuweisungen möglich. final verbietet aber keine Änderungen an Objekten – und so könnte final im Sinne der Übersetzung »endgültig« verstanden werden. Mit der Referenz des Objekts können wir sehr wohl den Zustand verändern. So ändert die folgende foo()-Methode die x-Koordinate eines Point-Objekts, egal, ob p final ist oder nicht.

public void foo( final Point p ) 
{ 
  p.x = 2; 
}

final erfüllt demnach nicht die Aufgabe, schreibende Objektzugriffe zu verhindern. Eine Methode mit übergebenen Referenzen kann also Objektveränderungen vornehmen, wenn es etwa setXXX()-Methoden oder Variablen gibt, auf die zugegriffen werden kann. Die Dokumentation muss also immer ausdrücklich beschreiben, wann die Funktion den Zustand eines Objekts modifiziert.

In C++ gibt es für Parameter den Zusatz const, an dem der Compiler erkennen kann, dass Objektzustände nicht verändert werden sollen. Ein Programm nennt sich »const-korrekt«, wenn es niemals ein konstantes Objekt verändert. Dieses const ist in C++ eine Erweiterung des Objekttyps, die es in Java nicht gibt. Zwar haben die Java-Entwickler das Schlüsselwort const reserviert, doch genutzt wird es bisher nicht.

final in der Vererbung

In der Vererbung spielt das final bei Parametern keine Rolle. Wir können es als zusätzliche Information für die jeweilige Methode betrachten. Eine Unterklasse kann demnach beliebig das final hinzufügen oder auch wegnehmen. Alte Bibliotheken lassen sich so leicht weiterverwenden.


Galileo Computing - Zum Seitenanfang

2.7.13 Rekursive Funktionen Zur nächsten ÜberschriftZur vorigen Überschrift

Wir wollen den Einstieg in die Rekursion mit einem kurzen Beispiel beginnen.

Auf dem Weg durch den Wald begegnet uns eine Fee. Sie sagt zu uns: »Du hast drei Wünsche frei.« Tolle Situation. Um das ganze Unglück aus der Welt zu räumen, entscheiden wir uns nicht für eine egozentrische Wunscherfüllung, sondern für die sozialistische: »Ich möchte Frieden für alle, Gesundheit und Wohlstand für jeden.« Und schwupps, so war es geschehen, und alle lebten glücklich bis ...

Einige Leser werden vielleicht die Hand vor den Kopf schlagen und sagen: »Quatsch! Ein Haus, ein Auto und einen Lebenspartner, der die Trägheit des Morgens duldet.« Glücklicherweise können wir das Dilemma mit der Rekursion lösen. Die Idee ist einfach – und in unseren Träumen schon erprobt –, sie besteht nämlich darin, den letzten Wunsch als »Nochmal drei Wünsche frei« zu formulieren.


Beispiel Beispiel Eine kleine Wunsch-Funktion:

static void fee() 
{ 
  wunsch(); 
  wunsch(); 
  fee(); 
}

Durch den dauernden Aufruf der fee()-Funktion haben wir unendlich viele Wünsche frei. Rekursion ist also das Aufrufen der eigenen Methode, in der wir uns befinden. Dies kann auch über einen Umweg funktionieren. Das nennt sich dann nicht mehr direkte Rekursion, sondern indirekte Rekursion. Sie ist ein sehr alltägliches Phänomen, das wir auch von der Rückkopplung Mikrofon/Lautsprecher oder dem Blick mit einem Spiegel in den Spiegel kennen.

Abbruch der Rekursion

Wir müssen nun die Fantasie-Programme (deren Laufzeit und Speicherbedarf auch sehr schwer zu berechnen sind) gegen Java-Funktionen austauschen.


Beispiel Beispiel Eine Endlos-Rekursion:

Listing 2.19 EndlessRecursion.java, down()

static void down( int n ) 
{ 
  System.out.print( n + ", " ); 
  down( n – 1 ); 
}

Rufen wir down(10) auf, dann wird die Zahl 10 auf dem Bildschirm ausgegeben und anschließend down(9) aufgerufen. Führen wir das Beispiel fort, so ergibt sich eine endlose Ausgabe, die so beginnt:

10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, –1, –2, ...

An dieser Stelle erkennen wir, dass Rekursion prinzipiell etwas Unendliches ist. Für Programme ist dies aber ungünstig. Wir müssen daher ähnlich wie bei Schleifen eine Abbruchbedingung formulieren und dann keinen Rekursionsaufruf mehr starten. Die sieht so aus, dass eine Fallunterscheidung das Argument prüft und mit return die Abarbeitung beendet.

Listing 2.20 Recursion.java, down1()

static void down1( int n ) 
{ 
  if ( n <= 0 )              // Rekursionsende 
    return; 
 
  System.out.print( n + ", " ); 
  down1( n – 1 ); 
}

Die down1()-Methode ruft jetzt nur noch so lange down1(n1) auf, wie das n größer null ist. Das ist die Abbruchbedingung einer Rekursion.

Unterschiedliche Rekursionsformen

Ein Kennzeichen der bisherigen Programme war, dass nach dem Aufruf der Rekursion keine Anweisung stand, sondern die Methode mit dem Aufruf beendet wurde. Diese Rekursionsform nennt sich Endrekursion. Diese Form ist verhältnismäßig einfach zu verstehen. Schwieriger sind Rekursionen, bei denen hinter dem Methodenaufruf Anweisungen stehen. Betrachten wir folgende Methoden, von denen die erste bekannt und die zweite neu ist:

Listing 2.21 Recursion.java, down1() und down2()

static void down1( int n ) 
{ 
  if ( n <= 0 )   // Rekursionsende 
    return; 
 
  System.out.print( n + ", " ); 
 
  down1( n – 1 ); 
} 
static void down2( int n ) 
{ 
  if ( n <= 0 )   // Rekursionsende 
    return; 
 
  down2( n – 1 ); 
 
  System.out.print( n + ", " ); 
}

Der Unterschied besteht darin, dass down1() zuerst die Zahl n ausgibt und anschließend rekursiv down1() aufruft. Die Methode down2() steigt jedoch erst immer tiefer ab, und die Rekursion muss beendet sein, bis es zum ersten print() kommt. Daher gibt im Gegensatz zu down1() die Methode down2() die Zahlen in aufsteigender Reihenfolge aus:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

Dies ist einleuchtend, wenn wir die Ablaufreihenfolge betrachten. Beim Aufruf down2(10) ist der Vergleich von n mit null falsch, also wird ohne Ausgabe wieder down2(9) aufgerufen. Ohne Ausgabe deshalb, da print() ja erst nach dem Funktionsaufruf steht. Es geht rekursiv tiefer, bis n gleich null ist. Dann endet die letzte Methode mit return, und die Ausgabe wird nach dem down2(), dem Aufrufer, fortgeführt. Dort ist print() die nächste Anweisung. Da wir nun noch tief verschachtelt stecken, gibt print(n) die Zahl 1 aus. Dann ist die Methode down2() wieder beendet (ein unsichtbares, nicht direkt geschriebenes return), und sie springt zum Aufrufer zurück. Das war wieder die Methode down2(), aber mit der Belegung n = 2. Das geht so weiter, bis es zurück zum Aufrufer kommt, der down(10) aufgerufen hat, zum Beispiel die main()-Methode. Der Trick bei der Sache besteht nun darin, dass jede Methode ihre eigene lokale Variable besitzt.

Eclipse
Die Tastenkombination Keyboard Ctrl + Keyboard Alt + Keyboard H zeigt die Aufrufhierarchie an. So ist zu sehen, wer eine Funktion aufruft. In den Aufrufen von down2() taucht also wiederum wegen des rekursiven Aufrufs down2() auf sowie main().


Galileo Computing - Zum Seitenanfang

2.7.14 Die Türme von Hanoi topZur vorigen Überschrift

Die Legende der Türme von Hanoi soll erstmalig von Ed Lucas in einem Artikel in der französischen Zeitschrift »Cosmo« im Jahre 1890 veröffentlicht worden sein. Wir halten uns hier an eine Überlieferung von C. H. A. Koster aus dem Buch »Top-down Programming with Elan«, Ellis Horwood 1987.

Der Legende nach standen vor langer Zeit im Tempel von Hanoi drei Säulen. Die erste war aus Kupfer, die zweite aus Silber und die dritte aus Gold. Auf der Kupfersäule waren einhundert Scheiben aufgestapelt. Die Scheiben hatten in der Mitte ein Loch und waren aus Porphyr [Gestein, das aus der porphyrischen Etschplattform gewonnen wird. Dies ist eine Gebirgsgruppe vulkanischen Ursprungs in Trentino/Südtirol. Besondere Eigenschaften von Porphyr sind: hohe Bruchfestigkeit, hohe Beständigkeit gegen physikalisch-chemische Wirkstoffe und hohe Wälz- und Gleitreibung. ] . Die Scheibe mit dem größten Umfang lag unten und alle kleiner werdenden Scheiben oben auf. Ein alter Mönch stellte sich die Aufgabe, den Turm der Scheiben von der Kupfersäule zur Goldsäule zu bewegen. In einem Schritt sollte aber nur eine Scheibe bewegt werden, und zudem war die Bedingung, dass eine größere Scheibe niemals auf eine kleinere bewegt werden durfte. Der Mönch erkannte schnell, dass er die Silbersäule nutzen musste; er setzte sich an einen Tisch, machte einen Plan, überlegte und kam zu einer Entscheidung. Er konnte sein Problem in drei Schritten lösen.

Am nächsten Tag schlug der Mönch die Lösung an die Tempeltür:

  • Falls der Turm aus mehr als einer Scheibe besteht, bitte deinen ältesten Schüler, einen Turm von (n – 1) Scheiben von der ersten zur dritten Säule unter Verwendung der zweiten Säule umzusetzen.
  • Trage selbst die erste Scheibe von einer zur anderen Säule.
  • Falls der Turm aus mehr als einer Scheibe besteht, bitte deinen ältesten Schüler, einen Turm aus (n – 1) Scheiben von der dritten zu der anderen Säule unter Verwendung der ersten Säule zu transportieren.

Und so rief der alte Mönch seinen ältesten Schüler zu sich und trug ihm auf, den Turm aus 99 Scheiben von der Kupfersäule zur Goldsäule unter Verwendung der Silbersäule umzuschichten und ihm den Vollzug zu melden.

Nach der Legende würde das Ende der Welt nahe sein, bis der Mönch seine Arbeit beendet hätte. Nun, so weit die Geschichte. Wollen wir den Algorithmus zur Umschichtung der Porphyrscheiben in Java programmieren, so ist eine rekursive Lösung recht einfach. Werfen wir einen Blick auf das folgende Programm, welches über die drei Pflöcke (engl. peg) die Umschichtungen vornimmt.

Listing 2.22 TowerOfHanoi.java

class TowerOfHanoi 
{ 
  static void move( int n, 
                    String fromPeg, String toPeg, String usingPeg ) 
  { 
    if ( n > 1 ) 
    { 
      move( n – 1, fromPeg, usingPeg, toPeg ); 
      System.out.printf( "Bewege Scheibe %d von der %s zur %s.%n", 
                         n, fromPeg, toPeg ); 
      move( n – 1, usingPeg, toPeg, fromPeg ); 
    } 
    else 
      System.out.printf( "Bewege Scheibe %d von der %s zur %s.%n", 
                         n, fromPeg, toPeg ); 
  } 
 
  public static void main( String[] args ) 
  { 
    move( 4, "Kupfersäule", "Silbersäule", "Goldsäule" ); 
  } 
}

Starten wir das Programm mit vier Scheiben, so bekommen wir folgende Ausgabe:

Bewege Scheibe 1 von der Kupfersäule zur Goldsäule. 
Bewege Scheibe 2 von der Kupfersäule zur Silbersäule. 
Bewege Scheibe 1 von der Goldsäule zur Silbersäule. 
Bewege Scheibe 3 von der Kupfersäule zur Goldsäule. 
Bewege Scheibe 1 von der Silbersäule zur Kupfersäule. 
Bewege Scheibe 2 von der Silbersäule zur Goldsäule. 
Bewege Scheibe 1 von der Kupfersäule zur Goldsäule. 
Bewege Scheibe 4 von der Kupfersäule zur Silbersäule. 
Bewege Scheibe 1 von der Goldsäule zur Silbersäule. 
Bewege Scheibe 2 von der Goldsäule zur Kupfersäule. 
Bewege Scheibe 1 von der Silbersäule zur Kupfersäule. 
Bewege Scheibe 3 von der Goldsäule zur Silbersäule. 
Bewege Scheibe 1 von der Kupfersäule zur Goldsäule. 
Bewege Scheibe 2 von der Kupfersäule zur Silbersäule. 
Bewege Scheibe 1 von der Goldsäule zur Silbersäule.

Schon bei vier Scheiben haben wir 15 Bewegungen. Selbst wenn unser Prozessor mit vielen Millionen Operationen pro Sekunde arbeitet, benötigt ein Computer für die Abarbeitung tausende geologische Erdzeitalter. An diesem Beispiel wird eines deutlich: Viele Funktionen sind im Prinzip berechenbar, nur praktisch ist so ein Algorithmus nicht.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






<< zurück



Copyright © Galileo Press 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de