Zahlen im Rechner
Ein Rechner kennt nur Bits. Wie daraus ganze Zahlen werden, warum negative Zahlen im Zweierkomplement so elegant sind, und was in den 32 Bits eines float wirklich steht – bis hin zur Frage, warum 0,1 nie genau 0,1 ist.
Im Rechner ist jede Zahl eine Folge von Bits, und ihre Bedeutung hängt allein davon ab, wie man sie liest. Ganze Zahlen stehen im Binärsystem, negative im Zweierkomplement: Das höchste Bit zählt negativ. Damit rechnet derselbe Addierer für positive und negative Zahlen – und eine Zahl, die über den Rand läuft, springt ohne Warnung auf die andere Seite.
Kommazahlen speichert man als Gleitkommazahl nach IEEE 754: Vorzeichen, Exponent und Mantisse, wie die wissenschaftliche Schreibweise 6,02 · 10²³, nur zur Basis 2. Dadurch reicht der Bereich von winzig bis riesig, aber die Genauigkeit ist begrenzt – und viele Dezimalbrüche wie 0,1 lassen sich binär gar nicht exakt darstellen.
Stellenwertsysteme
Die Zahl 2026 bedeutet 2·1000 + 0·100 + 2·10 + 6·1 – jede Stelle zählt zehnmal so viel wie die rechts von ihr. Im Binärsystem zählt jede Stelle doppelt so viel: 1, 2, 4, 8, 16 … Das Hexadezimalsystem zur Basis 16 ist nur eine Kurzschrift dafür: Eine Hex-Ziffer (0–9, A–F) steht für genau vier Bits. Klick Bits an oder gib eine Zahl ein:
Warum binär? Weil sich zwei Zustände physikalisch am zuverlässigsten unterscheiden lassen: Spannung hoch oder niedrig. Und warum Hex? Weil 0xBEEF leichter zu lesen ist als 1011111011101111 und man trotzdem jedes Bit sieht, wenn man vier Bits pro Ziffer im Kopf hat. Oktal (Basis 8, drei Bits pro Ziffer) ist heute fast nur noch bei Unix-Dateirechten wie chmod 755 zu sehen.
Negative Zahlen: das Zweierkomplement
Die naheliegende Idee – ein Bit fürs Vorzeichen, der Rest für den Betrag – hat zwei Nachteile: Es gäbe +0 und −0, und Addieren bräuchte Sonderfälle. Das Zweierkomplement löst beides: Bei n Bits zählt das höchste Bit nicht +2n−1, sondern −2n−1. Bei 8 Bits ist 1000 0000 also −128 und 1111 1111 ist −128 + 127 = −1. Negieren heißt: alle Bits umdrehen und 1 addieren.
Der Trick dahinter: Es wird einfach modulo 2n gerechnet, wie auf einem Zifferblatt. Deshalb funktioniert derselbe Addierer für alles. Addiere zwei 8-Bit-Zahlen:
Prozessoren merken sich beides in Statusbits: das Carry-Flag für Überlauf ohne Vorzeichen und das Overflow-Flag für Überlauf mit Vorzeichen. Java wertet sie bei int und long nicht aus – die Rechnung läuft stillschweigend modulo 2³² bzw. 2⁶⁴ weiter.
Gleitkommazahlen nach IEEE 754
Für Kommazahlen schreibt man die Zahl wie in der wissenschaftlichen Notation: ±1,m · 2e. Ein float hat 32 Bits: 1 für das Vorzeichen, 8 für den Exponenten (mit einem Versatz von 127 gespeichert, damit auch negative Exponenten ohne Vorzeichen auskommen) und 23 für die Nachkommastellen der Mantisse. Die führende 1 vor dem Komma wird gar nicht gespeichert – sie ist bei normalen Zahlen immer da. Ein double funktioniert genauso mit 1 + 11 + 52 Bits.
Gleitkommazahlen sind also nicht gleichmäßig verteilt: Zwischen 1 und 2 liegen genauso viele floats wie zwischen 1024 und 2048 – oben ist der Abstand entsprechend 1024-mal größer. Diesen Abstand zur nächsten darstellbaren Zahl nennt man ulp (unit in the last place). Beim double gibt es rund 16 gültige Dezimalstellen, beim float nur etwa 7. Ab 2²⁴ = 16 777 216 kann ein float nicht mehr jede ganze Zahl darstellen, ein double ab 2⁵³.
Dazu kommen Sonderwerte: ±Unendlich (für 1.0/0), NaN (für 0.0/0; NaN ist nicht einmal sich selbst gleich), −0 und die subnormalen Zahlen knapp über null, bei denen die führende 1 wegfällt, damit der Übergang zur Null weich wird.
Für Entwickler übersetzt
| Typ | Bits | Bereich | Bemerkung |
|---|---|---|---|
byte | 8 | −128 … 127 | mit Vorzeichen – (byte) 200 ist −56 |
short | 16 | −32 768 … 32 767 | |
char | 16 | 0 … 65 535 | der einzige Ganzzahltyp ohne Vorzeichen |
int | 32 | etwa ±2,1 Milliarden | Standard für Ganzzahlen |
long | 64 | etwa ±9,2 · 10¹⁸ | Literal mit L |
float | 32 | bis etwa 3,4 · 10³⁸ | etwa 7 Dezimalstellen |
double | 64 | bis etwa 1,8 · 10³⁰⁸ | etwa 16 Dezimalstellen – Standard für Kommazahlen |
Integer.toBinaryString(2026); // "11111101010"
Integer.toHexString(-1); // "ffffffff"
Integer.parseInt("BEEF", 16); // 48879
int x = 0b1011_0000; // Binärliteral mit Trennstrich
Math.addExact(Integer.MAX_VALUE, 1); // wirft ArithmeticException statt still überzulaufen
-8 >> 1; // -4 (arithmetisch: Vorzeichen bleibt)
-8 >>> 1; // 2147483644 (logisch: Nullen von links)
Integer.toUnsignedString(-1); // "4294967295" – dieselben Bits ohne Vorzeichen gelesen
b & 0xFF; // byte als 0..255 lesen
Float.floatToIntBits(0.1f); // 1036831949 = 0x3DCCCCCD – die Bits aus dem Beispiel
Math.ulp(1.0); // 2.220446049250313E-16
Double.NaN == Double.NaN; // false – deshalb Double.isNaN(x)
new BigDecimal(0.1); // 0.1000000000000000055511151231257827…
new BigDecimal("0.1"); // genau 0.1 – für Geld immer aus StringsFür Geldbeträge gilt deshalb: BigDecimal (aus Strings erzeugt) oder ganze Cent als long, nie double. Und Gleitkommazahlen vergleicht man nicht mit ==, sondern mit einer Toleranz.
Häufige Irrtümer
int und long nicht: Die Zahl springt stumm auf die andere Seite. Nur Math.addExact und Co. werfen eine Exception.Math.abs(Integer.MIN_VALUE) ist wieder Integer.MIN_VALUE, weil +2 147 483 648 in einem int keinen Platz hat.