Command Palette

Search for a command to run...

[Java Basics] Polymorphism in Java: Overriding vs Overloading

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.

One call expression, three method bodies, three different lines of output

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:

QuestionAnswered whenAnswered from
Does this member exist, and does the call type-check?compile timethe static type
Which method body actually executes?run timethe 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 declarationWhat the override may do
Name and parameter typesmust match exactly — anything else is a new method
Return typethe same type, or a subtype of it (covariant)
Access modifierthe same, or wider; never narrower
Checked exceptionsthe 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 @Override on 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

The compile-time half is checked against the static type and emits one invokevirtual; the run-time half picks the body from the dynamic type

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. Dog does not appear anywhere in this method, even though Dog.speak() is what ran for the first call.
  • Since the same instruction produced Woof and then some generic noise, the target cannot have been fixed at compile time. invokevirtual is 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

Overriding resolved at run time from the dynamic type, against overloading resolved at compile time from the static type, on one object

The two words describe unrelated mechanisms that happen to share the idea of one name serving several bodies.

OverridingOverloading
Signaturessame name, same parameter listsame name, different parameter lists
Where the methods livesuperclass and subclassanywhere, usually one class
Requires inheritanceyesno
Resolvedat run timeat compile time
Decided fromthe dynamic type of the receiverthe static types of the arguments
Return typemust be the same or covariantnot consulted at all
Can it be verified@Overridenothing 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

Upcast implicit and always safe, downcast explicit and checked at run time, with the instanceof pattern 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 overriddenprivate, 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:

MistakeWhat happensMessage
Changing a parameter type in the overridecompiles, becomes an overload, the superclass body runsnone without @Override
Misspelling the method namecompiles, the new method is never callednone without @Override
@Override on a method that overrides nothingcompile errormethod does not override or implement a method from a supertype
Narrowing access in the overridecompile errorattempting to assign weaker access privileges; was public
Return type that is not a subtypecompile errorreturn type int is not compatible with String
Adding a wider checked exceptioncompile erroroverridden method does not throw IOException
Overriding a final methodcompile erroroverridden method is final
@Override on a static methodcompile errorstatic methods cannot be annotated with @Override
Redeclaring a private method in a subclasscompiles, no override happensnone
Downcasting to the wrong classrun-time exceptionClassCastException: class Cat cannot be cast to class Dog
Casting between unrelated typescompile errorincompatible types: Animal cannot be converted to String
Expecting a hidden field to be polymorphiccompiles, wrong value readnone
Expecting a hidden static to be polymorphiccompiles, wrong method runs-Xlint:static warning only
equals(MyType) instead of equals(Object)compiles, collections misbehavenone without @Override
Calling an overridable method from a constructorcompiles, fields observed as null or 0none, 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.

Related Posts

[Java Basics] Loops in Java: for, while and do-while

Loops in Java explained by running them: the exact execution order of a for header, while vs do-while, the enhanced for and why it cannot write back, off-by-one errors against length, and the three ways to write an infinite loop.

[Java Basics] Methods in Java: Declaring and Calling Them

How to declare and call a method in Java: the parts of a declaration, static versus instance methods and the non-static method cannot be referenced from a static context error, the return statement, the call stack and StackOverflowError, reading a stack trace, Javadoc, and the real compiler errors beginners hit.

[Java Basics] Hello World in Java: The Anatomy of a .java File

A token-by-token breakdown of the Java Hello World program: the structure of a .java file, public static void main(String[] args), the filename rule, System.out.println and printf, escape sequences, text blocks and the first compile errors you will hit.

[Java Basics] Custom Exceptions in Java: Writing Your Own Exception Types

How to write a custom exception class in Java: extends Exception versus extends RuntimeException and what each really costs the caller, the four Throwable constructors, carrying structured data as fields, chaining a cause and reading the Caused by section and its ... N more line, exception translation at an API boundary, and when IllegalArgumentException already says everything you wanted to say.