• Class inheritance creates an “is-a” relationship between Classes
  • Each Class can only extend 1 other class
  • All Classes automatically inherit from the Object Class
  • Both fields and methods are inherited by Sub-Classes
  • Methods can be overridden, fields are hidden instead
    • When overridden, the parent’s method becomes inaccessible directly.
    • Fields are hidden instead, making them dependent on the reference type.

Purpose:

Classes represent “things”, like people, animals, concepts, etc… These “things” have:

  • Attributes/Properties (fields) - what they have
  • Behaviours (methods) - what they can do

When we extend a parent class, we’re saying:
“This new class is the same type of thing as the parent, but with some differences.”

For example: A Dog extends Animal because a dog is an animal, but has some dog-specific behaviours.

Inheritance of Classes helps to reduce code duplication as sub-classes can rely on the parent’s implementation. It also promotes polymorphism and extensibility as you can treat different objects which share the same parent in the same generic way.

public class Main {
	public static final List<Employee> employees = Arrays.asList(new Employee(/* smth */), new Manager(/* smth */));
	public static void main(String[] args) {
		// Get all Employee names
		for (Employee emp: employees) {
			System.out.println(emp.getName());
		}
	}
}
 
public class Employee {
	// Implmentations for Employee
	//...
}
 
public class Manager extends Employee {
	// Implmenetations for Manager
	// Reuse methods or fields already defined in Employee
	//...
}

Method Overriding

public class Main {
	public static void main(String[] args) {
		Parent childDisguisedAsParent = new Child();
		System.out.println(childDisguisedAsParent.who()); // "I am the child!"
	}
}
 
class Parent {  
    public String who() {  
        return "I am the parent!";
    }  
}
 
class Child extends Parent {  
	@Override
    public String who() {  
        return "I am the child!";
    }  
}

stdout:

I am the child!

Despite being of type Parent, the underlying object is of type Child which overrides the who() method! Thus in the above example, calling who() used the implementation defined by Child!

Methods in Java are polymorphic! Meaning, the implementation of the method depends on its instance type. During runtime, the JVM performs dynamic dispatch to find the method implementation of the instance type referenced.

Field Hiding

public class Main {
	public static void main(String[] args) {
		// Reference Type Parent
		System.out.println(((Parent)new GrandChild()).name); // Robert
		// Reference Type Child
		System.out.println(((Child)new GrandChild()).name); // Billy
		// Reference Type GrandChild
		System.out.println(new GrandChild().name); // Tung
	}
}
 
class Parent {  
    public String name = "Robert";
}
 
class Child extends Parent {  
	public String name = "Billy";
}
 
class GrandChild extends Child {
	public String name = "Tung";
}

stdout:

Robert
Billy
Tung

Fields on the other hand are not polymorphic! They are resolved at compile time based on the reference type of the object reference.
So if the object reference is of type Parent, it will return Parent’s field variables.
If it is of type Child, it returns Child’s field variables.\

Abstract Classes and Constructors

Abstract classes are used when you want to define a parent class to group sub-classes but, a pure instance of this class doesn’t make sense.
For example, I want to define Classes for the following characters/mobs in my game:

  • Villager (friendly)
  • Talking Statue (friendly)
  • Zombie (hostile)
  • Skeleton (hostile)

I will want to group these mobs under a parent class called NPC to define some common traits (fields) and behaviours (health).
However, an instance of just the NPC class shouldn’t exist as this class is just a template or blueprint that defines the common structure and behaviour that all NPCs should have, but doesn’t represent any specific, real NPC that could actually exist in the game.
Think about it, what would a generic NPC look like in the game? It doesn’t make sense. Every NPC needs to be either friendly or hostile, have specific behaviours, and belong to a concrete category. The NPC class exists purely to organise and share common code. An instance of it shouldn’t exist.
This is when Abstract Classes are used! They help us declare non-instantiatable Classes and allow us to define abstract methods within them that force subclasses to implement.

abstract class NPC {
	protected int health;
	
	public NPC(int health) {
		this.health = health;
	}
 
