Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
gp Kapitel 2 Sprachbeschreibung
  gp 2.1 Anweisungen und Programme
  gp 2.2 Programme
    gp 2.2.1 Kommentare
    gp 2.2.2 Funktionsaufrufe als Anweisungen
    gp 2.2.3 Die leere Anweisung
    gp 2.2.4 Der Block
  gp 2.3 Elemente einer Programmiersprache
    gp 2.3.1 Textkodierung durch Unicode-Zeichen
    gp 2.3.2 Bezeichner
    gp 2.3.3 Reservierte Schlüsselwörter
    gp 2.3.4 Token
    gp 2.3.5 Semantik
  gp 2.4 Datentypen
    gp 2.4.1 Primitive Datentypen
    gp 2.4.2 Wahrheitswerte
    gp 2.4.3 Variablendeklarationen
    gp 2.4.4 Ganzzahlige Datentypen
    gp 2.4.5 Die Fließkommazahlen
    gp 2.4.6 Zeichen
    gp 2.4.7 Die Typanpassung (Das Casting)
    gp 2.4.8 Lokale Variablen, Blöcke und Sichtbarkeit
    gp 2.4.9 Initialisierung von lokalen Variablen
  gp 2.5 Ausdrücke
    gp 2.5.1 Zuweisungsoperator und Verbundoperator
    gp 2.5.2 Präfix- oder Postfix-Inkrement und -Dekrement
    gp 2.5.3 Unäres Minus und Plus
    gp 2.5.4 Arithmetische Operatoren
    gp 2.5.5 Die relationalen Operatoren
    gp 2.5.6 Logische Operatoren
    gp 2.5.7 Reihenfolge und Rang der Operatoren in der Auswertungsreihenfolge
    gp 2.5.8 Was C(++)-Programmierer vermissen könnten
  gp 2.6 Bedingte Anweisungen oder Fallunterscheidungen
    gp 2.6.1 Die if-Anweisung
    gp 2.6.2 Die Alternative wählen mit einer if/else-Anweisung
    gp 2.6.3 Die switch-Anweisung bietet die Alternative
  gp 2.7 Schleifen
    gp 2.7.1 Die while-Schleife
    gp 2.7.2 Schleifenbedingungen und Vergleiche mit ==
    gp 2.7.3 Die do/while-Schleife
    gp 2.7.4 Die for-Schleife
    gp 2.7.5 Ausbruch planen mit break und Wiedereinstieg mit continue
    gp 2.7.6 Break und Continue mit Sprungmarken
  gp 2.8 Methoden einer Klasse
    gp 2.8.1 Bestandteil einer Funktion
    gp 2.8.2 Aufruf
    gp 2.8.3 Methoden ohne Parameter
    gp 2.8.4 Parameter und Wertübergabe
    gp 2.8.5 Methoden vorzeitig mit return beenden
    gp 2.8.6 Nicht erreichbarer Quellcode bei Funktionen
    gp 2.8.7 Rückgabewerte
    gp 2.8.8 Methoden überladen
    gp 2.8.9 Vorinitialisierte Parameter bei Funktionen
    gp 2.8.10 Finale lokale Variablen
    gp 2.8.11 Finale Referenzen in Objekten und das fehlende const
    gp 2.8.12 Rekursive Funktionen
    gp 2.8.13 Die Ackermann-Funktion
    gp 2.8.14 Die Türme von Hanoi
  gp 2.9 Noch mehr Operatoren
    gp 2.9.1 Bit-Operationen
    gp 2.9.2 Vorzeichenlose Bytes in ein Integer und Char konvertieren
    gp 2.9.3 Variablen mit Xor vertauschen
    gp 2.9.4 Die Verschiebe-Operatoren
    gp 2.9.5 Setzen, Löschen, Umdrehen, Testen von Bits
    gp 2.9.6 Der Bedingungsoperator
    gp 2.9.7 Überladenes Plus für Strings

Kapitel 2 Sprachbeschreibung

Wenn ich eine Oper hundertmal dirigiert habe,
dann ist es Zeit, sie wieder zu lernen.
– Arturo Toscanini (1867–1957)

Ein Programm in Java wird nicht umgangssprachlich beschrieben, sondern ein Regelwerk und eine Grammatik definieren die Syntax und die Semantik. In den nächsten Abschnitten werden wir kleinere Beispiele für Java-Programme kennen lernen und dann ist der Weg frei für größere Programme.


Galileo Computing

2.1 Anweisungen und Programme  downtop

Java zählt zu den imperativen Programmiersprachen, in denen der Programmierer die Abarbeitungsschritte seiner Algorithmen durch Anweisungen vorgibt. Diese Befehlsform ist für Programmiersprachen gar nicht selbstverständlich, da es Sprachen gibt, die zu einer Problembeschreibung selbstständig eine Lösung finden. Die Schwierigkeit hierbei liegt darin, die Aufgabe so präzise zu beschreiben, dass das System eine Lösung finden kann. Ein Vertreter dieser Art Sprachen ist Prolog. Auch die Datenbanksprache SQL ist keine imperative Programmiersprache. Denn wie die Datenbank zu unserer Anfrage die Ergebnisse ermittelt, müssen und können wir nicht vorgeben und auch nicht sehen.

Bleiben wir bei imperativen Sprachen. Um eine Aufgabe zu lösen, müssen wir jeden Abarbeitungsschritt angeben. Abarbeitungsfolgen befinden sich in jedem Kochbuch. Betrachten wir ein ...

Rezept für Flammkuchen

Aus der Hefe, der Milch (lauwarm) und dem Zucker einen dünnflüssigen homogenen Vorteig anrühren. Diesen mit allen anderen Zutaten außer dem Wasser vermischen (am besten mit einem Knethaken oder der Küchenmaschine). Solange Wasser (ca. 2 bis 2 1/2 dl für 500 g Mehl) hinzugeben, bis sich der Teig von der Schüssel (und von den Fingern) löst und sich trotzdem noch feucht anfühlt. Teig zu einem Klumpen formen, mit Mehl bestreuen und mit einem Handtuch abgedeckt ca. 1 Stunde gehen lassen.

Diese Garzeit ist temperaturabhängig. Wenn der Teig in der geheizten Küche, auf einen hohen Schrank gestellt wird, reichen manchmal auch nur 30–40 Minuten. Das Teigvolumen sollte sich danach verdoppelt haben. Generell gilt, dass der Teig eher länger als zu kurz gehen sollte.

In der Zwischenzeit wird der Speck gewürfelt, die Zwiebeln in feine Ringe geschnitten und der Käse gerieben. Die Crème fraîche wird flüssig gerührt und mit Pfeffer, Muskatnuss und nicht zuviel Salz (da ja schon in Speck, Käse und Teig enthalten) gewürzt. Mit dem Pfeffer und der Muskatnuss nicht zu sparsam sein, die Crème sollte hinterher schon recht würzig sein.

Wenn der Teig gegangen ist, kann man schon mal den Ofen einschalten. Der Teig wird nun in zwei Teile aufgeteilt (einen Teil wieder zurücklegen). Den Teigklumpen nochmals kurz von Hand durchkneten (vorher die Hände und den Teig leicht bemehlen) und dann auf einer bemehlten Fläche auf Blechgröße ausrollen. Dabei wird der Teig sehr dünn, je dünner umso besser. Den Teig auf das bemehlte Blech legen und mit der Hälfte der Creme bestreichen. Danach den Speck und die Zwiebeln und zum Schluss den Käse draufgeben und den Kuchen im Ofen garen. Nach ca. 10 Minuten, wenn eine leichte Bräunung eintritt, ist der Flammkuchen fertig.

gp  500 g Weißmehl
gp  40 g Hefe, frisch
gp  EL Milch
gp  1 TL Zucker
gp  1 TL Wasser
gp  1 TL Salz
gp  EL Öl
gp  200 g Speck, je nach Gusto mehr oder weniger
gp  Zwiebeln, in feine Ringe geschnitten
gp  3 dl Crème fraîche
gp  200 g Käse, gerieben
gp  Pfeffer, frisch gemahlener
gp  Salz
gp  Muskatnuss, frisch gerieben

Die Vorschriften kennzeichnen eine klare Sequenz der Operationen. Dies ist eine wichtige Eigenschaft von Java. Zusätzlich erkennen wir an dieser Abarbeitungsfolge weitere wichtige Eigenschaften, die sich auf imperative Programmiersprachen übersetzen lassen: Eine Folge von Anweisungen bildet einen Block. Später werden wir diese Operationen zur Vereinfachung des Programms in einen extra Programmblock setzen und »Unterprogramm« nennen.

Des weiteren lässt sich ein Flammkuchen nicht ohne Kontrolle von Ereignissen zubereiten. Betrachten wir dazu den letzten Satz der Anleitung:

Wenn eine leichte Bräunung eintritt, ist der 
Flammkuchen fertig.

Die Abfrage von Gegebenheiten ist sehr wichtig für imperative Sprachen. Genauso gut Wiederholungen wie

Solange Wasser hinzugeben, bis sich der Teig von 
der Schüssel löst.

Hier ist eine Anweisung solange auszuführen, bis etwas gilt oder nicht mehr gilt. Interessant ist auch folgende Anweisung:

Wenn der Teig gegangen ist, kann man schon mal den 
Ofen einschalten.

Dies ist eine Nebenläufigkeit, die in herkömmlichen Sprachen nicht unterstützt wird. Während der Ofen heiß wird, können wir weiterkneten. Java unterstützt ein paralleles Abarbeiten von Programmteilen.

Elementare Anweisungen

Atomare, also unteilbare Anweisungen, werden auch elementare Anweisungen genannt. Programme bestehen in der Regel aus mehreren Anweisungen, die dann eine Anweisungssequenz ergeben. Die Laufzeitumgebung von Java führt jede einzelne Anweisung der Sequenz in der angegebenen Reihenfolge hintereinander aus.


Galileo Computing

2.2 Programme  downtop

Programme setzen sich aus Anweisungen zusammen. In Java können jedoch nicht einfach Anweisungen in eine Datei geschrieben und dem Compiler übergeben werden. Sie müssen zunächst in einen Rahmen gepackt werden. Dieser Rahmen definiert die Haupt-Klasse und eine Funktion.

Auch wenn die folgenden Programmcodezeilen am Anfang etwas befremdend wirken, werden wir sie zu einem späteren Zeitpunkt noch genauer erklären. Wir geben der folgenden Datei den Namen Main.java.

Listing 2.1   Main.java
class Main
{
  public static void main( String args[] )
  {
  // Hier ist der Anfang unserer Programme

  // Jetzt ist hier Platz für unsere eigenen Anweisungen

  // Hier enden unsere Programme
  }
}

Ein Java-Programm muss immer in einer Klasse definiert sein. Wir haben sie Main genannt, der Name ist jedoch beliebig. In geschweiften Klammern folgen benutzerdefinierte Methoden, also Funktionen, die die Klasse anbietet. Eine Funktion ist eine Sammlung von Anweisungen unter einem Namen. Mathematische Funktionen sind mit Funktionen aus Programmiersprachen vergleichbar. Eine Sinusfunktion schafft es beispielsweise zu einem gegebenen Wert den Sinus zu berechnen. Wir wissen zwar nicht, wie die Funktion das macht, aber sie kann es. Der Begriff »Funktion« und der objektorientierte Name »Methode« sind in diesem Tutorial als synonym anzusehen. Vor einer Methode stehen unterschiedliche Modifizierer. Der Modifizierer static sagt, dass die Methode auch ohne Objekt benutzt werden kann. Wir werden in den folgenden Kapiteln nur mit statischen Methoden arbeiten.

Wir programmieren hier eine besondere Funktion, die sich main() nennt. Die Schlüsselwörter davor und die Angabe in dem Paar runder Klammern hinter dem Namen müssen wir einhalten. Die Funktion main() ist für die Laufzeitumgebung etwas ganz besonderes, denn beim Aufruf des Java-Interpreters mit einem Klassennamen wird unsere Funktion als erstes ausgeführt. Demnach werden genau die Anweisungen ausgeführt, die innerhalb der geschweiften Klammern stehen. Halten wir uns fälschlicherweise nicht an die Syntax für den Startpunkt, so kann der Interpreter die Ausführung nicht beginnen und wir haben einen semantischen Fehler gemacht. Innerhalb von main() befindet sich der Name args, mit dem die Parameter angesprochen werden. Der Name ist willkürlich, wir werden allerdings immer args verwenden.

Hinter den beiden Geteilt-Zeichen // befinden sich Kommentare. Sie gelten bis zum Ende der Zeile und dienen dazu, Erläuterungen zu den Quellcodezeilen hinzuzufügen, die ihn verständlicher machen.

Programme übersetzen und starten

Der Quellcode eines Java-Programms ist so alleine nicht ausführbar. Ein spezielles Programm, der Compiler (auch Übersetzer genannt), transformiert das geschriebene Programm in eine andere Repräsentation. Ein Quellcode mit Anweisungen für Programme muss aber nicht zwingend übersetzt werden. Eine andere Gattung für ein Ablaufmodell ist der Interpreter. Er liest die Datei Schritt für Schritt ein und führt dann die Anweisungen aus. Der Compiler liest die Datei in einem Rutsch und meldet Fehler. Häufig werden Skriptsprachen interpretiert, früher waren es oft BASIC-Programme. Compiler für geläufige Programmiersprachen wie C(++) oder Delphi übersetzen den Quellcode in Maschinen-Code. Das ist Binärcode, der vom Prozessor im Computer direkt ausführbar ist. Da unterschiedliche Rechner aber unterschiedliche Prozessoren besitzen, sind die Programme nicht direkt auf verschiedenen Rechnerplattformem ausführbar. Java ist jedoch als Programmiersprache entworfen worden, die plattformunabhängig ist, sich also nicht an einen physikalischen Prozessor klammert. Der Compiler übersetzt den Quellcode nicht in Binärcode für einen konkreten Prozessor, sondern in einen plattformunabhänigen Code, den Bytecode. Ein Prozessor wie die Intel- oder AMD-CPU kann aber mit diesem Bytecode nichts anfangen. Hier hilft ein Interpreter weiter. Dieser liest Anweisung für Anweisung aus dem Bytecode und führt entsprechende Befehle auf dem Mikroprozessor aus. Daher ist Java eine kompilierte und interpretierte Sprache zugleich.

Zum Übersetzen der Programme bietet Sun im JDK den Compiler javac an. Der Interpreter heißt java. Wir haben im einführenden Kapitel über den Ablauf und die möglichen Fehler schon gesprochen.

Beispiel Das Programm mit dem Namen Main.java übersetzen und starten:
javac Main.java
java  Main

Befindet sich im Quellcode nur die kleinste Ungenauigkeit, die der Compiler nicht toleriert, gibt er einen Fehler aus. Erst wenn Bytecode erzeugt wurde, kann der Interpreter diesen ausführen. Leider sehen wir noch nichts, da wir keine Anweisung gegeben haben. Dies soll sich nun ändern, denn wir wollen lernen, wie eine Bildschirmausgabe aussieht.


Galileo Computing

2.2.1 Kommentare  downtop

Programmieren heißt nicht nur, einen korrekten Algorithmus in einer Sprache auszudrücken, sondern auch, unsere Gedanken verständlich zu formulieren. Dies geschieht beispielsweise durch eine sinnvolle Namensgebung für Programmobjekte, wie Klassen, Funktionen und Variablen. Ein selbsterklärender Klassenname hilft den Entwicklern deutlich. Doch die Lösungsidee und der Algorithmus wird auch durch die schönsten Variablennamen nicht zwingend klarer. Damit Außenstehende unsere Lösungsidee schnell nachvollziehen und verstehen und später das Programm erweitern oder abändern können, werden Kommentare in den Quelltext eingeführt. Sie dienen nur den Lesern der Programme, haben aber auf die Abarbeitung keine Auswirkungen.

Kommentarblöcke können durch /* und */ abgetrennt werden. Zwischen diesen Zeichen kann nahezu beliebiger Text stehen. Da es keinen Präprozessor gibt, ist dies Aufgabe des Compilers, die Bemerkungen aus dem Quelltext zu entfernen. Da im Einzelfall nur Zeilen auskommentiert werden sollen, ist in Java auch das in C++ verwendete // erlaubt, welches wir schon kennengelernt haben.

// Zeilenkommentar

/*
 * Blockkommentar
 */

Dokumentationskommentare

Neben dem Blockkommentar ermöglicht Java eine interessante Möglichkeit der Programmdokumentation. Die Zeichen /** und */ beschreiben einen so genannten Dokumentationskommentar (engl. Doc-Comment), welches vor Methoden- oder Klassendefinitionen angewendet wird.

Beispiel Dokumentationskommentar:
/**
 * Hier drinnen ist ein Doc-Kommentar
 */

Der Teil zwischen den Kommentaren wird mit einem externen Dienstprogramm in HTML- oder Framemaker-Dokumente umgewandelt.

