Computer systems store signed integers using two's complement representation. Sign-magnitude and one's complement forms are only conceptual steps used to derive two's complement; they are not stored in memory.
Sign-Magnitude Representation
- A positive integer's sign-magnitude representation is identical to its binary form.
Example for a single-byte integer (8-bit): Decimal 10 is
0000 1010. - A negative integer's sign-magnitude uses the most significant bit (MSB) as the sign bit (1 for negative).
Example: Decimal -10 is
1000 1010.
One's Complement
- A positive integer's one's complement is the same as its sign-magnitude.
Example: 10's one's complement is
0000 1010. - A negative integer's one's complement is obtained by inverting all bits of its sign-magnitude, except the sign bit.
Example: -10's sign-magnitude is
1000 1010. Inverting the value bits yields1111 0101.
Two's Complement (The Actual Storage Format)
- A positive integer's two's complement matches its sign-magnitude.
Example: 10's two's complement is
0000 1010. This is the actual pattern stored for the integer 10. - A negative integer's two's complement is its one's complement plus 1.
Example: -10's one's complement is
1111 0101. Adding 1 yields1111 0110. This is the stored pattern for -10.
Why Two's Complement is Used
Sign-magnitude and one's complement lead to incorrect arithmetic results. Two's complement ensures accurate calculations within a fixed bit width.
-
Using Sign-Magnitude for 10 + (-10):
0000 1010(10) +1000 1010(-10) =1001 0100(-20). Incorrect. -
Using One's Complement for 10 + (-10):
0000 1010(10) +1111 0101(-10) =1111 1111(One's complement result). Converting1111 1111(one's complement) to sign-magnitude yields1000 0000(-0). This is also incorrect or ambiguous. -
Using Two's Complement for 10 + (-10):
0000 1010(10) +1111 0110(-10) =1 0000 0000. With an 8-bit storage limit, the carry-out1(the 9th bit) is discarded, leaving0000 0000(0). Correct.
Practice: Decoding Two's Complement
0b0000_1111-> 15 (Positive numbers have the same two's complement and value).0b1111_1111(Two's complement) -> Subtract 1:1111 1110(One's complement) -> Invert bits (excluding sign):1000 0001(Sign-magnitude) -> Value: -1.0b1111_0000-> Subtract 1:1110 1111-> Invert:1001 0000-> Value: -16.0b1000_0001-> Subtract 1:1000 0000-> Invert:1111 1111-> Value: -127.
Application in Network Byte Streams
When transmitting multi-byte data types (like int) over TCP as a byte stream, conversion to and from byte[] is necessary, requiring careful handling of two's complement.
In Java, primitive data types are signed. A byte uses 8 bits in two's complement, with an MSB as the sign bit, giving a range of -128 to 127.
Splitting an int into Bytes
int value = 9071;
byte[] byteArray = new byte[4];
byteArray[0] = (byte) (value >> 24); // Most significant 8 bits
byteArray[1] = (byte) (value >> 16);
byteArray[2] = (byte) (value >> 8);
byteArray[3] = (byte) (value); // Least significant 8 bits
Reconstructing an int from Bytes
The challenge is reconstructing the original int from a byte[]. When a negative byte (where the MSB is 1) is promoted to int or long, Java performs sign extension: the high-order bits (bits 8-31) are filled with 1s to preserve the negative value in two's complement. This corrupts the reconstruction if you only want the lower 8 bits.
Using a bitwise AND with 0xff (binary 11111111) masks the lower 8 bits and clears the upper 24 bits to zero during the promotion, preventing unwanted sign extension.
byte negativeByte = -1; // Stored as 11111111 (two's complement)
int incorrectPromotion = negativeByte; // Becomes 11111111 11111111 11111111 11111111 (-1)
int correctLowBits = negativeByte & 0xFF; // Becomes 00000000 00000000 00000000 11111111 (255)
Example Reconstruction:
byte[] receivedBytes = new byte[4];
// Assume receivedBytes is populated with the four bytes from the stream.
int reconstructedValue = ((receivedBytes[0] & 0xFF) << 24) |
((receivedBytes[1] & 0xFF) << 16) |
((receivedBytes[2] & 0xFF) << 8) |
((receivedBytes[3] & 0xFF));
The & 0xFF operation ensures each byte's value is treated as an unsigned 8-bit integer during the shift and OR operations, guaranteeing correct reconstruction for both positive and negative original int values.
Bitwise Shift Operators
Java provides three shift operators that behave differently with signed integers.
-
Left Shift (
<<): Shifts bits left, filling vacated low-order bits with 0. This is equivalent to multiplication by 2^n.int posLeft = 20 << 2; // 20 * 4 = 80 int negLeft = -20 << 2; // -20 * 4 = -80 -
Signed Right Shift (
>>): Shifts bits right. For positive numbers, high-order bits are filled with 0. For negative numbers, high-order bits are filled with 1 (sign extension). This is arithmetic shift, roughly equivalent to division by 2^n, rounding towards negative infinity.int posRight = 20 >> 2; // 20 / 4 = 5 int negRight = -20 >> 2; // -20 / 4 = -5 (Sign is preserved) -
Unsigned Right Shift (
>>>): Shifts bits right, always filling high-order bits with 0, regardless of sign. This is a logical shift. For positive numbers,>>and>>>yield the same result. For negative numbers, the result becomes a large positive number.int logicalShift = -20 >>> 2; // Results in a large positive integer (1073741819 for 32-bit int).
The unsigned right shift is particularly useful when treating an int as a container for 32 raw bits rather than a signed value.