Wie sind in Java numerische Datentypen strukturiert?
Numerische Datentypen unterscheiden sich in Java im Wesentlichen in dreierlei Hinsicht:
- ihrem Gültigkeitsbereich
- danach ob sie ganzzahlige oder Gleitkomma-Werte sind
- danach ob sie vorzeichenbehaftet sind
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:
- Der Wert 0 kann nicht positiv oder negativ sein, er wird immer dem positiven Bereich zugeordnet.
- 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.
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:
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 Wikipedia-Artikel https://de.wikipedia.org/wiki/Gleitkommazahl und https://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
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.