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

Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release)
Buch: Java ist auch eine Insel
gp Kapitel 2 Sprachbeschreibung
  gp 2.1 Anweisungen und Programme
  gp 2.2 Elemente der Programmiersprache Java
    gp 2.2.1 Textkodierung durch Unicode-Zeichen
    gp 2.2.2 Unicode-Tabellen unter Windows
    gp 2.2.3 Literale
    gp 2.2.4 Bezeichner
    gp 2.2.5 Reservierte Schlüsselwörter
    gp 2.2.6 Token
    gp 2.2.7 Semantik
    gp 2.2.8 Kommentare
    gp 2.2.9 Funktionsaufrufe als Anweisungen
    gp 2.2.10 Die leere Anweisung
    gp 2.2.11 Der Block
  gp 2.3 Datentypen
    gp 2.3.1 Primitive Datentypen
    gp 2.3.2 Wahrheitswerte
    gp 2.3.3 Variablendeklarationen
    gp 2.3.4 Ganzzahlige Datentypen
    gp 2.3.5 Die Fließkommazahlen
    gp 2.3.6 Alphanumerische Zeichen
    gp 2.3.7 Die Typanpassung (das Casting)
    gp 2.3.8 Lokale Variablen, Blöcke und Sichtbarkeit
    gp 2.3.9 Initialisierung von lokalen Variablen
  gp 2.4 Ausdrücke, Operanden und Operatoren
    gp 2.4.1 Zuweisungsoperator
    gp 2.4.2 Verbundoperatoren
    gp 2.4.3 Präfix- oder Postfix-Inkrement und -Dekrement
    gp 2.4.4 Unäres Minus und Plus
    gp 2.4.5 Arithmetische Operatoren
    gp 2.4.6 Die relationalen Operatoren
    gp 2.4.7 Logische Operatoren
    gp 2.4.8 Reihenfolge und Rang der Operatoren in der Auswertungsreihenfolge
    gp 2.4.9 Überladenes Plus für Strings
    gp 2.4.10 Was C(++)-Programmierer vermissen könnten
  gp 2.5 Bedingte Anweisungen oder Fallunterscheidungen
    gp 2.5.1 Die if-Anweisung
    gp 2.5.2 Die Alternative wählen mit einer if/else-Anweisung
    gp 2.5.3 Die switch-Anweisung bietet die Alternative
  gp 2.6 Schleifen
    gp 2.6.1 Die while-Schleife
    gp 2.6.2 Schleifenbedingungen und Vergleiche mit ==
    gp 2.6.3 Die do/while-Schleife
    gp 2.6.4 Die for-Schleife
    gp 2.6.5 Ausbruch planen mit break und Wiedereinstieg mit continue
    gp 2.6.6 break und continue mit Sprungmarken
  gp 2.7 Methoden einer Klasse
    gp 2.7.1 Bestandteil einer Funktion
    gp 2.7.2 Aufruf
    gp 2.7.3 Methoden ohne Parameter
    gp 2.7.4 Statische Funktionen (Klassenmethoden)
    gp 2.7.5 Parameter, Argument und Wertübergabe
    gp 2.7.6 Methoden vorzeitig mit return beenden
    gp 2.7.7 Nicht erreichbarer Quellcode bei Funktionen
    gp 2.7.8 Rückgabewerte
    gp 2.7.9 Methoden überladen
    gp 2.7.10 Vorgegebener Wert für nicht aufgeführte Argumente
    gp 2.7.11 Finale lokale Variablen
    gp 2.7.12 Rekursive Funktionen
    gp 2.7.13 Die Ackermann-Funktion
    gp 2.7.14 Die Türme von Hanoi
  gp 2.8 Dokumentationskommentare
    gp 2.8.1 Ein Dokumentationskommentar setzen
    gp 2.8.2 Mit javadoc eine Dokumentation erstellen
    gp 2.8.3 Generierte Dateien
    gp 2.8.4 Weitere Dokumentationskommentare
    gp 2.8.5 Schalter für das Programm javadoc
    gp 2.8.6 JavaDoc und Doclets
    gp 2.8.7 Bitoperationen
    gp 2.8.8 Vorzeichenlose Bytes in ein Integer und Char konvertieren
    gp 2.8.9 Variablen mit Xor vertauschen
    gp 2.8.10 Die Verschiebeoperatoren
    gp 2.8.11 Setzen, Löschen, Umdrehen und Testen von Bits
    gp 2.8.12 Der Bedingungsoperator
  gp 2.9 Einfache Benutzereingaben