	public int getHealth() {
		return health;
	}
	
	public abstract void takeDamage(int dmg);
}
 
abstract class PeacefulNPC extends NPC {
	// All peaceful mobs have a name
	protected final String name;
	
	public PeacefulNPC(int health, String name) {
		super(health);
		this.name = name;
	}
	
	@Override
	public void takeDamage(int dmg) {
		// All peaceful mobs take half damage
		health -= dmg/2;
	}
	
	public abstract String getGreeting();
}
 
class Villager extends PeacefulNPC {
	public Villager(String name) {
		// All Villagers have 100 hp
		super(100, name);
	}
	
	@Override
	public String getGreeting() {
		return "Hello my name is " + name;
	}
}
 
class TalkingStatue extends PeacefulNPC {
	public TalkingStatue(String name) {
		super(1, name);
	}
	
	@Override
	public String greet() {
		return "Greetings traveler, I am " + name + " the immortal";
	}
	
	@Override
	public void takeDamage(int _dmg){
		// Statues are immortal so do nothing.
	}
}
 
abstract class HostileNPC extends NPC {
	protected final int attackDmg;
	protected int level;
	
	public HostileNPC(int health, int attackDmg, int level) {
		super(health);
		this.attackDmg = attackDmg;
		this.level = level;
	}
 
	// All hostile mobs can be leveled up
	public void levelUp() {
		level++;
	}
 
	// All hostile mobs scale their attack based off their level
	public int getAttackDamage() {
		return attackDmg * level;
	}
	
	public abstract String getAttackingSound();
}
 
class Zombie extends HostileNPC {
	public Zombie(int level) {
		// Zombies have fixed base health and damage
		super(20, 10, level);
	}
	
	// Simplier constructor
	public Zombie() {
		this(1);
		// DO NOT USE super(...) here. The other constructor already calls super.
		// These constructor chaining keywords are mutally exclusive
	}
	
	@Override
	public void takeDamage(int dmg){
		// Damage is reduced by its level
		health -= dmg/level;
	}
	
	@Override
	public String getAttackingSound() {
		return "Growlll";
	}
}
 
class Skeleton extends HostileNPC {
	public Skeleton(int level) {
		super(10, 5, level);
	}
	
	public Skeleton() {
		this(1)
	}
	
	@Override
	public void takeDamage(int dmg){
		// Damage is reduced by its level
		health -= dmg/level;
		// But skeletons take bonus damage based on its level
		health -= level;
	}
	
	@Override
	public String getAttackingSound() {
		return "ARRRGGGg";
	}
}

Representation of the Class Hierarchy

	 	NPC (abstract)
            /              \
    PeacefulNPC          HostileNPC
     (abstract)           (abstract)
     /        \           /         \
 Villager  TalkingStatue  Zombie  Skeleton
 (concrete) (concrete)   (concrete) (concrete)

Benefits of Abstract Class

Prevents Meaningless Instantiation: You can’t create a generic NPC object, which makes sense because every NPC should be a specific type with concrete behaviours.

Enforces Implementation: Abstract methods like takeDamage() force subclasses to provide their own implementation, ensuring no critical behaviours are forgotten.

Partial Implementation: Unlike interfaces, abstract classes can provide some concrete methods (like getHealth()) while requiring others to be implemented by subclasses.

Design by Contract: Abstract classes establish what subclasses must be able to do without dictating exactly how they do it, creating a clear contract for developers.

Benefits of Inheritance

Code Reusability: Common fields and methods are defined once in the parent class and automatically inherited by all subclasses. Your NPC class provides health and getHealth() to all NPCs without repetition.

Combines Structure with Flexibility: Provides shared foundation while allowing customisation where needed.
PeacefulNPC gives default damage handling that TalkingStatue can override for immortality.

Polymorphism: You can treat objects of different subclasses uniformly through their common parent type. All NPCs can be stored in an NPC[] array and have takeDamage() called on them, even though each implements it differently.

Maintainability: Changes to shared behaviour only need to be made in one place. If you want to add a new field to all NPCs, you only modify the NPC class.