skip to content

Does casting a reference change the object or its behavior? Explain how casting interacts with field access versus overridden methods.

level: seniorimportance: should knowfreq 48%

answer

  1. Cast retypes the reference, not the object
  2. Methods: dynamic dispatch → runtime type wins
  3. Fields & statics: static binding → declared type wins
  4. Field hiding makes reads cast-sensitive; overriding does not
  5. Identity (==) preserved across a cast

basics

~20 s

Casting never changes the object on the heap; it only changes what type the compiler thinks the reference has, which controls which members are visible. Overridden method calls always run the object's real version, but field access uses the reference's declared type.

solid answer

~50 s

A cast is purely a compile-time/typing operation on the reference, not a transformation of the object. The object on the heap is identical before and after; what changes is the *declared type* the compiler uses to decide which members you may access. For instance methods this barely matters at runtime, because Java uses dynamic dispatch: an overridden method always executes the version belonging to the object's actual class, regardless of the reference type. So upcasting to a supertype and calling an overridden method still runs the subtype override. Fields and static methods are different: they are resolved by the *declared/static* type, not the runtime type — field access is not polymorphic. That asymmetry is exactly why casting can change which field you read (a hidden field) even though it can't change which overridden method you invoke. The practical guidance: rely on overriding for behavior, never shadow fields, and treat a cast as adjusting visibility, not identity.

code

java · 11 lines
java
class Animal { String name = "animal"; String sound() { return "..."; } }
class Dog extends Animal { String name = "dog"; @Override String sound() { return "Woof"; } }

Dog d = new Dog();
Animal a = d;                       // upcast: same object, narrower lens

System.out.println(a.sound());      // Woof   (method: runtime type)
System.out.println(((Animal) d).sound()); // Woof   (cast can't suppress override)
System.out.println(a.name);         // animal (field: declared type)
System.out.println(d.name);         // dog    (field: declared type)
System.out.println(a == d);         // true   (identity preserved)

go deeper

for a junior

Understands a cast changes the reference type, not the object, and overridden methods still run.

for a middle

Can show that methods dispatch on runtime type while casting affects only visible members.

for a senior

Explains the field-hiding asymmetry (static field binding vs dynamic method dispatch), why it exists, and avoids shadowed fields; uses upcasting deliberately to narrow APIs.

for a principal

Treats this as a design constraint: keeps state private behind polymorphic accessors, sets conventions banning field hiding, and reasons about dispatch costs and API surface when exposing supertypes.

## The core claim: a cast retypes the reference, not the object When you write `(Dog) a`, **no object is converted, copied, or modified**. The heap object stays exactly what `new` made it. A cast only changes the **declared (static) type** the compiler associates with the resulting reference, which governs *which members the compiler lets you touch*. Object **identity** (`==`) is preserved: `a == (Dog) a` is always true. To see why field access and method calls diverge, you need two resolution rules. ## Rule 1: instance methods → dynamic dispatch (runtime type) For a (non-static, non-private, non-final-in-the-relevant-sense) instance method, Java decides *which override to run* based on the object's **actual runtime class**, using the virtual method table. The reference's declared type only limits *which method names are visible to call*. ```java class Animal { String sound() { return "..."; } } class Dog extends Animal { String sound() { return "Woof"; } } Animal a = new Dog(); a.sound(); // "Woof" — override runs, even though a is declared Animal ((Animal) new Dog()).sound(); // still "Woof" ``` Upcasting a `Dog` to `Animal` cannot suppress the `Dog` override. Casting changes *visibility*, not *dispatch*. ## Rule 2: fields and static methods → static binding (declared type) **Fields are not polymorphic.** A field reference is resolved against the **declared type** of the reference at compile time. If a subclass declares a field with the same name as the superclass (**field hiding/shadowing**), which one you read depends on the reference type — so a cast *does* change the answer: ```java class Animal { String name = "animal"; } class Dog extends Animal { String name = "dog"; } Dog d = new Dog(); Animal a = d; // upcast System.out.println(a.name); // "animal" — declared type Animal System.out.println(d.name); // "dog" — declared type Dog System.out.println(((Animal) d).name); // "animal" — cast picks the Animal field ``` The **same object** yields different field values depending only on the reference type. Static methods behave the same way — they are *hidden*, not overridden, and resolve by declared type. ## Why the asymmetry exists Dynamic dispatch is the mechanism of polymorphism: it lets a supertype reference invoke specialized subtype behavior, which is the whole point of inheritance. Fields, by contrast, are implementation state, not behavior — making them virtual would be expensive and conceptually muddled, so the language binds them statically. The consequence is a well-known trap: **field hiding looks like overriding but isn't.** ## Practical implications of "casting = retyping" 1. **You can't use casting to choose a method version.** `((Animal) dog).sound()` still calls `Dog.sound()`. There is no way to call a subclass-overridden method's superclass version from outside; only the subclass itself can via `super.sound()`. 2. **Upcasting is a deliberate API-narrowing tool**, e.g. returning `List` instead of `ArrayList` to hide implementation — callers see only the contract. 3. **Never shadow fields.** Same-named fields across a hierarchy produce cast-sensitive reads that surprise everyone. Keep fields private and expose behavior through (overridable) accessor methods, which *are* polymorphic. 4. **Casting and identity:** because identity is preserved, casting is safe to use inside `equals`, hashing, etc.; it doesn't create a new object. ## Summary table | Member kind | Resolved by | Does a cast change it? | |---|---|---| | Overridden instance method | Runtime (actual) type | No | | Field | Declared (static) type | Yes (if hidden) | | Static method | Declared (static) type | Yes (if hidden) | | Object identity (==) | the object | No | The one-line model: **casting changes the lens, not the thing you're looking at — and methods see through the lens to the real object, while fields stop at the lens.**

  • Can you invoke a superclass's overridden method implementation from outside the object via casting?
    No. Dynamic dispatch always selects the object's actual override, so ((Super) obj).m() still runs the subclass m(). Only the subclass itself can reach the parent version, using super.m() from inside its own code.
  • Why do `a.name` and `((Animal) a).name` differ when name is hidden, but methods don't behave this way?
    Field access is bound statically to the reference's declared type, so casting selects which (hidden) field you read. Methods are bound dynamically to the runtime type, so casting can't redirect them.

saying these in an interview costs you the question

  • Saying you can cast to a supertype to call the supertype's method version — overrides still win.
  • Believing fields are polymorphic like methods.
  • Thinking a cast produces or mutates an object.
  • Designing with shadowed fields and expecting overriding-like behavior.

context