Java's numeric types have fixed sizes — and fixed limits. An int can only hold values between roughly negative two billion and positive two billion, and it cannot grow just because your data did. Push a calculation past that ceiling and the value wraps around like a car's odometer rolling back to zero. That wrap is called overflow, and Java does not throw an error or even warn you — it silently hands you a wildly wrong number and keeps running. The cost of an overflow bug is precisely that it corrupts silently: the program looks healthy while its numbers are quietly wrong.
The integer and floating-point types
Java gives you a ladder of integer types by width: byte (8 bits), short (16), int (32), and long (64), plus char (16, which holds a character code). For numbers with a fractional part there are float (32) and double (64). The type to reach for by default is int for whole numbers and double for fractions. The boundaries are exact, not approximate: an int runs from -2147483648 to 2147483647, and a byte from -128 to 127. Cross one of those lines and the value stops behaving like ordinary arithmetic.
graph TD B["byte"] --> S["short"] S --> I["int"] I --> L["long"] L --> F["float"] F --> D["double"] C["char"] --> I style I fill:#c2410c,color:#fff style D fill:#c2410c,color:#fff
Widening and narrowing
Widening converts a smaller type into a larger one automatically, because no information can be lost: a byte fits inside an int, which in turn fits inside a long. Narrowing goes the other way — int into byte — and Java refuses to do it silently, because the value may no longer fit. You must spell out the conversion with an explicit cast, (byte) 130, which is you telling the compiler 'I accept the possible loss'. And loss there is: 130 lies outside the byte range, so the cast keeps only the low bits and produces -126.
public class Main {
public static void main(String[] args) {
byte b = (byte) 130;
int big = Integer.MAX_VALUE;
long bigger = big + 1L;
double result = 5 / 2;
double precise = 5 / 2.0;
System.out.println(b);
System.out.println(bigger);
System.out.println(result);
System.out.println(precise);
}
}
flowchart LR A["… 2147483646"] --> B["2147483647 (MAX_VALUE)"] B -->|"+1 wraps around"| C["-2147483648 (MIN_VALUE)"] C --> D["-2147483647 …"] style B fill:#b45309,color:#fff style C fill:#b91c1c,color:#fff
Overflow: the silent odometer
Integers in Java are stored in two's complement, a fixed number of bits per type. When an addition's result is too large to fit, the extra high bit simply has nowhere to go — it falls off the end, and what remains reads as a large negative number. Adding 1 to 2147483647 does not produce 2147483648; it produces -2147483648, the most negative int. There is no exception, no log line, no crash — the program continues with a value its author never intended. Unit tests fed small inputs rarely probe that boundary, which is why overflow so often appears in production rather than in testing.
public class Main {
public static void main(String[] args) {
int max = Integer.MAX_VALUE; // 2147483647
System.out.println(max); // 2147483647
System.out.println(max + 1); // -2147483648 (wrapped!)
long safe = (long) max + 1; // promote first, then add
System.out.println(safe); // 2147483648
}
}
Promotion and integer division
In an expression that mixes types, Java promotes the narrower operand to match the wider one before it calculates — 7 + 2.0 promotes 7 to 7.0 and yields 9.0. The trap waits when both operands are whole numbers: 7 / 2 is integer division, which throws away any remainder and gives 3, not 3.5. Truncation always goes toward zero, so even 9 / 10 comes out as 0. The cure is to make at least one operand a double — write 7 / 2.0 or (double) 7 / 2 — so the division is carried out in floating point and nothing is discarded.
flowchart LR I["7 (int)"] --> P["promoted to 7.0"] D["2.0 (double)"] --> ADD["7.0 + 2.0"] P --> ADD ADD --> R["9.0 (double)"] style D fill:#3776ab,color:#fff style R fill:#15803d,color:#fff
public class Main {
public static void main(String[] args) {
System.out.println(7 / 2); // 3 — both ints, truncated
System.out.println(7.0 / 2); // 3.5 — 2 promoted to double
System.out.println(9 / 10); // 0 — truncated toward zero
}
}
Literal suffixes: L, F, and d
A whole-number literal is an int and a decimal literal is a double, but you can widen a literal right at the source with a suffix. Append L and 42 becomes a long; append F and 3.14 becomes a float; d makes a double explicit. That suffix is not decoration. Writing long big = 3000000000; fails to compile, because 3000000000 is read as an int literal and is already past int's limit — only 3000000000L tells the compiler to read it as a long from the very first character.
What does this program print? Read it carefully and type the exact output (four lines).
public class Main {
public static void main(String[] args) {
byte b = (byte) 130;
int big = Integer.MAX_VALUE;
long bigger = big + 1L;
double result = 5 / 2;
double precise = 5 / 2.0;
System.out.println(b);
System.out.println(bigger);
System.out.println(result);
System.out.println(precise);
}
}
byte range is -128 to 127; 130 wraps around to -126.
Adding 1L promotes the expression to long, so no int overflow occurs.
5 / 2 uses integer division before the assignment to double; 5 / 2.0 promotes to double first.
Integer division check. Predict all three lines. Decide for each whether the division runs in int or floating point.
public class Main {
public static void main(String[] args) {
System.out.println(11 / 2);
System.out.println(11 / 2.0);
System.out.println(7 / 10);
}
}
Two
intoperands mean integer division — the remainder is discarded.One
doubleoperand (the2.0) promotes the whole division to floating point.Truncation goes toward zero, so a fraction below 1 becomes 0.
The 'average' bug. This looks like it should print 3.5. Predict the single line it actually prints.
public class Main {
public static void main(String[] args) {
int a = 5;
int b = 2;
double avg = (a + b) / 2;
System.out.println(avg);
}
}
(a + b)is7, and7 / 2runs inintarithmetic first.Only afterward does the truncated result widen into the
doublevariable.
You write long big = 3000000000; and it fails to compile, even though long can easily hold three billion. Why, and what is the fix?
A bare whole-number literal is an int by default, and 3000000000 exceeds int's maximum of 2147483647 — so the literal itself is rejected before the assignment. Appending L (3000000000L) reads it as a long from the start. A cast would not help, since the literal overflows before the cast ever applies.
What cast is required to store a double value in an int variable?
double is wider than int, so storing it in an int requires an explicit narrowing cast: (int) value. The cast may truncate the decimal.
Recap
- Java's numeric types have fixed widths:
byte,short,int,longfor integers;float,doublefor fractions. - Widening is automatic and safe; narrowing needs an explicit cast and may lose data.
- Overflow wraps in two's complement and is silent —
Integer.MAX_VALUE + 1becomesInteger.MIN_VALUE. - Promotion widens the narrower operand before a mixed-type calculation runs.
- Two whole-number operands give integer division — the remainder is discarded, so
7 / 2is3. - Literal suffixes (
L,F) and a type wide enough for the job stop these traps at the source.
Next you will combine types, operators, and these arithmetic rules into full expressions.