algoz
Chapters/01 Java basics/Casting and type promotion

Casting and type promotion

Convert numeric types safely, navigate expression promotions, and stop silent overflow.

Builds on
You'll gain
Know every conversion Java makeswithout overflow or precision loss
Practice
Sheet coming sooncoding problems for this note
Summary

Java widens numbers to a bigger type on its own, but going to a smaller type needs a cast, and a cast keeps only the bits that fit.

The idea in real life

Think of pouring liquids between measuring cups in a kitchen. Pouring water from a one-cup cup into a four-cup jug is completely safe and never spills a single drop. Pouring water from a four-cup jug into a one-cup cup risks spilling liquid over the counter unless you know the jug holds only a small amount and you pour with deliberate care. In Java, widening conversions are the safe transfer into a larger container, while narrowing casts are the risky pour into a smaller container where excess data is discarded.

Why it exists

  1. Different calculations require different container sizes, from compact 8-bit bytes in network buffers to 64-bit integers for huge counts.
  2. The compiler prevents accidental data loss by refusing to squeeze large types into smaller variables automatically.
  3. The JVM only does arithmetic on int, long, float and double, so smaller types like byte and short are promoted to int first.
  4. Knowing these rules ensures you widen operands before operations overflow and catch subtle bit truncations before they produce wrong answers.

How it works

Widening conversions

Java widens smaller numeric types to larger ones automatically without casting.

A widening primitive conversion changes a value of a smaller primitive type into a wider primitive type. Java performs widening conversions automatically whenever you assign a narrower value to a wider variable, pass a narrower argument to a wider method parameter, or mix types in an expression. You never need an explicit cast operator for widening.

As introduced in variables and primitive types, Java provides six numeric types along with the char type. The Java Language Specification (JLS §5.1.2) defines 19 specific widening primitive conversions:

Source typeWidens automatically toConversion behavior
byteshort, int, long, float, doubleSign-extends 8 bits into 16, 32, or 64 bits
shortint, long, float, doubleSign-extends 16 bits into 32 or 64 bits
charint, long, float, doubleZero-extends unsigned 16 bits into 32 or 64 bits
intlong, float, doubleExact to long; preserves overall magnitude to floating point
longfloat, doublePreserves overall magnitude; may round least significant digits
floatdoublePreserves exact 32-bit float value in 64-bit double precision

Two boundaries in this table are worth special attention:

  • byte does not widen to char. A byte is signed (values -128 to 127), while a char is unsigned (values 0 to 65,535). Converting a byte to char requires both sign-extending to int and then truncating to 16 bits (JLS §5.1.4).
  • short does not widen to char, and char does not widen to short. Both occupy 16 bits, but short uses two's complement signed representation while char represents unsigned Unicode code points.

The following program demonstrates widening conversions across integer and floating-point types. In this program:

  • smallNumber is a 8-bit byte holding 42.
  • widerNumber widens smallNumber to a 16-bit short.
  • standardNumber widens widerNumber to a 32-bit int.
  • largeNumber widens standardNumber to a 64-bit long.
  • decimalNumber widens standardNumber to a 64-bit double.
Main.java
public class Main {
    public static void main(String[] args) {
        byte smallNumber = 42;
        short widerNumber = smallNumber;
        int standardNumber = widerNumber;
        long largeNumber = standardNumber;
        double decimalNumber = standardNumber;

        System.out.println("byte widened to int: " + standardNumber);
        System.out.println("int widened to long: " + largeNumber);
        System.out.println("int widened to double: " + decimalNumber);
    }
}
Output
byte widened to int: 42
int widened to long: 42
int widened to double: 42.0

Narrowing with a cast

Narrowing requires an explicit cast and discards high-order bits.

A narrowing primitive conversion converts a value of a wider type into a narrower type (JLS §5.1.3), such as converting long to int or int to byte. Because narrowing can discard information or alter the sign of a number, the Java compiler refuses to perform narrowing automatically.

To narrow a type, you must place the target type in parentheses immediately before the expression: (targetType) expression. This syntax is an explicit cast. It tells the compiler that you are aware of the conversion and accept any truncation or overflow that occurs.

When the original value fits within the range of the target type, the cast preserves the exact value. When the original value lies outside that range, Java does not report an error or throw an exception at runtime. Instead, it discards the higher-order bits and keeps only the lowest bits that fit the target type.

