double n = 123.4567, o = 123.4567, p = 123.4567;
n *= 100;
n = Math.round(n);
n = n / 100;
System.out.println(n);
System.out.println(Math.round(o * 100) / 100);
System.out.println(((double) Math.round(p * 100)) / 100);
I was playing around with easy ways to round a decimal number to 2 points and found this.
Output:
123.46
123
123.46
Is there a difference between the first and second approaches that I'm just not seeing? They should be identical algorithms, just compressed into one line. Why is a cast needed when it's all in one line and not needed when in separate lines?
You're missing two important things:
/
means different things depending on context./
means different thingsThe key thing you're missing is that /
means 2 different things in java; context determines which of the 2 you get. In your first and second snippets, the /
mean different things.
In the first and third, it means 'double division'. In the second, it means 'integer division'.
These 2 things are as different as guns and grandmas. Consider how +
means completely different things. Given a + b
, well:
String a = "Hello";
String b = "World";
String c = a + b; // means string concatenation
int a = 5;
int b = 7;
int c = a + b; // means integer addition
You should consider /
to be the same way.
In general all java math operations are homogenous. Meaning, java knows how to add an 'int' to an 'int' but cannot deal with the situation when the two arguments aren't of the same type. To 'fix' that, you will have to convert one of the two sides to be the same type as the other side. To make things a bit complicated, javac itself will assume you meant to do just that and will convert one of them for you, if it can: The rulebook states that java will only apply widening conversions - once that are generally considered lossless. For example, converting an int to a double is considered lossless. Converting an int to a short is not (a short
can contain fewer values than int
can).
Given:
int a = 11;
int b = 10;
double c = a / b;
System.out.println(c);
You might think that prints "1.1". It will not. Try it - run that. It'll print simply '1'.
That's because That slash there means integer division: Because both of its arguments are of type int
, int division is what it means. The fact that you then assign the result of this to a variable of type double
is not relevant to this determination. So, a / b
is integer division, in integer division, all remainder is just lopped off (so, rounding down for positive numbers but rounding up for negative numbers, -5.5 becomes -5), producing just 1. This is then assigned to a double
which requires conversion. Because that is 'widening', java injects the cast for you silently, there is no need to explicitly write it, and c
is now 1.0.
The same is happening your code: The second snippet is dividing an integer by an integer (Because the return type of the round
method in the Math
class is int
), thus, you get integer math.
When you use /
and the operands are of mixed types, one of type double
and the other of int
, java will silently widening-convert the int to a double, so that just 'works':
double x = 11.0;
int y = 10;
double z = x / y;
System.out.println(z); // really will print '1.1'
Hence why a cast can be necessary:
int x = 11;
int y = 10;
double z = (double) x / y;
System.out.println(z); // really will print '1.1'
Here the /
expression's left hand side is the expression (double) x
which is of type double
, the RHS is of type int
, so that's not possible without conversion - but int
can be silently converted to double
so java compiles that as if you wrote: (double) x / (double) y
which is double division, and that whole expression is of type double, so can be assigned verbatim to z
.
doubles are imprecise. In weird ways. If I ask you to write the result of 1 divided by 3 on a small piece of paper you cannot do this - you write 0.33333 and run out of room.
Computers are the same way: They have limited space (64 bit, for double
), and store decimals and not ratios (you can't write "1 / 3" in this system, only "0.3333"). Except, computers don't use decimal, they use binary.
The reason "1 / 10" 'works' (It's 0.1, no infinite sequence of digits) is because the divisors of 10 are 2 and 5, so any divisor that can be expressed solely as multiplying any number of 2s and 5s together 'works' (10 = 2 * 5
- that works), and anything else won't.
In binary, it's just multiples of 2.
This means 0.1 is not a double and causes rounding errors. Try it! Write a java file that just does this:
double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(1.0 == v);
And you will marvel! Because that prints false
and that seems incorrect. It's 'correct' in the same sense that if I ask you to write 1/3 on a piece of paper three times and I then ask you to tell me the sum of those 3 pieces of paper, you won't tell me 1. No, you'd tell me '0.999999'. Which is very close to, but not quite equal to, 1.0.
Hence, you cannot think of double
in this way - they cannot be used to hold exactly 'some number rounded to 2 decimals' because a thing that works in decimal (such as "0.10") may not work in binary.
Instead, you have 3 options:
Whatever you're doing, find out if it has a workable atomic unit and just use those. For example, if doing finances, don't store the price of an object that costs 4 bucks and 20 cents as double price = 4.2;
. No, store it as int priceInCents = 420
.
But, not everything has an atomic unit.
Accept that slight rounding errors will occur. NEVER try to round a number itself, instead, when printing it, round then:
double v = 1.2345; // v's value is not quite 1.2345, but something very close.
System.out.printf("%.2f\n", v);
That prints 1.23. Because '%.2f' rounds to 2 digits. Don't round the value, ask the thing that prints to round.
Use java's BigDecimal class which represents decimal numbers to any precision you want. But, this does mean:
Generally BD is overkill. Don't use it unless you really, really know what you are doing.
Always use long
to represent your finances (unless you really really know what you are doing and think BD is a better idea. It rarely is). No matter what happens, double
is never the correct data type for finance.
And, of course, represent atomics. So, cents for euros and dollas, yen for yen, satoshis for bitcoin, pennies for pounds, and so on.