Type Conversion & Casting
Java converts values between types under strict rules — some conversions happen automatically and safely, others require an explicit cast and can lose data or fail at runtime.
Type conversion is the process of turning a value of one type into a value of another type. Java distinguishes conversions that are always safe and happen implicitly (widening) from ones that can lose information or fail, which require you to explicitly ask for them with a cast (narrowing). The same widening/narrowing vocabulary applies to both primitives and references, though the mechanics differ.
Widening Primitive Conversion
A widening conversion moves a value into a larger, more general type that can represent every value of the original type without loss. Java performs these automatically, with no cast needed, following a defined hierarchy.
Widening from int/long to float/double can lose precision (not magnitude) for very large integers, because float and double have limited mantissa bits — this is still classified as widening because the target type can represent the approximate magnitude.
byte -> short -> int -> long -> float -> double
char -> intint i = 100;
long l = i; // widening, automatic
double d = l; // widening, automatic
char c = 'A';
int code = c; // widening: char -> int, code becomes 65Narrowing Primitive Conversion
A narrowing conversion moves a value into a smaller or less general type, which may not be able to represent it exactly — data can be lost. Java requires an explicit cast for narrowing so that data loss is always visible in the source code, never accidental.
double d = 9.99;
int i = (int) d; // narrowing, explicit cast required — i becomes 9 (truncated, not rounded)
long big = 123456789012L;
int truncated = (int) big; // narrowing — result is implementation-defined by overflow rules, not simply an errorCasting Syntax
A cast is written as the target type in parentheses immediately before the expression being converted.
double price = 19.95;
int dollars = (int) price; // primitive cast
Object obj = "hello";
String s = (String) obj; // reference castNumeric Promotion
When operands of different numeric types appear together in an expression (e.g. an arithmetic or comparison operator), Java automatically promotes the smaller operand's type to match, following widening rules, before performing the operation. byte, short, and char are always promoted at least to int in arithmetic, even when combined with another byte/short/char.
byte b1 = 10, b2 = 20;
// byte sum = b1 + b2; // COMPILE ERROR: b1 + b2 is promoted to int
int sum = b1 + b2; // correct — result of byte+byte arithmetic is int
int x = 5;
double y = 2.0;
double result = x + y; // x is promoted to double before additionOverflow Behavior on Integer Narrowing
Narrowing an integer type discards the high-order bits that don't fit in the target type. This is a defined, deterministic bit-truncation operation in the JLS — not undefined behavior — but the resulting value can look nonsensical if you're not expecting it.
int big = 300;
byte b = (byte) big; // byte range is -128..127
System.out.println(b); // prints 44 (300 mod 256, reinterpreted as signed byte)Floating-Point to Integer Conversion
Casting a floating-point value to an integer type truncates toward zero (discards the fractional part; it does not round). Special floating-point values convert per well-defined JLS rules: NaN becomes 0, and values outside the target integer's range clamp to that type's minimum or maximum.
System.out.println((int) 9.99); // 9
System.out.println((int) -9.99); // -9 (truncates toward zero, not floor)
System.out.println((int) Double.NaN); // 0
System.out.println((int) Double.POSITIVE_INFINITY); // Integer.MAX_VALUEReference Widening and Narrowing
Reference types have their own version of widening and narrowing, based on the class/interface hierarchy rather than numeric range. Widening reference conversion (upcasting) — assigning a subtype reference to a supertype-typed variable — is always safe and implicit. Narrowing reference conversion (downcasting) — treating a supertype reference as a more specific subtype — requires an explicit cast and can fail at runtime.
Object obj = "hello"; // widening: String -> Object, implicit
String s = (String) obj; // narrowing: Object -> String, explicit cast requiredinstanceof Before Downcasting
Because a downcast is only safe when the object's actual runtime type genuinely is (or extends) the target type, it's standard practice to check with instanceof first — or, in modern Java, to use pattern-matching instanceof that combines the check and the cast in one expression.
Object obj = getSomeObject();
if (obj instanceof String s) { // pattern matching instanceof (Java 16+)
System.out.println(s.length());
} else {
System.out.println("not a String");
}Casting Failures: ClassCastException
If you downcast to a type the object does not actually belong to, the JVM throws a ClassCastException at runtime — the compiler cannot always catch this, because it depends on the object's real runtime type, which may not be knowable until the program runs.
Object obj = Integer.valueOf(42);
String s = (String) obj; // compiles fine, throws ClassCastException at runtimeBoxing and Unboxing Conversions
Autoboxing converts a primitive value into its corresponding wrapper object (int -> Integer, double -> Double, etc.); auto-unboxing does the reverse. Java performs these conversions automatically in many contexts — assignment, method calls, collections — but they are still real conversions with real costs and pitfalls, not free operations.
Auto-unboxing a null wrapper throws NullPointerException — Integer x = null; int y = x; compiles fine but blows up at runtime.
Integer boxed = 5; // autoboxing: int -> Integer
int unboxed = boxed; // auto-unboxing: Integer -> int
List<Integer> list = new ArrayList<>();
list.add(10); // autoboxed to Integer for storage
int first = list.get(0); // auto-unboxed back to intConversion vs. Parsing
Casting and boxing convert one already-typed value into a differently-typed value — they never interpret text. Turning a String like "42" into an int is not a cast at all; it's parsing, done through methods like Integer.parseInt(), Double.parseDouble(), or wrapper valueOf() methods.
String text = "42";
// int n = (int) text; // COMPILE ERROR: cannot cast String to int
int n = Integer.parseInt(text); // correct: parsing, not castingEdge Cases
A cast on a numeric literal that is a compile-time constant expression can sometimes be folded by the compiler and allowed even where a variable of the same type would need an explicit narrowing cast (e.g. assigning the literal 100 directly to a byte variable works without a cast, because the compiler verifies at compile time that 100 fits in a byte).
byte b = 100; // OK: 100 is a constant expression known to fit in byte
int i = 100;
// byte b2 = i; // COMPILE ERROR: i is a variable, not a constant expression
byte b2 = (byte) i; // requires explicit castComplete Example
public class ConversionDemo {
public static void main(String[] args) {
// Widening (implicit)
int i = 65;
long l = i;
double d = l;
System.out.println(d); // 65.0
// Narrowing (explicit)
double price = 19.999;
int wholeDollars = (int) price;
System.out.println(wholeDollars); // 19
// Reference widening/narrowing
Object obj = "engineered";
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
// Boxing/unboxing
Integer boxedCount = 3;
int rawCount = boxedCount + 1;
System.out.println(rawCount); // 4
// Parsing (not casting)
int parsed = Integer.parseInt("123");
System.out.println(parsed + 1); // 124
}
}Quick Reference
Widening (implicit, safe): byte->short->int->long->float->double, char->int
Narrowing (explicit cast): reverse direction, may lose data
Integer overflow on narrow: bits truncated, deterministic but surprising
float/double -> int cast: truncates toward zero, not rounds
Upcast (subtype->supertype): implicit, always safe
Downcast (supertype->subtype):explicit cast, may throw ClassCastException
Boxing/unboxing: primitive <-> wrapper, automatic but has cost/null risk
Parsing: String -> number, via parseX()/valueOf(), NOT a castKey Takeaways
Widening conversions are automatic and lossless; narrowing conversions require an explicit cast and can lose data.
Casting a float/double to an integer type truncates toward zero, it does not round.
Integer narrowing truncates bits deterministically per the JLS, which can produce surprising but well-defined results.
Upcasting a reference is always safe; downcasting requires a cast and can throw ClassCastException at runtime.
Autoboxing/unboxing convert between primitives and wrapper objects automatically, but unboxing a null wrapper throws NullPointerException.
Converting text to a number is parsing, not casting — casting cannot convert a String to a numeric primitive.
int n = (int) "42";
int n = Integer.parseInt("42");
Casting only converts between compatible types in the type hierarchy or numeric conversion rules; it cannot parse text. Use Integer.parseInt (or similar) to convert a String to a number.
int rounded = (int) 9.99; // expecting 10 int rounded = Math.round(9.99f); // or (int) Math.round(9.99)
A cast to int truncates toward zero, it does not round. Use Math.round() when rounding is actually wanted.
Integer count = null; int total = count + 1;
Integer count = null; int total = (count != null) ? count + 1 : 0;
Auto-unboxing a null wrapper reference throws NullPointerException. Check for null before an operation that triggers unboxing.
Object o = getValue(); String s = (String) o; // no check Object o = getValue(); if (o instanceof String s) { // use s safely }
An unchecked downcast throws ClassCastException if the object's actual runtime type isn't the target type. Check with instanceof (ideally pattern-matching instanceof) first.
Why does Java allow widening primitive conversions to happen automatically but require an explicit cast for narrowing conversions?
Widening never loses information — every value of the smaller type fits in the larger type — so it's always safe to do silently. Narrowing can lose data or magnitude, so Java forces the programmer to write an explicit cast, making potential data loss visible in the source.
What does (int) 7.9 evaluate to, and what does (int) -7.9 evaluate to?
7 and -7. Casting a floating-point value to an integer type truncates toward zero; it does not floor or round.
What happens when you cast an int value of 300 to a byte?
The high-order bits that don't fit in a byte are discarded (a deterministic bit-truncation per the JLS). 300 becomes 44 after truncation and signed reinterpretation, not a runtime error.
Why can casting from Object to String compile successfully but still throw an exception at runtime?
The compiler only checks that the cast is plausible given the declared type (Object could be a String). Whether it's actually valid depends on the object's real runtime type, which isn't always known until execution — if it's wrong, a ClassCastException is thrown at runtime.
What guarantee does the JLS provide about integer narrowing overflow, and what does it not guarantee?
The JLS guarantees that narrowing an integer type performs a specific, deterministic bit-truncation and (for signed types) reinterpretation. It does not guarantee that the resulting value will be intuitive or 'safe' for your program's logic — that's the programmer's responsibility to check.
Why does Integer.parseInt("42") not count as a cast?
A cast only reinterprets or converts a value that's already typed as a number/object according to the type system's conversion rules. Converting text (a String) into a numeric value requires parsing — interpreting the characters — which is a completely different operation performed by a library method, not the cast syntax.
What is the risk of relying on autoboxing/unboxing inside a loop that runs millions of times?
Each autobox operation allocates a wrapper object (unless a small cached value is reused), and each unbox is a method call — repeated over millions of iterations this adds real allocation and CPU overhead compared to working with primitives directly.
Write a method that takes a double representing a price and returns the price rounded to the nearest whole number of cents-free dollars (rounding, not truncating).
static int roundToDollars(double price) { return (int) Math.round(price); }
Math.round() rounds to the nearest whole value (returning a long for double input, hence the (int) cast on the result), unlike a bare (int) cast which would simply truncate the fractional part.
What does this program print?
It prints 0. 2^32 in binary is a 1 followed by 32 zero bits; narrowing to a 32-bit int keeps only the low-order 32 bits, which are all zero.
Write code that safely converts an Object reference to a Student, printing the student's name if the conversion is valid and a message otherwise.
Object obj = getSomeObject(); if (obj instanceof Student s) { System.out.println(s.getName()); } else { System.out.println("Not a Student"); }
Using pattern-matching instanceof checks the runtime type and performs the cast together, avoiding an unchecked cast that could throw ClassCastException.
public class Trace {
public static void main(String[] args) {
long big = 4_294_967_296L; // 2^32
int result = (int) big;
System.out.println(result);
}
}