For example, when casting a 32-bit int with value 300 to an 8-bit byte:

  1. The integer 300 in binary is 00000000 00000000 00000001 00101100.
  2. Casting to byte discards the top 24 bits, leaving the low 8 bits: 00101100.
  3. In decimal, 00101100 equals 32 + 8 + 4 = 44.

Java makes one helpful exception for constant expressions (JLS §5.2). If you assign a compile-time constant integer literal to a byte, short, or char variable, you do not need an explicit cast as long as the constant value fits within the target type's valid range:

Java
byte validConstant = 100; // Compiles: 100 fits in byte (-128 to 127)

However, if the expression uses a variable rather than a constant literal, the compiler requires an explicit cast even if the runtime value would fit:

text
Main.java:5: error: incompatible types: possible lossy conversion from int to byte
        byte target = variable;
                      ^

The following program demonstrates narrowing casts that preserve values as well as casts that truncate bits. In this program:

  • normalCount holds 1,000,000 in a long, which fits cleanly inside an int.
  • narrowedCount stores the result of casting normalCount to int.
  • smallValue holds 100 in an int, which fits inside a byte.
  • narrowedSmall stores the result of casting smallValue to byte.
  • overflowValue holds 300 in an int, which exceeds a byte's maximum range of 127.
  • narrowedOverflow stores the truncated result of casting overflowValue to byte.
Main.java
public class Main {
    public static void main(String[] args) {
        long normalCount = 1_000_000L;
        int narrowedCount = (int) normalCount;

        int smallValue = 100;
        byte narrowedSmall = (byte) smallValue;

        int overflowValue = 300;
        byte narrowedOverflow = (byte) overflowValue;

        System.out.println("long narrowed to int: " + narrowedCount);
        System.out.println("int 100 narrowed to byte: " + narrowedSmall);
        System.out.println("int 300 narrowed to byte: " + narrowedOverflow);
    }
}
Output
long narrowed to int: 1000000
int 100 narrowed to byte: 100
int 300 narrowed to byte: 44

Numeric promotion in expressions

Binary arithmetic promotes byte, short, and char operands to 32-bit integers.

Java does not perform arithmetic directly on byte, short, or char values. Before evaluating arithmetic operators, Java applies numeric promotion rules (JLS §5.6).

Under binary numeric promotion, if neither operand is a double, float, or long, Java automatically promotes both operands to 32-bit int before executing the operator. This means adding two byte variables produces an int result, not a byte.

If you try to store the sum of two byte variables directly back into a byte, the compiler rejects the code:

text
Main.java:5: error: incompatible types: possible lossy conversion from int to byte
        byte sum = first + second;
                         ^

To store the result back into a byte, you must explicitly cast the entire sum: byte sum = (byte) (first + second);. Notice the parentheses around (first + second). Without parentheses, (byte) first + second casts only first to byte, which is immediately promoted back to int when added to second.

Unary operators also trigger promotion. Under unary numeric promotion (JLS §5.6.1), applying unary plus (+), unary minus (-), or bitwise complement (~) to a byte, short, or char operand promotes the operand to an int. Thus, -first produces a 32-bit int.

The following program demonstrates binary and unary numeric promotion with byte operands. In this program:

  • first and second are byte variables holding 40 and 50.
  • sumAsInt receives the uncast sum, which has type int.
  • sumAsByte casts the evaluated sum back into byte.
  • negated receives the result of applying unary minus to first.
Main.java
public class Main {
    public static void main(String[] args) {
        byte first = 40;
        byte second = 50;

        int sumAsInt = first + second;
        byte sumAsByte = (byte) (first + second);
        int negated = -first;

        System.out.println("byte + byte as int: " + sumAsInt);
        System.out.println("byte + byte cast back to byte: " + sumAsByte);
        System.out.println("negated byte as int: " + negated);
    }
}
Output
byte + byte as int: 90
byte + byte cast back to byte: 90
negated byte as int: -40

char and int conversions

char stores 16-bit Unicode codes and promotes to int during arithmetic.

As covered in variables and primitive types, a char stores a 16-bit unsigned integer representing a Unicode code point. The character 'a', for example, has the numeric code 97.