Blockkommentare dürfen nicht geschachtelt sein, wobei /* nicht das Problem ist. Ein Zeilenkommentar darf aber im Blockkommentar enthalten sein.


Galileo Computing

2.2.2 Funktionsaufrufe als Anweisungen  downtop

Bisher kennen wir keine konkreten Anweisungen. Daher möchte ich jetzt eine einfache Anweisung vorstellen, die wichtig für Programme ist: der Funktionsaufruf. Allgemein hat er folgendes Format:

funktionsname();

Innerhalb der Klammern dürfen wir Parameter angeben. Es lassen sich auch Funktionen in dieser Form anwenden, die ein Ausdruck sind und ein Ergebnis zurückgeben, wie eine Sinus-Funktion. Der Rückgabewert wird dann verworfen. Im Fall der Sinus-Funktion macht das wenig Sinn, denn sie macht außer der Berechnung nichts anderes.

Wir interessieren uns für eine Funktion, die eine Zeichenkette auf dem Bildschirm ausgibt. Sie heißt println(). Die meisten Methoden verraten durch ihren Namen, was sie leisten und für eigene Programme ist es sinnvoll, aussagekräftige Namen zu verwenden. Wenn die Java-Entwickler die Methode glubschi() genannt hätten, würde uns der Sinn der Methode versteckt bleiben. println() zeigt jedoch durch den Wortstamm »print« an, dass es etwas geschrieben wird. Die Endung ln (kurz für line) bedeutet, dass noch ein Zeilenvorschubzeichen ausgegeben wird. Umgangssprachlich heißt das: Eine neue Ausgabe beginnt in der nächsten Zeile. Neben println() existiert die Bibliotheksfunktion print(), die keinen Zeilenvorschub macht.

Die printXXX() -Methoden können in Klammern unterschiedliche Parameter bekommen. Ein Parameter ist ein Wert, den wir der Funktion beim Aufruf mitgeben wollen. Wir wollen die Diskussion über Parameter aber noch etwas verschieben. Unser printXXX()-Aufruf soll lediglich Zeichenketten ausgeben. (Ein anderes Wort für Zeichenketten ist String.) Ein String ist eine Folge von Buchstaben, Ziffern und Sonderzeichen in doppelten Anführungszeichen:

"Ich bin ein String"
"Ich auch. Und ich koste 7.59 D"

Um die obere Ausgabe mit dem Funktionsaufruf und einem trennenden Zeilenvorschub auf den Bildschirm zu bringen, schreiben wir:

System.out.println( "Ich bin ein String" );
System.out.println();
System.out.println( "Ich auch. Und ich koste 7.59 D" );

Auch wenn eine Funktion keine Parameter erwartet, muss beim Aufruf hinter dem Funktionsnamen ein Klammerpaar folgen. Dies ist konsequent, da wir so wissen, dass es ein Funktionsaufruf ist und nichts anderes. Andernfalls führt es zu Verwechselungen mit Variablen.

Auch eine weitere Eigenschaft von Java wird an dem Funktionsaufruf sichtbar. Es gibt Methoden, die unterschiedliche Parameter besitzen, aber gleichen Namen tragen. Diese Funktionen nennen wir überladen. Die printXXX()-Methoden sind sogar vielfach überladen und akzeptieren neben Strings auch weitere Werte als Parameter.

Programmieren wir eine ganze Java-Klasse, die etwas auf dem Bildschirm ausgibt.

Listing 2.2   Main.java
class Main
{
  public static void main( String args[] )
  {
  // Hier ist der Anfang unserer Programme

    System.out.println( "Hallo Javanesen" );

  // Hier enden unsere Programme
  }
}
Hinweis Anweisungen wie ein Funktionsaufruf enden immer mit einem Semikolon.

Erste Idee der Objektorientierung

In einer objektorientierten Programmiersprache sind alle Methoden an bestimmte Objekte gebunden (daher der Begriff Objekt-orientiert). Betrachten wir zum Beispiel das Objekt Radio. Ein Radio spielt Musik ab, wenn der Ein-Schalter betätigt wird und ein Sender und die Lautstärke eingestellt sind. Ein Radio bietet also bestimmte Dienste (Operationen) an, wie Musik an/aus, lauter/leiser. Zusätzlich hat ein Objekt auch noch einen Zustand. Es ist zum Beispiel die Farbe oder das Baujahr. Wichtig in objektorientierten Sprachen ist, dass die Operationen und Zustände immer (und da gibt es keine Ausnahmen) an Objekte bzw. Klassen gebunden sind. (Mehr zu dieser Unterscheidung später.) Der Aufruf einer Methode auf ein Objekt richtet die Anfrage genau an ein bestimmtes Objekt. Steht in einem Java-Programm einfach nur die Anweisung lauter, so weiß der Compiler nicht, wen er fragen soll. Was ist, wenn es auch noch einen Fernseher gibt? Aus diesem Grunde verbinden wir das Objekt, das etwas kann, mit der Operation. Ein Punkt trennt das Objekt von der Operation oder dem Zustand.

So gehört auch println() zu einem Objekt, welches die Bildschirmausgabe übernimmt. Dass eine Methode immer zu einem Objekt gehört, können wir auch an unserem eigenen Programm überprüfen. main() gehört zu der Klasse Main, die später ein Objekt bilden kann. Daher können wir in Java auch nicht einfach die Ausgabeanweisung schreiben. println() gehört zu einem Objekt mit dem Namen out. Dieses Objekt ist wiederum Teil der Klasse System. Wir können das vergleichen mit einem Aufruf zum Beispiel von

BRD.aktuellerBundeskanzler.fragen( "Wieso kassieren 
ARD" +
 " und ZDF jährlich 11 Mrd. Mark Rundfunkgebühren"+
 " und müssen davon nichts abführen?" );

Mehr zu diesen Aufrufen zu einem späterem Zeitpunkt. Das obige Beispiel macht aber jetzt schon deutlich, dass Strings mit dem Plus zusammengehängt werden können.


Galileo Computing

2.2.3 Die leere Anweisung  downtop

Die einfachste Anweisung besteht nur aus einem Semikolon und ist die leere Anweisung:

;

Sie wird verwendet, wenn die Sprachgrammatik eine Anweisung vorschreibt, aber wenn in dem Programmablauf keine Anweisung vorkommen muss. So muss etwa hinter dem Schleifenkopf eine Anweisung folgen. Wir werden bei den Schleifen eine Anwendung der leeren Anweisung sehen.


Galileo Computing

2.2.4 Der Block  downtop

Ein Block innerhalb von Methoden oder statistischen Blöcken fasst eine Gruppe von Anweisungen zusammen, die hintereinander ausgeführt werden. Dazu werden die Anweisungen zwischen geschweiften Klammern geschrieben:

{
  Anweisung1;
  Anweisung2;
  ...
}

Ein Block kann überall dort verwendet werden, wo auch eine einzelne Anweisung stehen kann. Der neue Block hat jedoch eine Besonderheit für Variablen, denn der neue Block bildet einen lokalen Bereich für die darin befindlichen Anweisungen inklusive der Variablen. Blöcke können auch geschachtelt werden.


Galileo Computing

2.3 Elemente einer Programmiersprache  downtop

Wir haben nun einige Beispiele für Java-Programme gesehen und wollen nun über das Regelwerk, die Grammatik und Syntax sprechen. Wir wollen uns unter anderem über die Codierung in Unicode, die Token und Bezeichner Gedanken machen.


Galileo Computing

2.3.1 Textkodierung durch Unicode-Zeichen  downtop

Die Algorithmen für Java-Programme bestehen aus einer Folge von Anweisungen und Unterprogrammen. In Anweisungen und Funktionsnamen werden Folgen von Zeichen als Bezeichner eingesetzt, die diese Bezeichner und Funktionen kennzeichnen. Wir müssen ihnen Namen geben, und dabei dürfen wir uns der Zeichen auf der Tastatur bedienen. Der Zeichenvorrat nennt sich auch Lexikalik.

Texte werden in Java durch 16 Bit lange Unicode-Zeichen kodiert. Der Unicode-Zeichensatz beinhaltet die ASCII-Zeichen nach ISO8859-1 (Latin-1), daher gehören alle gewöhnlichen Zeichen auch zum erweiterten Zeichensatz. Da mit 16 Bit etwa 65.000 Zeichen kodiert werden können, sind auch alle wichtigen Zeichensätze für andere Schriftsprachen kodiert. Eine angenehme Konsequenz ist, dass auch der Quellcode in der Landessprache programmiert werden kann. Deutsche Umlaute stellen demnach für den Compiler kein Hindernis dar.

Tipp Obwohl Java intern Unicode für alle Bezeichner einsetzt, ist es dennoch ungünstig, Klassennamen zu wählen, die Unicode-Zeichen enthalten. Das Problem liegt nicht in der Programmiersprache begründet, sondern im Dateisystem der meisten Betriebssysteme. Sie speichern die Namen oft im alten 8-Bit ASCII-Zeichensatz ab.

Schreibweise für Unicode-Zeichen

Kaum ein Editor dürfte in der Lage sein, alle Unicode-Zeichen anzuzeigen. Beliebige Unicode-Zeichen lassen sich als \uxxxx schreiben, wobei x eine hexadezimale Ziffer ist – also 0..1, A..F bzw. a..f. Diese Sequenzen können an beliebiger Stelle eingesetzt werden. So können wir anstatt eines Anführungszeichens alternativ \u0027 schreiben, und dies wird vom Compiler als gleichwertig angesehen. Das Unicode-Zeichen \uffff ist nicht definiert und kann daher bei Zeichenketten als Ende-Symbol verwendet werden. Unicode-Zeichen für deutsche Sonderzeichen sind folgende.

Tabelle 2.1   Deutsche Sonderzeichen in Unicode
Zeichen Unicode
Ä,ä \u00c4, \u00e4
Ö, ö \u00d6, \u00f6
Ü,ü \u00dc, \u00fc
ß \u00df

Anzeige der Unicode-Zeichen

Die Darstellung der Zeichen ist unter den meisten Plattformen noch ein großes Problem. Die Unterstützung für die Standardzeichen des ASCII-Alphabets ist dabei weniger ein Problem als die Sonderzeichen, die der Unicode-Standard definiert. Unter ihnen zum Beispiel der beliebte Smiley :-), der als Unicode \u263A (WHITE SMILING FACE) und \u2369 (WHITE FROWNING FACE) :-( definiert ist. Das vermutlich in nächster Zeit häufig benutzte Euro-Zeichen ist unter \u20ac zu finden.

Tipp Sofern die Sonderzeichen und Umlaute sich auf der Tastatur befinden, sollten keine Unicode-Kodierungen Verwendung finden. Der Autor von Quelltext sollte seine Leser nicht zwingen, eine Unicodetabelle zur Hand zu haben. Die Alternativdarstellung lohnt sich daher nur, wenn der Programmtext bewusst unleserlich gemacht werden soll.

Ein Versuch, den Smiley auf die Standardausgabe zu drucken, scheitert oft an der Fähigkeit des Terminals bzw. der Shell. Hier ist eine spezielle Shell nötig, die aber bei den meisten Systemen erst noch in der Entwicklung ist. Und auch bei grafischen Oberflächen ist die Integration noch mangelhaft. Es wird Aufgabe der Betriebssystementwickler bleiben, dies zu ändern.


Galileo Computing

2.3.2 Bezeichner  downtop

Für Variablen (und damit Konstanten), Methoden, Klassen und Schnittstellen werden Bezeichner vergeben, die die entsprechenden Bausteine anschließend im Programm identifizieren. Unter Variablen sind dann Daten verfügbar, Methoden sind die Prozeduren in objektorientierten Programmiersprachen und Klassen sind die Bausteine objektorientierter Programme.

Ein Bezeichner ist eine Folge von Zeichen, die fast beliebig lang sein kann. (Die Länge ist nur theoretisch festgelegt). Die Zeichen sind Elemente aus dem gesamten Unicode-Zeichensatz und jedes Zeichen ist für die Identifikation wichtig. Das heißt, ein Bezeichner, der 100 Zeichen lang ist, muss auch immer mit allen 100 Zeichen korrekt angegeben werden. Manche C- und FORTRAN-Compiler sind in dieser Hinsicht etwas großzügiger und bewerten nur die ersten Stellen. Jeder Bezeichner muss mit einem Unicode-Buchstaben beginnen. Dies sind Unicode-Zeichen zum Beispiel aus dem Bereich »A« bis »Z« und »a« bis »z« (nicht beschränkt auf lateinische Zeichen) sowie »_« und »$«. Nach dem ersten Buchstaben können neben den Buchstaben auch Ziffern folgen. Dass der Unterstrich mittlerweile mit zu den Buchstaben zählt, ist nicht weiter verwunderlich, doch dass das Dollarzeichen mitgezählt wird, ist schon erstaunlich. Sun erklärt den Einsatz einfach damit, dass diese beiden Zeichen »aus historischen Gründen« mit aufgenommen wurden. Eine sinnvollere Erklärung ist, dies mit der Verwendung von maschinen-generiertem Code zu erklären. Es bleibt noch einmal zu erwähnen, dass zwischen Groß/Kleinschreibung unterschieden wird.

Die folgende Tabelle listet einige gültige und ungültige Bezeichner auf.

Tabelle 2.2   Beispiele für Bezeichner in Java
Gültige Bezeichner Ungültige Bezeichner
mami 2und2macht4
kulliReimtSichAufUlli class
IchWeißIchMussAndréAnrufen hose gewaschen
raphaelIstLieb hurtig!

mami ist genau ein Bezeichner, der nur aus Alphazeichen besteht und daher korrekt ist. Auch kulliReimtSichAufUlli ist korrekt. Der Bezeichner zeigt zusätzlich die in Java übliche Bezeichnerbildung; denn besteht dieser aus mehreren einzelnen Wörtern, werden diese einfach ohne Leerzeichen hintereinandergesetzt, jedes Teilwort (außer dem ersten) beginnt jedoch dann mit einem Großbuchstaben. Leerzeichen sind in Bezeichnern nicht erlaubt und daher ist auch hose gewaschen ungültig. Auch das Ausrufezeichen ist, wie viele Sonderzeichen, ungültig. IchWeißIchMussAndréAnrufen ist jedoch wieder korrekt, auch wenn es ein Apostroph-é enthält. Treiben wir es weiter auf die Spitze, dann sehen wir einen gültigen Bezeichner, der nur aus griechischen Zeichen gebildet ist. Auch der erste Buchstabe ist ein Zeichen, anders als in 2und2macht4. Und class ist ebenso ungültig, da der Name schon von Java belegt ist. Die Tabelle im nächsten Abschnitt zeigt uns, welche Namen wir nicht verwenden können. Für class nehmen Programmierer als Ersatz gerne clazz.


Galileo Computing

2.3.3 Reservierte Schlüsselwörter  downtop

Bestimmte Wörter sind als Bezeichner nicht zulässig, da sie als Schlüsselwörter durch den Compiler besonders behandelt werden. Schlüsselwörter bestimmen die »Sprache« eines Compilers. Nachfolgende Zeichenfolgen sind Schlüsselwörter in Java und daher nicht als Bezeichnernamen möglich:

Tabelle 2.3   Reservierte Schlüsselwörter in Java
abstract boolean break byte
byvalue† case cast† catch
char class const† continue
default do double else
extends final finally float
for future† generic† goto†
if implements import instanceof
int inner† interface long
native new null operator†
outer† package private protected
public rest† return short
static strictfp super switch
synchronized this throw throws
transient try var† void
volatile while    

Obwohl die mit † gekennzeichneten Wörter zurzeit nicht von Java benutzt werden, können doch keine Variablen dieses Namens definiert werden.


Galileo Computing

2.3.4 Token  downtop

Ein Token ist eine lexikalische Einheit, die dem Compiler die Bausteine des Programms liefern. Der Compiler erkennt an der Grammatik einer Sprache, welche Folgen von Zeichen ein Token bilden. Für Bezeichner heißt dies beispielsweise: Nimm die nächsten Zeichen, solange auf einen Buchstaben nur Buchstaben oder Ziffern folgen. Eine Zahl wie 1982 bildet zum Beispiel ein Token durch folgende Regel: Lese solange Ziffern, bis keine Ziffer mehr folgt. Bei Kommentaren bilden die Kombinationen /* und */ ein Token.

Problematisch wird es in einer Sprache immer dann, wenn der Compiler die Token nicht voneinander unterscheiden kann. Daher fügen wir Trennzeichen (engl. White-Spaces), auch Wortzwischenräume genannt, ein. Zu den Trennern zählen Leerzeichen, Tabulatoren, Zeilenvorschub- und Seitenvorschubzeichen. Außer als Trennzeichen haben diese Zeichen keine Bedeutung. Daher können sie in beliebiger Anzahl zwischen die Token gesetzt werden. Das heißt auch, beliebig viele Leerzeichen sind zwischen Token gültig. Und da wir damit nicht geizen müssen, können sie einen Programmabschnitt enorm verdeutlichen. Programme sind besser lesbar, wenn sie luftig formatiert sind.


Galileo Computing

2.3.5 Semantik  downtop

Die Syntax eines Java-Programms definiert die Token und bildet so das Vokabular. Richtig geschriebene Programme müssen aber dennoch nicht korrekt sein. Unter dem Begriff »Semantik« fassen wir daher die Bedeutung eines syntaktisch korrekten Programms zusammen. Die Semantik bestimmt daher, was das Programm macht. Die Abstraktionsreihenfolge ist also Lexikalik, Syntax und Semantik. Der Compiler durchläuft diese Schritte bevor er den Bytecode erzeugen kann.


Galileo Computing

2.4 Datentypen  downtop

Java nutzt, wie es für imperative Programmiersprachen typisch ist, Variablen zum Ablegen von Daten. Eine Variable ist ein reservierter Speicherbereich und belegt eine feste Anzahl von Bytes. Alle Variablen (und auch Ausdrücke) haben einen Typ, der zur Übersetzungszeit bekannt ist. Der Typ wird auch Datentyp genannt, da eine Variable einen Datenwert, auch »Datum« genannt, hält. Für jeden Typ lässt sich die Speichergröße berechnen. Beispiele für Datentypen sind: Ganzzahlen, Fließkommazahlen, Wahrheitswerte, Zeichen und Objekte. Da jede Variable einen festen Datentyp hat, zählt Java zu den streng-typisierten Sprachen . Der Datentyp erlaubt dem Übersetzer auch, die Daten im Speicher nach bestimmten Regeln zu behandeln. Wenn wir einen Speicherauszug lesen und dort die Bitinformationen 01011010, 11010010, 01010011, 10100010 finden, ist uns auch nicht klar, ob dies nun vier Buchstaben sind, eine Fließkommazahl oder eine Ganzzahl ist.

Die grobe Richtung

Die Datentypen in Java zerfallen in zwei Kategorien: Primitive Typen und Referenztypen (auch Klassentypen). Die primitiven Typen sind die eingebauten Datentypen, die nicht als Objekte verwaltet werden. Referenztypen sind genau das, was noch übrig bleibt. Es gibt aber auch Sprachen, die keine primitiven Datentypen besitzen. Als Beispiel sei noch einmal auf das unten angesprochene Smalltalk verwiesen.

Warum Sun sich für diese Teilung entschieden hat, lässt sich aus zwei Gründen erklären:

gp  Viele Programmierer kennen Syntax und Semantik von C(++) und anderen imperativen Programmiersprachen. Auf die neue Sprache Java zu wechseln fällt leicht, und das objektorientierte Denken aus C++ verhilft dazu, sich auf der Insel zurechtzufinden.
gp  Der andere Grund ist die Geschwindigkeit der häufig vorkommenden elementaren Rechenoperationen. Ein Compiler kann einerseits viele Optimierungen vornehmen, und das ist bei einer Sprache, die in der Industrie eingesetzt wird, ein wichtiges Kriterium. Die Klassen liefern andererseits die optimale Unterstützung für eine Wiederverwendung und skalierbare Produkte.

Wir werden uns im Folgenden erst mit primitiven Datentypen beschäftigen. Referenzen werden nur dann eingesetzt, wenn Objekte ins Spiel kommen. Dies dauert jedoch noch etwas.


Galileo Computing

2.4.1 Primitive Datentypen  downtop

In Java gibt es einige eingebaute Datentypen für ganze Zahlen, Gleitkommazahlen nach IEEE 754, Zeichen, Wahrheitswerte. (Strings werden bevorzugt behandelt, sind aber lediglich Verweise auf Objekte.) Die folgende Tabelle gibt darüber einen Überblick. Anschließend betrachten wir jeden Datentyp präziser.

Tabelle 2.4   Java-Datentypen und deren Wertebereiche
Schlüsselwort Länge in Bytes Belegung (Wertebereich)

boolean

1 true oder false

char

2 16-Bit Unicode Zeichen (0x0000...0xffff)

byte

1 -2^7 bis 2^7 – 1 (-128...127)

short

2 -2^15 bis 2^15 – 1 (-32768...32767)

int

4 -2^31 bis 2^31 – 1 (-2147483648...2147483647)

long

8 -2^63 bis 2^63 – 1 (-9223372036854775808...9223372036854775807)

float

4 1.40239846E-45f…3.40282347E+38f

double

8 4.94065645841246544E-324... 1.79769131486231570E+308

Zwei wesentliche Punkte zeichnen die primitiven Datentypen aus:

gp  Alle Datentypen haben eine festgesetzte Länge, die sich unter keinen Umständen ändert. Der Nachteil, dass der Programmierer die Länge eines Datentyps nicht kennt, besteht in Java nicht. In den Sprachen C/C++ bleibt dies immer unsicher und die Umstellung auf 64-Bit-Maschinen bringt viele Probleme mit sich. Bei der Betrachtung der Auflistung fällt auf, dass char 16 Bit lang ist.
gp  Die numerischen Datentypen byte, short, int und long sind vorzeichenbehaftet, Fließkommzahlen sowieso. Dies ist leider nicht immer praktisch, aber wir müssen uns stets daran erinnern. Probleme gibt es, wenn wir einem Byte zum Beispiel den Wert 240 zuweisen wollen. Ein char ist im Prinzip ein vorzeichenloser Ganzzahltyp.

Galileo Computing

2.4.2 Wahrheitswerte  downtop

Der Datentyp boolean beschreibt einen Wahrheitswert, der entweder true oder false ist. Die Zeichenketten true und false sind reservierte Wörter und bilden so genannte Literale. Kein anderer Wert ist für Wahrheitswerte möglich.

Der boolesche Typ wird beispielsweise bei Bedingungen, Verzweigungen oder Schleifen benötigt.

Abbildung


Galileo Computing

2.4.3 Variablendeklarationen  downtop

Mit Variablen lassen sich Daten speichern, die vom Programm gelesen und geschrieben werden können. Um Variablen zu nutzen, müssen sie deklariert werden. Wir sprechen hier auch von der Definition einer Variablen. Die Schreibweise einer Variablendeklaration ist immer die Gleiche: Hinter dem Typnamen folgt der Name der Variablen.

Typname Variablenname;

Ein Variablenname (der dann Bezeichner ist) kann alle Buchstaben und Ziffern des Unicode-Zeichensatzes beinhalten, mit der Ausnahme, dass am Anfang einer Zeichenkette keine Ziffer stehen darf. Ebenfalls darf der Variablenname mit keinem reservierten Schlüsselwort identisch sein.

Hinweis Zwei Variablen ähnlicher Schreibweise, etwa counter und counters, führen schnell zur Verwirrung. Als Programmierer sollten wir uns konsistent an ein Namensschema halten.

Die Variablendeklaration ist eine Anweisung und wird daher mit einem Semikolon abgeschlossen.

Werden mehrere Variablen gleichen Typs bestimmt, so können diese mit einem Komma getrennt werden. Eine Deklaration kann in jeden Block geschrieben werden:

Typname Variablenname1[, Variablenname2, ... ]

Schreiben wir ein einfaches Programm, welches eine Wahrheitsvariable definiert und zuweist. Die Variablenbelegung erscheint zusätzlich auf dem Bildschirm.

Listing 2.3   FirstVariable.java
class FirstVariable
{
  public static void main( String args[] )
  {
    boolean trocken;

    trocken = true;

    System.out.print( "Ist die Socke trocken? " );
    System.out.println( trocken );
  }
}

Die Zeile trocken = true ist eine Zuweisung, die die Variable trocken mit einem Wert belegt. Sie ist ebenfalls eine Anweisung und wird mit einem Semikolon beendet. Steht auf der rechten Seite keine Variable, so steht dort ein Literal, eine Konstante, wie in unserem Fall true. Wir haben schon erwähnt, dass es für Wahrheitswerte nur die Literale true und false gibt.


Galileo Computing

2.4.4 Ganzzahlige Datentypen  downtop

