在前面的章节,我们已经定义了Person

  1. class Person{
  2. private String name;
  3. private int age;
  4. public String getName(){...}
  5. public int getAge(){...}
  6. public void setName(String name){..}
  7. public void setAge(int age){...}
  8. }

现在假设要定义一个Student类,如下

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

    public String getName(){...}
    public int getAge(){.}
    public int getScore(){.}

    public void setName(String name){.}
    public void setAge(int age){.}
    public void setScore(int score){.}
}

对比PersonStudent 的字段和方法,只是Student多了一个字段score和对应的get、set方法。

能不能在Student中不写重复的代码 ?

这个时候继承就是派上用场了。

继承是面向对象编程中,非常强大的一种机制,它首先可以复用代码。当我们让StudentPerson类中继承后,Student就已经拥有了Person类中所有的功能,我们只需要为Student编写新增的功能.

Java 用extends关键字来实现继承

class Student extends Person{
    private int score;

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

注意:子类自动获取得了父类的所有字段,严禁定义与父类重名的字段!
在OOP的术语中,我们把Person称为超类(super class)、父类(parent class)、基类(base class)
Student称为子类(subclass),扩展类(extended class)

继承树

注意到我们在定义Person类时,没有写extends。在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。

Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类,只有Object特殊,它没有父类。

protected

继承有个特点,就是子类无法访问父类的private字段或private方法,这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们可以把private改成protected。用protected修饰的字段可以被子类访问

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

class Student extends Person {
    public String hello(){
        return "hello" + name;
    }
}

因此,protected关键字可以把字段和方法的访问权限控制在继承树内部。一个protected字段和方法可以被其子类,以及子类的子类所访问。

super

super关键字表示父类(超类)。子类调用父类的字段时,可以使用super.fieldName

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

实际上,这里使用super.name,或者this.name,或者name,效果都是一样的,编译器会自动定位到父类的name字段。
但是,在某些时候,就必须使用super

public class Main{
    public static void main(String[] args) {
        Student s = new Student("xiao ming",12, 90);
    }
}

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;
    }
}

运行上面的代码,会得到一个编译错误,大意是在student的构造方法中,无法调用 Person的构造方法.
这是因为在java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们加一句super(); 所以,Student类的构造方法实际上是这样:

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

但是Person类并没有无参的构造方法,因此,编译失败。

解决方法是调用Person类存在的某个构造方法。

class Student extends Person{
    protected int score;
    public Student(String name,int age,int score){
        super(name,age);
        this.score;
    }
}

因此我们得出结论:

如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器自动定位到一个合适的构造方法。
这里还引出另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

正常情况下,只要class没有final修饰符,那么任何类都可以从该class继承。
从Java15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。
例如:定义一个Shape

public sealed Shape permits Rect,Cirecle,Triangle{

}

上述Shape类就是一个sealed类,它只允许指定的3个类继承它。如果写:

public final class Rect extends Shape{ ...}

是没问题的,因为Rect出现在Shapepermits列表中。
这种 sealed类主要用于一些框架,防止继承被滥用。
sealed类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview--source 15

向上转型

如果一个变量的引用类型是Student,那么它可以指向一个Student类型的实例:

Student s = new Student();

如果一个变量的引用类型是Person,那么它可以指向一个Person类型的实例

Person p = new Person();

现在问题来了,如果Student是从Person类中继承下来的,那么一个引用类型为Person的变量,能否指向Student类型的实例

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

测试一下就可以发现这种指向是允许的!
这是因为Student继承自Person,Student拥有Person的所有字段和功能。Person类型的变量如果指向Student类型的实例,对它进行操作,是没有问题的。
这种把一个子类类型安全的变为父类类型的赋值,被称为向上转型(upcasting)
向上转型实际上是把一个子类型安全地变为更加抽象的父类类型:

Student s = new Student();
Person p = s;
Object o1 = p;
Object o2 = s;

向下转型

和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。

Person p1 = new Student();
Person p2 = new Person();

Student s1 = (Student) p1;
Student s2  =(Student) p2; //error

p2 实际类型是Person,不能把父类变成子类,因为子类功能比父类功能多,多的功能无法凭空变出来。

因此向下转型可能会失败,为了避免向下转型失败,Java提供了instanceof操作符,可以先判断一个实例是不是属于某种类型。

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


Person p1 = new Student();

System.out.println(p1 instanceof Person);//true
System.out.println(p1 instanceof Student);//true
System.out.println(p1 instanceof Object);//true


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

instanceof实际上判断 一个变量所指向的实例是否是指定类型,或者这个类型的子类,如果是一个引用类型的实例是null,那么对任何instanceof的判断都为false
利用instanceof,在向下转型前可以判断:

Person p = new Student();
if(p instanceof Student){
    Student s= (Student) p;
}

上述代码从Java14开始,判断instanceof后,可以直接转型为变量,避免再次强制转型。

Person p = new Student();
if(p instanceof Student) {
    System.out.println(p.getScore());
}

区分继承和组合

在使用继承时,要注意逻辑一致性。

考察下面这个类:

public class Book {
    private String name;
    public String getName(){};
    public setName(){}
}

这个Book类也有name字段,那我们能不能让Student继承自Book

显然,这从逻辑上讲是不合理的。Student应该从Person继承,而不是Book

究其原因,是因为,StudentPerson的一种,它们是is关系,而Student并不是Book。实际上StudentBook关系是has关系。
具有has关系不应该使用继承,而是使用组合。即Student持有一个Book

 public class Student extends Person{
     private Book book;
    private int score;
 }

因此,继承是is关系,组合是has关系。

练习:
定义PrimaryStudentStudent继承,并新增一个grade字段。

public class Main{
    public static void main(String[] args) {
        Person p = new Person("小明",12);
        Student s = new Student("小红",13,99);
        //todo: 定义PrimaryStudent,从Student 继承,新增grade字段

        Student ps = new PrimaryStudent("小军",9,100,5);

        System.out.println(ps.getScore());
    }
}

class Person{
    protected String name;
    protected int age;

    public get/set(){}

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


class Student extends Person{
    protected int score;
    public get/set(){}

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



//do
class PrimaryStudent extends Student{
    private int grade;
    public get/set(){}

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

}

小结:

  • 继承是面向对象编程的一种强大的代码利用方式
  • Java只允许单继承,所有类的最终根类是Object
  • Protected允许子类访问父类的字段和方法
  • 子类的构造方法可以通过super()调用父类的构造方法
  • 可以安全的向上转型成更抽象的类
  • 可以强制向下转型,最好借助instanceof判断
  • 子类和父类关系是ishas关系不能用继承