Java is statically typed, so every value carries a type the compiler knows about. Type casting is how a value of one type becomes a value of another. Java performs some of those conversions for you and refuses to perform the rest unless you ask in writing — because the second kind can quietly change your number.
This article covers every conversion that can happen between primitives, the exact rule the compiler applies in each case, and the traps: (byte) 300 printing 44, byte + byte refusing to compile, += truncating without a warning, and the difference between a cast and a parse. Every value and every error message below comes from a real run on OpenJDK 21.0.6.
![]()
There are two directions and two rules: widening happens on its own, narrowing needs a cast operator and can change the value you started with.
Widening and narrowing: the two directions of a conversion
A conversion is widening when the target type can represent every value of the source type. Nothing can be lost, so the compiler inserts it silently — this is the implicit conversion. A conversion is narrowing when the target type cannot represent every source value. The compiler refuses to guess, so you write a cast operator (type) and take responsibility — this is the explicit conversion.
The widening chain for primitives is fixed:
byte -> short -> int -> long -> float -> double
char joins that chain at int: a char widens to int, long, float and double, but not to byte or short. Anything moving right along the chain is automatic; anything moving left needs a cast.

byte b = 42;
short s = b; // byte -> short
int i = s; // short -> int
long l = i; // int -> long
float f = l; // long -> float
double d = f; // float -> double
char c = 'A';
int ci = c; // char -> int
System.out.println(b + " " + s + " " + i + " " + l + " " + f + " " + d);
System.out.println("char 'A' widened to int: " + ci);
42 42 42 42 42.0 42.0
char 'A' widened to int: 65
Not a single cast operator appears, and not a single value changes. That is the whole point of widening.
Widening that is automatic but still loses precision
Two hops on that chain are the exception most tutorials skip. int to float, and long to float or double, are widening conversions — the compiler performs them without a cast — but they can still lose precision. A float has 24 bits of significand, so it stores every integer only up to 2^24 = 16,777,216. A double has 53 bits, so it is exact only up to 2^53. Beyond that, the nearest representable value is used.
int big = 16777217; // 2^24 + 1
float bf = big; // widening: no cast needed
System.out.println("int 16777217 -> float -> " + bf);
System.out.println("back to int: " + (int) bf);
System.out.println("equal? " + (big == (int) bf));
long lbig = 9007199254740993L; // 2^53 + 1
double ld = lbig; // widening again
System.out.println("long 9007199254740993 -> double -> " + (long) ld);
int 16777217 -> float -> 1.6777216E7
back to int: 16777216
equal? false
long 9007199254740993 -> double -> 9007199254740992
The value changed and nothing complained. Widening guarantees the magnitude survives, not the exact value. int to double is always exact, because 32 bits of integer fit inside 53 bits of significand, and short to float is exact for the same reason; int to float and anything starting from long is not.
Narrowing a floating-point value truncates, it does not round
The cast operator on a double or float drops the fractional part and moves toward zero. It never rounds.
System.out.println("(int) 3.99 = " + (int) 3.99);
System.out.println("(int) -3.99 = " + (int) -3.99);
System.out.println("(int) 3.5 = " + (int) 3.5);
System.out.println("Math.round(3.99) = " + Math.round(3.99));
System.out.println("Math.round(-3.99) = " + Math.round(-3.99));
System.out.println("Math.round(3.5) = " + Math.round(3.5));
System.out.println("Math.round(-3.5) = " + Math.round(-3.5));
(int) 3.99 = 3
(int) -3.99 = -3
(int) 3.5 = 3
Math.round(3.99) = 4
Math.round(-3.99) = -4
Math.round(3.5) = 4
Math.round(-3.5) = -3
(int) -3.99 is -3, not -4: truncation goes toward zero, which is up for a negative number. If you want rounding, Math.round is the tool. Watch its return type — Math.round(double) returns a long, while Math.round(float) returns an int, so int n = Math.round(3.99); does not compile but int n = Math.round(3.99f); does. Math.floor and Math.ceil both return double and still need a cast.
A floating-point value that is too large for the target integer type does not wrap around — it saturates at the type's limit, and NaN becomes zero:
System.out.println("(int) 1e20 = " + (int) 1e20);
System.out.println("(int) -1e20 = " + (int) -1e20);
System.out.println("(int) Double.NaN = " + (int) Double.NaN);
(int) 1e20 = 2147483647
(int) -1e20 = -2147483648
(int) Double.NaN = 0
Remember that behaviour, because narrowing between integer types works in a completely different way.
What (byte) 300 actually does
Narrowing one integer type to another keeps the low-order bits of the value and discards everything above them. There is no range check, no exception and no warning.