Because char values are numeric under the hood, you can use arithmetic operators on characters, as discussed in operators. When you evaluate an expression like 'a' + 1, binary numeric promotion widens both 'a' (code 97) and 1 to int. The addition evaluates to the integer 98.

If your goal is to find the next character in sequence ('b'), you must cast the integer result back to char: (char) ('a' + 1).

In algorithm problems, you will frequently map lowercase English letters to array indices from 0 to 25. Subtracting 'a' from any lowercase character triggers numeric promotion and calculates the character's zero-based alphabet position:

  • 'a' - 'a' evaluates to 97 - 97 = 0.
  • 'b' - 'a' evaluates to 98 - 97 = 1.
  • 'z' - 'a' evaluates to 122 - 97 = 25.

The following program demonstrates converting characters to integers, advancing a character with a cast, and computing an alphabet index. In this program:

  • baseCharacter holds 'a'.
  • nextCode stores 'a' + 1 as an int.
  • nextCharacter casts the advanced code back to a char.
  • sampleLetter holds 'c'.
  • alphabetIndex computes sampleLetter - 'a'.
Main.java
public class Main {
    public static void main(String[] args) {
        char baseCharacter = 'a';
        int nextCode = baseCharacter + 1;
        char nextCharacter = (char) (baseCharacter + 1);

        char sampleLetter = 'c';
        int alphabetIndex = sampleLetter - 'a';

        System.out.println("'a' + 1 as int: " + nextCode);
        System.out.println("(char) ('a' + 1): " + nextCharacter);
        System.out.println("Alphabet index of 'c': " + alphabetIndex);
    }
}
Output
'a' + 1 as int: 98
(char) ('a' + 1): b
Alphabet index of 'c': 2

Going deeper

Mixed expressions

Java promotes operands step by step based on operator precedence and types.

When an expression combines different primitive types, Java does not convert every variable to the widest type all at once. Instead, binary numeric promotion (JLS §5.6.2) applies to each operator one at a time, moving strictly according to operator precedence and associativity:

  1. If either operand is of type double, the other operand is converted to double.
  2. Otherwise, if either operand is of type float, the other operand is converted to float.
  3. Otherwise, if either operand is of type long, the other operand is converted to long.
  4. Otherwise, both operands are converted to type int.

Consider the mixed expression count * factor + rate where count is an int (5), factor is a long (2L), and rate is a double (1.5):

  1. Multiplication first: Multiplication (*) has higher precedence than addition (+). The operands are count (int) and factor (long). Following rule 3, Java widens count to long (5L). The multiplication evaluates in 64-bit integer arithmetic to 10L.
  2. Addition second: The remaining expression is 10L + rate. The operands are 10L (long) and rate (double). Following rule 1, Java widens 10L to double (10.0). The addition evaluates in 64-bit floating point to 11.5.

Because promotion happens operator by operator, subexpressions evaluated earlier can lose precision before a wider type ever touches them. In (1 / 2) * 3.0, the subexpression 1 / 2 involves two integers. Integer division truncates the result to 0 before multiplication with 3.0 occurs. Writing (1.0 / 2) * 3.0 ensures the initial division uses floating-point arithmetic and produces 0.5, yielding 1.5.

The following program traces step-by-step promotion in mixed expressions. In this program:

  • count is an int set to 5.
  • factor is a long set to 2L.
  • rate is a double set to 1.5.
  • combinedResult evaluates count * factor + rate.
  • truncatedDivision evaluates (1 / 2) * 3.0.
  • floatingDivision evaluates (1.0 / 2) * 3.0.
Main.java
public class Main {
    public static void main(String[] args) {
        int count = 5;
        long factor = 2L;
        double rate = 1.5;

        double combinedResult = count * factor + rate;
        double truncatedDivision = (1 / 2) * 3.0;
        double floatingDivision = (1.0 / 2) * 3.0;

        System.out.println("5 * 2L + 1.5: " + combinedResult);
        System.out.println("(1 / 2) * 3.0: " + truncatedDivision);
        System.out.println("(1.0 / 2) * 3.0: " + floatingDivision);
    }
}
Output
5 * 2L + 1.5: 11.5
(1 / 2) * 3.0: 0.0
(1.0 / 2) * 3.0: 1.5

Overflow before widening

Integer multiplication overflows at 32 bits before assignment widens to long.

A common source of errors in algorithm code is writing an assignment like long total = width * height; where both width and height are 32-bit int variables.

