Type Conversions in an Assignment Context – Basic Elements, Primitive Data Types, and Operators

Type Conversions in an Assignment Context

If the target and the source have the same type in an assignment, then obviously the source and the target are assignment compatible and the source value need not be converted. Otherwise, if a widening primitive conversion is permissible, then the widening conversion is applied implicitly; that is, the source type is converted to the target type in an assignment context.

Click here to view code image

// Widening Primitive Conversions
int    smallOne = 1234;               // No widening necessary.
long   bigOne   = 2020;               // Widening: int to long.
double largeOne = bigOne;             // Widening: long to double.
double hugeOne  = (double) bigOne;    // Cast redundant but allowed.

A widening primitive conversion can result in loss of precision. In the next example, the precision of the least significant bits of the long value may be lost when it is converted to a float value:

Click here to view code image

long bigInteger = 98765432112345678L;
float fpNum = bigInteger;  // Widening but loss of precision: 9.8765436E16

Additionally, implicit narrowing primitive conversions on assignment can occur in cases where all of the following conditions are fulfilled:

  • The source is a constant expression of type byte, short, char, or int.
  • The target type is of type byte, short, or char.
  • The value of the source is determined to be in the range of the target type at compile time.

A constant expression is an expression that denotes either a primitive or a String literal; it is composed of operands that can be only literals or constant variables, and operators that can be evaluated only at compile time (e.g., arithmetic and numerical comparison operators, but not increment/decrement operators and method calls). A constant variable is a final variable of either a primitive type or the String type that is initialized with a constant expression.

Click here to view code image

int result = 100;               // Not a constant variable. Not declared final.
final char finalGrade = ‘A’;    // Constant variable. ’A’

System.out.printf(“%d%n%s%n%d%n%.2f%n%b%n%d%n%d%n”,
    2022,                       // Constant expression. 2022
    “Trust ” + “me!”,           // Constant expression. “Trust me”
    2 + 3 * 4,                  // Constant expression. 14
    Math.PI * Math.PI * 10.0,   // Constant expression. 98.70
    finalGrade == ‘A’,          // Constant expression. true
    Math.min(2020, 2021),       // Not constant expression. Method call.
    ++result                    // Not constant expression. Increment operator.
);

Here are some examples that illustrate how the conditions mentioned previously affect narrowing primitive conversions:

Click here to view code image

// Conditions fulfilled for implicit narrowing primitive conversions.
short s1 = 10;       // int value in range.
short s2 = ‘a’;      // char value in range.
char c1 = 32;        // int value in range.
char c2 = (byte)35;  // byte value in range. (int value in range, without cast.)
byte b1 = 40;        // int value in range.
byte b2 = (short)40; // short value in range. (int value in range, without cast.)
final int i1 = 20;   // Constant variable
byte b3 = i1;        // final value of i1 in range.

All other narrowing primitive conversions will produce a compile-time error on assignment and will explicitly require a cast. Here are some examples:

Click here to view code image

// Conditions not fulfilled for implicit narrowing primitive conversions.
// A cast is required.
int i2 = -20;            // i2 is not a constant variable. i2 is not final.
final int i3 = i2;       // i3 is not a constant variable, since i2 is not.
final int i4 = 200;      // i4 is a constant variable.
final int i5;            // i5 is not a constant variable.
short s3 = (short) i2;   // Not constant expression.
char  c3 = (char)  i3;   // Final value of i3 not determinable at compile time.
char  c4 = (char)  i2;   // Not constant expression.
byte  b4 = (byte)  128;  // int value not in range.
byte  b5 = (byte)  i4;   // Value of constant variable i4 is not in range.
i5 = 100;                // Initialized at runtime.
short s4 = (short) i5;   // Final value of i5 not determinable at compile time.

Floating-point values are truncated when cast to integral values.

Click here to view code image

// The value is truncated to fit the size of the target type.
float huge   = (float) 1.7976931348623157d;  // double to float.
long  giant  = (long)  4415961481999.03D;    // (1) double to long.
int   big    = (int)   giant;                // (2) long to int.
short small  = (short) big;                  // (3) int to short.
byte  tiny   = (byte)  small;                // (4) short to byte.
char  symbol = (char)  112.5F;               // (5) float to char.

Table 2.19 shows how the values are truncated for assignments from (1) to (5).

Table 2.19 Examples of Truncated Values

The discussion of numeric assignment conversions also applies to numeric parameter values at method invocation (§3.10, p. 129), except for the narrowing conversions, which always require a cast.

The following examples illustrate boxing and unboxing in an assignment context:

Click here to view code image Boolean   boolRef = true;  // Boxing.
Byte      bRef =  2;       // Constant in range: narrowing, then boxing.
// Byte  bRef2 =  257;     // Constant not in range. Compile-time error!

short s = 10;              // Narrowing from int to short.
// Integer   iRef1 = s;    // short not assignable to Integer.
Integer iRef3 = (int) s;   // Explicit widening with cast to int and boxing

boolean bv1 = boolRef;     // Unboxing.
byte  b1 = bRef;           // Unboxing.
int   iVal = bRef;         // Unboxing and widening.

Integer iRefVal = null;           // Always allowed.
// int j = iRefVal;               // NullPointerException at runtime.
if (iRef3 != null) iVal = iRef3;  // Avoids exception at runtime.