Upcasting and Downcasting in Java: Casting Objects
The line HeavyBox1 heavy = (HeavyBox1) box; compiles without a single warning, but if box actually holds a ColorBox, the program crashes with a ClassCastException at runtime. The compiler only checks that the cast is possible in principle. The real type of the object is known only to the JVM, which is why you need to understand how reference type casting works in Java.
Reference type casting in Java means changing the type of a reference to an object within an inheritance hierarchy: either up, to a superclass or interface (upcasting), or down, to a subclass (downcasting). The object itself never changes. Only the type you use to access it does. We covered casting of primitive types in a previous lesson. Here we look at type casting of objects, the instanceof operator and arrays.
All examples use the following class hierarchy:
class Box6 {
int width, height, depth;
}
class HeavyBox1 extends Box6 {
int weight;
}
class ColorBox extends Box6 {
String color;
} 1. Upcasting in Java
Upcasting (also called a widening reference conversion) is a move from a more specific type to a less specific one, that is, from a subclass to its parent. It relies on the "is-a" relationship described in the lesson on inheritance in Java.
As with primitive types, widening happens automatically, so you don't need an explicit cast operator. Since Box6 is the superclass and HeavyBox1 is its subclass, you can assign a HeavyBox1 object to a variable of type Box6 directly:
Box6 box = new HeavyBox1(); // implicit upcasting
Box6 sameBox = (Box6) new HeavyBox1(); // explicit upcast: legal, but redundant Upcasting is always safe, because a subclass is guaranteed to have all the fields and methods of its parent. Through a variable of type Box6 you can access only the members of Box6 (width, height, depth), not weight. However, if HeavyBox1 overrides a method of Box6, the overridden version is still called. That is polymorphism at work.
Converting the null type to any reference type is a widening conversion too:
Box6 box = null; 2. Downcasting in Java
The opposite move, down the inheritance tree to a subclass, is called downcasting (a narrowing reference conversion). It requires an explicit cast operator, (Type). You can cast a variable box of type Box6 to HeavyBox1 like this:
Box6 box = new HeavyBox1();
HeavyBox1 heavyBox = (HeavyBox1) box; The next example shows why downcasting is needed.
Suppose a variable box1 of type Box6 points to a HeavyBox1 object. We want to print the weight field of box1. But weight is declared in HeavyBox1, so a Box6 reference can't see it. To read the weight, you have to cast to HeavyBox1: HeavyBox1 heavyBox1 = (HeavyBox1) box1;.
If you try to cast the variable box2, which points to a ColorBox, to HeavyBox1, you get a runtime ClassCastException. There is no compile error, because ColorBox is also a subclass of Box6 and the compiler can't know which object the variable holds. The same happens if the object is an instance of the superclass Box6 itself.
public class CastingExample1 {
public static void main(String[] args) {
Box6 box1 = new HeavyBox1();
// System.out.println(box1.weight); // compile error
HeavyBox1 heavyBox1 = (HeavyBox1) box1;
System.out.println("Weight: " + heavyBox1.weight);
Box6 box2 = new ColorBox();
HeavyBox1 heavyBox2 = (HeavyBox1) box2; // ClassCastException
Box6 box3 = new Box6();
HeavyBox1 heavyBox3 = (HeavyBox1) box3; // ClassCastException
}
} Good to know
An explicit cast is a promise to the compiler: “I know this object has the right type.” The compiler trusts you, and the JVM does the actual check at runtime. If the promise is broken, you get a ClassCastException, so you should usually check the type with instanceof before downcasting.
3. The instanceof operator in Java
The instanceof operator checks whether an object is an instance of a given class, a subclass of it, or a class that implements a given interface. It returns true if the reference is not null and a cast to that type would succeed without a ClassCastException, and false otherwise. For null the result is always false.
public class CastingExample2 {
public static void main(String[] args) {
Box6 box1 = new HeavyBox1();
if (box1 instanceof HeavyBox1) {
System.out.println("Cast 1");
}
if (box1 instanceof Box6) {
System.out.println("Cast 2");
}
if (box1 instanceof Object) {
System.out.println("Cast 3");
}
Box6 box2 = new ColorBox();
if (box2 instanceof HeavyBox1) {
System.out.println("Cast 4");
}
Box6 box3 = new Box6();
if (box3 instanceof HeavyBox1) {
System.out.println("Cast 5");
}
}
} Program output:
Cast 1
Cast 2
Cast 3 A HeavyBox1 object is at the same time a HeavyBox1, a Box6 and an Object, so the first three checks return true. The ColorBox and Box6 objects are not HeavyBox1, so "Cast 4" and "Cast 5" are not printed. Those are exactly the cases where the cast in CastingExample1 threw a ClassCastException.
The typical pattern is to check the type first and then cast. In the example below, the classes Transport and Robot (assumed to be declared elsewhere) implement the Moveable interface:
public class CastingExample3 {
public static void main(String[] args) {
Moveable moveable1 = new Transport();
if (moveable1 instanceof Transport) {
Transport transport = (Transport) moveable1;
transport.start();
}
Moveable moveable2 = new Robot();
if (moveable2 instanceof Transport) {
Transport transport = (Transport) moveable2;
transport.stop();
}
}
} The first check passes and start() is called. In the second branch, moveable2 refers to a Robot, the check returns false, and the cast is skipped, so no exception is thrown.
3.1. Pattern matching for instanceof (Java 16+)
Since Java 16, you can combine the check and the cast. After instanceof you declare a variable that already has the right type (pattern matching for instanceof, JEP 394). No explicit cast is needed:
Box6 box = new HeavyBox1();
if (box instanceof HeavyBox1 heavy) {
System.out.println("Weight: " + heavy.weight);
}
// you can use it right in the condition
if (box instanceof HeavyBox1 heavy && heavy.weight > 10) {
System.out.println("Heavy box");
} The variable heavy is in scope only where the check is guaranteed to have passed. Java 21 extends the same idea to switch (pattern matching for switch), so you can handle several types without a chain of if-else statements:
static String describe(Box6 box) {
return switch (box) {
case HeavyBox1 h -> "Heavy box, weight " + h.weight;
case ColorBox c -> "Color box, " + c.color;
default -> "Plain box";
};
} The default branch is required here because Box6 is not a sealed class, so the compiler can't be sure the listed cases cover every possible subclass.
4. Incompatible casts
Casting between classes is possible only within one inheritance hierarchy. If the compiler can see that the types are unrelated, it reports an error. This example doesn't compile, because Box6 and String have nothing in common:
Box6 box1 = new HeavyBox1();
String str = (String) box1; // compile error: incompatible types The rules for interfaces are looser. A cast to an interface compiles even if the class doesn't implement it, as long as the class is not final: a subclass might exist that does implement the interface. The check then moves to runtime:
Box6 box = new Box6();
Runnable r = (Runnable) box; // compiles, but throws ClassCastException at runtime
// Runnable r2 = (Runnable) "abc"; // compile error: String is final and doesn't implement Runnable 5. Casting arrays
5.1. Arrays and primitive types
You can't convert between an array and a primitive type:
public class ArrayCastingExample1 {
public static void main(String[] args) {
int[] array = new int[5];
// int someNumber = array; // compile error
int someNumber = array[0];
}
} 5.2. Primitive and reference arrays
An array of a primitive type can't be converted to an array of a reference type, and vice versa. Autoboxing doesn't apply to arrays:
public class ArrayCastingExample2 {
public static void main(String[] args) {
Integer[] array1 = new Integer[4];
int[] array2 = new int[4];
// array1 = array2; // compile error
// array2 = array1; // compile error
}
} 5.3. Arrays of different primitive types
You can't convert between arrays of different primitive types, even if the elements themselves widen implicitly (int to long):
public class ArrayCastingExample3 {
public static void main(String[] args) {
int[] array1 = new int[5];
long[] array2 = new long[5];
// array2 = array1; // compile error
}
} 5.4. Reference arrays from the same hierarchy
An array of HeavyBox1 can be assigned to an array of Box6, because HeavyBox1 itself can be converted to Box6. The reverse (narrowing) cast compiles but is checked at runtime: if the real object is a Box6[] array, you get a ClassCastException.
public class ArrayCastingExample4 {
public static void main(String[] args) {
rightConversion();
wrongConversion();
}
private static void rightConversion() {
Box6[] boxArray = new Box6[5];
HeavyBox1[] heavyBoxArray = new HeavyBox1[6];
boxArray = heavyBoxArray;
}
private static void wrongConversion() {
Box6[] boxArray = new Box6[5];
HeavyBox1[] heavyBoxArray = new HeavyBox1[6];
heavyBoxArray = (HeavyBox1[]) boxArray; // ClassCastException
}
} 5.5. Arrays and other reference types
An array can be cast to a non-array reference type in only three cases: to Object, and to the interfaces Cloneable and Serializable, which every array implements.
import java.io.Serializable;
public class ArrayCastingExample5 {
public static void main(String[] args) {
Box6[] array = new Box6[5];
Object object = array;
Cloneable cloneable = array;
Serializable serializable = array;
}
} 5.6. ArrayStoreException
Arrays in Java are covariant: a HeavyBox1[] can be assigned to a variable of type Box6[]. But an array remembers the real type of its elements, and if you try to store an object of the wrong type, the JVM throws an ArrayStoreException:
public class ArrayCastingExample6 {
public static void main(String[] args) {
HeavyBox1[] heavyBox = new HeavyBox1[4];
Box6[] box = heavyBox;
box[0] = new Box6(); // ArrayStoreException
}
} Worth remembering
Generic collections work differently: you can't assign a List<HeavyBox1> to a List<Box6> variable, and the compiler reports an error. The language protects you from a situation that, with arrays, shows up only as an ArrayStoreException at runtime.
6. Upcasting vs downcasting
| Criterion | Upcasting | Downcasting |
|---|---|---|
| Direction | From a subclass to a superclass or interface | From a superclass or interface to a subclass |
| Syntax | Implicit: Box6 b = new HeavyBox1(); | Explicit: (HeavyBox1) b |
| Safety | Always safe | May throw ClassCastException |
| Accessible members | Only those declared in the superclass | All members of the subclass |
| Check before casting | Not needed | instanceof or pattern matching |
Frequently Asked Questions
Does the object change when you cast a reference type?
No. Casting changes only the type of the reference, that is, the set of fields and methods you can access through it. The object in memory stays the same, and overridden methods are always called based on its real class, even after upcasting.
What does instanceof return for null, and can you cast null?
The expression null instanceof AnyType always returns false. Casting (HeavyBox1) null is allowed and doesn't throw an exception. The result is null.
How do you avoid ClassCastException?
Check the type with instanceof before downcasting, and in Java 16+ use pattern matching: if (obj instanceof HeavyBox1 h). Even better, avoid downcasting where you can: move the behavior you need into methods of the superclass or interface, and use generics instead of Object.
How is reference type casting different from casting primitives?
When you cast primitives, the value itself changes: (int) 3.9 gives 3, and narrowing can lose data without any error. When you cast reference types, the value (the object) doesn't change, and a type mismatch makes the JVM throw a ClassCastException. For primitives, see the lesson Primitive Type Conversion and Casting in Java.
Why does a cast to an interface compile even if the class doesn't implement it?
If the class is not final, it may have a subclass that implements the interface, so the compiler allows the cast and defers the check to runtime. For a final class that doesn't implement the interface, the cast is a compile error.
Comments