Wie sind in Java numerische Datentypen strukturiert?

Ein Zahlenwert wird in Java durch einen numerischer Datentyp repräsentiert. Da Computer Rechenmaschinen sind, arbeiten sie hauptsächlich mit dieser Art von Werten. Es ist also sinnvoll, hierauf einmal einen genaueren Blick zu werfen.

Numerische Datentypen unterscheiden sich in Java im Wesentlichen in dreierlei Hinsicht:

Bis auf char sind in Java alle primitiven numerischen Datentypen vorzeichenbehaftet. Einen Überblick gibt Tabelle 1. Gemeinsam mit dem nichtnumerischen Wahrheitsdatentyp boolean werden die numerischen Datentypen auch als primitive Datentypen bezeichnet. Sie sind unveränderlich in Java fest eingebettet und ihre Bezeichner werden grundsätzlich klein geschrieben.

Der Gültigkeitsbereich, die Größe, eines Datentyps bestimmt die minimalen und maximalen Werte, die er aufnehmen kann. Sie sind direkt vom Speicherbedarf des jeweiligen Typs abhängig.
Computer rechnen nicht mit Dezimal-, sondern mit Binärzahlen. Intern werden somit alle eingegebenen Dezimalzahlen in ihre binäre Repräsentanz umgewandelt, sodass es üblich ist, die Größe des Datentyps durch die Anzahl seiner Binärstellen anzugeben. Diese ist für einen primitiven Datentyp konstant und unveränderlich. Ist ein Wert kleiner als die maximale Größe des jeweiligen Typs, so werden seine Binärstellen von links nach rechts mit Nullen aufgefüllt. Ist er größer, so kann er im entsprechenden Datentyp nicht vollständig gespeichert werden.

Typ Vorzeichen Größe Wertebereich
byte ja 8 bit -27 bis 27 - 1 (-128...127)
short ja 16 bit -215 bis 215 - 1 (-32768...32767)
int ja 32 bit -231 bis 231 - 1 (-2147483648...2147483647)
long ja 64 bit -263 bis 263 - 1 (-9223372036854775808...9223372036854775807)
char nein 16 bit 16-Bit Unicode Zeichen (0x0000...0xffff (6553510))
float ja 32 bit
V: 1 bit
E: 8 bit
M: 23 bit
-3.40282347 *1038 bis 3.40282347 *1038
double ja 64 bit
V: 1 bit
E: 11 bit
M: 52 bit
-1.79769313486231570 *10308 bis 1.79769313486231570 *10308

Ganzzahlige Werte

Das an der höchsten Stelle stehende bit einer Binärzahl wird als msb (most sigificant bit) bezeichnet. Bei vorzeichenbehafteten Typen wird dieses msb, gemäß dem Zweierkomplement, mit dem Vorzeichen belegt. Eine 1 an dieser Stelle weist einen negativen Wert aus, eine 0 einen positiven. Die nachfolgenden Stellen werden von den Binärstellen des (positiven) Betrags belegt.

Hieraus ergeben sich zwei Phänomene: Die tatsächliche Größe z.B. eines 8-bit-Wertes kann im positiven Bereich lediglich 7 bit betragen und ein vollständig mit '0' aufgefüllter Wertebereich stellt abhängig vom Vorzeichenbit keinesfalls unbedingt den Zahlenwert '0' dar.
Sehen wir uns den Datentyp byte als Beispiel an. Er trägt seinen Namen, weil er eine Länge von einem Byte , also 8 bit besitzt. Dies entspricht einer achtstelligen Binärzahl, deren Wert eigentlich dezimal von 0 bis 255 (28 Werte) reichen sollte. Man würde vermuten, dass der maximale Wert eines byte somit dezimal gleich 255 beträgt. Das ist jedoch nicht der Fall. Dezimal 255 entspricht binär einer Folge von 8 Einsen, die jedoch wegen des Vorzeichenbits nicht vollständig zur Speicherung des Betrags zur Verfügung stehen.
Tatsächlich deckt der Datentyp byte in Java nur den Wertebereich von -128 bis +127 ab, da die Position des Vorzeichens für die Berechnung des Wertes nur im negativen Bereich - wenn eine 1 als msb steht - zur Verfügung steht.

