Polymorphism is the ability of one call expression to run different code depending on which object happens to be at the other end of the reference. It is the reason a single loop can handle a Dog, a Cat and a Cow without naming any of them, and it is the payoff for everything inheritance set up.
The whole mechanism rests on one sentence: the static type decides what compiles, the dynamic type decides what runs. Everything below is that sentence applied to methods, fields, static members and casts. Every output line and every error message was produced by compiling and running the code on OpenJDK 21.0.6.
![]()
Two things are decided about every call, at two different times, by two different types — keep them apart and nothing in this article is surprising.
The static type decides what compiles, the dynamic type decides what runs
Every reference variable in Java carries two types. The static type is the one written in the declaration; it is the compiler's entire view of the variable. The dynamic type is the class of the object the variable actually refers to while the program runs, and in general the compiler cannot know it.
class Animal {
String speak() {
return "some generic noise";
}
}
class Dog extends Animal {
@Override
String speak() {
return "Woof";
}
String fetch() {
return "returns the ball";
}
}
public class StaticVsDynamic {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println("declared (static) type : Animal");
System.out.println("actual (dynamic) type : " + a.getClass().getSimpleName());
System.out.println("a.speak() -> " + a.speak());
}
}
declared (static) type : Animal
actual (dynamic) type : Dog
a.speak() -> Woof
One variable, two types, two separate questions:
| Question | Answered when | Answered from |
|---|---|---|
| Does this member exist, and does the call type-check? | compile time | the static type |
| Which method body actually executes? | run time | the dynamic type |
The first row is why the object being a Dog does not help here:
public class StaticTypeLimits {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.fetch());
}
}
StaticTypeLimits.java:13: error: cannot find symbol
System.out.println(a.fetch());
^
symbol: method fetch()
location: variable a of type Animal
1 error
Dog declares fetch(), and the object really is a Dog. javac refuses anyway, and the message names the type it consulted: location: variable a of type Animal. The second row is why a.speak() printed Woof rather than the Animal version. Those two rows explain every result in the rest of this article.
Method overriding: same name, same parameter list, in a subclass
An override is a method in a subclass that replaces a method inherited from a superclass. The two declarations must agree on the name and on the ordered list of parameter types; four further rules constrain everything else.
The signature must match exactly
javac pairs an override with the superclass method by name and by the ordered list of parameter types. Nothing else takes part — not the parameter names, not final on a parameter, not the return type. Change a parameter type and you have declared a second method that merely lives in a subclass, which the next section is about.
An override replaces the body, but the body it replaced stays reachable through super:
class Animal {
String speak() { return "some generic noise"; }
}
class Dog extends Animal {
@Override
String speak() { return super.speak() + ", then Woof"; }
}
public class SuperCall {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println("a.speak() -> " + a.speak());
}
}
a.speak() -> some generic noise, then Woof
super.speak() is the one form of instance call that is not dispatched dynamically: it compiles to invokespecial Animal.speak rather than invokevirtual, so it names one specific implementation and cannot recurse back into Dog.speak().
The return type may be covariant
The override may return the same type, or any subtype of it. That is covariant return, allowed since Java 5, and it lets a caller holding the subclass type skip a cast.
class Animal {
Animal reproduce() { return new Animal(); }
public String toString() { return getClass().getSimpleName(); }
}
class Dog extends Animal {
@Override
Dog reproduce() { return new Dog(); } // covariant: Dog is an Animal
}
public class Covariant {
public static void main(String[] args) {
Animal a = new Dog();
Dog d = new Dog();
Animal child1 = a.reproduce(); // static type Animal
Dog child2 = d.reproduce(); // static type Dog, no cast needed
System.out.println("a.reproduce() -> " + child1);
System.out.println("d.reproduce() -> " + child2);
}
}
a.reproduce() -> Dog
d.reproduce() -> Dog
Both calls run Dog.reproduce() and both produce a Dog. Only the static type of the expression differs, which is why child2 can be declared Dog and child1 cannot. A return type that is not a subtype is rejected, and with @Override present the compiler reports it twice — once for the incompatible type and once because the annotation's promise is now broken:
class Dog extends Animal {
@Override
int speak() { return 42; }
}
BadReturn.java:7: error: speak() in Dog cannot override speak() in Animal
int speak() { return 42; }
^
return type int is not compatible with String
BadReturn.java:6: error: method does not override or implement a method from a supertype
@Override
^
2 errors
The access level may not be narrowed
An override may widen access — protected to public is fine — but never narrow it. If it could, upcasting to the superclass would let a caller reach a method the subclass author tried to hide.
class Animal {
public String speak() { return "some generic noise"; }
}
class Dog extends Animal {
@Override
protected String speak() { return "Woof"; }
}
NarrowAccess.java:7: error: speak() in Dog cannot override speak() in Animal
protected String speak() { return "Woof"; }
^
attempting to assign weaker access privileges; was public
1 error
A wider checked exception may not be thrown
The override may throw fewer checked exceptions, or subclasses of the declared ones, or none at all. It may not add a checked exception the caller was never told to handle.
import java.io.IOException;
class Animal {
void feed() throws IllegalStateException { }
}
class Dog extends Animal {
@Override
void feed() throws IOException { }
}
WiderException.java:9: error: feed() in Dog cannot override feed() in Animal
void feed() throws IOException { }
^
overridden method does not throw IOException
1 error
Unchecked exceptions are exempt entirely — a RuntimeException needs no declaration, so nothing constrains it. All three legal relaxations compile together:
import java.io.FileNotFoundException;
import java.io.IOException;
class Animal {
protected Animal reproduce() throws IOException { return new Animal(); }
@Override public String toString() { return getClass().getSimpleName(); }
}
class Dog extends Animal {
@Override
public Dog reproduce() throws FileNotFoundException { // wider access, covariant return,
return new Dog(); // narrower checked exception
}
}
class Cat extends Animal {
@Override
public Cat reproduce() { // no checked exception at all
throw new IllegalStateException("unchecked is always allowed");
}
}
new Dog().reproduce() -> Dog
Cat.reproduce() threw IllegalStateException: unchecked is always allowed
The four rules in one table:
| Part of the declaration | What the override may do |
|---|---|
| Name and parameter types | must match exactly — anything else is a new method |
| Return type | the same type, or a subtype of it (covariant) |
| Access modifier | the same, or wider; never narrower |
| Checked exceptions | the same, fewer, subtypes of them, or none |
@Override turns a silent mistake into a compile error
@Override is optional. It generates no code and changes no behaviour. What it does is ask the compiler to verify that the method really overrides something, which converts the most common bug in this area from a silent runtime surprise into a build failure.
Without it, a typo and a wrong parameter type both compile cleanly:
class Animal {
String speak() { return "some generic noise"; }
String greet(String name) { return "generic hello, " + name; }
}
class Dog extends Animal {
String Speak() { return "Woof"; } // typo: capital S
String greet(Object name) { return "Woof, " + name; } // wrong parameter type
}
public class SilentOverload {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println("a.speak() -> " + a.speak());
System.out.println("a.greet(\"Rex\") -> " + a.greet("Rex"));
Dog d = new Dog();
System.out.println("d.greet(\"Rex\") -> " + d.greet("Rex"));
System.out.println("d.greet((Object) \"Rex\") -> " + d.greet((Object) "Rex"));
}
}
a.speak() -> some generic noise
a.greet("Rex") -> generic hello, Rex
d.greet("Rex") -> generic hello, Rex
d.greet((Object) "Rex") -> Woof, Rex
Nothing failed, and nothing the author intended happened either. Speak() is simply an unrelated method that no one calls. greet(Object) is worse: it is a legal overload of the inherited greet(String), so d.greet("Rex") still picks the superclass version — a String argument matches greet(String) exactly — and the subclass body only runs when the argument's static type is widened to Object. Two methods with the same name now mean two different things on the same object.
Add the annotation and both mistakes become build failures:
class Dog extends Animal {
@Override String Speak() { return "Woof"; }
@Override String greet(Object name) { return "Woof, " + name; }
}
OverrideCatches.java:7: error: method does not override or implement a method from a supertype
@Override String Speak() { return "Woof"; }
^
OverrideCatches.java:8: error: method does not override or implement a method from a supertype
@Override String greet(Object name) { return "Woof, " + name; }
^
2 errors
⚠️ Put
@Overrideon every method you intend as an override. It is the only mechanical protection against a subclass that quietly stops overriding when someone changes a parameter type in the superclass.
Dynamic dispatch: one call site, three method bodies