Galileo Computing

2.3 Datentypedowntop

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 einfache Datentypen sind: Ganzzahlen, Fließkommazahlen, Wahrheitswerte, Zeichen. Zu den einfachen Datentypen gesellen sich die komplexen Datentypen Array und Objekte. Da jede Variable einen festen Datentyp hat und diesen nicht mehr ändern kann, 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. Der Typ bestimmt also auch die zulässigen Operationen.

Die grobe Richtung

Die Datentypen in Java zerfallen in zwei Kategorien: primitive Typen und Referenztypen (auch Klassentypen). Die einfachen 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 in der Fußnote angesprochene Smalltalk verwiesen.

Warum Sun sich für diese Teilung entschieden hat, lässt sich mit 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++ hilft, sich auf der Insel zurechtzufinden.
gp  Der andere Grund ist, dass häufig vorkommende elementare Rechenoperationen schnell durchgeführt werden müssen und bei einem einfachen Typ leicht Optimierungen durchzuführen sind.

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.3.1 Primitive Datentypen  downtop

In Java gibt es eingebaute Datentypen für ganze Zahlen, Gleitkommazahlen nach IEEE 754, Zeichen und 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/Typ 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

Abbildung
Hier klicken, um das Bild zu Vergrößern

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 sich bei einigen Hochsprachen die Länge eines Datentyps ändern kann, besteht in Java nicht. In den Sprachen 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ßkommazahlen sowieso. Dies ist leider nicht immer praktisch, aber wir müssen stets daran denken. Probleme gibt es, wenn wir einem Byte zum Beispiel den Wert 240 zuweisen wollen, denn der Wertebereich ist –128 bis 127. Ein char ist im Prinzip ein vorzeichenloser Ganzzahltyp.

Galileo Computing

2.3.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, insbesondere werden numerische Werte nicht als Wahrheitsweite interpretiert.

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


Galileo Computing

2.3.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;

Der Typname ist entweder ein einfacher Typ (wie int) oder ein Referenztyp. Viel schwieriger ist eine Definition nicht – kryptische Definitionen wir in C gibt es in Java nicht. 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 zu Verwirrung. Auch 0 und O und 1 und l sind leicht zu verwechseln. Als Programmierer sollten wir uns konsistent an ein Namensschema halten. Auch sollten wir korrekt schreiben und auf Rechtschreibfehler achten, leicht wird aus nessesaryConnection nesesarryConnection. Extra verwirrende Bezeichner sind zu vermeiden. Gültig – aber böse – ist etwa:

int ínt, ìnt, înt;
boolean bôõleañ;
Die Kombination ‚rn’ ist auch schwer zu lesen und je nach Zeichensatz leicht mit ‚m’ zu verwechseln. Abstrakte Bezeichner sind zu vermeiden. Eine gemeine Idee ist auch folgende:

boolean FALSE = true;
boolean TRUE = false;
Im Programmcode wird dann mit FALSE und TRUE gearbeitet. Einer der obersten Plätze bei den verpfuschtesten Java-Programmen ist uns sicher …

Da in Java alle Variablen einen Typ besitzen, heißt die Variablendeklaration auch Typdefinition. Sie ist eine Anweisung und wird daher mit einem Semikolon abgeschlossen.


Beispiel   Unicode-Sequenzen können vom Programmierer überall im Programm aufgenommen werden. Folgende Deklarationen mit den Bezeichnernamen sind daher gleich:

double übelkübel;
double \u00FCbelk\u00FCbel;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Ist ein Bezeichnername unglücklich gewählt, so lässt er sich ohne Probleme konsistent umbenennen. Dazu ist im Menü Refactor, Rename – oder auch kurz (Alt)+(ª)+(R) – auszuwählen; der Cursor muss auf dem Bezeichner stehen. Ein optionaler Preview zeigt an, welche Änderungen die Umbenennung nach sich ziehen wird.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 2.2   Definition mehrerer Variablen gleichzeitig

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 hatBesucher;
    hatBesucher = true;

    System.out.print( "Sind Personen in der Disco? " );
    System.out.println( hatBesucher );
  }
}