Der Wert +127 stellt binär geschrieben eine Folge von sieben Einsen dar. Die vorderste, achte Stelle, wird in diesem Fall mit 0 (+) belegt. Wird diese jedoch mit 1 (-) belegt, so repräsentiert diese achtstellige Binärzahl dann nicht den dezimalen Wert 255, wie es die rechnerische Umwandlung der achtstelligen Binärzahl vermuten ließe, sondern den Wert -1, da zur Berechnung des Betrages die rechten sieben Einsen (+127) zu -128 (die vordere 1 gefolgt von 7 Nullen) addiert werden. Im negativen Bereich findet also eine Verrechnung des negativen Maximalwertes (führende Eins gefolgt von Nullen) mit dem positiven Betrag des Zahlenwertes (ohne msb) statt.

Was aber ergibt sich, bei einer Folge von Nullen? Sind alle acht Stellen mit 0 belegt, so stellt diese Binärzahl den Wert 0 dar, genau genommen +0, da die erste Stelle ja das Vorzeichen bestimmt. Ist diese jedoch mit 1 belegt, wird der Wert negativ. In diesem Fall wird die Vorzeichen-Eins jedoch zur Darstellung des Wertes mit herangezogen (-128). Es ergibt sich somit ein Betrag von 128. Die Vorzeichen-Eins wird also zusätzlich zur Definition des Vorzeichens mit zur Berechnung des Betrags herangezogen (-128 + 0). Der minimale Wert eines byte beträgt somit -128. Wird nun das kleinste, rechte bit mit einer 1 belegt, so wird dieser Betrag zu -128 addiert. Es ergibt sich dann -127. Hier zeigt sich zweierlei:

  1. Der Wert 0 kann nicht positiv oder negativ sein, er wird immer dem positiven Bereich zugeordnet.
  2. Das Wissen um den Datentyp ist immens wichtig zur korrekten Interpretation des binären Zahlenwertes, da z.B. ein 16-bit-Wert durchaus die 8-stellige Bitfolge eines byte beinhalten kann, die dann aber natürlich einen anderen Wert darstellen kann, da das msb des 16-bit-Wertes gar nicht mit einfließt.

Gleitkommawerte

Die Darstellung von Gleitkommazahlen gestaltet sich etwas komplizierter. Die Grundlage dieser Notation entspricht im Prinzip der Exponentialschreibweise eines Gleitkommawertes, wie er in dezimaler Form z.B. in den Naturwissenschaften allgemein üblich ist, besonders um sehr große oder sehr kleine Zahlen recht kurz darstellen zu können. Durch Anpassung des Exponenten kann das Komma hierbei unter Beibehaltung des Wertes durch die Zahl gleiten.

Computer machen sich dieses Prinzip zu Nutze und speichern, allerdings natürlich in dualer Form, Vorzeichen, Exponent und Mantisse in Folge. Ein 32-bit-Wert mit einfacher Genauigkeit, der in Java einem float entspricht, wird von links nach rechts durch ein Vorzeichenbit, 8 Exponentenbits und 23 Mantissenbits dargestellt. Das Vorzeichen bezieht sich auf die Mantisse und ist gleich 1, wenn die Mantisse negativ ist, ansonsten 0.

4,7 = 0,47 * 101 = 0,047 * 102 = 0,0047 * 103 =...

Nehmen wir an, die positive dezimale Zahl 4,7 soll als binäre Gleitkommazahl dargestellt werden. Hierzu muss als erstes die Mantisse der Exponentialschreibweise ermittelt und diese in die binäre Schreibweise umgewandelt werden. Der Vorkommabetrag (4) und der Nachkommabetrag (0,7), werden zunächst getrennt in das duale Zahlensystem umgerechnet und dann gemeinsam als Kommazahl geschrieben:

Vorkomma:    4  100
Nachkomma: 0,7 1011001...
4,7             100,1011001...

