Inheritance



1. Introduction

Inheritance is a convenient method of sharing code and data between related classes. When one class inherits from another, we say there is an is-a relationship between them. When one class inherits from another, it receives copies of relevant instance variables and methods without you needing to rewrite the code for them.

In these notes, we’ll go over the basics of inheritance by using a simple example.

2. Doing Things the Hard Way

Imagine that you are working on a Java program and you need a class to store some basic information about a person, such as their name and age. You would likely come up with something like this:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void birthday() {
        this.age++;
    }

    public String toString() {
        return this.name + " (Age: " + this.age + ")";
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }
}

It is a simple class with a basic constructor, a birthday method to change the age, a toString, and a few getter methods for the name and age.

Now, later on, while extending the program you also need a class to store information about a student. In your mind, a student is-a Person, so they will have a lot in common. So much so that you find yourself copying a lot of code from Person into your new Student class:

public class Student {
    private String name;
    private int age;
    private String major;

    public Student(String name, int age, String major) {
        this.name = name;
        this.age = age;
        this.major = major;
    }

    public void birthday() {
        this.age++;
    }

    public String toString() {
        return this.name + " (Age: " + this.age + ")" + " (Major: " + this.major + ")";
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }

    public String getMajor() {
        return this.major;
    }
}

The code is so similar that this is basically the Student class with just a little bit of extra code added to handle the major.

Situations like this come up a lot in development, and there is a much better way to handle it than simply duplicating code.

3. Doing Things the Better Way

Remember, we previously noticed that a student is-a person. Given this, we can tell Java this and have the Student class automatically receive data and code from the Person class. To do this we use the extends keyword. Let’s take a look:

public class Student extends Person {
    private String major;

    public Student(String name, int age, String major) {
        super(name, age);
        this.major = major;
    }

    public String toString() {
        return super.toString() + " (Major: " + this.major + ")";
    }

    public String getMajor() {
        return this.major;
    }
}

This code is much smaller but is functionally identical to the previous Student class we wrote. Here’s another version of the same thing, but with comments to talk you through it:

// Notice how we use "extends" to tell Java that Student should inherit from Person.
public class Student extends Person {

    /*
     * Here we declare a new instance variable, major, to store the major. We don't
     * need to redeclare name and age; Student inherits those for free.
     */
    private String major;

    // We don't, however, inherit constructors. So we need to provide one.
    public Student(String name, int age, String major) {
        /*
         * This next line is interesting. We can call the constructor from
         * Person using super(), so we don't need to repeat that code.
         */
        super(name, age);

        /*
         * But, that parent constructor doesn't initialize major for us, since that only
         * exists here in Student. So we do that here.
         */
        this.major = major;
    }

    /*
     * We provide a new version of toString because we want ours to be different
     * than the one in Person. Notice, however, that this toString() does call the
     * toString() from Person and use its result.
     */
    public String toString() {
        return super.toString() + " (Major: " + this.major + ")";
    }

    /*
     * We need our own getter for major because Person doesn't have one.
     */
    public String getMajor() {
        return this.major;
    }
}

4. Terminology and Limitations

Let’s take a look at some terminology around inheritance so we have a common vocabulary to talk about it.

A superclass is the parent class that is extended. (In this case, Person is the parent or superclass.)

A subclass is a child that inherits from the parent. (In this case, Student is the child or subclass.)

There are also some limitations when using inheritance:

5. Superclass and Subclass Compatibility

In a program, subclasses can be stored in any place the superclass can. A variable of type T can hold an object of type T or any subclass of T. This means, for example, that a Person variable can have a Student object assigned to it. When this happens, however, you can’t access any methods or variables that only exist in the Student.

Person p = new Student("John Smith", 19, "InfoSys");
System.out.println(p.getName());
System.out.println(p);

Will print out:

John Smith
John Smith (Age: 19) (Major: InfoSys)

However, you can’t do this:

Person p = new Student("John Smith", 19, "InfoSys");
System.out.println(p.getMajor());

In this case, the call to getMajor() can’t happen because you’ve declared p to be a Person, and the Person class doesn’t have a getMajor() method. Even though the object stored in p is a Student, because the type of p is Person Java will not allow you to treat it like a Student.

6. Superclasses and Subclasses in Lists

You can store superclasses and their subclasses into the same arrays or lists. When you pull the items out, however, they will all have the same type as the superclass. To access subclasses as subclasses, you’ll need to cast.

To cast a variable is to manually force Java to treat it as a different type.

Here is an example of casting:

ArrayList<Person> bigList = new ArrayList<Person>();
bigList.add(new Person("Bob Jones", 45));
bigList.add(new Student("John Smith", 19, "InfoSys"));
bigList.add(new Person("Bridget Johnson", 22));
bigList.add(new Student("Muna Al-Mannai", 21, "CompSci"));

for (Person p : bigList) {
    System.out.println(p);
}

for (Person p : bigList) {
    // Get the name of the person
    String name = p.getName();

    // Check if p is a Student
    if (p instanceof Student) {
        /*
         * Since they are, we can cast the Person to Student. This allows us to call
         * getMajor() on it.
         */
        Student s = (Student) p;
        String major = s.getMajor();
        System.out.println(name + " is a Student with major " + major);
    } else {
        System.out.println(name + "is a Person");
    }
}