Type Conversion and Casting
Know which numeric conversions Java does for you, what an explicit cast throws away, and how to convert strings safely.
long l = someInt; // widening, automatic
int i = (int) someDouble; // narrowing, explicit, truncates
int safe = Math.toIntExact(someLong);
int n = Integer.parseInt(text.strip());Java converts between numeric types in two directions, and treats them very differently.
Widening: automatic
Going to a bigger type happens without a cast: byte to short to int to long to float to double, and char to int and anything wider. The value is kept, with one exception: long or int to float, and long to double, can lose low digits because floats have fewer significant bits. (float) 16_777_217 becomes 1.6777216E7.
Narrowing: you must cast
Going to a smaller type needs an explicit cast, and the cast is your promise that the loss is acceptable. Java does not check anything:
- Double to int truncates toward zero:
(int) 9.99is9,(int) -9.99is-9. UseMath.roundwhen you mean rounding. Out-of-range values saturate instead of wrapping:(int) 1e10isInteger.MAX_VALUEand(int) Double.NaNis0. - A larger integer to a smaller one keeps the low bits:
(byte) 200is-56and(int) 3_000_000_000Lis negative. UseMath.toIntExactwhen the value must fit, so you get an exception instead of a wrong number.
Promotion inside expressions
Arithmetic on byte, short and char is done as int. If either side is long the whole operation is long, and if either side is double it is double. The usual bug is integer division: done / total * 100 with two ints is 0 for any done smaller than total, because 3 / 4 is 0 before the multiplication happens. Cast one operand before dividing, (double) done / total; casting the result, (double) (done / total), is too late.
Compound assignment hides a cast: short s = 1; s += 1; compiles, but s = s + 1; does not.
Strings are not numbers
You cannot cast a String to an int. Parse it with Integer.parseInt, Long.parseLong or Double.parseDouble, and go the other way with String.valueOf or Integer.toString. Parsing is strict: spaces, a decimal point in an integer, or a value out of range all throw NumberFormatException. Everything that arrives over HTTP or from environment variables is a string, so this is where back-end code converts: strip the input, parse it inside a try/catch, and report a clear 400 or a clear configuration error.
Reference casts, such as (String) obj, are a separate topic: they are checked at runtime and throw ClassCastException. Pattern matching with instanceof replaces most of them.
Example
When to use it
- Compute a completion percentage for a job-status endpoint without integer division returning 0.
- Convert a long row count from the database to an int page size with Math.toIntExact.
- Parse PORT and timeout values from environment variables and fail with a clear message if they are not numbers.
More examples
Percentages and integer division
A cast binds tighter than division, so (double) done converts done before the divide. Wrapping the division in brackets casts its already-truncated int result.
void main() {
int done = 3, total = 4;
IO.println(done / total * 100); // 3 / 4 is 0
IO.println(done * 100 / total); // multiply first
IO.println((double) done / total * 100); // cast before dividing
IO.println((double) (done / total) * 100); // cast too late
}
// Output:
// 0
// 75
// 75.0
// 0.0Checked narrowing and float precision
Casting a long that does not fit into an int keeps the low 32 bits and produces a negative number. Math.toIntExact throws instead, and the float example shows that even an automatic widening can drop precision.
void main() {
long rows = 3_000_000_000L;
IO.println((int) rows); // silently wrong
try {
int n = Math.toIntExact(rows);
IO.println(n);
} catch (ArithmeticException e) {
IO.println("toIntExact: " + e.getMessage());
}
IO.println((float) 16_777_217); // widening that loses a digit
IO.println((int) -9.99 + " " + Math.round(-9.99));
}
// Output:
// -1294967296
// toIntExact: integer overflow
// 1.6777216E7
// -9 -10Parsing request input safely
Stripping first lets " 12 " parse, a missing value falls back to a default, and both bad formats and bad ranges become one exception type a handler can map to a 400.
int parsePage(String raw) {
if (raw == null || raw.isBlank()) return 1;
try {
int page = Integer.parseInt(raw.strip());
if (page < 1) throw new IllegalArgumentException("page must be at least 1");
return page;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("page is not a number: " + raw.strip());
}
}
void main() {
for (String raw : new String[] {"3", " 12 ", null, "2.5", "0"}) {
try {
IO.println("page=" + parsePage(raw));
} catch (IllegalArgumentException e) {
IO.println("400: " + e.getMessage());
}
}
IO.println(String.valueOf(42) + Integer.toString(7) + Long.toString(9L));
}
// Output:
// page=3
// page=12
// page=1
// 400: page is not a number: 2.5
// 400: page must be at least 1
// 4279
Discussion