Start learning free

LESSON 187 OF 344 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in Java: An Animal Hierarchy

Lesson Read the full explanation

TL ; DR:

Put it all together: an abstract parent class, three concrete subclasses, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


abstract class Animal {
    public String name;
    public Animal(String name) { this.name = name; }
    public abstract String sound();      // each subclass picks
    public void speak() {                 // shared by all
        System.out.println(name + ": " + sound());
    }
}
// Dog, Cat, Bird each override sound()
// Animal[] zoo = { new Dog("Rex"), ... };
// for (Animal a : zoo) a.speak();


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Inheritance — three concrete classes share a common parent.
  2. Constructors — each subclass forwards a name to the parent.
  3. Abstract methods — the parent declares sound() with no body; each child must provide one.
  4. Shared behavior — the parent has a concrete speak() that calls sound(); every subclass inherits it for free.
  5. Polymorphism — a collection typed as Animal holds a Dog, a Cat, and a Bird; speak() dispatches to the right sound() at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the abstract parent), concrete variants below, and the rest of the program written against the abstract type. New variants get added without touching the consumer code.


Your task

The abstract Animal class is already defined, with a concrete speak() that calls the abstract sound(). Add three subclasses — Dog (returns "Woof"), Cat (returns "Meow"), and Bird (returns "Tweet"). Then in MainFunction / main, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 2
    Ctrl + Enter to check

    JAVA · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in Java, explained

    What the capstone combines

    This exercise uses the main ideas of object-oriented programming in one small program: an abstract parent class, three concrete subclasses and a loop that treats them all the same way. The pieces are:

    1. Inheritance. Dog, Cat and Bird all extend Animal.
    2. Constructors. Each subclass passes the animal's name up to the parent.
    3. An abstract method. Animal declares sound() with no body, so every concrete subclass must supply one.
    4. Shared behaviour. Animal has a normal speak() method that calls sound(). Subclasses inherit it unchanged.
    5. Polymorphism. An array typed Animal[] holds all three kinds, and each call to speak() reaches the right sound().

    The parent class

    The lesson gives you Animal:

    abstract class Animal {
        public String name;
        public Animal(String name) { this.name = name; }
        public abstract String sound();
        public void speak() {
            System.out.println(name + ": " + sound());
        }
    }
    

    The abstract keyword on the class means you can't write new Animal("x"). The class exists to be extended. The abstract sound() is the part that varies, so the parent leaves it open. speak() is the part that stays the same, so the parent writes it once. Inside speak(), the call to sound() runs the version belonging to the actual object, and that is how one method produces different output for different animals.

    This split between a fixed routine and a part that subclasses fill in is a common design: a small contract at the top, concrete variants below it, and the rest of the program written against the parent type.

    Step 1: the three subclasses

    Each subclass needs two things:

    • A constructor that calls super(name). Java doesn't inherit constructors. Animal has only a constructor that takes a String, so the compiler has nothing to call for a subclass that doesn't say which parent constructor to use. Each subclass therefore declares its own constructor, and its first statement is super(name).
    • An override of sound(). The method must be public, because the parent's version is. Marking it @Override makes the compiler check that the signature really matches. You don't override speak(), since the inherited one already does the job.

    If a subclass forgets to implement sound(), it doesn't compile, unless it is itself declared abstract.

    Step 2: the polymorphic loop

    In mainFunction, build an array whose element type is Animal and put one object of each subclass in it. The order matters for the expected output: Rex, Whiskers, Tweety. Then loop with an enhanced for and call speak() on each element.

    The loop variable a has the type Animal, so the compiler only lets you use what Animal offers. At run time, Java looks at the real object each time. The first call reaches Dog.sound(), the second Cat.sound() and the third Bird.sound(). This is called dynamic dispatch.

    The solution in Java

    abstract class Animal {
        public String name;
        public Animal(String name) { this.name = name; }
        public abstract String sound();
        public void speak() {
            System.out.println(name + ": " + sound());
        }
    }
    
    class Dog extends Animal {
        public Dog(String name) { super(name); }
        @Override public String sound() { return "Woof"; }
    }
    
    class Cat extends Animal {
        public Cat(String name) { super(name); }
        @Override public String sound() { return "Meow"; }
    }
    
    class Bird extends Animal {
        public Bird(String name) { super(name); }
        @Override public String sound() { return "Tweet"; }
    }
    
    class Solution {
        public void mainFunction() {
            Animal[] animals = { new Dog("Rex"), new Cat("Whiskers"), new Bird("Tweety") };
            for (Animal a : animals) {
                a.speak();
            }
        }
    }
    

    Running mainFunction prints:

    Rex: Woof
    Whiskers: Meow
    Tweety: Tweet
    

    The array initialiser { new Dog("Rex"), ... } is shorthand that is allowed in a declaration. The array's type is Animal[] even though every element is a subclass. A Dog is an Animal, so it fits.

    Why this design pays off

    The loop doesn't mention Dog, Cat or Bird. To add a fourth animal, you write one more subclass and add one more object to the array. Animal.speak() and the loop stay as they are. If the loop had an if for each type, every new animal would mean editing it.

    Common mistakes

    • No constructor in the subclass. class Dog extends Animal { ... } with no constructor makes the compiler look for an Animal() with no arguments, which doesn't exist, and compilation fails.
    • Overriding with weaker access. Writing String sound() without public is an error, because an override can't reduce the visibility of the method it replaces.
    • Misspelling the method name. Without @Override, Sound() would be a brand new method and the class would still lack sound(). With the annotation, the compiler tells you at once.
    • Overriding speak() too. This repeats the printing in each class, which is what the shared method is there to avoid.
    • Wrong order in the array. The output follows the array, so Rex must come first, then Whiskers, then Tweety.
    • Instantiating the parent. new Animal("Rex") doesn't compile because Animal is abstract.

    Key takeaways

    • An abstract class holds what subclasses share and declares what they must provide.
    • A subclass constructor starts with super(...) when the parent has no no-argument constructor.
    • A variable of the parent type can refer to any subclass object, and method calls run the subclass's version.
    • Code written against the parent type keeps working when you add new subclasses.
    Keep going with JavaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free