The assignment operator (=) evaluates its entire right-hand expression before storing the result in the variable on the left. In width * height, both operands are int. Following binary promotion rule 4, the multiplication executes entirely in 32-bit signed integer arithmetic.

If width is 1,000,000 and height is 3,000, their mathematical product is 3,000,000,000. Because 3,000,000,000 exceeds Integer.MAX_VALUE (2,147,483,647), the multiplication overflows in 32-bit two's complement and wraps to -1,294,967,296. Only after this overflow occurs does Java widen the corrupt negative value to long.

To fix this, cast at least one operand to long before the operator runs:

Java
long total = (long) width * height;

The cast operator (long) binds tighter than multiplication. Java converts width to long first. Then, by binary numeric promotion rule 3, Java widens height to long and performs the entire multiplication in 64-bit integer arithmetic, producing the correct product without overflow.

The following program demonstrates 32-bit integer overflow during multiplication and the fix using an explicit cast. In this program:

  • width holds 1,000,000 and height holds 3,000.
  • overflowedArea calculates the product without a cast, overflowing into a negative long.
  • correctArea casts width to long before multiplying, preserving the full product.
Main.java
public class Main {
    public static void main(String[] args) {
        int width = 1_000_000;
        int height = 3_000;

        long overflowedArea = width * height;
        long correctArea = (long) width * height;

        System.out.println("Overflowed product in long: " + overflowedArea);
        System.out.println("Correct product in long: " + correctArea);
    }
}
Output
Overflowed product in long: -1294967296
Correct product in long: 3000000000

Compound assignment's hidden cast

Compound assignment operators quietly insert a cast back to the target type.

As explained in operators, Java offers compound assignment operators such as +=, -=, *=, and /=. Beyond updating a variable in place, compound assignment operators contain an important specification rule: they secretly insert an explicit cast.

According to JLS §15.26.2, a compound assignment expression of the form variable op= expression is exactly equivalent to:

Java
variable = (TargetType) ((variable) op (expression))

where TargetType is the declared type of variable, with variable evaluated only once.

This hidden cast explains why short score = 30000; score = score + 10000; fails to compile with an incompatible types error (the sum is an int), but score += 10000; compiles cleanly without complaint. Java transforms the statement into score = (short) (score + 10000);.

While convenient, this automatic cast can silently mask severe truncation bugs. If score is 30,000 and you add 10,000, the mathematical sum 40,000 exceeds Short.MAX_VALUE (32,767). The inserted (short) cast discards the upper bits and leaves -25,536 in score. The compiler gives no warning, and no runtime exception is thrown.

The following program demonstrates silent overflow caused by the hidden cast inside compound assignment operators. In this program:

  • score is a 16-bit short initialised to 30,000.
  • score += 10_000 overflows Short.MAX_VALUE and wraps to -25,536.
  • level is an 8-bit byte initialised to 100.
  • level *= 2 overflows Byte.MAX_VALUE (127) and wraps to -56.
Main.java
public class Main {
    public static void main(String[] args) {
        short score = 30_000;
        score += 10_000;

        byte level = 100;
        level *= 2;

        System.out.println("short 30000 += 10000: " + score);
        System.out.println("byte 100 *= 2: " + level);
    }
}
Output
short 30000 += 10000: -25536
byte 100 *= 2: -56

Casting doubles to integers

Floating-point casts truncate toward zero, clamp infinities, and turn NaN into zero.

Converting a double or float to an integer type requires an explicit cast (JLS §5.1.3). As discussed in floating point, floating-point numbers store fractional values that integer types cannot hold.

Java applies three specific rules when casting a floating-point value to an integer:

  1. Truncation toward zero: The fractional digits are discarded. Positive values round downward ((int) 3.8 becomes 3), while negative values round upward toward zero ((int) -3.8 becomes -3). This differs from Math.floor, which always rounds downward toward negative infinity (Math.floor(-3.8) gives -4.0).
  2. Clamping at boundaries: If the floating-point value is larger than Integer.MAX_VALUE (2,147,483,647) or equals Double.POSITIVE_INFINITY, the cast clamps the result to Integer.MAX_VALUE. If the value is smaller than Integer.MIN_VALUE (-2,147,483,648) or equals Double.NEGATIVE_INFINITY, the cast clamps the result to Integer.MIN_VALUE.
  3. NaN becomes zero: If the floating-point value is Double.NaN ("Not a Number"), the cast produces 0.

