Skip to content

Inheritance

In the previous chapters, we have defined the Person class:

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

  public String getName() {...}
  public void setName(String name) {...}
  public int getAge() {...}
  public void setAge(int age) {...}
}

Now, suppose you need to define a Student class with the following fields:

java
class Student {
    private String name;
    private int age;
    private int score;

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
    public int getScore() { … }
    public void setScore(int score) { … }
}

Observing carefully, we found that the Student class contains the existing fields and methods of Person class, except for an extra score field and the corresponding getScore() and setScore() methods.

Can I avoid writing duplicate code in Student ?

At this time, inheritance comes in handy.

Inheritance is a very powerful mechanism in object-oriented programming. It can first reuse code. When we let Student inherit from Person , Student gets all the functions of Person , and we only need to write new functions for Student .

Java uses the extends keyword to implement inheritance:

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

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
}

class Student extends Person {
    // Do not repeat name and age fields/methods,
    // Just define the new score field/method:
    private int score;

    public int getScore() { … }
    public void setScore(int score) { … }
}

It can be seen that through inheritance, Student only needs to write additional functions and no longer needs to repeat code.

Notice

The subclass automatically obtains all the fields of the parent class. It is strictly forbidden to define fields with the same name as the parent class!

In OOP terminology, we call Person a super class, parent class, and base class, and Student a subclass and extended class.

Inheritance Tree

Notice that when we defined Person , we did not write extends . In Java, if there is no explicitly written extends class, the compiler will automatically add extends Object . Therefore, any class, except Object , will inherit from a certain class.

The following figure is the inheritance tree of Person and Student :

┌───────────┐
│  Object   │
└───────────┘


┌───────────┐
│  Person   │
└───────────┘


┌───────────┐
│  Student  │
└───────────┘

Java only allows a class to inherit from a class. Therefore, a class has only one parent class. Only Object is special, it has no parent class.

Similarly, if we define a Teacher inherits from Person , their inheritance tree relationship is as follows:

       ┌───────────┐
       │  Object   │
       └───────────┘


       ┌───────────┐
       │  Person   │
       └───────────┘
          ▲     ▲
          │     │
          │     │
┌───────────┐ ┌───────────┐
│  Student  │ │  Teacher  │
└───────────┘ └───────────┘

protected {#protected

One characteristic of inheritance is that subclasses cannot access the private fields or private methods of the parent class. For example, the Student class cannot access name and age fields of Person class:

java
class Person {
    private String name;
    private int age;
}

class Student extends Person {
    public String hello() {
        return "Hello, " + name; // Compile error: name field cannot be accessed
    }
}

This weakens the role of inheritance. In order to allow subclasses to access the fields of the parent class, we need to change private to protected . Fields modified with protected can be accessed by subclasses:

java
class Person {
    protected String name;
    protected int age;
}

class Student extends Person {
    public String hello() {
        return "Hello, " + name; // OK!
    }
}

Therefore, the protected keyword can control the access rights of fields and methods within the inheritance tree. A protected field and method can be accessed by its subclasses and subclasses of subclasses. We will explain in detail later.

super

The super keyword represents the parent class (super class). When a subclass refers to a field of a parent class, super.fieldName can be used. For example:

java
class Student extends Person {
    public String hello() {
        return "Hello, " + super.name;
    }
}

In fact, using super.name , or this.name , or name here has the same effect. The compiler will automatically locate the name field of the parent class.

However, at some point, you have to use super . Let's look at an example:

java
public class Main {
    public static void main(String[] args) {
        Student s = new Student("bob", 12, 89);
    }
}

class Person {
    protected String name;
    protected int age;

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

class Student extends Person {
    protected int score;

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

When you run the above code, you will get a compilation error, which means that Person constructor cannot be called in the Student constructor.

This is because in Java, the first line of the constructor of any class must be to call the constructor of the parent class. If the constructor of the parent class is not explicitly called, the compiler will automatically add super(); for us. Therefore, the constructor of Student class is actually as follows:

java
class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(); // Automatically call the constructor of the parent class
        this.score = score;
    }
}

However, the Person class does not have a parameterless constructor, so the compilation fails.

The solution is to call a constructor that exists in the Person class. For example:

java
class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // Call the constructor Person(String, int) of the parent class
        this.score = score;
    }
}

This will compile normally!

Therefore, we conclude that if the parent class does not have a default constructor, the subclass must explicitly call super() and give parameters to allow the compiler to locate an appropriate constructor of the parent class.

This also leads to another problem: that is, the subclass will not inherit any constructor method of the parent class. The default construction method of a subclass is automatically generated by the compiler and is not inherited.

Prevent inheritance

Under normal circumstances, as long as a class does not have the final modifier, any class can inherit from that class.

Starting from Java 15, it is allowed to use sealed to modify the class, and clearly write the name of the subclass that can inherit from the class through permits .

