Java Type Casting
Type casting converts a value from one type to another. Java performs some conversions automatically when no information can be lost, and requires an explicit cast when it can. The rules exist to make every potential loss of data visible in the source code.
Widening Conversion (Implicit)
Converting to a type that can hold every value of the original type is a widening conversion. The compiler does it automatically.
byte -> short -> int -> long -> float -> double
^
char
int i = 100;
long l = i; // int to long: automatic
double d = l; // long to double: automatic
float f = 'A'; // char to float: automatic, f is 65.0
No cast is needed because a long can represent every int, and a double can represent every long in magnitude.
One subtlety: int to float and long to float/double are widening by rule, yet can lose precision. A float has 24 bits of mantissa, so an int above about 16 million may be rounded:
int big = 123_456_789;
float f = big;
System.out.println((int) f); // 123456792
The conversion is permitted implicitly because the magnitude always fits, but the least significant digits may change. When exact large integers matter, do not route them through float.
Narrowing Conversion (Explicit Cast)
Converting to a type that cannot hold every value of the original is narrowing, and the compiler refuses to do it silently.
double price = 9.99;
int whole = price; // compile error: possible lossy conversion from double to int
int whole = (int) price; // allowed: whole is 9
The cast syntax is (TargetType) value. It tells the compiler and the reader that truncation is intended.
Floating point to integer: truncation
Casting a double or float to an integer type discards the fractional part, always toward zero. It does not round.
System.out.println((int) 9.99); // 9
System.out.println((int) -9.99); // -9
System.out.println((int) 0.5); // 0
For rounding, use Math.round, which returns a long for a double argument:
long rounded = Math.round(9.99); // 10
int roundedInt = (int) Math.round(9.5); // 10
A value too large for the target type saturates: (int) 1e20 is Integer.MAX_VALUE, (int) Double.NaN is 0.
Integer to a smaller integer: bit truncation
Casting between integer types keeps the low-order bits and discards the rest. The result can look nothing like the original.
int i = 300;
byte b = (byte) i;
System.out.println(b); // 44
300 in binary is 1 0010 1100. A byte holds eight bits: 0010 1100, which is 44. Values within the target's range survive unchanged; values outside it wrap.
long big = 4_000_000_000L;
int truncated = (int) big;
System.out.println(truncated); // -294967296
This is why reading a large value into an int without range checking is dangerous. The compiler makes you write the cast, but it cannot know whether the runtime value fits.
char and integer conversions
char is unsigned 16-bit; short and byte are signed. Converting between them is narrowing in both directions.
char c = 'A';
int code = c; // widening: 65
char back = (char) code; // narrowing: 'A'
char next = (char) (c + 1); // 'B'; the arithmetic produces an int, so a cast is needed
Conversions in Arithmetic
Before any binary arithmetic operation, Java promotes both operands to a common type:
- If either is
double, both becomedouble. - Otherwise, if either is
float, both becomefloat. - Otherwise, if either is
long, both becomelong. - Otherwise, both become
int, even if both arebyte,short, orchar.
byte a = 10, b = 20;
byte sum = a + b; // compile error: a + b is an int
byte sum = (byte) (a + b); // allowed
int x = 7;
double y = 2;
double result = x / y; // 3.5: x is promoted to double before dividing
int bad = x / 2; // 3: both int, integer division
Rule 4 is the one that surprises people. There is no byte addition in the JVM; small types are always computed as int.
Compound assignment hides a cast
+=, -=, *=, and the other compound operators include an implicit cast back to the variable's type:
byte b = 10;
b = b + 5; // compile error
b += 5; // allowed: equivalent to b = (byte) (b + 5)
This is convenient but can silently truncate, so be aware of it when the right-hand side can be large.
Casting Between Reference Types
Casting a reference does not change the object. It changes only the static type through which the compiler lets you access it.
Upcasting
Treating an object as its parent type or an interface it implements is always safe and needs no cast:
Dog dog = new Dog();
Animal animal = dog; // upcast: a Dog is an Animal
Object obj = dog; // every class extends Object
After the upcast, only Animal's methods are accessible through animal, even though the object is still a Dog. Overridden methods still dispatch to the Dog version at runtime.
Downcasting
Going from a parent type back to a child requires an explicit cast, because the compiler cannot know whether the object really is that subtype:
Animal animal = new Dog();
Dog dog = (Dog) animal; // allowed: the object really is a Dog
Animal other = new Cat();
Dog notADog = (Dog) other; // compiles, but throws ClassCastException at runtime
The cast is checked at runtime. If the object is not an instance of the target type, ClassCastException is thrown.
Checking before casting
instanceof tests whether a downcast will succeed:
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.fetch();
}
Since Java 16, pattern matching combines the test and the cast:
if (animal instanceof Dog dog) {
dog.fetch(); // dog is already a Dog here
}
The variable dog is declared and assigned only when the test passes. This is the preferred form; it removes the duplicated type name and the possibility of casting to the wrong type after a correct check.
A cast between unrelated classes is rejected at compile time: (String) new Integer(5) does not compile, because no object can be both. Casts involving interfaces are always allowed at compile time (a subclass might implement the interface), so they are checked at runtime instead.
Converting Between Strings and Numbers
A cast cannot turn text into a number. (int) "42" is a compile error: String and int are unrelated types. Parsing is done with methods on the wrapper classes:
int n = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
long big = Long.parseLong("9000000000");
boolean flag = Boolean.parseBoolean("true");
Malformed input throws NumberFormatException:
Integer.parseInt("42abc"); // NumberFormatException
Integer.parseInt(" 42 "); // NumberFormatException: whitespace is not trimmed
Integer.parseInt(""); // NumberFormatException
Input from users or files should be trimmed and validated, and the exception caught:
String input = " 42 ";
try {
int value = Integer.parseInt(input.trim());
System.out.println("Parsed: " + value);
} catch (NumberFormatException e) {
System.out.println("Not a valid integer: " + input);
}
Converting a number to text:
String s1 = String.valueOf(42);
String s2 = Integer.toString(42);
String s3 = 42 + ""; // works, but the first two are clearer
Boxing Conversions
Converting between a primitive and its wrapper is a boxing or unboxing conversion, performed automatically:
Integer boxed = 42; // boxing
int unboxed = boxed; // unboxing
A cast between a wrapper and a different primitive is not allowed in one step:
Integer i = 42;
long l = i; // allowed: unbox to int, then widen to long
double d = (double) i; // allowed: unbox, then widen
Long wrong = i; // compile error: Integer is not a Long
Long right = (long) i; // unbox, widen, then box as Long
The wrapper types are distinct classes with no inheritance relationship, so Integer to Long requires going through the primitives.
Where Casts Appear in Real Code
Averaging integers. The cast must happen before the division:
int total = 7, count = 2;
double avg = total / count; // 3.0: integer division, then widening
double avg = (double) total / count; // 3.5: cast first, then double division
Reading generic or legacy APIs. Older APIs and some reflection code return Object:
Object raw = map.get("count");
if (raw instanceof Integer count) {
process(count);
}
Working with bytes. I/O and binary protocols produce int values that must be stored as byte, and bytes that must be read as unsigned:
byte b = (byte) 200; // stores -56
int unsigned = b & 0xFF; // recovers 200
int unsigned2 = Byte.toUnsignedInt(b); // same, more readable
Random numbers in a range. Math.random() returns a double; the cast truncates to an integer index:
int dice = (int) (Math.random() * 6) + 1; // 1 to 6
The parentheses around Math.random() * 6 are essential. Without them, (int) Math.random() is always 0, and the result is always 1.