Inheritance
Sharing Features Between Classes!
Sharing Features Between Classes!
The animal kingdom is full of a wide variety of creatures, such as cats, dogs, and mice. Without even thinking, we can probably easily list some of the major differences and similarities between these different species. Dogs bark, cats meow, and mice squeak, for example, making each one unique and different from the others. They each have 4 legs, a tail, and live on land, showing that they also have many things in common.
In fact, if we look at how all life is classified biologically, we see that we can use those differences and similarities to describe exactly how all species are related to one another:
In biology, all life is divided into a number of groups, with species in each group sharing a number of traits. For example, all animals are in the kingdom animalia, whereas all plants are in the kingbom plantae. So, we know that every animal species shares some characteristics with all other animals, and also some that are different from all plant species.
Then, each kingdom can be divided into a number of phyla, such as chordata, for all animals who share a similar spinal chord structure during some point in their life cycle. Other animals might be in the anthropoda phylum, showing that they share a segmented body and exoskeleton.
We can repeat the process all the way down to the individual species level, uniquely describing each species and the similarities and differences between it and any other species of life we’ve discovered. For example, the common domesticated dog is classified as:
Animalia > Chordata > Mammalia > Carnivora > Canidae > Canis > C. Lupus > C. l. Familiaris
where as the common house cat is classified as:
Animalia > Chordata > Mammalia > Carnivora > Feliformia > Felidae > Felinae > Felis > F. catus
Based on those classifications alone, we know that each species shares common traits through the order carnivora, but they differ beyond that.
Put another way, we can say that both cats and dogs have inherited certain traits from the order carnivora, as well as other classifications they share in common.
This concept of inheritance is the other key concept behind object-oriented programming. In this chapter, we’ll explore how we can use inheritance to show the common elements shared between classes that represent similar items, and how that makes our programs much more powerful when it comes to storing and using those objects.
https://www.pinclipart.com/pindetail/ibxxwm_vector-cats-open-mouth-dog-cat-and-mouse/ ↩︎
File:Biological classification L Pengo vflip.svg. (2019, April 5). Wikimedia Commons, the free media repository. Retrieved 19:26, October 31, 2019 from https://commons.wikimedia.org/w/index.php?title=File:Biological_classification_L_Pengo_vflip.svg&oldid=344938263. ↩︎
Object-oriented programming allows us to represent items from the real world in our code: we use a class as a general description of each type of item, and then we can instantiate objects based on that class to represent specific items in the world.
A classic example is to consider writing a computer program to represent a school, with students and teachers represented in code as separate classes. As we can see in the diagram above, both students and teachers can have attributes and methods that help describe the information and behaviors associated with that type of item.
However, this really doesn’t give us the whole picture. For starters, we know that both students and teachers are people, and so they share many of the same features common to all people (name, age, birthday, etc.). Similarly, they can perform many of the same actions.
Of course, we could just add those shared attributes and methods to each class, as we did with the name
attribute in the example above. However, hopefully by now our programming instincts tell us that this solution violates the Don’t Repeat Yourself (DRY) mantra, since we’ll have multiple copies of the same code in each class1. If we add an additional class to represent a school administrator, we’ll have to copy all of that code to the new class, making it even more complicated.
This is where we can take our inspiration from the biological system of classificationāwhat if we were able to create a class containing the items that students and teachers have in common?
“Sharing attributes” by itself is not a good justification for using inheritance. The real-world objects or concepts should be related in some fashion. ↩︎
The arrowheads in the video's UML are the wrong type. UML uses different arrowheads to mean different things. Inheritance uses an "open triangle" not filled in arrows as depicted here. The images in the text have been updated.
For example, we could create a class called Person in our program, and that class could represent all of the attributes and methods that are shared by both students and teachers, as well as by any other people we might want to include in our program.
Then, we can update the Student and Teacher classes to inherit those attributes and methods from the Person class. In effect, we are saying that a student or a teacher is also a person, so anything that a person is or does also applies to a student or a teacher.
There are some special terms we can use to describe the classes in an inheritance relationship. In this example:
Person is the parent class, base class, or superclass. Student and Teacher are child classes, derived classes, or subclasses of Person.
This is a really important concept in object-oriented programming. It allows us to easily define the similarities between several classes.
In a UML diagram, we can show this inheritance relationship using an open arrow between the classes. It’s important to remember that the arrow points to what the class is inheriting from. So, the arrow going from the Student class to the Person class says “the Student class inherits from the Person class.” We can also remember this by saying the arrow “Points to the Parent” class. This can be confusing, so we must always make sure we look closely at the direction the arrow is pointing in our UML diagrams.
Finally, we can even go further and create another set of classes to represent Graduate and Undergraduate students, and have them inherit from the Student class. There is no limit to how many layers of inheritance we can create in our programs. In addition, some languagesāsuch as Pythonāallow a class to inherit from multiple parent classes!
Allowing a class to inherit attributes and methods from another class allows us to use those classes in very unique ways.
One way we can use those classes is through the application of polymorphism. Polymorphism can be loosely defined as “the condition of occurring in several different forms”^[https://www.lexico.com/en/definition/polymorphism], but in programming we use the term to describe the fact that an object instantiated from a class that inherits from another class can take on multiple forms, depending on how it is used.
Let’s go back to basics for just a minute and talk about what this means in the simplest sense. In most programming languages, such as Java or C, a variable must be declared with a data type that tells us what type of data we can store in that variable. It could be an integer, a floating point number, or even a particular type of object such as a Student
object. Other languages, such as Python, don’t require us to declare the data type of a variable in advance, but internally it keeps track of exactly the type of data stored in that variable when it is assigned.
For example, let’s assume that we’ve instantiated an object using the Teacher
class. So, initially, we know that the data type of that variable is Teacher
, since it was created from that class.
However, what if we try to store that object in a variable with the Person
data type? Will that work? Put another way, will the program consider an object of the Teacher
data type to also be an object of the Person
data type?
Indeed it will! This is because an object instantiated from a class that inherits from other classes can exhibit polymorphism, existing as many different data types at the same time. Depending on how we use it, an object created using the Teacher
class can also be thought of as a Person
.
However, if we create an object from the Person
class, we can’t say whether it is a Teacher
or not. So, we aren’t allowed to go in that direction—a Person
object cannot be stored in a variable of the Teacher
data type.
A great way to think of this would be the logical statement:
All Teachers are Persons
But Not All Persons are Teachers
Consider the following
We say that inheritance is transitive. A Border Collie
is a Dog
is a Pet
. Thus a Border Collie
is a Pet
.
This relationship is one-way, sub-classes have all their sub-class-specific features, as well as those of all their super-classes.
Most languages have at the top level, an Object
class, from which all objects implicitly inherit. Both Python and Java have an Object class, from which all programmer defined objects get their default constructors and toString(__str__
) methods.
Inheritance IS NOT symmetric
Inheritance is not symmetric. The fact that a Border Collie
is a Dog
DOES NOT MEAN a Dog
is a Border Collie
.
Inheritance is not equality. Equality is both transitive and symmetric.
In this chapter, we’ll see how we can use this feature in our programs. Polymorphism is sometimes difficult to define or describe without seeing it in action.
Another handy feature of class inheritance is the ability to override certain methods of the parent class. For example, in our Person class, we’ve included a method called birthday()
that will simply increase that person’s age by 1.
However, what if we want to do something special when a student has a birthday? In that case, we can override the birthday()
method from the Person class by providing our own code for the method in the Student class. Then, when we create an object using the Student class, it will use the birthday()
method from the Student class instead of the one from the Person class. In most languages, this will even work if we have the Student object stored in a variable using the Person data type. It’s pretty handy!
Finally, we can also use another concept in object-oriented programming to create abstract methods in our classes. An abstract method is a method that is declared to be part of the parent class but is not implemented with any code. We call a class containing such a method an abstract class. Since it has a method containing no code, we can no longer instantiate that class and use it.
However, any class that inherits from that class has the option to implement the abstract methods by overriding and providing code for those methods. By doing so, the child class is no longer abstract and can be instantiated. However, if it does not do so, then the child class will also be abstract.
In our UML diagram above, the do_work()
method and the class Person are abstract. Because at least one method is abstract, the class must also be abstract. We know this since their names are printed in an italic font. In the two child classes, both Student and Teacher have included an entry for the do_work()
method. Since neither of those classes contains any italicized items, we know that they are not abstract.
That covers most of the major concepts when working with inheritance and polymorphism in our programs. Before we learn how to write code using these ideas in our language of choice, we’ll take a minute to review the important terminology we’ve learned so far in this chapter.
Inheritance in Java
Now that we’ve learned a bit about the theory behind inheritance in object-oriented programming, let’s dive into the code and see how we can accomplish this in Java.
For these examples, we’ll be implementing the following UML diagram:
The UML diagram above includes some items that we haven’t discussed yet. Don’t panic! We’ll cover them as they become relevant in this module.
For now, feel free to ignore the fact that methods are italicized. Similarly, you can treat a hash symbol #
in front of an attribute or method the same as a plus +
that denotes they should be public.
We’ll learn what each of these indicate later in this module.
The first step is to build the inheritance relationships. Let’s start by just declaring the Vehicle
class for now. Remember that the Vehicle
class should be defined in Vehicle.java
. We won’t add any methods or attributes at this point:
public class Vehicle{
}
As you’ll recall, a class declaration is pretty simple.
Next, let’s declare the MotorVehicle
class in MotorVehicle.java
. This class inherits from the Vehicle
class, so we’ll need to use a new keyword to make that work:
public class MotorVehicle extends Vehicle{
}
In the example above, we’ve used the extends
keyword in Java to show that the MotorVehicle
class inherits from, or extends, the Vehicle
class. That’s all we need to do to show that a class inherits from another class.
Let’s see if we can do the same process for the Car
, Truck
, and Airplane
classes that are shown in the UML diagram above. We must make sure we place the code for each class in the correct file.
Now that we’ve created some classes that follow different inheritance relationships, let’s explore how we can use those relationships to change some methods in each class through the use of method overriding.
Vehicle
ClassFirst, let’s look at a couple of methods in the Vehicle
class, the move()
and describe()
methods. As we might recall, the describe()
method is an abstract method since it is in italicized text in the UML diagram. We’ll discuss how to build an abstract method later in this chapter, but for now we’ll just implement it as a normal method that does nothing. Likewise, we haven’t discussed what a hash symbol #
means in front of the speed
attribute or the constructor, so we’ll just make them public for now.
public class Vehicle{
private String name;
public double speed;
public String getName(){ return this.name; }
public Vehicle(String name){
this.name = name;
this.speed = 1.0;
}
public double move(double distance){
System.out.println("Moving");
return distance / this.speed;
}
public String describe(){
return "";
}
}
That’s a very simple implementation of the Vehicle class. The move()
method simply accepts a distance to move as a floating point number, and then divides that by the speed of the vehicle to get the time it takes to go that distance. For the default vehicle, we’ll assume that it moves at a speed of 1.0.
Now that we’ve created some methods in the Vehicle class, let’s go back to the Airplane
class and see how we can build it. First, we’ll need to add the attributes and the constructor for this class:
public class Airplane extends Vehicle{
private double wingspan;
private int capacity;
public Airplane(String name, double wingspan, int capacity){
this.name = name;
this.wingspan = wingspan;
this.capacity = capacity;
}
}
Before we go any further, let’s stop there and compile this code to make sure that it works. Recall that we’ll need to manually run the compiler from the terminal and include both of these files in the compiler command. So, to do that, we can open the terminal in Codio and use the following two commands to open the directory containing these files and then compile them:
cd ~/workspace/12j-inherit/vehicle
javac Vehicle.java Airplane.java
When we do that, we’ll get some errors as shown in this screenshot:
There are actually two errors here. Let’s talk about the second error first. It says that the name
attribute in the Vehicle
class is private, so we can’t access it from Airplane
. This may seem strange, since Airplane
is inheriting from Vehicle
, but in Java, the private
keyword also prevents any child classes from accessing that data.
This actually makes sense if we think about it. For example, consider a class containing private data that we’d like to access. We could just create our own class that inherits from that class, and then we’d have direct access to all those private attributes and methods. Sounds like a pretty bad security flaw, right? That’s why Java enforces the rule that any private
attribute cannot be accessed by child classes. Later in this chapter, we’ll learn about another security modifier keyword that allows child classes to access these variables.
So, to set the value of the name
attribute, we’ll need to somehow provide that value to the constructor of the Vehicle
class. However, the first error addresses that problem directly, so let’s look at it and see how the solution to that error fixes both of these problems.
The first error is telling us that we cannot create an instance of the Vehicle class because we didn’t provide the required parameter for the constructor. But wait, why is it trying to create a Vehicle object? Doesn’t this constructor just instantiate an Airplane object?
One of the major things to recall when inheriting from a class is that each object instantiated from the child class is also an object of the parent class. We can think of it like the child object contains an instance of the parent object. Because of that, when we try to create an instance of the child class, or Airplane
in this example, that constructor must also be able to call the constructor for the parent class, or Vehicle
.
We run into a snag, however, because we’ve provided a constructor in Vehicle
that requires an argument. In that case, we must provide an argument to the constructor for Vehicle
in order to instantiate that object, since the default constructor is no longer available. How can we do that?
Thankfully, there is a quick an easy way to handle this in Java. Inside of our constructor, we can use the special method super()
to call the parent class’s constructor. We can provide any needed arguments to that method call, which are then provided to the constructor in the parent class. So, let’s update our constructor to use a call to super()
:
public class Airplane extends Vehicle{
private double wingspan;
private int capacity;
public Airplane(String name, double wingspan, int capacity){
super(name);
this.wingspan = wingspan;
this.capacity = capacity;
}
}
Now, inside of the constructor for Airplane
, we have added the line super(name)
, which calls the constructor of the parent class Vehicle
, providing name
as the argument for the String parameter. This will resolve both of our errors.
It is important to note, however, that the call to the parent class’s constructor using super()
must be the first line inside of this constructor, before any other code. The Java compiler will helpfully enforce this restriction, providing us with a helpful error if we forget.
Finally, we can explore how to override a method in our child class. In fact, it is as simple as providing a method declaration that uses the same method signature as the original method. A method signature in programming describes the method name, return type, and type and order of parameters defined in the function. So, we can override the move()
and describe()
methods in Airplane
using code similar to the following:
public class Airplane extends Vehicle{
private double wingspan;
private int capacity;
public Airplane(String name, double wingspan, int capacity){
super(name);
this.wingspan = wingspan;
this.capacity = capacity;
}
public void landing_gear(boolean set){
if(set){
System.out.println("Landing gear down");
}else{
System.out.println("Landing gear up");
}
}
@Override
public double move(double distance){
this.landing_gear(false);
System.out.println("Moving");
this.landing_gear(true);
return distance / this.speed;
}
@Override
public String describe(){
return String.format("I am an airplane with a wingspan of %f and capacity %d", this.wingspan, this.capacity);
}
}
In this code, we see that we have included method declarations for both move()
and describe()
that use the exact same method signatures as the ones declared in Vehicle
. Also, since we are overriding a method from a parent class, we must also use the @Override
method decorator above each method. This tells the Java compiler that we intend to override a method with this code, and it will make sure that we’ve done it correctly or give us errors when we try to compile the code.
To really understand how this works, let’s look at a quick main()
method that explores how each of these work.
public class Main{
public static void main(String[] args){
Vehicle vehicle = new Vehicle("Boat");
Airplane airplane = new Airplane("Plane", 175, 53);
System.out.println(vehicle.move(10));
System.out.println(airplane.move(10));
System.out.println(vehicle.describe());
System.out.println(airplane.describe());
}
}
This code will simply call each method, printing whatever values are returned by the methods themselves. We must also remember that some of the methods may also print information, so we’ll see that output before we see the return value printed.
To compile and run this code, we can use these commands:
cd ~/workspace/12j-inherit/vehicle
javac Vehicle.java Airplane.java Main.java
java Main
and we should see the following output:
In this screenshot, we can see that calling the move()
method on the Vehicle
object just prints the message “Moving”, while calling it on the Airplane
object causes the messages about landing gear to be printed as well. This shows that our Airplane
object is using the code from the overridden move()
method correctly.
In a later part of this chapter, we’ll discuss polymorphism and how overridden methods have a major impact on the functionality of objects stored in that way.
Let’s see if we can do the same for the overridden methods in the Truck
and Car
classes. First, we’ll start with this code for the MotorVehicle
class:
public class MotorVehicle extends Vehicle{
public int number_of_wheels;
public double engine_volume;
public MotorVehicle(String name){
super(name);
this.number_of_wheels = 2;
this.engine_volume = 125;
}
public String honk_horn(){
return "";
}
}
See if you can complete the code for the Truck
and Car
classes to do the following:
Truck.describe()
should return “I’m a big semi truck hauling cargo”Truck.honk_horn()
should return “Honk”Car.describe()
should return either “I’m a sedan” if it has 4 doors, “I’m a coupe” if it has 2 doors, or “I’m different” if it has any other number of doorsCar.honk_horn()
should return “Beep”You’ll also need to write the constructors for each class. Inside the constructor, you should store the parameters provided to the appropriate attributes.
Working with inherited classes also gives us an opportunity to learn about how data can be secured in the parent class so that any child class can easily access it, without any external class being able to do so.
In Java, we can simply use the protected
keyword as a security modifier, just like we learned how to use private
and public
in an earlier module. In effect, anything marked as protected
in Java will be accessible to the class in which it is declared, as well as to any child classes, but not to any other classes.
In our UML diagram, we use the hash symbol #
before an item to denote that it should be protected using the protected
keyword. So, we can update our Vehicle
class to make the speed
attribute as well as the constructor protected:
public class Vehicle{
private String name;
protected double speed;
public String getName(){ return this.name; }
protected Vehicle(String name){
this.name = name;
this.speed = 1.0;
}
public double move(double distance){
System.out.println("Moving");
return distance / this.speed;
}
public String describe(){
return "";
}
}
It is worth noting, however, that any developer could simply chose to inherit from one of these classes, giving them access to all of that data. So, while call these “security modifiers”, they aren’t actually providing a real sense of security. Instead, they are simply making it more difficult to accidentally access or use these items. Any determined programmer will probably be able to figure out a way around it.
Also, since we are making the constructor in this class protected, it will prevent any class that doesn’t extend this class from instantiating an object based on this class. A bit later in this chapter, we’ll also learn how to declare this class as an abstract class, which will also prevent any other class from instantiating it.
Let’s go ahead and update all of the items marked as protected in the UML diagram above in our code.
Note: this video contains errors in the UML diagram, these errors have been fixed below.
Now that we’ve learned how to build the structure needed for classes to inherit attributes and methods from other classes, let’s explore how we can use those classes in our code.
Earlier, we defined polymorphism as “the condition of occurring in several different forms”1. In code, this allows us to create an object of a child class, and then store it in the data type for the parent class. Let’s look at an example.
public class Main{
public static void main(String[] args){
Vehicle plane = new Airplane("Plane", 123, 45);
System.out.println(plane.getName());
System.out.println(plane.describe());
System.out.println(plane.move(10));
Vehicle car = new Car("Car", 4);
System.out.println(car.getName());
System.out.println(car.describe());
System.out.println(car.move(10));
}
}
In this code, we are instantiating an Airplane
object and a Car
object, but we are storing them in a variable declared with the Vehicle
data type. This is polymorphism at work! Since both Airplane
and Car
are inheriting from the Vehicle
data type, we can store object of each of those types in the Vehicle
data type.
Also, since those variables are stored using the Vehicle
data type, we can use any methods and access any attributes that are available publicly from the Vehicle
class.
So, when we run this code using these commands:
cd ~/workspace/12j-inherit/vehicle
javac Vehicle.java Airplane.java MotorVehicle.java Car.java Truck.java Main.java
java Main
we’ll get the following output:
As we can see, even though we are calling methods defined in the Vehicle
class, it is actually using the code from the methods that were defined in the Airplane
and Car
classes, respectively. This is because those methods are overridden in the child classes.
What if we want to use the honk_horn()
method of the Car
object? Could we do that?
Vehicle car = new Car("Car", 4);
System.out.println(car.honk_horn());
When we try to compile that code, we’ll get the following error:
This is because the Vehicle
class doesn’t have a method called honk_horn()
defined. So, even though the object is a Car
, since it is stored in a variable using the data type Vehicle
, we can only access things that were defined in the Vehicle
class.
So, when it comes to polymorphism, there are a couple of important rules to remember:
Here’s another example of the power of polymorphism. In this case, we’ll create an array that stores Vehicles
, and fill that array with different types of vehicles.
public class Main{
public static void main(String[] args){
Vehicle[] array = new Vehicle[3];
array[0] = new Airplane("Plane", 123, 45);
array[1] = new Car("Car", 4);
array[2] = new Truck("Truck", 157);
for(Vehicle v : array){
System.out.println(v.getName());
System.out.println(v.describe());
System.out.println(v.move(10));
}
}
}
In this example, we are able to use an enhanced for loop to iterate over the objects in the array and call methods on each one. As long as those methods are defined in the Vehicle
class, we can use them, no matter what type of object was instantiated and placed in the array.
So, when we run this program, we’ll see the following output:
Polymorphism is a very powerful tool for object-oriented programmers. Feel free to modify the Main
class open to the left, then compile and run the code for this example. See if you can create Car
and Truck
objects and store them in the MotorVehicle
data type, then use the honk_horn()
method!
Of course, polymorphism can make things a bit more complicated when it comes to determining exactly what type of object is stored in a variable. Thankfully, Java includes a few easy ways to determine what type of object is really stored in a variable, as well as ways that we can convert the types if needed.
Let’s go back to the previous example from the last page, where we had placed all of our objects in an array.
public class Main{
public static void main(String[] args){
Vehicle[] array = new Vehicle[3];
array[0] = new Airplane("Plane", 123, 45);
array[1] = new Car("Car", 4);
array[2] = new Truck("Truck", 157);
for(Vehicle v : array){
System.out.println(v.getName());
System.out.println(v.describe());
System.out.println(v.move(10));
}
}
}
What if we’d like to call the honk_horn()
method, but only if the object supports that method? To do that, we’ll need to determine what type of object is stored in the variable, and then convert it, or cast it, to a type that supports the honk_horn()
method. So, we can update the code as shown below:
public class Main{
public static void main(String[] args){
Vehicle[] array = new Vehicle[3];
array[0] = new Airplane("Plane", 123, 45);
array[1] = new Car("Car", 4);
array[2] = new Truck("Truck", 157);
for(Vehicle v : array){
if(v instanceof MotorVehicle){
MotorVehicle m = (MotorVehicle)v;
System.out.println(m.honk_horn());
}else{
System.out.println(v.getName() + " can't honk!");
}
}
}
}
Here, we are doing two very important operations. First, we are using v instanceof MotorVehicle
to determine if the object stored in v
can be stored in a variable using the MotorVehicle
data type. So, for objects created from the Car
and Truck
classes, this operation will return True
since both Car
and Truck
are child classes of MotorVehicle
.
Then, once we’ve determined that we can store the object in v
as a MotorVehicle
, we must convert it. To do that, we use the expression (MotorVehicle)v
. This is called a cast operation. To do this, we put the data type we’d like to convert the variable to in parentheses, directly in front of the variable to be converted. Then, we can store this result in a variable using the MotorVehicle
data type.
As you may recall, we’ve done this before to convert numbers stored as integers to floating point numbers.
It is important to note, however, that if we try to cast an object to a type that isn’t allowed, we will get an exception. So, we’ll either need to use an If-Then statement to confirm that we can make the conversion before attempting it, or use a Try-Catch statement and be prepared to catch an exception if it fails.
Place the code above in the Main
class and see what it does. Can you come up with any other programs that would require us to convert objects between types?
Note: this video contains errors in the UML diagram, these errors have been fixed below.
Another major feature of class inheritance is the ability to define a method in a parent class, but not provide any code that implements that function. In effect, we are saying that all objects of that type must include that method, but it is up to the child classes to provide the code. These methods are called abstract methods, and the classes that contain them are abstract classes. Let’s look at how they work!
In the UML diagram above, we see that the describe()
method in the Vehicle
class is printed in italics. That means that the method should be abstract, without any code provided. To do this in Java, we simply must use the abstract
keyword on both the method and the class itself:
public abstract class Vehicle{
private String name;
protected double speed;
public String getName(){ return this.name; }
protected Vehicle(String name){
this.name = name;
this.speed = 1.0;
}
public double move(double distance){
System.out.println("Moving");
return distance / this.speed;
}
public abstract String describe();
}
Notice that the keyword abstract
goes after the security modifier, but before the class
keyword on a class declaration and the return type on a method declaration.
In addition, since we have declared the method describe()
to be abstract, we must place a semicolon after the method declaration, without any curly braces. This is because an abstract method cannot include any code.
Now, any class that inherits from the Vehicle
class must provide an implementation for the describe()
method. If it does not, that class must also be declared to be abstract. So, for example, in the UML diagram above, we see that the MotorVehicle
class does not include an implementation for describe()
, so we’ll also have to make it abstract.
We can also declare a class to be abstract without including any abstract methods. By doing so, it prevents the class from being instantiated directly. Instead, the class can only be inherited from, and those child classes can choose to be instantiated by omitting the abstract
keyword.
Let’s see if we can update the Vehicle
and MotorVehicle
classes to be abstract, with an abstract definition for the describe()
and horn_honk()
method as well.
Finally, we can also build classes that are able to inherit from multiple parent classes. In Java, this is done through the use of interfaces.
Before we can really understand the importance of multiple inheritance, we must first discuss the “Diamond Problem.” This is a very common example in object-oriented programming, and it is used to describe one of the common pitfalls for multiple inheritance in programming.
Consider the following code, which defines 4 classes:
public abstract class A{
public abstract void do_something();
}
public class B extends A{
public void do_something(){
System.out.println("B");
}
}
public class C extends A{
public void do_something(){
System.out.println("C");
}
}
public class D extends B, C{
}
In this code, we are trying to inherit from both class B
and C
inside of D
. So, we’ll end up with a class hierarchy diagram that looks something like this:
The problem arises when we try to use the class D
, as in this sample program:
public class Main{
public static void main(String[] args){
D obj = new D();
obj.do_something(); // what happens?
}
}
In this example, we are calling the do_something()
method on an object instantiated from class D
. However, that method is defined in both B
and C
, which are parents of D
. So, what version of the code should we use?
In short, we have no idea! That is the crux of the diamond problem: if we allow multiple inheritance, we can run into situations where the program has no idea which version of a function’s code to use.
Java allows us to solve this by the use of a special type of abstract class called an interface. An interface is an abstract class that only includes abstract methods, with no other data. One way to think about it is that it is simply describing the actions a class should be able to perform, also known as an interface.
So, we can update the example above a bit using interfaces instead of abstract classes.
public interface B{
public void do_something();
}
public interface C{
public void do_something();
}
public class D implements B, C{
public void do_something(){
System.out.println("D");
}
}
Here, we have used the keyword interface
to declare that classes B
and C
are interfaces, and will only include abstract method declarations. Then, we can use the implements
keyword to show that our class D
is implementing both of these interfaces. We can also extend D
from another class, and that would be placed before the implements
keyword.
Of course, by doing so, we have to provide the implementation for do_something()
in D. But, we can store an instance of D
in variables for storing both B
and C
data types. Pretty neat, right?
We won’t use multiple inheritance in this course, but it is helpful to know that it is available as we go forward in our programming experience.
File:Diamond inheritance.svg. (2018, September 25). Wikimedia Commons, the free media repository. Retrieved 02:42, November 4, 2019 from https://commons.wikimedia.org/w/index.php?title=File:Diamond_inheritance.svg&oldid=321823174. ↩︎
Now that we’ve seen how to build classes that can inherit attributes and methods from other classes, let’s work through a simple example program together to see how it all works in practice. The code in this program will be very simple, because the purpose is to explore how we can use the structure of inheritance in our programs.
First, let’s start with a problem statement. In this problem, we are going to build a program that will help us find an object in a toolbox based on several criteria provided from the user. To represent the objects in the toolbox, we’ll use a structure of class inheritance as shown in the UML diagram below:
Right-click image and choose “Open image in new tab” or similar to view larger version
The completed program should be able to perform the following steps:
true
to that method call, then that tool is able to perform that action. The program should print the description of the appropriate tool to the terminal and terminate. In this example, each query will only result in one matching tool, if any.For example, here’s a sample input file that could be provided to this program:
3
AdjustableWrench 170 10 25
CombinationWrench 135 8
CrossCutSaw 350 wood:drywall
Then, if the user inputs the following query:
tighten 150 8
The program will respond with the following output:
CombinationWrench Length: 135 Size: 8
Let’s walk through this program step by step and see how we need to build it.
Tool
ClassFirst, we can start with the Tool
class. Looking at the UML diagram, we see that the describe()
method is in italics, meaning it should be an abstract method. Likewise, we see that the constructor is protected, so the class cannot be instantiated directly. Both of those help us realize that the entire Tool
class should be abstract. So, we can easily create it and define the constructor and the describe()
method in code:
public abstract class Tool{
protected Tool(){
// do nothing
}
public abstract String describe();
}
That’s really it! In many cases, the base class includes very little, if any, content or code. Instead, it simply gives us a shared starting point for the other classes in this program, and defines a single method, describe()
, that each child class must implement.
Wrench
and Saw
ClassesNext, we can go down a level and implement the Wrench
and Saw
classes. Each of these classes contains a single attribute with a getter method. They also each contain a protected constructor, and an abstract method defining what each type of tool can do. Since neither of these classes implements the describe()
method, even though they inherit from Tool
, they will also be abstract. So, the code for these classes will be very similar to what we already created for the Tool
class:
public abstract class Wrench extends Tool{
private int length;
protected Wrench(int length){
this.length = length;
}
public int getLength(){ return this.length; }
public abstract boolean tighten(int clearance, int size);
}
public abstract class Saw extends Tool{
private int length;
protected Saw(int length){
this.length = length;
}
public int getLength(){ return this.length; }
public abstract boolean cut(int length, String material);
}
As we can see in the code above, these classes are nearly identical, differing only in the name of the class and the method signatures of the different abstract methods.
At this point, we can quickly check our program structure to make sure everything is built correctly so far.
AdjustableWrench
ClassNext, let’s look at one of the child classes of Wrench
. As we can see in the UML diagram above, this class has both a min_size
and a max_size
attribute that are set through the constructor, as well as getter methods for each one. So, most of the code for this class is already pretty straight forward, just based on the structure of the class alone.
public class AdjustableWrench extends Wrench{
private int min_size;
private int max_size;
public AdjustableWrench(int length, int min_size, int max_size){
super(length);
this.min_size = min_size;
this.max_size = max_size;
}
public int getMinSize(){ return this.min_size; }
public int getMaxSize(){ return this.max_size; }
// other methods go here
}
So, that just leaves the describe()
and tighten()
methods. Let’s tackle describe()
first. In the example above, we see that the describe()
method seems to just print the name of the class, followed by each attribute’s name and value. So, we can pretty easily implement that method in code:
public String describe(){
return String.format("AdjustableWrench: Length: %d MinSize: %d MaxSize: %d", this.length, this.min_size, this.max_size);
}
However, if we try to compile this code, we’ll get an error message:
AdjustableWrench.java:16: error: length has private access in Wrench
return String.format("AdjustableWrench: Length: %d MinSize: %d MaxSize: %d", this.length, this.min_size, this.max_size);
^
2 errors
You can see for yourself by trying to compile your code at this point. You should get a similar message (you’ll probably see another stating that we haven’t implemented tighten()
yet, which is expected).
Looking at the UML diagram above, we see that the length
attribute in the parent Wrench
class is indeed private instead of protected. So, we’ll need to use the getter method getLength()
to get that value instead:
public String describe(){
return String.format("AdjustableWrench: Length: %d MinSize: %d MaxSize: %d", this.getLength(), this.min_size, this.max_size);
}
That should fix the error! You can try it with the button above after making the change. We’ll still get an error about not implementing tighten()
, which is the last step in building this class.
The tighten()
method should determine whether this wrench is able to tighten the item described. To really understand what we are dealing with, we must understand what an adjustable wrench looks like. Here’s a picture of one from the real world:
The function accepts two parameters: a clearance value, which shows how much room between the item and the surrounding equipment there is, and the size of the item to be tightened itself. So, we know that if our wrench is shorter than the clearance, and supports an item of the given size, we’ll be able to tighten it.
An adjustable wrench has a head that can be adjusted to multiple sizes, so as long as the size given is between the minimum and maximum size our wrench is able to tighten, we can return true. So, to put that into code:
public boolean tighten(int clearance, int size){
return clearance >= this.getLength() && size >= this.min_size && size <= this.max_size;
}
As you may recall from an earlier module, we can directly return the result of a Boolean logic expression, so that makes this method even simpler.
CombinationWrench
and OpenEndWrench
ClassesNow that we’ve written the code for the AdjustableWrench
class, it should be pretty simple to write the code for the other two types of wrenches.
First, a CombinationWrench
, which typically only supports one size of bolt or nut. It typically looks like this.
So, the tighten()
method must simply check the clearance and the size of the item provided against the size of the wrench. Here’s the code for that class:
public class CombinationWrench extends Wrench{
private int size;
public CombinationWrench(int length, int size){
super(length);
this.size = size;
}
public int getSize(){ return this.size; }
public String describe(){
return String.format("CombinationWrench Length: %d Size: %d", this.getLength(), this.size);
}
public boolean tighten(int clearance, int size){
return clearance >= this.getLength() && size == this.size;
}
}
The other type of wrench, an OpenEndWrench
, typically has two heads of different size on either end:
So, it can tighten bolts or nuts of two different sizes. Therefore, the tighten()
method must determine if either size is applicable to the bolt or nut to be tightened. The code for that class is as follows:
public class OpenEndWrench extends Wrench{
private int size_one;
private int size_two;
public OpenEndWrench(int length, int size_one, int size_two){
super(length);
this.size_one = size_one;
this.size_two = size_two;
}
public int getSizeOne(){ return this.size_one; }
public int getSizeTwo(){ return this.size_two; }
public String describe(){
return String.format("OpenEndWrench Length: %d SizeOne: %d SizeTwo: %d", this.getLength(), this.size_one, this.size_two);
}
public boolean tighten(int clearance, int size){
return clearance >= this.getLength() && (size == this.size_one || size == this.size_two);
}
}
That’s really it! As we can see, while there is quite a bit of code in this program, much of the code is very similar between classes. We’re simply implementing the important bits and pieces of each class, with a slightly different implementation of the describe()
and tighten()
methods in each one.
At this point, we can check our code to confirm that the structure is correct.
CrossCutSaw
ClassThe CrossCutSaw
class is very similar to the classes we created for the different type of wrenches above. The only difference is that it uses a cut()
method to determine if the saw is able to cut the material described when we call that method.
First, let’s look at the rest of the code for that class. In the constructor, we are given a string that contains a list of materials that can be cut by the saw, separated by colons. So, we’ll need to use the String.split()
method to split that string into an array of strings to be stored in the class’s materials
attribute.
Likewise, since the getMaterials()
method should return a simple string, we can use the String.join()
method to make a string out of the array, with each element separated by a comma followed by a space. Finally, we can use that to help populate the describe()
method.
public class CrossCutSaw extends Saw{
private String[] materials;
public CrossCutSaw(int length, String materials){
super(length);
this.materials = materials.split(":");
}
public String getMaterials(){ return String.join(", ", this.materials); }
public String describe(){
return String.format("CrossCutSaw Length: %d Materials: %s", this.getLength(), this.getMaterials());
}
// additional methods here
}
It might be tempting to have the CrossCutSaw
class simply accept an array of materials in the constructor, and then return that array in the getMaterials()
method. However, recall that arrays are complex data type that are handled using call by reference. So, that leaves this class vulnerable to manipulation from an external code source.
For example, if the Main
class gives an array of materials to CrossCutSaw
via the constructor, we could simply store the reference to that array in our materials
attribute. However, if Main
proceeds to change some of the elements in the array, it would also update the array referenced by this class. Likewise, any code that calls the getMaterials()
method would also get a reference to the same array.
By creating our own array in the constructor, and then only returning a newly formed string each time a class calls the getMaterials()
method, we can protect our data from malicious changes.
An alternative method would be to create a deep copy of the array and store that copy in this class. We haven’t discussed how to do that in this course, but a future course on data structures will cover that process in depth.
The CrossCutSaw
has two more methods that we’ll need to implement: cut()
and findMaterial()
. The findMaterial()
method is a private method that allows us to search the array of materials that can be cut by this CrossCutSaw
object, and simply return a boolean value if the provided material is in the list. So, let’s address that method first.
private boolean findMaterial(String material){
for(String m : this.materials){
if(m.equals(material)){
return true;
}
}
return false;
}
This method simply iterates through each material in the materials
array, and returns true if it finds a material that exactly matches the material provided as a parameter. If it can’t find a match and reaches the end of the list, then the method will return false
.
We can then use this method in our cut()
method to determine whether the given material can be cut by this saw:
public boolean cut(int length, String material){
return length < this.getLength() && this.findMaterial(material);
}
This method will simply return true
if the length of the item to be cut is shorter than the saw and the material of the item is contained in the list of materials that can be cut by this saw. That covers the CrossCutSaw
class.
HackSaw
ClassThe HackSaw
class is very similar to the CrossCutSaw
class. However, instead of having a list of materials that it can cut, a HackSaw
can only cut a single material: metal. So, we can just hard-code that material into the saw’s class, as shown in the code below:
public class HackSaw extends Saw{
public HackSaw(int length){
super(length);
}
public String describe(){
return String.format("HackSaw Length: %d Material: metal", this.getLength());
}
public boolean cut(int length, String material){
return length < this.getLength() && material.equals("metal");
}
}
That’s all there is to it! At this point, we can check our code to confirm that the structure is correct.
Main
ClassFinally, we need to create a few methods in the Main
class to build the actual logic for our program. Before we build the main()
method, let’s look at the other two methods.
First, the readInput()
method should receive the name of a file as a string, and then return an array of tools that represents the tools specified in the given file. Also, looking at the UML diagram above, that method should be static, since it is underlined. In fact, all of the methods in the Main
class are static, so we can call them directly without instantiating an object using the Main
class.
import java.util.Scanner;
import java.nio.file.Paths;
import java.lang.Exception;
public class Main{
// other methods go here
public static Tool[] readInput(String filename){
try(
Scanner scanner = new Scanner(Paths.get(filename))
){
int num_tools = Integer.parseInt(scanner.nextLine());
Tool[] tools = new Tool[num_tools];
for(int i = 0; i < num_tools; i++){
String[] line = scanner.nextLine().split(" ");
int length = Integer.parseInt(line[1]);
if(line[0].equals("AdjustableWrench")){
int min_size = Integer.parseInt(line[2]);
int max_size = Integer.parseInt(line[3]);
tools[i] = new AdjustableWrench(length, min_size, max_size);
}else if(line[0].equals("OpenEndWrench")){
int size_one = Integer.parseInt(line[2]);
int size_two = Integer.parseInt(line[3]);
tools[i] = new OpenEndWrench(length, size_one, size_two);
}else if(line[0].equals("CombinationWrench")){
int size = Integer.parseInt(line[2]);
tools[i] = new CombinationWrench(length, size);
}else if(line[0].equals("CrossCutSaw")){
tools[i] = new CrossCutSaw(length, line[2]);
}else if(line[0].equals("HackSaw")){
tools[i] = new HackSaw(length);
}else{
throw new Exception("Unknown Tool: " + line[0]);
}
}
return tools;
}catch(Exception e){
System.out.println("Invalid Input");
return new Tool[0];
}
}
}
The readInput()
method looks quite complex, but it is actually really simple. First, it tries to open the file provided using a Try with Resources statement. Then, inside of that statement, it will read the first line of input and use that as an integer to create the array of tools. Then, using a For loop, it will read each line of input. Those lines can immediately be split into an array of tokens using the String.split()
method. Then, we simply use a bunch of If-Then-Else statements to determine which type of tool must be created based on the first token in the input. Then, we can use subsequent tokens as input to the constructors for each class, converting inputs to integers as needed.
If we can’t find a matching tool, we can simply throw a new exception with a helpful error message.
Finally, since we simply need to catch any possible exception, we’ll just add a catch statement for the generic exception and print the “Invalid Input” message before returning an empty array of tools.
Once we have an array of tools, we can also write the findTool()
method that will search the list of tools for a tool that can do the job. We could do so using this code:
public static Tool findTool(Tool[] tools, String query){
String[] query_parts = query.split(" ");
if(query_parts[0].equals("tighten")){
int clearance = Integer.parseInt(query_parts[1]);
int size = Integer.parseInt(query_parts[2]);
for(Tool t : tools){
if(t instanceof Wrench){
Wrench w = (Wrench)t;
if(w.tighten(clearance, size)){
return t;
}
}
}
return ??;
}else if(query_parts[0].equals("cut")){
int length = Integer.parseInt(query_parts[1]);
for(Tool t : tools){
if(t instanceof Saw){
Saw s = (Saw)t;
if(s.cut(length; query_parts[2]))){
return s;
}
}
}
return ??;
}else{
return ??;
}
}
This method is also a bit complex, but upon closer inspection it should be pretty straightforward. We simply parse the query into individual tokens. Then, we use the first token to determine if we are looking for a wrench or a saw. Next, we iterate through the entire list of tools, and inside of the Enhanced For loop, we check to see if the current tool is either a wrench or a saw, whichever type we are looking for. If it is, we cast it to that type, and then call the appropriate method. If that method returns true
, we know that the tool can perform the requested task, so we can just return it right there!
What if we get to the end and can’t find a tool that matches? This method still needs to return an object of the Tool
type. For arrays, we’ve been returning an empty array to show that the method was unsuccessful. Is there such as thing as an “empty object”?
It turns out there is! Java uses a special keyword called null
to represent an empty object. So, we can just return null
anywhere we aren’t sure what to return, and we’ll use that value in our main()
method to determine whether we found a tool or not.
public static Tool findTool(Tool[] tools, String query){
String[] query_parts = query.split(" ");
if(query_parts[0].equals("tighten")){
int clearance = Integer.parseInt(query_parts[1]);
int size = Integer.parseInt(query_parts[2]);
for(Tool t : tools){
if(t instanceof Wrench){
Wrench w = (Wrench)t;
if(w.tighten(clearance, size)){
return t;
}
}
}
return null;
}else if(query_parts[0].equals("cut")){
int length = Integer.parseInt(query_parts[1]);
for(Tool t : tools){
if(t instanceof Saw){
Saw s = (Saw)t;
if(s.cut(length, query_parts[2])){
return s;
}
}
}
return null;
}else{
return null;
}
}
Finally, we can simply write the main()
method:
public static void main(String[] args){
if(args.length != 1){
System.out.println("Invalid Input");
return;
}
Tool[] tools = readInput(args[0]);
if(tools.length == 0){
return;
}
Scanner scanner = new Scanner(System.in);
String query = scanner.nextLine();
Tool t = findTool(tools, query);
if(t != null){
System.out.println(t.describe());
}else{
System.out.println("Invalid Tool");
}
}
In the main()
method, we check to make sure that we’ve received exactly one command-line argument. If so, we pass that argument to the readInput()
method to read from the input file and produce an array of tools. If that array is empty, we know that we failed to read the input file correctly, so we should simply return.
If the array is populated, then we must read input from the terminal. So, we’ll read a user’s query, and then pass that query to the findTool()
method along with the array of tools. As a reminder, try with resources should NEVER be used when reading from System.in
.
If the findTool()
method returns anything other than null
, we know that we found a tool and should print the tool’s description to the terminal. Otherwise, we can do nothing since we are at the end of the program.
There we go! This is a very simple program, but it helps demonstrate the power of using inheritance in our programs to represent real-world objects that are closely related to each other.
In this chapter, we discovered how we can build class hierarchies through the use of inheritance between our classes. Using that technique, we can share attributes and methods from parent classes to child classes, minimizing repeated code.
In addition, we can take advantage of polymorphism to treat instances of child classes as instances of their parent class, and call methods and attributes inherited from the parent class, regardless of the type of the object we are using.
As we continue to build more and more complex programs, the ability to represent not only objects in the real world, but also the relationships and commonalities between those objects, will prove to be a very useful technique.