© APSIS GmbH extern.gif (1249 Byte), Polling, 2008


Kapitel 7

Implementierungslogik

Im Kapitel 4.1. haben wir gelernt, wie man Klassen mit Hilfe einer anderen, fertigen Klasse programmiert. Im Besitz der Aufzählungs­klassen sind wir jetzt schon in der Lage, die Logik einiger Methoden der Klasse EinEimer eigenständig zu programmieren. Auf das Anzeigen am Bildschirm verzichten wir hier; die Animation wird mit Hilfe einer gesonderten, grafisch orientierten Klasse durchgeführt.

Das Neue dabei ist, dass wir statt eines Objekts der Klasse Eimer (wie im Kapitel kap_rmpf4.1.) zwei Aufzählungsvariablen eimerGefuellt und eimerInhalt als Speicher anlegen. In der ersten wird die Information gespeichert, ob der Eimer überhaupt gefüllt ist, in der zweiten, mit welchem Getränk. Die Zugriffs­operationen greifen auf diese klasseninternen globalen Referenzen zu. Der Vorbesetzungswert XE "Vorbesetzungswert"  sorgt dafür, dass der Eimer zu Anfang leer ist.

package lehrbuch;
public class EinEimer {
  private enum Gefuellt { JA, NEIN;
   public Gefuellt naechster() throws Ausnahme { … }
   public Gefuellt vorheriger() throws Ausnahme { … } // Rümpfe im Kapitel 8.1.5.
   public Ausnahme extends Exception {}
  }
  private static Gefuellt eimerGefuellt = Gefuellt.NEIN; // oder JA
  private static Getraenk eimerInhalt = Getraenk.WASSER; // oder WEIN
  // öffentliche Konstanten:
  public static final Getraenk WASSER = Getraenk.WASSER;
  public static final Getraenk WEIN = Getraenk.WEIN;
  // Ausnahmeobjekte (diesmal global static):
  private final static LeerError leerError = new LeerError();
  private final static VollError vollError = new VollError();
  // Mutatoren:
  public static void fuellen(final Getraenk getraenk) throws VollError {
   try {
     eimerGefuellt = eimerGefuellt.naechster(); // throws Gefuellt.Ausnahme
     eimerInhalt = getraenk; }
   catch (Gefuellt.Ausnahme ausnahme) { // bei JA
     throw vollError; } }
  public static void fuellen() throws VollError {
   fuellen(WASSER); }
  public static void entleeren() throws LeerError {
   try {
     eimerGefuellt = eimerGefuellt.vorheriger(); } // throws Gefuellt.Ausnahme
   catch (Gefuellt.Ausnahme ausnahme) { // bei NEIN
     throw leerError; } }
  // Informator:
  public Getraenk inhalt() throws LeerError {
   try {
     attrappe = eimerGefuellt.naechster(); // throws Gefuellt.Ausnahme
     return eimerInhalt; }
   catch (Gefuellt.Ausnahme ausnahme) {
     throw leerError; } }
  private static Gefuellt attrappe; // global, damit der Compiler es nicht wegoptimiert
}

Typischerweise speichert eine Datenbehälterklasse den Zustand des Objektes in Variablen (wie eimerInhalt und eimerGefuellt); im Falle eines statischen Datenbehälters sind diese Klassenreferenzen (sta­tic). Da sie privat sind, können sie von außen nicht direkt, sondern nur über Operationsaufrufe mani­pu­liert werden. Für die Methoden der Klasse sind diese Referenzen global.

Die Ausnahmen haben wir hier mit einem Trick ausgelöst: Der Aufruf der Aufzählungsmethode naechster für einen Gefuellt-Wert NEIN liefert den Wert JA; für den Wert JA löst sie eine Ausnahme aus. Diese haben wir aufgefangen und die gewünschte Ausnahme mit Hilfe von throw ausgelöst. Diese „Kaskadierung“ von Ausnahmen wird oft benutzt, um sie beim Weiterreichen mit zusätzlichen Informationen „anzureichern“. Den vorliegenden Fall programmiert man jedoch eher mit einer Fallunterscheidung (if), die wir im Kapitel 8.1.5. kennen lernen werden.

