☕ Java

Type Casting

Type casting is converting a value from one type to another. Java supports two kinds: widening (automatic, safe) and narrowing (explicit, potentially lossy). Getting casting wrong causes silent data corruption, ClassCastExceptions, and hard-to-trace bugs. Here's how casting actually works.

What Is Type Casting?

Type casting is the process of converting a value of one data type into another. Java is a strongly typed language — you can't just use a double where an int is expected, or treat a Dog as a Cat. Casting is the mechanism that bridges compatible types when conversion is needed. Java has two categories of casting: Widening casting (implicit) — Converting a smaller type to a larger type. Safe because no data is lost. Java does this automatically with no syntax required. Narrowing casting (explicit) — Converting a larger type to a smaller type. Potentially lossy because the larger value may not fit. You must write the cast explicitly — Java's way of making you acknowledge the risk.

Widening Casting — Automatic and Safe

Widening casting happens automatically when you assign a smaller type to a larger compatible type. No syntax needed — Java inserts the conversion silently because it's guaranteed to be safe.
Java
// The widening chain — each type can widen to any type to its right:
// byteshortintlongfloatdouble

byte b = 42;
short s = b;     // byteshort (automatic)
int i = s;       // shortint (automatic)
long l = i;      // intlong (automatic)
float f = l;     // longfloat (automatic)
double d = f;    // floatdouble (automatic)

// All of these compile with no cast syntax:
int intVal = 100;
long longVal = intVal;       // intlong
double doubleVal = intVal;   // intdouble
float floatVal = intVal;     // intfloat

// char widens to int and up:
char c = 'A';
int charAsInt = c;           // 65 — Unicode code point
long charAsLong = c;         // 65L
double charAsDouble = c;     // 65.0

// Method parameters — widening happens automatically:
void printDouble(double d) { System.out.println(d); }
printDouble(42);        // int 42 widens to double 42.0 automatically
printDouble(100L);      // long widens to double automatically

Narrowing Casting — Explicit and Lossy

Narrowing casting converts a larger type to a smaller type. You must write the target type in parentheses before the value. If the value doesn't fit in the target type, data is silently truncated or corrupted — no exception, no warning.
Java
// Syntax: (targetType) value
double d = 9.99;
int i = (int) d;           // 9 — decimal part is truncated, NOT rounded
System.out.println(i);     // 9

long l = 123456789012L;
int fromLong = (int) l;    // data may be lost if value exceeds int range
System.out.println(fromLong);  // 1912276372 — corrupted value

// The full narrowing chain (each requires explicit cast):
// doublefloatlongintshortbytechar

double pi = 3.14159;
float piFloat = (float) pi;       // 3.141593.1415901 (precision loss)
long piLong = (long) pi;          // 3.141593 (decimal truncated)
int piInt = (int) pi;             // 3.141593
short piShort = (short) pi;       // 3
byte piByte = (byte) pi;          // 3

// Overflow — value wraps around when it doesn't fit:
int big = 300;
byte small = (byte) big;          // 300 doesn't fit in byte (-128 to 127)
System.out.println(small);        // 44 — wrapped around silently

int negative = -1;
byte negByte = (byte) negative;   // -1
char negChar = (char) negative;   // 65535char is unsigned, -1 wraps

// int to char and back:
int code = 65;
char letter = (char) code;        // 'A'
int back = (int) letter;          // 65

// Missing cast = compile error:
double x = 3.14;
int y = x;         // COMPILE ERROR — possible lossy conversion
int z = (int) x;   // correct — explicit acknowledgment of loss

Casting with Objects — Reference Type Casting

Casting also applies to object references — casting up and down an inheritance hierarchy. Upcasting (child to parent) is automatic. Downcasting (parent to child) requires an explicit cast and can fail at runtime with a ClassCastException if the object isn't actually an instance of the target type.
Java
class Animal {
    void speak() { System.out.println("..."); }
}

class Dog extends Animal {
    void speak() { System.out.println("Woof!"); }
    void fetch() { System.out.println("Fetching!"); }
}

class Cat extends Animal {
    void speak() { System.out.println("Meow!"); }
}