Die Zeile hatBesucher = true ist eine Zuweisung – und somit ein Ausdruck, da sie einen Wert liefert –, die die Variable hatBesucher mit einem Wert initialisiert. Sie ist ebenfalls eine Anweisung und wird daher mit einem Semikolon abgeschlossen. 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.3.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 definierte Länge ist eine wesentliche Eigenschaft von Java. Ganzzahlige Typen (mit der Ausnahme von char) sind in Java immer vorzeichenbehaftet; einen Modifizierer unsigned wie in C(++) gibt es nicht.


Beispiel   Variablendeklaration mit Wertinitialisierung

int quadratmeter;

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. Negative Zahlen werden durch Voranstellen eines Minuszeichens gebildet. Ein Pluszeichen für positive Zeichen ist möglich.

Das hexadezimale und oktale Zahlensystem

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.

gp  Ein oktaler Wert beginnt mit dem Präfix »0«. Mit der Basis 8 werden nur die Ziffern »0« bis »7« für oktale Werte benötigt. Der Name stammt aus dem lateinischen »octa«, was zu Deutsch »acht« heißt. Das Oktalsystem war früher eine verbreitete Darstellung, da nicht mehr einzelne Bits solo betrachtet werden mussten, sondern 3 Bits zu einer Gruppe zusammengefasst wurden. In der Kommunikationselektronik ist das Oktalsystem noch weiterhin beliebt. In Java-Programmen sollten Oktalzahlen mit Bedacht eingesetzt werden. Wer aus optischen Gründen mit der 0 eine Zahl linksbündig auffüllt, erlebt eine Überraschung.
int i = 118;
int j = 012; // Oktal!
gp  Ein hexadezimaler Wert beginnt mit »0x«. Da zehn Ziffern für 16 hexadezimale Zahlen nicht ausreichen, besteht eine Zahl zur Basis 16 zusätzlich aus den Buchstaben »a« bis »f« (beziehungsweise »A« bis »F«). Das Hexadezimalsystem heißt auch Sedezimalsystem.

Für Dualzahlen (also Binärzahlen zur Basis 2) gibt es keine Notation.


Achtung   Wer sich im kalifornischen Cupertino (unter anderem Apple-Hauptsitz) aufhält und dieses Buch liest, sollte es vermeiden, gut hörbar das Hexadezimalsystem rückwärts aufzuzählen. Das ist gesetzlich verboten!

Der Datentyp long

Ein Literal für Ganzzahlen doppelter Größe wird mit einem »l« oder »L« am Ende versehen.


Beispiel   Deklaration eines long mit angehängtem »L«

long l = 123456789098L, m =1L, n = 0xC0B0L;

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, das 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. Er 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 der Ziffer Eins besitzt. Daher sollte bei Längenangaben immer ein großes »L« hinten angestellt werden.


System.out.println( 123456789012345L );

Allerdings ist das Compilerverhalten verwirrend, denn bei folgender Anweisung findet er auch automatisch die richtige Größe:


byte b = 12;

Überläufe bei Ganzzahlen

Passt das Ergebnis einer Berechnung nicht in den Wertebereich einer Zahl, so wird dieser Fehler nicht vom System angezeigt; weder der Compiler noch die Laufzeitumgebung melden dieses Problem.

Mathematisch ergibt a * a / a = a, also etwa zum Beispiel 100.000 * 100.000 / 100.000 = 100.000. In Java ist das so nicht, da wir bei 100.000 * 100.000 einen Überlauf im int haben, und


System.out.println( 100000 * 100000 / 100000 );     // 14100

liefert 14100. Wenn wir den Datentyp auf long erhöhen, in dem wir hinter ein 100.000 ein L setzen, sind wir bei dieser Multiplikation noch sicher, da ein long das Ergebnis aufnehmen kann.


