The Anatomy of Bits: A Journey into Number Primitives’ Bit Mysteries
Introduction
As programmers, we often take the presence primitives for granted, thinking of them as atomic non de-composable entities. However, as we go through this article, we will learn the secrets encoded within the bits, deciphering the language that computers use to process information. From integers to floating-point numbers, each primitive type carries its own set of rules and bit patterns, contributing to the rich tapestry of data manipulation.
Java Primitive Types
For this article, we will be exploring the Java types.
let’s sort these into three categories…
Integral Numbers (2’s Compliment):
These are used to encode whole numbers in Java. Usually, people use int, However — depending on the ranges you require — we can use different number types to be more efficient.
- byte: Allows you to represent values between -128 to 127 if you have a small data domain you want to represent (like color channels in RGB) we could use 3 bytes instead, and use 1/8th the amount of bits that using 3 ints would.
- short: These allow you to represent a smaller range of numbers using 16 bits.
- int: These 32 bit data types are extremely versatile (small in size but representing of a large range). As such, they are the chosen “default” number type for java.
- long: Longs are used in the event that ints do not have enough values. For instance, if you needed to give every human on earth a unique numerical id, you would need the 64 bits of a long.
Floating Point Numbers:
These are often used to represent very large or very small fractional values, they sacrifice some precision in order to allow for faster arithmetic and a much wider range of values compared with the whole number types.
- float: These take up 32 bits and allow us to represent +/- values using a fraction and an exponent to represent the number in binary scientific notation (i.e. 10011.01010 * 2⁰¹¹).
- double: These are the same as floats but can encode even more precision, however they take up more space — 64 bits.
Others (Enum Like):
These ones are not thought of as numerical in the programming languages we use, but under the surface they are in fact just numbers that are interpreted differently.
- boolean: These are represented by 1 bit (0 for False 1 for True). This is the smallest primitive data type in Java and can sometimes be used in the place of an int to greatly reduce memory size. For instance, turning an int based binary mask into a boolean based one reduces memory size by 32x.
- char: A char is the building block of a String — which is an array of chars. As far as binary representation, they take the same bit space as shorts (16 bits). They are interpreted using unicode tables, which map bit values to human readable characters.
2’s Complement
Most of our numbers use 2’s Complement in order to encode negative numbers.
Here’s a step-by-step explanation of how two’s complement works using a 4-bit number system (0 to 15) and turning it into (-8 to 7):
Positive Numbers:
Positive integers are represented in standard binary form. For example, the decimal number 5 is represented as 0101
in binary in a 4-bit number.
Negative Numbers:
To represent a negative integer, start with its positive binary representation. Then, invert (flip) all the bits (change 0s to 1s and vice versa). Add 1 to the inverted binary representation.
For example, let’s consider the decimal number -5:
- Positive binary representation of 5:
0101
- Invert all bits:
1010
- Add 1 to the front of the binary:
1011
- So, -5 is represented as
1011
in two's complement. Which you can think of as -8 (most significant bit * -1) + rest of bits (2 bit + 1 bit) = -5.
Arithmetic Operations:
Two’s complement simplifies arithmetic operations because addition and subtraction can be performed using the same set of rules, regardless of whether the numbers are positive or negative.
For example, to add two numbers, align them and add bit by bit, including any carry. If an overflow occurs, it wraps around, creating a consistent behavior for addition.
Let’s add 4 and 5 and see this wrap-around behavior.
0100
+0101
1001
= -7
Starting from 0 and adding one, we will see the order for the wrap-around goes.
0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 (MAX) -> -8 (MIN) -> -7 -> -6 -> -5 -> -4 ->
-3 -> -2 -> -1 -> 0...
Floating Point
Floating-point representation is a method used in computers to represent real numbers (those with fractional parts) in a way that allows a wide range of values to be represented with a reasonable trade-off at the expense of precision. They are called floating points because the value of the exponent will move — or float — the decimal point around in the bit string.
Components of a Floating-Point Number:
- Sign bit: Represents the sign of the number (positive or negative).
- Exponent: Represents the power to which the base (usually 2) is raised.
- Fraction (or Mantissa): Represents the fractional part of the number.
Normalized Form:
- The representation is normalized, meaning that the binary point is placed after the first non-zero digit of the fraction. So if our number was
010.011
. We would represent the fractional part as as0011.
Binary Scientific Notation:
- The general form is
(-1)^s * 1.f * 2^e
, where: s
is the sign bit.f
is the fraction (mantissa) in binary.e
is the exponent in binary, often with a bias (-127) to allow for both positive and negative exponents.
As an example, suppose we have the number 0101.01101
. We would represent our sign bit as 0
, we would set our mantissa to be 0101101
and we would need our exponent to represent 2 right shifts (so 129 to account for the -127 bias) 10000001
. This would then let us do 0101101
>> 10
= 101.01101
.
Conclusion
In conclusion, our journey through number primitives and bit widths has unveiled the intricate dance of ones and zeros shaping the digital landscape. From understanding two’s complement to exploring floating-point representation, we’ve navigated the binary ballet that underlies modern computing.