// Upcasting — child to parent (automatic, always safe):
Dog dog = new Dog();
Animal animal = dog;    // Dog IS-A Animal — no cast needed
animal.speak();         // "Woof!" — polymorphism, calls Dog's speak()
// animal.fetch();      // COMPILE ERROR — Animal doesn't have fetch()

// Downcasting — parent to child (explicit, risky):
Animal a = new Dog();           // upcast — a holds a Dog object
Dog d = (Dog) a;                // downcast — safe because a IS actually a Dog
d.fetch();                      // works fine

// ClassCastException — downcasting to wrong type:
Animal a2 = new Cat();
Dog d2 = (Dog) a2;   // COMPILE ERROR? No — compiles fine
                      // throws ClassCastException at RUNTIME
                      // because a2 holds a Cat, not a Dog

// Always use instanceof before downcasting:
Animal unknown = getAnimal();
if (unknown instanceof Dog) {
    Dog safedog = (Dog) unknown;   // safe — checked first
    safedog.fetch();
}

// Pattern matching instanceof (Java 16+) — cleaner syntax:
if (unknown instanceof Dog safedog) {
    safedog.fetch();   // no explicit cast needed — already typed as Dog
}

Numeric Casting Pitfalls

These are the specific traps that catch developers when casting between numeric types:
Java
// ── Pitfall 1: double to int truncates, does NOT round ───────────
double d = 9.9999;
int i = (int) d;
System.out.println(i);   // 9 — not 10

// To round, use Math.round() first:
long rounded = Math.round(9.9999);   // 10
int roundedInt = (int) Math.round(9.9999);  // 10

// ── Pitfall 2: long to int silently truncates high bits ──────────
long big = 10_000_000_000L;    // 10 billion — exceeds int range
int truncated = (int) big;
System.out.println(truncated);  // 1410065408 — garbage value, no warning

// Safe check before casting:
if (big >= Integer.MIN_VALUE && big <= Integer.MAX_VALUE) {
    int safe = (int) big;
}

// ── Pitfall 3: float/double precision loss ───────────────────────
double precise = 1234567890.123456789;
float imprecise = (float) precise;
System.out.println(precise);    // 1.2345678901234567E9
System.out.println(imprecise);  // 1.23456794E9 — precision lost

// ── Pitfall 4: char and int interaction ─────────────────────────
char c = 'A';
int code = c;              // widening — 65, no cast needed
char back = (char)(c + 1); // narrowing — 'B', cast required
System.out.println(c + 1);         // 66int arithmetic, not 'B'
System.out.println("" + (char)(c + 1)); // "B" — cast first, then concatenate

// ── Pitfall 5: integer division before widening ──────────────────
int a = 5, b = 2;
double wrong = a / b;          // 2.0 — division happens as int first
double right = (double) a / b; // 2.5 — cast before division

String Conversions — Not True Casting

Converting between String and other types is not casting in the Java sense — there's no inheritance relationship between String and int. These conversions use dedicated methods.
Java
// Primitive → String:
int i = 42;
String s1 = String.valueOf(i);      // "42" — works for any type
String s2 = Integer.toString(i);    // "42"
String s3 = "" + i;                 // "42" — concatenation trick (less preferred)

double d = 3.14;
String ds = String.valueOf(d);      // "3.14"
String ds2 = Double.toString(d);    // "3.14"

boolean b = true;
String bs = String.valueOf(b);      // "true"

// String → Primitive (parsing):
int parsed = Integer.parseInt("42");          // 42
long parsedL = Long.parseLong("123456789");   // 123456789L
double parsedD = Double.parseDouble("3.14");  // 3.14
boolean parsedB = Boolean.parseBoolean("true"); // true

// Parsing fails with NumberFormatException if String isn't a valid number:
int bad = Integer.parseInt("hello");   // throws NumberFormatException
int bad2 = Integer.parseInt("3.14");   // throws NumberFormatException — not an int

// Safe parsing with try-catch:
try {
    int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.out.println("Invalid number: " + input);
}

// Stringchar array and back:
String str = "Hello";
char[] chars = str.toCharArray();     // ['H','e','l','l','o']
String back = new String(chars);      // "Hello"