Dynamic dispatch is the mechanism behind the second row of the table at the top: when a superclass-typed reference holds a subclass object, the call runs the subclass method. Prove it by putting three different subclasses into one array of superclass-typed slots and calling them in a single loop.
class Animal {
private final String name;
Animal(String name) { this.name = name; }
String name() { return name; }
String speak() { return "some generic noise"; }
}
class Dog extends Animal {
Dog(String name) { super(name); }
@Override String speak() { return "Woof"; }
}
class Cat extends Animal {
Cat(String name) { super(name); }
@Override String speak() { return "Meow"; }
}
class Cow extends Animal {
Cow(String name) { super(name); }
@Override String speak() { return "Moo"; }
}
public class Dispatch {
public static void main(String[] args) {
Animal[] pen = { new Dog("Rex"), new Cat("Mia"), new Cow("Bella") };
for (Animal a : pen) { // static type: Animal
System.out.println(a.name()
+ " dynamic type = " + a.getClass().getSimpleName()
+ " a.speak() -> " + a.speak());
}
}
}
Rex dynamic type = Dog a.speak() -> Woof
Mia dynamic type = Cat a.speak() -> Meow
Bella dynamic type = Cow a.speak() -> Moo
There is exactly one a.speak() in the source, and the variable a is declared Animal on every iteration. Three different bodies ran. The loop contains no if, no type test and no cast, and it names none of the three subclasses — the only thing that changed between iterations is which object the reference pointed at.
This is the whole point of arranging classes into a hierarchy. Code written against Animal keeps working for subclasses that did not exist when it was written.
What the bytecode shows
The claim that the target is chosen at run time is checkable rather than a matter of belief. Compile a method whose parameter is declared Animal and disassemble it.
class Animal {
String speak() { return "some generic noise"; }
}
class Dog extends Animal {
@Override String speak() { return "Woof"; }
}
public class DispatchBytecode {
static String announce(Animal a) { // parameter static type: Animal
return a.speak();
}
public static void main(String[] args) {
System.out.println(announce(new Dog()));
System.out.println(announce(new Animal()));
}
}
Woof
some generic noise
javap -c DispatchBytecode.class prints the instructions for announce:
static java.lang.String announce(Animal);
Code:
0: aload_0
1: invokevirtual #7 // Method Animal.speak:()Ljava/lang/String;
4: areturn
Three facts are visible there, and each one confirms part of the model:
- There is one instruction for the call,
invokevirtual, not a branch or a table of alternatives. The compiler emitted no decision logic. - Its constant-pool reference names
Animal.speak, the static type of the parameter.Dogdoes not appear anywhere in this method, even thoughDog.speak()is what ran for the first call. - Since the same instruction produced
Woofand thensome generic noise, the target cannot have been fixed at compile time.invokevirtualis defined to look up the method on the class of the object on the stack, so the JVM does that lookup on each execution.
That final lookup is what "virtual" means, and it is why a superclass reference can reach a subclass body at all.
Overriding versus overloading