The following program demonstrates truncation toward zero, extreme value clamping, and NaN conversion. In this program:

  • positiveDecimal holds 3.8 and negativeDecimal holds -3.8.
  • positiveTruncated and negativeTruncated store the results of casting them to int.
  • clampedMax casts 1e12 (1 trillion), which clamps to Integer.MAX_VALUE.
  • clampedMin casts -1e12, which clamps to Integer.MIN_VALUE.
  • nanConverted casts Double.NaN, which produces 0.
Main.java
public class Main {
    public static void main(String[] args) {
        double positiveDecimal = 3.8;
        double negativeDecimal = -3.8;

        int positiveTruncated = (int) positiveDecimal;
        int negativeTruncated = (int) negativeDecimal;

        int clampedMax = (int) 1e12;
        int clampedMin = (int) -1e12;
        int nanConverted = (int) Double.NaN;

        System.out.println("(int) 3.8: " + positiveTruncated);
        System.out.println("(int) -3.8: " + negativeTruncated);
        System.out.println("(int) 1e12: " + clampedMax);
        System.out.println("(int) -1e12: " + clampedMin);
        System.out.println("(int) Double.NaN: " + nanConverted);
    }
}
Output
(int) 3.8: 3
(int) -3.8: -3
(int) 1e12: 2147483647
(int) -1e12: -2147483648
(int) Double.NaN: 0

Under the hood

Precision loss in widening

Widening to floating point can lose low-order digits without any warning.

Although converting an int to float or a long to double is classified by Java as a widening conversion (JLS §5.1.2) and requires no cast, it can silently lose precision.

The magnitude of the number is never lost because the floating-point exponent accommodates numbers far larger than 64-bit integers. However, floating-point types have fewer bits dedicated to precision (the significand) than integer types have in total:

  • An int has 32 bits of precision. A float has only 24 bits of significand (23 stored bits plus 1 implicit leading bit).
  • A long has 64 bits of precision. A double has only 53 bits of significand (52 stored bits plus 1 implicit leading bit).

Because a float has 24 bits of significand, it can represent every integer up to $2^24$ (16,777,216) exactly. Beyond $2^24$, the gap between representable floats exceeds 1. The odd number $16,777,217$ ($2^24 + 1$) requires 25 significant binary bits. When widened to float, IEEE 754 round-to-nearest-even rounds the value down to $16,777,216.0f$. Casting back to int yields $16,777,216$, losing the last digit.

Similarly, a double can represent every integer up to $2^53$ ($9,007,199,254,740,992L$) exactly. The value $2^53 + 1$ ($9,007,199,254,740,993L$) requires 54 significant binary bits. Widening it to double rounds away the least significant bit, producing $9,007,199,254,740,992.0$.

The following program demonstrates precision loss when widening large integers to floating-point types. In this program:

  • originalInt holds $16,777,217$.
  • floatValue widens originalInt to float, rounding away the last bit.
  • roundedInt casts floatValue back to int.
  • originalLong holds $9,007,199,254,740,993L$.
  • doubleValue widens originalLong to double, rounding away the last bit.
  • roundedLong casts doubleValue back to long.
Main.java
public class Main {
    public static void main(String[] args) {
        int originalInt = 16_777_217;
        float floatValue = originalInt;
        int roundedInt = (int) floatValue;

        long originalLong = 9_007_199_254_740_993L;
        double doubleValue = originalLong;
        long roundedLong = (long) doubleValue;

        System.out.println("Original int: " + originalInt);
        System.out.println("Converted to float and back: " + roundedInt);
        System.out.println("Original long: " + originalLong);
        System.out.println("Converted to double and back: " + roundedLong);
    }
}
Output
Original int: 16777217
Converted to float and back: 16777216
Original long: 9007199254740993
Converted to double and back: 9007199254740992

What a narrowing cast does to the bits

Narrowing keeps the lowest bits and interprets them in two's complement.

At the machine level, narrowing an integer value to a smaller type discards the high-order bits and retains the lowest $n$ bits, where $n$ is the bit width of the destination type (JLS §5.1.3). The processor does not check whether the truncated value is sensible. It takes the remaining $n$ bits and interprets them directly using signed two's complement representation.