For example, define a Shape class:

java
public sealed class Shape permits Rect, Circle, Triangle {
    ...
}

The above Shape class is a sealed class, which only allows the specified 3 classes to inherit it. If you write:

java
public final class Rect extends Shape {...}

There is no problem, because Rect appears in Shape 's permits list. However, if you define an Ellipse an error will be reported:

java
public final class Ellipse extends Shape {...}
// Compile error: class is not allowed to extend sealed class: Shape

The reason is that Ellipse does not appear in Shape 's permits list. This sealed class is mainly used in some frameworks to prevent inheritance from being abused.

sealed classes are currently in preview status in Java 15. To enable it, the parameters --enable-preview and --source 15 must be used.

Upward transformation

If a reference variable is of type Student , then it can point to an instance of type Student :

java
Student s = new Student();

If a reference type variable is Person , then it can point to an instance of type Person :

java
Person p = new Person();

Now the question arises: If Student inherits from Person , can a variable with a reference type of Person point to an instance of the Student type?

java
Person p = new Student(); // ???

After testing, you can find that this kind of pointing is allowed!

This is because Student inherits from Person , therefore, it has all the functions of Person . If a variable of the Person type points to an instance of the Student type, there is no problem in operating on it!

This type of assignment, which safely changes a subclass type to a superclass type, is called upcasting.

Upcasting actually safely transforms a subtype into a more abstract parent type:

java
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok

Note that the inheritance tree is Student > Person > Object , so the Student type can be converted to Person , or a higher-level Object .

Downward transformation

Contrary to upward casting, if a parent class type is forced to a subclass type, it is downcasting. For example:

java
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

If you test the above code, you can find:

Person type p1 actually points to the Student instance, and Person type variable p2 actually points to the Person instance. During downward transformation, the transformation of p1 into Student will succeed because p1 does point to Student instance. However, the transformation of p2 into Student will fail because the actual type of p2 is Person . The parent class cannot be changed into a subclass because the subclass It has more functions than the parent class, and many functions cannot be conjured out of thin air.

Therefore, the downward transition is likely to fail. When it fails, the Java virtual machine reports ClassCastException .

In order to avoid downcast errors, Java provides the instanceof operator, which can first determine whether an instance is of a certain type:

java
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

instanceof actually determines whether the instance pointed to by a variable is of the specified type, or a subclass of this type. If a reference variable is null , then any instanceof evaluation will be false .

Using instanceof , you can judge before downward transformation:

java
Person p = new Student();
if (p instanceof Student) {
    // Only when it is judged successful will it transform downward.:
    Student s = (Student) p; // will definitely succeed
}

Starting from Java 14, after judging instanceof , you can directly transform it into a specified variable to avoid forced transformation again. For example, for the following code:

java
Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}

It can be rewritten as follows:

java
public class Main {
    public static void main(String[] args) {
        Object obj = "hello";
        if (obj instanceof String s) {
            // You can use the variable s directly:
            System.out.println(s.toUpperCase());
        }
    }
}

This way of writing using instanceof is more concise.

Distinguish between inheritance and composition

When using inheritance, we must pay attention to logical consistency.

Consider the following Book`` class:

java
class Book {
    protected String name;
    public String getName() {...}
    public void setName(String name) {...}
}

This Book class also has a name field, so can we let Student inherit from Book ?

java
class Student extends Book {
    protected int score;
}

Obviously, logically this is unreasonable, Student should not inherit from Book , but from Person .

The reason is that Student is a type of Person , they are in an is relationship, and Student is not Book . In fact, the relationship between Student and Book is a has relationship.

If you have a has relationship, you should not use inheritance, but use combination, that is, Student can hold a Book instance:

java
class Student extends Person {
    protected Book book;
    protected int score;
}

Therefore, inheritance is an is relationship, and composition is a has relationship.

Practise

Define PrimaryStudent , inherit from Student , and add a grade field:

java
public class Main {
    public static void main(String[] args) {
        Person p = new Person("bob", 12);
        Student s = new Student("john", 20, 99);
        // TODO: Define PrimaryStudent, inherit from Student, add grade field:
        Student ps = new PrimaryStudent("jack", 9, 100, 5);
        System.out.println(ps.getScore());
    }
}

class Person {
    protected String name;
    protected int age;

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

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

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

class Student extends Person {
    protected int score;

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

    public int getScore() { return score; }
}

class PrimaryStudent {
    // TODO
}

Summary

Inheritance is a powerful method of code reuse in object-oriented programming;

Java only allows single inheritance, and the final root class of all classes is Object ;

protected allows subclasses to access the fields and methods of the parent class;

The constructor of a subclass can call the constructor of the parent class through super() ;

Can be safely upcast to more abstract types;

You can force downward transformation, it is best to use instanceof to judge

Inheritance has loaded