Java will not let you pour a large value into a small variable without a cast, but it will happily promote a small value into a large one. Understanding the two directions — and the arithmetic rules that follow from them — removes most of the "why does this number look wrong" moments in a Java program.
What is widening?
Converting a type to a larger, more capable one happens automatically. A byte becomes a short, a short an int, an int a long, a long a float, a float a double. Nothing is lost, so the compiler does it for you.
int count = 42;
long total = count; // fine: int widens to long
double ratio = count; // fine: int widens to double
short small = 7;
int widened = small; // fine: short widens to int
System.out.println(widened + " " + total + " " + ratio);
Widening is why you can add a byte to an int without thinking about it: the byte is promoted first.
What is narrowing?
Going the other way can lose information, so the compiler demands an explicit cast that says you meant it. The cast does not check anything; it simply reinterprets the bits or the numeric range.
long big = 300L;
int narrowed = (int) big; // 300 — explicit cast required
double price = 19.99;
int whole = (int) price; // 19 — the fraction is discarded, not rounded
System.out.println(narrowed + " " + whole);
Casting a double to an int truncates toward zero, so 19.99 becomes 19 and -19.99 becomes -19. If you want rounding, say so explicitly with Math.round instead of relying on a cast to do something surprising.
What happens in mixed-type arithmetic?
When an operator mixes two numeric types, Java promotes the narrower one until both operands have the same type. The rule is: if either operand is a double, the result is a double; otherwise if either is a float, the result is a float; otherwise if either is a long, the result is a long; otherwise the result is an int.
int a = 7;
long b = 3;
double c = 2.5;
System.out.println(a + b); // 10 (int widened to long)
System.out.println(a + c); // 9.5 (int widened to double)
System.out.println(a / b); // 2 (int / long -> long division)
System.out.println(a / 2.0); // 3.5 (one double -> double division)
System.out.println(7 / 2); // 3
System.out.println(7 / 2.0); // 3.5
System.out.println(7 % 2); // 1
The division line is the one that catches everybody: 7 / 2 is 3, because both operands are integers and integer division discards the remainder. Multiply one of them by a floating literal and the same expression produces 3.5. If you want the remainder as well, the modulus operator gives it to you: 7 % 2 is 1.
What about compound assignments?
Operators such as += and *= have a rule of their own: the right-hand side is promoted to the type of the variable on the left, and the result is converted back. That is why adding a double to an int variable throws away the fraction.
int total = 10;
total += 5; // 15
total *= 2; // 30
total /= 7; // 4 — integer division happens first
byte small = 100;
small += 50; // compile error: 150 does not fit in a byte
That last line is a genuine safety net: the compiler refuses a compound assignment whose result cannot fit the variable, even though the plain assignment small = 150 would compile and overflow at runtime.
What happens when a value is too big?
Integer arithmetic wraps silently. There is no exception and no warning at runtime, which is why overflow bugs surface as impossible numbers days later.
int max = Integer.MAX_VALUE;
System.out.println(max + 1); // wraps to Integer.MIN_VALUE
long big = Long.MAX_VALUE;
System.out.println(big + 1); // wraps to Long.MIN_VALUE
For arithmetic where a wrap is unacceptable, use Math.addExact and Math.multiplyExact, which throw an exception instead of wrapping. And for money, keep the value in integer minor units so no rounding or overflow question arises.
Why do characters turn into numbers?
Arithmetic on a char promotes it to an int, because the arithmetic has to happen somewhere and char is too narrow to hold the result. This is why 'A' + 1 is 66 and why the character codes of letters are contiguous.
char a = 'A';
int code = a + 1; // 66
char b = (char) (a + 1); // 'B'
int sum = 'x' + 'y'; // 232, not "xy"
System.out.println(code + " " + b + " " + sum);
What happens when you cast into a collection?
Mixing primitives and collections causes a surprise worth knowing early. An int added to a List<Integer> is boxed automatically, which works. But comparing an int with the result of equals on an Integer does not: list.contains(5) is true, while list.get(0) == 5 is false, because the == compares an Integer object with a primitive and unboxes one side.
List<Integer> numbers = List.of(1, 2, 3);
System.out.println(numbers.contains(2)); // true
System.out.println(numbers.get(0) == 1); // false!
System.out.println(numbers.get(0).equals(1)); // true
int value = 1;
System.out.println(numbers.get(0) == value); // true — both sides unbox
Two values of the same primitive type are compared by value; an object and a primitive are compared by unboxing the object first, and only then by value. This is why == against a boxed value is a classic source of "impossible" bugs — and why equals is the safe habit for anything that might be boxed.
How do you convert between text and numbers safely?
Casting is not the only way to move between a String and a number, and the two directions fail in different ways. Parsing text into a number is the risky one: Integer.parseInt("12a") throws NumberFormatException at runtime, because the compiler has no idea whether the text holds a number. Going the other way never fails, because every value can be turned into text.
The trap in the other direction is arithmetic on strings. "1" + 2 produces "12", not 3, because the left operand is text and string concatenation wins. You have to convert the text first, and the conversion has to happen before the addition, not after it.
public class TextAndNumbers {
public static void main(String[] args) {
String input = "42";
int parsed = Integer.parseInt(input);
System.out.println("parsed : " + parsed);
System.out.println("doubled : " + (parsed * 2));
System.out.println("text plus number : " + (input + 2));
System.out.println("number as text : " + (parsed + 2));
try {
Integer.parseInt("12a");
} catch (NumberFormatException e) {
System.out.println("bad text : " + e.getMessage());
}
}
}parsed : 42
doubled : 84
text plus number : 422
number as text : 44
bad text : For input string: "12a"The two lines in the middle are the whole lesson. The same + operator produced 422 and 44 from nearly identical-looking code, and only the first operand decided which one you got. When you are unsure, convert the text to a number first and keep it in a variable with a numeric type.
Quick reference
- Widening is automatic; narrowing needs an explicit cast that does not check.
- Arithmetic promotes both operands to the widest type present.
7 / 2is3— integer division truncates; use%for the remainder.- Casting a
doubletointtruncates toward zero, it does not round. - Compound assignment converts the result back to the variable's type, and refuses overflow at compile time.
- Plain integer arithmetic wraps silently;
Math.addExactthrows instead.
Try it yourself: write a program with an int and a double that both hold 7, divide them by 2, and print both results. Then change the divisor to 2.0 and explain the difference. Finally, print Integer.MAX_VALUE + 1 and work out which direction the number wrapped.