Boolesche Werte

In der obigen Implementierung haben wir eine Aufzählungsklasse Gefuellt mit den Werten NEIN und JA definiert. Diese Unterscheidung zwischen zwei Zuständen ist eine so häufige Notwendigkeit, dass praktisch alle Programmiersprachen für die zwei Werte besondere Sprachelemente definieren; meistens werden sie mit „true“ und „false“ bezeichnet; Sie heißen "logischer Wert" oder Boolesche Werte (benannt nach dem englischen Mathematiker George Boole).

Diese Werte können auf zweierlei Weise benutzt werden:

· Sie können in Datenbehälter-Objekten gespeichert werden, wie WASSER und WEIN in Objekten der Klasse Eimer.

· Sie können in Variablen gespeichert werden, ähnlich wie WASSER und WEIN in Referenzen der Klasse Getraenk.

Boolesche Klassen

Das Paket java.lang enthält die Klasse Boolean. Sie exportiert zwei Werte TRUE und FALSE:

java.lang.Boolean log = java.lang.Boolean.TRUE;

In vielen Programmen werden komplexe Klassen wie Eimer benötigt, die z.B. ihre Objekte auf dem Bildschirm darstellen können. Die Klasse Boolean ist viel einfacher: In ihrem Objekt kann ein Boolescher Wert gespeichert werden, sonst nichts. Ein wichtiger Unterschied ist das Fehlen der Behältergestalt: Die Darstellung erfolgt einfach durch die Ausgabe des enthaltenen Werts im Fenster.

Der wichtigste Unterschied ist aber, dass ein Boolean-Objekt beim Füllen nicht untersucht wird, ob es schon voll ist oder nicht: Eine wiederholte Schreiboperation überschreibt den eventuell schon vorhandenen Inhalt mit dem neuen ohne Warnung: Es wird keine Ausnahme der Art VollException ausgelöst. Dies ist eine Eigenschaft aller einfachen Objekte: Sie speichern keine Information über ihren eigenen Zustand, sie speichern nur ihren Inhalt. Ein Boolean-Objekt kann man sich als eine einzige Zelle im Speicher des Rechners vorstellen, in die ein Bit geschrieben und dort gespeichert werden kann.

Boolesche Literale

Die booleschen Werte FALSE und TRUE (welcher Klasse auch immer) werden so häufig benutzt, dass die Sprache eine besondere Darstellung für die von ihnen repräsentierten (abstrakten) Werte definiert. Sie werden durch die reservierten Worte false und true dargestellt und heißen Boolesche Literale. Literale können z.B. in Methoden oder Konstruktoren als Parameter benutzt werden:

Boolean b = new Boolean(true); // gleichwertig mit new Boolean(Boolean.TRUE)

Von der Standardklasse Boolean weitere Objekte erzeugt werden, wobei dem Konstruktor auch ein Boolesches Literal als Parameter mitgegeben wird. Ein Zeichenkettenparameter ist ebenso möglich; einen parameterlosen Konstruktor exportiert diese Klasse nicht:

log = new Boolean(true); // java.lang implizit
log = new Boolean("true"); // gleichwertig
Ölog = new Boolean(); // Fehler

Der eigentliche Sinn der Booleschen Literale aber ist nicht die Benutzung in Konstruktoren. Sie stellen die Werte eines neuen Sprachelements, des primitiven Typs boolean dar.

Klassen und primitive Typen

Es gibt gewisse Parallelen zwischen Klassen und primitiven Typen, aber auch Unterschiedlichkeiten. Während von Klassen Objekte gebildet werden, heißen sie im Falle von primitiven Typen Variablen. Klassen beschreiben die Eigenschaften ihrer Objekte, primitive Typen beschreiben die Eigenschaften ihrer Variablen. Während aber Klassen Methoden exportieren, mit deren Hilfe ihre Objekte manipuliert werden können, stehen für Variablen nur einige – von der Sprache definierte – Operatoren, wie z.B. die Zuweisung  oder Addition  zur Verfügung.

