Math is hard

Math is infinite.

Computers are finite.

Thus computer math is full of compromises.

Integer compromises

In order to make int math fast, ints are limited to a fixed range of values from -2,147,483,648 to 2,147,483,647 (a.k.a. -231 to 231 - 1) and division truncates so it still produces an int.

Consequences

Division can lose arbitrarily large amounts of precision, e.g. 99999/100000 == 0

Addition and subtraction “overflow”, wrapping around from the largest positive int to the largest negative and vice versa.

The largest negative value, also called the “weird number”, has no positive counterpart. Thus multiplying it by -1 or taking its absolute value also wraps around to itself! Weird.

Examples

Integer.MAX_VALUE + 1 == Integer.MIN_VALUE // overflow

Integer.MAX_MIN - 1 == Integer.MAX_VALUE // overflow

Integer.MAX_VALUE / 2 * 2 == 2147483646 // truncation loses 1

Integer.MAX_VALUE * 2 / 2 == -1 // wrapping way around

-1 * Integer.MIN_VALUE == Integer.MIN_VALUE // weird

Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE // weird

Double compromises

The double type is more complex and works well to represent scientific kinds of numbers from very small (4.9E-324) to very large (1.7976931348623157E308) and has more than 9 quadrillion gradations between 0.0 and 1.0 while still allowing for pretty efficient computation.

But it has a bunch of compromises built in such as limited precision (about 16 decimal places) and the fact that it is a binary, rather than decimal, format.

Consequences

Values requiring more precision than a double can represent are only approximate.

Order of operations matters because of rounding of intermediate results.

Particularly unexpected

Values with fractional parts that can’t be expressed with an integral power of 2 in the denominator can’t be represented exactly.

This is for the same reason that 1/3 can’t be represented exactly in a finite decimal number.

This includes such common numbers as 0.1, and 0.05.

On the other hand, 0.5, 0.25, and 0.125 are all fine.

Examples

// Adding or subtracting very large and very small
// numbers loses the small number entirely
1e9 + 1e-9 == 1e9
1e16 - 1 == 1e16

// We'd expect 0.1 + 0.2 to equal 0.3
0.1 + 0.2 == 0.30000000000000004

// Mathematically these are the same
(1.2 + 1.1) * (1.2 - 1.1) != (1.2 * 1.2) - (1.1 * 1.1)

// 0.1 can't be represented exactly. (Neither of these
// are actually the exact value.)
0.1 == 0.10000000000000001

So what do we do?

For this course

You mostly just need to be aware of these issues.

In particular know that == can provide surprising results with doubles since values that should be the same mathematically may not be exactly the same.

In the real world

In the real world, if the limits of ints are just a bit too confining we can sometimes use Java’s long datatype which is a 64-bit integer and which is basically just as fast on modern CPUs.

For doing efficient computation with doubles there’s a whole field called Numerical Analysis devoted to figuring out the best ways to do complex calculations while minimizing the loss of precision.

And if that’s not enough

There’s also a class called BigInteger that lets us do math with arbitrarily large integer values, at the cost of some speed.

Similarly, there’s a class BigDecimal that can represent arbitrary precision decimal numbers with control over how many decimal places are used for unrepresentable values like 1/3.

Money

Serious computations involving money are very particular and there tend to be special purpose classes for dealing with them.

Whatever you do, don’t use doubles to represent money as they can’t even accurately represent a nickle or a dime!

In a pinch use ints or longs to count pennies and only convert to dollars and cents when displaying values to humans.