在前面的章节,我们已经定义了Person类
class Person{private String name;private int age;public String getName(){...}public int getAge(){...}public void setName(String name){..}public void setAge(int age){...}}
现在假设要定义一个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){.}
}
对比Person 和 Student 的字段和方法,只是Student多了一个字段score和对应的get、set方法。
能不能在Student中不写重复的代码 ?
这个时候继承就是派上用场了。
继承是面向对象编程中,非常强大的一种机制,它首先可以复用代码。当我们让Student从Person类中继承后,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出现在Shape 的permits列表中。
这种 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。
究其原因,是因为,Student是Person的一种,它们是is关系,而Student并不是Book。实际上Student和Book关系是has关系。
具有has关系不应该使用继承,而是使用组合。即Student持有一个Book。
public class Student extends Person{
private Book book;
private int score;
}
因此,继承是is关系,组合是has关系。
练习:
定义PrimaryStudent 从Student继承,并新增一个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判断 - 子类和父类关系是
is,has关系不能用继承