Java stellt vier ganzzahlige Datentypen zur Verfügung: byte, short, int und long. Sie unterscheiden sich nur in der Länge, die jeweils 1, 2, 4 und 8 Byte umfasst. Die ganzzahligen Typen (lassen wir char einmal außen vor) sind in Java immer vorzeichenbehaftet und einen Modifizierer unsigned wie in C(++) gibt es nicht.10 

Beispiel Variablendeklaration mit Wertinitialisierung
int schuhGröße;

int i = 1243, j = 01230, k = 0xcafebabe;

Den Variablen kann gleich bei der Definition ein Wert zugewiesen werden. Hinter einem Gleichheitszeichen wird der Wert geschrieben, der oft ein Literal ist. Eine Zuweisung gilt nur für immer genau eine Variable.

Die Literale für Ganzzahlen lassen sich in drei unterschiedlichen Zahlensystemen angeben. Das natürlichste ist das Dezimalsystem, wie das Beispiel an der Variablen i zeigt. Die Literale bestehen aus den Ziffern »0« bis »9«. Zusätzlich existiert die Oktal- und Hexadezimalform, die die Zahlen zur Basis 8 und 16 schreiben. Ein oktaler Wert beginnt mit Präfix »0« und ein hexadezimaler Wert mit »0x«. Mit der Basis 8 werden nur die Ziffern »0« bis »7« für oktale Werte benötigt. Da zehn Ziffern für hexadezimale Zahlen nicht ausreichen, besteht eine Zahl zur Basis 16 zusätzlich aus den Buchstaben »a« bis »f« (beziehungsweise »A« bis »F«). Ganzzahlen doppelter Größe werden mit einem »l« oder »L« am Ende versehen.

Beispiel Deklaration eines long mit angehängtem »L«
long l = 123456789098L, m = -1L, 
n = 0xC0B0L;

Negative Zahlen werden durch Voranstellen eines Minus-Zeichens gebildet. Ein Plus-Zeichen für positive Zeichen ist optional.

Gefährliche Syntax bei langen Datentypen

Betrachten wir folgende Zeile, so ist auf den ersten Blick kein Fehler zu erkennen:

System.out.println( 123456789012345 );

Der Übersetzungsvorgang fördert jedoch noch einmal zu Tage, dass alle Datentypen ohne explizite Größenangabe als int angenommen werden, dass heißt 32 Bit lang sind. Obige Zeile führt daher zu einem Compilerfehler, da die Zahl nicht im Wertebereich von - 2147483648 bis 2147483647 liegt. Java reserviert also nicht so viele Bits wie benötigt und wählt nicht automatisch den passenden Wertebereich. Der muss ausdrücklich angegeben werden. Um die Zahl 123456789012345 gültig ausgeben zu lassen, müssen wir schreiben:

System.out.println( 123456789012345l );

Ersichtlich wird, dass ein kleines »l« sehr viel Ähnlichkeit mit einer Eins besitzt. Daher sollte bei Längenangaben immer ein großes »L« hinten angestellt werden.

System.out.println( 123456789012345L 
);

Allerdings ist das Compiler-Verhalten verwirrend, denn bei folgender Anweisung findet er auch automatisch die richtige Größe.

byte b = 12;

Galileo Computing

2.4.5 Die Fließkommazahlen  downtop

Java unterscheidet Fließkommazahlen einfacher Genauigkeit (float) und doppelter Genauigkeit (double). Die Datentypen sind im IEEE-754 Standard beschrieben und haben eine Länge von 4 Byte für float und 8 Byte für double.

Die Literale bestehen aus einem Vorkommateil, einem Dezimalpunkt (kein Komma) und einem Nachkommateil. Optional kann ein Exponent angegeben werden. Standardmäßig sind die Literale vom Typ double. Ein nachgestelltes »f« (oder »F«) zeigt an, dass es sich um ein float handelt. Vorkommateil und Exponent dürfen durch die Vorzeichen »+« oder »-« eingeleitet werden.

Beispiel Gültige Zuweisungen für Fließkommazahlen vom Typ double und float:
double pi = 3.1415, klein = .001, x = 3.00e+8;
float y = 3.00E+8F;

Der Exponent kann entweder positiv oder negativ11  sein, muss aber eine Ganzzahl sein.


Galileo Computing

2.4.6 Zeichen  downtop

Der Datentyp char ist 2 Bytes groß und nimmt ein Unicode-Zeichen auf. Ein char ist nicht vorzeichenbehaftet. Die Literale für Zeichen werden in einfache Hochkommata gesetzt. Spracheinsteiger verwechseln häufig die einfachen Hochkommata mit den Anführungszeichen der Zeichenketten (Strings). Die einfache Merkregel: Ein Zeichen, ein Hochkomma, mehrere Zeichen zwei Hochkommata (Gänsefüßchen).

Beispiel Korrekte Hochkommata für Zeichen und Zeichenketten:
char   c = 'a';
String s = "Heut' schon gebeckert?";

Escape-Sequenzen/Fluchtsymbole

Für spezielle Zeichen stehen Escape-Sequenzen zur Verfügung, die so nicht direkt als Zeichen dargestellt werden können.

Tabelle 2.5   Escape-Sequenzen
Zeichen Bedeutung

\b

Rückschritt (Backspace)

\n

Zeilenschaltung (Newline)

\f

Seitenumbruch (Formfeed)

\r

Wagenrücklauf (Carriage return)

\t

Horizontaler Tabulator

\"

Doppeltes Anführungszeichen

\'

Einfaches Anführungszeichen

\\

Backslash

Beispiel Zeichenvariablen mit Initialwerten und Sonderzeichen:
char          a = 'a',
    singlequote = '\'',
        newline = '\n',
Die Fluchtsymbole sind für Zeichenketten die gleichen. Auch dort können bestimmte Zeichen mit Escape-Sequenzen dargestellt werden.

Beispiel String s = "Er fragte: \"Wer lispelt wie Katja Burkard?\"";


Galileo Computing

