Superclasses and Subclasses
Hierarchies
Java classes are structured into hierarchies, where each class is either an ancestor or descendant of one or more other classes. This hierarchy defines a set of inherited properties and behaviors, much like our own familial lineages define certain traits about ourselves.
As we dive more into the concept of inheritance and class extension, we'll see just how this works. We'll see ways in which we can define custom hierarchies, as well as parent-child relationships that exist amongst built-in Java classes.
A Parent to them All
At the top of the hierarchy of classes sits the Object
class. All classes, whether they are built into the language or defined by another programmer (i.e., you!), implicitly inherit properties from the Object
class. We say this extension is implicit because you, as the programmer, do not to include any special keywords. In fact, you cannot prevent your class from inheriting from the Object
class even if you wanted to.
Passed Down
From the Object
class, all Java classes inherit the following methods.
clone()
equals(Object obj)
finalize()
getClass()
hashcode()
notify()
notifyAll()
toString()
wait()
wait(long timeout)
wait(long timeout, int nanos)
Many of these are beyond the scope of this course. However, we've already used (and overridden, which we'll talk more about later) a couple of these: equals(Object obj)
and toString()
. You can read more about the behavior and uses of these methods in the documentation.
Any class your write or use in Java will have access to these methods, and programmers have the option of overriding their behavior (as we've done with toString()
). You haven't written any of these methods, but have access to them because they are inherited from the Object
class.
Relationships
In object oriented programming, we typically encounter is-a or has-a relationships. We'll be reviewing both, but it determines whether inheritance is the right design decision or not.
When deciding if a class we're writing is a good candidate for inheritance, we need to ask ourselves a simple question. Is the class we're writing a type of some other potential parent class we've already written? Let's clarify.
Suppose we write an Animal
class, and later we're designing a Tiger
class. The question we need to ask is: Is a Tiger
an Animal
? Clearly, the answer is yes. So, yes, the Tiger
class is a good candidate to inherit from the Animal
class. If the answer is no, then perhaps inheritance is not the best choice.
To Inherit or Not to Inherit
Let's quickly review the is-a relationship. Suppose we've written a BankAccount
class. Now, as we're writing CheckingAccount
and SavingsAccount
classes, we ask ourselves:
Are checking accounts and savings accounts bank accounts?
In both cases, the answer is yes. CheckingAccount
and SavingsAccount
should inherit from BankAccount
. They are both types of bank accounts.
Now, suppose we have an AccountHolder
class that keeps track of the information of the owner of a bank account. We, again, ask ourselves:
Is an account holder a bank account?
Eh, not really. But, a bank account has an AccountHolder
. In this case, the AccountHolder
should be an instance variable of the BankAccount
class, but should not extend BankAccount
.
Parents and Children
When we talk about class inheritance and hierarchies, we need a reliable way in which we can refer to each component of the relationship. A superclass refers to the class from which other classes inherit. It is the parent in the relationship, and is often referred to as the parent class. A subclass refers to the class that inherits from another class. It is the child in the relationship, and is often referred to as the child class.
Naturally, a class can be simultaneously a parent and a child (just as we, as humans, have parents and might one day also have children of our own). A subclass can have only one direct parent class; however, parent classes might (and often do) have many children who extend it.
Parents
When we create a class intended to serve as a parent, we do not need to do anything differently from what we've always done. Suppose we're writing a Vehicle
class that will serve as our parent. The Vehicle
class will store general properties and support general actions that might be common of all vehicles.
public class Vehicle {
protected int wheels;
protected int capacity;
public Vehicle(int wheels, int capacity) {
this.wheels = wheels;
this.capacity = capacity;
}
public void drive() {
// implementation details here
}
}
Surely, there would be more to this class, but we can leave it here for now. This is purely for demonstration purposes. You'll notice that our instance variables have been declared as protected
, which is different from the typical private
access modifier we've used. Protected variables are visible (i.e., accessible) from any subclasses, while private variables are not (subclasses can still access private instance variables of their parents using the parent-provided getter/setter methods).
Children
Our children classes can be specific types of vehicles: cars, motorcycles, busses, trucks, etc. These will have similar basic functionality, but their implementations might need to be altered or enhanced in some way. Enter inheritance and the Java keyword extends
.
We use the extends
keyword to create a class that inherits all properties of its parent class plus whatever additional properties and functionalities we include in the child class. Suppose we wanted to write a Motorcycle
class.
public class Motorcycle extends Vehicle {
public Motorcycle(int capacity) {
super(2, capacity); // more on the super keyword later
}
public void popWheelie() {
// implementation details here
}
}
Our Motorcycle
class will now include everything the Vehicle
class included, plus any additional things we choose to add to it (i.e., popping a wheelie). We're enhancing the functionality of the Vehicle
class through the Motorcycle
class. Even though we haven't created a drive
method in our Motorcycle
class, we can use it because it exists in the Vehicle
class. It's as if we've copied and pasted everything from Vehicle
into Motorcycle
.
Working from the Child
There are a few things we can do from our child classes, both within the context of the child class itself and in respect to the parent class. Let's take a look at few options.
Overriding Methods
We've already seen the concept of overriding methods with the toString
method. Now, we'll take a look at overriding any method of a superclass.
public class MySuperClass {
public void sayHello() {
System.out.println("Hello, there!");
}
}
The sayHello
method will simply print Hello, there!
to the console. A subclass will have access to this method, but can also change its behavior by overriding it using the @Override
keyword.
public class MySubClass extends MySuperClass {
@Override
public void sayHello() {
System.out.println("Hey!");
}
}
Now, the sayHello
method will print either Hello, there!
or Hey!
to the console depending on the calling class.
MySuperClass superClass = new MySuperClass();
MySubClass subClass = new MySubClass();
superClass.sayHello(); // prints Hello, there!
subClass.sayHello(); // prints Hey!
Referencing the Parent
We can access methods, constructors, and variables (depending on their visibilities) of the parent class from the child class. All we need to do is use the super keyword. Consider the example from last section.
/*
* Parent class.
*/
public class MySuperClass {
public void sayHello() {
System.out.println("Hello, there!");
}
}
/*
* Child class.
*/
public class MySubClass extends MySuperClass {
@Override
public void sayHello() {
System.out.println("Hey!");
}
}
If we want to specifically access the sayHello
method of the parent class, we use the super
keyword.
super.sayHello(); // calls the sayHello method of MySuperClass
Let's take a look at how this might work in practice.
We need to revisit constructors for a minute, since we can also call the constructors of parent classes (there might be some setup required in the parent class) by using the super
keyword.
super(); // pass in arguments to super() like we would for any other constructor
I omitted this in our initial discussion of constructors because we first needed to gain an understanding of inheritance and class hierarchies. You'll also get a look at another use of the this
keyword.
Note that calling super
needs to take place as the first line in the child constructor.
// assume we've already written the Vehicle class
public class Bus extends Vehicle {
public Bus(int wheels, int capacity) {
super(wheels, capacity); // calls the parent constructor
// child-specific setup
}
}
As we've seen, there are certain fields in parent classes that are not inherited by the parent class. Typically, these are private methods or instance variables. Remember the getter and setter methods we looked at a couple sections back? Well, this is their time to shine.
Getter and setter methods are public, and therefore accessible. We can use these to access private data in not only parent classes, but any class that defines them.
Referencing the Child
While it is often only used for clarity, the this
reference specifies that the method (or constructor or variable) that we are referring to is declared and defined in the class from which we're calling it (i.e., this class).
public class MyClass {
private int x;
public MyClass() {
this(-1);
}
public MyClass(int x) {
this.x = x;
}
}
There are a couple examples we'll need to look at here. The simplest is the variable assignment in the second constructor (yes, classes can have more than one constructor).
this.x = x; // assign x value into this.x value
// x is the parameter of the constructor, whereas this.x
// is the instance variable defined at the class level
The this
reference is to clarify to the constructor that the value stored in the variable x
(the one within the scope of the constructor) is being assigned the variable x
(declared at the class level). The this reference implies that it is an instance member.
The next example is the call to this
as a method (well, really as a constructor). What we're doing here is calling another constructor of this class and passing in the argument -1
. Just like super
, calling other constructors in this manner must take place on the first line in another constructor.
this(-1); // call constructor of this class, passing in -1 as argument
Polymorphism
Polymorphism is a powerful concept in Java that allows us to manipulate many different objects (provided they share a base implementation) in one program.
We can write a singlet block of code, rather than blocks of code for each object we need to support. Consider the follow structure.
public class Question {
// base implementation for Question
public void askQuestion() {
// details here
}
}
public class MultipleChoice extends Question {
// extended implementation for MultipleChoice
public void askQuestion() {
// details here
}
}
public class FillInTheBlank extends Question {
// extended implementation for FillInTheBlank
public void askQuestion() {
// details here
}
}
Each class is a Question
(by extension) and each class has its own implementation of askQuestion
. If we need to manipulate objects of each of these classes, we can do so in a single setting without having to write custom code for Question
, MultipleChoice
, and FillInTheBlank
.
public class Tester {
public boolean isCorrect(Question q) {
q.askQuestion();
// get answer from user and check if it is right
// return true or false
}
}
The important part here is that isCorrect
accepts a Question
object as input, but we can pass in any of our Question
classes (Question
, MultipleChoice
, or FillInTheBlank
). Java will determine what type of object q
is and direct the method call appropriately.
Question q1 = new Question();
boolean b1 = isCorrect(q1);
Question q1 = new MultipleChoice();
boolean b2 = isCorrect(q2);
In this case, Java will figure out that it should call the askQuestion
method of the Question
class for q1
and the askQuestion
method of the MultipleChoice
class for q2
.
Object Casting
We've gone over casting, but it works a little differently with objects. Upcasts occur automatically, while we, the programmers, must downcast manually. Upcasting outlines two things for us:
The methods that are available, which is governed by the variable type.
The version of the method that is called, which is governed by the object type.
Downcasting might like a little chaotic at first glance. Suppose we have two classes: Employee
and Teacher
. In the Employee
class, there is just one method: work
. The Teacher
class inherits this method, but also provides one of its own: teach
.
Employee patrick = new Employee();
Employee susan = new Teacher(); // Teacher extends from Employee
/*
* The variable type of susan is Employee, so susan only has access to
* methods that were defined in the Employee class. So, how can we call
* the teach method with the susan object?
*/
((Teacher) susan).teach(); // cast susan to a Teacher
Last updated
Was this helpful?