Der Inhalt von (unseren bisherigen, d.h. einfachen) Datenbehälter-Objekten kann durch Wertereferenzen definiert werden; für Variablen stehen Literale zur Verfügung. Während die Werte der Datenbehälter als öffentliche konstante Klassenre­ferenzen (public final static) auf spezielle Objekte dargestellt werden (wie z.B. out der Klasse System), sind Literale (wie 'a') besondere syntaktische Konstruktionen der Sprache. Man kann sich so vorstellen, dass das Zeichenliteral 'a' den abstrakten Wert (nämlich den Buchstaben a) ähnlich bezeichnet wie die Referenz out das Objekt Standardausgabe referenziert.

Der wichtigste Unterschied zwischen Klassenobjekten und  primitiven Variablen ist, dass die letzteren nicht erzeugt werden, sondern durch ihre Vereinbarung bestehen, und dass auf sie direkt zugegriffen werden kann; Objekte müssen erzeugt werden und sie sind über Referenzen erreichbar.

Referenzen sind Variablen spezieller Art. Bis jetzt hatten wir als Datenkomponenten von Klassen nur Referenzen kennen gelernt; im Kapitel 7.5.4. werden wir sehen, dass auch Variablen von primitiven Typen diese Rolle übernehmen können.

Primitive Variablen können lokal in einer Methode sein; dann verhalten sie sich wie lokale Referenzen, d.h. sie werden auf dem Stapel gespeichert; ihre Lebensdauer be­schränkt sich auf die Laufzeit der Methode. Sie werden – ähnlich wie Referen­zen – als Parameter per Wert übergeben (s. Kapitel 7.10.).

Objekte werden durch Methoden manipuliert, primitive Variablen durch Operatoren. Während die Namen von Methoden Bezeichner sind, sind die Namen von Operatoren Zeichen oder kurze Zeichenfolgen. Programmierer können neue Klassen und Methoden definieren. primitive Typen und ihre Operatoren sind in Java vorgegeben. Sie können in Java auch nicht überladen oder überschrieben werden.

 

Objekt

primitive Variable

wird beschrieben durch

Klasse
(frei definierbar)

primitiver Typ
(Bestandteil der Sprache)

ist erreichbar

über Referenz

direkt

enthält

komplexen Wert

Primitiven Wert

Wert darstellbar durch

(evtl. Wertereferenz)

Literal

manipulierbar durch

Methode

Operator

Tab: Objekte und primitive Variablen

Operatoren

Ein Operator kann mit einem Methodenaufruf verglichen werden.

Während eine parameterlose Methode mit der Syntax

zielreferenz.methode()

aufgerufen wird, braucht ein (monadischer) Operator weder den Punkt, noch die Klammer. Er kann vor oder nach seinem Operanden stehen, dementsprechend heißt er Präfix- oder Postfix-Operator:

operator zielvariable
zielvariable operator

Eine parametrisierte Methode wird mit der Syntax

zielreferenz.methode(parameter)

aufgerufen, der Aufruf eines diadischen Operators lautet:

zielvariable operator parameter

Eine statische Methode mit zwei (Lese-)Parametern

Klasse.Methode(links, rechts)

kann ebenfalls als Operator

links operator rechts

formuliert werden.

Einige Operatoren verändern ihren linken Operanden, diese sind Mutatoren; sie werden wie Prozeduren aufgerufen. Andere werden wie Funktionen aufgerufen. Die Zuweisung ist ein Beispiel für das Erste, der new-Operator für das Zweite:

zielvariable = wert; // Zuweisung als Mutator
methode(new Klasse()); // Objekterzeugung als Funktionsaufruf

Bemerkung: In Java liefern alle Operatoren einen Wert, so auch die Zuweisung:

prozedur(variable = wert); // Zuweisung als Funktion

