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!
What is @Override?
When overriding a method we can optionally use the @Override annotation. This tells readers of the code and the compiler, that the following method is overrides a parent’s method. Which is useful for code readability and better compiler error messages if a method is not correctly overridden.
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.
Accessing the parent's implementation of an overridden method
In Java, if the child class overrides a method of the parent class, the parent method will no longer be accessible via an instance of the child, unless the child explicitly defines a new public method to call the parent’s class method.
public class Main { public static void main(String[] args) { Child child = new Child(); System.out.println(child.parentWho()); // "I am the parent!" }}class Parent { public String who() { return "I am the parent!"; } }class Child extends Parent { @Override public String who() { return "I am the child!"; } // wrapper to call parent's implementation public String parentWho() { return super.who(); }}
stdout:
I am the parent!
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"; }}
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.