Die Zahlendarstellung muss danach normalisiert werden. Dies bedeutet, dass das Komma so lange nach links verschoben wird, bis nur noch eine Stelle ungleich 0 vor dem Komma steht. Diese Stelle ist immer eine 1. Sie muss deshalb nicht extra gespeichert werden und wird später nicht in die Darstellung der Mantisse mit aufgenommen (hidden bit). Der Exponent muss bei der Verschiebung natürlich je nach Anzahl der verschobenen Stellen angepasst werden.

Normalisierung: 100,1011001...  1,001011001...
hidden bit entfernt: 1,001011001...    001011001...
Mantisse vollständig (23-stellig): 00101100110011001100110

Die Anzahl der Stellen, um die das Komma nach links verschoben wurde, stellt die Grundlage für die Berechnung des Exponenten dar.
Um diesen auch negativ fassen zu können, kommt ein Verschiebeverfahren zum Einsatz, dessen Kern der sog. Bias ist. Er stellt eine Konstante dar, die auf der Anzahl der Exponentenstellen des Datentyps basiert. Beim thematisierten 32 bit langen float beträgt er 127.
Der dezimale Bias wird zur dezimalen Anzahl der Stellen, um die das Komma verschoben wurde addiert und der resultierende Betrag in eine Dualzahl gewandelt. Diese stellt den Exponenten dar.

Verschiebung + Bias:   2 + 127 = 129
Umrechnung nach binär: 129  10000001

Als vollständige Darstellung der binären Ziffernfolge des dezimalen Wertes 4,7 ergibt sich:

4,710  0 10000001 00101100110011001100110

Zur Vertiefung des Themas sei auf die sehr verständliche Darstellung des IEEE Gleitkommaformates unter http://www.iti.fh-flensburg.de/lang/informatik/ieee-format.htm und die Wikipedia-Artikel http://de.wikipedia.org/wiki/Gleitkommazahl und http://de.wikipedia.org/wiki/IEEE_754 verwiesen.

Die Arbeit mit numerischen Datentypen

Formen numerischer Literale

Ganzzahlige numerische Literale können auf verschiedene Arten und Weisen formuliert werden. Die gängigste Form ist sicherlich die dezimal Darstellung, die durch Angabe der Ziffern 0 bis 9 erfolgt.

System.out.println(12345); // 12345

Darüber hinaus können aber auch oktale, hexadezimale und ab Java 7.0 auch binäre Angaben erfolgen.
Hierzu stehen die folgenden Präfixe zur Verfügung, die den zulässigen Zeichen vorangestellt werden:

Zahlensystem Präfix gültige Zeichen
oktal 0 0...7
hexadezimal 0x oder 0X 0...9, a...f bzw. A...F
binär 0b oder 0B 0, 1

Durch die Angabe des Präfix '0' für Oktalzahlen ergibt sich, dass in Java Zahlen nicht einfach nach links durch Nullen aufgefüllt werden dürfen, da sonst merkwürdige Effekte zu erwarten sind.

System.out.println(012345); // 30071 (oktal)
System.out.println(0x12345); // 3039 (hexadezimal)
System.out.println(0b10011); // 19 (binär)

Gleitkommawerte werden in Java in amerikanischer Schreibweise mit einem Punkt als Dezimaltrenner angegeben und können darüber hinaus auch in wissenschaftlicher Notation formuliert werden:

System.out.println(3.14159); // 3,14159
System.out.println(0.314159E1); // 3,14159

Ebenfalls ab Java 7.0 dürfen zur Verbesserung der Lesbarkeit großer Zahlen auch Unterstriche verwendet werden. Allerdings darf die Angabe nicht missversändlich sein. So dürfen die Unterstriche nicht vor oder nach Dezimaltrennern oder am Anfang oder am Ende einer Zahl stehen.

int i = 1_2_3_4_5678;		// 12345678
    i = 0b1001_0010_0100_1011;	// 37451
double d = 1234_5678;		// 1.2345678E7
       d = 123_456.7;		// 123456.7
       d = 123_456_.7;		// Fehler
       d = 123_456._7;		// Fehler
       d = _123_456.7;		// Fehler
       d = 123_456.7_;		// Fehler