The two words describe unrelated mechanisms that happen to share the idea of one name serving several bodies.
| Overriding | Overloading | |
|---|---|---|
| Signatures | same name, same parameter list | same name, different parameter lists |
| Where the methods live | superclass and subclass | anywhere, usually one class |
| Requires inheritance | yes | no |
| Resolved | at run time | at compile time |
| Decided from | the dynamic type of the receiver | the static types of the arguments |
| Return type | must be the same or covariant | not consulted at all |
| Can it be verified | @Override | nothing equivalent |
The rules that pick between overloads — the four-phase search, widening before boxing, most-specific wins — were covered in full in the article on method parameters, return values and overloading, and none of them change in the presence of inheritance. Only the fact underneath them matters here: an overload is chosen by javac from the declared types of the arguments, and the choice is baked into the class file.
One object, two answers
Put both mechanisms on a single object and the difference stops being theoretical.
class Animal {
String speak() { return "some generic noise"; }
}
class Dog extends Animal {
@Override String speak() { return "Woof"; }
}
public class OverrideVsOverload {
// OVERLOADS: two methods, different parameter lists, one class
static String feed(Animal a) { return "feed(Animal) ran"; }
static String feed(Dog d) { return "feed(Dog) ran"; }
public static void main(String[] args) {
Dog d = new Dog();
Animal a = d; // ONE object, two static types
System.out.println("a == d -> " + (a == d));
System.out.println("a.speak() -> " + a.speak()); // override
System.out.println("d.speak() -> " + d.speak()); // override
System.out.println("feed(a) -> " + feed(a)); // overload
System.out.println("feed(d) -> " + feed(d)); // overload
}
}
a == d -> true
a.speak() -> Woof
d.speak() -> Woof
feed(a) -> feed(Animal) ran
feed(d) -> feed(Dog) ran
a == d is true, so there is exactly one object on the heap. Ask it to speak through either variable and Dog.speak() runs both times, because the override follows the object. Pass that same object to feed through either variable and different methods run, because the overload follows the declaration. The reference a is the surprising half: a.speak() reaches the Dog body while feed(a) reaches the Animal overload, on the same object in the same statement block.
The rule of thumb that falls out of this: if you want subclass-specific behaviour, override; if you overload on a superclass and a subclass, be certain every call site declares the type you meant.
The same bug in production code: equals(Dog)
The most expensive form of this mistake is writing equals with the wrong parameter type. Object.equals takes an Object, so a version taking anything narrower is an overload, and the JDK never calls it.
import java.util.List;
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
// OVERLOAD of Object.equals(Object), not an override
public boolean equals(Point p) {
return p != null && x == p.x && y == p.y;
}
}
public class EqualsTrap {
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
Object o = b;
System.out.println("a.equals(b) -> " + a.equals(b));
System.out.println("a.equals(o) -> " + a.equals(o));
System.out.println("List.of(a).contains(b) -> " + List.of(a).contains(b));
}
}
a.equals(b) -> true
a.equals(o) -> false
List.of(a).contains(b) -> false
The class compiles without a warning and behaves correctly in the one test the author writes by hand. Every collection in the JDK holds its elements as Object, calls equals(Object), and gets the inherited identity comparison instead. @Override on that method would have failed the build immediately.
Fields are not polymorphic
Field access is resolved entirely from the static type. A subclass field with the same name as a superclass field does not replace it — both exist on the same object, and the expression's declared type decides which one is read.
class Animal {
String kind = "animal";
String speak() { return "some generic noise"; }
String kindFromAnimal() { return kind; }
}
class Dog extends Animal {
String kind = "dog"; // HIDES Animal.kind, does not replace it
@Override String speak() { return "Woof"; }
String kindFromDog() { return kind; }
}
public class FieldHiding {
public static void main(String[] args) {
Dog d = new Dog();
Animal a = d; // one object, two static types
System.out.println("a == d -> " + (a == d));
System.out.println("a.speak() -> " + a.speak());
System.out.println("d.speak() -> " + d.speak());
System.out.println("a.kind -> " + a.kind);
System.out.println("d.kind -> " + d.kind);
System.out.println("((Animal) d).kind -> " + ((Animal) d).kind);
System.out.println("d.kindFromAnimal() -> " + d.kindFromAnimal());
System.out.println("d.kindFromDog() -> " + d.kindFromDog());
}
}
a == d -> true
a.speak() -> Woof
d.speak() -> Woof
a.kind -> animal
d.kind -> dog
((Animal) d).kind -> animal
d.kindFromAnimal() -> animal
d.kindFromDog() -> dog
Read the fourth and fifth lines against the second and third. One object; the method call gives Woof through both variables, and the field gives two different strings through the same two variables. Field hiding itself — the fact that a subclass may redeclare an inherited field name and that both storage slots then exist on one object — was covered in the article on inheritance. What matters here is the resolution rule: a cast changes the answer for a field and cannot change it for a method, and d.kindFromAnimal() returns animal because the code inside Animal was compiled against Animal.kind.
The bytecode makes the asymmetry explicit. The two methods below differ only in the declared type of the parameter, and both are called with the same Dog. category() is a static method, which the next section is about.
class Animal {
String kind = "animal";
String speak() { return "some generic noise"; }
static String category() { return "Animal.category()"; }
}
class Dog extends Animal {
String kind = "dog";
@Override String speak() { return "Woof"; }
static String category() { return "Dog.category()"; }
}
public class FieldBytecode {
static String viaAnimal(Animal a) { return a.kind + " " + a.speak() + " " + a.category(); }
static String viaDog(Dog d) { return d.kind + " " + d.speak() + " " + d.category(); }
public static void main(String[] args) {
Dog d = new Dog();
System.out.println("viaAnimal(d) -> " + viaAnimal(d));
System.out.println("viaDog(d) -> " + viaDog(d));
}
}
viaAnimal(d) -> animal Woof Animal.category()
viaDog(d) -> dog Woof Dog.category()
static java.lang.String viaAnimal(Animal);
1: getfield #7 // Field Animal.kind:Ljava/lang/String;
5: invokevirtual #13 // Method Animal.speak:()Ljava/lang/String;
10: invokestatic #17 // Method Animal.category:()Ljava/lang/String;
static java.lang.String viaDog(Dog);
1: getfield #24 // Field Dog.kind:Ljava/lang/String;
5: invokevirtual #27 // Method Dog.speak:()Ljava/lang/String;
10: invokestatic #28 // Method Dog.category:()Ljava/lang/String;
getfield and invokestatic name a different member in each version, and that member is what runs. invokevirtual also names a different member, but the JVM ignores that name when picking the body, which is why speak() printed Woof on both lines. Hiding a field is almost always a mistake: rename the subclass field instead.
Static methods are hidden, not overridden
A static method belongs to a class, not to an object, so there is nothing to dispatch on. Declaring one in a subclass with the same signature hides the superclass version, and every call is resolved from a type known at compile time.
class Animal {
static String category() { return "Animal.category()"; }
String speak() { return "some generic noise"; }
}
class Dog extends Animal {
static String category() { return "Dog.category()"; } // HIDES, does not override
@Override String speak() { return "Woof"; }
}
public class StaticHiding {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println("Animal.category() -> " + Animal.category());
System.out.println("Dog.category() -> " + Dog.category());
System.out.println("a.category() -> " + a.category());
System.out.println("a.speak() -> " + a.speak());
}
}
Animal.category() -> Animal.category()
Dog.category() -> Dog.category()
a.category() -> Animal.category()
a.speak() -> Woof
The third line is the one worth staring at. a refers to a Dog, and a.category() ran Animal.category(). The fourth line, on the same object in the same run, ran Dog.speak(). Calling a static method through a reference is legal but misleading, and javac says so under -Xlint:static:
StaticHiding.java:17: warning: [static] static method should be qualified by type name, Animal, instead of by an expression
System.out.println("a.category() -> " + a.category());
^
1 warning
The warning even names the type it used. Write Animal.category() or Dog.category() and the ambiguity disappears. @Override cannot rescue this either, because there is no override to annotate:
OverrideStatic.java:6: error: static methods cannot be annotated with @Override
@Override
^
1 error
Upcasting, downcasting and the instanceof guard

