Search code examples
javaconstructorfloating-pointjoml

Why is it that when I call the constructor with Float.MIN_VALUE parameters, they are 0?


So I have a Vector3f class with a constructor:

public Vector3f(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

but when I call a constructor like this:

Vector3f max = new Vector3f(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);

and print the values to the console:

System.out.printf("x: %f, y: %f, z: %f\n", max.x, max.y, max.z);

the output will be

x: 0.000000, y: 0.000000, z: 0,000000

Why 0?

I used Vector3f from the Java OpenGL Math Library (JOML).


Solution

  • Your question can be read in two completely different ways. I can answer them both.

    You expected it to be a very large negative number

    That's just not what MIN_VALUE does. However, your confusion is understandable; Integer.MIN_VALUE is a very large negative number. The concept doesn't apply to float/double, as they have a value that represents negative infinity, which makes 'MIN_VALUE' in the sense of 'that number that is more negative than any other number float can represent' dubious / non-existent. The name is still unfortunate but java does not as a rule change things without a better reason than 'naming is confusing' to avoid backwards compatibility issues.

    Float.MIN_VALUE represents the smallest positive representable float that is not equal to 0.

    I know that. But it prints 0!

    The problem with float and double is that they are imprecise. If I hand you a postit and ask you to write 1/3 in decimal on it, you can't. You'll write 0.3333 and eventually run out of space. double and float are more or less the same, except in binary except decimal which is particularly painful because only stuff that fits in 2s can be accurately represented. We can do 1/10th in decimal (0.1, after all). In IEEE math that is as problematic as 1/3 is. Almost all fractions are problematic, except 1/2, 3/4th, etc. (the divisor a power of 2 - that's allright).

    Let's see it in action:

    double v = 0.0;
    for (int i = 0; i < 10; i++) v += 0.1;
    System.out.println(v == 1.0);
    

    That prints... oof, false, which is wrong. 10 * 0.1 is obviously 1.0. But not in IEEE double math, evidently. Just like the sum of 3 postits with your best effort at 1/3 is not quite 1.0 (it'll be 0.9999999), we run into the same problem here.

    So how does that lead to print 0.0000?

    Because the number is stored in binary but printed in decimal, and that results in some real wonky and unexpected behaviour if println would print the exact value. You'd get 0.9999999999999999999876 a lot, which is annoying. So, instead, printf/println just takes a wild stab in the dark and does some dubious rounding to make it all look right when none of it is.

    This is why you should never just print a double. Instead use, as you are, printf / String.format. And specify how many digits you want, so you are in control of this necessary rounding. Without an explicit specifier, %f defaults to 6 digits. And 'the smallest non-zero positive float', rounded so that it fits in 6 digits, is 0.000000. You need way more digits to see this tiny, tiny number:

    System.out.printf("%.99f\n", Float.MIN_VALUE);
    
    > 0.0000000000000000000000000000000000000000000014012984643248170000000
    

    That's how many digits you'd need to see it.

    Some important notes

    • float is useless. double is just as fast or faster, takes just as much space in 99% of all cases (except float[] which is usually a bit smaller, but that's the only place where it tends to come up), and is less precise. Just don't use it - double instead.
    • Another way to solve for this 'its inaccurate' issue is to not use floats/doubles at all. Find an atomic unit and represent those with int/long, or use BigDecimal. For GUI purposes the inaccuracy is usually worth it, though.