Primitive Type Conversions
March 31, 2009 Leave a comment
Type conversions between primitive types in Java can be broadly categorized as follows:
- Widening conversions
- Implicit narrowing conversions
- Narrowing conversions requiring explicit cast
- Numeric promotion
Only numeric types can participate in type conversions. These types include the integer types char
, byte
, short
, int
and long
and the floating-point types float
and double
. boolean
is not type-compatible with any other primitive data type i.e. boolean
values can’t be converted to other primitive types and vice-versa.
The table that follows presents a summary of the sizes and bit representations of these types, that will go a long way in understanding the conversions better.
Type | Bit representation |
char | 16 bits; Unsigned Unicode UTF-16 |
byte | 8 bits; Signed 2’s complement |
short | 16 bits; Signed 2’s complement |
int | 32 bits; Signed 2’s complement |
long | 64 bits; Signed 2’s complement |
float | 32 bits; IEEE 754-1985 |
double | 64 bits; IEEE 754-1985 |
Widening and Narrowing
A type conversion is termed widening when it occurs from a narrower data type to a broader data type. Narrower and broader here mean that the set of valid values of the narrower type is a subset of that of the broader type. The picure below shows the widening conversions. The direction of the arrows represent the direction of widening.
- Figure 1: Widening primitive conversions
All other conversions are termed narrowing as they convert a wider type to a narrower type. Note that conversions between char and byte/short are narrowing. Byte is a smaller type (8 bits). Short is same size as a char (both 16 bits), but short is signed whereas char is unsigned. So a short can’t cover the entire range of valid char values.
Let’s now look at the conversions and the contexts in which they can occur in more detail.
1. Widening conversions
Widening conversions are done implicitly and don’t require a cast. A cast, though redundant, can still be used. Lets look at the following assignments.
int a = 5; long b = a; //(1) long c = (long) a; //(2)
At (1), int
is implicitly converted to a long
. At (2), int
is converted to a long
using a redundant but valid cast.
Resulting value:
Since the destination type can hold all values in the range of the source type, destination type being broader, there is no loss of magnitude. However there can be a loss of precision when the source type is an integer type and the destination type is a floating-point type. Although the range of magnitudes of floating-point types is larger than any integer types, floating-point types store only a limited precision which may result in loss of precision of the least significant digits. Lets consider the following example.
long x = 12345676899L; float y = x; // (1) System.out.println("y = " + y);
outputs
y = 1.23456768E10
The widening conversion at (1) results in a loss of precision as is evident from the output.
Context:
A widening conversion can occur in the context of assignment as well as parameter passing during method invocation. Numeric Promotion, discussed in section 4 below, is actually a special form of widening conversion that occurs under some specific conditions as we’ll see.
Mechanism:
A widening conversion between integer types is done by extending the sign (leftmost) bit to take up newly added higher order bits. This preserves the original value as per 2’s complement notation. A widening conversion from a char
to an integer type is done by setting higher order bits to zero. Conversion from integral to floating-point values involves conversion from 2’s complement to IEEE 754-1985 notation.
2. Implicit narrowing conversions
Narrowing conversions can occur implicitly when all of the following conditions are satisfied:
- The source value is a constant expression i.e. an expression involving only literals or compile-time constants
- The source type is one of
byte
,char
,short
orint
- The destination type is one of
byte
,char
orshort
- The source value is within the valid range of values for the destination type
For example, we can assign an int
value to a byte
or short
variable without needing a cast if the source expression is constant and the value of the expression is within the valid range for the destination variable type.
int x = 12; final int y = 12; byte a = 12; // (1) OK byte b = y; // (2) OK // byte c = 130; // (3) Not OK // byte d = x; // (4) Not OK
In the code above, the assignment at (1) results in an implicit conversion from int
literal 12
to byte
since 12
is a constant expression and is within the range of byte
type. Assignment at (2) also works since y
is a constant expression. Assignment at (3), does not compile since the source value is outside the valid range for byte
type. The assignment at (4) doesn’t compile since the source is not a constant expression. The assignments at (3) and (4) require an explicit cast to compile as we’ll see in next section.
Context:
An implicit narrowing conversion can occur only during assignments and not during method invocation. The reason for this restriction is to simplify the process of binding a method invocation to a method definition in case of method overloading.
Mechanism:
A narrowing conversion is done by discarding the higher order extra bits in the 2’s complement bit representation of the source value. For example, in a conversion from int
to short
, the higher order (i.e. leftmost) 16 bits are discarded and the remaining 16 bits constitute the new short
value. Since for an implicit narrowing conversion, the source value is within the range of the destination type, this means that the discarded bits are all 0s (in case of a positive value) or all 1s (in case of a negative value). Thus discarding the extra higher order bits affects neither the magnitude nor the sign when destination type is short
or byte
. When destination type is char
, the resulting bit pattern is interpreted as a Unicode-16 char
value.
Resulting value:
Since a pre-requisite for an implicit narrowing conversion is that the source value be within the valid range of the destination type, such conversions don’t result in any loss of information.
3. Narrowing conversions requiring explicit cast
Any narrowing conversion that does not satisfy the conditions for an implicit conversion require an explicit cast. For example, the assignments at (3) and (4) in the code above can be rewritten with an explicit cast to remove the compilation errors:
byte c = (byte) 130; // (5) OK byte d = (byte) x; // OK
Context:
An explicit narrowing conversion can occur during assignments and parameter passing.
Mechanism:
The narrowing between integer types is done as described in the previous section. Narrowing from floating-point to integer types involves conversion from IEEE 754-1985 to 2’s complement notation.
Resulting value:
When source value is larger than the range of the destination type, the discarded bits actually contributed towards the magnitude in the original value and hence there is a loss of magnitude. Also, the highest order (leftmost) bit in the resulting value is interpreted as the sign bit as per 2’s complement notation. This may be different from the highest order bit in the original value and hence the sign of the value may change. For example, if we print the value of the variable c
declared at (5) above:
System.out.println("c = " + c);
the output is
c = -126
At (5), c
is assigned int literal 130
which is 00000000000000000000000010000010
. Discarding the 24 high order bits results in 10000010
which is -126
when interpreted as per 2’s complement notation. Both the magnitude and sign got changed.
Both char
and short
types are 16 bits each however char
is unsigned whereas short
is a signed type. Conversions between them can also result in loss of information.
Floating-point values get rounded down towards zero when converted to integer types. For example,
float x = 12.5F; int y = (int) x; System.out.println("y = " + y);
outputs
y = 12
When the floating-point value is outside the range of the integer type, the result is Integer.MIN_VALUE or Integer.MAX_VALUE.
Conversion from double
to float
type can also result in loss of magnitude as well as precision. Specifically, double
values that are too large for the float
type result in +Infinity
or -Infinity
. double
values that are too small result in +0.0
or -0.0
.
4. Numeric promotion
Numeric promotion is a widening conversion that applies implicitly to the operands of numeric operators under specific contexts. There are two types of numeric promotions:
- Unary numeric promotion applies to the operand of a unary operator or to the individual operands of a binary operator. The rule states that if the type of the operand is narrower than
int
, then it is converted toint
, otherwise it is not changed. Unary numeric promotion is applied to the following:- Operand of unary + and – operators and unary negation operator ~
- Each individual operand of the binary shift operators <<, >> and >>>
- Size expression in array creation using “
new arr[size]
” and index expression in array element access using “arr[index]
“
- Binary numeric promotion applies to the operands of a binary numeric operator taking both the operands into account together. The rule states that if T is the broader type of the types of the two operands, then both the operands are converted to int if T is narrower than
int
, otherwise both are converted to T. Binary numeric promotion is applied to the following:- Operands of arithmetic operators +, -, *, /, %
- Operands of relational operators <, <=, >, >= and equality operators == and !=
- Operands of integer bitwise operators &, ^ and |
Numeric promotion is applied during the evaluation of an expression. After the evaluation, implicit or explicit type conversions may be required for the assignment of the evaluated value to a variable. For example,
short a = 10; short b = (short) (a + 10); // (1)
At (1) above, binary numeric promotion occurs during evaluation of a + 10
, a
‘s value is widened to int
and int
10
is added. The result is an int
value of 20
which requires an explicit short
cast to be assigned to a short
variable b
. Let’s look at another example:
float a = 12.5; int b = 5; byte c = 10; float d = a * ( b + c ) // (1)
Evaluation of a * ( b + c )
at (1) above proceeds as follows (in terms of types):
float * ( int + byte ) => float * ( int + ( byte => int ) ) => float * ( int + int ) => float * int => float * ( int => float ) => float * float => float
Compound assignment operators (+=, -=, *=, /=, %=) and unary increment/decrement operators (++, –) apply a cast implicitly. For example, for an initialized byte variable a
a = (byte) (a + 10); // requires explicit cast a += 10; // doesn't require a cast; cast is implicit a = (byte) (a + 1); // requires explicit cast a++; // doesn't require a cast; cast is implicit
Context:
A numeric promotion occurs during the evaluation of an expression.
Mechanism:
A numeric promotion is a widening conversion and uses the same mechanism as a normal widening conversion.
Effect on resulting value:
There is no loss of information during a numeric promotion.
Other types of conversions involving primitive types
- Boxing/unboxing: Boxing and unboxing refers to implicit conversions from primitive to the corresponding wrapper types and vice-versa. Such conversions occur automatically in the context of both assignment and parameter passing.
- String concatenation: + operator is an overloaded operator that works with String operands to effect concatenation. When one of the operands of the + is a String, the operator works as concatenation operator resulting in another String. If one of the operands is not a String, it automatically gets converted to a String before applying the concatenation. In case of primitive types, the converted String is a string representation of the operand’s value. In case of references, the String is obtained by calling the toString() method on the operand.