Universität Hamburg - Fachbereiche - Fachbereich Mathematik

Java-Kurs (1)

Numerische Mathematik, WiSe 99/00 und SoSe 00, Bodo Werner

Zurück zum Inhaltsverzeichnis.

Zahlen in Java

In der Mathematik kennt man natürliche, ganze, rationale, reelle und komplexe Zahlen. In Java korrespondieren mit diesen sogenannte Grunddatentypen (primitive, einfache Typen), die ganzen (byte, short, int, long) und reellen (float, double) Zahlen entsprechen. Jede Variable eines solchen Datentyps beansprucht bei ihrer Allokierung (Initialisierung) einen Speicherplatz fester Größe, die in Byte gemessen wird. Dadurch kann es nur endlich viele Zahlen des jeweiligen Typs geben.
So werden 4 Byte (32 Bit) für eine int-Zahl verwendet. Daher gibt es hiervon genau 2^(32) Zahlen, nämlich die Zahlen -2^(31) bis 2^(31)-1. 8 Byte werden für eine double-Zahl verwendet. Diese werden intern als Gleitpunkt-Dualzahlen dargestellt mit 11 Bit für den Exponenten, 1 Bit für das Vorzeichen und verbleibenden 52 Bit für die Mantisse. Es gibt insgesamt 2^64 double-Zahlen, die ungleichabständig auf dem Zahlenstrahl verteilt sind. Näheres in der Vorlesung.

Das erste Beispiel-Programm

Ermittlung der größten byte-Zahl

