Field Shadowing
Shadowing ≠ Overriding
If a subclass declares a field with the same name as a field in its superclass, the subclass field shadows (hides) the superclass field. This is fundamentally different from method overriding.
The critical rule:
Field access is resolved statically by the compile-time (declared) type of the reference, not by the runtime type of the object.
This is one of the most frequently examined concepts on the Tripos paper because it produces counterintuitive results that differ sharply from method dispatch.
A Concrete Example
class A {
int x = 1;
}
class B extends A {
int x = 2;
}
public class Main {
public static void main(String[] args) {
A a = new B();
System.out.println(a.x); // prints 1
System.out.println(((B) a).x); // prints 2
B b = new B();
System.out.println(b.x); // prints 2
System.out.println(((A) b).x); // prints 1
}
}
Even though the object on the heap is a B, a.x prints 1 because:
- The variable
ahas declared typeA. - Field access uses the declared type — the compiler resolves
a.xtoA.xat compile time. - The object’s runtime type (
B) is irrelevant for field access.
This is not a bug — it is by design. The Java Language Specification (JLS) explicitly states that field access expressions are resolved using the compile-time type.
Contrast with Method Overriding
Methods behave differently. Method calls use dynamic dispatch based on runtime type:
class A {
int x = 1;
int getX() { return x; } // returns A.x
}
class B extends A {
int x = 2;
@Override
int getX() { return x; } // returns B.x
}
A a = new B();
System.out.println(a.x); // 1 — field access, declared type A
System.out.println(a.getX()); // 2 — method call, dynamic dispatch to B.getX()
The same object, accessed through the same reference, gives different values depending on whether you access the field directly or call a method. This is the classic Tripos trap.
Another Subtle Example
class A {
int x = 10;
void show() { System.out.println(x); }
}
class B extends A {
int x = 20;
}
B b = new B();
b.show(); // prints 10 — show() is defined in A, and inside A, 'x' means A.x
When show() executes, this refers to the B object. But x inside a method of A always means A.x because field references are lexically scoped — the compiler binds x to A.x when compiling A.show(). The presence of B.x does not affect the code in A.
More Layers
With three classes, the same rule applies:
class A { int x = 1; }
class B extends A { int x = 2; }
class C extends B { int x = 3; }
C c = new C();
System.out.println(c.x); // 3 — C's x
System.out.println(((B) c).x); // 2 — B's x
System.out.println(((A) c).x); // 1 — A's x
A a = c;
System.out.println(a.x); // 1 — declared type A
Using super to Access Shadowed Fields
Within a subclass, super.x accesses the superclass’s version:
class B extends A {
int x = 2;
void printBoth() {
System.out.println("B.x = " + x); // 2
System.out.println("A.x = " + super.x); // 1
}
}
But super.x only goes up one level — you cannot chain super.super.x.
Why Shadow Fields?
Shadowing fields is almost always a mistake. It causes confusion and bugs. In well-written code, subclasses should not redeclare fields with the same name as superclass fields. If a subclass needs additional state, use a different field name.
The JLS permits shadowing because forbidding it would cause problems when independently developed libraries have classes in the same hierarchy with coincidental field name clashes.
The Exam Strategy
When tracing code that involves field access and method calls on objects in a class hierarchy:
- Is it a field access? → Use the declared type of the reference. Go to that class and find the field.
- Is it a method call? → Use the runtime type of the object. Start at that class and look for a matching override, working up the hierarchy if not found.
This distinction — static resolution for fields, dynamic dispatch for methods — is tested year after year.