System.out.println( 100000L * 100000 / 100000 );    // 100000

Galileo Computing

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

Zur Darstellung der Fließkomma-Literale gibt es zwei Notationen: Standard und Wissenschaftlich. Egal ob Standard oder Wissenschaftlich, die Literale bestehen aus einem Vorkommateil, einem Dezimalpunkt (kein Komma) und einem Nachkommateil. Eine Fließkomma-Literal muss keine Nachkommastellen besitzen, so dass auch gültig ist:


double d = 10. + 20.;

Die Wissenschaftliche Notation ist eine Erweiterung der Standard-Notation. Es folgt hinter den Nachkommastellen ein »E« (oder »e«) mit einem Exponenten zur Basis 10. Der Exponent kann entweder positiv oder negativ sein, muss aber eine Ganzzahl sein. Die Tabelle stellt drei Beispiele zusammen:


Standard Wissenschaftlich
123450.0 1.2345E5
123450.0 1.2345E+5
0.000012345 1.2345E-5

Der Datentyp float

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;

Genauere Fließkommazahlen

Einen höher auflösenden beziehungsweise präziseren Datentyp für Gleitkommazahlen als double gibt es nicht. Sun bietet für diese Aufgabe im Paket java.math die Klasse DecimalFormat an. Das ist sinnvoll für Daten, die eine sehr gute Genauigkeit aufweisen sollen, wie zum Beispiel Währungen. Erstaunlicherweise gab es einmal in C# den Datentyp currency für ganzzahligen Währungen – MS ist Vorreiter der Initiative: ‚Weg mit den Cents’ –, doch der wurde eingestampft und war auch nur so groß wie in Java long. Dafür gibt es in C# den Datentyp decimal, der mit 14 Bytes genügend Präzision bietet. Mit 14 Byte lässt sich noch eine Zahl wie 0.0000000000000000000000000001 ausdrücken.

Intern bestehen Fließkommazahlen aus drei Teilen: Einem Vorzeichen, einem Exponenten und einer Mantisse. Während die Mantisse die Genauigkeit bestimmt, gibt der Exponent die Größenordnung der Zahl an. Das Vorzeichen kostet immer ein Bit und die Anzahl Bits für Exponent und Mantisse richtet sich nach dem Datentyp. Bei float benötigt der Exponent 8 Bit, die Mantisse 23 und beim Datentyp double 11 Bit und 52 Bit.


Galileo Computing

2.3.6 Alphanumerische Zeichen  downtop

Der alphanumerische Datentyp char (von engl. character, Zeichen) ist 2 Byte 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.3.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 (implizit) dem größeren angepasst. Der Compiler nimmt diese Anpassung selbstständig vor.
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

Die Anpassung wird im Englischen auch widening conversion genannt, weil sie den Wertebereich automatisch erweitert.


Hinweis Natürlich kann die Konvertierung von double in long nicht verlustfrei sein. Wie sollte das auch gehen? Zwar verfügt ein long als auch ein double über 64 Bit zur Datenspeicherung, aber ein double kann eine Ganzzahl nicht so effizient speichern wie ein long und hat etwas »Overhead«. Daher können bei der impliziten Konvertierung eines long in ein double einige Bits als Informationsträger herausfallen, wie es das folgende Beispiel illustriert.

long   z = 1111111111111111111L;  // 1111111111111111111

double d = z;                     // 1111111111111111170 
                                          (1.11111111111111117E18)

long   m = (long) d;              // 1111111111111111168

Explizite Typanpassung

Die explizite Anpassung engt einen Typ ein, sodass diese Operation auch narrowing conversion genannt wird. Der gewünschte Typ für eine Typanpassung wird vor den umzuwandelnden Datentyp geschrieben. Der gewollte Datentyp ist geklammert.


Beispiel   Umwandlung einer Fließkommazahl in eine Ganzzahl:

int n = (int) 3.1415;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Passt der Typ eines Ausdrucks nicht, lässt er sich mit (Strg)+(1) korrigieren.