Schlüsselwort class, die Methode main(), do-while-Schleife, logischer Operator ==, primitive Datentypen byte, int und ihre Deklaration, Kommentar //, Blöcke { }, In- und Dekrementoperatoren (++ --), Ausgabe von Zeichenketten auf den Bildschirm mit System.out.println() und Addition von Zeichenketten (und Zahlen)
Wir beginnen mit einem Beispielprogramm, besser mit einer Beispielklasse (die Ziffern vor jeder Zeile dienen nur der Orientierung; sie gehören nicht zum Programm).
 1  class bsp1{
 2  static void main(String[] args){
 3   byte b=0;
 4   int i=0;
 5   do{
 6    b++;
 7    i++;
 8    }//Ende do-Block
 9   while (i==b);
10   System.out.println("Nach Verlas-
      sen der Schleife: i="+i+" b="+b);
11   i--;
12   b--;
13   System.out.println("Vor Verlas-
      sen der Schleife: i="+i+" b="+b);
14   }//Ende main()
15  }//Ende class
Die Java-spezifischen Schlüsselworte ("Terminalsymbole") sind alle fett hervorgehoben, die besonders wichtigen sind rot. In der folgenden Erläuterung habe ich die Termini, die die Grammatik beschreiben ("Nichtterminalsymbole"), ebenfalls rot hervorgehoben.

Das Programm besteht aus einer einzigen Klasse ( class) mit Bezeichner bsp1: Es wird ausführbar durch die Methode main(), deren Programmzeile 2 hier noch nicht erläutert wird. Man spricht daher auch von einer Applikationsklasse, da nie eine Instanz dieser Klasse gebildet wird. Dieses Programm hat mit OOP nichts zu tun. Dennoch hilft es, mit den ersten Sprachelementen vertraut zu machen.

Zur Strukturierung achte man auf (ineinandergeschachtelte) Blöcke , die durch geschweifte Klammern { und } eingeschlossen werden und Anweisungen enthalten. Der größte Block ist der, der die Klasse umfasst. Dann kommt der Teilblock zur Methode main, als kleinster der "do-Block" in den Zeilen 6-7. Kommentare (in einer Zeile) werden durch zwei Slashs (//) eingeleitet.

Die Zeilen 3 und 4 sind Deklarationen der Variablen mit Namen (Bezeichner) b und i vom Typ byte und int, verbunden mit einer Wertzuweisung. Grundsätzlich muss jede Variable im Programm deklariert werden: ihr Name und ihr Typ wird hierdurch festgelegt. Die Deklaration kann, muss aber nicht mit einer Wertzuweisung (bei Objekten Allokation) verbunden werden.

In den Zeilen 4-9 steht eine do-while-Schleife, die solange ("while") durchlaufen wird, wie die Bedingung in den runden Klammern (ein Boole'scher Ausdruck) nach while erfüllt (true) ist. Sie werden noch andere Schleifen (mit while (ohne do) und for) kennenlernen. Eine do-while-Schleife zeichnet sich dadurch aus, dass der do-Block mindestens einmal durchlaufen, weil die while-Bedingung nachgestellt ist. Die Anweisungen in den Zeilen 6 und 7 erhöhen den Wert von b und i jeweils um 1 (Inkrementoperator ). Entsprechendes gilt für die Zeilen 11, 12 (Dekrementoperator).

System.out.println() in den Zeilen 10, 13 ist eine von Java gelieferte Methode, die Ausdrücke in Form von Zeichenketten (Schlüsselwort String) auf dem Bildschirm ausgibt. Dabei bewirkt die Addition von Zeichenketten (+) ein Aneinanderreihen, wobei Zahlen automatisch in Zeichenketten umgewandelt werden. Falls in Zeile 10 i=b=7 , so erscheint auf dem Bildschirm Nach Verlassen der Schleife: i=7 b=7 .

Das Ziel dieses kleinen, aber doch schon nicht ganz einfachen Programms ist die Ermittlung der größten byte-Zahl (127=2^7-1). Dazu muss man den Boole'schen Ausdruck in den runden Klammern, i==b, verstehen. Dieser ist wahr, falls i und b gleiche Werte haben (bei == handelt es sich um einen logischen Operator). Auch muss man wissen, dass b++ die kleinste (negative) byte-Zahl (-128) liefert, falls b die größte byte-Zahl ist (127++=-128). Man kann sich die 256 byte-Zahlen auf einem Kreis angeordnet denken, wobei 127 und -128 benachbart sind. Da die int-Zahlen die byte-Zahlen umfassen, wird erstmals die Gleichheit i==b verletzt, wenn i=128, b=-128 .

Schlüsselworte (Terminalsymbole) nennt man übrigens alle von Java schon vordefinierten Worte. Im obigen Programm sind dies class, static, void, main, String, byte, int, do, while, System . Die von mir "erfundenen" Worte sind Bezeichner, hier bsp1, args, b, i.

Will man dieses Programm "laufen" lassen (run), so muss man es erst in eine Text-Datei namens bsp1.java mit einem Texteditor (z.B. emacs) schreiben, abspeichern (gleicher Name wie die Klasse!), sodann durch javac bsp1.java (z.B. auch unter emacs!) compilieren , wodurch eine Datei bsp1.class erzeugt wird, und schließlich (z.B. wieder unter emacs) durch java bsp1) ausführen - das geht nur, weil es eine Methode main() gibt.

Die Klasse bsp1 enthält zwei Methoden , main() und System.out.println(), welche in runden Klammern () eingeschlossene Parameter enthalten - deshalb füge ich bei allen Methodennamen runde Klammern an.


Das zweite Beispiel-Programm

Größte double-Zahl, Maschinengenauigkeit

Kommentare über mehrere Zeilen ( /* */), der primitive Datentyp double, for-Schleife, Kurzoperatoren +=, -=, /=, *=, die Methode Math.pow des Pakets Math
/* Ziel des Programms:
   Ermittlung der groessten double-Zahl.
   Dabei wird benutzt:
   Fuer den Exponenten zur Basis 2 werden
   11 Bit verwendet, der groesste ist
   also 2^10-1. Fuer das Vorzeichen wird
   1 Bit verwendet, bleiben fuer
   die Mantisse 64-12=52 Bit (t=53).*/
 1  class bsp2{
 2  static void main(String[] args){
 3  double d=0;
 4  double x=2;
 5  for(int i=0;i<53;i++){ //for-Schleife
 6   x/=2;// x wird fortlaufend halbiert
 7   d+=x;//zu d wird fortlaufend x addiert
 8  }//Ende for
 9  d*=Math.pow(2,Math.pow(2,10)-1);
     //(1+2^(-1)+...+2^(-52))2^(2^10-1)
10  System.out.println("groesste
       double-Zahl: "+d);//1.79769...E308
11  System.out.println("Maschinen-
         genauigkeit: "+Math.pow(2,-53));
         //1.1102...E-16
12  }//Ende main
13 }//Ende class

Am Anfang steht ein sich & uuml;ber mehrere Zeilen erstreckende Kommentar, der durch /* und */ eingeschlossen wird.

Die for-Schleife in den Zeilen 5-8 wird 53 mal durchlaufen. Der Laufindex i wird bei seinem erstmaligen Auftreten in Zeile 5 vom Typ int vereinbart. Solange der Boole'sche Ausdruck i<53 wahr ist, wird i um eins erhöht (i++), x in Zeile 6 durch 2 dividiert und zu d addiert (Zeile 7). Am Ende der Schleife ist d gleich dem ersten Faktor im Kommentarteil von Zeile 9. Sie sollten die Syntaxregeln für die verschiedenen Schleifenarten ( for, while, do-while) in einem Lehrbuch studieren.

Die Deklaration des Schleifenzählers i durch int i=0; innerhalb der Schleife hat den Vorteil einer lokalen Deklaration.

Man beachte die Verwendung der Kurzoperatoren /=, +=, *= in den Zeilen 6, 7 und 9. x/=2 ist eine Kurzform von x=x/2.
Mathematische Funktionen stellt Java in der Klasse Math bereit. Eine ihrer Methoden ist die Potenzfunktion ("power") pow(). Genauer: Math.pow(x,y) berechnet x^y (Basis x, Exponent y). Siehe Zeilen 9, 11.
Statt der for-Schleife hätte es auch eine while-Schleife getan:

double x=2; double d=0; int i=0;
while(i< 53)
{
 x/=2;
 d+=x;
 i++}
Der Unterschied einer while- und for-Schleife zur do-while-Schleife ist der, dass in letzterem Fall der do-Block mindestens einmal durchlaufen wird, weil die (while-)Bedingung erst am Ende überprüft wird. Bei einer for- und while-Schleife wird die Bedingung zu Beginn getestet. Ist sie nicht erfüllt, wird nichts getan.

Auch diese Klasse ist wie das Beispiel 1 eine reine Applikationsklasse. Auch hier ist noch nichts von OOP zu spüren.


Das dritte Beispiel-Programm

Die Klasse Gleitpunktzahl

Bildung eines Objekts einer Klasse

Daten, Konstruktoren, Methoden einer Klasse, (Rückgabe-) Wert und Parameter von Methoden mit der Rückgabe return, eindimensionale Felder (z.B. byte[] m, String[] args) als Objekte mit dem Attribut length, der Operator new zur Initialisierung (Allokierung) eines Objekts, die Punktnotation zum Zugriff auf Daten und Methoden von Objekten, Zeichenkette String, das Schlüsselwort this
Das folgende Programm besteht ebenfalls aus einer (Applikations-) Klasse mit der Methode main(), die es ausführbar macht. Im Gegensatz zu seinen Vorgängern wird jedoch ein Objekt der Klasse gebildet und eine Methode dieses Objekts angewendet, d.h es kommen erste Elemente des OOP vor. Daher ist der Umfang von neuen Begriffen und Konzepten gewaltig. Aber keine Angst: in den nachfolgenden Beispielen werden diese ständig wieder aufgegriffen.

Die Systematik in der Darstellung (fett, Farbe) ist dieses Mal eine andere. Es soll mehr der Blick auf das Neue dieses Beispiels gelenkt werden.

 1 class Gleitpunktzahl{
 2 //Daten:
 3 byte[] m; //Mantisse - ein Array (Feld)
 4 byte N; //Basis
 5 int t;//Mantissenlaenge - Laenge des Feldes m
 6 int e; //Exponent
 7 //Konstruktor:
 8 Gleitpunktzahl(byte[] mm, byte NN, int tt, int ee){
 9  m=mm;
10  N=NN;
11  t=tt;
12  e=ee;
13 }//Ende Konstruktor
14 //Methode mit Wert double:
    //ohne Parameter - (), Bezeichner Dezimalwert
15 double Dezimalwert(){
16  //wandelt in Standard-Dezimalform um
17  double y=m[0];
18  for(int i=1;i<t;i++) y+=Math.pow(N,-i)*m[i];
19  y*=Math.pow(N,e);
20  return y;
21 } //Ende Methode Dezimalwert()
22 static void main(String[] args){
23  //Ziel: groesste float-Zahl
24  byte[]
   m={1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};
25  byte N=2;
26  int t=m.length; //t=24
27  int e=127; //groesster Exponent
28  Gleitpunktzahl x
     =new Gleitpunktzahl(m,N,t,e);
29  double y=x.Dezimalwert();
30  System.out.println("Groesste float-Zahl: "+y);
31     //3.402823466....E38
32 }//Ende main()
33 }//Ende class

Vor den ersten Kontakten zum OOP soll der neue Datentyp

Array (Feld)

in den Zeilen 3, 8, 17, 18, 24, 26 erklärt werden: Die Mantisse m der Länge t einer Gleitpunktzahl besteht bekanntlich aus t Ziffern zur Basis N (hier vom Typ byte), indiziert von 0 bis t-1, in Java mit m[0], m[1], ...,m[t-1] bezeichnet. Zu jedem Datentyp kann es Array's (Felder) geben: Mit byte[] m; (Zeile 3) wird ein Feld von noch nicht bekannter Länge mit Bezeichner m vom Typ byte (als Referenzvariable, hierzu später mehr) deklariert. Technisch wird ihm erst Speicherplatz zugeordnet, wenn das Feld mit gegebener Länge allokiert wird, z.B. in Zeile 24, denkbar wäre aber auch m=new byte[24]. Das Schlüsselwort new taucht stets im Zusammenhang mit der Allokierung (Initialisierung) von Objekten einer Klasse auf; gewissermaßen ist Array eine vordefinierte Klasse. Dies wird auch durch die Punktnotation in Zeile 26 (m.length) deutlich, wobei das Schlüsselwort length eine der Daten (Attribute) eines Feldes ist und die Länge des Feldes ergibt (die hätte man in Zeile 24 auch ablesen können. Zählen Sie die Einsen!).

Jetzt verstehen wir auch etwas besser (formal) den Term String[] in der Parameterliste der Methode main(): es handelt sich um ein Feld von Zeichenketten (String) mit Bezeichner args (dem eventuelle Eingaben in der Kommandozeile beim Aufrufen des Java-Compilers zugeordnet werden).

Die Klasse Gleitpunktzahl

Mit dieser Klasse, die nichts anderes als ein neuer Variablen- oder Datentyp ist, wird beabsichtigt, dass eine Instanz (Exemplar) dieser Klasse gebildet wird (hier in Zeile 28). Formal beinhaltet Zeile 28 zwei Vorgänge, die durch zwei getrennte Anweisungen
28a  Gleitpunktzahl x; //Deklaration eines Objekts namens x
28b  x=new Gleitpunktzahl(m,N,t,e); //Initialisierung (Allokierung) des Objekts x
wiedergegeben werden können. Man achte auf das wichtige Schlüsselwort new in Zeile 28b, das hier verbunden wird mit einer ganz speziellen, zur Klasse zugehörigen Methode, dem Konstruktor, der den gleichen Bezeichner wie die Klasse haben muss (Zeilen 7, 8) und keinen Rückgabewert haben darf. Nach erfolgter Anweisung 28b (Konstruktion des Objektes x) weist der Compiler der Variablen (dem Objekt) x einen Speicherplatz zu, der dem Umfang seiner in der Parameterliste aufgeführten Daten (Zeilen 2-7) und der mit ihm verbundenen Methoden (Zeilen 14-21) entspricht.

Daten der Objekte einer Klasse

Jedes Objekt einer Klasse wird durch gewisse Eigenschaften und Verhaltensweisen charakterisiert. Erstere heißen Daten oder Attribute oder auch wieder Variablen. Verschiedene Objekte einer Klasse haben i.a. verschiedene Eigenschaften vom gleichen Typ, welche durch ihre Konstruktion mittels Konstruktor festgelegt werden können. Wir wissen, dass eine positive Gleitpunktzahl (nur solche betrachten wir hier) eindeutig durch ihre Mantisse (und deren Länge), durch die Basis und einen Exponenten definiert ist. Diese vier Größen sind die Daten (eines Objekts) der Klasse Gleitpunktzahl (Zeilen 3-6). Die Verwendung der Grunddatentypen byte und int in den Zeilen 3-6 ist etwas willkürlich und soll (durch byte) Speicherplatz sparen. Auf die Daten eines Objekts wird durch die Punktnotation zugegriffen: So hätte man durch
String s=x.m[0]+".";
for (int i=1;i<x.t;i++) s+=x.m[i];
System.out.println(s);
die Mantisse ausgeben können! Diese Punktnotation kommt auch in System.out.println() vor: println ist eine Methode des Objekts out der Klasse System.

Aber Achtung: Wenn innerhalb einer Klasse auf Daten oder Methoden dieser Klasse zurückgegriffen wird, ist eine Punktnotation allenfalls nach Voranstellen von this möglich. I.a. wird auf diese ohne jede Punktnotation zugegriffen!

Diese Punktnotation kommt auch in einer eleganteren Fassung der Zeilen 7-13 unter Verwendung des Schlüsselworts this vor:

 7  //Konstruktor:
 8a Gleitpunktzahl(byte[] m, byte N, int t, int e){
 9a  this.m=m;
10a  this.N=N;
11a  this.t=t;
12a  this.e=e;
13 }//Ende Konstruktor
Um dieses zu verstehen, ist es nötig zu bemerken, dass die Namensgebung für die Parameter dieser Methode in Zeile 8a identisch ist mit der für die Daten der Klasse - im Gegensatz zu Zeile 8. Innerhalb des Methodenblocks können daher die Daten der Klasse nicht mehr mit ihrem Namen angesprochen werden: sie wurden schon intern vergeben (lokale Variablen). Um sie anzusprechen dient der Zusatz this.name in den Zeilen 9a-12a. Gut verstehen kann man dies, wenn man wartet, bis der Konstruktor in Zeile 28 aufgerufen wird. In den Zeilen 9a-12a wird dann this durch den Bezeichner x des zu konstuierenden Objekts ersetzt. So muss Zeile 9a so gelesen werden: Das Attribut x.m des Objekts x wird durch den Parameter m des Konstruktors besetzt. Verstanden?

Methoden

Verhaltensweisen von Objekten einer Klasse werden duch Methoden beschrieben. Ein Konstruktor ist eine ganz spezielle Methode. Aus mathematischer Sicht sind Methoden Funktionen, falls sie einen Rückgabewert haben.

Wir haben in den ersten beiden Beispielen die Methode main() kennengelernt, die die Ausführung erzwingt. In diesem Beispiel gibt es zusätzlich noch die spezielle Methode eines Konstruktors, aber auch - und das ist ein weiteres neues Konzept - die Methode Dezimalwert() in Zeilen 15-21. Ziel dieser Methode ist die Umwandlung der "konstruierten" Gleitpunktzahl (Zeile 28) in eine Dezimalzahl. Das Ergebnis (der Rückgabe-Wert der Methode) ist sinnvollerweise eine Variable vom Typ double, wie in Zeile 15 vereinbart wird. Der Methodenblock muss am Ende mit Hilfe von return das Ergebnis zurückgeben. Die Zeilen 17-19 leisten offenbar das Gewünschte.

Eine Methode muss keinen Rückgabeert haben - dann trägt sie den Zusatz void wie die Methode main(). Aber Achtung: Zwar hat ein Konstruktor keinen Wert, dennoch darf der Zusatz void hier nicht stehen.

Am Ende des Bezeichners einer Methode kann es eine in runden Klammern eingeschlossene Parameterliste geben. Hier gibt es eine solche bei dem Konstruktor und der Methode main(), nicht aber bei der Methode Dezimalwert(). Dennoch müssen die runden Klammern immer mitgeführt werden (Zeilen 15, 29).

Was leistet die Methode main() hier? Eine (positive) float-Zahl ist bekanntlich durch 23 Bits der Mantisse, 1 Bit des Vorzeichens und 7 Bits des Exponenten bestimmt. Der größte Exponent ist daher 127 (Zeile 27), die größte Mantisse besteht aus lauter Einsen (Zeile 24). Durch Zeile 29 wird also der Dezimalwert der größten float-Zahl ermittelt.

In den mir bekannten Lehrbüchern wird das Grundkonzept OOP an Hand nichtmathematischer Objekte wie Fahrrad, Auto, Bankkonto erklärt. In diesem Zusammenhang ist dann auch von Kapselung von Daten die Rede, ein Konzept, das ich erst später im Rahmen von Zugriffsrechten von Attributen und Methoden wie public, private behandeln werde. Auch wird im Zusammenhang mit Methoden gerne davon gesprochen, dass eine Nachricht von einem Objekt an ein anderes gesendet wird. In diesem Sprachgebrauch wird in der Methode main() an das frisch konstruierte Objekt x der Klasse Gleitpunktzahl in Zeile 29 die Nachricht gesendet, es möge doch bitte seine Methode Dezimalwert() einsetzen und ihren Wert y übermitteln.

Etwas komplexer als die Umrechnung einer Gleitpunktzahl in eine Dezimalzahl ist der umgekehrte Vorgang - die Umrechung einer Dezimalzahl in eine Gleitpunktzahl zu gegebener Basis N und Mantissenlänge t. Hier verweise ich auf die Applikationsklasse DezimalUmwandeln mit der Methode Dezimal() mit Wert Gleitpunktzahl.

Wem die Klasse Gleitpunktzahl mathematisch zu kompliziert ist, sei die Klasse DoubleFeld empfohlen, eine Klasse, die als Daten ein double-Feld x und dessen Länge und als Methode Max() mit Wert double besitzt, die die maximale Komponente des Feldes (Vektor) x berechnet. In der Applikationsklasse Applikation werden dann ein Objekt namens X vom Typ DoubleFeld zu einem künstlichen Vektor x mit Hilfe des Konstruktors und dem new-Operator "instanziiert" und X.Max() ausgegeben.

Weiter mit "Funktionen und Polynomen"