1. What is a class hierarchy?
In programming (and beyond), a hierarchy is a tree-like structure where we have “general” entities at the top of the tree (base classes) and more specific ones (subclasses) below. In Java, a class hierarchy is built using inheritance: each subclass can itself be a parent for other subclasses, and so on.
Analogy:
- An animal (Animal) is a general/base class.
- A mammal (Mammal) is a more specific kind of animal.
- A dog (Dog) is a more specific kind of mammal.
- And a specific “Buddy” is an instance of the Dog class.
In code it looks like this:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
A class hierarchy lets you describe common properties and behavior “at the top,” and the details “at the bottom.” This makes code more logical and eliminates duplication.
Hierarchy diagram
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. How to build a hierarchy: logic and practice
Identify the general and the specific
The main rule: the base class should contain what is characteristic of all its descendants. Everything unique should go into subclasses.
Example:
- All animals can breathe and eat — therefore, the breathe() and eat() methods should be in the Animal class.
- Only birds can fly — therefore, the fly() method belongs in the Bird class, not in Animal.
- Only dogs can bark — the bark() method belongs in the Dog class.
Example: Animals
// Base class
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " is eating.");
}
void makeSound() {
System.out.println(name + " makes a sound.");
}
}
// Subclass: Mammal
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " feeds its young with milk.");
}
}
// Subclass: Dog
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " barks: Woof-woof!");
}
void wagTail() {
System.out.println(name + " wags its tail.");
}
}
// Subclass: Cat
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " meows: Meow!");
}
void purr() {
System.out.println(name + " purrs.");
}
}
// Subclass: Bird
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " flies.");
}
@Override
void makeSound() {
System.out.println(name + " chirps: Chirp-chirp!");
}
}
Invocation in main:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Buddy");
Cat murka = new Cat("Kitty");
Bird sparrow = new Bird("Sparrow");
sharik.eat(); // Buddy is eating.
sharik.makeSound(); // Buddy barks: Woof-woof!
sharik.feedMilk(); // Buddy feeds its young with milk.
sharik.wagTail(); // Buddy wags its tail.
murka.eat(); // Kitty is eating.
murka.makeSound(); // Kitty meows: Meow!
murka.feedMilk(); // Kitty feeds its young with milk.
murka.purr(); // Kitty purrs.
sparrow.eat(); // Sparrow is eating.
sparrow.makeSound(); // Sparrow chirps: Chirp-chirp!
sparrow.fly(); // Sparrow flies.
}
}
Visualization: class tree
| Class | Parent | Features |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. More real-world examples
Geometric shapes
Class hierarchies are great for modeling geometry.
// Base class
class Shape {
void draw() {
System.out.println("Drawing a shape.");
}
}
// Circle
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Drawing a circle with radius " + radius);
}
}
// Rectangle
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("Drawing a rectangle " + width + "x" + height);
}
}
Usage:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // Drawing a circle with radius 5.0
s2.draw(); // Drawing a rectangle 3.0x4.0
}
}
Transportation
class Vehicle {
void move() {
System.out.println("The vehicle is moving.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("The car is driving on the road.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("The cyclist is pedaling.");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // The car is driving on the road.
v2.move(); // The cyclist is pedaling.
Users
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " logged in."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " deleted user " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " made a purchase."); }
}
4. Useful tips
Don't overuse inheritance. Inheritance is a tool for “is-a” relationships. If you want to say “A Cat is an Animal,” use inheritance. If “A Cat has a Tail,” use composition (has-a).
Bad:
class Engine { /* ... */ }
class Car extends Engine { /* Is a car an engine? No, this is composition! */ }
Good:
class Car {
Engine engine; // A car contains an engine
}
Avoid hierarchies that are too deep. The more levels you have, the harder the code is to maintain and understand. Usually, 2–3 levels are the maximum for most tasks.
Avoid hierarchies that are too “flat.” If you have 20 classes all inheriting directly from one base class, you may need to revisit the architecture.
Inheritance “for convenience.” Sometimes you want to “borrow” methods or fields without thinking through the logic. That leads to chaos! If class A and class B are not logically related, do not make class B extends A just for a couple of methods.
Violating the Liskov substitution principle. If a subclass cannot be used in place of its parent without surprises, the hierarchy is designed incorrectly.
Code duplication. If you catch yourself thinking “oh, this method is being copied from class to class,” it may be time to move it to the base class.
5. Common mistakes when building class hierarchies
Error No. 1: Inheritance without an is-a rationale.
If you use inheritance only to access methods or fields, and not because the subclass truly “is” the parent, your code will quickly turn into a mess. For example, “A Car inherits Engine” is not is-a, it is has-a.
Error No. 2: Ignoring a subclass’s unique features.
If all your subclasses look the same and add nothing new, you may not need a hierarchy at all — use a single class.
Error No. 3: Duplicating code in subclasses.
If you copy the same implementation into multiple subclasses, you should move it to the parent class.
Error No. 4: An overly complex or deep hierarchy.
Multi-level hierarchies are hard to maintain and test. The simpler, the better!
Error No. 5: Overriding methods without the @Override annotation.
Without the annotation, it is easy to make a mistake in the method signature, and then the method will not be overridden but treated as new. Always use @Override!
GO TO FULL VERSION