Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are Java integer-type primitive casts "capped" at the MAX_INT of the casting type?

I was trying to track down some very weird Java behavior. I have a formula that involves a double, but is "guaranteed" to give an integer answer -- specifically, an unsigned 32-bit integer (which, alas, Java doesn't do well). Unfortunately, my answers were sometimes incorrect.

Eventually I found the issue, but the behavior is still very odd to to me: a double cast directly to an int seems to be capped at the MAX_INT for a signed integer, whereas a double cast to a long that is then cast to an int gives me the expected answer (-1; the MAX INT of an unsigned 32-bit integer represented as a signed 32-bit integer).

I wrote a little test program:

public static void main(String[] args) {
    // This is the Max Int for a 32-bit unsigned integer
    double maxUIntAsDouble = 4294967295.00;
    long maxUintFromDoubleAsLong = (long)maxUIntAsDouble;
    long maxUintFromDoubleAsInt = (int)maxUIntAsDouble;
    int formulaTest = (int) (maxUintFromDoubleAsLong * 1.0);
    int testFormulaeWithDoubleCast =  (int)((long) (maxUintFromDoubleAsLong * 1.0));
    // This is a more-or-less random "big number"
    long longUnderTest = 4123456789L;
    // Max int for a 32-bit unsigned integer
    long longUnderTest2 = 4294967295L;
    int intFromLong = (int) longUnderTest;
    int intFromLong2 = (int) longUnderTest2;
    System.out.println("Long is: " + longUnderTest);
    System.out.println("Translated to Int is:" + intFromLong);
    System.out.println("Long 2 is: " + longUnderTest2);
    System.out.println("Translated to Int is:" + intFromLong2);
    System.out.println("Max UInt as Double: " + maxUIntAsDouble);
    System.out.println("Max UInt from Double to Long: " + maxUintFromDoubleAsLong);
    System.out.println("Max UInt from Double to Int: " + maxUintFromDoubleAsInt);
    System.out.println("Formula test: " + formulaTest);
    System.out.println("Formula Test with Double Cast: " + testFormulaeWithDoubleCast);
}

When I run this little program I get:

Long is: 4123456789
Translated to Int is:-171510507
Long 2 is: 4294967295
Translated to Int is:-1
Max UInt as Double: 4.294967295E9
Max UInt from Double to Long: 4294967295
Max UInt from Double to Int: 2147483647
// MAX INT for an unsigned int
Formula test: 2147483647
// Binary: all 1s, which is what I expected
Formula Test with Double Cast: -1

The bottom two lines are the ones I'm trying to understand. The double cast gives me the expected "-1"; but the straight cast gives me MAX_INT for a 32-bit signed integer. Coming from a C++ background, I would understand if it gave me an "odd number" instead of the expected -1 (aka "naive casting"), but this has me perplexed.

So, to the question then: is this "expected" behavior in Java (e.g. any double cast directly to an int will be "capped" to MAX_INT)? Does casting do this for any unexpected types? I would expect it to be similar for short and byte, for instance; but what is the 'expected behavior' when casting an oversized-double to float?

Thanks!

like image 968
John Price Avatar asked May 17 '12 18:05

John Price


2 Answers

This is expected behavior. Remember that there are no primitive unsigned long or int types in Java, and the Java Language Specification (Java 7) for Narrowing primitive conversion (5.1.3) states that casting a "too small or too large" floating point value (be it double or float) to an integral type of int or long will use the minimum or maximum value of signed integral types (emphasis mine):

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, or to an int, if T is byte, short, char, or int, as follows:

    • If the floating-point number is NaN (§4.2.3), the result of the first step of the conversion is an int or long 0.
    • Otherwise, if the floating-point number is not an infinity, the floating-point value is rounded to an integer value V, rounding toward zero using IEEE 754 round-toward-zero mode (§4.2.3). Then there are two cases:

      • a. If T is long, and this integer value can be represented as a long, then the result of the first step is the long value V.
      • b. Otherwise, if this integer value can be represented as an int, then the result of the first step is the int value V.
    • Otherwise, one of the following two cases must be true:

      • a. 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.
      • b. 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. *
  2. In the second step: * If T is int or long, the result of the conversion is the result of the first step. * If T is byte, char, or short, the result of the conversion is the result of a narrowing conversion to type T (§5.1.3) of the result of the first step.

Example 5.1.3-1. Narrowing Primitive Conversion

class Test {
    public static void main(String[] args) {
        float fmin = Float.NEGATIVE_INFINITY;
        float fmax = Float.POSITIVE_INFINITY;
        System.out.println("long: " + (long)fmin + ".." + (long)fmax);
        System.out.println("int: " + (int)fmin + ".." + (int)fmax);
        System.out.println("short: " + (short)fmin + ".." + (short)fmax);
        System.out.println("char: " + (int)(char)fmin + ".." + (int)(char)fmax);
        System.out.println("byte: " + (byte)fmin + ".." + (byte)fmax);
    }
}

This program produces the output:

long: -9223372036854775808..9223372036854775807
int: -2147483648..2147483647
short: 0..-1
char: 0..65535
byte: 0..-1

The results for char, int, and long are unsurprising, producing the minimum and maximum representable values of the type.

The results for byte and short lose information about the sign and magnitude of the numeric values and also lose precision. The results can be understood by examining the low order bits of the minimum and maximum int. The minimum int is, in hexadecimal, 0x80000000, and the maximum int is 0x7fffffff. This explains the short results, which are the low 16 bits of these values, namely, 0x0000 and 0xffff; it explains the char results, which also are the low 16 bits of these values, namely, '\u0000' and '\uffff'; and it explains the byte results, which are the low 8 bits of these values, namely, 0x00 and 0xff.

The first case int formulaTest = (int) (maxUintFromDoubleAsLong * 1.0); thus promotes maxUintFromDoubleAsLong to a double via multiplication and then casts it to an int. Since the value is too large to represent as a signed integer, the value becomes 2147483647 (Integer.MAX_VALUE) or 0x7FFFFFFF.

As for the latter case:

A narrowing conversion of a signed integer to an integral type T simply discards all but the n lowest order bits, where n is the number of bits used to represent type T. In addition to a possible loss of information about the magnitude of the numeric value, this may cause the sign of the resulting value to differ from the sign of the input value.

So int testFormulaeWithDoubleCast = (int)((long) (maxUintFromDoubleAsLong * 1.0)); first promotes maxUintFromDoubleAsLong to double, back to long (still fitting) and then to an int. In the last cast, the excess bits are simply dropped, leaving you with 0xFFFFFFFF, which is -1 when interpreted as a signed integer.

like image 116
esaj Avatar answered Sep 30 '22 18:09

esaj


This is just the way the language spec is written. Converting a floating-point to an integer type, if the value is too large for the destination then the maximum value is substituted. In a narrowing conversion from one integer type to a smaller one, the high-order bits are discarded.

See the JLS 5.1.3. Narrowing Primitive Conversion

So, the answer to the question in the title is "yes".

like image 40
Jim Garrison Avatar answered Sep 30 '22 17:09

Jim Garrison