2.2
How Numbers Live in Memory
Fixed-width two's complement represents signed integers. IEEE 754 represents binary floating point, including rounding and special values.
Previously on
Section 2.1 introduced fixed-width integer types and IEEE 754 floating-point types.
This section gives the bits an interpretation. The width matters at every step. 1111_1111 can mean 255 as an unsigned pattern or -1 as an eight-bit signed pattern.
The problem
Run this complete program:
public class StoredValues {
public static void main(String[] args) {
byte negative = -42;
float exact = 8.125f;
float rounded = 0.7f;
System.out.println(Integer.toBinaryString(negative & 0xFF));
System.out.printf("%.20f%n", exact);
System.out.printf("%.20f%n", rounded);
}
}Output:
11010110
8.12500000000000000000
0.69999998807907100000Three questions now have evidence:
- How does
11010110represent -42? - Why does 8.125 survive exactly?
- Why is the stored float near 0.7 rather than equal to decimal 0.7?
The idea
Bits gain meaning from a representation rule and a width.
| Signed integers | Binary floating point | |
|---|---|---|
| Java types | byte, short, int, long | float, double |
| Representation | fixed-width two's complement | IEEE 754 binary32 or binary64 |
| Main limit | finite range | finite range and finite precision |
| Past the limit | ordinary arithmetic wraps | rounding, infinity, or underflow |
Positive binary values
Binary place values are powers of two.
The same eight positions can encode signed values because Java reads them using two’s complement.
Under the hood
Going deeperTwo’s complement at a fixed width
To form -42 in eight bits:
| Step | Bits |
|---|---|
| +42 | 0010_1010 |
| invert every bit | 1101_0101 |
| add one within eight bits | 1101_0110 |
The subtraction rule is often faster than inverting:
if the top bit is 0: signed value = unsigned value
if the top bit is 1: signed value = unsigned value - 2^widthFor 1111_0011, the unsigned value is 243. At eight bits, 243 - 256 = -13.
Why addition works without a separate negative-number circuit
Add 42 and -42 while keeping eight bits:
0010_1010
+ 1101_0110
-----------
1_0000_0000The ninth carry bit falls outside the width. The stored eight bits are 0000_0000, which is zero.
This is the central advantage of two’s complement. One fixed-width addition rule works for positive and negative operands. There is one zero pattern. Subtraction can use addition of the two’s complement.
The asymmetric minimum
An eight-bit signed range is -128 through 127.
0111_1111 = 127
1000_0000 = -128There is no eight-bit +128. Negating the minimum within the same width returns the same pattern. This behavior generalizes to Short.MIN_VALUE, Integer.MIN_VALUE, and Long.MIN_VALUE.
Overflow is fixed-width wraparound
The maximum byte pattern plus one gives the minimum byte pattern:
0111_1111 127
+ 1
----------
1000_0000 -128Java promotes byte arithmetic to int, so this program uses a cast to return to eight bits:
byte maximum = Byte.MAX_VALUE;
byte wrapped = (byte) (maximum + 1);
System.out.println(wrapped); // -128Ordinary int and long arithmetic also wraps at their own widths. Math.addExact, Math.subtractExact, and Math.multiplyExact provide checked alternatives.
IEEE 754 binary32
A normal finite float uses three fields:
For normal finite values, the interpretation is:
(-1)^sign * 1.fraction * 2^(storedExponent - 127)The implied leading 1 gives 24 bits of precision even though only 23 fraction bits are stored.
Build 8.125
8.125 is 1000.001 in binary. Normalize it:
1000.001 = 1.000001 * 2^3The sign is 0. The stored exponent is 3 + 127 = 130, or 1000_0010. The fraction field holds the digits after the leading 1.: 000001, followed by zeros. Every required binary digit fits, so the result is exact.
Why 0.7 is rounded
Convert the fractional part by repeatedly multiplying by two:
| Input | Times 2 | Next bit | Remainder |
|---|---|---|---|
| 0.7 | 1.4 | 1 | 0.4 |
| 0.4 | 0.8 | 0 | 0.8 |
| 0.8 | 1.6 | 1 | 0.6 |
| 0.6 | 1.2 | 1 | 0.2 |
| 0.2 | 0.4 | 0 | 0.4 |
The remainder 0.4 has returned, so the sequence repeats:
0.7 decimal = 0.10110011001100110011... binaryA float cannot store infinitely many fraction bits. IEEE 754 rounds to a nearby representable value. The exact decimal value of the resulting float is:
0.699999988079071044921875printf("%.20f", value) rounds that stored value to twenty digits after the point. Float.toString(value) uses the shortest decimal text that parses back to the same float, which is why println displays 0.7.
Exponent patterns also encode special values
The normal-value formula does not cover every exponent pattern.
| Exponent bits | Fraction bits | Meaning |
|---|---|---|
00000000 |
all zero | positive or negative zero |
00000000 |
nonzero | subnormal finite value |
00000001 to 11111110 |
any | normal finite value |
11111111 |
all zero | positive or negative infinity |
11111111 |
nonzero | NaN, meaning not a number |
This program exposes the arithmetic behavior:
public class SpecialFloats {
public static void main(String[] args) {
double positiveInfinity = 1.0 / 0.0;
double notANumber = 0.0 / 0.0;
System.out.println(positiveInfinity);
System.out.println(notANumber);
System.out.println(notANumber == notANumber);
System.out.println(+0.0 == -0.0);
}
}Output:
Infinity
NaN
false
trueInteger division by zero is different: 1 / 0 throws ArithmeticException.
Equality needs a contract
0.1 + 0.2 == 0.3 is false because the two computations land on different representable doubles.
double sum = 0.1 + 0.2;
System.out.printf("%.17f%n", sum); // 0.30000000000000004For measured or calculated quantities, compare against a tolerance chosen for the domain:
static boolean closeEnough(double a, double b, double tolerance) {
return Math.abs(a - b) <= tolerance;
}An absolute tolerance alone is not suitable across every magnitude. Production numeric code often combines absolute and relative tolerances and defines behavior for NaN and infinity.
Use BigDecimal when the domain requires exact decimal arithmetic, such as a currency amount with defined decimal rounding. Construct it from decimal text or BigDecimal.valueOf, not from an already rounded binary floating-point value.
What it costs
Two’s complement makes signed arithmetic efficient and predictable. Its fixed width also makes overflow silent unless code asks for checked operations.
IEEE 754 provides a large dynamic range, hardware support, infinities, NaN, and gradual underflow through subnormal values. It cannot represent every real or decimal value. Arithmetic must define acceptable error instead of assuming printed decimal text is exact.
Bit-level explanations depend on width and category. The normal float formula is not a formula for zero, subnormal values, infinity, or NaN.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
In eight-bit two's complement, what value does `1111_1111` represent?
Show the answer
The top bit is set, so read the unsigned pattern, 255, and subtract 2^8. The result is -1. You can also invert the bits and add one to recover magnitude 1.
Why is `-Byte.MIN_VALUE` still -128 after conversion back to `byte`?
Show the answer
An eight-bit byte has a pattern for -128 but no pattern for +128. Negating
1000_0000within eight bits produces1000_0000again. Java first promotes a byte operand toint, so a cast is needed to observe byte-width wrapping.Why is 8.125 exact as a float while 0.7 is not?
Show the answer
8.125 equals 8 + 1/8, so its binary form
1000.001terminates. The binary expansion of 0.7 repeats. A float has finite precision, so Java stores the nearest representable value, which is approximately 0.699999988079071.Should every comparison between two `double` values use a tolerance?
Show the answer
No. A tolerance is useful when rounded measurements or calculations should be considered close within a domain-specific bound. Exact comparisons can be valid for sentinels, values copied without arithmetic, and some exactly representable results. Also handle
NaN, infinities, and signed zero according to the program's contract. Exact decimal domains often needBigDecimal.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises90 pointsabout 100 minutes
Two's Complement By Hand
ex-2-2-aDo the recipe by hand until it is automatic.
Work out the bit patterns for -7, -1 and -128 on paper first. Then run the program and see whether you were right.
Then go the other way. Here is a bit pattern: 11110011. Read it as a signed byte and tell me what number it is. Use the same flip-and-add-one steps, because they work in both directions.
Pay attention to -128. It is the one value in the byte range that has no positive twin, and working out its pattern will show you why.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Complement {
static String bits(int value, int n)
static int negate(int value, int n)
static int signed(int rawBits, int n)
static int fromBits(String bits)
}bits returns exactly n binary digits, padded. negate applies flip-and-add-one within n bits. signed reads n raw bits as a signed number. fromBits takes a pattern and gives back the number, using its length as the width.
What your program must do
- Do flip-and-add-one on paper for 42 and for -42 before writing any code
- Implement all four methods so they work at 8 bits and at 4
- Show that negating twice gives you back what you started with
- Find the one value that is its own negative, and say why
public class Complement {
// Exactly n binary digits, padded with leading zeros.
static String bits(int value, int n) {
return ""; // TODO
}
// Flip every bit, then add one. Stay within n bits.
static int negate(int value, int n) {
return 0; // TODO
}
// Read n raw bits as a signed number. Top bit set means negative.
static int signed(int rawBits, int n) {
return 0; // TODO
}
// A pattern like "11010110" back to the number. Its length is the width.
static int fromBits(String bits) {
return 0; // TODO
}
public static void main(String[] args) {
// TODO: print the 8 bit pattern for 42, -42, 7, -7, 1, -1, 127, -128 and 0
// TODO: negate a few by hand first, then check against your method
}
}
Hint 1
(1 << n) - 1 is n ones, so value & mask keeps only the bits you care about.Hint 2
Hint 3almost the answer
Inspect Exact Float Values
ex-2-2-bJava usually prints a compact round-trip representation of a float. Compare that text with the exact decimal expansion of the stored binary value.
Print each value with Float.toString, with printf at twenty decimal places, and with the
BigDecimal(float) technique from the hint. The first form is shortest-round-trip text. The second is
rounded to twenty places. The third exposes the exact decimal expansion. Predict which values are exact
before running.
Classify each value before running. Binary fractions built from powers of two terminate; values such as 0.7 require rounding to a representable float.
Once you can look at a decimal and say “that one will be exact” or “that one will not”, you have understood this section.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Hidden {
static boolean isExact(float f)
static String exactValue(float f)
static boolean printsTheSame(float f)
}isExact answers whether the float equals the intended decimal value. exactValue returns the exact decimal expansion of the stored binary float. printsTheSame compares that expansion with Float.toString.
What your program must do
- Predict which of the nine values are stored exactly before running anything
- Print the exact decimal expansion of each stored value and compare it with Float.toString
- Say what the exact ones have in common
- Explain why Float.toString showing 0.7 is a round-trip representation, not a claim that the stored value equals decimal 0.7
import java.math.BigDecimal;
public class Hidden {
// Is this float storing the decimal you wrote, or something near it?
static boolean isExact(float f) {
return true; // TODO
}
// The exact decimal expansion of the stored binary value.
static String exactValue(float f) {
return ""; // TODO
}
// Does Float.toString show the full exact decimal expansion?
static boolean printsTheSame(float f) {
return true; // TODO
}
public static void main(String[] args) {
// Predict which of these are exact BEFORE running:
// 0.5f 0.25f 0.125f 8.125f 1.0f 0.7f 0.1f 0.2f 0.3f
// TODO: print each one's exact stored value and whether it is exact
}
}
Hint 1
new BigDecimal(float) takes the bits exactly as stored. new BigDecimal(Float.toString(f)) takes the decimal you wrote. Comparing the two answers the question.Hint 2
Hint 3almost the answer
Float.toString, which println uses here, prints the shortest decimal that parses back to the same float. The exact decimal expansion can be longer.Take a Float Apart
ex-2-2-cGo all the way down. Take a float apart into its three fields and put one back together yourself.
Do -2.5f on paper first, following all five steps from the section: convert, normalise, add the bias, take the mantissa, set the sign. Write down all 32 bits. Then run the program and see whether you got it.
Then look hard at 0.7f’s mantissa. The repeating group is visible. Find it, name it, and connect it back to the multiply-by-two loop that produced it.
Finish by decoding 1.0f. Apply the reverse formula to the printed fields and verify that the result is 1.0.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Dissect {
static String allBits(float f)
static int signBit(float f)
static String exponentBits(float f)
static String mantissaBits(float f)
static int rawExponent(float f)
static int realExponent(float f)
static float rebuild(int sign, int rawExponent, String mantissa)
}allBits returns all 32 padded. The three pieces are 1, 8 and 23 bits. rawExponent is what is stored, realExponent has the 127 bias removed. rebuild puts the pieces back and must give the original float.
What your program must do
- Work out the 32 bits of 8.125f on paper before writing any code
- Split a float into its three pieces and print them
- Remove the bias and say what the real exponent means for each value
- Rebuild the float from the pieces and confirm you get the original back
public class Dissect {
// All 32 bits, padded with leading zeros.
static String allBits(float f) { return ""; } // TODO
// 1 bit, 8 bits, 23 bits.
static int signBit(float f) { return 0; } // TODO
static String exponentBits(float f) { return ""; } // TODO
static String mantissaBits(float f) { return ""; } // TODO
// What is stored, and what it means once the bias is removed.
static int rawExponent(float f) { return 0; } // TODO
static int realExponent(float f) { return 0; } // TODO
// Put the three pieces back together. This must give the original float.
static float rebuild(int sign, int rawExponent, String mantissa) {
return 0f; // TODO
}
public static void main(String[] args) {
// TODO: take 8.125f, 0.7f, 1.0f, -2.5f and 0.5f apart and print the pieces
// TODO: work 8.125f out on paper first and check your answer matches
}
}
Hint 1
Float.floatToIntBits(f) gives you the raw 32 bits as an int, and Float.intBitsToFloat goes the other way.Hint 2
Hint 3almost the answer
(sign << 31) | (rawExponent << 23) | mantissaAsInt. Getting that round trip to work is what proves you understood the layout rather than just described it.The Money Bug
ex-2-2-dTen items at 0.10 each. The total should be exactly 1.00. Run it and see what you get.
Then fix it properly with BigDecimal, and pay close attention to how you create the BigDecimal. There is a constructor that takes a double and it will quietly undo your entire fix, because by the time BigDecimal receives the value the error has already happened.
Finish by going back to The Registry. Your unit needs a running balance, or a supply budget, or anything measured in money. Add it, choose the type deliberately, and write a comment saying why you chose what you chose.
This is the first time The Registry gets a decision that has a wrong answer.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Money {
static double tenDimesAsDouble()
static BigDecimal tenDimesAsBigDecimal()
static boolean doubleEqualsOne()
static boolean bigDecimalEqualsOne()
static boolean closeEnough(double a, double b, double tolerance)
}Both totals add 0.10 ten times. The BigDecimal version must build its values from Strings, not from doubles, or it inherits the same problem it is supposed to fix.
What your program must do
- Show the double total failing to equal 1.0
- Show that the error is small enough to look right when printed
- Do the same with BigDecimal and get it exactly right
- Show what goes wrong if you build a BigDecimal from a double instead of a String
import java.math.BigDecimal;
public class Money {
// Add 0.10 ten times.
static double tenDimesAsDouble() { return 0; } // TODO
// The same, with BigDecimal. Build the values from Strings.
static BigDecimal tenDimesAsBigDecimal() { return null; } // TODO
static boolean doubleEqualsOne() { return false; } // TODO
static boolean bigDecimalEqualsOne() { return false; } // TODO
// One comparison policy for approximate measurements: within a supplied tolerance.
static boolean closeEnough(double a, double b, double tolerance) {
return false; // TODO
}
public static void main(String[] args) {
// TODO: print both totals and both comparisons
// TODO: try new BigDecimal(0.1) instead of new BigDecimal("0.1") and see what happens
}
}
Hint 1
BigDecimal compares with compareTo, not equals. equals also compares the scale, so 1.0 and 1.00 are not equal by it, which is Section 10.8's territory.Hint 2
Hint 3almost the answer
new BigDecimal(0.1) preserves the exact binary double value. new BigDecimal("0.1") and BigDecimal.valueOf(0.1) represent the decimal text 0.1. Build monetary values from decimal inputs before binary floating-point arithmetic occurs.After the credits
Section 2.3 uses these representations to explain narrowing casts, precision loss during widening, and arithmetic promotion.
Two’s complement returns with bitwise and shift operators. IEEE 754 returns in statistics, streams, money, and every API that accepts floating-point measurements.
Threads you opened in this section
- Two's complementhashCode() returns an int, so it can be negative. That matters.8.3 - The Object Class: `equals`, `hashCode`, `toString`
- Two's complementHashMap has to handle a negative hash before it picks a bucket.Phase X. The Collections Framework
- Floating pointNever compare two doubles with ==. This is why.3.2 - Conditionals
- Floating pointequals() on a class with a double field needs special care.8.3 - The Object Class: `equals`, `hashCode`, `toString`
Two's complement will return in 3.1 - Operators