Im Interesse der Lesbarkeit von Programmen sollte diese Eigenschaft von mutierenden Operatoren nicht ausgenutzt werden (s. auch ++ im Kapitel 7.4.4. ).

Verwendung von primitiven Typen in Klassen

Bis jetzt haben wir Klassen nur mit Hilfe von anderen vorhandenen Klassen implementiert. Schließlich werden sie aber alle auf primitive Typen zurückgeführt.

Informatoren mit primitivem Ergebnistyp

Viele Klassen benutzen primitive Typen, um Informationen von sich zu veröffentlichen. Dies geschieht durch die schon kennen gelernten Informatoren. Ein Informator ist eine Funktion, deren Ergebnis vom Zustand des Datenbehälters abhängt. So ein Informator ist inhalt der Klasse Eimer, der als Ergebnis das Getränk liefert. Es ist allerdings möglich, dass ein Eimer noch gar nicht gefüllt ist, weil sein Mutator fuellen noch nicht oder der Mutator entleeren zuletzt aufgerufen wurde. Für einen nicht gefüllten Eimer den Informator inhalt aufzurufen, löst die Ausnahme LeerException aus.

Es ist also sinnvoll, dass die Klasse Eimer auch den Informator istGefuellt zur Verfügung stellt, mit dem abgefragt werden kann, ob ein Datenbehälter gefüllt ist oder nicht. Da zwei verschiedene Zustände möglich sind, ist das Ergebnis dieses Informators vom Typ boolean. Der gelieferte Wert kann z.B. mit Hilfe von System.out.println auf den Bildschirm geschrieben werden.

Eimer eimer = new Eimer();
  … // Eimer evtl. anzeigen, Zustand "gefüllt" oder "leer" einstellen
Bool fuellZustand = new Bool(eimer.istGefuellt());
  // das Ergebnis wird in einer Booleschen Variable gespeichert
System.out.println(fuellZustand); // oder der Inhalt des gelieferten Objekts angezeigt
System.out.println(eimer.istGefuellt());

Hier sehen wir wieder ein Beispiel für die Verwendung von Hüllenklassen: Der Informator istGefuellt liefert ein Er­geb­nis vom Typ boolean, für den keine Methoden zur Verfügung stehen.

Im Kapitel 8.1. werden wir die Verzweigungen kennen lernen, mit deren Hilfe in Abhängigkeit von einem boolean-Wert unterschiedliche Aktionen durchgeführt werden können.

Gleichheitsoperationen

boolean ist auch der übliche Ergebnistyp von Vergleichsoperationen; sie untersuchen das Verhältnis (Relation) zweier Datenbehälter. Sie sind keine Informatoren, da sie nicht über den Zustand eines Datenbehälters berichten.

Die einfachste Vergleichsoperation dieser Art ist die Gleichheit. Die Vergleichsfunktion istGleich der Klasse Eimer untersucht den Inhalt zweier Objekte der Klasse Eimer:

boolean vergleichswert = eimer1.istGleich(eimer2);
System.out.println(vergleichswert);

Die Untersuchung, ob zwei Datenbehälter den gleichen Inhalt haben oder nicht, ist eine so häufige Operation, dass die Klasse java.lang.Object (die implizite Oberklasse aller Klassen) eine solche Methode an jede Klasse vererbt. Sie heißt equals und liefert ebenfalls einen Wert vom Typ boolean:

vergleichswert = eimer1.equals (eimer2);

Sie hat allerdings eine in java.lang.Object festgelegte Funktionalität: Alle Daten der Objekte werden miteinander verglichen. Im Falle von Eimern schließt dies auch die Position im Fenster mit ein. Sollte dies dem Klassenprogrammierer nicht geeignet erschienen, muss er die Methode überschreiben. Ihr Profil ist

public boolean equals(Object object);

Wegen der impliziten Aufwärtskompatibilität kann ein beliebiges Objekt als Parameter eingesetzt werden:

boolean b = eimer.equals(kreis);

