Command Palette

Search for a command to run...

[Java Basics] Type Casting in Java: Implicit vs Explicit Conversion

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.

Type casting in Java: implicit and explicit conversion

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.

The Java numeric conversion chain: widening right, narrowing left

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.

What (byte) 300 actually does: the high 24 bits are discarded

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) 300 is 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:

Both operands promoted to int before the operator runs

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 isThe narrower operand is promoted toResult type
doubledoubledouble
otherwise floatfloatfloat
otherwise longlonglong
otherwise (including byte, short, char)intint

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:

Integer narrowing wraps, floating-point narrowing saturates

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:

Cast converts between primitive types; parsing reads a number out of text

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 haveI needUseNote
intlong, float, doubleassign it directlywidening, implicit
doubleint, truncated(int) ddrops the fraction toward zero
doubleint, rounded(int) Math.round(d)Math.round(double) returns long
floatint, roundedMath.round(f)Math.round(float) already returns int
doubleint, always down / up(int) Math.floor(d) / (int) Math.ceil(d)both return double
intbyte, short, char(byte) nkeeps the low bits, never throws
longint, must be exactMath.toIntExact(l)throws instead of wrapping
charintassign it directly'A' becomes 65
intchar(char) n66 becomes B
byte + bytea byte(byte) (a + b)the sum is an int
int and inta fractional quotient(double) a / bcast one operand before dividing
StringintInteger.parseInt(s)a cast does not compile
StringdoubleDouble.parseDouble(s)throws NumberFormatException on junk
any numberStringString.valueOf(x)x + "" does the same
intIntegerassign it directlyautoboxing
Integerintassign it directlyunboxing, NullPointerException if null
booleana numberno conversion existswrite b ? 1 : 0
numberbooleanno conversion existswrite 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.

Related Posts

[Java Basics] Exception Handling in Java: try, catch, finally, throw and throws

Exception handling in Java on OpenJDK 21: the Throwable hierarchy, try-catch-finally and what finally really guarantees, multi-catch and catch ordering, throw versus throws, checked versus unchecked exceptions, stack unwinding and how to read a stack trace, plus the traps — the empty catch, catch (Exception e), and return inside finally.

[Java Basics] Recursion in Java: How It Works and When to Use It

How recursion works in Java: the base case and the recursive case, factorial traced frame by frame, a real StackOverflowError from a missing base case, recursion depth and -Xss, why naive Fibonacci needs 2692537 calls for fib(30) while memoisation needs 59, recursion versus iteration, and why the JVM does not optimise tail calls.

[Java Basics] Custom Exceptions in Java: Writing Your Own Exception Types

How to write a custom exception class in Java: extends Exception versus extends RuntimeException and what each really costs the caller, the four Throwable constructors, carrying structured data as fields, chaining a cause and reading the Caused by section and its ... N more line, exception translation at an API boundary, and when IllegalArgumentException already says everything you wanted to say.

[Java Basics] Inheritance in Java: extends and super

How extends works in Java, what a subclass inherits and what it does not, why constructors are never inherited, how super(...) chains constructors up to java.lang.Object and back down, field hiding versus overriding, protected across packages, final classes, the fragile base class problem, and when composition is the better answer.