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:
- Inheritance.
Dog,CatandBirdall extendAnimal. - Constructors. Each subclass passes the animal's name up to the parent.
- An abstract method.
Animaldeclaressound()with no body, so every concrete subclass must supply one. - Shared behaviour.
Animalhas a normalspeak()method that callssound(). Subclasses inherit it unchanged. - Polymorphism. An array typed
Animal[]holds all three kinds, and each call tospeak()reaches the rightsound().
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.Animalhas only a constructor that takes aString, 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 issuper(name). - An override of
sound(). The method must bepublic, because the parent's version is. Marking it@Overridemakes the compiler check that the signature really matches. You don't overridespeak(), 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 anAnimal()with no arguments, which doesn't exist, and compilation fails. - Overriding with weaker access. Writing
String sound()withoutpublicis 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 lacksound(). 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 becauseAnimalis 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.