Upcasting assigns a subclass reference to a superclass variable. It needs no cast operator, no run-time check, and can never fail, because every Dog is an Animal by construction. Downcasting goes the other way, and is a claim about the object that the JVM verifies as the cast executes.
class Animal {
@Override public String toString() { return getClass().getSimpleName(); }
String speak() { return "some generic noise"; }
}
class Dog extends Animal {
@Override String speak() { return "Woof"; }
String fetch() { return "returns the ball"; }
}
class Cat extends Animal {
@Override String speak() { return "Meow"; }
}
public class Casting {
public static void main(String[] args) {
Animal a = new Dog(); // upcast: implicit, always safe
System.out.println("upcast Animal a = new Dog() -> a.speak() = " + a.speak());
Dog d = (Dog) a; // downcast: explicit, checked at run time
System.out.println("downcast (Dog) a -> d.fetch() = " + d.fetch());
Animal c = new Cat();
System.out.println("c instanceof Dog -> " + (c instanceof Dog));
for (Animal x : new Animal[] { new Dog(), new Cat() }) {
if (x instanceof Dog dog) { // Java 16+: binds dog only when the test passes
System.out.println("pattern " + x + " -> " + dog.fetch());
} else {
System.out.println("pattern " + x + " -> no fetch, skipped");
}
}
Dog boom = (Dog) c; // c really is a Cat
System.out.println("never reached: " + boom);
}
}
upcast Animal a = new Dog() -> a.speak() = Woof
downcast (Dog) a -> d.fetch() = returns the ball
c instanceof Dog -> false
pattern Dog -> returns the ball
pattern Cat -> no fetch, skipped
Exception in thread "main" java.lang.ClassCastException: class Cat cannot be cast to class Dog (Cat and Dog are in unnamed module of loader 'app')
at Casting.main(Casting.java:34)
The successful downcast is not a conversion. Nothing about the object changed; the cast only widened what the compiler will let you ask for, which is how d.fetch() became legal. The failing one shows what the cast really is: a run-time type check that throws ClassCastException when the claim is false.
instanceof is the guard. Since Java 16 it has a pattern form that tests and binds in one step, so the cast disappears from the source entirely:
if (x instanceof Dog dog) {
System.out.println(dog.fetch());
}
dog is in scope only where the test succeeded, which is strictly better than declaring a variable and casting into it — there is no way to use the binding on a path where the type was not checked. instanceof is also false for null, so the guard needs no separate null check. A cast between types with no subtyping relation at all is not even a run-time question; the compiler rejects it:
BadCast.java:6: error: incompatible types: Animal cannot be converted to String
String s = (String) a;
^
1 error
getClass() or instanceof?
instanceof asks whether the object is assignable to the type, so it is true for subclasses; getClass() returns the exact runtime class, so a.getClass() == Dog.class is true for a Dog and false for any subclass of Dog. Use instanceof almost always, because code that works for a type should work for its subtypes — that is the whole point of the hierarchy. Reach for getClass() only when exact identity is genuinely required, the classic case being equals, where accepting a subclass on one side and not the other would break symmetry.
Polymorphism as a design tool
The practical value of all this is a loop that does not know what it is looping over, and does not need changing when a new type arrives.
class Shape {
double area() { return 0.0; }
String describe() {
return String.format("%-10s area = %6.2f", getClass().getSimpleName(), area());
}
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
}
class Rectangle extends Shape {
private final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override double area() { return w * h; }
}
public class Shapes {
static double totalArea(Shape[] shapes) {
double total = 0;
for (Shape s : shapes) {
total += s.area(); // ONE call site, every subclass
}
return total;
}
public static void main(String[] args) {
Shape[] shapes = { new Circle(2), new Rectangle(3, 4) };
for (Shape s : shapes) {
System.out.println(s.describe());
}
System.out.printf("%-10s total = %6.2f%n", "", totalArea(shapes));
}
}
Circle area = 12.57
Rectangle area = 12.00
total = 24.57
describe() is written once in Shape and calls area(), which is overridden. totalArea contains one s.area(). Neither mentions Circle or Rectangle. Now add a type:
class Triangle extends Shape {
private final double base, height;
Triangle(double base, double height) { this.base = base; this.height = height; }
@Override double area() { return base * height / 2; }
}
Add new Triangle(6, 5) to the array and recompile. totalArea, describe() and the loop in main are untouched:
Circle area = 12.57
Rectangle area = 12.00
Triangle area = 15.00
total = 39.57
Compare that with the alternative — a chain of if (s instanceof Circle) ... else if (s instanceof Rectangle) ... — which has to be found and edited in every place it was written, for every new type, forever. A long instanceof chain over a class hierarchy is usually a missing override.
One weakness is visible in the code above: Shape.area() returns 0.0, a value that is meaningless for every real shape and that a subclass author can silently forget to replace. Java has a construct that removes both problems by declaring the method without a body and refusing to compile a subclass that does not implement it. That is the next article.
Never call an overridable method from a constructor
Object construction runs the superclass constructor first, before any subclass field initialiser or constructor body. Dynamic dispatch, however, is already fully active. If the superclass constructor calls an overridable method, the subclass override runs against a half-built object.
import java.util.ArrayList;
import java.util.List;
class Animal {
Animal() {
System.out.println("Animal() starts");
System.out.println(" describe() -> " + describe()); // overridable call
System.out.println("Animal() ends");
}
String describe() { return "an animal"; }
}
class Dog extends Animal {
private final String name;
private int legs = 4;
private final List<String> tricks = new ArrayList<>();
Dog(String name) {
// super() runs here, implicitly, BEFORE anything below
this.name = name;
System.out.println("Dog(String) ends");
}
@Override
String describe() {
return "a dog named " + name + " with " + legs + " legs, tricks = " + tricks;
}
}
public class ConstructorTrap {
public static void main(String[] args) {
Dog d = new Dog("Rex");
System.out.println();
System.out.println("after construction: " + d.describe());
}
}
Animal() starts
describe() -> a dog named null with 0 legs, tricks = null
Animal() ends
Dog(String) ends
after construction: a dog named Rex with 4 legs, tricks = []
The same method, on the same object, returned nonsense and then the truth. name is null, legs is 0 and tricks is null during the superclass constructor, because the object's memory is zeroed and none of Dog's initialisers have run yet — not even = 4 and = new ArrayList<>(). javap -c on Dog shows why: the super() call is the first thing in the constructor, and every field write is compiled in after it (the instructions past offset 22 are the println).
Dog(java.lang.String);
Code:
0: aload_0
1: invokespecial #1 // Method Animal."<init>":()V
4: aload_0
5: iconst_4
6: putfield #7 // Field legs:I
9: aload_0
10: new #13 // class java/util/ArrayList
13: dup
14: invokespecial #15 // Method java/util/ArrayList."<init>":()V
17: putfield #16 // Field tricks:Ljava/util/List;
20: aload_0
21: aload_1
22: putfield #20 // Field name:Ljava/lang/String;
Note that final gives no protection at all; name and tricks are both final and both observed empty.
When the override touches one of those fields rather than printing it, the result is a NullPointerException from a constructor:
class Animal {
Animal() { System.out.println("trick count: " + trickCount()); }
int trickCount() { return 0; }
}
class Dog extends Animal {
private final List<String> tricks = new ArrayList<>();
Dog() { tricks.add("sit"); }
@Override int trickCount() { return tricks.size(); }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because "this.tricks" is null
at Dog.trickCount(ConstructorNPE.java:12)
at Animal.<init>(ConstructorNPE.java:5)
at Dog.<init>(ConstructorNPE.java:11)
at ConstructorNPE.main(ConstructorNPE.java:17)
The stack trace is the proof: Dog.trickCount was reached from Animal.<init>, which was reached from Dog.<init>. A subclass method ran before its own constructor body.
The fix is a rule, not a workaround: a constructor may only call methods that cannot be overridden — private, static, or final ones. If a subclass genuinely needs to contribute to initialisation, take the value as a constructor parameter and pass it up with super(...), or do the work in a factory method after the object is fully built.
Common mistakes and the errors they produce
Two more declarations refuse to be overridden. A final method is closed deliberately, and the article on inheritance covers why; the diagnostic here is overridden method is final.
A private method is not inherited at all, so a subclass method of the same name is simply a new method — and without @Override there is no diagnostic:
class Animal {
private String secret() { return "animal secret"; }
String reveal() { return secret(); }
}
class Dog extends Animal {
private String secret() { return "dog secret"; } // a brand new method, not an override
}
a.reveal() -> animal secret
The full list:
| Mistake | What happens | Message |
|---|---|---|
| Changing a parameter type in the override | compiles, becomes an overload, the superclass body runs | none without @Override |
| Misspelling the method name | compiles, the new method is never called | none without @Override |
@Override on a method that overrides nothing | compile error | method does not override or implement a method from a supertype |
| Narrowing access in the override | compile error | attempting to assign weaker access privileges; was public |
| Return type that is not a subtype | compile error | return type int is not compatible with String |
| Adding a wider checked exception | compile error | overridden method does not throw IOException |
Overriding a final method | compile error | overridden method is final |
@Override on a static method | compile error | static methods cannot be annotated with @Override |
Redeclaring a private method in a subclass | compiles, no override happens | none |
| Downcasting to the wrong class | run-time exception | ClassCastException: class Cat cannot be cast to class Dog |
| Casting between unrelated types | compile error | incompatible types: Animal cannot be converted to String |
| Expecting a hidden field to be polymorphic | compiles, wrong value read | none |
Expecting a hidden static to be polymorphic | compiles, wrong method runs | -Xlint:static warning only |
equals(MyType) instead of equals(Object) | compiles, collections misbehave | none without @Override |
| Calling an overridable method from a constructor | compiles, fields observed as null or 0 | none, or a NullPointerException |
The dangerous half of that table is the rows whose message column says none. Every one of them is a case where the static type quietly answered a question you thought the dynamic type would answer, and every one of the method-related ones is caught by @Override.
FAQ
What is the difference between overriding and overloading in Java?
Overriding replaces a superclass method with one of the same signature in a subclass, and the JVM picks the body at run time from the object's actual class. Overloading declares several methods with the same name and different parameter lists, and javac picks one at compile time from the declared types of the arguments. Overriding needs inheritance; overloading does not. @Override verifies the first and has no counterpart for the second.
Why does the wrong method run when I pass a subclass?
Because the method is overloaded rather than overridden. feed(Animal) and feed(Dog) are two different methods, and the compiler chose between them using the declared type of the argument expression, not the class of the object. Assigning a Dog to an Animal variable and calling feed(a) selects feed(Animal) even though a refers to a Dog. If you want the object to decide, the behaviour must live in an overridden method on the hierarchy instead of in an overload set outside it.
Can a static method be overridden in Java?
No. A subclass may declare a static method with the same signature, which hides the superclass version, and both are resolved from a compile-time type — so a superclass-typed reference reaches the superclass version even when it points at a subclass instance. @Override on a static method is a compile error: static methods cannot be annotated with @Override. Always call static members through the class name.
Why is my field showing the superclass value?
Because fields are resolved from the static type, not the dynamic one. If a subclass declares a field with the same name as one in its superclass, both slots exist on the object, and ((Animal) d).kind reads the Animal slot while d.kind reads the Dog slot. Code inside Animal reads the Animal slot too, whatever the object turns out to be. There is no way to make a field polymorphic; expose it through an overridable getter, or do not reuse the name.
Is @Override required in Java?
No, and it produces no bytecode. It is worth writing on every override anyway, because it makes the compiler check the claim. Without it, a renamed or retyped parameter turns a working override into a silent overload, and the program keeps compiling and running with the superclass behaviour. The same annotation is what catches equals(MyType) written in place of equals(Object).
Why should a constructor not call an overridable method?
Because the superclass constructor runs before any subclass field initialiser, while dispatch is already dynamic. The subclass override therefore executes against an object whose fields are still at their default values, including final ones — null for references, 0 for numbers — which produces wrong results or a NullPointerException thrown from inside a constructor. Call only private, static or final methods during construction.
Conclusion
One sentence carries the whole subject: the static type decides what compiles, the dynamic type decides what runs. Methods are the only members that follow the object — an override with a matching signature, a covariant-or-identical return type, access that is not narrowed and no wider checked exception, reached through a single invokevirtual whose target the JVM resolves on every execution. Fields, static methods and overload selection all follow the declaration instead, which is why a hidden field, a hidden static and an overload chosen on a superclass parameter each give an answer that contradicts the method call sitting next to it.
The practical rules are short. Write @Override on every override. Never hide a field or a static method. Upcast freely, downcast behind an instanceof pattern, and prefer an overridden method to a chain of type tests. Do not call an overridable method from a constructor.
The one weak point left is Shape.area() returning 0.0 — a body that exists only because the language demanded one, and that a subclass can forget to replace. Next in this series: abstract classes and abstract methods — declaring a method with no body, forcing every subclass to supply one, and the difference between a class you can extend and a class you can instantiate.