Introduction
When discussing the concepts of class and object in a previous article, we mentioned that objects of different classes can be interconnected through relationships. In object-oriented design, the meaning of relationships can extend beyond objects and involve the classes whereby all those objects are created. This occurs, for example, in scenarios where a general category (superclass or parent) has a relationship with a more specific type (subclass or child), termed generalisation or "is-a-kind-of" relationship.
Consider the example of generalisation where a general kind, Mammal, can relate with a more specific kind, such as a Dog, Cat, etc. Therefore, a Dog (or a Cat) "is-a-kind-of" Mammal. Let's assume a Dog class defines the attributes eyeColour
, hairColour
and barkFrequency
, and the behaviours getEyeColour()
, getHairColour()
and bark()
. Meanwhile, a Cat class defines the attributes eyeColour
, hairColour
and meowFrequency
, and the behaviours getEyeColour()
, getHairColour()
and meow()
. The following UML Class Diagram shows those three classes with their attributes, methods (behaviours) and relationships:
The diagram indicates the generalisation relationship via an arrow with an empty connector from the subclass to the superclass. However, even though we stated that a generalisation relationship exists between those classes, at the moment, the Mammal class does not define any attributes or methods. On the other hand, some common (duplicated) attributes and behaviours are defined in both subclasses (represented in bold).
We will now see how generalisation and inheritance can be used to improve the code organisation of these classes.
Designing Classes through Generalisation and Inheritance
Inheritance is an object-oriented principle manifested when two or more classes are related through generalisation. Specifically, common attributes and behaviours can be moved from the child classes (Dog and Cat in this case) to the parent class (Mammal in this case), facilitating code reuse among subclasses, as shown in the class diagram below:
In the diagram, the Mammal superclass now contains attributes (eyeColour
, hairColour
) and behaviours (getEyeColour()
, getHairColour
) common to the Dog and Cat subclasses.
The subclasses inherit these common features from the superclass and do not need to be defined again—they are described once in the Mammal superclass and can be reused by all the subclasses.
On the other hand, the subclasses can define specific attributes and behaviours pertaining solely to them, i.e., the barkFrequency
and bark()
for the Dog subclass and the meowFrequency
and meow()
for the Cat subclass.
Inheritance in Practice: A Code Example
Following the design in the previous class diagram, the Mammal class could be defined via the code below:
public class Mammal
{
private String eyeColour;
private String hairColour;
public Mammal(string ec, string hc)
{
eyeColour = ec;
hairColour = hc;
}
public String getEyeColour()
{
return eyeColour;
}
public String getHairColour()
{
return hairColour;
}
}
Whereas the Dog class could be defined as follows:
public class Dog extends Mammal
{
int barkFrequency;
public Dog(string ec, string hc, int bf)
{
// attributes initialisation
}
public void bark()
{
// uses barkFrequency
}
// inherits getEyeColour and getHairColour from Mammal
// no need to define them again here
}
In Java, the use of extends
after the class name (Dog), followed by the superclass name (Mammal), indicates a generalisation relationship between those two classes. A similar concept exists in C#, but the symbol :
is used instead of extends
.
It is important to note that inheritance allows for code reuse, i.e., the code of getEyeColour
and getHairColour
does not need to be redefined in the Dog (Cat, or any) subclass. Moreover, any changes to these methods' code in the Mammal superclass will inherently be reflected in all subclasses.
In the previous Dog class definition, we deliberately did not provide the code within the constructor. The reason is that although a subclass inherits attributes and methods from a superclass, it still does not have direct access to private
members and constructors of its superclass. Therefore, in the above code, the Dog constructor cannot access the eyeColour
and hairColour
attributes directly—how can the attributes initialisation be done?
Calling a Superclass Constructor: The super
keyword
A constructor can be chained with another constructor of the same class using this
. Similarly, a subclass constructor can call a superclass constructor and initialise private attributes of the superclass using the super
keyword:
public class Dog extends Mammal
{
int barkFrequency;
public Dog(String ec, String hc, int bf)
{
super(ec, hc);
barkFrequency = bf;
}
// ...
}
This allows the Dog constructor to invoke the Mammal constructor:
public Mammal(String ec, String hc)
{
eyeColour = ec;
hairColour = hc;
}
And initialise the private
Mammal attributes eyeColour
and hairColour
, which can then be accessed by the Dog class through the inherited public
methods getEyeColour
and getHairColour
.
It should be noted that a similar mechanism for invoking a superclass’ constructor from a subclass is available in C#, but the base
keyword must be used in place of super
.
Maximise Code Reuse: Defining Multiple Subclasses
Similarly, the Cat class can be defined using generalisation and inheritance via the following code:
public class Cat extends Mammal
{
int meowFrequency;
public Cat(String ec, String hc, int mf)
{
super(ec, hc);
meowFrequency = mf;
}
public void meow()
{
// uses meowFrequency
}
// inherits getEyeColour and getHairColour from Mammal
// no need to define them again here
}
As seen earlier, attributes and behaviours common for the subclasses are defined only once in the Mammal class and are inherited by the Dog and Cat subclasses. This leads to the advantage of reusing the same code and avoiding duplication. Moreover, if additional classes were to be defined for other mammals, such as Rabbit, Racoon, etc., they could all extend the Mammal class and reuse the code of getEyeColour
and getHairColour
through inheritance.
Conclusions
Let's conclude this article by discussing the usage of the Dog, Cat, and Mammal classes in the provided Program
class example:
public class Program
{
public static void main(String[] args)
{
// Create a Dog called Alan
Dog alan = new Dog("Brown", "Short", 3);
String alanEyeColour = alan.getEyeColour();
alan.bark();
// Create a Cat called Felix
Cat felix = new Cat("Green", "Spotted", 5);
string felixHairColour = felix.getHairColour();
felix.meow();
}
}
In the main
method of the above Program
class, we see the creation of two distinct objects—a Dog named alan
and a Cat named felix
—which both inherit characteristics from the common Mammal
superclass.
Specifically, because of the existing generalisation relationship between the associated classes, both the Dog
and Cat
objects inherit common methods from the Mammal
superclass, such as getEyeColour()
and getHairColour()
. This allows for code reuse by sharing common functionalities, as the alan
and felix
objects can call these methods directly. By defining methods like getEyeColour()
and getHairColour()
in the Mammal
superclass, we avoid duplicating these methods in the Dog
and Cat
subclasses. This makes the code better organised and easier to maintain.
Finally, the program demonstrates how specific behaviours can be defined for each subclass while benefiting from the above-mentioned shared characteristics. For instance, after creating the Dog
and Cat
objects, we can call alan.bark()
to trigger Alan's barking behaviour, and felix.meow()
to make Felix meow. These methods are specific to the Dog
and Cat
classes, respectively, and highlight how subclass-specific functionalities can coexist with inherited methods.