Appearance
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