Math is infinite.
Computers are finite.
Thus computer math is full of compromises.
In order to make int
math fast, int
s 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
.
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.
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
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.
Values requiring more precision than a double
can
represent are only approximate.
Order of operations matters because of rounding of intermediate results.
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.
// 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
You mostly just need to be aware of these issues.
In particular know that ==
can provide surprising
results with double
s since values that should be the
same mathematically may not be exactly the same.
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 double
s 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.
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.
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 double
s to represent money
as they can’t even accurately represent a nickle or a dime!
In a pinch use int
s or long
s to count
pennies and only convert to dollars and cents when displaying values
to humans.