System.out.println("(byte) 300 = " + (byte) 300);
System.out.println("(byte) 200 = " + (byte) 200);
System.out.println("(byte) 128 = " + (byte) 128);
System.out.println("(byte) -200 = " + (byte) -200);
System.out.println("(short) 70000 = " + (short) 70000);
System.out.println("(short) 40000 = " + (short) 40000);
System.out.println("(int) 4294967298L = " + (int) 4294967298L);
(byte) 300 = 44
(byte) 200 = -56
(byte) 128 = -128
(byte) -200 = 56
(short) 70000 = 4464
(short) 40000 = -25536
(int) 4294967298L = 2
Take 300. As a 32-bit int it is 00000000 00000000 00000001 00101100. A byte is 8 bits, so (byte) keeps only 00101100 and throws the leading 24 bits away. 00101100 is 44, and because its top bit is 0 the result is positive: 44.
200 is 11001000 in its low 8 bits. That top bit is 1, and the top bit of a byte is its sign bit, so the same 8 bits now read as a negative number: 200 − 256 = −56. The same arithmetic explains every row above — (short) 70000 is 70000 − 65536 = 4464, and (int) 4294967298L is 4294967298 − 4294967296 = 2.
⚠️ A narrowing cast is a promise you make to the compiler that the value fits. Nothing checks that promise at runtime.
(byte) 300is not an error, it is a different number.
Converting between char and int
char is a 16-bit unsigned integer type that happens to print as a character. Both directions work, and both are worth knowing.
System.out.println("(int) 'A' = " + (int) 'A');
System.out.println("(char) 66 = " + (char) 66);
System.out.println("(char) ('a' + 1) = " + (char) ('a' + 1));
System.out.println("'a' + 1 = " + ('a' + 1));
System.out.println("'7' - '0' = " + ('7' - '0'));
(int) 'A' = 65
(char) 66 = B
(char) ('a' + 1) = b
'a' + 1 = 98
'7' - '0' = 7
'a' + 1 is 98, not 'b', because arithmetic on char produces an int (see the next section). Adding a cast turns it back into a character.
Now the part that confuses people. This compiles:
char c = 'a' + 1; // fine
and this does not:
char a = 'a';
char c = a + 1; // error
CharFail.java:4: error: incompatible types: possible lossy conversion from int to char
char c = a + 1;
^
1 error
The difference is not char versus int, it is constant versus variable. 'a' + 1 is a compile-time constant expression, and Java allows a narrowing conversion of a constant that provably fits the target type. a + 1 reads a variable, so the compiler cannot prove anything and demands (char)(a + 1). The same rule is why byte ok = 100; compiles while byte no = 200; does not — 200 does not fit in a byte.
Numeric promotion: why byte plus byte is an int
This is the rule that surprises everyone at least once:

The promotion happens to the operands, before the operator ever runs:
byte a = 10;
byte b = 20;
byte c = a + b; // error
PromoFail.java:5: error: incompatible types: possible lossy conversion from int to byte
byte c = a + b;
^
1 error
Both operands are byte, the result is 30, and it still does not compile. Java applies binary numeric promotion before any arithmetic operator: any operand narrower than int — that is byte, short and char — is first widened to int. So a + b has type int, and assigning an int to a byte is a narrowing conversion. Write the cast yourself:
byte c = (byte) (a + b); // 30
Once both operands are at least int wide, the wider type wins:
| If either operand is | The narrower operand is promoted to | Result type |
|---|---|---|
double | double | double |
otherwise float | float | float |
otherwise long | long | long |
otherwise (including byte, short, char) | int | int |
Printing the runtime class of each expression confirms it:
byte + byte -> Integer
short + short-> Integer
char + char -> Integer
byte + short -> Integer
int + long -> Long
long + float -> Float
float+ double-> Double
int + double -> Double
char + int -> Integer
The hidden cast in compound assignment
b = b + 300 and b += 300 are not the same statement. The first one fails to compile; the second one compiles and silently truncates.
byte b = 10;
b += 300;
System.out.println("byte b = 10; b += 300; -> " + b);
byte q = 10;
q += 1.9; // a double on the right, still compiles
System.out.println("byte q = 10; q += 1.9; -> " + q);
int i = 10;
i += 3.75;
System.out.println("int i = 10; i += 3.75; -> " + i);
short s = 32767;
s += 1;
System.out.println("short s = 32767; s += 1;-> " + s);
byte b = 10; b += 300; -> 54
byte q = 10; q += 1.9; -> 11
int i = 10; i += 3.75; -> 13
short s = 32767; s += 1;-> -32768
The Java Language Specification defines E1 op= E2 as E1 = (T)(E1 op E2), where T is the type of E1. Every compound assignment operator therefore contains an invisible narrowing cast. b += 300 is really b = (byte)(10 + 300), and (byte) 310 is 54. q += 1.9 is q = (byte)(10 + 1.9), which truncates 11.9 to 11.
The explicit form has no such cast, which is why it is rejected:
CompoundFail.java:4: error: incompatible types: possible lossy conversion from int to byte
b = b + 300;
^
1 error
Two statements that look equivalent, one compile error and one wrong answer. If you rely on += with a byte, short or char, write the cast yourself so the truncation is visible.
boolean converts to nothing
boolean is not a number in Java, and no cast makes it one. Neither direction exists:
int n = (int) true;
boolean b = (boolean) 1;
BoolFail.java:3: error: incompatible types: boolean cannot be converted to int
int n = (int) true;
^
BoolFail.java:4: error: incompatible types: int cannot be converted to boolean
boolean b = (boolean) 1;
^
2 errors
The same rule rejects if (1), with the identical message: int cannot be converted to boolean. If you came from C, this is the habit to unlearn — there is no truthiness in Java. To turn a number into a boolean, compare it: n != 0. To turn a boolean into a number, use a conditional expression: b ? 1 : 0.
Overflow in a cast versus overflow in arithmetic
Both wrap, and neither tells you:

Same syntax, two different rules:
int max = Integer.MAX_VALUE;
System.out.println("Integer.MAX_VALUE + 1 = " + (max + 1));
long l = 3_000_000_000L;
System.out.println("(int) 3000000000L = " + (int) l);
int a = 100000, b = 100000;
System.out.println("int 100000*100000 = " + (a * b));
System.out.println("with a long operand = " + ((long) a * b));
Integer.MAX_VALUE + 1 = -2147483648
(int) 3000000000L = -1294967296
int 100000*100000 = 1410065408
with a long operand = 10000000000
The third line is a classic: both operands are int, so the multiplication happens in 32 bits and overflows before anything is assigned. Casting the result afterwards cannot recover it. Casting one operand first — (long) a * b — promotes the whole expression to long and gives the right answer.
When a value must fit and you would rather know than guess, use the exact methods. Math.toIntExact throws instead of wrapping:
try {
int n = Math.toIntExact(3_000_000_000L);
} catch (ArithmeticException e) {
System.out.println("Math.toIntExact -> " + e);
}
System.out.println(Math.addExact(Integer.MAX_VALUE, 1));
Math.toIntExact -> java.lang.ArithmeticException: integer overflow
Exception in thread "main" java.lang.ArithmeticException: integer overflow
Math.addExact, subtractExact, multiplyExact and negateExact behave the same way. The message in every case is exactly integer overflow.
Casting is not parsing
This is where beginners lose the most time. A cast reinterprets a value the compiler already understands; it does not read text. String is not a number, so no cast connects the two:

