Alternative Radices: Negative Integers and Two's Complement

Common representation of negative integers.

Overview

In modern computers, almost all numeric values are processed and represented internally in some base-2 form. Starting with that, a reasonable next question might be: “How are negative values represented?” There’s more than one way to do that—“Positional notation: Non-integral values” mentioned that the float and double types use a sign bit—but for integers, two’s complement is used far more often by CPUs (including the JVM) and compilers (including javac) than any other.

In two’s complement, the representation of the negative value of an integer n is found by flipping all of the bits in n, and adding 1. In other words, the value of the following boolean expression is (with one exception) true:

-n == ~n + 1

Example

For a concrete example, let’s look at 42, which we can write in Java as the base-2 literal 0b00101010. If we flip all the bits, we get 0b11010101. If we add 1 to that value, we end up with 0b11010110, which must be (according to the above) the two’s-complement representation of -42.

Another way of describing two’s complement is that it treats the left-most bit as the negative of its unsigned value. To illustrate, remember that a byte has 8 bits, and let’s revisit positional notation for base-2, but with a change in the sign of the left-most column. Again, let’s use 42:

-128 64 32 16 8 4 2 1
0 0 1 0 1 0 1 0

That is, 42 = 32 + 8 + 2.

Note that the value of left-most column is -128, not 128.

Now, flip the bits in the above:

-128 64 32 16 8 4 2 1
1 1 0 1 0 1 0 1

Next, add 1, carrying as necessary:

-128 64 32 16 8 4 2 1
1 1 0 1 0 1 1 0

We now have -128 + 64 + 16 + 4 + 2, which adds up to -42.

This approach works equally well (with the exception mentioned above, and explained below) for taking the negative of a value that’s already negative. For a simple example of this, start with -1, which we can express (in the case of a byte) as the Java literal 0b11111111 (-128 + 64 + 32 + 16 + 8 + 4 + 2 + 1). Flip the bits, and we have 0b00000000. Add 1, and we have 0b00000001, which is (of course) 1, or -(-1).

Another advantage of this representation is that the basic arithmetic operations work the same for positive and negative values: little (if any) adjustment is needed in the underlying CPU instructions, even if we switch between signed and unsigned types.

Wider types

This representation works for wider integer types, as well: In a type composed of $N$ bits, the leftmost bit represents $-(2^{N - 1})$, while all the other bits count as normal. Consider the short (16-bit signed) value expressible as 0b00000101_01010101:

\[\begin{aligned} 10101010101_2 &= 2^{10} + 2^8 + 2^6 + 2^4 + 2^2 + 2^0 \\ &= 1024 + 256 + 64 + 16 + 4 + 1 \\ &= 1365 \end{aligned}\]

Now, flip the bits to get 0b11111010_10101010:

\[\begin{aligned} & -2^{15} + 2^{14} + 2^{13} + 2^{12} + 2^{11} + 2^9 + 2^7 + 2^5 + 2^3 + 2^1 \\ =& -32768 + 16384 + 8192 + 4096 + 2048 + 512 + 128 + 32 + 8 + 2 \\ =& -1366 \end{aligned}\]

Finally, add 1, and we get -1365.

(Note that just as we can use underscores as digit separators in Java literals expressed in base-10, we can do the same with binary, octal, and hexadecimal literals.)

With the 8 bits of a Java byte, we can count from 0 (0b00000000) to 127 (0b01111111) in the usual fashion; if we add 1 more, we get 0b10000000, or -128. Continuing to add 1 repeatedly, we get the values from 0b10000001, or -127, up to 0b11111111, or -1. Add 1 more to that, and throw away the overflow bit (produced when our addition carries over to a 9th bit, which doesn’t fit into an 8-bit byte), and we’re back to 0b00000000, or 0.

Unexpected consequence

One interesting implication of all of the above is that a two’s-complement representation always allows for a negative value that is 1 larger in absolute value than the largest positive value. For example, the values allowed by the byte type range from -128 to 127; for the short type, the range is -32,768 to 32,767; and so on. Because of this, something curious happens when we compute (for example) the negative of a byte that has the value -128. As we saw above, byte only allows a maximum value of 127, so where does that leave -(-128)?

Let’s try it in JShell:

byte b = -128;
b *= -1;

In the second line, the value of b is multiplied by -1, and the result is assigned back to b. But what was the value assigned? If you didn’t notice the value displayed after the assignment, simply type b on a new line and then [Enter].

Note that we could written the first line above as

byte b = Byte.MIN_VALUE;

Let’s try it with the int type: Using JShell, write the following, to see what happens when we multiply Integer.MIN_VALUE by -1:

-Integer.MIN_VALUE

Do you notice anything strange? It sure looks like the result of negating the Integer.MIN_VALUE is the same value!

Let’s check our suspicion about this, by evaluating this expression:

-Integer.MIN_VALUE == Integer.MIN_VALUE

Of course, we expect an expression like -0 == 0 to evaluate to true, but for a non-zero value to be equal to its negation can be a bit puzzling.

All finite numeric representation schemes have weaknesses. This simple JShell experiment demonstrates one such weakness in two’s complement: The result of negating the minimum value in the representable range is the same minimum value.