Let us trace what occurs during (byte) 200:

  1. The number 200 stored in a 32-bit int has the binary pattern: 00000000 00000000 00000000 11001000
  2. Casting to byte retains only the lowest 8 bits: 11001000
  3. In an 8-bit signed byte, the highest bit (bit 7) is the sign bit. Because bit 7 is 1, the value is negative.
  4. Using two's complement decoding: the value is $-2^7 + 2^6 + 2^3 = -128 + 64 + 8 = -56$. Alternatively, subtracting the modulus: $200 - 256 = -56$.

A positive number cast to a smaller container can flip into a negative number whenever its bit at position $n - 1$ is 1. This bit-level truncation never halts program execution; it silently stores the transformed two's complement value into memory.

Bytecode for casts

The JVM uses explicit conversion opcodes on the operand stack.

The Java Virtual Machine (JVM) operand stack operates primarily on 32-bit integers, 64-bit longs, 32-bit floats, and 64-bit doubles. There are no separate bytecode instructions for adding or subtracting byte, short, or char values. Those types are held on the stack as 32-bit integers.

When a Java program performs widening or narrowing conversions between primitive types, the compiler emits dedicated conversion instructions into bytecode:

  • i2l: Widens a 32-bit int to a 64-bit long by sign-extending the top 32 bits.
  • l2i: Narrows a 64-bit long to a 32-bit int by discarding the upper 32 bits on the operand stack.
  • i2b: Narrows a 32-bit int on the stack to an 8-bit byte value, truncating the value to 8 bits and then sign-extending it back to a 32-bit int on the stack.
  • d2i: Converts a 64-bit double to a 32-bit int, executing the IEEE 754 truncation, clamping, and NaN checks directly.

Here is part of the javap -c output for a method with these conversions. The // comments are added here to explain each line; javap does not print them:

text
       3: iload_1             // Load int variable 'count' onto stack
       4: i2l                 // Widen int to long on stack
       5: lstore_2            // Store into long variable 'widerCount'
       6: lload_2             // Load long variable 'widerCount' onto stack
       7: l2i                 // Narrow long to int (discards top 32 bits)
       8: istore        4     // Store into int variable 'narrowedCount'
      10: iload_1             // Load int variable 'count' onto stack
      11: i2b                 // Narrow int to byte (truncates and sign-extends)
      12: istore        5     // Store into byte variable 'narrowedByte'
      14: ldc2_w        #7    // Load double constant 3.14 onto stack
      17: dstore        6     // Store into double variable 'ratio'
      19: dload         6     // Load double variable 'ratio' onto stack
      21: d2i                 // Convert double to int (truncates toward zero)
      22: istore        8     // Store into int variable 'truncatedRatio'

Notice instruction 11 (i2b). Even though narrowedByte is stored as a byte variable in local variable slot 5, on the operand stack it is manipulated as a 32-bit integer. This design of the JVM is why expressions on byte values give an int.

In Java

The following program demonstrates safe widening for accumulator products, safe clamping of floating-point coordinates to integer bounds, and character offset conversions for alphabet indexing.

In this program:

  • multiplySafely(quantity, unitPrice) multiplies two 32-bit integers without overflow by casting the first argument to long, returning their 64-bit product.
  • clampToGrid(coordinate, maxIndex) converts a floating-point coordinate to an integer cell index. It returns 0 for negative or NaN coordinates, truncates valid decimals toward zero, and clamps values exceeding maxIndex to maxIndex.
  • letterToIndex(character) converts a lowercase ASCII letter into its zero-based index between 0 and 25 by subtracting 'a'.
  • quantity holds 2,000,000 and unitPrice holds 3,000.
  • totalCost stores the safely calculated 64-bit product of 6,000,000,000.
  • coordinateInside, coordinateBelow, and coordinateBeyond test normal, negative, and out-of-range floating-point coordinates against maxIndex (99).
  • sampleLetter holds 'h' and index captures its 0-based alphabet index (7).
Main.java
public class Main {
    // Multiplies two 32-bit integers safely, returning their 64-bit product without overflow.
    static long multiplySafely(int quantity, int unitPrice) {
        return (long) quantity * unitPrice;
    }