One fails at compile time, the other at run time:
String s = "42";
int n = (int) s;
ParseFail.java:4: error: incompatible types: String cannot be converted to int
int n = (int) s;
^
1 error
(String) n fails in the other direction with int cannot be converted to String. Text goes through parse and format methods instead:
System.out.println(Integer.parseInt("42") + 1);
System.out.println(Double.parseDouble("3.5"));
System.out.println(Long.parseLong("9999999999"));
System.out.println(String.valueOf(42));
System.out.println(42 + "");
43
3.5
9999999999
42
42
Integer.parseInt is strict. Anything that is not a valid integer throws NumberFormatException at runtime, and the message quotes the input back at you:
Integer.parseInt("42abc");
Exception in thread "main" java.lang.NumberFormatException: For input string: "42abc"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
"3.5" and " 42" fail the same way — a leading space is not trimmed. "" gives For input string: "" and null gives Cannot parse null string. If the text may be junk, catch NumberFormatException; there is no cast that will save you.
Autoboxing and unboxing are conversions too
Integer is an object, int is a primitive, and the compiler converts between them for you. That is autoboxing in one direction and unboxing in the other:
Integer boxed = 42; // autoboxing: int -> Integer
int back = boxed; // unboxing: Integer -> int
Unboxing is a method call in disguise — boxed.intValue() — so it throws when the reference is null:
Integer n = null;
int bad = n;
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "n" is null
at Q3.main(Q3.java:4)
The variable name appears because the class was compiled with debug information (javac -g, which Maven and Gradle turn on by default); without it the message names a slot such as "<local3>" instead. This is the most common NullPointerException in Java code that never dereferences anything explicitly.
Boxing does not combine with widening, either. Long x = 5; fails with incompatible types: int cannot be converted to Long, because assignment allows either a widening primitive conversion or a boxing conversion, never a widening followed by a boxing. Write Long x = 5L;.
Casting between reference types
Casting also exists for objects, and it follows different rules. Assigning a subtype to a supertype variable — upcasting — is implicit and always safe. Going the other way — downcasting — requires an explicit cast and is checked at runtime, throwing ClassCastException when the object is not actually of that type, as in class java.lang.Integer cannot be cast to class java.lang.String. Reference casting only means something once inheritance is on the table, so this series covers it with inheritance rather than here.
I have X and need Y
| I have | I need | Use | Note |
|---|---|---|---|
int | long, float, double | assign it directly | widening, implicit |
double | int, truncated | (int) d | drops the fraction toward zero |
double | int, rounded | (int) Math.round(d) | Math.round(double) returns long |
float | int, rounded | Math.round(f) | Math.round(float) already returns int |
double | int, always down / up | (int) Math.floor(d) / (int) Math.ceil(d) | both return double |
int | byte, short, char | (byte) n | keeps the low bits, never throws |
long | int, must be exact | Math.toIntExact(l) | throws instead of wrapping |
char | int | assign it directly | 'A' becomes 65 |
int | char | (char) n | 66 becomes B |
byte + byte | a byte | (byte) (a + b) | the sum is an int |
int and int | a fractional quotient | (double) a / b | cast one operand before dividing |
String | int | Integer.parseInt(s) | a cast does not compile |
String | double | Double.parseDouble(s) | throws NumberFormatException on junk |
| any number | String | String.valueOf(x) | x + "" does the same |
int | Integer | assign it directly | autoboxing |
Integer | int | assign it directly | unboxing, NullPointerException if null |
boolean | a number | no conversion exists | write b ? 1 : 0 |
| number | boolean | no conversion exists | write n != 0 |
FAQ
Why does (byte) 300 print 44 instead of throwing?
Because a narrowing cast between integer types is defined as keeping the low-order bits, not as a range check. 300 is ...00000001 00101100; a byte keeps 00101100, which is 44. The JVM does no validation, which is exactly why the cast has to be written explicitly.
Does a cast round or truncate?
It truncates toward zero. (int) 3.99 is 3 and (int) -3.99 is -3. Use Math.round when you want rounding, remembering that Math.round(double) returns a long.
Why can I write byte b = 100; but not byte b = a + 1;?
100 is a compile-time constant that provably fits in a byte, and Java permits a narrowing conversion of an in-range constant. a + 1 involves a variable, so the expression has type int and the compiler cannot prove it fits. Write (byte) (a + 1), or use a constant expression like byte b = 100 + 1;.
Why does (int) "42" not compile?
String and int are unrelated types, so there is no conversion for a cast to perform. The compiler says incompatible types: String cannot be converted to int. Reading a number out of text is parsing, not casting: Integer.parseInt("42").
Can casting a double to an int throw at runtime?
No. It saturates instead: (int) 1e20 gives Integer.MAX_VALUE, (int) -1e20 gives Integer.MIN_VALUE, and (int) Double.NaN gives 0. Nothing is thrown, so if an out-of-range value is a bug in your program you have to check the range yourself.
When should I use Math.toIntExact instead of (int)?
Whenever a long that does not fit in an int means something is wrong. (int) l wraps silently and hands you a plausible-looking wrong number; Math.toIntExact(l) throws ArithmeticException: integer overflow at the point of the mistake. The same applies to Math.addExact and Math.multiplyExact inside arithmetic.
Conclusion
Type casting in Java comes down to a short list of rules. Widening moves right along byte -> short -> int -> long -> float -> double and happens on its own, with int to float and anything from long losing precision along the way. Narrowing moves left, needs a cast operator, truncates floating-point values toward zero and keeps only the low bits of integer values. Arithmetic promotes anything narrower than int to int before it runs, += hides a narrowing cast, boolean converts to nothing, and text goes through parseInt, never through a cast.
Next in this series: the String class — why strings are immutable, what that costs, and the methods you will use every single day.