Experimenting with Signed Integers

We conduct several programming experiments in order to understand integer representation in computer systems.

Signed Integer in Java

How does Java represent signed integers? Answer this question by reading and experimenting this program:

public class IntegerDemo {
    public static void main(String[] args) {
        System.out.printf("Java byte type is of %d bits\n", Byte.SIZE);
        byte b = 3;
        String bStr = getBinaryString(b);
        System.out.printf("%4d_10 = %02x_16 = %s_2\n", b, b, bStr);

        b = -3;
        bStr = getBinaryString(b);
        System.out.printf("%4d_10 = %02x_16 = %s_2\n", b, b, bStr);
    }

    public static String getBinaryString(byte b) {
        StringBuilder sb = new StringBuilder();

        int bitMask = 0x1;
        for (int i=0; i<Byte.SIZE; i++) {
            int bit = bitMask & b;
            sb.insert(0, Character.forDigit(bit, 10));
            b = (byte)(b >> 1);
        }
        return sb.toString();
    }
}

Integer Overflow

When performing integer operations, we should be aware of Integer Overview problem. Let’s take a look at this program that computes the average of the two numbers. In mathematics, the average of two numbers are,

\(m = \frac{a + b}{2}\) where we denote the average as \(m\), the numbers \(a\) and \(b\).

Following this understanding, we design a program as follows:

import java.util.Scanner;

public class AverageTwoNum {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        System.out.print("Enter integer a: ");
        int a = input.nextInt();
        System.out.print("Enter integer b: ");
        int b = input.nextInt();

        int m = (a + b) / 2;

        System.out.printf("(%d + %d)/2 = 0x%d = %x\n", a, b, m, m);
        input.close();
    }
}

Explore the following questions:

  1. Given \(a = 2147483641\) and \(b = 8\), what do you expect the output of the program to be? Why?
  2. How do you fix this problem?

Integer Wrap-around

To understand the phenomenon of integer wrap-around, let’s run and observe the following program.

public class IntegerWrapAround {
    public static void main(String[] args) {
        byte b = 0;
        final int n = 512;

        System.out.printf("b = %d = 0x%x\n", b, b);
        System.out.printf("Increment b for %d times\n", n);
        for (int i=0; i<n; i++) {
            b ++;
            System.out.printf(" b ++ -> b = %d = 0x%x\n", b, b);
        }

        b = 0;
        System.out.printf("b = %d = 0x%x\n", b, b);
        System.out.printf("Decrement b for %d times\n", n);
        for (int i=0; i<n; i++) {
            b --;
            System.out.printf(" b -- -> b = %d = 0x%x\n", b, b);
        }
    }
}

Does the result surprise you? Can you explain the result?

Truncation

In a computer system, regardless data type used, an integer is of fixed length. Truncation happens when we cast an integer data type to shorter integer. Run the following program.

public class IntegerTruncation {
    public static void main(String[] args) {
        int a = 256;
        int b = 255;

        byte c = (byte)(a + b);

        System.out.printf("%d + %d = %d -> to byte -> %d\n", a, b, a+b, c);

        a = 256;
        b = 127;

        c = (byte)(a + b);
        System.out.printf("%d + %d = %d -> to byte -> %d\n", a, b, a+b, c);
    }
}

Does the result surprise you? Can you explain the result?

Multiplication and Division via Shift Operation

Computers generally support shift operations. In Java, we have two bit shift operators, >> is to shift an integer 1 bit to the right while << 1 bit to the right. With shift operations, we can compute multiplication or division by a power of 2 efficiently. Observe the following program.

import java.util.Scanner;

public class IntegerShift {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a byte integer for right shift: ");
        byte n = input.nextByte();
        System.out.printf("User entered %d = 0x%x\n", n, n);