Für Objekte unterschiedlicher Klassen ist das Ergebnis selbstverständlich false. Um dies schon zur Übersetzungszeit zu unterbinden, kann eine andere Methode ähnlicher Funktionalität definiert werden:

public boolean istGleich(Eimer eimer); // liefert boolean-Wert

Viele Klassen exportieren weitere Vergleichsfunktionen, z.B. istUngleich; typischerweise liefert sie das Gegenteil von istGleich.

Ordnungsoperationen

Es ist oft sinnvoll, Werte (z.B. Aufzählungswerte) miteinander auf ihre Definitionsreihenfolge hin zu vergleichen (in Übereinstimmung mit den Methoden anfang und naechster). Hieraus ergibt sich die Notwendigkeit, in einer Klasse die Ordnungs­operationen istKleiner, istNichtKleiner, istGroesser und istNichtGroesser zu definieren.

class Planet { …
  boolean istKleiner(final Planet that) { // const
   … } // vergleicht die Größe der Planeten miteinander
  boolean istNichtGroesser(final Planet that) { return !istKleiner(that); } // const
  boolean istGroesser(final Planet that) { return !istKleiner(that) && !equals(that); }
  boolean istNichtGroesser(final Planet that) { return !istGroesser(that); } }
… // Planet-Objekte erzeugen
boolean b = mars.istKleiner(erde); // true
b = jupiter.istNichtGroesser(erde); // false
b = jupiter.istUngleich(mars); // true

Häufig wird Ordnung unter den Objekten einer Klasse durch die Implementierung der Standard-Schnittstelle Comparable eingeführt. Sie definiert die Methode int compareTo (mit einem Comparable-Parameter), die zwei Objekte dieser Klasse miteinander vergleicht. Wenn sie (in irgendwelchem Sinne) gleich sind, ist das Ergebnis 0; wenn das Zielobjekt kleiner ist, als das Parameterobjekt, ist das Ergebnis -1, ansonsten +1:

class Planet implements Comparable { …
  int compareTo(Planet that) { // const
   … // vergleicht die Größe der Planeten miteinander
… // Planet-Objekte erzeugen
int b = mars.compareTo(erde); // -1, weil kleiner
b = jupiter.compareTo(erde); // +1
b = jupiter.compareTo(jupiter); // 0

Diese (etwas weniger lesbare) Konvention stammt aus C/C++.

Variablen als Datenkomponenten

Primitive Variablen können, genauso wie Referenzen, als Datenkomponenten einer Klasse verwendet werden.

In der Implementierung unserer Eimer-Klasse wird beispielsweise mit einer boolean-Variable gekennzeichnet, ob der Eimer voll ist oder nicht; seine Position im Fenster steht in einer int-Variable. Im Konstruktor werden diese mit Vorbesetzungswert versehen, d.h. jeder Eimer wird leer und unsichtbar (Position ist 0) angelegt. Dieser Anfangszustand wird später durch Mutatoraufrufe verändert.

Das Besondere an der folgenden Klassenimplementierung ist noch, dass sie sowohl Klassen - wie auch Objektkomponenten  enthält (ähnlich wie die Klasse MischEimer im Kapitel 4.3.2.): Neben der Information über jedes Eimerobjekt (in Objektkomponenten) über seinen Füllzustand und seine Position im Fenster enthält es in seinen Klassenkomponenten Information über die letzte Position im Fenster, an der ein Eimer angezeigt wurde. Die Objektkomponenten werden im Konstruktor, die Klassenkomponenten werden im Klassenkonstruktor (s. Kapitel 4.2.3. ) vorbesetzt:

public class Eimer {
  // drei Objektkomponenten:
   private boolean eimerGefuellt;
  private Getraenk eimerInhalt; // WASSER oder WEIN
  private int eimerPos; // Position des Eimers 0 .. 4
  // Klassenkomponente:
  private static int naechstePos; // Position des letzten angezeigten Eimers
  private final static LeerException leerException = new LeerException();
  private final static VollException vollException = new VollException();
  static { // Vorbesetzung der Klassenkomponente im Klassenkonstruktor:
   naechstePos = 0; } // noch kein Eimer wurde angezeigt
  public Eimer() { // Vorbesetzung der Objektkomponenten im Konstruktor
   eimerGefuellt = false;
   eimerPos = 0; }
  public boolean istGleich (final Eimer that) { // const
   // vergleicht Füllzustand und Inhalt
   return (! this.eimerGefuellt && ! that.eimerGefuellt) || // beide leer, oder
     this.eimerGefuellt && that.eimerGefuellt && // beide voll, und
     this.eimerInhalt.istGleich(that.eimerInhalt); } // gleicher Inhalt
  … // Implementierung weiterer Methoden im Kapitel 8.1.5. 

Die Implementierung der Funktion istGleich zeigt den Unterschied zur von Object geerbten Methode equals. Hier werden nur die Objektkomponenten eimerGefuellt, und (wenn beide voll sind) eimerGetraenk verglichen; der Inhalt der Komponente eimerPos ist für das Ergebnis irrelevant. Im Gegensatz dazu vergleicht equals alle Datenkomponenten.

Die Implementierung der Klasse EinEimer im Kapitel 7.1.4. ist dieser sehr ähnlich; dort wurden jedoch alle Daten und Methoden als Klassenkomponenten (mit static) vereinbart, um einen statischen Datenbehälter zu implementieren.

Datenkomponenten erhalten in Java immer eine implizite Vorbesetzung: Arithmetische Variablen werden mit dem Nullwert ihres Typs (z.B. 0 für int, '‚\u0000’ für char) vorbesetzt, Boolesche Variablen mit false, Referenzvariablen mit null. Eine explizite Vorbesetzung überschreibt diesen Wert. Da aber Java für lokale Variablen keine implizite Vorbesetzung vorsieht, dient es der Lesbarkeit, alle Variablen grundsätzlich explizit vorzubesetzen.

Während Objekte in Klassen nicht eingebettet, sondern nur referenziert werden, ist eine Variable Teil eines Klassenobjekts:

Abb.: Eingebettete Variablen und referenzierte Objekte

Ganzzahlkomponenten

Als Beispiel für eine einfache Klasse mit int-Komponenten diene ein Zähler für Verkehrszählung:

class Zaehler {
  private int zaehler = 0;
  public void fahrzeug() {
   zaehler++; }
  public int ergebnis() {
   return zaehler; } }
public class Verkehrszaehlung {
  public static void main(String[] kzp) {
   Zaehler muellerstrasse = new Zaehler();
   Zaehler bahnhofsplatz = new Zaehler();
     …
   System.out.println(muellerstrasse.ergebnis() + " Fahrzeuge in der Müllerstraße");
   System.out.println(bahnhofsplatz.ergebnis() + " Fahrzeuge am Bahnhofsplatz"); } }

An der Stelle der drei Punkte steht nun ein ereignisgesteuertes Programmstück, das beim Drücken eines Knopfs des Verkehrszählers in der Müllerstraße bzw. am Bahnhofsplatz die Methode fahrzeug des entsprechenden Zaehler-Objekts aufruft. Für weitere Verkehrszähler an anderen Orten können weitere Objekte der Klasse Zaehler ausgeprägt werden.

Typsicherheit durch Hüllenklassen

Während in der konventionellen Programmierung (wie auch in C oder C++ XE "C++"  üblich) der Typ int für Daten der verschiedenen Art verwendet wird, wird empfohlen, statt dessen Hüllenklassen – z.B. Unterklassen von lehrbuch.Int – zu verwenden. Ein Grund hierfür ist, dass der Compiler dann viele Programmierfehler entdecken kann, die sonst als logische Fehler schwer zu lokalisieren wären. Unterklassen haben dieselben Operationen, sind jedoch untereinander nicht kompatibel.

Angenommen, man definiert zwei Variablen

int t = 41; // Temperatur in Celsius
int s = 41; // Schuhgröße

mit den nicht allzu aussagekräftigen Namen t und s, würde die Fehlerhaftigkeit der Zuweisung

t = s; // logischer Typfehler, Compiler und Interpreter entdecken ihn nicht

nicht auf den ersten Blick auffallen. Werden demgegenüber geeignete Unterklassen

class Temperatur extends lehrbuch.Int { }
class Schuhgroesse extends lehrbuch.Int { }
Temperatur t = new Temperatur();
Schuhgroesse s = new Schuhgroesse();

für die Objekte t und s vereinbart, entdeckt der Compiler einen solchen Gedankensprung. Eine konsequent strenge Typisierung der Datenbehälter verhindert frühzeitig die kostspielige Fehlersuche.

Ähnlich wird ein falscher Parameter vom Compiler als fehlerhaft abgelehnt: Wenn eine Funktion mit

public Temperatur waerme(Schuhgroesse schuh);

vereinbart wurde, dann ist der Aufruf

Öt = waerme(t); // Typfehler

fehlerhaft.

Die im Kapitel 4.6.3. eingeführte explizite Konvertierung kann aber auch für Hüllenklassen verwendet werden. Hier müssen wir doppelt konvertieren: zuerst zur Originalklasse, dann zur gewünschten Parallelklasse:

t = (Temperatur)(lehrbuch.Int)s; // kein Typfehler
t = waerme((Schuhgroesse)(lehrbuch.Int)t); // kein Typfehler

Die Standard-Hüllenklasse java.lang.Integer kann nicht in dieser Weise benutzt werden, weil sie als final – d.h. nicht erweiterbar – vereinbart worden ist, wohl aber die vom Paket java.math exportierte Klasse BigInteger mit den Methoden add, negate XE (für Subtraktion), multiply, divide, mod und rem  (für Divisionsreste), pow  (für Potenzierung) usw. Ab der Java-Version 1.3 steht die Klasse BigInteger als plattformunabhängiges Java-Programm (also nicht nur als „native Code“) zur Verfügung.

Sicheres Rechnen mit Hüllenklassen

Das folgende Programm (in Anlehnung auf [Gr]) illustriert die verborgenen Gefahren der Gleitpunktarithmetik, die in der weitgehend unbeachteten Theorie der numerischen Mathematik analysiert werden. Hier werden in einer Schleife zwei gleichwertige Formeln wiederholt berechnet:

static void fehlerkulminierung(
  // Zwei mathematisch gleichwertige Formeln werden wiederholt berechnet:
  // x = (1+r)*x - r*x*x = x + r*x*(1-x); ihr Quotient sollte daher immer 1 sein.
  // Durch die Ungenauigkeit des Rechners ergibt sich aber eine Abweichung.
    double aufhoeren, // aufhören, wenn Abweichung größer
    double r, // Formelkonstante
    double x0) { // Anfangswert
  double x = x0, y = x0, quotient = 0.0; int zaehler = 0;
  while (quotient < aufhoeren) {
   zaehler ++;
     x = (1 + r) * x - r * x * x;
     y = y + r * y * (1 - y);
   quotient = x / y; // sollte immer 1.0 sein
   System.out.println (zaehler + ": " + quotient); } } // 1.0 ?
   // Block wird wiederholt, solange quotient < aufhoeren

In die Variablen x und y werden also in den markierten Zeilen Werte aus zwei gleichwertigen Formeln zugewiesen, daher sollte ihr quotient immer 1.0 sein. So mag es erstaunlich sein, dass es im Schritt 57 schon 14 beträgt, im Schritt 287 die Grenze zu 100, im Schritt 422 sogar 1000 überschreitet. Die Ursache ist die Ungenauigkeit der Brucharithmetik; die Fehler werden kumuliert. Auch der Typ double garantiert also keine ausreichende Genauigkeit, um Kraftfahrzeuge automatisch zu steuern.


© APSIS GmbH extern.gif (1249 Byte), Polling, 2008