Inheritance and Polymorphism
You have seen this video earlier, but we ignored the inheritance and polymorphism sections of it.
Watch them now (starting from 3 minutes into the video – but you might want to watch again from the start to revise the earlier concepts!)
First, a quick recap of classes and OOP
Why do we need classes and OOP?
First of all, how annoying would it be to represent cartoon animals without classes (at all!!)?
There are so many problems with the code above that we aren’t even going to bother tweaking it!
Say hello to OOP classes
By making a new class of type Animal
, we can do something as follows:
Isn’t that a lot more elegant?
Get your hands dirty!
Before we continue with introducing inheritance, edit the above Repl:
- First of all, this code is not gender inclusive!
boolean
is not a suitable type for gender. So instead, change the code as follows:- Inside the
Animal
class, define the followingenum
type:public enum Gender {FEMALE, MALE, NON_BINARY}
- Remove the use of the
boolean isFemale
everywhere, and replace it withGender gender
. - In the
main()
method, assign values for the gender. You will write the values asAnimal.Gender.FEMALE
etc instead oftrue
/false
. - Print the
gender
in theintroduce()
method, or use itsgender
to determine the corresponding title (e.g.,"Mr"
,"Ms"
, or"Mx"
). Using aswitch
case statement is a nice way to go about it.
- Inside the
- Now implement the
walk()
method in theAnimal
class, with a series ofif-else
statements (or aswitch
case statement) to check thetype
:if (type.equals("Rabbit")) { System.out.println("I'm a Rabbit, so I hop on my 2 legs!"); } else if (type.equals("Cat")) { System.out.println("I'm a Cat, so I stroll along!"); } else { System.out.println("Don't know how a " + type + " walks!"); }
- Wow – that code above looks bad!
- Call the
walk()
method on theAnimal
instances we have in themain()
method. - Mistype one of the animal types (e.g. make
lola
aRabitt
). The compiler didn’t seem to mind! - We can begin to appreciate that some
Animal
s have the same functionality (e.g. the way theyintroduce()
themselves), but that we might also want a more elegant way to differentiate between different types ofAnimal
s.
Inheritance
Say hello to OOP inheritance
In the recap above, the Get your hands dirty! exercise should have made you appreciate that the Animal
class is not perfect.
Inheritance is a way to address this.
In its simplest form, inheritance allows us to extend an existing class.
This will allow us to reuse the logic in the other class (i.e. we inherit from it). The Cat
and Rabbit
classes will each extend the Animal
class, and inherit the non-private
methods and non-private
fields.
The class we inherit from is commonly called one of the following: parent class, super class, or base class. This is really just different names meaning the same thing. The Animal
class above is an example of a super-class.
The extending class is commonly called one of the following: child class, sub class, or derived class. Again, different names meaning the same thing. The Cat
and Rabbit
classes above are examples of sub-classes.
We think of inheritance as defining an “is a” relationship:
- A
Rabbit
“is a”Animal
. - A
Cat
“is a”Animal
. - A
Student
“is a”Person
. - A
Teacher
“is a”Person
.
Understanding the super
keyword
Before we have a look at example code, let’s first talk about a new keyword that is often used with inheritance: super
.
As we said above, the “super” class (also known as parent or base class) is the class that we extend.
So, the super
keyword simply means “the class we are extending”. We can use this keyword while inside a sub class (also known as child or derived class).
Another way to think of super
is to relate it to what we already understand regarding this
:
- We can think of
this
as being synonymous with “me” or “my”. - We can therefore think of
super
as being synonymous with “my parent”.
Now, back to our example! We can implement it with the following code:
Notice the following improvements compared to when we only had the Animal
class:
- We no longer need a
type
variable – the class itself (i.e.Cat
andRabbit
) is the type! - We no longer need to specify the number of legs (in the
main()
method when constructing the instances). ACat
will have 2 legs and aRabbit
will have 4 legs.
Notice also how the Cat
and Rabbit
classes are pretty much empty! All they have is a constructor that calls what appears to be a super()
method. This is actually calling the Animal
constructor! If we said super
is synonymous with “my parent”, then super()
means “call my parent’s constructor”. This is important, because:
- In constructing a
Cat
(child class), we need to first construct anAnimal
(parent class). - In constructing a
Rabbit
(child class), we need to first construct anAnimal
(parent class).
Default (super) constructors
Remember how the compiler will automatically insert a default constructor for a class you have defined (assuming you don’t provide another constructor)?
Well, the compiler also automatically tries to call the default super constructor if you don’t call a super constructor explicitly yourself! The default call to a super constructor is to call the zero-parameter super constructor (i.e. super()
). In other words, the immediate parent must have a zero-parameter constructor for this to work!
Let’s visualise the code!
Following the same conventions of the “blueprint” analogy we previously used to visualise classes, here’s how we can visualise classes with inheritance. In particular, we visualise the code snippet shown just now.
The parent class (Animal
)
First, let’s look at our base/parent class:
There is nothing unusual with this Animal
class. But let’s just mention a couple of things:
- We have added icons to denote what fields and methods are declared as:
-
private
, or -
public
.
-
- We can also visualise what an
enum
declaration might look like. We notice this particularenum
is:- named
Gender
(which names the type), pubilc
, and- names three possible values (
FEMALE
,MALE
, andNON_BINARY
).
- named
The child classes (Cat
and Rabbit
)
Let’s now look at the children classes (Cat
and Rabbit
) that extend Animal
:
The visualisation gives us an appreciation for what it means to say “Cat
extends Animal
“:
- The
Cat
blueprint quite literally extends theAnimal
blueprint! - There is nothing new added in the
Cat
(orRabbit
) blueprint sections, other than their respective constructors!- All the other logic is in the
Animal
parent blueprint.
- All the other logic is in the
But the real question is, why are some of the sections from the Animal
class covered up?! Well, these were declared as private
in the Animal
class. As such, they will not be inherited to the children classes and may only be accessed from within the Animal
class! Only what’s declared as public
(and soon we will see protected
) will be accessible by the children classes.
Get your hands dirty!
Let’s understand this a little more:
- Add print statements in all three constructors (
Animal
,Cat
, andRabbit
) to make it clear what constructor is getting called and when. Then run your program. If you do this correctly, you should see the super-class constructor getting called before the sub-class constructor!- Hint: In your sub-class constructors, you must keep the
super()
call as the first statement otherwise the code won’t compile.
- Hint: In your sub-class constructors, you must keep the
- We will continue improving this code below…
Overriding inherited methods
I don’t know about you, but I’m really not happy with this method in the Animal
class:
public void walk() {
if (getClass() == Rabbit.class ) {
System.out.println("I'm a Rabbit, so I hop on my 2 legs!");
} else if (getClass() == Cat.class) {
System.out.println("I'm a Cat, so I stroll along!");
} else {
System.out.println("Don't know how a " + getClass().getName() + " walks!");
}
}
What happens if we want to add more sub-classes later on? It’s going to look pretty ugly, especially if we need to do a lot of things inside each block!
To fix this, each sub-class will override the walk()
method:
Get your hands dirty!
Edit the same code above:
- Remove all of the content from the
Animal
’swalk()
method, but keep one of the print statements that says “Don’t know how a … walks!”. - Copy-paste the
walk()
method signature fromAnimal
into the other sub-classes, and then include only the relevant print statements depending on what sub-class you are in. Notice how now we have customised thewalk()
methods for each respective sub-class, and it’s looking a lot cleaner! - It is a good practice to explicitly state that the
walk()
method is overriding, by using the@Override
annotation. - Add another sub-class (e.g.
Dog.java
), and add the relevant constructor for that sub-type and also override thewalk()
method for it. Then create an instance of that sub-class, and check it prints what you expect. - What happens if you create an instance of an
Animal
in themain()
method? Does it make sense to be able to create a somewhat abstract animal type? Probably not! Should we prohibit this from happening? Probably! So, later on we’ll introduce the idea of anabstract class
!
If you make the changes above, this is what we will ultimately have:
Both Cat
and Rabbit
have decided to @Override
the public void walk()
instance method that is defined in the Animal
class. This doesn’t mean that the walk()
method in Animal
ceases to exist, it just means there is a “revised version” of this method specifically catered for Cat
(or Rabbit
) instances respectively.
In fact, we can still see that there is a public void walk()
declared at the Animal
class – it is only its implementation that has been revised.
Protected vs private vs public
It’s not only methods that we inherit from super classes—we also inherit fields!
Actually, strictly speaking, we don’t “inherit” them. Rather, it would be more accurate to say we “inherit access to them”.
However, to inherit access to fields and methods from the super-class, the super-class must allow them to be inherited in the first place!
private
fields and methods will not be accessible to sub-classes (or any other class for that matter),protected
fields and methods will be accessible to sub-classes (regardless of what package they are in), or classes part of the same package (but not other classes that are not sub-classes or part of the same package), andpublic
fields and methods will be accessible to sub-classes (and also other classes that are not sub-classes).
Get your hands dirty!
Continue editing the same code above:
- In the sub-classes (e.g. the overridden
walk()
method), try to print out the animal’sname
. This is currently declared asprivate
in theAnimal
class, so access is not inherited by theCat
(orRabbit
) sub-class. - Change the modifier of the instance fields inside the
Animal
class toprotected
instead ofprivate
. - You should now be able to access these instance fields in the sub-classes and use them.
- Keep the
yearNow
static field asprivate
, as it’s not going to be needed for the sub-classes as it’s only something that theAnimal
class uses to figure out the animal’s age.
If you make the changes above, this is what we will ultimately have:
In the Cat
(or Rabbit
) class, we are now able to access any fields or methods declared as protected
in the Animal
class. The yearNow
field we decided to keep as private
, so it cannot be accessed from the children classes.
So, what’s the difference of protected
versus public
?
- Both are inherited by the children classes, and accessible to the children classes. No differences in this regard from the point of view of the child class.
- The difference comes outside the parent-child class hierarchy:
- With
protected
, these fields and methods cannot be accessed outside the hierarchy (or outside the package). They are effectively “private” for any class that is not a child class of theAnimal
class (or for any class not part of the same package if its not a child class), and effectively “public” for any class that is a child (or grandchild) class of theAnimal
class (or part of the same package). - With
public
, these fields and methods can be accessed outside the hierarchy (and outside the same package). They are effectively “public” for any class (regardless of whether it is part of the class hierarchy or same package).
- With
Calling the parent from the child class
Calling a method that is overriden will automatically execute the implementation closest to the child (derived/sub) class.
What happens if we do not want to completely override the parent’s implementation? Or maybe the child class wants to call the parent’s version of an overridden method—and not the child’s version?
To illustrate this, let’s update how animals introduce()
themselves:
- Before animals introduce themselves, they will first say hello in their unique language:
- A
Cat
will say “Meow”, and - A
Rabbit
will say “Prrrr”.
- A
- We still want the sub-classes to introduce themselves in the generic manner afterwards (“Hello …” etc), and ideally we still want to inherit this generic functionality from the
Animal
class (i.e. without copy-pasting it for each animal sub-class). At the same time, we still want to allow each animal sub-class to “add their own unique touch”. - No surprise, we achieve this with the
super
keyword again! We want to say “I want to call my parent’s version of introduce”:
public class Cat extends Animal {
...
@Override
public void introduce() {
System.out.print("Meow..."); // Cat's own unique touch
super.introduce(); // Now call "my parent's" version of introduce()
}
}
Get your hands dirty!
Continue editing the same code above:
@Override
theintroduce()
methods for the sub-classes, while also reusing theAnimal
’sintroduce()
method (as in the example above). Do this for all your sub-classes.- Define a
public String getTitledName()
instance method in theAnimal
class:- Use a
switch
statement on thegender
, returning the animal’s name with the title:Gender.FEMALE
: returns"Ms <name>"
Gender.MALE
: returns"Mr <name>"
Gender.NON_BINARY
: returns"Mx <name>"
- Use a
@Override
thepublic String getTitledName()
method for theCat
class:- Regardless of gender, cats demand respect. So, return
"Royal <name>"
.
- Regardless of gender, cats demand respect. So, return
- Make use of the
getTitledName()
method as part of theAnimal
’sintroduce()
method.- Notice how we execute the version that is closest to the child class.
If you make the changes above, this is what we will ultimately have:
- There is a public
getTitledName()
method defined in theAnimal
class, so the children will inherit this functionality.- The
Cat
class is not happy with that implementation, and so it overrides it with its own definition. - The
Rabbit
class is happy with the existing implementation provided in theAnimal
class, and therefore does not override it.
- The
- Both the
Cat
andRabbit
classes override theintroduce()
method, but they still also make use of the originalintroduce()
logic defined in theAnimal
class. - Both the
Cat
andRabbit
classes override thewalk()
method, and completely disregard the originalwalk()
logic defined in theAnimal
class.
Inheritance calls summary
Here’s an overview of which method gets called in the context of inheritance. What’s important to note, is that the (overriding) method implementation closest to the derived class gets called, regardless of whether we are in the parent or child class. The fact that the method has been overridden (by a child class) implies that its version should be used – otherwise the parent’s version shouldn’t have been overridden!
One Object
to rule them all…
Now that we understand inheritance, we can fully comprehend what the Object
class is all about!
If you declare a class, but it doesn’t extend any other class, you are implicitly extending Object
! This allows you to use the implementations of toString()
, equals()
, hashCode()
etc inherited from the Object
class!
If you declare a class, and you extend some other class, then you still (indirectly) extend Object
as that class will be extend Object
(or one of its super classes will).
In other words, every class type in Java extends Object
one way or another! Every class “is a” Object
.
Get your hands dirty!
Continue editing the same code above:
@Override
the toString()
of some of our classes.
- First override it in the
Animal
class, then see what the output is when you print the animal instance. - Then override the
toString()
method in theCat
class (notRabbit
). Observe the output from both cats and rabbits. - Comment out the
toString()
in theAnimal
class. Observe the output from both cats and rabbits.
Abstract classes
So, we felt it was wrong to be able to create an instance of an Animal
type from the main()
method:
// Some weird animal with 100 legs!??!
Animal whatKindAmI = new Animal("Weird Animal", 1990, 100, Animal.Gender.MALE);
whatKindAmI.walk();
We say it feels wrong because Animal
represents a somewhat abstract type. Besides, we have no meaningful implementation for the walk()
method of an abstract animal!
We can therefore decide to declare Animal
as an abstract class
!
Get your hands dirty!
Continue editing the same code above:
- Make the
Animal
classabstract
. - The compiler will now prohibit you from creating an instance of
Animal
! Nice. - What happens if you comment out the
walk()
method from theCat
class? How will cats walk? Looks like it will use the originalwalk()
method defined in theAnimal
class. Does this make sense? Maybe we should force all sub-classes ofAnimal
to define awalk()
method! - In the
Animal
class, remove the body of thewalk()
method and replace it with a semi-colon and add theabstract
keyword, i.e:public abstract class Animal { ... public abstract void walk(); }
- Since the
Animal
class isabstract
, it’s allowed to have such “empty” methods (but these empty methods need to be declared asabstract
also, just like the class is declared asabstract
). TheAnimal
class is essentially saying “I’m too abstract to know how an animal walks, so I will rely on sub-classes to define this particular functionality!” - What it means is, we must provide a
walk()
implementation of theCat
class (so bring back thewalk()
method you commented out above). If you don’t, then you must make theCat
anabstract class
also! And if it’s abstract, then you cannot create an instance of it!
If you make the changes above, this is what we will ultimately have for the (now abstract
) Animal
class:
From the above visualisation, we can appreciate why it’s not possible to create an instance of an Animal
. If we did, then what would be the logic for the abstract void walk()
method? The method signature is declared, but there is absolutely no body for the method! Not even a pair of {}
!
Even though we are no longer allowed to create instances of Animal
, we are still allowed to extend the Animal
class:
- The
Animal
class remains anabstract
class. - The extending child classes (
Cat
andRabbit
) are not abstract, because they provide an implementation for all ofAnimal
’sabstract
methods (namelywalk()
). - Even though we can no longer create just a “pure”
Animal
instance, we still end up creating anAnimal
instance in the process of creating aCat
instance. - If a class (e.g.
Insect
) were to extend theAnimal
class, and not provide an implementation for thewalk()
method, then thatInsect
class will remain anabstract
class.
Generalisation and specialisation
Inheritance allows for both generalisation (“up the hierarchy”) and specialisation (“down the hierarchy”).
Generalisation:
- Factors out commonality (in terms of methods and fields) of the types.
- For example, all
Animal
sintroduce()
themselves and havelegs
. - Abstraction is closely related to generalisation, where we ignore subtle sub-type differences (e.g. all of our
Animal
swalk()
—albeit differently).
Specialisation:
- Sub-types have their own particular methods and fields.
- For example, only
Rabbit
s cancarryEasterEgg(Egg egg)
, whileCat
s canchaseMouse(Mouse mouse)
.
Here’s how we might specialise the child classes, by adding methods that don’t exist in the parent class:
Get your hands dirty!
Edit the code above as follows:
- Specialise the
Rabbit
class some more:- Add a
private String favVegetable
instance field, - Add a parameter to the
Rabbit
constructor to set the field, and - Add a getter
getFavVegetable()
.
- Add a
- Specialise the
Cat
class some more:- Add a
private int numLivesRemaining
instance field, - Initialise the field to
9
(allCat
s start off with 9 lives), and - Add a getter
getNumLivesRemaining()
.
- Add a
- Complete the
Egg
class:- Add a simple constructor requires a
String
name for the egg being created, then @Override
thetoString()
for theEgg
, so it prints its name.
- Add a simple constructor requires a
- Complete the
Mouse
class:- The
Mouse
classextends
theAnimal
class, - Add a simple constructor, much like the
Cat
orRabbit
, - Implement all the methods that are required (notice that
Animal
is anabstract class
), - Add a
runAway()
instance method for theMouse
, which prints the mouse is scared and running away.
- The
- Make instances of these two new classes where they are currently declared, and then:
- Update the
carryEasterEgg()
method to:- Take a parameter of type
Egg
, - In the current print message, also print the egg being carried.
- Take a parameter of type
- Update the
chaseMouse()
method to:- Take a parameter of type
Mouse
, - It should print which mouse the
Cat
is chasing, and - Invoke that mouse’s
runAway()
method.
- Take a parameter of type
- Update the
If you make the changes above, this is what we end up with:
- The
Cat
class contains a new fieldnumLivesRemaining
that does not exist in theAnimal
class. - The
Cat
class contains a new methodgetNumLivesRemaining()
that does not exist in theAnimal
class. - The
Cat
class contains a new methodchaseMouse()
that does not exist in theAnimal
class. - The
Rabbit
class contains a new fieldfavVegetable
that does not exist in theAnimal
class. - The
Rabbit
class contains a new methodgetFavVegetable()
that does not exist in theAnimal
class. - The
Rabbit
class contains a new methodcarryEasterEgg()
that does not exist in theAnimal
class.
Visualising instances with inheritance
Here’s how we can visualise our instances following the same visual conventions we have been seeing before:
The instances are still represented as pieces of paper.
We can appreciate why we need to first call the parent class’ constructor:
- Since
Rabbit
andCat
instances are still partiallyAnimal
instances, we see the commonAnimal
fields shared by child classes.
Depending on the actual instance type (i.e. Cat
or Rabbit
), we have the additional instance fields that are unique to that child class.
Polymorphism
Po ■ ly-
- Prefix from Ancient Greek, many or much.
-morph
- Suffix, from Ancient Greek, form or shape.
Inheritance allowed us to inherit features from another class.
Polymorphism allows us to treat all Animal
s alike at compile-time, but at run-time the behaviour will depend on the actual object type.
public static void main(String[] args) {
Animal lola = new Rabbit("Lola Bunny", 1996, Animal.Gender.FEMALE);
Rabbit bugs = new Rabbit("Bugs Bunny", 1938, Animal.Gender.MALE);
Cat garfield = new Cat("Garfield", 1978, Animal.Gender.MALE);
...
}
Here’s how we can visualise these three different instances:
Animal lola = new Rabbit("Lola Bunny", 1996, Gender.FEMALE);
The lola
reference variable:
- Is declared as being type
Animal
. - Has a value (instance) assigned to it of type
Rabbit
.
This “mismatch” of declared type and value type is perfectly fine, as a Rabbit
“is a” Animal
.
- We can place any value of an
Animal
inside this box – as long as it’s anAnimal
or a sub-class ofAnimal
. - This means later on, we can put a different kind of
Animal
inside thislola
box (e.g. an instance of aCat
). - The only methods that are allowed to be invoked on the
lola
variable are methods declared in theAnimal
class (or its super-class, e.g.Object
). - Similarly, the only fields that may be accessed externally on the
lola
variable are fields declared in theAnimal
class.- Note: don’t confuse this with fields in general declared in the
Rabbit
class (e.g.favVegetable
). They are still accessible once polymorphism takes us into the context of theRabbit
class (e.g. when we execute an overridden method).
- Note: don’t confuse this with fields in general declared in the
- Why can’t we access
Rabbit
functionality through thelola
variable?lola
might later on refer to an instance of aCat
(or some otherAnimal
sub-class).
- The
lola
reference variable is therefore polymorphic.
Rabbit bugs = new Rabbit("Bugs Bunny", 1938, Gender.MALE);
The bugs
reference variable:
- Is declared as being type
Rabbit
. - Has a value (instance) assigned to it of type
Rabbit
.
The declared type and value type are both referring to the Rabbit
class.
- We can only place a value of a type
Rabbit
inside this box (or a sub-class ofRabbit
). - This means later on, we can put another
Rabbit
inside thisbugs
box (based on its declared type). - The only methods that are allowed to be invoked on the
bugs
variable are methods declared in theRabbit
class (or its super-classes, i.e.Animal
andObject
).
Cat garfield = new Cat("Garfield", 1978, Gender.MALE);
The garfield
reference variable:
- Is declared as being type
Cat
. - Has a value (instance) assigned to it of type
Cat
.
The declared type and value type are both referring to the Cat
class.
- We can only place a value of a type
Cat
inside this box (or a sub-class ofCat
). - This means later on, we can put another
Cat
inside thisgarfield
box (based on its declared type). - The only methods that are allowed to be invoked on the
garfield
variable are methods declared in theCat
class (or its super-classes, i.e.Animal
andObject
).
Methods with polymorphic parameters
Consider the following method declared in the same class the main()
method:
public static void moveAnimalTwice(Animal animal) {
animal.introduce(); // all Animals can introduce() themselves
animal.walk(); // all Animals can walk()
animal.walk();
System.out.println();
}
In very much the same spirit as the Animal lola
reference variable above, the Animal animal
parameter may refer to an instance of an Animal
, a Rabbit
or a Cat
.
It doesn’t matter which it is, as all Animal
types (and sub-types) can introduce()
and walk()
!
Sure, some of these methods might be overridden by the sub-classes, but we don’t care about that. All we care about is that these methods exist at the Animal
type level.
As such, the Animal animal
parameter in the moveAnimalTwice()
method is similarly said to be polymorphic.
In OOP languages, we often use the term “dynamic dispatch”, or “dynamic binding”, to explain such behaviour:
- The compiler doesn’t commit (at compile time) as to which version of
walk()
orintroduce()
it should be executing. In fact, it’s not possible for it to commit to a particular version, as it doesn’t know what type theanimal
variable will refer to. - Instead, the decision is deferred to runtime. It dynamically binds (and consequently dispatches) to the appropriate method implementation depending on the particular instance type.
Here’s how we might visualise the animal
parameter:
void moveAnimalTwice(Animal animal) { ... }
Quite frankly, the Animal animal
parameter of the moveAnimalTwice()
method doesn’t care if it is assigned a reference to a Cat
or a Rabbit
(or any other type). All it cares about is being assigned an Animal
instance or a sub-class of Animal
. But of course, keep in mind that in our example Animal
is abstract
class.
Here’s how we might make use of this method:
public static void main(String[] args) {
Animal lola = new Rabbit("Lola Bunny", 1996, Animal.Gender.FEMALE);
Rabbit bugs = new Rabbit("Bugs Bunny", 1938, Animal.Gender.MALE);
Cat garfield = new Cat("Garfield", 1978, Animal.Gender.MALE);
moveAnimalTwice(lola); // hops...
moveAnimalTwice(bugs); // hops...
moveAnimalTwice(garfield); // strolls...
}
Casting object reference types
But what happens if we really need to access some of the sub-type specific logic (whether it be fields or methods)?
Lets have a look at the following example to illustrate. We know that Cat
s are pretty lazy! They sure aren’t going to move twice unless they’re chasing a mouse! So, let’s revise the moveAnimalTwice()
method above as follows:
public static void moveAnimalTwice(Animal animal) {
animal.introduce(); // all Animals can introduce() themselves
animal.walk(); // all Animals can walk()
// check if the instance is actually a Cat. If so, chase a mouse!
if (animal instanceof Cat) {
Cat cat = (Cat) animal; // cast the Animal reference into a Cat reference
cat.chaseMouse(); // access Cat-specific content with the Cat reference
}
animal.walk(); // walk() a second time
}
It’s usually safest to double-check that the animal
reference is indeed pointing to an instance of Cat
by using the instanceof
check. This evaluates to true
if the reference denoted by the animal
variable indeed refers to an instance of a Cat
(or a sub-class of Cat
!), otherwise it evaluates to false
.
Only if the check succeeds, will we cast the reference to a Cat
reference variable (here, Cat cat
). This now allows us to access all Cat
-specific logic via this cat
reference variable.
So, what happens if you incorrectly cast a Rabbit
instance to a Cat
? Test it out by removing the instanceof
check! The compiler won’t care, but at runtime you’ll encounter an unwelcoming ClassCastException
!
In summary, here’s what we have done:
On the left is how we can visualise the situation with the Animal animal
reference. Even though we might indeed be pointing to a Cat
instance, the compiler can never be too sure! So, it will only allow us to access methods declared in the Animal
class.
So what we end up doing, is casting this reference (notice it’s the same memory location: 0x790
) by declaring another reference of type Cat
. We tell the compiler, “trust me, I’m pretty sure the instance that will appear here is indeed a Cat
type”. The compiler allows us to do this, and we now have a Cat cat
reference. From this reference, we can see everything specific to the Cat
class.