2.4.7 Die Typanpassung (Das Castingdowntop

Möglicherweise kommt es vor, dass Datentypen konvertiert werden müssen. Dies nennt sich Typanpassung (engl. Typecast) oder auch casten. Java unterscheidet zwei Arten der Typanpassung:

gp  Automatische Typanpassung
Daten eines kleineren Datentyps werden automatisch dem größeren angepasst.
gp  Explizite Typanpassung
Ein größerer Typ kann einem kleineren Typ nur mit Verlust von Informationen zugewiesen werden.

Automatische Anpassung der Größe

Werte der Datentypen byte und short werden bei Rechenoperationen automatisch in den Datentyp int umgewandelt. Ist ein Operand vom Datentyp long, dann werden alle Operanden auf long erweitert. Wird aber short oder byte als Ergebnis verlangt, dann ist dieses durch einen expliziten Typecast anzugeben und nur die niederwertigen Bits des Ergebniswerts werden übergeben. Folgende Typumwandlungen sind ohne Informationsverlust möglich:

Tabelle 2.6   Zuweisungen ohne Informationsverlust
Von Typ in Typ
byte short, char, int, long, float, double
short int, long, float, double
char int, long, float, double
int long, float, double
long float, double
float double

Explizite Typanpassung

Der gewünschte Typ für eine Typanpassungen wird vor den umzuwandelnden Datentyp geschrieben. Der gewollte Datentyp ist geklammert.

Beispiel Umwandlung einer Fließkommazahl in eine Ganzzahl:
int n = (int) 3.1415;

Eine Typumwandlung hat eine sehr hohe Priorität. Daher muss der Ausdruck gegebenenfalls geklammert werden.

Beispiel Die Zuweisung an n verfehlt das Ziel.
int n = (int) 1.0315 + 2.1;
int m = (int)(1.0315 + 2.1);       // das ist korrekt

Typumwandlung von Fließkomma- nach Ganzzahlen

Bei der expliziten Typumwandlung von double und float in einen Ganzzahltyp kann es selbstverständlich zum Verlust von Genauigkeit kommen sowie zur Einschränkung des Wertebereichs. Bei der Konvertierung von Fließkommazahlen verwendet Java eine Rundung gegen Null.

Beispiel Zahlen, die bei der Konvertierung die Rundung nach Null aufzeigen.
double w = +12.34;*
double x = +67.89;
double y = -12.34;
double z = -67.89;
System.out.println( (int) w );  // 12
System.out.println( (int) x );  // 67
System.out.println( (int) y );  // -12
System.out.println( (int) z );  // -67

Bei der Konvertierung eines größeren Ganzzahltyps in einen kleineren werden einfach die oberen Bits abgeschnitten. Eine Anpassung des Vorzeichens findet nicht statt.

int i = 123456789;
int j = -123456;
System.out.println( (short) i );  // -13035
System.out.println( (short) j );  // 7616

short und char

Ein short hat wie ein char eine Länge von 16 Bit. Doch diese Umwandlung ist nicht ohne ausdrückliche Konvertierung möglich. Das liegt am Vorzeichen vom short. Zeichen sind per Definition immer ohne Vorzeichen. Würde ein char mit einem gesetzten höchstwertigen letzen Bit in ein short konvertiert, käme eine negative Zahl heraus. Ebenso, wenn ein short eine negative Zahl bezeichnet, wäre das oberste Bit im char gesetzt, was unerwünscht ist. Die ausdrückliche Umwandlung erzeugt immer nur positive Zahlen.

Der Verlust bei der Typumwandlung von char nach short tritt etwa bei der Han-Zeichenkodierung für chinesische, japanische oder koreanische Zeichen auf. Denn dort ist im Unicode das erste Bit gesetzt, welches bei der Umwandlung in ein short dem nicht gesetzten Vorzeichenbit weichen muss.

Probleme bei Zuweisungen

Leider ist die Typanpassung nicht ganz so einleuchtend, wie folgendes Beispiel zeigt.

Listing 2.4   AutoConvert.java
public class AutoConvert
{
  public static void main( String args[] )
  {
    int   i1 = 1, i2 = 2, i3;
    long  l1 = 1, l2 = 2, l3;
    short s1 = 1, s2 = 2, s3;
    byte  b1 = 1, b2 = 2, b3;
    i3 = i1 + i2;              // das ist noch OK
    l3 = l1 + l2;
//    s3 = s1 + s2;              // Compilerfehler!
//    b3 = b1 + b2;
    s3 = (short) ( s1 + s2 );  // das ist wieder OK
    b3 = (byte)  ( b1 + b2 );
  }
}

Dies ist auf den ersten Blick paradox. Es ist nicht möglich, ohne explizite Typumwandlung zwei short- oder byte-Zahlen zu addieren. Das Verhalten des Übersetzers lässt sich mit der automatischen Anpassung erklären. Wenn Ganzzahl-Ausdrücke vom Typ kleiner int mit einem Operator verbunden werden, passt der Compiler eigenmächtig den Typ auf int an. Die Addition der beiden Zahlen arbeitet also nicht mit short- oder byte-Werten, sondern int-Werten. So werden auch Überläufe korrekt behandelt.

 

Bei der Zuweisung wird dies zum Problem. Denn dann steht auf der rechten Seite ein int und auf der linken Seite der kleinere Typ byte oder short. Nun muss der Compiler meckern, da Zahlen abgeschnitten werden könnten. Mit der ausdrücklichen Typumwandlung erzwingen wir diese Konvertierung und akzeptieren ein paar fehlende Bits. Diese Eigenart ist insofern verwunderlich, als dass doch auch ein int nur dann zu einem long erweitert wird, wenn einer der Operanden eines Ausdrucks vom Typ long ist.

Materialverlust durch Überläufe

Überläufe bei Berechnungen können zu schwerwiegenden Fehlern führen, so wie beim Absturz der Ariane 5 am 4. Juni 1996 genau 36.7 Sekunden nach dem Start. Die europäische Raumfahrtbehörde European Space Agency (ESA) startete von Französisch Guyana aus eine unbemannte Rakete mit vier Satelliten an Bord, die 40 Sekunden nach dem Start explodierte. Glücklicherweise kamen keine Menschen ums Leben, doch der materielle Schaden beläuft sich auf etwa 500 Millionen Dollar. In dem Projekt steckten zusätzlich Entwicklungskosten von etwa 7 Milliarden Dollar. Grund für den Absturz war ein Rundungsfehler, der durch eine Umwandlung einer 64-Bit Fließkommazahl (die horizontale Geschwindigkeit) in eine 16 Bit vorzeichenbehaftete Ganzzahl auftrat. Die Zahl war leider größer als 215  und die Umwandlung war nicht gesichert, da die Programmierer diesen Zahlenbereich nicht angenommen haben. Die Konsequenz war, dass das Lenksystem zusammenbrach und die Selbstzerstörung ausgelöst wurde, da die Triebwerke abzubrechen drohten. Das wirklich Dumme an dieser Geschichte ist, dass die Software nicht wirklich für den Flug notwendig war und nur den Startvorbereitungen diente. Im Falle einer Unterbrechung während des Countdowns hätte dann das Programm schnell abgebrochen werden können. Ungünstig war, dass der Programmteil unverändert durch Wiederverwendung per Copy-and-Paste aus der Ariane 4 Software kopiert wurde, die Ariane 5 aber schneller flog.


Galileo Computing

2.4.8 Lokale Variablen, Blöcke und Sichtbarkeit  downtop

In jedem Block und auch in jeder Klasse12  können Variablen deklariert werden. Globale Variablen, die für alle Funktionen und Klassen sichtbar sind, gibt es in Java nicht. (Eine globale Variable müsste in einer Klasse definiert werden, die dann alle Klassen übernehmen.)

Sichtbarkeit

Jede Variable hat einen Geltungsbereich (engl. Scope). Sie sind nur in dem Block gültig, in dem sie definiert wurden. In dem Block ist die Variable lokal.

 

 

 

Beispiel Da ein Block immer mit geschweiften Klammern angegeben wird, erzeugen wir durch folgende Funktionen Blöcke, die einen weiteren inneren Block besitzen. Somit sind Blöcke ineinander geschachtelt.
void foo()
{
  int i;
  {
    int j;                // j gilt nur in dem Block
    j = 1;
  }
//  j = 2;                // Funktioniert auskommentiert nicht
}
void bar()
{
  int i, k;               // i hat mit oberem i nicht zu tun
  {
//    int k;              // Das würde nicht gehen!
  }
}

Zu jeder Zeit können Blöcke definiert werden. Außerhalb des Blocks sind deklarierte Variablen nicht sichtbar. Nach Abschluss des inneren Blocks, der j deklariert, ist ein Zugriff auf j nicht mehr möglich; auf i ist der Zugriff weiterhin erlaubt. Falls Objekte im Block angelegt wurden, wird der GC diese wieder freigeben, falls keine zusätzliche Referenz besteht.

Variablennamen können innerhalb eines Blocks nicht genauso gewählt werden wie lokale Variablennamen eines äußeren Blocks oder wie die Namen für die Parameter einer Funktion. Das zeigt zum Beispiel die Definition der Variablen k. Obwohl andere Programmiersprachen das erlauben, haben sich die Java-Sprachentwickler dagegen entschieden, um Fehlerquellen zu vermeiden.


Galileo Computing

2.4.9 Initialisierung von lokalen Variablen  downtop

Während Objektvariablen automatisch mit einem Null-Wert initialisiert werden, geschieht dies bei lokalen Variablen nicht. Das heißt, der Programmierer muss sich selber um die Initialisierung kümmern.

Beispiel Häufig passieren Fehler bei falsch angewendeten bedingten Anweisungen, wie das folgende Programmsegment demonstriert.
void test()
{
  int nene, williWurm;
  nene += 1;                   // Compilerfehler
  nene = 0; nene = nene + 1;
  if ( nene == 1 )
    williWurm = 2;
  williWurm = williWurm + 1;   // Compilerfehler
}

Die beiden lokalen Variablen nene und williWurm werden nicht automatisch mit Null initialisiert – so wie dies für Objektvariablen der Fall ist. So kommt es bei der Inkrementierung von nene zu einem Compilerfehler. Denn dazu ist erst ein Lesezugriff auf die Variable nötig, um anschließend den Wert 1 zu addieren. Der erste Zugriff muss aber eine Zuweisung sein. Das bedeutet, nene=0 ist in Ordnung. Den Fehler würden wir auch bekommen, wenn wir in System.out.println(nene) die Variablenbelegung auslesen würden.

Oftmals gibt es jedoch bei Zuweisungen in bedingten Anweisungen Probleme. Da williWurm nur nach der if-Abfrage auf den Wert 2 gesetzt wird, wäre nur unter der Bedingung nene gleich 2 ein Lesezugriff auf williWurm möglich. Da diese Variable jedoch sonst vorher nicht gesetzt wurde, ergäbe sich das oben angesprochene Problem.


Galileo Computing

2.5 Ausdrücke  downtop

Beginnen wir mit mathematischen Ausdrücken, um dann die Schreibweise in Java zu finden. Eine mathematische Formel, etwa der Ausdruck -27*9, besteht aus Operanden und Operatoren. Ein Operand ist eine Variable oder ein Literal. Im Falle einer Variablen wird der Wert der Variablen ausgelesen und danach die Berechung gemacht.

Beispiel Ein Ausdruck mit Zuweisungen:
int i = 12, j;
j = i * 2;

Die Multiplikation berechnet das Produkt von 12 und 2 und speichert das Ergebnis in j ab. Von allen primitiven Variablen, die in dem Ausdruck vorkommen, wird also der Wert ausgelesen und in den Ausdruck eingesetzt.13  Dies nennt sich auch Wertoperation, da der Wert der Variablen betrachtet wird und nicht ihr Speicherort oder gar ihr Variablenname.

Die Arten von Operatoren

Operatoren verknüpfen die Operanden. Ist ein Operator auf genau einem Operand definiert, so nennt er sich unärer Operator (oder einstelliger Operator). Das Minus (negatives Vorzeichen) vor einem Operand ist ein unärer Operator, da er für genau den folgenden Operanden gilt. Die üblichen Operatoren Plus, Minus, Mal und Division sind binäre (zweistellige) Operatoren. Es gibt auch einen Fragezeichenoperator für bedingte Ausdrücke, der dreistellig ist.

Ausdrücke

Ein Ausdruck ist eine besondere Form der Anweisung, denn ein Ausdruck ergibt bei der Auswertung ein Ergebnis (auch »Resultat« genannt). Dieses ist oft ein

gp  numerischer Typ (von arithmetischen Ausdrücken) oder ein
gp  Referenz-Typ (von einer Objekt-Allokation).

Operatoren erlauben die Verbindung von einzelnen Ausdrücken zu neuen Ausdrücken. Einige Operatoren sind aus der Schule bekannt, wie Addition, Vergleich, Zuweisung und weitere. C(++)-Programmierer werden viele Freunde wiedererkennen.


Galileo Computing

2.5.1 Zuweisungsoperator und Verbundoperator  downtop

Der Zuweisungsoperator kopiert den Wert eines Ausdrucks der rechten Seite in die Variable der linken Seite. In Java wird eine Zuweisung mit dem Gleichheitszeichen = dargestellt.

int a;
a = 12*3;

Da eine Zuweisung keine Anweisung, sondern ein Ausdruck mit einem Wert ist, der einen Rückgabewert ergibt, kann die Zuweisung auch an jeder anderen Stelle eingesetzt werden, wo ein Ausdruck stehen darf. Die Zuweisung kann auch in einem Funktionsaufruf erfolgen:

System.out.println( a = 19 );

Auch Zuweisungen der Form

a = b = 0;

sind erlaubt und gleichbedeutend mit b=0 und a=b beziehungsweise a=(b=0).

Daran lässt sich ablesen, dass beim Zuweisungsoperator die Auswertung von rechts nach links erfolgt.

Beispiel Die Wahrheitsvariable hatVorzeichen soll dann true sein, wenn das Zeichen vorzeichen gleich dem Minus ist. Für Vergleiche dient der Operator ==:
boolean hatVorzeichen = (vorzeichen == '-');

Schön zu sehen an dem Beispiel ist die Auswertungsreihenfolge. Erst wird das Ergebnis des Vergleichs berechnet und dieser Wahrheitswert wird anschließend in hatVorzeichen kopiert.

Verbundoperatoren

In Java können Zuweisungen mit numerischen (und auch bitweisen, aber dazu später) Operatoren kombiniert werden. Für einen Operator # in dem Ausdruck a = a # (b) gilt die Abkürzung durch einen Verbundoperator a #= b. So addiert der Ausdruck a += 2 auf die Variable a 2 hinzu. Der Rückgabewert ist die um 2 erhöhte Variable a.

Falls es sich bei der rechten Seite um einen komplexeren Ausdruck handelt, wird dieser nur einmal ausgewertet. Dies ist wichtig bei Methodenaufrufen, die Seiteneffekte besitzen.

Beispiel Wir profitieren auch bei einem Feldzugriff von Verbundoperationen, da der Zugriff auf das Feldelement nur einmal stattfindet.
feld[2*i+j] = feld[2*i+j] + 1;

Leichter zu lesen ist die folgende Anweisung:

feld[2*i+j] += 1;

In der Langform a = a # (b) ist die Klammerung wichtig, denn bei dem Ausdruck a*=3+5 gilt a=a*(3+5) und durch die Punkt-vor-Strich Regelung nicht a=a*3+5.


Galileo Computing

2.5.2 Präfix- oder Postfix-Inkrement und -Dekrement  downtop

Erhöhen/Erniedrigen von Variablen ist eine sehr häufige Operation, wofür die Entwickler in der Vorgängersprache C auch einen Operator spendiert haben. Die praktischen Operatoren ++ und -- kürzen die Programmzeilen zum Inkrement und Dekrement ab.

i++;     // Abkürzung 
für i=i+1
j--;     //               j=j-1

Eine lokale Variable muss allerdings vorher initialisiert sein, da ein Lesezugriff vor einem Schreibzugriff stattfindet.

Vorher oder Nachher

Die beiden Operatoren sind glücklicherweise Ausdrücke, so dass sie einen Wert liefern. Es gibt jedoch einen feinen Unterscheid, wo dieser Operator platziert wird. Es gibt ihn nämlich in zwei Varianten: Vor der Variablen und hinter. Steht das Inkrement vor der Variablen, sprechen wir von Prä-Inkrement/Prä-Dekrement, steht es dahinter von Post-Inkrement/Post-Dekrement; kurz auch Präfix/Postfix. Nutzen wir einen Präfix-Operator, so wird die Variable erst erhöht bzw. erniedrigt und dann der Wert geliefert. Neben der Wertrückgabe gibt es eine Veränderung der Variablen.

Beispiel Präfix/Postfix in einer Ausgabeanweisung:
int i = 10, j = 20;
System.out.println( ++i );       // 11
System.out.println( --j );       // 19
System.out.println( i );         // 11
System.out.println( j );         // 19
Wir erkennen hier, dass der Wert erst erhöht wird, und anschießend in die Berechnung eingeht. Deutlicher ist der Unterschied beim Postfix:
System.out.println( i++ );       // 10
System.out.println( j-- );       // 20
System.out.println( i );         // 11
System.out.println( j );         // 19

Das bedeutet, der Wert wird im Ausdruck verwendet und erst anschließend wird der Wert erhöht. Wir bekommen mit dem Präfix den Ausdruck nach der Operation und mit dem Postfix den Ausdruck davor.

Den Post-Inkrement finden wir auch im Namen der Programmiersprache C++. Es soll ausdrücken, dass es C-mit-eins-drauf ist, also ein verbessertes C. Mit dem Wissen über den Postfix-Operator ist klar, dass wir erst einen Zugriff haben und dann die Erhöhung stattfindet – also C++ ist auch nur C und der Vorteil kommt später. (Einer der Entwickler von Java, Bill Joy, hat einmal Java als C++-- beschrieben. Er meinte damit C++ ohne die schwer zu pflegenden Eigenschaften.)

Einige Besonderheiten

Wir wollen uns abschließend noch mit einer Kuriosität des Postinkrements und Präinkrements beschäftigen, die nicht nachahmenswert ist:

a = 2;
a = ++a;      // a = 3

b = 2;
b = b++;      // b = 2

Im ersten Fall bekommen wir den Wert 3 und im zweiten Fall 2. Der erste Fall überrascht nicht. Denn a=++a erhöht den Wert 2 um 1 und anschließend wird 3 der Variablen a zugewiesen. Bei b ist es raffinierter. Der Wert von b ist 2 und dieser Wert wird intern vermerkt. Anschließend erhöht b++ die Variable b. Doch die Zuweisung setzt b auf den gemerkten Wert, der 2 war. Also ist b=2.


Galileo Computing

2.5.3 Unäres Minus und Plus  downtop

Die binären Operatoren sitzen zwischen zwei Operanden, während sich ein unärer Operator genau einen Operanden vornimmt. Das unäre Minus (Operator zur Vorzeichenumkehr) dreht etwa das Vorzeichen des Operanden um. So wird aus einem positiven Wert ein negativer und aus einem negativen ein positiver. Das unäre Plus ist eigentlich unnötig; dies haben die Entwickler jedoch aus Symmetriegründen mit eingeführt.

Minus und Plus sitzen direkt vor dem Operator und der Compiler weiß selbständig, ob dies unär oder binär ist. Er hat es leicht, wenn typische Ausdrücke wie

a = -2;

geschrieben werden.

Beispiel Der Compiler erkennt auch folgende Konstruktion:
int i = – - – 2 + – + 3;
Das wertet sich zu = -5 aus.


Galileo Computing

2.5.4 Arithmetische Operatoren  downtop

Ein arithmetischer Operator verknüpft die Operanden mit den Operatoren Addition (+), Subtraktion (-), Multiplikation (*), Division (/). Zusätzlich gibt es einen Modulo-Operator (auch Restwertoperator genannt), der den bei der Division verbleibenden Rest betrachtet. Das Zeichen für den Modulo-Operator ist %. Er ist für ganzzahlige Werte sowie Fließkommazahlen definiert. Die arithmetischen Operatoren sind binär und auf der linken und rechten Seite sind die Typen numerisch. Der Ergebnistyp ist ebenfalls numerisch. Bei unterschiedlichen Datentypgrößen werden vor der Anwendung der Operation alle Operanden auf den größten vorkommenden gebracht. Anschließend wird die Operation ausgeführt und der Ergebnistyp entspricht dem umfassenderen Typ.

Der Divisionsoperator

Der binäre Operator »/« bildet den Quotienten aus Dividend und Divisor. Auf der linken Seite steht der Dividend und auf der rechten Seite der Divisor. Die Division ist für Ganzzahlen und für Fließkommazahlen definiert. Bei der Ganzzahldivision wird zu Null hin gerundet. Schon in der Schulmathematik war die Division durch Null nicht definiert. Führen wir eine Ganzzahldivision mit dem Divisor 0 durch, so bestraft uns Java mit einer ArithmeticException. Bei Fließkommazahlen verläuft dies anders. Eine Division durch 0 liefert meistens bei +/- Unendlich eine NaN; außer bei 0.0/0.0. Ein NaN steht für Not-A-Number und wird vom Prozessor erzeugt, falls er eine mathematische Operation wie die Division durch Null nicht durchführen kann.

Der Modulo-Operator %

Bei einer Ganzzahldivision kann es passieren, dass wir einen Rest bekommen. So geht die Division 9/2 nicht auf. Der Rest ist 1. In Java sowie in C(++) ist es der Modulo-Operator (engl. Remainder Operator), der uns diese Zahl liefert. Somit ist 9%2 gleich 1. Im Gegensatz zu C(++)14  erlaubt der Modulo-Operator in Java auch Fließkommazahlen und die Operanden können negativ sein. Die Sprachdefinition von C(++) schreibt bei der Division und beim Modulo mit negativen Zahlen keine Berechnungsmethode vor. In Java richtet sich die Division und der Modulo nach einer einfachen Formel: int(a/b)*b + (a%b) = a15 

Hinweis In Java sind Modulo (%), Inkrement (++) und Dekrement (--) für alle numerischen Datentypen erlaubt.16 

Beispiel Die Gleichung ist erfüllt, wenn wir etwa a = 10 und b = 3 wählen. Es gilt: int(10/3) = 3. 10%3 ergibt 1. Dann ergeben 3*3+1 = 10.

Aus dieser Gleichung folgt, dass beim Modulo das Ergebnis nur dann negativ ist, falls der Dividend negativ ist; er ist nur dann positiv, wenn der Dividend positiv ist. Es ist leicht einzusehen, dass das Ergebnis der Modulo-Operation immer echt kleiner ist als der Wert des Divisors. Wir haben den gleichen Fall wie bei der Ganzzahldivision, dass ein Divisor mit dem Wert 0 eine ArithmeticException auslöst.

Beispiel Unterschiedliche Vorzeichen beim Modulo-Operator: Listing 2.5   ModuloDivDemo.java
class ModuloDivDemo
{
  public static void main( String args[] )
  {

  System.out.println( "5%3 = " + (5%3) ); // 2 
  System.out.println( "5/3 = " + (5/3) ); // 1 

  System.out.println( "5%-3 = " + (5%-3) ); // 2 
  System.out.println( "5/-3 = " + (5/-3) ); // -1

  System.out.println( "-5%3 = " + (-5%3) ); // -2
  System.out.println( "-5/3 = " + (-5/3) ); // -1

  System.out.println( "-5%-3 = " + (-5%-3) ); // -2
  System.out.println( "-5/-3 = " + (-5/-3) ); // 1

  } 
}

Modulo für Fließkommazahlen

Über die oben genannte Formel können wir auch bei Fließkommazahlen das Ergebnis einer Modulo-Operation leicht berechnen. Dabei muss beachtet werden, dass sich der Operator nicht so wie unter IEEE 754 verhält. Denn diese Norm schreibt vor, dass die Modulo-Operation den Rest von einer rundenden Division berechnet und nicht von einer abschneidenden Division. So wäre das Verhalten nicht analog zum Modulo bei Ganzzahlen. Java definiert das Modulo jedoch bei Fließkommazahlen genauso wie das Modulo auf Ganzzahlen. Wünschen wir ein Modulo-Verhalten wie es IEEE 754 vorschreibt, so können wir immer noch die Bibliotheksfunktion Math.IEEEremainder() verwenden.

Auch bei der Modulo-Operation bei Fließkommazahlen werden wir niemals eine Exception erwarten. Eventuelle Fehler werden, wie im IEEE-Standard beschrieben, mit NaN angegeben. Ein Überlauf oder Unterlauf kann zwar passieren, aber nicht geprüft werden.

Beispiel Modulo bei Fließkommazahlen:
5.0%3.0 = 2.0
5.0%-3.0 = 2.0
-5.0%3.0 = -2.0
-5.0%-3.0 = -2.0

Anwendung des Modulo-Operators

Im Folgenden wollen wir eine Anwendung vom Modulo-Operator kennenlernen, denn dieser wird häufig dafür verwendet, eine einfache Überprüfung vorzunehmen. Bei einer eingegebenen Nummer wird dann einfach der Modulowert zu einer fest vorgegeben Zahl berechnet und ist dieser zum Beispiel 0, so ist die Nummer eine gültige Codenummer. Erstaunlicherweise gibt es Firmen, die tatsächlich nach diesem einfachen Codierungsverfahren ihre Software schützen, vorne dabei ist Microsoft mit Windows 95 und Windows NT 4.0. Nachdem die Software installiert ist, wird der Benutzer aufgefordert, einen CD-Key einzugeben. Es werden von den älteren Softwarepaketen drei unterschiedliche Schlüssellängen verwendet.

gp  Normale Version mit 10 Stellen: xxx-NNNNNNN
Die ersten drei Stellen werden nicht geprüft. Die sieben letzten Ziffern müssen eine Quersumme ergeben, die durch 7 teilbar ist, also für die x % 7 = 0 gilt. So ist 1234567 eine gültige Zahl, da 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28 und 28 % 7 = 0 ist.
gp  OEM Version: xxxxx-OEM-NNNNNNN-xxxxx
Die ersten acht und die letzten fünf Stellen werden nicht geprüft. Die in der Mitte liegenden restlichen sieben Stellen werden nach dem gleichen Verfahren wie in der normalen Version geprüft.
gp  Neuer CD-Schlüssel: xxxx-NNNNNNN
Die ersten vier Ziffern steuern, ob es sich um eine Vollversion (0401) oder eine Update-Version (0502) handelt. Die restlichen sieben Ziffern sind wieder Modulo 7 zu nehmen.

Das Verfahren ist mehr als einfach. Die Hintergründe sollen natürlich nicht zum Installieren illegal erworbener Software führen. Dies verstößt selbstverständlich gegen das Urheberrecht! Diese Variante darf höchstens dann angewendet werden, wenn die Product-ID für die lizenzierte Software verloren ging!

Produktkeys generieren

Wenn wir eine große Applikation schreiben und wir diese verkaufen wollen, dann fragen viele Hersteller vor oder nach der Installation nach einem so genannten Installations-Schlüssel, auch genannt Installations-ID, Produkt-Schlüssel oder Produkt-ID. Diese Nummer muss dann vom Benutzer eingegeben werden, damit er das Programm nutzen kann, bzw. erst installieren kann. Natürlich wird nicht zu jeder Kopie der Programme eine neue Produkt-ID generiert. Das würde ja bedeuten, dass auf jeder einzelnen CD eine unterschiedliche Nummer eingebrannt wäre. Vielmehr schreibt der Softwarehersteller eine Entschlüsselungs-Software, die die Schlüssel nach einem geheimen Verfahren umrechnet. So etwa bei den älteren Windows Programmen von MS. Hier musste die Zahl einfach nur durch 7 teilbar sein. Doch mittlerweile sind diese Produkt-Schlüssel komplizierter geworden und nicht mehr so leicht nachzuvollziehen, etwa die umständlich einzugebende Nummer UVTXG-Y9WVT-BWAKB-FTAP3-43FPQ. (Produkt-Key einer Vor-98-Version, leicht verändert.) Für eigene Projekte müssen wir uns keine umständlichen Nummern generieren lassen. Denn dies ist etwas kniffliger als sich einen schönen Algorithmus auszudenken. Doch auch hier helfen uns Internet-Standards weiter. So etwa der Standard für Globally Unique IDentifiers. Die Generierung dieser GUIDs ist unter http://www.kblabs.com/lab/lib/drafts/draft-leach-uuids-guids-00.txt.html genau beschrieben.

Rundungsfehler

Prinzipiell sollten Anweisungen wie 1.1 – 0.1 immer 1.0 ergeben, doch interne Rundungsfehler bei der Darstellung treten auf und lassen das Ergebnis von Berechnung zu Berechnung immer ungenauer werden. Ein besonders ungünstiger Fehler trat 1994 beim Pentium Prozessor im Divisions-Algorithmus Radix-4 SRT auf, ohne dass der Programmierer der Schuldige war.

double x, y, z;

x = 4195835.0;
y = 3145727.0;
z = x – (x/y) * y;

System.out.println( z );

Ein fehlerhafter Prozessor liefert hier 256, obwohl laut Rechenregel das Ergebnis 0 sein muss. Laut Intel sollte für einen normalen Benutzer (Spieler? Softwareentwickler? Surfer?) der Fehler nur alle 27.000 Jahre auftauchen. Glück für die meisten. Eine Studie von IBM errechnet eine Fehlerhäufigkeit von einmal in 24 Tagen. Alles in allem hat Intel die CPUs zurückgenommen, über 400 Millionen Dollar verloren und spät den Kopf gerade noch aus der Schlinge gezogen.

Die meisten Rundungsfehler resultieren aber daher, dass endliche Dezimalbrüche im Rechner als Näherungswerte für periodische Binärbrüche repräsentiert werden müssen. 0,1 entspricht einer periodischen Mantisse im IEEE-Format.


Galileo Computing

2.5.5 Die relationalen Operatoren  downtop

Ein anderes Wort für relationale Operatoren ist Vergleichsoperatoren. Sie vergleichen Ausdrücke miteinander und geben einen Wahrheitswert vom Typ boolean zurück. Die von Java zur Verfügung gestellten Operatoren sind Größer (>) und Kleiner (<), Gleichheits- (==) und Ungleichheitstest (!=) und die Verbindung zu einem Größer gleich (>=) beziehungsweise Kleiner gleich (<=). Ebenso wie arithmetische Operatoren passen die relationalen Operatoren ihre Operanden an einen gemeinsamen Typ an. Handelt es sich bei den Typen um Referenztypen, so sind nur die Vergleichsoperatoren == und != erlaubt.

Verwechselungsprobleme durch == und =

Die Verwendung des relationalen Operators == und der Zuweisung = führt bei Einsteigern oft zu Problemen, da die Mathematik für beide immer nur ein Gleichheitszeichen kennt. Glücklicherweise ist das Problem in Java nicht so drastisch wie beispielsweise in C(++), da die Typen der Operatoren unterschiedlich sind. Der Vergleichsoperator ergibt immer nur den Rückgabewert boolean. Zuweisungen von numerischen Typen ergeben jedoch wieder einen numerischen Typ. Es kann also kein Problem wie das Folgende geben:

int a = 10, b = 11;
boolean result1 = ( a = b );        // 
Compilerfehler
boolean result2 = ( a == b );       // 
Das ist OK

Galileo Computing

2.5.6 Logische Operatoren  downtop

Mit logischen Operatoren werden Wahrheitswerte nach definierten Mustern verknüpft. Logische Operatoren operieren nur auf boolean Typen, andere Typen führen zu Compiler-Fehlern. Java bietet die Operatoren Und (&&), Oder (||), Xor (^) und Nicht (!) an. Xor ist eine Operation, die genau dann falsch zurückgibt, wenn entweder beide Operatoren wahr oder beide falsch sind. Sind sie unterschiedlich, so ist das Ergebnis wahr.

Kurzschlussoperatoren

Eine Besonderheit sind die Kurzschlussoperatoren (engl. Short-Circuit-Operator) && bzw || für Und und Oder. In der Regel wird ein logischer Ausdruck nur dann weiter ausgewertet, wenn er das Schlussergebnis noch beeinflussen kann. Sonst optimiert der Compiler die Programme zum Beispiel bei zwei Operanden:

gp  Und: Ist einer der beiden Ausdrücke falsch, so kann der Ausdruck schon nicht mehr wahr werden. Das Ergebnis ist falsch.
gp  Oder: Ist mindestens einer der Ausdrücke schon wahr, so ist auch der gesamte Ausdruck wahr.

Es ist aber in einigen Fällen gewünscht, dass alle Teilausdrücke ausgewertet werden, insbesondere wenn Funktionen Seiteneffekte bewirken. Daher führt Java zusätzliche nicht-Kurzschlussoperatoren | und & ein, die in einem komplexen Ausdruck alle Teilausdrücke auswerten. Für das ausschließende Oder Xor (Operator ^) kann es keinen Kurzschluss-operator geben, da immer beide Operanden ausgewertet werden müssen, bevor das Ergebnis feststeht.

Beispiel In dem ersten Ausdruck wird die Methode foo()nicht aufgerufen, im zweiten schon.
b = true || foo(); 
  // foo() wird nicht aufgerufen.
b = false & foo();   // foo() wird 
aufgerufen.
 


Galileo Computing

2.5.7 Reihenfolge und Rang der Operatoren in der Auswertungsreihenfolge  downtop

Aus der Schule ist der Spruch »Punktrechnung geht vor Strichrechnung« bekannt, sodass Ausdrücke der Art

1 + 2 * 3

zu 7 und nicht zu 9 ausgewertet werden. In den meisten Programmiersprachen gibt es eine Unzahl von Operatoren neben Plus und Mal, die alle ihre eigene Vorrangsregeln besitzen. Der Multiplikationsoperator besitzt zum Beispiel eine höhere Vorrangsregel (kurz Rang) und damit eine andere Auswertungsreihenfolge als der Plus-Operator.16 

Beispiel Zur Umwandlung einer Temperatur von Fahrenheit in Celsius wird von dem Wert in Fahrenheit 32 abgezogen und das Ergebnis mit 5/9 multipliziert:
celsius = fahrenheit – 32 * 5 / 9;

Die erste Idee ist aber leider falsch, denn hier berechnet der Compiler 32*5/9. Das Ergebnis 17 wird von Fahrenheit abgezogen, was keine gültige Umrechnung ist. Richtig ist folgendes:

celsius = ( fahrenheit – 32 ) * 5 / 9;

Die Rechenregeln für Mal vor Plus kann sich jeder noch leicht merken. Komplizierter ist die Auswertung bei den zahlreichen Operatoren, die seltener im Programm vorkommen.

Beispiel Wie ist die Auswertung bei dem nächsten Ausdruck?
boolean A = false,
        B = false,
        C = true;
System.out.println( A && B || C 
);

Gilt, dass entweder A && B oder C wahr sein müssen, oder etwa A und B || C? Das Ergebnis fällt unterschiedlich aus. Entweder ist es true oder false.

Für dieserlei Feinheiten gibt es zwei Lösungen. Entweder in einer Tabelle mit Vorrangsregeln nachschlagen oder auf diese Ungenauigkeiten verzichten.

Tabelle 2.7   Operatoren mit Rangordnung in Java
Operator Rang Typ Beschreibung
++, -- 1 arithmetisch Inkrement u. Dekrement
+, - 1 arithmetisch Unäres Plus u. Minus
~ 1 integral Bitweises Komplement
! 1 boolean Logisches Komplement
(type) 1 jedes Cast
*, /, % 2 arithmetisch Multiplikation, Division, Rest
+, - 3 arithmetisch Addition und Subtraktion

+

3

String Stringkonkatenation

<<

4

integral Shift links

>>

4

integral Shift rechts m. Vorzeichenerweiterung

>>>

4

integral Shift rechts o. Vorzeichenerweiterung

<, <=, >, >=

5

arithmetisch Numerische Vergleiche

instanceof

5

Objekt Typvergleich

==, !=

6

Primitiv Gleich-/Ungleichheit von Werten

==, !=

6

Objekt Gleich-/Ungleichheit von Referenzen

&

7

Integral Bitweises Und

&

7

boolean Logisches Und, Kurzschluss

^

8

Integral Bitweises Xor

^

8

boolean Logisches Xor

|

9

Integral Bitweises Oder

|

9

boolean Logisches Oder, Kurzschluss

&&

10

boolean Logisches konditionales Und

||

11

boolean Logisches konditionales Oder

?:

12

alles Bedingungsoperator

=

13

Jede Zuweisung

*=, /=, %=, +=, -=, <<=, >>=, >>>=, &=, ^=, |=

14

Jede Zuweisung mit Operation

Die Tabelle lehrt uns, dass im Beispiel A && B || C das Und stärker als das Oder bindet, also der Wert mit der Belegung A=false, B=false, C=true zu true ausgewertet wird. Vermutlich gibt es Programmierer, die dies wissen, oder eine Tabelle mit Rangordnungen immer am Monitor kleben haben. Aber beim Durchlesen von fremden Code ist es nicht schön, immer wieder die Tabelle konsultieren zu müssen, die verrät, ob nun das binäre Xor oder das binäre Und stärker binden.

Tipp Alle Ausrücke, die über die einfache Regel »Punktrechung geht vor Strichrechnung« hinausgehen, sollten geklammert werden. Da die unären Operatoren ebenfalls sehr stark binden, kann eine Klammerung wegfallen.

Beispiel Bei den Operatoren +, * gilt die mathematische Kommutativität und Assoziativität. Das heißt, die Operanden können prinzipiell umgestellt werden und das Ergebnis sollte davon nicht beeinträchtigt sein. Bei der Division gilt das nicht.
A / B / C

Der Ausdruck wird von links nach rechts ausgewertet, und zwar als (A/B)/C. Hier sind Klammern angemessen. Denn würde der Compiler den Ausdruck zu A/(B/C) auswerten, käme es einem A*C/B gleich.

Die mathematische Assoziativität gilt bei Gleitkommazahlen natürlich nicht, da diese nicht ohne Rechenfehler ablaufen. Daher gilt eine Auswertung von links nach rechts.


Galileo Computing

2.5.8 Was C(++)-Programmierer vermissen könnten  downtop

Da es in Java keine Pointeroperationen gibt, existiert das Operatorzeichen zur Referenzierung (&) und Dereferenzierung (*) nicht. Ebenso ist ein sizeof unnötig, da das Laufzeitsystem und der Compiler immer die Größe von Klassen kennt bzw. die primitiven Datentypen immer eine feste Länge haben. Der Kommaoperator ist in Java nur im Kopf von for–Schleifen erlaubt.


Galileo Computing

2.6 Bedingte Anweisungen oder Fallunterscheidungen  downtop

Verzweigungen dienen in einer Programmiersprache dazu, Programmteile unter bestimmten Bedingungen auszuführen. Java bietet eine if- und if/else-Anweisung sowie die switch-Anweisung zum Ausführen verschiedener Programmteile. Obwohl ein Sprung mit goto nicht möglich ist, besitzt Java eine spezielle Sprungvariante mit continue und break mit definierten Sprungzielen.


Galileo Computing

2.6.1 Die if-Anweisung  downtop

Die if-Anweisung besteht aus dem Schlüsselwort if, dem zwingend ein Ausdruck mit dem Typ boolean in Klammern folgt. Es folgt eine Anweisung, die oft eine Blockanweisung ist.

if ( ausdruck )
  anweisung

Die Abarbeitung der Anweisung hängt nun vom Ausdruck ab. Ist das Ergebnis des Ausdrucks wahr, (true), so wird die Anweisung ausgeführt. Ist das Ergebnis des Ausdrucks falsch (false), so wird mit der ersten Anweisung nach der if-Anweisung fortgefahren.

Beispiel Ein Relationenvergleich.
if ( x < y )
  System.out.println( "x ist kleiner als y" );

Im Gegensatz zu C(++) muss der Testausdruck in der if-Anweisung (übrigens auch in den folgenden Schleifen) vom Typ boolean sein. In C(++) wird ein numerischer Ausdruck als wahr bewertet, wenn das Ergebnis des Ausdruckes ungleich 0 ist.

Betrachten wir in einer if-Anweisung den Vergleich, ob ein Objekt existiert. Dann ist dies mit null zu vergleichen.17  Die Referenz auf das Objekt steht in der Variablen ref.

if ( ref != null )
  ...

if-Anfragen und Blöcke

Hinter dem if erwartet der Compiler eine Anweisung. Wenn wir jedoch mehrere Anweisungen in Abhängigkeit der Bedingung ausführen wollen, so müssen wir einen Block verwenden, denn andernfalls ordnet der Compiler nur die nächst folgende Anweisung der Fallunterscheidung zu, auch wenn mehrere Anweisungen optisch abgesetzt sind.18  Dies ist eine große Gefahr für Programmierer, die optisch Zusammenhänge schaffen wollen, die in Wirklichkeit nicht existieren.

Beispiel Eine if-Anweisung soll testen, ob die Variable y den Wert 0 hat. In dem Fall soll sie die Variable x auf 0 setzen und zusätzlich auf dem Bildschirm »Null« bringen. Zunächst die semantisch falsche Variante:
if ( y == 0 )
  x = 0;
  System.out.println( "Null" );

Sie ist semantisch falsch, da unabhängig von y immer eine Ausgabe erscheint. Denn der Compiler interpretiert die Zeilen in folgendem Zusammenhang:

if ( y == 0 )
  x = 0;

System.out.println( "Null" );

Damit das Programm korrekt wird, müssen wir einen Block verwenden und die Anweisungen zusammensetzen.

Beispiel Ein korrekt geklammerter Ausdruck:
if ( y == 0 ) {
x = 0;
System.out.println( "Null" );
}

Zusammengesetzte Bedingungen

Unsere bisherigen Abfragen waren sehr einfach, jedoch kommen in der Praxis viel komplexere Bedingungen vor. Dafür werden häufig die logischen Operatoren &&, || beziehungsweise ! verwendet. Wenn wir etwa testen wollen, ob eine Zahl x entweder gleich 7 oder größer gleich 10 ist, schreiben wir die zusammengesetzte Bedingung

if ( x == 7 || x >= 10 )
  ...

Sind die logisch verknüpften Ausdrücke komplexer, so sollten zur Unterstützung der Lesbarkeit die einzelnen Bedingungen in Klammern gesetzt werden, da nicht jeder sofort die Tabelle mit den Vorrangsregeln für die Operatoren im Kopf hat.


Galileo Computing

2.6.2 Die Alternative wählen mit einer if/else-Anweisung  downtop

Neben der einseitigen Alternative existiert die zweiseitige Alternative. Das optionale Schlüsselwort else veranlasst die Ausführung der alternativen Anweisung, wenn der Test falsch ist:

if ( ausdruck )
  anweisung1
else
anweisung2

Falls der Ausdruck wahr ist, wird die Anweisung 1 ausgeführt, andernfalls Anweisung 2. Somit ist sichergestellt, dass in jedem Fall eine Anweisung ausgeführt wird.

if ( x < y )
  System.out.println( "x ist echt kleiner als y." );
else
  System.out.println( "x ist größer oder gleich y." );

Dangling-Else Problem

Bei Verzweigungen mit else gibt es ein bekanntes Problem, welches Dangling-Else Problem genannt wird. Zu welcher Anweisung gehört das folgende else?

if ( a )
  if ( b )
    a1;
else
  a2;

Die Einrückung suggeriert, dass das else zur ersten Anweisung gehört. Dies ist aber nicht richtig. Die Semantik von Java (und auch der von fast allen anderen Programmiersprachen) ist so definiert, dass das else zum innersten if gehört. Daher lässt sich nur der Programmiertipp geben, die if-Anweisungen zu klammern:

if ( a )
{
  if ( b )
  {
    a1;
  }
}
else
{
  a2;
}

So kann eine Verwechslung gar nicht erst auftreten.

Beispiel Wenn das else immer zum innersten if gehört, und das ist nicht erwünscht, können wir einmal, wie gerade gezeigt, mit geschweiften Klammern arbeiten, oder auch eine leere Anweisung im else-Zweig zufügen:
if ( x >= 0 )
  if ( x != 0 )
    System.out.println( "x echt größer Null" );
  else
    ; // x ist gleich Null
else
  System.out.println( "x echt kleiner Null" );

Mehrfachverzweigung oder auch geschachtelte Alternativen

if-Anweisungen zur Progammführung kommen sehr häufig in Programmen vor und noch häufiger ist es, eine Variable auf einen bestimmten Wert zu prüfen. Dazu werden if- und if/else-Anweisung gerne geschachtelt. Wenn eine Variable einem Wert entspricht, dann wird eine Anweisung ausgeführt, sonst wird die Variable mit einem anderen Wert getestet usw.19 

Beispiel Eine Ganzzahl-Funktion sign(), die abhängig vom Vorzeichen einer Zahl entweder +1 für positive Zahlen, -1 für negative und 0 für Null zurückgibt. Der erste Ansatz:20 
if ( x > 0 )
  signum = 1;

if ( x < 0 )
  signum = -1;

if ( x == 0 )
  signum = 0;
  if ( x < 0 )
    signum = -1;
  else
signum = 0;

Dieser Ansatz ist ein sehr umständlicher Ansatz und kostet zudem noch Rechenzeit, da in jedem Fall drei Bedingungen geprüft werden. Wenn also x größer Null ist, werden dennoch zwei Vergleiche gemacht. Wir schachteln daher in einer kleinen Programmverbesserung die Alternativen und arbeiten dann mit einer Abfolge von sequentiell abhängigen Alternativen:

if ( x > 0 )
  signum = 1;
else
  if ( x < 0 )
    signum = -1;
  else
    signum = 0;

Jetzt werden nur noch so viele Bedingungen geprüft, wie zur Entscheidung notwendig sind.

Die eingerückten Verzweigungen nennen sich auch angehäufte if-Anweisungen oder if-Kaskade, da jede else-Anweisung ihrerseits weitere if-Anweisungen enthält bis alle Abfragen gemacht sind.

Beispiel Kaskadierte if-Anweisungen:
if ( monat == 4 )
  tage = 30;
else
  if ( monat == 6 )
    tage = 30;
  else
    if ( monat == 9 )
      tage = 30;
    else
      if ( monat == 11 )
        tage = 30;
      else
        if ( monat == 2 )
          if ( schaltjahr )
            tage = 29;
          else
            tage = 28;
        else

tage = 31;



Galileo Computing

2.6.3 Die switch-Anweisung bietet die Alternative  downtop

In Java20  gibt es eine Kurzform für speziell gebaute, angehäufte if-Anweisungen – die Anweisung mit switch und case:

switch ( Ausdruck )
{
case Konstante: Anweisung; }

Die switch-Anweisung ist eine einfache Form der Mehrfachverzweigung. Sie vergleicht nacheinander den Ausdruck hinter dem switch (ein primitiver Typ wie byte, char, short oder int) mit jedem einzelnen Fallwert. Alle Fallwerte müssen unterschiedlich sein. Stimmt der Ausdruck mit der Konstanten überein, so wird die Anweisung bzw. Anweisungen hinter der Sprungmarke ausgeführt.

Hinweis Eine Einschränkung der switch-Anweisung besteht darin, dass die Tests und die Konstanten nur auf den primitiven Datentyp int beschränkt sind.

Es können also keine größeren Typen wie long oder Fließkommazahlen wie float bzw. double oder gar Objekte benutzt werden. Als Alternative bleiben nur angehäufte if-Anweisungen. Dies ist auch der einzige Weg, um Bereiche abzudecken.

Alles andere abdecken mit default

Gibt es keine Übereinstimmung mit einer Konstanten, so lässt sich optional die Sprungmarke default einsetzen:

switch ( ausdruck )
{
  case Konstante:
    Anweisung;
  ...
  default:
}

Ohne Übereinstimmung mit einem konkreten Ziel, geht die Abarbeitung des Programmcodes hinter default weiter. default kann auch zwischen den Konstanten eingesetzt werden.

Beispiel Ein Taschenrechner mit Alternative
switch ( op )
{
  case '+':   // addiere
    break;

  case '-':   // subtrahiere
    break;

  case '*':   // multipliziere
    break;

  case '/':   // dividiere
    break;

  default:
    System.err.println( "Operand nicht definiert!" );
}

switch hat Durchfall

Bisher haben wir in die letzte Zeile eine break-Anweisung gesetzt. Ohne ein break würden nach einer Übereinstimmung alle nachfolgenden Anweisungen ausgeführt. Sie laufen somit in einen neuen Abschnitt herein bis ein break oder das Ende von switch erreicht ist. Da dies vergleichbar mit einem Spielzeug ist, bei dem Kugeln von oben nach unten durchfallen, nennt sich dieses auch Fall-Through. Es ist ein häufig gemachter Programmierfehler, das break zu vergessen und daher sollte ein beabsichtigter Fall-Through immer als Kommentar angegeben werden.

Beispiel Über dieses Durchfallen ist es möglich, bei unterschiedlichen Werten immer die gleiche Anweisung ausführen zu lassen.
switch ( buchstabe )
{
  case 'a':                    // Durchfallen
  case 'e':
  case 'i': case 'o': case 'u':
    vokal = true;
    break;

  default:
    vokal = false;
}

In dem Beispiel bestimmt eine case-Anweisung, ob die Variable buchstabe ein Vokal ist. Fünf case-Anweisungen decken jeweils einen Buchstaben ab. Stimmt die Variable mit einer Konstanten überein, so »fällt« der Interpreter in den Programmcode der Zuweisung. Dieses Durchfallen über die case-Zweige ist praktisch, so wie es unser Programmcode für das Ist-Vokal-Problem zeigt. Der erste case-Zweig setzt die boolesche Variable vokal bei einem Vokal auf wahr. Tritt die Bedingung nicht ein, so weist die Anweisung im default-Teil der Variablen vokal den Wert falsch zu.

Hinweis Obwohl ein fehlendes break zu lästigen Programmierfehlern führt, haben die Java-Entwickler dieses Verhalten vom syntaktischen Vorgänger C übernommen. Eine interessante Lösung wäre gewesen, das Verhalten genau umzudrehen und das Durchfallen explizit einzufordern, zum Beispiel mit einem Schlüsselwort.


Galileo Computing

2.7 Schleifen  downtop

Schleifen dienen dazu, bestimmte Anweisungen immer wieder abzuarbeiten. Zu einer Schleife gehören die Schleifenbedingung und der Rumpf. Die Schleifenbedingung entscheidet darüber, unter welcher Bedingung die Wiederholung ausgeführt wird. Sie muss ein boolescher Ausdruck sein. In Abhängigkeit von der Schleifenbedingung kann der Rumpf mehrmals ausgeführt werden. Dazu wird bei jedem Schleifendurchgang die Schleifenbedingung geprüft. Das Ergebnis entscheidet, ob der Rumpf ein weiteres Mal durchlaufen wird (true) oder die Schleife beendet wird (false).


Galileo Computing

2.7.1 Die while-Schleife  downtop

Die while-Schleife ist eine abweisende Schleife, da sie vor jedem Schleifeneintritt die Schleifenbedingung prüft:

while ( Ausdruck )
Anweisung

Vor jedem Schleifendurchgang wird der Ausdruck neu ausgewertet und ist das Ergebnis true, so wird der Rumpf ausgeführt. Die Schleife ist beendet, wenn das Ergebnis false ist. Ist die Bedingung schon vor dem ersten Eintritt in den Rumpf nicht wahr, so wird der Rumpf erst gar nicht durchlaufen. Der Typ der Bedingung muss boolean sein.

Wird innerhalb des Schleifenkopfes schon alles Interessante erledigt, so muss trotzdem eine Anweisung folgen. Dies ist der passende Einsatz für die leere Anweisung. Etwa

while ( leseWeiterBisZumEnde() )
  ;                              // Rumpf ist leer

Die Methode leseWeiterBisZumEnde() gibt true zurück, falls noch Zeichen gelesen werden können. Wenn der Rückgabewert false ist, so wird die Schleife beendet.

Da der Typ wiederum boolean sein muss, sehen die Anweisungen in Java im Gegensatz zu C(++) etwas präziser aus:

while ( i != 0 ) {               // und nicht while 
( i ) wie in C(++)
  ...
}

Endlosschleifen

Ist die Bedingung einer while-Schleife immer wahr, dann handelt es sich um eine Endlosschleife:

while ( true )
{
  // immer wieder und immer wieder
}

Aus dieser Endlosschleife lässt sich mittels break entkommen. (Aber auch eine Exception oder System.exit() würden die Methode beenden.)


Galileo Computing

2.7.2 Schleifenbedingungen und Vergleiche mit ==  downtop

Eine Schleifenabbruchbedingung kann ganz unterschiedlich aussehen. Beim Zählen ist es häufig der Vergleich auf einen Endwert. Oft steckt an dieser Stelle ein absoluter Vergleich mit ==, der aus zwei Gründen problematisch werden kann.

Lange, lange durchhalten

Beispiel Schauen wir uns das erste Problem an einigen Programmzeilen an.
int i = Wert;

while ( i != 9 )
  i++;

Ist der Wert der Variablen i kleiner als 9, so haben wir beim Zählen kein Problem, denn dann ist anschließend spätestens bei 9 Schluss. Ist der Wert allerdings größer als 9, so ist die Bedingung ebenso wahr und der Schleifenrumpf wird ziemlich lange durchlaufen. Genaugenommen soweit, bis wir durch einen Überlauf wieder bei 0 beginnen und dann auch bei 9 landen. Die Absicht ist sicherlich eine andere gewesen. Die Schleife sollte nur solange zählen wie i kleiner 9 ist und sonst nicht. Daher passt Folgendes besser:

int i = Wert;

while ( i < 9 )
  i++;

Jetzt rennt der Interpreter bei Zahlen größer 9 nicht endlos weiter, sondern stoppt die Schleife sofort.

Rechenungenauigkeiten

Das zweite Problem ergibt sich bei Gleitkommazahlen. Es ist sehr problematisch, echte Vergleiche zu fordern:

double d = 0.0;

 while ( d != 1.0 )
{
  d += 0.1;
  System.out.println( d );
}

Lassen wir das Programmsegment laufen, so sehen wir, dass die Schleife hurtig über das Ziel hinausschießt:

0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
1.3

... bis das Auge müde wird ...

Bei Fließkommawerten bietet sich daher immer an, mit den relationalen Operatoren < oder > zu arbeiten.

Eine zweite Möglichkeit neben dem echten Kleiner/Größer-Vergleich ist, eine erlaubte Abweichung zu definieren. Mathematiker bezeichnen die Abweichung von zwei Werten mit dem griechischen Kleinbuchstaben Epsilon. Wenn wir einen Vergleich von zwei Fließkommazahlen anstreben und bei einem Gleichheitsvergleich die Abweichung mit betrachteten wollen, so schreiben wir einfach:

if ( Math.abs(a – b) <= epsilon )
  ...

Epsilon ist die erlaubte Abweichung. Math.abs(x) berechnet von einer Zahl x den Absolutwert.


Galileo Computing

2.7.3 Die do/while-Schleife  downtop

Dieser Schleifentyp ist eine annehmende Schleife, da die Schleifenbedingung erst nach jedem Schleifendurchgang geprüft wird. Bevor es zum ersten Test kommt, ist der Rumpf also schon einmal durchlaufen:

do
  Anweisung
while ( Ausdruck ) ;

Es ist wichtig, auf das Semikolon hinter der while-Anweisung zu achten. Liefert die Bedingung ein true, so wird der Rumpf erneut ausgeführt. Andernfalls wird die Schleife beendet und das Programm wird mit der nächsten Anweisung nach der Schleife fortgesetzt.

Beispiel Eine Zählschleife
int pos = 1;

do
{
  System.out.println( pos );
  pos++;
} while ( pos < 10 );

Äquivalenz einer while- und einer do/while-Schleife

Die do-Schleife wird seltener gebraucht als die while-Schleife. Dennoch lassen sich beide ineinander überführen. Zunächst der erste Fall. Wir ersetzen eine while-Schleife durch eine do/while-Schleife:

while ( Expression )
  Statement

Führen wir uns noch einmal vor Augen, was hier passiert. In Abhängigkeit vom Ausdruck wird der Rumpf ausgeführt. Da zunächst ein Test kommt, wäre die do/while-Schleife schon eine Blockausführung weiter. So fragen wir in einem ersten Schritt mit einer if-Anweisung ab, ob die Bedingung wahr ist oder nicht. Wenn ja, dann lassen wir den Programmcode in einer do/while-Schleife abarbeiten. Die äquivalente do/while-Schleife sieht also wie folgt aus:

if ( Ausdruck )
  do
    Anweisung
  while ( Ausdruck ) ;

Nun der zweite Fall. Wir ersetzen die do/while-Schleife durch eine while-Schleife:

do
  Anweisung
while ( Ausdruck ) ;

Da zunächst die Anweisungen ausgeführt werden und anschließend der Test, schreiben wir für die while-Variante die Ausdrücke einfach vor den Test. So ist sichergestellt, dass diese zumindest einmal abgearbeitet wurden:

Statement
while ( Ausdruck )
  Statement

Galileo Computing

2.7.4 Die for-Schleife  downtop

Die for-Schleife ist eine spezielle Variante einer while-Schleife und wird typischerweise zum Zählen benutzt. Genauso wie while-Schleifen sind for-Schleifen abweisend, der Rumpf wird also erst dann ausgeführt, wenn die Bedingung wahr ist.

Beispiel Gebe die Zahlen von 1 bis 10 auf dem Bildschirm aus:
for ( int i = 1; 
i <= 10; i++ )
System.out.println( i );

Eine genauere Betrachtung der Schleife zeigt die unterschiedlichen Segmente:

gp  Initialisierung der Schleife
Der erste Teil der for-Schleife ist ein Ausdruck, wie i=1, der vor der Durchführung der Schleife genau einmal ausgeführt wird. Dann wird das Ergebnis verworfen. Tritt in der Auswertung ein Fehler auf, so wird die Abarbeitung unterbrochen und die Schleife kann nicht vollständig ausgeführt werden. Der erste Teil kann lokale Variablen definieren und initialisieren. Diese Zählvariable ist dann außerhalb des Blockes nicht mehr gültig.21 
gp  Schleifentest/Schleifenbedingung
Der mittlere Teil, wie i<=10, wird vor dem Durchlaufen des Schleifenrumpfes – also vor jedem Schleifeneintritt – getestet. Ergibt der Ausdruck false, wird die Schleife nicht durchlaufen und beendet. Das Ergebnis muss, wie bei einer while-Schleife, vom Typ boolean sein.
gp  Schleifen-Inkrement durch einen Fortschaltausdruck
Der letzte Teil, wie i++, wird immer am Ende jedes Schleifendurchlaufes, aber noch vor dem nächsten Schleifeneintritt ausgeführt. Das Ergebnis wird nicht weiter verwendet. Ergibt die Bedingung des Tests true, dann befindet sich beim nächsten Betreten des Rumpfs der veränderte Wert im Rumpf.

Betrachten wir das Beispiel, so ist die Auswertungsreihenfolge folgender Art:

1. Initialisiere i mit 1.

Eine Endlosschleife

Da alle drei Ausdrücke im Kopf der Schleife optional sind, können sie weggelassen werden und es ergibt sich eine Endlosschleife. Die trennenden Semikolons dürfen nicht verschwinden:

for ( ; ; )
  ;

Falls demnach keine Schleifenbedingung angegeben ist, ist der Ausdruck immer wahr. Es folgt keine Initialisierung und keine Auswertung des Fortschaltausdrucks.

Wann for und wann while?

Da sich while- und for-Schleife sehr ähnlich sind, besteht die berechtigte Frage, wann die eine und wann die andere zu nutzen ist. Leider verführt die kompakte for-Schleife sehr schnell zur einer Überladung. Manche Programmierer packen gerne alles in den Schleifenkopf hinein und der Rumpf besteht nur aus einer leeren Anweisung. Dies ist ein schlechter Stil und muss vermieden werden. Die for-Schleife sollte dort eingesetzt werden, wo sich alle drei Ausdrücke im Schleifenkopf auf dieselbe Variable beziehen, etwa zum Durchzählen einer Variablen (Zählvariable oder Laufvariable):

// Zahlen von 1 bis 10 ausgeben

for(int i = 1; i <= 10; i++ )
  System.out.println( i );

Vermieden werden sollten unzusammenhängende Ausdrücke im Schleifenkopf.

for-Schleifen und ihr Kommaoperator

Im ersten und letzen Teil einer for-Schleife lässt sich ein Komma einsetzen. Damit lassen sich entweder mehrere Variablen deklarieren – wie wir es schon kennen – oder mehrere Ausdrücke nebeneinander schreiben.22 

Beispiel Schleife mit zwei Zählern:
for ( int i=1, j=9; 
i <= j; i++, j-- )
  System.out.println( i + "*" + j + " = " + i*j );

Dann ist die Ausgabe:

1*9 = 9
2*8 = 16
3*7 = 21
4*6 = 24
5*5 = 25

Beispiel Berechne vor dem Schleifendurchlauf den Startwert für die Variablen x und y. Erhöhe dann x und y und führe die Schleife aus bis x und y beide 10 sind.
int x, y;
for ( x=initX(), y=initY(), 
x++, y++;
      x==10 && y==10;
      x+=xinc(), y+=yinc() )
{
  // ...
}

Tipp Komplizierte for-Schleifen sind lesbarer, wenn die drei for-Teile in getrennten Zeilen stehen.

Wird das Komma für die Deklaration mehrerer Variablen verwendet, so kann dahinter kein Ausdruck mit Komma abgetrennt werden. Wenn der Compiler mit einer Deklaration beginnt, könnte er gar nicht zwischen einer zweiten Deklaration für eine Variable und dem folgenden Ausdruck unterscheiden, da das Komma die Variablennamen abtrennt.

Beispiel Folgende for-Schleife ergibt einen Fehler
int i;
for ( int cnt=0, i=1; cnt < 10; cnt++ )
  ;

Der erste Teil leitet eine Anweisung ein. Nach dem Komma folgt für den Compiler aber die Deklaration einer zweiten Variablen. Da sie jedoch schon eine Zeile vorher definiert wurde, meldet der Compiler einen Fehler.

Auch umgekehrt funktioniert das nicht, denn eine Variablendeklaration ist kein Ausdruck, sie ist formal betrachtet eine Anweisung.

Beispiel Einer Anweisung kann keine Variablendeklaration folgen. Daher ist auch Folgendes falsch:
for ( i=0, int j=0; ; )
  ;

Im letzten Teil von for, dem Fortschaltausdruck, darf keine Variablendeklaration stehen. Für was sollte das auch gut sein?


Galileo Computing

2.7.5 Ausbruch planen mit break und Wiedereinstieg mit continue  downtop

Wird innerhalb einer for-, while- oder do/while-Schleife eine break-Anweisung eingesetzt, so wird der Schleifendurchlauf beendet.

Beispiel Führe die Schleife so lange durch, bis i den Wert 0 hat.
int i = 10;

while ( true )
  if ( i-- == 0 )
    break;

Die Anweisung ist nützlich, um im Programmblock festzustellen, ob die Schleife noch mal durchlaufen werden soll. Sie entlastet den Schleifenkopf, der sonst die Bedingung testen würde. Da ein kleines break jedoch im Programmtext verschwinden könnte, die Bedeutung aber groß ist, sollte ein kleiner Hinweis auf diese Anweisung gesetzt werden.

Innerhalb einer for-, while- oder do/while-Schleife lässt sich eine continue-Anweisung einsetzen, die nicht wie break die Schleife beendet, sondern zum Schleifenkopf zurückgeht, so dass dort eine neue Prüfung gemacht werden kann, ob die Schleife weiter durchlaufen werden soll. Ein häufiges Einsatzfeld sind Schleifen, die im Rumpf immer wieder Werte solange holen und testen bis sie passend zur Weiterverarbeitung sind.

Beispiel Gebe die geraden Zahlen von 0 bis 10 aus:
for ( int i=0; i<=10; i++ )
{
  if ( i%2 == 1 )
    continue;

System.out.println( i + " ist eine gerade Zahl" );
}

Abbildung


Galileo Computing

2.7.6 Break und Continue mit Sprungmarken  downtop

Obwohl das Schlüsselwort goto in der Liste der reservierten Wörter auftaucht, ist diese Operation nicht erlaubt. Programmieren mit goto sollte vermieden werden. Mit dem Konzept vom break lässt sich gut leben und es auch noch ruhigen Gewissens einsetzen. Doch zum Schrecken vieler kann break noch schmutziger eingesetzt werden, nämlich mit einer Sprungmarke. Das bringt Java verdächtig nahe in die goto-Welt der unstrukturierten Programmiersprachen, was die Entwickler eigentlich vermeiden wollten. Da jedoch Abbruchbedingungen – der häufigste Einsatzort eines gotos – vereinzelt auftreten, wurden in Java break und continue mit Sprungmarken eingeführt.

 Beispiel   Der Einsatz von break 
oder continue:one:

  while ( condition )
  {
    ...

  two:
    while ( condition )
    {
     ...
     // break oder continue
    }
  // nach two
  }
// nach one

Wird innerhalb der zweiten while-Schleife ein break platziert, dann würde es beim Aufruf die while-Schleife beenden. Das continue würde zur Fortführung der while-Schleife führen. Dieses Verhalten entspräche C, aber in Java ist es erlaubt, hinter den Schlüsselworten break und continue Sprungmarken zu setzen. Das C-Verhalten kann in Java mit break two oder continue two beschrieben werden. Dass aber auch beispielsweise break one möglich ist, zeigt die Mächtigkeit dieses Befehls. Durch das break und continue mit Marken ist daher ein goto nahezu überflüssig.


Galileo Computing

2.8 Methoden einer Klasse  downtop

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, 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 Nachrichtenversandes nicht mehr so genau betrachten, sondern wir sagen nur noch, dass eine Methode aufgerufen wird.

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:

gp  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.
gp  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.

Galileo Computing

2.8.1 Bestandteil einer Funktion  downtop

Eine Funktion besteht aus mehren 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. Der Methodenname, die Parameter und die Typen der Parameter definieren die Signatur einer Methode. Pro Klasse darf es nur eine Methode mit derselben Signatur geben, sonst meldet der Compiler einen Fehler. Der Rückgabewert fließt nicht in die Signatur mit ein.

Beispiel Die Funktionen
Object habHunger( Object o )

und

void habHunger( Object p )

werden vom Compiler als gleich angesehen und können deshalb nicht zusammen in einer Klasse vorkommen.


Beispiel Betrachten wir eine Funktion, die es schon gibt, und die in der API-Hilfe dokumentiert ist.
gp  public static double max(double a, double b)
Returns the greater of two double values. That is, the result is the argument closer to positive infinity. If the arguments have the same value, the result is that same value. If either value is NaN, then the result is NaN. Unlike the numerical comparison operators, this method considers negative zero to be strictly smaller than positive zero. If one argument is positive zero and the other negative zero, the result is positive zero:
Parameters: a – a double value. b – a double value. Returns: the larger of a and b.

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 Modifzierer. 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.

Beispiel Es gibt Funktionen, die noch andere Modifizierer und eine erweiterte Signatur besitzen. Ein weiteres Beispiel aus der API.
gp  protected final void implAccept(Socket s)
throws IOException
Subclasses of ServerSocket use this method to override accept() to return their own subclass of socket. So a FooServerSocket will typically hand this method an empty FooSocket. On return from implAccept the FooSocket will be connected to a client:
Parameters: s – the Socket Throws: IOException – if an I/O error occurs when waiting for a connection. Since: JDK1.1

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 throw eine Ausnahme. Dies 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. Ein eigenes Kapitel widmet sich der Ausnahmebehandlung.


Galileo Computing

2.8.2 Aufruf  downtop

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 müssen auch dann gesetzt werden, wenn die Methode keine Parameter enthält. Eine Funktion wie System.out.println() gibt nichts als Ergebnis einer Berechung zurück. Anders ist die Funktion max(). Sie liefert ein Ergebnis. Damit ergeben sich vier unterschiedliche Funktionentypen:

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

2.8.3 Methoden ohne Parameter  downtop

Die einfachste Funktion besitzt keinen Rückgabewert und keine Parameter. Im mathematischen Sinne ist dann vielleicht auch der Name »Funktion« falsch, wenn sie keinen Wert zurückgeliefert, aber das soll uns nicht kümmern. Der Funktionscode wird in geschweiften Klammern hinter den Kopf geschrieben und bildet damit den Körper der Methode. Gibt eine Funktion nichts zurück (das mathematische Dilemma), dann ist als spezieller Rückgabetyp void vorgesehen. Im klassischen Sinne ist dieser Typ von Funktion unter dem Namen »Prozedur« bekannt, die von der Aufgabe abstrahiert, in dem sie Funktionalität hinter einem Namen verbirgt. Der Begriff »Prozedur« ist jedoch in der Objektwelt nicht anzutreffen.

Beispiel Eine Funktion ohne Rückgabe und Parameter, die einfach etwas auf dem Bildschirm ausgibt. Listing 2.6   SimpleFunction.java
class SimpleFunction
{
  static void tollHier()
  {
    System.out.println( "Toll hier im Java-Land" );
  }
  public static void main( String args[] )
  {
    tollHier();
  }
}

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.


Galileo Computing

2.8.4 Parameter und Wertübergabe  downtop

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 Werfen wir einen Blick auf eine Funktion max() für Gleitkommazahlen, die den größeren Wert auf dem Bildschirm ausgibt.
static void max( 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 mit dem 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 max() etwa mit max(10, y) auf, so ist das Literal 10 und die Variable y ein Argument. Die Argumente müssen vom Typ her passen. 10 kann auf ein double konvertiert werden und y muss ebenfalls automatisch angepasst werden können. Für die Typanpassung gelten die bekannten Regeln.

Hinweis Anzahl der Parameter: Im Gegensatz zu C(++) muss beim Aufruf der Funktion die Anzahl der Parameter exakt stimmen. Eine variable Parameteranzahl – wie in C(++) durch »...« angedeutet – ist in Java nicht möglich. Alle Parameter sind fest und folglich typsicher.

Wertübergabe: Copy 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 besitzen eine ganze Reihe von verwirrenden Möglichkeiten. Java definiert nur einen Mechanismus, der Wertübergabe (engl. Copy by Value) genannt wird. Der in der Methode definierte 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 Parameter-Variable.

Beispiel Die Implementierung der Funktion max():
static void max( double a, double b )
{
  if ( a > b )
    System.out.println( a );
  else
    System.out.println( b );
}

Innerhalb des Funktions-Körpers nutzen wir einfach die übergebenen Werte über die Variable. Beim Aufruf werden die Werte des Arguments in die Variablen kopiert.

Der Wert von 10 gelangt in die Variable a und der Inhalt von i wird ausgelesen und der Variablen b in der Methode zugänglich gemacht:

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

Da der Wert der Variablen ü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« zu Stande. Lediglich der Wert wird übergeben und nicht zum Beispiel eine Referenz auf die Variable.

Auswertung der Argumentenliste von links nach rechts

Bei einem Methodenaufruf werden erst alle Argumente ausgewertet und anschließend der Methode übergeben. Das 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

2.8.5 Methoden vorzeitig mit return beenden  downtop

Läuft eine Methode bis zum Ende durch, dann ist die Methode damit beendet und es geht zurück zum Aufrufer. In Abhängigkeit 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, das vor dem Ende der Funktion automatisch ein verstecktes return steht.

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 gemacht:
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

2.8.6 Nicht erreichbarer Quellcode bei Funktionen  downtop

Folgt direkt hinter einem return 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 machen 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:

void faul()
{
  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:

void f() {
  ;
  return;;
}

In diesem Beispiel sind zwei Null-Anweisungen enthalten. Eine vor dem return und eine dahinter. Doch das zweite Semikolon hinter dem return ist illegal, da es nicht erreichbarer Code ist.


Galileo Computing

2.8.7 Rückgabewerte  downtop

Funktionen wie die Math.max() liefern in Abhängigkeit von den Parametern ein Ergebnis zurück. Für den Aufrufer ist die Implementierung egal, er abstrahiert und nutzt lediglich die Methode als einen Ausdruck. Damit Methoden wie echte Funktionen Rückgabewerte an den Aufrufer weitergeben können, müssen zwei Dinge gelten:

gp  Eine Methode muss mit einem Rückgabetyp ungleich void definiert werden und
gp  sie muss eine return-Anweisung besitzen, die einen geeigneten Wert zurückgibt.

Die allgemeine Syntax ist

return Ausdruck;

Ein allgemeines return ohne Ausdruck ist in einer Funktion mit Ergebnis ein Programmfehler. Der Rückgabewert muss an der Aufrufstelle nicht zwingend benutzt werden.

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

Beispiel Eine Methode bildet aus drei Ganzzahlen das Maximum und gibt dieses zurück. Wir nutzen dazu eine Methode, die es schon gibt: Math.max().

static double max(double a, double b, double c )
{
  return Math.max( Math.max( a, b), c );
}

Innerhalb der Funktion steckt wieder ein Funktionsaufruf. Der Typ hinter dem Rückgabewert muss kompatibel zum angegebenen Rückgabewert sein. Das passt in unserem Beispiel, denn die Math.max()-Funktion liefert ein double. Für die Anpassung gelten sonst wieder die bekannten Typanpassungsregeln.

Beispiel Die nächste Funktion isLeap() stellt nach der Methode CMI fest, ob es sich bei einem Jahr um ein Schaltjahr handelt. Die Funktion arbeitet mit dem gregorianischen Kalender und gibt nur eine korrekte Antwort, wenn das Jahr zwischen 1583 und 20000 liegt.

Listing 2.7   LeapYear.java
class LeapYear
{
 /**
  * @return true, falls year ein Schaltjahr ist
  */

  static boolean isLeap( int year )
  {
     return  year %   4 == 0  &&
            ( year % 100 != 0 || year % 400 == 0 );
  }

   public static void main( String args[] )
  {
     System.out.println( isLeap( 2000 ) );
  }
}

Mehrere return-Anweisungen

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 weiter wie bei den normalen void-Methoden.

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 && b == 2 )
  return 0;
else if ( a == 2 && b == 1 )   
// mit else
  return 1;

Dies lässt sich durch Folgendes ersetzen:

if ( a == 1 && b == 2 )
  return 0;

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

Passt die erste Bedingung, so endet die Funktion, und das nachfolgende if würde sowieso nicht vom Interpreter beachtet.

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

Beispiel Die Funktion gerade() soll bei geraden Zahlen 1 und bei ungeraden Zahlen 0 liefern.
static int gerade( 
int i )
{
  switch ( i%2 )  {
    case 0: return 1;
    case 1: return 0;
  }
}

Die Funktion lässt sich nicht kompilieren, obwohl für uns der Rest der Division nur 0 oder 1 sein kann. Bei den Dingen, die für den Benutzer meistens offensichtlich sind, muss der Compiler passen, da er nicht hinter die Bedeutung schauen kann. (Was ist im Übrigen mit negativen Zahlen?)

Dies passiert auch oft bei Methoden, die nur einen bestimmten Wertebereich zurückgeben, den wir dann behandeln, aber der Compiler nicht kennt. Ein typisches Beispiel ist etwa eine Wochen-Funktion, die den Ganzzahl-Rückgabewert einer Funktion mit einem Wochentag als String kodiert verbindet. Wenn wir die Fälle 0=Sonntag bis 6=Samstag 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 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";
}

Es wird überraschen, aber dieser Programmcode ist 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, das 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.

Bei Methoden, die einen Fehlerwert wie -1 zurückliefern, ist es eine häufig aufzufindende Implementierung, dass am Ende immer automatisch der Fehlerwert zurückgeliefert und dann in der Mitte die Methode bei passendem Ende verlassen wird.

Wir wollen nun eine Methode programmieren, die testet, ob ein Wert zwischen zwei Grenzen liegt. Dazu gibt es zwei Lösungen, wobei die meisten Programmierer zur ersten Lösung neigen.

Nennen wir unsere untere Schranke a und die obere Schranke b. Dann soll die Funktion between() testen, ob x zwischen a und b 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 (<=) gemacht werden soll. Wir wollen hier auch die Gleichheit betrachten.

Die erste Lösungsidee formuliert sich durch eine mathematische Gleichung. Wir möchten gerne a <= x <= b schreiben, doch dies ist in Java nicht erlaubt23  . 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

2.8.8 Methoden überladen  downtop

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 gleichem Namen wie eine andere Funktion, aber eine davon verschiedene Parameterliste. Das geht auf zwei Weisen:

gp  Eine Funktion kann die gleiche Anzahl von Parametern wie eine andere besitzen und unterschiedliche, aber für den Compiler unterscheidbare, Typen annehmen oder
gp  eine unterschiedliche Anzahl von Parametern akzeptieren.

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, jedoch nicht in Java. Zum Beispiel bei der in der Mathe-Klasse Math angebotenen Funktion max(). Sie ist definiert auf unterschiedlichen Typen, zum Beispiel int und double. Das ist viel schöner als die separaten Funktionen maxInt() und maxDouble() zu benutzen.

Beispiel Eine unterschiedliche Anzahl von Parametern ist ebenfalls eine sinnvolle Angelegenheit. Die Funktion max() könnten wir so für drei Parameter definieren.

Schreiben wir unserer eigene Variante für zwei und drei Parameter:

static int max( int i, int j ) {
  return Math.max(i,j);
}

static int max( int i, int j, int k ) {
  return max(i, max(j,k) );               // Methode von oben aufrufen.
}

Variable Parameterlisten wie in C(++) werden durch die Möglichkeit der überladenen Methoden nahezu unnötig.

print() und println() sind überladen

Das bekannte print() ist eine überladene Funktion, die etwa wie folgt definiert ist:

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, welche Objekte ausgibt. Und da Date, wie auch alle anderen Klassen, eine Unterklasse von Object ist, wird diese print()-Funktion gewählt. Natürlich kann nicht erwartet werden, dass das Datum in einem unbestimmten Format (etwa nur das Jahr) ausgeben wird, jedoch wird eine Ausgabe auf dem Schirm sichtbar. Denn jedes Objekt kann sich durch den Namen identifizieren und dieser würde in dem Falle ausgegeben. Obwohl es sich so anhört, als ob immer die Funktion mit dem Parameter Objekt 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 Falle 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 ein paar Ausnahmen. In der Grafik-Bibliothek finden sich die Funktionen

gp  drawString(String,int,int),
gp  drawChars(char[],int,int) und
gp  drawBytes(byte[],int,int).

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 Unterscheidung allerdings sinnvoll. Ein Objekt vom Typ DataOutputStream dient zum Schreiben von primitiven Werten, und davon gibt es bekannterweise einige, mit unterschiedlichen Längen. Gäbe es in DataOutputStream etwa eine Methode write(int) und write(short), und wir fütterten sie mit write(21), 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 etwa nicht write(short) auf und schreibt zwei Bytes, sondern es ruft write(int) auf und schreibt somit vier Bytes. Um also Übersicht über die geschriebenen Bytes zu behalten, ist eine ausdrückliche Kennzeichnung der Datentypen in manchen Fällen gar nicht so dumm.


Galileo Computing

2.8.9 Vorinitialisierte Parameter bei Funktionen  downtop

Überladene Funktionen lassen sich auch in dem Fall verwenden, wenn vorinitialisierte Werte bei nicht vorhandenen Parametern genutzt werden sollten. Ein Beispiel: Wir möchten eine Funktion zum Konvertieren von Zahlen kodiert als Zeichenkette in ein Zahlenformat erlauben. Dazu implementieren wir toDecimal(String s, int radix). Wir möchten, dass radix automatisch 10 ist, wenn keine Basis angeben ist. Die Sprache C++ erlaubt dies, Java jedoch nicht. Doch in Java überladen wir einfach die Funktion und rufen die andere Funktion mit 10 auf:

int toDecimal( String s, int radix )
{
  // Umwandlung
}
int toDecimal( String s )
{
  toDecimal( s, 10 );
}

Galileo Computing

2.8.10 Finale lokale Variablen  downtop

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 diese nicht mehr verändert werden kann. Dies gibt dem Compiler die Chance, zusätzliche Optimierungen vorzunehmen:

int 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 definiert die sogenannte aufgeschobene Initialisierung. Das heißt im Zusammenhang mit finalen Werten, dass nicht zwingend zum Zeitpunkt der Variabelendeklaration ein Wert zugewiesen werden muss. Dies kann auch genau einmal im Programmcode geschehen. Folgendes ist gültig:

final int a;
...
a = 2;

In der Vergangenheit enthielten Java- und Jikes-Compiler Fehler, so dass mehrfache Zuweisungen fälschlicherweise erlaubt waren.24 

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 anschauen. 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 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 sehen. Eine Unterklasse kann demnach beliebig das final hinzufügen oder auch wegnehmen. Alte Bibliotheken lassen sich so leicht weiterverwenden.


Galileo Computing

2.8.11 Finale Referenzen in Objekten und das fehlende const  downtop

Wir haben gesehen, dass finale Variablen dem Programmierer vorgeben, dass er Variablen nicht beschreiben darf. Das heißt, Zuweisungen sind tabu. Leider löst das noch nicht das Problem, dass eine Methode mit übergebenen Referenzen Objektveränderungen vornehmen kann. Greifen wir etwas vor

Beispiel Die foo()-Methode ändert ein Attribut von einem Point-Objekt.
public void foo( final Point p )
{
//  p = new Point();
  p.x = 2;
}

Die Zuweisung ist für eine Referenz nicht weiter tragisch, da wir den Objektzustand dadurch nicht verändern, sondern lediglich die lokale Variable auf ein neues Objekt lenken. Nur Zuweisungen lässt final nicht zu. Was final nicht überprüft ist, dass wir die Referenz auf der linken Seite einer Zuweisung haben können und dadurch das Objekt verändern können, wie im oberen Beispiel. final erfüllt demnach nicht die gewünschte Aufgabe, schreibende Objektzugriffe zu verhindern. Die Dokumentation muss also immer ausdrücklich beschreiben, wann die Funktion den Zustand eines Objekts modifiziert.

Obwohl die Javaentwickler das Schlüsselwort const reserviert haben, ist diese Funktionalität noch nicht in die Sprache eingeflossen. Schade eigentlich. Wir sollten jedoch bemerken, dass es höchstens Schreibzugriffe anmerken könnte, Änderungen aber oft über setXXX()-Methoden realisiert werden.


Galileo Computing

2.8.12 Rekursive Funktionen  downtop

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

Auf dem Weg durch den Wald begegnet uns eine Fee. Sie spricht 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 – den letzten Wunsch als »Noch mal drei Wünsche frei« zu formulieren.

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. Dies nennt sich dann nicht mehr direkte Rekursion, sondern die indirekte. Rekursion ist ein sehr alltägliches Phänomen, das wir auch von der Rückkopplung Mikrofon/Lautsprecher oder den Blick mit einem Spiegel in den Spiegel kennen.

Abbruch der Rekursion

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

Beispiel Eine Endlos-Rekursion:
static void runter( 
int n )
{
  System.out.print( n + ", " );

  runter( n – 1 );
}

Rufen wir runter(10) auf, dann wird die Zahl 10 auf dem Bildschirm ausgegeben und anschließend runter(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.

Beispiel Die Abbruchbedingung einer Rekursion:
static void runter( int n )
{
  if ( n == 0 )   // Rekursionsende
  return;

System.out.print( n + ", " );

runter( n – 1 );
}

Die runter() Methode ruft jetzt nur noch so lange runter(n-1) auf, wie das n ungleich Null ist.


Unterschiedliche Rekursionsformen

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, in der die erste bekannt und die zweite neu ist.

static void runter1( 
int n )
{
  if ( n == 0 )   // Rekursionsende
    return;

  System.out.print( n + ", " );

  runter1( n – 1 );
}

static void runter2( int n )
{
  if ( n == 0 )   // Rekursionsende
    return;

  runter2( n – 1 );

  System.out.print( n + ", " );
}

Der Unterschied zu den beiden ist, dass bei runter1() zuerst die Zahl n ausgegeben wird und dann runter1() noch einmal aufgerufen wird. Die Methode runter2() geht jedoch immer tiefer ab, und die Rekursion muss beendet sein, bis es zum ersten print() kommt. Daher gibt im Gegensatz zu runter1() die Methode runter2() die Zahlen in aufsteigender Reihenfolge aus:

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

Dies ist einleuchtend, wenn wir die Ablaufreihenfolge betrachteten. Beim Aufruf runter2(10) ist der Vergleich von n mit Null falsch, also wird ohne Ausgabe wieder runter2(9) aufgerufen. Ohne Ausgabe deshalb, da print() ja erst nach dem Funktionsaufruf steht. Es geht rekursiv tiefer bis n gleich Null ist. Dann beendet die letzte Methode mit return und die Ausgabe wird nach dem runter2(), 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 runter2() wieder beendet (ein unsichtbares nicht direkt geschriebenes return) und sie springt zum Aufrufer zurück. Das war wieder die Methode runter2(), aber mit der Belegung n=2. Das geht so weit, bis es zurück zum Aufrufer kommt, der runter(10) aufgerufen hat, zum Beispiel die main()-Methode. Der Trick bei der Sache ist nun zu sehen, dass jede Methode ihre eigene lokale Variable besitzt.

Ausblick

Der niederländische Maler Maurits Cornelis Escher (1898–1972) machte die Rekursion auch in Bildern berühmt. Seiten mit Bildern und Vita finden sich zum Beispiel unter folgenden Webadressen:

gp  http://www.worldofescher.com
gp  http://www.etropolis.com/escher
gp  http://www.iproject.com/escher/escher100.html

Zwei weitere klassische Beispiele für Rekursionen sollen nachfolgend diskutiert werden.


Galileo Computing

2.8.13 Die Ackermann-Funktion  downtop

Wir wollen als mathematisch orientiertes Beispiel für eine rekursive Funktion die Ackermann-Funktion kennenlernen.25  Sie ist nach F. Wilhelm Ackermann (1886–1962) benannt. Viele Funktionen der mathematischen Praxis sind primitiv rekursiv26  und David Hilbert stellte 1926 die Frage, ob alle Funktionen, deren Argumente und Werte natürliche Zahlen sind, primitiv rekursiv sind. Die Ackermann-Funktion steigt sehr stark an und ist für Theoretiker ein Beispiel dafür, dass es berechenbare Funktionen gibt, die aber nicht primitiv-rekursiv sind. Im Jahre 1928 zeigte Ackermann dies an einem Beispiel:27  Der Ackermann-Funktion. Sie wächst stärker als es Substitution und Rekursion ermöglichen und nur für kleine Argumente lassen sich die Funktionswerte noch ohne Rekursion berechnen. Darin bestand auch die Beweisidee von Ackermann, eine Funktion zu definieren, die schneller wächst als alle primitv-rekursiven Funktionen. Wir wollen hier nicht die originale Version von Ackermann benutzen, die durch die Funktionalgleichung

f(n’, x’, y’) = f(n, f(n’, x, y), x)

ausgedrückt wird, sondern die vereinfachte Variante von Hans Hermes. Wir wollen die Version von Hermes aber fortan auch Ackermann-Funktion nennen, da sie direkt aus dem Original gewonnen wird. Für die oben angegebene Funktion muss in der Abhandlung von Ackermann (siehe dazu Fußnote) nachgeblättert werden, um den Nachweis des nicht primitiv-rekursiv-seins zu finden.

Die neue Ackermann-Funktion ist eine Abbildung von zwei ganzen Zahlen auf eine ganze Zahl a(n,m). Sie ist mathematisch durch folgende Gesetzmäßigkeit definiert:

a(0,m) = m + 1 a(n,0) = a(n – 1, 1) a(n,m) = a(n – 1, a(n, m – 1)).

Die Ackermann-Funktion ist dafür berühmt, die Rechenkapazität ganz schnell zu erschöpfen. Schauen wir uns die Implementierung in Java an und testen wir das Programm mit ein paar Werten.

Listing 2.8   Ackermann.java
class Ackermann
{
  public static long ackermann( long n, long m )
  {
    if ( n == 0 )
      return m+1;

    else
    {
      if ( m == 0 )
        return ackermann( n-1, 1 );
      else
        return ackermann( n-1, ackermann(n,m-1) );
    }
  }

  public static void main( String args[] )
  {
    int x = 2,
        y = 2;

    System.out.println("ackermann("+x+","+y+")="+ackermann(x,y) );
  }
}

Für den Aufruf ackermann(1,2) veranschaulicht die folgende Ausgabe die rekursive Eigenschaft der Ackermann-Funktion. Die Stufen der Rekursion sind durch Einrückungen deutlich gemacht:

a(1,2):
 a(0,a(1,1)):
  a(0,a(1,0)):
   a(1,0):
    a(0,1)=2
   a(1,0)=2
   a(0,2)=3
  a(0,a(1,0))=3
  a(0,3)=4
 a(0,a(1,1))=4
a(1,2)=4

Bei festen Zahlen lässt sich der Wert der Ackermann-Funktion direkt angeben.

a(1,n) = n + 2 a(2,n) = 2n + 3 a(3,n) = 2n+3  – 3

Für große Zahlen übersteigt die Funktion aber schnell alle Berechungsmöglichkeiten.


Galileo Computing

2.8.14 Die Türme von Hanoi  downtop

Die Legende von den Türmen von Hanoi soll erstmalig von Ed Lucas in einem Artikel 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 Säule 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 Porphyr28  . Die Scheibe mit dem größtem 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:

gp  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.
gp  Trage selbst die erste Scheibe von einer zur anderen Säule.
gp  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äle 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, soweit die Geschichte. Wollen wir den Algorithmus zur Umschichtung der Porphyrscheiben in Java programmieren, so machen wir dies sicherlich rekursiv.

Listing 2.9   Hanoi.java
final class Hanoi
{
  static void bewegeScheibe( int n, String von, String nach )
  {
    System.out.println( "Scheibe " + n + " von " + von +
    " nach " + nach );
  }
  static void versetzeTurm( int n, String kupfer,
                            String silber, String gold )
  {
    if ( n > 1 )
    {
      versetzeTurm( n-1, kupfer, gold, silber );
      bewegeScheibe( n, kupfer, gold );
      versetzeTurm( n-1, silber, kupfer, gold );
    }
    else
    {
      bewegeScheibe( n, kupfer, gold );
    }
  }
  public static void main( String args[] )
  {
    versetzeTurm( 4, "Kupfer", "Silber", "Gold" );
  }
}

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

Scheibe 1 von Kupfer nach Silber
Scheibe 2 von Kupfer nach Gold
Scheibe 1 von Silber nach Gold
Scheibe 3 von Kupfer nach Silber
Scheibe 1 von Gold nach Kupfer
Scheibe 2 von Gold nach Silber
Scheibe 1 von Kupfer nach Silber
Scheibe 4 von Kupfer nach Gold
Scheibe 1 von Silber nach Gold
Scheibe 2 von Silber nach Kupfer
Scheibe 1 von Gold nach Kupfer
Scheibe 3 von Silber nach Gold
Scheibe 1 von Kupfer nach Silber
Scheibe 2 von Kupfer nach Gold
Scheibe 1 von Silber nach Gold

Schon bei vier Scheiben haben wir 15 Bewegungen. Nun wollen wir uns die Komplexität bei n Porphyrscheiben überlegen. Bei einer Scheibe haben wir nur eine Bewegung zu machen. Bei zwei Scheiben aber schon doppelt so viele wie vorher und noch eine zusätzlich. Formaler:

S1 = 1 S2 = 1 + 2S1 = 3 S3 = 1 + 2S2 = 7

Führen wir die Berechung induktiv fort, so folgt für Sn, das 2 – 1 Schritte auszuführen sind, um n Scheiben zu bewegen. Nehmen wir an, unser Prozessor arbeitet mit 100 MIPS, also 100 Millionen Operationen pro Sekunde, dann ergibt sich für n = 100 eine Zeit von 4*1013  Jahren (etwa 20.000 geologische Erdzeitalter). An diesem Beispiel wird uns wie beim Beispiel mit der Ackermann-Funktion deutlich: Die Funktionen sind im Prinzip berechenbar, nur praktisch ist so ein Algorithmus nicht.


Galileo Computing

2.9 Noch mehr Operatoren  downtop


Galileo Computing

2.9.1 Bit-Operationen  downtop

Zahlen und Werte sind im Computer als Sammlung von Bits gegeben. Ein Bit ist ein Informationsträger, für den Wert wahr oder falsch. Bits werden zusammengesetzt zu Folgen, wie einem Byte, das aus 8 Bits besteht. Die Belegung der Bits ergibt einen Wert. Wenn also jedes der 8 Bits unterschiedliche Belegungen annehmen kann, so ergeben sich 256 unterschiedliche Zahlen. Jede Stelle im Bit bekommt dabei eine Wertigkeit zugeordnet.

Beispiel Die Wertebelegung für die Zahl 19. Sie ist zusammengesetzt aus einer Anzahl von Summanden der Form 2 . Die Zahl 19 berechnet sich aus 16+2+1. Genau die Bits sind gesetzt.

Tabelle 2.9   Bitbelegung für die Zahl 19
Bit 7 6 5 4 3 2 1 0
Wertigkeit 2 =128 2 =64 2 =32 2 =16 2 =8 2 =4 2 =2 2 =0
Belegung für 19 0 0 0 1 0 0 1 1

Mit Bit-Operatoren lassen sich Binäroperationen auf Operanden durchführen, um beispielsweise Bits eines Worts zu setzen. Zu den Bit-Operationen zählen Verknüpfungen, Schiebeoperationen und das Komplement. Durch die bitweisen Operatoren können die einzelnen Bits abgefragt und manipuliert werden. Als Verknüpfungen bietet Java die folgenden Bit-Operatoren an:

Tabelle 2.10   Bit-Operatoren in Java
Operator Bezeichnung Funktion
~ Komplement alle Bits von a werden invertiert
| bitweises Oder jedes Bits von a und b bei a|b wird einzeln Oder-verknüpft
& bitweises Und jedes Bits von a und b bei a&b wird einzeln Und-verknüpft
^ bitweises exklusives Oder (Xor) jedes Bit von a und b bei a^b wird einzeln Xor-verknüpft

Betrachten wir allgemein die binäre Verknüpfung a # b. Bei der binären bitweisen Und-Verknüpfung mit & gilt für jedes Bit: Nur wenn beide Operanden a und b 1 sind, dann ist auch das Ergebnis 1. Bei der Oder-Verknüpfung mit | muss nur einer der Operanden 1 sein, damit das Ergebnis 1 ist. Bei einem Exklusiven Oder (Xor) ist das Ergebnis 1, wenn nur genau einer der Operanden 1 ist. Sind beide gemeinsam 0 oder 1, ist das Ergebnis 0. Dies entspricht einer binären Addition oder Subtraktion. Fassen wir das Ergebnis noch einmal in einer Tabelle zusammen:

Tabelle 2.11   Bitoperatoren in einer Wahrheitstafel
Bit 1 Bit 2 ~Bit 1 Bit 1 & Bit 2 Bit 1 | Bit 2 Bit 1 ^ Bit 2
0 0 1 0 0 0
0 1 1 0 1 1
1 0 0 0 1 1
1 1 0 1 1 0


Galileo Computing

2.9.2 Vorzeichenlose Bytes in ein Integer und Char konvertieren  downtop

Liegt ein Byte als Datentyp vor, so kann es zwar zu einem int automatisch angepasst werden, aber die automatische Typkonvertierung erfüllt nicht immer den gewünschten Zweck:

byte b1 =       100;
byte b2 = (byte)200;

 System.out.println( b1 );  // 100
System.out.println( b2 );  // -56

Beim zweiten Byte ist eine Typanpassung nötig, da 0x90 größer als 0x7f ist und daher den Zahlenbereich eines Byte –128 bis 127 überschreitet. Dennoch kann ein Byte das gegebene Bitmuster annehmen und repräsentiert dann die negative Zahl -56. Wird diese ausgegeben, findet bei println(int) eine automatische Typanpassung auf ein int statt und die negative Zahl als byte wird zur negativen int-Zahl. Das bedeutet, das Vorzeichen wird übernommen. In einigen Fällen ist jedoch wünschenswert, ein byte vorzeichenlos zu behandeln. Bei der Ausgabe soll dann ein Datenwert zwischen 0 und 255 herauskommen. Um das zu erreichen, müssen wir etwas mit den Bits spielen. Die Lösung zeigt die Funktion byteToInt():

static int byteToInt( byte b )
{
  return ((int)b & 0x7f) + ( b<0 ? 0x80 : 0);
}

Für die Anpassung kann zunächst der Datenwert der 7 Bits ohne das Vorzeichen betrachtet werden. Daher schneidet die Und-Verknüpfung mit 0x7f das Vorzeichen heraus. Der übriggebliebene Wert stimmt für Zahlen kleiner gleich 0x7f. Wenn jetzt das Vorzeichen gesetzt ist – das ist es, wenn das Byte b kleiner Null ist – dann muss auf das Ergebnis noch 0x80 addiert werden.

Mit einer ähnlichen Arbeitsweise können wir auch die Frage lösen, wie sich ein Byte, dessen Integerwert im Minusbereich liegt, in ein char konvertieren lässt. Der erste Ansatz über eine Typumwandlung (char)byte ist falsch und auf der Ausgabe dürfte nur ein rechteckiges Kästchen oder ein Fragezeichen erscheinen:

byte b = (byte) 'ß';
System.out.println( (char) b );    // Ausgabe ist  ?

Das Dilemma ist wieder die fehlerhafte Vorzeichenanpassung. Bei der Benutzung des Bytes wird es zuerst in ein int konvertiert. Das »ß« wird dann zu -33. Im nächsten Schritt wird diese –33 dann zu einem char umgesetzt. Das ergibt 65503, was einen Unicodebereich trifft, der zur Zeit kein Zeichen definiert. Das wird wohl auch noch etwas dauern, bis die ersten Außerirdischen uns neue Zeichensätze schenken. Gelöst wird der Fall, in dem von b nur die unteren 8 Bits betrachtet werden. Das geschieht wieder durch ein Ausblenden über den Und-Operator. Damit ergibt sich korrekt:

char c = (char) (b & 0x00ff);

Galileo Computing

2.9.3 Variablen mit Xor vertauschen  downtop

Eine besonders trickreiche Idee für das Vertauschen von Variableninhalten arbeitet mit der Xor-Funktion und benötigt keine temporäre Zwischenvariable. Die Zeilen zum Vertauschen von x und y lauten wie folgt:

int x = 12,
    y = 49;

x ^= y;  // x = x ^ y
y ^= x;  // y = y ^ x
x ^= y;  // x = x ^ y

System.out.println( x + " " + y);  // Ausgabe ist: 49 12

Der Trick funktioniert, da wir mit Xor etwas »rein- und rausrechnen« können. Zuerst rechnen wir in der ersten Zeile das y in das x. Wenn wir anschießend die Zuweisung an das y machen, dann ist das der letzte schreibende Zugriff auf y, also muss hier schon das vertauschte Ergebnis stehen. Das stimmt auch, denn expandieren wir die zweite Zeile, steht dort »y wird zugewiesen an y ^ x« und dies ist y ^ (x ^ y ). Der letzte Ausdruck verkürzt sich zu y = x, da aus der Definition der Xor-Funktion für einen Wert a hervorgeht a ^ a = 0. Die Zuweisung hätten wir zwar gleich so schreiben können, aber dann wäre der Wert von y verlorengegangen. Der steckt aber noch in x aus der ersten Zuweisung. Betrachten wir daher die letzte Zeile x ^ y. y hat den Startwert von x, doch in x steckt ein Xor-y drin. Daher ergibt x ^ y den Wert x ^ x ^ y und der verkürzt sich zu y. Demnach haben wir den Inhalt der Variablen vertauscht. Im Übrigen können wir für die drei Xor-Zeilen alternativ schreiben:

y ^= x ^= y;   // Auswertung automatisch y ^= (x 
^= y)
x ^= y;

Da liegt es doch nahe, die Ausdrücke weiter abzukürzen zu x ^= y ^= x ^= y. Doch leider ist das falsch (es kommt für x immer Null heraus). Den motivierten Lesern bleibt dies als Denksportaufgabe.


Galileo Computing

2.9.4 Die Verschiebe-Operatoren  downtop

Unter Java gibt es drei Verschiebe-Operatoren (engl. Shift-Operator). Sie bewegen die Bits eines Datenworts in eine Richtung. Obwohl es nur zwei Richtungen rechts und links gibt, muss noch der Fall betrachtet werden, ob das Vorzeichen beim links-Shift beachtet wird oder nicht.

i << n

Der Operand i wird unter Berücksichtigung des Vorzeichens n mal nach links geshiftet (mit 2 multipliziert). Der rechts freiwerdende Bitplatz wird mit 0 aufgefüllt. Das Vorzeichen ändert sich jedoch, sobald eine 1 von (Position MSB29  – 1) nach (MSB) geshiftet wird. Das ist jedoch akzeptabel, da es ja ohnehin zu einem Bereichsüberlauf gekommen wäre.

i >> n

Da es in Java nur vorzeichenbehaftete Datentypen gibt, kommt es unter Berücksichtigung des Vorzeichens beim normalen Rechts-Shift zu einer vorzeichenrichtigen Ganzzahldivision durch 2. Dabei bleibt das linke Vorzeichenbit unberührt. Je nachdem, ob das Vorzeichenbit gesetzt ist oder nicht, wird eine 1 oder eine 0 von links eingeschoben.

i >>> n

Der Operator >>> verschiebt eine Variable (Bitmuster) bitweise um n Schritte nach rechts ohne das Vorzeichen der Variablen zu berücksichtigen (vorzeichenloser Rechtsshift). So werden auf der linken Seite (MSB) nur Nullen eingeschoben; das Vorzeichen wird mitgeschoben. Bei einer positiven Zahl hat dies keinerlei Auswirkungen und das Verhalten ist wie beim >>-Operator.

Beispiel Verschiebung einer negativen Zahl: Listing 2.10   ShiftRightDemo.java
class ShiftRightDemo
{
  public static void main( String args[] )
  {
    int i, j;

    i = 64;
    j = i >>> 1;

    System.out.println( " 64 >>> 1 = " + j );

    i = -64;
    j = i >>> 1;

    System.out.println( "-64 >>> 1 = " + j );
  }
}

Die Ausgabe ist für den negativen Operanden besonders spannend:

 64 >>> 1 = 32
-64 >>> 1 = 2147483616

Ein <<<-Operator macht allerdings keinen Sinn, da beim Linksshiften sowieso nur Nullen eingefügt werden.

Division und Multiplikation mit Verschiebeoperatoren

Wird ein Wert um eine Stelle nach links geschoben, so kommt in das niedrigste Bit (das Bit ganz rechts) eine Null hinein und alle Bits schieben sich eine Stelle weiter. Das Resultat ist, dass die Zahl dadurch mit 2 multipliziert wird. Natürlich müssen wir mit einem Verlust von Informationen rechnen, wenn das höchste Bit gesetzt ist, denn dann wird es herausgeschoben, aber das Problem haben wir auch schon bei der normalen Multiplikation. Es gibt in Java keinen Operator, der die Bits rollt, der also die an einer Stelle herausfallenden wieder an der anderen Seite einfügt.

Wenn ein einmaliger Shift nach links mit 2 multipliziert, so würde eine Verschiebung um zwei Stellen nach links eine Multiplikation mit 4 bewirken. Allgemein gilt: Bei einem Shift von i nach links ergibt sich eine Multiplikation mit 2 . Wir können dies dazu nutzen, beliebige Multiplikationen durch Verschiebung nachzubilden.

Beispiel Multiplikation mit 10 durch Verschiebung der Bits:
10*n == n<<3 + n<<1

Das funktioniert, da 10*n = (8+2)*n = 8*n + 2*n gilt.


Diese Umsetzung ist nicht immer einfach, und es gibt tatsächlich kein Verfahren, welches eine optimale Umsetzung liefert. Doch arbeiteten viele Prozessoren auf diese Weise intern die Multiplikation ab und ein Compiler nutzt dies gern zur Optimierung der Laufzeit. Eine Verschiebeoperation ist bei vielen Prozessoren schneller als eine Multiplikation. Doch ist hier Obacht zu geben, denn eine lange Folge von Verschiebungen ist nicht schneller, sondern langsamer als eine direkte Multiplikation.

Neben der Addition kommt selbstverständlich auch die Subtraktion in Frage. Ersetzen wir im oberen Beispiel das Plus durch ein Minus, so bekämen wir eine Multiplikation mit 6. Natürlich müssen wir auf die Überläufen der Zwischenergebnisse bei großen Zahlen achten. Diese würde es bei einer echten Multiplikation nicht geben.

Was wir am Beispiel der Verschiebung nach links gezeigt haben, funktioniert genauso mit einem Shift nach rechts. Jetzt wird bei einmaliger Verschiebung durch 2 dividiert. Jetzt können beliebige Ausdrücke mit * und / und einer Konstante auf Verschiebungen und einfachen arithmetischen Operationen abgebildet werden.

Die Bit-Operatoren in Assembler verglichen mit <<<, << und >>

Auch in Assembler gibt es zwei Gruppen von Schiebeoperatoren: Die arithmetischen Schiebebefehle (SAL und SAR), die das Vorzeichen des Operanden beachten, und die logischen Schiebebefehle (SHL und SHR), die den Operanden ohne Beachtung eines etwaigen Vorzeichens schieben. Die Befehle SAL und SHL haben die gleiche Wirkung. So ist >>> der Bit-Operator, in dem das Vorzeichen nicht beachtet wird, wie SHR in Assembler. Es gibt in Java auch keinen Bit-Operator <<<, da – wie in Assembler – SAL = SHL gilt ( <<< würde die gleiche Wirkung haben wie <<).


Galileo Computing

2.9.5 Setzen, Löschen, Umdrehen, Testen von Bits  downtop

Die Bit-Operatoren lassen sich zusammen mit den Shift-Operatoren gut dazu verwenden, ein Bit zu setzen, respektive herauszufinden, ob ein Bit gesetzt ist. Betrachten wir folgende Funktionen, die ein bestimmtes Bit setzten, abfragen, invertieren und löschen.

Beispiel Anwendung aller Bit-Operatoren:
int setBit( int n, int pos )
{
  return n | (1 << pos);
}

int clearBit( int n, int pos )
{
  return n & ~ (1 << pos);
}

int flipBit( int n, int pos )
{
  return n ^ (1 << pos);
}

boolean testBit( int n, int pos )
{
  int mask = 1 << pos;

  return (n & mask) == mask;
  // alternativ: return (n & 1<<pos)!=0;
}


Galileo Computing

2.9.6 Der Bedingungsoperator  downtop

In Java gibt es ebenso wie in C(++) einen Operator, der drei Operanden benutzt. Dies ist der Bedingungsoperator, der auch Konditional-Operator, ternärer Operator beziehungsweise trinärer Operator genannt wird. Er erlaubt es, den Wert eines Ausdrucks von einer Bedingung abhängig zu machen, ohne dass dazu eine if-Anweisung verwendet werden muss. Die Operanden sind durch ? bzw. : voneinander getrennt:

ConditionalOrExpression ? Expression : ConditionalExpression

Der erste Ausdruck muss vom Typ boolean sein und bestimmt, ob das Ergebnis Expression oder ConditionalExpression ist. Der Bedingungsoperator kann eingesetzt werden, wenn der zweite und dritte Operand ein numerischer Typ, boolescher Typ oder Referenz-Typ ist. Der Aufruf von Methoden, die demnach void zurückgeben, ist nicht gestattet.

Eine Anwendung für den trinären Operator ist oft eine Zuweisung an eine Variable:

variable = bedingung ? ausdruck1 : ausdruck2;

Der Wert der Variablen wird jetzt in Abhängigkeit der Bedingung gesetzt. Ist sie erfüllt, dann erhält die Variable den Wert des ersten Ausdrucks, andernfalls wird der Wert des zweiten Ausdruckes zugewiesen.

Beispiel So etwa für ein Maximum:
max = ( a > b ) ? 
a : b;

Dies entspricht beim herkömmlichen Einsatz für if/else:

if ( a > b )
  max = a;
else
  max = b;

Mit dem Rückgabewert können wir alles mögliche machen, wie ihn etwa direkt ausgeben. Das wäre mit if/else nur mit temporären Variablen möglich.

System.out.println( ( a > b ) ? a : b );

Beispiele

Der Bedingungsoperator findet sich häufig in kleinen Funktionen. Dazu einige Beispiele.

Beispiel Um das Maximum oder Minimum zweier Ganzzahlen zurückzugeben, definieren wir die kompakte Funktion:
static int min( int a, int b ) { return a < b 
? a : b; }
static int max( int a, int b ) { return a > b ? a : b; }

Beispiel Der Absolutwert einer Zahl wird zurückgegeben durch
x >= 0 ? x : -x

Beispiel Der Groß-/Kleinbuchstabe im ASCII-Alphabet soll zurückgegeben werden. Dabei ist c vom Typ char.
// Konvertierung in Großbuchstaben

c = (char)(Character.isLowerCase(c) ? (c-'a'+'A' ) : c);

// Konvertierung in Kleinbuchstaben

c = (char)(Character.isUpperCase(c) ? (c-'A'+'a' ) : c);

Da diese Umwandlung nur für die 26 ASCII-Buchstaben funktioniert, ist es besser, Bibliotheksmethoden für alle Unicode-Zeichen zu verwenden.


Beispiel Es soll eine Zahl n, die zwischen 0 und 15 liegt, zur Hexadezimalzahl konvertiert werden.
(char)( (n < 10) ? ('0' + n) : ('a' – 10 
+ n ) )

Einige Fallen

Die Anwendung des trinären Operators führt schnell zu schlecht lesbaren Programmen und sollte daher vorsichtig eingesetzt werden. In C(++) führt die unbeabsichtigte Mehrfachauswertung in Makros zu schwer auffindbaren Fehlern. Gut, dass uns das in Java nicht passieren kann. Durch ausreichende Klammerung muss sichergestellt werden, dass die Ausdrücke auch in der beabsichtigten Reihenfolge ausgewertet werden. Im Gegensatz zu den meisten Operatoren ist der Bedingungsoperator rechtsassoziativ. (Die Zuweisung ist ebenfalls rechtsassoziativ.) Die Anweisung

b1 ? a1 : b2 ? a2 : a3

ist demnach gleichbedeutend mit

b1 ? a1 : ( b2 ? a2 : a3 )
Beispiel Wollen wir eine Funktion schreiben, die für eine Zahl n abhängig vom Vorzeichen -1, 0 oder 1 liefert, lösen wir das Problem mit geschachteltem trinärem Operator.
int sign( int n )
{
  return (n < 0) ? -1 : (n > 0) ? 1 : 0;
}

Der Bedingungsoperator ist kein lvalue

Der trinäre Operator liefert als Ergebnis einen Ausdruck zurück, der auf der rechten Seite einer Zuweisung verwendet werden kann. Da er rechts vorkommt, nennt er sich auch rvalue. Er lässt sich nicht so auf der linken Seite einer Zuweisung einsetzen, dass er eine Variable auswählt, der ein Wert zugewiesen wird.30 

Beispiel Folgende Anwendung des trinären Operators ist in Java nicht möglich.31 
((richtung>=0) ? hoch : runter) = true;


Galileo Computing

2.9.7 Überladenes Plus für Strings  toptop

Obwohl sich in Java die Operatoren fast alle auf primitive Datentypen beziehen, gibt es doch eine bemerkenswerte Verwendung des Plus-Operators. Objekte vom Typ String können mit dem Plus-Operator mit anderen Strings verbunden werden. Dies wurde in Java eingeführt, da ein Aneinanderhängen von Zeichenketten oft benötigt wird. Im Kapitel über die verschiedenen Klassen wird String noch etwas präziser dargestellt. Insbesondere werden die Gründe dargelegt, die zur Einführung eines String-Objekts auf der einen Seite, aber auch zur Sonderbehandlung in der Sprachdefinition auf der anderen Seite führten.

Listing 2.11   HelloName.java
// Ein Kleines ,Trallala'-Programm in Java

class HelloName
{
  public static void main( String args[] )
  {
    // Zwei Strings deklarieren
    String name,
           intro;

    // Ersetze "Ulli" durch deinen Namen
    name = "Ulli";

    // Nun die Ausgabe
    intro = "Tri Tra Trallala "+
            "\"Trullala\," sage " + name;
    System.out.println( intro );
  }
}

Nachdem ein String intro aus verschiedenen Objekten zusammengesetzt wurde, läuft die Ausgabe auf dem Bildschirm über die Funktion println() ab. Die Funktion schreibt den String auf die Konsolenausgabe und setzt hinter die Zeile noch einen Zeilenvorschub. (Obwohl das \n nicht unbedingt plattformunabhängig ist, wollen wir es trotzdem nutzen.) Das Objekt System.out definiert den Ausgabekanal.

Beispiel Da der Plus-Operator für Zeichenketten streng von links nach rechts geht, bringt das mit eingebetteten arithmetischen Ausdrücken mitunter Probleme. Diese müssen dann geklammert werden, wie im Folgenden zu sehen:
"Ist 1 größer als 2? " + (1>2 ? "nein" 
: "ja");

Wäre der Ausdruck um den Bedingungsoperator nicht geklammert, dann würde der Plus-Operator den Ausdruck 1>2 auswerten und in einen String umwandeln, und diesen an die erste Zeichenkette anhängen. Jetzt käme das Fragezeichen. Dies ist aber nur für boolesche Werte erlaubt, aber links stände die Zeichenkette. Das wäre ein Fehler.






1    Na ja, so ganz präzise ist das auch nicht. In einem static-Block könnten wir auch einen Funktionsaufruf reinsetzen, doch das wollen wir hier einmal nicht annehmen. static-Blöcke werden beim Laden der Klassen in die virtuelle Maschine ausgeführt. Andere Initialisierungen sind dann auch schon gemacht.

2    In C++ haben die Entwickler übrigens das aus der Vor-Vorgängersprache BCPL wieder eingeführt, was in C entfernt wurde.

3    Bei uns am Niederrhein steht glubschi für etwas Glitschiges, Schleimiges.

4    Abkürzung für Methoden, die mit print- beginnen, also print() und println().

5    Mit veränderten Dateiströmen lässt sich dies etwas in den Griff bekommen. So lässt sich beispielsweise mit einem speziellen OutputStream-Objekt eine Konvertierung für die Windows-NT Shell vornehmen, sodass auch dort die Sonderzeichen erscheinen.

6    Ob ein Zeichen ein Buchstabe ist, verrät uns die Funktion Character.isJavaLetter().

7    Im Gegensatz dazu steht Smalltalk. In Smalltalk sind zuerst einmal alles Objekte und diese haben keinen Typ. Die Operationen werden erst zur Laufzeit an die Objekte gebunden.

8    In C(++) bedeutet Definition und Deklaration etwas Verschiedenes. In Java kennen wir diesen Unterschied nicht und betrachten daher beide Begriffe als gleichwertig.

9    Eine Software wie Mathematica warnt vor Variablen mit fast identischem Namen.

Beispiel Unicode-Sequenzen können vom Programmierer überall im Programm aufgenommen werden. Folgende Deklaration mit den Bezeichnernamen sind daher gleich: double übelkübel;
double \u00FCbelk\u00FCbel;



10    In Java bilden long und short einen eigenen Datentyp. Sie dienen nicht wie in C(++) als Modifizierer. Eine Deklaration wie long int ist also falsch.

11    LOGO verwendet für negative Exponenten den Buchstaben N anstelle des E. In Java bleibt das E mit einem folgenden unären Plus- oder Minus-Zeichen.

12    Die so genannten Objektvariablen oder Klassenvariablen, doch dazu später.

13    Es gibt Programmiersprachen, da werden Wertoperationen besonders gekennzeichnet. So etwa in LOGO. Eine Wertoperation schreibt sich mit einem Doppelpunkt vor der Variablen. Etwa :X + :Y.

14    Wir müssten in C(++) die Funktion fmod() benutzen.

15    In C sind sie nur für Ganzzahlen definiert.

16    Es gibt Programmiersprachen, wie APL, die keine Vorrangsregeln kennen. Sie werten die Ausdrücke streng von rechts nach links oder umgekehrt aus.

17    Und nicht einfach if (ref ) wie in C(++).

18    In der Programmiersprache Python bestimmt die Einrückung die Zugehörigkeit.

19    In der Programmiersprache Euphoria (die Webseite http://www.rapideuphoria.com wirbt mit safe und sexy – Huuh) heißt das einfach: return (x>0)-(x<0).

20    Und auch in C(++).

21    Im Gegensatz zu C++ ist das Verhalten klar definiert und kein hin und her. In C++ implementierten Compilerbauer die Variante einmal so, dass die Variable nur im Block gilt, andere interpretieren die Sprachspezifikation so, dass diese auch außerhalb gültig blieb. Die aktuelle C++-Definition schreibt nun vor, dass die Variable außerhalb des Blockes nicht mehr gültig ist. Da es jedoch noch alten Programmcode gibt, haben viele Compilerbauer eine Option eingebaut, mit der das Verhalten der lokalen Variablen bestimmt werden kann.

22    Wenn Java eine ausdruckorientierte Sprache wäre, dann könnten wir hier beliebige Programme hineinlegen.

23    ... im Gegensatz zur Programmiersprache Python.

24    Ein Beispiel, welches den Fehler reproduziert, findet der Leser unter http://java-tutor.com/faq.html.

25    Das macht dann auch die Informatiker glücklich ...

26    Für Theoretiker: Eine Funktion heißt primitiv-rekursiv, falls sie elementar ist oder in endlich vielen Schritten aus einer elementaren Funktion durch Ersetzung (Substitution) und Rekursion hervorgeht.

27    Nachzulesen in »Zum Hilbertschen Aufbau der reellen Zahlen« von Ackermann, W. Math. Ann 99, 118–133 (1928).

28    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.

29    Most Significant Bit. Das Bit mit der höchsten Wertigkeit in der binären Darstellung.

30    In C(++) kann dies durch *((Bedingung) ? &a : &b) = Ausdruck; über Pointer gelöst werden.

  

Java 2




Copyright © Galileo Press GmbH 2002
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 GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de