Why does writing a number in scientific notation make a difference in this code?
Asked Answered
R

3

24

I am trying to write a code to determine when the number of milliseconds since the beginning of 1970 will exceed the capacity of a long. The following code appears to do the job:

public class Y2K {
    public static void main(String[] args) {
        int year = 1970;
        long cumSeconds = 0;

        while (cumSeconds < Long.MAX_VALUE) {
            // 31557600000 is the number of milliseconds in a year
            cumSeconds += 3.15576E+10;
            year++;
        }
        System.out.println(year);
    }
}

This code executes within seconds and prints 292272992. If instead of using scientific notation I write cumSeconds as 31558000000L, the program seems to take “forever” to run (I just hit pause after 10 mins or so). Also notice that writing cumSeconds in scientific notation does not require specifying that the number is a long with L or l at the end.

Refract answered 18/9, 2015 at 18:1 Comment(4)
31557600000 is the number of milliseconds in an average year on the Julian calendar. For the Gregorian calendar, it's 31556952000.Predisposition
Alright, I'll say it. This is not a good variable name.Gestation
In case doing this without a loop is of interest to you; you can use division: System.out.println( 1970 + (long)Math.ceil( Long.MAX_VALUE/31557600000L ) );Suzetta
@isanae: Stack has been overflowed with laughters. Made my day.Counterproductive
P
41

The reason it makes a difference is because the scientific notation number 3.1558E+10 is a double literal, whereas the literal 31558000000L is of course a long literal.

This makes all the difference in the += operator.

A compound assignment expression of the form E1 op= E2 is equivalent to E1 = (T) ((E1) op (E2)), where T is the type of E1, except that E1 is evaluated only once.

Basically, long += long yields a long, but long += double also yields a long.

When adding a double, the initial value of cumSeconds is widened to a double and then the addition occurs. The result undergoes a narrowing primitive conversion back to long.

A narrowing conversion of a floating-point number to an integral type T takes two steps:

  1. In the first step, the floating-point number is converted either to a long, if T is long

(snip)

  • Otherwise, one of the following two cases must be true:

    • The value must be too small (a negative value of large magnitude or negative infinity), and the result of the first step is the smallest representable value of type int or long.

    • The value must be too large (a positive value of large magnitude or positive infinity), and the result of the first step is the largest representable value of type int or long.

(bold emphasis mine)

The result eventually is too big to be represented in a long, so the result is narrowed to Long.MAX_VALUE, and the while loop ends.

However, when you use a long literal, you are continuously adding an even value to an even value, which will eventually overflow. This does not set the value to Long.MAX_VALUE, which is odd, so the loop is infinite.

But instead of relying on an addition eventually yielding Long.MAX_VALUE, with Java 1.8+ you can explicitly test for overflow with Math.addExact.

Returns the sum of its arguments, throwing an exception if the result overflows a long.

Throws:

ArithmeticException - if the result overflows a long

Push answered 18/9, 2015 at 18:15 Comment(2)
Note: this also means that x += 0.0 or even x += 0.0f can cause a long to lose precision.Fenderson
Any pointers to use Math.addExact() or something to detect an overflow?Roxana
P
5

The key observation is that cumSeconds < Long.MAX_VALUE where cumSeconds is a long can only be false if cumSeconds is exactly Long.MAX_VALUE.

If you do the calculation with long numbers it takes a quite some time to reach this value exactly (if it is ever reached) because long arithmetic wraps around when you leave the number range.

Doing the arithmetic with double numbers will yield the max value when the double value is large enough.

Plenipotentiary answered 18/9, 2015 at 18:16 Comment(0)
N
5

@rgettman has already gone into detail about the round-off gymnastics that take place when you're using a double instead of a long. But there's more.

When you repeatedly add a large number to a long, you'll eventually end up with a negative result. For example, Long.MAX_VALUE + 1L = Long.MIN_VALUE. When that happens, you'll just repeat the process indefinitely.

So if you changed your code to:

    while (cumSeconds >= 0L) {
        // 31557600000 is the number of milliseconds in a year
        cumSeconds += 31557600000L;

you'll catch where things go negative because cumSeconds rolled over.

Nicolais answered 18/9, 2015 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.