JAVA / VARIABLES, PRIMITIVES AND TYPES
Casting, narrowing and data loss
Convert primitives on purpose: predict what a narrowing cast keeps and destroys, and choose truncation, rounding or a checked conversion deliberately.
What you will learn
- Predict integral narrowing by keeping the low bits and rereading the top one as sign
- Choose between (int) truncation, Math.round and Math.floor for double-to-int on purpose
- Explain why long-to-int wraps but double-to-int clamps at Integer.MAX_VALUE
- Spot the hidden cast in b += 300 and the constant rule behind byte b = 100
Understanding Casting, narrowing and data loss
Java converts for you whenever the target type can hold every possible value of the source type; that is widening and it needs no syntax. When the target is smaller the compiler refuses to guess and demands the (type) prefix. A cast is not a conversion routine that checks anything: it is a statement that you accept whatever the language's conversion rule produces. For primitives that rule never fails at runtime, so there is no exception, no warning and no flag to inspect afterwards.
Narrowing between integral types is defined on bits, not on values: the result is the low 8, 16 or 32 bits of the source's two's complement representation, and the top bit of what survives becomes the new sign bit. That one rule explains everything you will see. (byte) 300 keeps 00101100 and gives 44, (byte) 200 keeps 11001000 whose leading 1 means -56, and a nine-digit long cast to int produces a plausible-looking but unrelated number. Casting back widens by sign extension, so the discarded bits are gone for good.
Converting a floating-point value to an integral type follows a completely different rule, and mixing the two mental models is the usual source of surprise. The fraction is discarded toward zero, so (int) 2.99 is 2 and (int) -2.99 is -2, never rounded and never floored. A value too large for the target does not wrap: it clamps to the target's MIN_VALUE or MAX_VALUE, and NaN becomes 0. So an out-of-range double quietly turns into the largest legal number rather than into garbage.
Because the compiler stops complaining the moment you write the parentheses, the cast is the one place where the type system hands responsibility back to you. Treat every (byte), (short), (int) or (char) as a claim you have already checked the range.
public class Narrowing {
public static void main(String[] args) {
int i = 300;
byte b = (byte) i;
System.out.println("(byte) 300 = " + b);
long big = 3_000_000_000L;
System.out.println("(int) 3000000000 = " + (int) big);
double d = -7.99;
System.out.println("(int) -7.99 = " + (int) d);
System.out.println("(int) 1e300 = " + (int) 1e300);
System.out.println("(int) NaN = " + (int) (0.0 / 0.0));
System.out.println("no exception was thrown for any of these");
}
}A narrowing cast is not a request for a safe conversion; it is permission for Java to keep only the bits or magnitude the target type can hold and discard the rest.
Worked examples
Where a cast is not required, and where one is inserted for you
Shows the compile-time constant exemption and the narrowing cast hidden inside compound assignment.
public class ConstantNarrowing {
public static void main(String[] args) {
byte small = 100;
final int limit = 120;
byte fromFinal = limit;
byte sum = 10;
sum += 300;
char letter = 65;
System.out.println(small + " " + fromFinal + " " + sum + " " + letter);
// byte tooBig = 200; // does not compile: 200 is outside byte range
}
}Example explained
Line 1byte small = 100; needs no cast because 100 is a compile-time constant that provably fits, so the compiler can see the assignment is lossless.
Line 2limit is final and initialised with a literal, which makes it a constant expression as well, so it may be assigned to a byte without a cast.
Line 3sum += 300 is defined as sum = (byte)(sum + 300), so the compiler supplies the narrowing cast itself and 310 silently becomes 54.
Line 4Uncommenting byte tooBig = 200; is a compile error: the exemption only covers constants that fit, while the same 200 arriving through an int variable would need (byte) and give -56.
Which bits survive (byte) 456
Makes integral narrowing visible as bit truncation plus reinterpretation of the sign bit.
public class Bits {
public static void main(String[] args) {
int value = 456;
byte narrowed = (byte) value;
System.out.println("456 as int bits: " + Integer.toBinaryString(value));
System.out.println("kept as byte bits: " + Integer.toBinaryString(narrowed & 0xFF));
System.out.println("byte value: " + narrowed);
System.out.println("same bits unsigned: " + (narrowed & 0xFF));
System.out.println("widened back to int: " + (int) narrowed);
}
}Example explained
Line 1456 needs nine bits and a byte stores eight, so the leading 1 is dropped and nothing records that it existed.
Line 2The eight survivors are 11001000, and since bit 7 is now the sign bit the byte reads as -56 rather than 200.
Line 3narrowed & 0xFF promotes the byte to int with sign extension and then masks the extension off, recovering the unsigned reading 200.
Line 4Casting -56 straight back to int sign-extends and yields -56, which is why narrowing is not reversible.
Truncation, rounding and clamping
Contrasts (int) on a double with Math.round and Math.floor, and shows an out-of-range double clamping instead of wrapping.
public class Rounding {
public static void main(String[] args) {
double score = 89.7;
System.out.println("cast: " + (int) score);
System.out.println("round: " + Math.round(score));
System.out.println("cast -0.9: " + (int) -0.9);
System.out.println("floor -0.9: " + (int) Math.floor(-0.9));
System.out.println("cast 5e9 to int: " + (int) 5_000_000_000.0);
System.out.println("cast 5e9 to long: " + (long) 5_000_000_000.0);
}
}Example explained
Line 1(int) score discards the fraction, so 89.7 becomes 89; the cast has no notion of nearest.
Line 2Math.round adds a half and floors, giving 90, and it returns a long, so storing it in an int needs its own cast.
Line 3Truncation moves toward zero, so -0.9 becomes 0 while Math.floor(-0.9) is -1.0: the two disagree for every negative fraction.
Line 45e9 is outside int range so the cast clamps to Integer.MAX_VALUE, yet the identical double lands in a long exactly, proving the clamp depends on the target type and not on the double.
Important notes
Primitive casts never throw. If you want loud failure instead of silence, use Math.toIntExact for long to int, or test against Integer.MIN_VALUE and Integer.MAX_VALUE yourself.
Not all loss requires a cast: int to float and long to double are widening and compile silently, yet float f = 16777217; stores 16777216 because a float has only 24 significand bits.
Common mistakes
Using (int) as a rounding operator: (int) 89.7 is 89 and (int) 89.5 is also 89, so averages, grades and prices computed this way come out systematically one unit low.
Casting a long id, timestamp or file size to int because the values fit today; the first one past 2147483647 wraps to a negative number that still passes every non-null and non-empty check.
Reading a cast that compiles as a validated conversion: byte b = (byte) count; turns 200 into -56 with no exception, and the damage surfaces later as a negative length or index.
Try it yourself
Change, predict, then run
Declare long ms = 5_000_000_000L; then print (int) ms and (short) ms, and finally call Math.toIntExact(ms) inside a try/catch that prints the exception message. Note which of the three actually tells you the value did not fit.
Open the Java workspaceCheck your understanding
Given long big = 5_000_000_000L; and double d = 5_000_000_000.0;, (int) big prints 705032704 while (int) d prints 2147483647. What explains the difference?
- (int) d rounds to the nearest representable int, and 2147483647 is the int closest to 5000000000
- A double stores more digits than a long, so the compiler can detect the overflow for d but not for big
- Integral narrowing keeps only the low-order bits, while a floating-point to integral conversion clamps to the target's range
- (int) big wrapped around twice and landed back in positive territory, while (int) d wrapped only once
Show answer
Casting between integral types is defined as discarding all but the low 32 bits of the two's complement value, so 5,000,000,000 loses its 33rd bit and lands on 705032704. Converting a floating-point value is a separate rule: the fraction is dropped and anything beyond the target range clamps to MIN_VALUE or MAX_VALUE, with NaN becoming 0. The wrapping option is tempting because 705032704 looks like arithmetic overflow, but nothing wraps twice; the bit pattern is truncated once and its top surviving bit happened to be 0, which is why the result came out positive.