Grundlagen

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.

Lesestand

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:

16 Bits, vier Schreibweisen
Dezimal → binär: immer wieder durch 2 teilen, die Reste von unten lesen

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:

Ein 8-Bit-Addierer
a
b

Die Regler stellen das Bitmuster ein. Dieselben Bits werden einmal ohne Vorzeichen (0 bis 255) und einmal im Zweierkomplement (−128 bis 127) gelesen.

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.

Was in einem float steht
VorzeichenExponent (8 Bit)Mantisse (23 Bit)

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

TypBitsBereichBemerkung
byte8−128 … 127mit Vorzeichen – (byte) 200 ist −56
short16−32 768 … 32 767
char160 … 65 535der einzige Ganzzahltyp ohne Vorzeichen
int32etwa ±2,1 MilliardenStandard für Ganzzahlen
long64etwa ±9,2 · 10¹⁸Literal mit L
float32bis etwa 3,4 · 10³⁸etwa 7 Dezimalstellen
double64bis 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 Strings

Fü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

„double ist ungenau.“ – Jedes Rechenergebnis ist exakt die nächstgelegene darstellbare Zahl; das ist genau festgelegt und reproduzierbar. Ungenau wird es, weil viele Dezimalbrüche keine exakte Binärdarstellung haben – und weil sich kleine Rundungen über viele Schritte summieren.
„Ein Überlauf löst in Java einen Fehler aus.“ – Bei int und long nicht: Die Zahl springt stumm auf die andere Seite. Nur Math.addExact und Co. werfen eine Exception.
„Math.abs liefert immer etwas Positives.“ – Math.abs(Integer.MIN_VALUE) ist wieder Integer.MIN_VALUE, weil +2 147 483 648 in einem int keinen Platz hat.
„float spart Speicher, also nehme ich float.“ – Nur bei großen Arrays lohnt sich das. Für einzelne Werte kostet die geringere Genauigkeit mehr, als die vier gesparten Bytes bringen.

Verwandte Begriffe