Binary foundations
Computers store everything as bits. Understanding binary, hexadecimal, bit positions and two's complement tells you what a number looks like in memory and why Java behaves the way it does with negative numbers and overflow.
Learning goals
- Convert between decimal, binary and hexadecimal
- Read a two's complement number and find its negative
- Number bit positions and build masks
- Know the width and range of Java's integer types
Prerequisites
Binary and hexadecimal
Binary is base 2: the digit at position i is worth 2^i. So 1011₂ = 8 + 0 + 2 + 1 = 11. Hexadecimal is base 16 and uses 0-9 and A-F. One hex digit is exactly four bits, so hex is a compact way to write binary: 0x2F = 0010 1111₂ = 47.
Java writes hexadecimal literals with 0x, binary with 0b, and allows underscores: 0xFF, 0b1010_1100, 1_000_000.
| Decimal | Binary | Hex |
|---|---|---|
| 10 | 1010 | A |
| 15 | 1111 | F |
| 47 | 0010 1111 | 2F |
| 255 | 1111 1111 | FF |
| 256 | 1 0000 0000 | 100 |
1int a = 0b1011; // 112int b = 0x2F; // 473int c = 0xFF; // 2554System.out.println(Integer.toBinaryString(47)); // 1011115System.out.println(Integer.toHexString(255)); // ff6System.out.println(Integer.parseInt("101111", 2)); // 47
Signed two's complement
Java stores signed integers in two's complement. In an n-bit value the top bit has weight -2^(n-1), and the other bits have their usual positive weights. So 1111 1011 in 8 bits is -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5.
To negate a number, invert every bit and add 1. That is why -x == ~x + 1. Try 5, then -5, and then -128 in the animation: -128 has no positive partner, so negating it gives -128 again. In Java Integer.MIN_VALUE behaves the same, and so does Math.abs(Integer.MIN_VALUE).
Two's complement: negate by invert and add one
Interactive · edit the inputx = 5
In two's complement the top bit has weight -128 and the other bits have their usual positive weights. The top bit is 0, so x is not negative.
1int negate(int x) {2return ~x + 1; // invert every bit, then add one3}
Live state
x
5
Execution trace (dry run table)Show 3 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | x=5 | In two's complement the top bit has weight -128 and the other bits have their usual positive weights. The top bit is 0, so x is not negative. |
| 2 | L2 | x=5 ~x=-6 | Step 1: invert every bit. ~x = -6, which equals -x - 1. |
| 3 | L2 | return=-5 | Step 2: add 1. The carry ripples through the trailing 1s. Result: -5, so -(5) = -5. |
Bit positions
Bits are numbered from the right, starting at 0. The rightmost bit is the least significant bit (LSB), and the leftmost the most significant bit (MSB). Bit i has the value 2^i, which Java writes as 1 << i.
To check which positions are set in 44 = 0010 1100₂: bits 2, 3 and 5. Each animation in this chapter labels the position under every bit.
| Position | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Weight 2^i | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| 44 in binary | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
Bit masks
A mask is an integer used to select or change particular bits with &, | and ^. A single-bit mask is 1 << i. A mask for the lowest k bits is (1 << k) - 1. For example (1 << 4) - 1 = 0b1111 = 15.
Constants like 0xFF (the low 8 bits) and 0x0F (the low 4 bits) are masks. x & 0xFF extracts the low byte of x, which is how you read an unsigned byte value from a signed byte.
1int lowByte = x & 0xFF; // keep the lowest 8 bits2int upper = (x >>> 8) & 0xFF; // the next 8 bits3int lowBits = x & ((1 << k) - 1); // keep the lowest k bits (x mod 2^k)4int unsigned = someByte & 0xFF; // byte -56 -> 200
Binary conversion
To convert decimal to binary, divide by 2 repeatedly and collect the remainders; the first remainder is the lowest bit. Reading them in reverse gives the binary number. The animation shows the remainders arriving lowest bit first and then the reversed result.
To convert back, add 2^i for each set bit, or use Horner's rule: start at 0 and for each bit from the left do value = value * 2 + bit. Java does both for you with Integer.toBinaryString and Integer.parseInt(s, 2).
1static int fromBinary(String s) {2int value = 0;3for (char c : s.toCharArray()) {4value = value * 2 + (c - '0'); // Horner's rule5}6return value;7}
Convert decimal to binary by repeated division
Interactive · edit the inputremainders (lowest bit first)
Divide by 2 repeatedly. Each remainder is one bit, starting from the lowest.
1static String toBinary(int n) {2if (n == 0) return "0";3StringBuilder sb = new StringBuilder();4while (n > 0) {5sb.append(n % 2); // the remainder is the next bit, lowest first6n /= 2;7}8return sb.reverse().toString();9}
Live state
n
156
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=156 | Divide by 2 repeatedly. Each remainder is one bit, starting from the lowest. |
| 2 | L5 | n=156 n % 2=0 n / 2=78 | 156 % 2 = 0, so bit 0 is 0. Then n = 156 / 2 = 78. |
| 3 | L5 | n=78 n % 2=0 n / 2=39 | 78 % 2 = 0, so bit 1 is 0. Then n = 78 / 2 = 39. |
| 4 | L5 | n=39 n % 2=1 n / 2=19 | 39 % 2 = 1, so bit 2 is 1. Then n = 39 / 2 = 19. |
| 5 | L5 | n=19 n % 2=1 n / 2=9 | 19 % 2 = 1, so bit 3 is 1. Then n = 19 / 2 = 9. |
| 6 | L5 | n=9 n % 2=1 n / 2=4 | 9 % 2 = 1, so bit 4 is 1. Then n = 9 / 2 = 4. |
| 7 | L5 | n=4 n % 2=0 n / 2=2 | 4 % 2 = 0, so bit 5 is 0. Then n = 4 / 2 = 2. |
| 8 | L5 | n=2 n % 2=0 n / 2=1 | 2 % 2 = 0, so bit 6 is 0. Then n = 2 / 2 = 1. |
| 9 | L5 | n=1 n % 2=1 n / 2=0 | 1 % 2 = 1, so bit 7 is 1. Then n = 1 / 2 = 0. |
| 10 | L8 | return=10011100 | Read the remainders from last to first: 156 = 10011100 in binary. It took 8 divisions, one per bit (⌊log₂ n⌋ + 1). |
Fixed-width representation
Java's integer types have fixed widths: byte 8 bits, short 16, int 32, long 64, and char is an unsigned 16 bits. A value that does not fit wraps around: (byte) 200 is -56, and Integer.MAX_VALUE + 1 is Integer.MIN_VALUE.
Fixed width matters in bit tricks: ~x flips all 32 bits of an int, and x << 33 shifts by 33 & 31 = 1 because the shift distance is taken modulo the width.
| Type | Bits | Range |
|---|---|---|
| byte | 8 | -128 to 127 |
| short | 16 | -32,768 to 32,767 |
| int | 32 | -2,147,483,648 to 2,147,483,647 |
| long | 64 | about ±9.22 × 10^18 |
| char | 16 (unsigned) | 0 to 65,535 |
1byte b = (byte) 200; // -56 (200 - 256)2int wrapped = Integer.MAX_VALUE + 1; // -21474836483int shift = 1 << 33; // 2, because 33 & 31 == 14long big = 1L << 40; // use 1L, not 1, for shifts beyond 31
Practice ladder
Now try it yourself
Easy
- Convert 1101 0110₂ to decimal and hex
- Write -9 as an 8-bit two's complement number
- What is
(byte) 130?
Medium
- Convert a hexadecimal string to an int without parseInt
- Extract bits 4 to 7 of a value
- Pack three 10-bit values into one int and unpack them
Advanced
- Implement toBinaryString for negative ints from scratch
- Detect int overflow when adding two numbers without using long
- Explain why Integer.MIN_VALUE == -Integer.MIN_VALUE
Key takeaways
- Bit i is worth 2^i and is counted from the right starting at 0; one hex digit is four bits.
- Two's complement: negate by inverting the bits and adding 1; the range is asymmetric.
- Java ints are fixed-width and wrap around on overflow; use 1L for 64-bit shifts.