2.3
Type Conversion, Promotion, Casting
Follow the source type, the conversion rule, and the expression type to predict what Java keeps and what it loses.
Previously on
Section 2.1 defined the primitive types. Section 2.2 showed how fixed-width integers wrap and why binary floating-point values can be rounded.
Now values move between those types. The important question is not whether the current number appears to fit. The compiler starts from the source and destination types, then applies specific conversion and promotion rules.
The problem
These two assignments contain the same numeric value:
byte small = 24;
int wide = small;
int source = 24;
// byte narrow = source; // compile-time error: possible lossy conversionThe first conversion is permitted without a cast. The second is not.
The compiler knows that every byte value fits in int. It cannot infer from an ordinary int variable that its value will always fit in byte. A cast changes the second line from a rejected implicit conversion into an accepted explicit conversion:
byte narrow = (byte) source;That cast is permission to apply Java’s narrowing rule. It is not a promise that the value will survive.
The idea
Widening primitive conversions
Java permits these numeric paths without a cast:
byte -> short -> int -> long -> float -> double
| | | |
+-------+------+-------+
char ------------> int -> long -> float -> doubleThe diagram shows legal language conversions, not guaranteed exactness.
| Source and destination | Exact for every source value? |
|---|---|
byte to short, int, long, float, or double |
yes |
short or char to int, long, float, or double |
yes |
int to long or double |
yes |
int to float |
no |
long to float or double |
no |
float to double |
yes, including its already rounded binary value |
char and short are both 16 bits, but neither contains the other’s full value set. char is 0 through 65,535. short includes negatives and stops at 32,767. Neither converts to the other without a cast.
boolean does not convert to or from a numeric type, even with a cast.
Narrowing primitive conversions
Moving in the other direction usually requires a cast:
long population = 8_100_000_000L;
int reduced = (int) population;
double measurement = 16.9;
int whole = (int) measurement;Different source and destination families use different narrowing mechanisms. Integer to narrower integer keeps low-order bits. Floating point to integer truncates, clamps out-of-range values, and gives NaN a defined result.
Trace one assignment
For every numeric expression, ask these questions in order:
- What is the type of each operand?
- Which promotion happens before the operation?
- What is the type and value of the result?
- Which conversion happens during assignment?
That sequence explains the examples in the rest of the section.
Under the hood
Going deeperNarrowing integers keeps low bits
300 as a 32-bit int ends with these bits:
0010_1100 is 44 as a signed byte, so (byte) 300 is 44.
For (byte) 200, the kept pattern is 1100_1000. Read as an eight-bit signed value, it is 200 - 256 = -56.
System.out.println((byte) 300); // 44
System.out.println((byte) 200); // -56This is not equivalent to Java’s remainder operator for all values:
System.out.println(200 % 256); // 200
System.out.println((byte) 200); // -56The reliable model is: keep the low bits, then interpret them in the destination type.
Floating point to integer has explicit edge rules
For a direct cast to int, Java applies these rules:
| Floating-point source | int result |
|---|---|
| NaN | 0 |
| finite and in range | fractional part discarded toward zero |
| positive value too large, including positive infinity | Integer.MAX_VALUE |
| negative value too small, including negative infinity | Integer.MIN_VALUE |
public class FloatToInt {
public static void main(String[] args) {
System.out.println((int) 16.9);
System.out.println((int) -16.9);
System.out.println((int) Double.NaN);
System.out.println((int) Double.POSITIVE_INFINITY);
}
}Output:
16
-16
0
2147483647A direct cast from floating point to byte, short, or char first follows the int conversion behavior, then narrows that integer to the final width.
Constant-expression narrowing
Java has a compile-time exception to the usual assignment rule. An int constant expression may initialize byte, short, or char when its value is representable.
byte a = 100;
short b = 20_000;
char c = 65;
final int LIMIT = 127;
byte d = LIMIT;
// byte tooLarge = 128; // compile-time error
int runtimeValue = 100;
// byte e = runtimeValue; // compile-time errorLIMIT is a constant variable because it is final, has primitive type, and is initialized with a constant expression. An ordinary variable does not qualify.
This special assignment conversion does not make an integer literal fit a method parameter:
static void accept(byte value) {}
// accept(1); // compile-time error
accept((byte) 1); // validUnary and binary numeric promotion
Most arithmetic does not operate in byte, short, or char. Unary numeric promotion converts them to int for operators such as unary +, unary -, and bitwise ~.
Binary numeric promotion chooses a common type for operators such as +, -, *, /, %, and numeric comparisons:
Binary numeric promotion
- If either operand is doubleConvert the other numeric operand to
double. The numeric result isdouble. - Otherwise, if either operand is floatConvert the other operand to
float. The numeric result isfloat. - Otherwise, if either operand is longConvert the other operand to
long. The numeric result islong. - OtherwiseConvert both operands to
int. This includes byte, short, and char.
That final rule explains this error:
byte a = 10;
byte b = 20;
// byte sum = a + b; // a + b has type int
byte sum = (byte) (a + b);The parentheses matter. (byte) a + b casts only a, which is promoted back to int before addition. Casting (a + b) narrows the result.
Compile-time constants create a useful contrast:
byte oneHundred = 50 * 2; // constant expression, value fits
byte fifty = 50;
// byte result = fifty * 2; // runtime expression has type intCompound assignment includes a conversion
These statements are not equivalent at compile time:
byte count = 127;
// count = count + 1; // error: int cannot be assigned to byte
count += 1; // valid, equivalent to a cast back to byte
System.out.println(count); // -128Conceptually, count += 1 behaves like count = (byte) (count + 1). The implicit narrowing can overflow, so compact syntax does not remove the risk.
Integer division happens before assignment
int total = 7;
int count = 2;
double wrong = total / count;
double stillWrong = (double) (total / count);
double correct = (double) total / count;| Expression | Operation type | Intermediate result | Assigned result |
|---|---|---|---|
total / count |
int |
3 | 3.0 |
(double) (total / count) |
int, then cast |
3 | 3.0 |
(double) total / count |
double |
3.5 | 3.5 |
The assignment target does not change how the right side was evaluated. Convert an operand before division when a fractional result is required.
Widening can round
int exact = 16_777_216;
int next = 16_777_217;
float first = exact;
float second = next;
System.out.println((long) first);
System.out.println((long) second);
System.out.println(first == second);Output:
16777216
16777216
trueA float has 24 bits of binary precision, including its implied leading bit. 2^24 + 1 needs another precision bit, so it rounds. The conversion is widening in the Java language because no cast is required, but it is not value-preserving for every int.
What it costs
Implicit conversion keeps common code concise. The category name “widening” does not guarantee that every numeric value stays exact.
A cast documents permission to narrow. It does not check that the value fits. Validate a range before casting when wraparound or saturation would be an error.
Promotion prevents small-width intermediate arithmetic, but it can make an expression’s type wider than its variables. Trace operand types before looking at the assignment target.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
Why does `byte sum = a + b;` fail when both operands are byte and the result happens to fit?
Show the answer
Binary numeric promotion converts both
byteoperands tointbefore addition. The expression therefore has typeint. Assigning that expression tobyteneeds an explicit narrowing cast.Can a widening primitive conversion lose information?
Show the answer
Yes. Java classifies
inttofloat,longtofloat, andlongtodoubleas widening conversions, but they can round because the floating-point target has fewer precision bits. For example, 16,777,217 becomes 16,777,216 as a float.What does `(int) Double.NaN` produce, and what happens to a finite double above `Integer.MAX_VALUE`?
Show the answer
NaN converts to 0. A value above the int range converts to
Integer.MAX_VALUE; a value below the range converts toInteger.MIN_VALUE. A finite in-range value has its fractional part discarded toward zero.Why does `byte b = 100;` compile while `int n = 100; byte b = n;` does not?
Show the answer
The first initializer is an int constant expression whose value is known at compile time and fits in byte. Java permits this limited constant-expression narrowing. The variable
nis not a constant variable, so assigning it requires a cast even though its current value is 100.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises85 pointsabout 95 minutes
Predict Then Run
ex-2-3-aTwelve lines. Each one either compiles or it does not.
Write your twelve answers down first, on paper, before you touch the compiler. That is the whole exercise. Uncommenting and seeing what happens teaches you nothing if you had no prediction to be wrong about.
Then uncomment them one at a time and check.
Pay special attention to the two lines involving char and short. Both fail, and working out why will tell you that the widening ladder is not quite the straight line it looks like.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Ladder {
static boolean widensWithoutCast(String from, String to)
}Given two type names, answer whether Java performs that conversion with no cast. Work it out from the ladder rather than listing every pair, since the checks include combinations the starter does not mention.
What your program must do
- Predict all twelve before writing any code
- Implement the rule rather than listing the pairs
- Write the twelve as real declarations and confirm the compiler agrees with you
- Explain why short and char do not convert into each other in either direction
public class Ladder {
// Does Java do this conversion with no cast?
// Work it out from the ladder. Do not hard code every pair.
static boolean widensWithoutCast(String from, String to) {
return false; // TODO
}
public static void main(String[] args) {
// For each pair: predict yes or no BEFORE running.
// byte->short short->byte int->long long->int
// char->int int->char float->double double->float
// char->short short->char int->float long->double
// TODO: print your method's answer for all twelve
// TODO: then write each one as real code and confirm the compiler agrees
}
}
Hint 1
Hint 2
char sits off to the side. It widens to int and above, and nothing widens into it, not even short.Hint 3almost the answer
short and char are both 16 bits, and short runs -32768 to 32767 while char runs 0 to 65535. Each one has values the other cannot hold, so neither direction is safe.Trace Narrowing at the Bit Level
ex-2-3-bSeven narrowing casts. Predict every single one before you run anything.
Do not write “data is lost”. That is not a prediction. Write the actual number, and show the bit pattern that produced it.
Then test the popular shortcut. Somebody will tell you that casting to byte is the same as % 256. Check it against all seven of your cases and find out exactly where it stops being true.
Finish by explaining (byte) 200 from the retained eight bits and their signed interpretation.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Narrow {
static byte toByte(int v)
static short toShort(int v)
static int toInt(long v)
static int keepLowBits(long value, int bits)
static boolean modShortcutWorks(int value)
}keepLowBits implements the always-correct rule by hand and must agree with the cast for every value. modShortcutWorks reports whether the commonly quoted percent 256 shortcut gives the same answer as the cast.
What your program must do
- Predict the exact result of every cast before running, not just that something is lost
- Implement the low bits rule by hand and show it agrees with the cast every time
- Find a value where the percent 256 shortcut gives a different answer
- Say why the shortcut can never produce the right answer for that value
public class Narrow {
static byte toByte(int v) { return 0; } // TODO
static short toShort(int v) { return 0; } // TODO
static int toInt(long v) { return 0; } // TODO
// The rule, written out by hand: keep the low n bits, then read them as signed.
// This must agree with the cast for EVERY value, including negatives.
static int keepLowBits(long value, int bits) {
return 0; // TODO
}
// People say a byte cast is the same as % 256. Is it?
static boolean modShortcutWorks(int value) {
return true; // TODO
}
public static void main(String[] args) {
// PREDICT each of these before running:
// (byte) 300, (byte) 200, (byte) 128, (byte) 127, (byte) -300
// (short) 70000, (int) 9000000000L
}
}
Hint 1
(1L << bits) - 1 to keep the low bits, then check the top bit of what is left. If it is set, subtract 2 to the power of bits.Hint 2
(byte) 300 is 44 and 300 % 256 is also 44, so the shortcut looks right. Try 200.Hint 3almost the answer
(byte) 200 is -56 and 200 % 256 is 200. The shortcut has no way to produce a negative number, so it can never be the same rule.Convert Before Division
ex-2-3-cIf the required average is fractional, 7 / 2 must not be evaluated as integer division before assignment to double.
Work out why. The answer is entirely about when the division happens relative to the conversion.
Trace the four attempted fixes by identifying the operand types at the division operator. A cast applied after division cannot recover a discarded fraction.
Finish by finding a fifth fix that is not on the list. There is more than one good answer.
This bug appears in real production code constantly. It never crashes. It just reports the wrong average to somebody who trusts it.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Average {
static double brokenAverage(int total, int count)
static double castTheResult(int total, int count)
static double castTheTop(int total, int count)
static double castTheBottom(int total, int count)
static double multiplyByOne(int total, int count)
static boolean fixWorks(String which)
}Four attempted fixes, written exactly as named. fixWorks takes one of the four method names and answers whether that fix actually works. Fill it in after you have run them.
What your program must do
- Predict all five results for 7 divided by 2 before running
- Implement each fix exactly as its name describes
- Fill in fixWorks only after you have the results
- Say why one of the four fixes changes nothing
public class Average {
// The bug: assigning to a double does not undo integer division.
static double brokenAverage(int total, int count) { return 0; } // TODO
// Four attempted fixes. Write each one exactly as its name says.
static double castTheResult(int total, int count) { return 0; } // TODO: (double) (total / count)
static double castTheTop(int total, int count) { return 0; } // TODO: (double) total / count
static double castTheBottom(int total, int count) { return 0; } // TODO: total / (double) count
static double multiplyByOne(int total, int count) { return 0; } // TODO: total * 1.0 / count
// Which of the four actually work? Answer AFTER you have run them.
static boolean fixWorks(String which) {
return true; // TODO
}
public static void main(String[] args) {
// TODO: print all five for 7 and 2. Predict each one first.
}
}
Hint 1
total / count using the types of the operands. Two ints give an int, and the assignment to a double happens afterwards, far too late.Hint 2
castTheResult is the one that fails. The division has already produced 3 by the time the cast runs, and casting 3 gives 3.0.Hint 3almost the answer
Widening Can Round
ex-2-3-dEvery conversion in this program is a widening primitive conversion. Java permits each one without a cast. That language category does not promise that every source value remains exact.
Run it and watch two different int values turn into the same float.
Then work out why, from first principles, using what Section 2.2 told you about how a float is built. Do not look it up. The number of mantissa bits is the whole answer, and you already know it.
Once you have that, predict where the same thing starts happening for long into double. You should be able to name the exact number by reasoning alone.
Finish by writing down which widening conversions really are lossless, and which ones only look it. This is the list you will want in your head when someone insists that widening is always safe.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class NotSafe {
static boolean intSurvivesAsFloat(int value)
static boolean longSurvivesAsDouble(long value)
static int firstIntLostAsFloat()
static long firstLongLostAsDouble()
static boolean collidesAsFloat(int a, int b)
}Survives means the value comes back unchanged after a round trip through the wider type. collidesAsFloat answers whether two different ints become the same float.
What your program must do
- Show that widening from int to float can lose information
- Work out where it starts from the precision bit count, not by searching
- Find two different ints that become the same float
- Find where the same thing happens for long to double
public class NotSafe {
// Does this value come back unchanged after a trip through a float?
static boolean intSurvivesAsFloat(int value) { return true; } // TODO
// And the same question for long through double.
static boolean longSurvivesAsDouble(long value) { return true; } // TODO
// The smallest value that does NOT survive. Work it out from the
// number of precision bits, do not search for it.
static int firstIntLostAsFloat() { return 0; } // TODO
static long firstLongLostAsDouble() { return 0; } // TODO
// Two DIFFERENT ints that become the same float.
static boolean collidesAsFloat(int a, int b) { return false; } // TODO
public static void main(String[] args) {
// No casts anywhere in this exercise. Every conversion here is a
// WIDENING one, which Java performs happily and silently.
}
}
Hint 1
Hint 2
Hint 3almost the answer
long, not through an int. Casting a float back to an int saturates at Integer.MAX_VALUE rather than wrapping, so the very largest values look like they survived when they did not.After the credits
Overload resolution in Phase V reuses primitive widening rules when Java chooses a method. Boxing in Phase VII adds another conversion category with a different priority.
Shift operators in Phase III have related promotion rules, but their right operand is not converted to the left operand’s type. Its low bits determine the shift distance.
Threads you opened in this section
- Type promotionAdd autoboxing to promotion and overload resolution gets strange.7.3 - Wrapper Types, Autoboxing, and POJOs
Type promotion will return in 5.1 - Methods, Calls, Recursion, and Overloading