Einfache Punkt- und Strich-Rechenoperationen gestalten sich in Java auf die gleiche Art und Weise wie aus der Mathematik bekannt. Interessant wird es, wenn verschiedene Datentypen ineinander umgewandelt werden müssen (casting). Man spricht von implizitem casting, wenn die Typwandlung automatisch ohne weitere Angaben erfolgt und von explizitem casting, wenn die Wandlung durch die geklammerte Voranstellung des Zieltyps erfolgt. Als Grundregel gilt hier: Kleinere Typen können verlustfrei in größere gewandelt werden, bei der Umwandlung von größeren in kleinere kann es jedoch zu Datenverlust kommen, da die Binärrepräsentation des Wertes u.U. am oberen (linken) Ende beschnitten wird und es so zu auf den ersten Blick unvorhersehbaren Ergebnissen kommen kann.

 long l = 123456789012345L;
  int i = l;         // Exception
      i = (int)l;    // -2045911175
    
short s = 461;       // 111001101
 byte b = (byte) s;  //  11001101 -> -51 da -128 (10000000) + 77 (1001101)

Bei Berechnungen muss entsprechend besonders auf die Überschreitung des Wertebereiches geachtet werden:

int i = 123456789;
    i *= i;	//-1757895751

Etwas unübersichtlicher können sich Berechnungen mit Gleitkommazahlen gestalten. Die folgenden Beispiele verdeutlichen das Verhalten.
Zeile [1] zeigt, dass bei der Verwendung von ganzzahligen Datentypen in Ausdrücken, die zu nicht ganzzahligen Werten führen, die Nachkommastellen ersatzlos abgeschnitten werden. Achtung, dies ist keine Rundung, sondern ein echter Wegfall der Stellen.
Das Ergebnis der Berechnung aus Zeile [3] weist zwar den Punkt als Dezimaltrennzeichen auf, lässt jedoch die Dezimalwerte der korrekten Berechnung vermissen. Dies ist so zu erklären, dass bei der Durchführung der Rechnung intern der ausgewiesene Datentyp der beteiligten Zahlen ( int ) verwendet wird. Die Kommastellen werden also gestrichen bevor in double gecastet wird.
In den Zeilen [4] und [5] zeigt sich, dass das Casten eines der beteiligten Werte in den Gleitkommatyp ausreicht, um den gesamten Ausdruck als Gleitkommawert zu berechnen. Der Hintergrund hierfür ist, dass der größte Operandentyp (hier double mit 64 bit) den Typ bestimmt, in dem die Berechnung des Ausdrucks stattfindet. Anders in [6]: Durch die Klammerung wird wieder zunächst in int berechnet und erst im zweiten Schritt das Ergebnis zu double gewandelt.

[1]	int i = 10/3;			//3
[2]	double d;
[3]	d = 10/3;			//3.0
[4]	d = (double)10/3;		//3.3333333333333335
[5]	d = 10/(double)3;		//3.3333333333333335
[6]	d = (double)(10/3);		//3.0

Der Typ char

Genau genommen handelt es sich bei char gar nicht um einen numerischen Datentyp im engeren Sinne. Er repräsentiert vielmehr ein 16bit Unicode-Zeichen und wird beim Ausdruck als solches ausgegeben. Intern wird er jedoch als int behandelt und kann entsprechnd umgewandelt werden. Allerdings muss man ggf. auf eigenartiges Verhalten bei Überläufen gefasst sein. Das zeigt sich u.a. darin, dass ein Zuweisen und Wandeln eines zu großen Wertes nicht mit einer Exception beantwortet wird, sondern mit unstimmigen Werten.

 char c = 65535;      // 2^16
 byte b = (byte)c;    // -1
short s = (short)c;   // -1
  int i = c;          // 65535

Darüber hinaus lässt sich ein char aus diesem Grund auch nicht einfach durch Addition oder Subtraktion verändern. Das Ergebnis ist in einem solchen Fall ein numerischer int-Wert, der sich wegen der geringeren Größe des char wiederum nicht einfach casten lässt. Erreichbar ist ein solches Vorhaben durch entsprechendes Casting nach der Berechnung:

char c = 68;                // D;
     c = c+1;               // Exception
     c = (char)(c+1);       // E
     c = (char)c+(char)1;   // Exception
 int i = (int)(c+1);        // 69
     i = (int)c+1;          // 69