Arithmetic

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.MIN_VALUE - 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 should be equal and yet ...
5.0 * 2.0 / 3.0 != 5.0 * (2.0 / 3.0)

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

So what do we do?

Most of the time

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.

Slightly more sophisticated

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 and accurate 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.

Java’s arithmetic operators

+ - addition

- - subtraction

* - multiplication

/ - division

% - remainder

Not operators

** - nothing in Java. (It means exponentiation in Javascript)

^ - also not exponentiation. (We’re not in Desmos any more Toto!)

Order of operations

Arithmetic operators and parentheses have basically normal PEMDAS precedence.

Between operators with the same precedence (+ and - are the same while *, /, and % are also grouped) operators are evaluated from left to right.

Casting

Because we have two types of numbers we need a way to convert between them.

The cast operator (int) and (double) operates on a single numeric value and converts it to the given type.

(int) 3.14 ⟹ 3

(double) 10 ⟹ 10.0

Casting a double to an int truncates the value, chopping off the fractional part. It does not round to the nearest integer.

(int) 1.99999 ⟹ 1

Cast has very high precedence

The cast operator is higher precedence than all the arithmetic operators but below parenthesis.

Doubles are zombies

Arithmetic operators applied to two ints return an int. The result will be truncated and may over- or underflow.

Arithmetic operators applied to two doubles return a double.

Arithmetic operators applied to an int and a double return a double.

Examples

(int) Math.random() * 10 ⟹ 0
(int) (Math.random() * 10) ⟹ 0 to 9 (inclusive)
(double) 3 / 4 ⟹ 0.75
(double) (3 / 4) ⟹ 0.0
3 / 4 * 1.0 ⟹ 0.0
1.0 * 3 / 4 ⟹ 0.75

Division by zero

Dividing by zero with ints results in an ArithmeticException.

Dividing by zero with doubles results in the special value Double.POSITIVE_INFINITY