        for (int i=0; i<Byte.SIZE+1; i++) {
            byte shiftResult = (byte)(n >> 1);
            byte divisionResult = (byte)(n / 2);
            System.out.printf("%d = 0x%x >> 1 = %d = 0x%x;\t %d/2 = %d = 0x%x\n",
                n, n, shiftResult, shiftResult, n, divisionResult, divisionResult);
            n = shiftResult;
        }

        System.out.print("Enter a byte integer for left shift: ");
        n = input.nextByte();
        System.out.printf("User entered %d = 0x%x\n", n, n);

        for (int i=0; i<Byte.SIZE+1; i++) {
            byte shiftResult = (byte)(n << 1);
            byte multiplyResult = (byte)(n * 2);
            System.out.printf("%d = 0x%x >> 1 = %d = 0x%x;\t %d*2 = %d = 0x%x\n",
                n, n, shiftResult, shiftResult, n, multiplyResult, multiplyResult);
            n = shiftResult;
        }

        input.close();
    }
}

Again, does the result surprise you? Can you explain the result?

Most Negative Number

As discussed in the beginning, Java represents integers using two’s complement notation. In this notation, an integer is a fixed length binary number. Denote the length as \(N\) and the integer as \(b_{N-1} b_{N-2} \ldots b_1 b_0\). The value of the integer is,

\[v = - b_{N-1} 2^{N-1} + \sum\limits_{i=0}^{N-2} a_i 2^i\]

For instance, for an imaginary integer type of length of 3, we write out the correspondence of binary number and its value as follows:

Binary Representation Value
000 0
001 1
010 2
011 3
100 -4
101 -3
110 -2
111 -1

which can serve as notations for numbers between -4 and 3 where -4 is the negative number with the largest magnitude (most negative number) and 3 the positive number with the largest magnitude (most positive number).
From this example, we see that in two’s complement notation, the most negative number and the most positive number have unequal magnitude and the most negative number has a greater magnitude. Despite the benefits of two’s complement, this can be a source of potential problems in our programs. Observe the output of the following program:

class MostNegativeDemo {
    public static void main(String[] args) {
        int mostPositive = Integer.MAX_VALUE;
        int mostNegative = Integer.MIN_VALUE;

        System.out.printf("%30s: %12d\n", "Most Positive", mostPositive);
        System.out.printf("%30s: %12d\n", "Most Negative", mostNegative);

        int mostNegativeFromBinary = 1 << (Integer.SIZE - 1);
        String what = String.format("1 << %d", Integer.SIZE - 1);
        System.out.printf("%30s: %12d\n", what, mostNegativeFromBinary);

        int absMostNegative = Math.abs(mostNegative);
        what = String.format("Math.abs(%d)", mostNegative);
        System.out.printf("%30s: %12d\n", what, absMostNegative);
        int negMostNegative = - mostNegative;
        what = String.format("- (%d)", mostNegative);
        System.out.printf("%30s: %12d\n", what, negMostNegative);
        int multMostNegative = -1 * mostNegative;
        what = String.format("-1 * %d", mostNegative);
        System.out.printf("%30s: %12d\n", what, multMostNegative);
        int divMostNegative = mostNegative/(-1);
        what = String.format("%d/(-1)", mostNegative);
        System.out.printf("%30s: %12d\n", what, divMostNegative);

        System.out.println("but");
        int negativeNum = -10;
        int absNegativeNum = Math.abs(negativeNum);
        what = String.format("Math.abs(%d)", negativeNum);
        System.out.printf("%30s: %12d\n", what, absNegativeNum);
        int negNegativeNum = - negativeNum;
        what = String.format("- (%d)", negativeNum);
        System.out.printf("%30s: %12d\n", what, negNegativeNum);
        int multNegativeNum = -1 * negativeNum;
        what = String.format("-1 * %d", negativeNum);
        System.out.printf("%30s: %12d\n", what, multNegativeNum);
        int divNegativeNum = negativeNum/(-1);
        what = String.format("%d/(-1)", negativeNum);
        System.out.printf("%30s: %12d\n", what, divNegativeNum);        
    }
}

Can you explain what you observe?