Java is strict about types. A value with one type doesn't automatically become another type unless the conversion is safe, and when it isn't safe, an explicit cast is required. This lesson covers how the two kinds of conversion work between primitive types, why Java forces explicit casts for lossy conversions, and the small handful of arithmetic rules to watch for. The lesson closes with a brief look at casting between reference types so the term (String) something isn't a mystery on first sight.
There are exactly two ways one primitive type turns into another in Java:
The compiler enforces this split because dropping precision or wrapping a number around without warning is a common bug. Widening is always safe, so it happens automatically. Narrowing is sometimes safe and sometimes a disaster, so the compiler hands the responsibility back to the developer.
The same idea in table form:
| Kind | Direction | Cast needed? | Data loss possible? | Example |
|---|---|---|---|---|
| Widening | small to big | No | No | int orderCount = 5; long total = orderCount; |
| Narrowing | big to small | Yes | Yes | double subtotal = 89.95; int rounded = (int) subtotal; |
The next few sections unpack both directions.
A widening conversion takes a value of a smaller type and stores it in a variable of a bigger type. Every value that fits in the smaller type also fits in the bigger one, so nothing has to be dropped. Java performs the conversion automatically.
The last line: 250 printed as 250.0. The value didn't change, but the type did. A double always has a decimal part, so Java tacks on .0 when it widens an integer.
The full chain of widening conversions for numeric types runs from byte all the way up to double. Anything earlier in the chain widens automatically to anything later.
A byte widens to a short, an int, a long, a float, or a double. A long widens to a float or a double. The chain is one-way: nothing later in the chain widens to something earlier.
One thing that looks odd at first: long widens to float, even though a long is 8 bytes and a float is only 4 bytes. The conversion is still classified as widening because float can represent numbers far larger than any long. What it can't always do is represent them exactly. A very large long might land on the nearest float value, losing a little precision in the last digits. The compiler still treats this as widening and allows it without a cast.
Narrowing is the other direction: a bigger type squeezed into a smaller one. The bigger type can hold values the smaller one cannot, so the conversion can lose data. Java refuses to do this automatically; a cast is required.
The syntax is simple: put the target type in parentheses in front of the value to convert.
Two different kinds of damage happened here, and both are worth understanding.
The first line, (int) 89.95, turned 89.95 into 89. Casting a double to an int throws away the fractional part. It doesn't round. 89.95 becomes 89, 89.99 becomes 89, and (int) 3.99 is 3. For rounding, use Math.round explicitly.
The second cast, from a long value of 5 billion down to an int, gave back 705032704, which has no obvious relationship to the original. An int only has 32 bits, and 5 billion needs more. When the value doesn't fit, Java keeps the low 32 bits and discards the rest. The result is whatever bit pattern those low 32 bits represent, interpreted as a signed int. The compiler doesn't warn, because the cast already declared this is the intended behavior.
A smaller example makes the wraparound easier to see:
A byte holds values from -128 to 127. 300 is well outside that range. The cast keeps the low 8 bits of 300, which work out to 44. There's no error, no warning, just a wrong number. This is why Java forces the explicit cast: the compiler wants the developer to confirm this is the intended behavior.
Narrowing casts have no runtime overhead, but they have a real correctness cost. If the source value doesn't fit in the target type, the result is a wrong number. Check ranges before casting, or use Math.toIntExact for long to int conversions that should throw on overflow.
A cast is one piece of syntax: (targetType) value. The parentheses go around the type, not the value. The cast applies to the single expression that follows it. Here's the same shape in a few contexts:
Look at the second line carefully: (int) (price * quantity). The cast applies only to the result of price * quantity, because the parentheses make the multiplication happen first. Without those inner parentheses, (int) price * quantity would cast price to int first (turning 12.5 into 12), then multiply by 3, giving 36 instead of 37. The placement of parentheses matters.
char and int Convert Both Wayschar is an integer type in Java. It holds a 16-bit unsigned number that represents a character code. Because it's a number internally, it widens to int automatically, and an int can be cast back to a char.
'A' has the character code 65. The line int code = letter; widens the char to an int, so code is 65. The expression 'A' + 1 is 65 + 1, which is 66, also an int. Getting back to a char requires a cast: (char) 66 is 'B'. This is the standard way to walk through an alphabet of category codes or shift letters by a fixed amount.
When types mix in an arithmetic expression, Java picks a single type to do the math in. The rules are predictable:
double, both are promoted to double.float, both are promoted to float.long, both are promoted to long.int.If two byte values or two short values are added, the result is an int, not a byte or a short. Java promotes anything smaller than int up to int before doing arithmetic.
The commented-out line is the case to note. itemA and itemB are both byte. Their sum is mathematically 30, which fits in a byte. But Java promoted both to int before adding, so the result has type int, and the assignment to a byte is a narrowing conversion that requires a cast:
This rule exists because the JVM does all its integer arithmetic at int width or wider. Promoting smaller types to int keeps the bytecode simple. The trade-off is that arithmetic on byte and short keeps producing int results, and casts are needed to push them back down.
The compound assignment operators (+=, -=, *=, /=, %=) have a small but useful quirk: they perform an implicit narrowing cast back to the variable's type. The version below compiles even though the equivalent long-form does not.
The line itemsInCart += 5; is effectively itemsInCart = (byte) (itemsInCart + 5);. The compiler inserts the cast automatically. The expanded form itemsInCart = itemsInCart + 5; does not get that free cast, so it fails to compile with the same "incompatible types" error as any other narrowing without a cast.
This shortcut is handy, but it has the same data-loss risk as any other narrowing. byte total = 120; total += 50; wraps to -86, because 170 doesn't fit in a byte. The implicit cast doesn't change the math; it just skips writing the cast.
Everything above is about primitive types. Reference types (objects) can also be cast, but the rules are different and the topic deserves its own treatment.
The short version: a reference of a parent type that actually points to an object of a more specific child type can be cast down to the child type to use the child's features. The most common case is Object holding a String:
The cast doesn't change the object; it changes how the compiler sees the reference. If the object pointed to isn't really a String, the cast throws ClassCastException at runtime. There's no truncation here, just a clean failure when the assumption is wrong.
For now, the syntax (String) someObject exists, it looks like a primitive cast, and it does something different internally.
10 quizzes