    // Truncates a floating-point coordinate to an integer cell, clamping values to [0, maxIndex].
    static int clampToGrid(double coordinate, int maxIndex) {
        if (Double.isNaN(coordinate) || coordinate <= 0.0) {
            return 0;
        }
        int cell = (int) coordinate;
        return Math.min(cell, maxIndex);
    }

    // Converts a lowercase ASCII letter to its zero-based index in the range [0, 25].
    static int letterToIndex(char character) {
        return character - 'a';
    }

    public static void main(String[] args) {
        int quantity = 2_000_000;
        int unitPrice = 3_000;
        long totalCost = multiplySafely(quantity, unitPrice);

        double coordinateInside = 4.75;
        double coordinateBelow = -2.5;
        double coordinateBeyond = 150.0;
        int maxIndex = 99;

        int cellInside = clampToGrid(coordinateInside, maxIndex);
        int cellBelow = clampToGrid(coordinateBelow, maxIndex);
        int cellBeyond = clampToGrid(coordinateBeyond, maxIndex);

        char sampleLetter = 'h';
        int index = letterToIndex(sampleLetter);

        System.out.println("Safe total cost: " + totalCost);
        System.out.println("Grid cell for 4.75: " + cellInside);
        System.out.println("Grid cell for -2.5: " + cellBelow);
        System.out.println("Grid cell for 150.0: " + cellBeyond);
        System.out.println("Alphabet index for 'h': " + index);
    }
}
Output
Safe total cost: 6000000000
Grid cell for 4.75: 4
Grid cell for -2.5: 0
Grid cell for 150.0: 99
Alphabet index for 'h': 7

Traps

  1. Multiplying two ints before assigning to long

    Two int operands multiply as a 32-bit int. If their product exceeds 2_147_483_647, it overflows before the assignment widens it to long. Cast the first operand to long so multiplication executes in 64 bits.

    ✕ WRONGlong total = first * second;
    ✓ RIGHTlong total = (long) first * second;
  2. Casting negative doubles expecting downward rounding

    Casting a double to int truncates toward zero rather than downward. A coordinate of -2.7 becomes -2, not -3. When grouping negative coordinates into spatial buckets, use Math.floor before casting.

    ✕ WRONGint bucket = (int) coordinate;
    ✓ RIGHTint bucket = (int) Math.floor(coordinate);
  3. Unexpected sign flip during byte narrowing

    Casting an int between 128 and 255 to byte sets the high bit to 1, turning it into a negative number in two's complement. If you need the raw 0 to 255 value as an integer, mask with & 0xFF.

    ✕ WRONGbyte stored = (byte) unsignedValue;
    ✓ RIGHTint masked = unsignedValue & 0xFF;
  4. Adding an int variable to a char without a cast

    Arithmetic on char promotes both operands to int. Even when offset is 1, the result current + offset is an int, which cannot be stored in a char variable without an explicit cast.

    ✕ WRONGchar next = current + offset;
    ✓ RIGHTchar next = (char) (current + offset);
  5. Losing precision when assigning large int to float

    Widening an int to float is automatic, but float has only 24 bits of significand. Values greater than 16_777_216 lose their least significant bits. Use double or keep IDs in long or int.

    ✕ WRONGfloat metric = largeId;
    ✓ RIGHTdouble metric = largeId;
  6. Assigning Math.round on double directly to int

    Math.round(double) returns a long, not an int. Assigning it directly to an int fails at compile time with a lossy conversion error. Use an explicit cast to (int) or store the result in a long.

    ✕ WRONGint rounded = Math.round(score);
    ✓ RIGHTint rounded = (int) Math.round(score);
  7. Compound assignment hiding silent numeric overflow

    The compound assignment operator += inserts a hidden cast back to short. If shortCount + delta exceeds 32,767, the value silently wraps around to negative without any compile or runtime error.

    ✕ WRONGshortCount += delta;
    ✓ RIGHTshortCount = (short) Math.min(Short.MAX_VALUE, shortCount + delta);
  8. Casting the result of integer division instead of operands

    Parentheses around numerator / denominator cause integer division to run first, discarding the fraction before the cast occurs. Cast numerator directly so division runs in floating point.

    ✕ WRONGdouble ratio = (double) (numerator / denominator);
    ✓ RIGHTdouble ratio = (double) numerator / denominator;
AlgozSet in Geist & Geist MonoJava track