Abbildung
Hier klicken, um das Bild zu Vergrößern

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ßkommazahlen zu 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 von short. Zeichen sind per Definition immer ohne Vorzeichen. Würde ein char mit einem gesetzten höchstwertigen letzten Bit in ein short konvertiert, käme eine negative Zahl heraus. Ebenso wäre, wenn ein short eine negative Zahl bezeichnet, 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 Vorzeichen-Bit weichen muss.

Typanpassungen von int und char

Die Methode printXXX() reagiert auf die Typen char und int, und eine Typumwandlung führt zu der gewünschten Ausgabe.


int  c1 = 65;
char c2 =A;

System.out.println( c1 );              // 65
System.out.println( (int)c2 );         // 65
System.out.println( (char)c1 );        // A
System.out.println( c2 );              // A
System.out.println( (char)(c1 + 1) );  // B
System.out.println( c2 + 1 );          // 66

Einen Ganzzahlwert in einem int können wir als Zeichen ausgeben, genauso wie eine char-Variable als Zahlenwert. Wir sollten beachten, dass eine mathematische Operation auf char-Typen zu einem int führt. Daher funktioniert für ein char c Folgendes nicht:


c = c + 1;

Richtig wäre c = (char)(c+1).

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 mit 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 schwer wiegenden 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 belief sich auf etwa 500 Millionen US-Dollar. In dem Projekt steckten zusätzlich Entwicklungskosten von etwa 7 Milliarden US-Dollar. Grund für den Absturz war ein Rundungsfehler, der durch die Umwandlung einer 64-Bit-Fließkommazahl (die horizontale Geschwindigkeit) in eine vorzeichenbehaftete 16-Bit-Ganzzahl auftrat. Die Zahl war leider größer als 215, und die Umwandlung war nicht gesichert, da die Programmierer diesen Zahlenbereich nicht angenommen hatten. 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 unbedingt für den Flug notwendig war und nur den Startvorbereitungen diente. Im Fall 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 worden war, die Ariane 5 aber schneller flog.


Galileo Computing

2.3.8 Lokale Variablen, Blöcke und Sichtbarkeidowntop

In jedem Block und auch in jeder Klasse 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), auch Gültigkeitsbereich beziehungsweise Lebensdauer genannt. Sie ist nur in dem Block lebendig, in dem sie definiert wurde. 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 nichts 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.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Soll eine Variable in ihrem lokalen Kontext umbenannt werden, so gibt es neben dem Rename auch eine andere Möglichkeit. Dazu lässt sich auf der Variablen mit (Strg)+(1) ein Popup-Fenster mit Local Rename öffnen. Der Bezeichner wird selektiert und lässt sich ändern. Gleichzeitig ändern sich alle Bezüge auf die Variable mit.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung
Hier klicken, um das Bild zu Vergrößern


Galileo Computing

2.3.9 Initialisierung von lokalen Variablen  toptop

Während Objektvariablen automatisch mit einem Nullwert initialisiert werden, geschieht dies bei lokalen Variablen nicht. Das heißt, der Programmierer muss sich selbst 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.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Ein Hinweis und Verbesserungsvorschlag, wenn eine lokale Variable nicht initialisiert ist.

Abbildung
Hier klicken, um das Bild zu Vergrößern






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

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

3   Das ist natürlich eine Anspielung auf C, in dem Deklarationen wie char (*(*a[2])())[2] möglich sind. Gut, dass es mit cdel ein Programm zum »Vorlesen« solcher Definitionen gibt.

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

5   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. Auf den iSeries Servern von IBM – früher AS/400 – gibt es auch einen Datentyp unsigned long long int.

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

7  In C# ist ein int nur 2 Byte, also ein Java-short. Und auch ein C#-long ist nur 4 Byte, was in Java lediglich ein int ist. Den Datentyp float gibt es in C# nicht, und double hat ebenfalls 8 Byte. Das gibt viel Spaß bei Konvertierungen.

8   Nicht alle aus C stammenden Escape-Sequenzen finden sich auch in Java wieder. Es gibt kein '\a' (Alert), '\v' (vertikaler Tabulator), '\?' (Ausrufezeichen) und kein '\x', was eine hexadezimale Zahl einleitet (dafür lässt sich in Java \uXXXX nutzen).

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





Copyright © Galileo Press GmbH 2004
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, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de