继承详解13
  cnCTZTo8tgOC 2023年11月02日 31 0

我们已经定义了​​Person​​类:

class Person {
private String name;
private int age;

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

现在,假设需要定义一个​​Student​​类,字段如下:

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) { … }
}

仔细观察,发现​​Student​​类包含了​​Person​​类已有的字段和方法,只是多出了一个​​score​​字段和相应的​​getScore()​​、​​setScore()​​方法。

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

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

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

Java使用​​extends​​关键字来实现继承:

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 {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;

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

可见,通过继承,​​Student​​只需要编写额外的功能,不再需要重复代码。

 注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

在OOP的术语中,我们把​​Person​​称为超类(super class),父类(parent class),基类(base class),把​​Student​​称为子类(subclass),扩展类(extended class)。

继承树

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

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


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


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

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

类似的,如果我们定义一个继承自​​Person​​的​​Teacher​​,它们的继承树关系如下:

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


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

protected

继承有个特点,就是子类无法访问父类的​​private​​字段或者​​private​​方法。例如,​​Student​​类就无法访问​​Person​​类的​​name​​和​​age​​字段:

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

class Student extends Person {
public String hello() {
return "Hello, " + name; // 编译错误:无法访问name字段
}
}

这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把​​private​​改为​​protected​​。用​​protected​​修饰的字段可以被子类访问:

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

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

因此,​​protected​​关键字可以把字段和方法的访问权限控制在继承树内部,一个​​protected​​字段和方法可以被其子类,以及子类的子类所访问,后面我们还会详细讲解。

super

​super​​关键字表示父类(超类)。子类引用父类的字段时,可以用​​super.fieldName​​。例如:

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

实际上,这里使用​​super.name​​,或者​​this.name​​,或者​​name​​,效果都是一样的。编译器会自动定位到父类的​​name​​字段。

但是,在某些时候,就必须使用​​super​​。我们来看一个例子:

// super

 Run

运行上面的代码,会得到一个编译错误,大意是在​​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); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}

这样就可以正常编译了!

因此我们得出结论:如果父类没有默认的构造方法,子类就必须显式调用​​super()​​并给出参数以便让编译器定位到父类的一个合适的构造方法。

这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

正常情况下,只要某个class没有​​final​​修饰符,那么任何类都可以从该class继承。

从Java 15开始,允许使用​​sealed​​修饰class,并通过​​permits​​明确写出能够从该class继承的子类名称。

例如,定义一个​​Shape​​类:

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

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

public final class Rect extends Shape {...}

是没问题的,因为​​Rect​​出现在​​Shape​​的​​permits​​列表中。但是,如果定义一个​​Ellipse​​就会报错:

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

原因是​​Ellipse​​并未出现在​​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​​,因此,它拥有​​Person​​的全部功能。​​Person​​类型的变量,如果指向​​Student​​类型的实例,对它进行操作,是没有问题的!

这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。

向上转型实际上是把一个子类型安全地变为更加抽象的父类型:

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

注意到继承树是​​Student > Person > Object​​,所以,可以把​​Student​​类型转型为​​Person​​,或者更高层次的​​Object​​。

向下转型

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

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

如果测试上面的代码,可以发现:

​Person​​类型​​p1​​实际指向​​Student​​实例,​​Person​​类型变量​​p2​​实际指向​​Person​​实例。在向下转型的时候,把​​p1​​转型为​​Student​​会成功,因为​​p1​​确实指向​​Student​​实例,把​​p2​​转型为​​Student​​会失败,因为​​p2​​的实际类型是​​Person​​,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

因此,向下转型很可能会失败。失败的时候,Java虚拟机会报​​ClassCastException​​。

为了避免向下转型出错,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

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; // 一定会成功
}

从Java 14开始,判断​​instanceof​​后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:

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

可以改写如下:

// instanceof variable:

 Run

这种使用​​instanceof​​的写法更加简洁。

区分继承和组合

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

考察下面的​​Book​​类:

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

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

class Student extends Book {
protected int score;
}

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

究其原因,是因为​​Student​​是​​Person​​的一种,它们是is关系,而​​Student​​并不是​​Book​​。实际上​​Student​​和​​Book​​的关系是has关系。

具有has关系不应该使用继承,而是使用组合,即​​Student​​可以持有一个​​Book​​实例:

class Student extends Person {
protected Book book;
protected 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("小红", 20, 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 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; }
}


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

上一篇: 多态 下一篇: Java集合
  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  LxKByvFwtHdi   2023年11月02日   41   0   0 主键字段spring
  lh6O4DgR0ZQ8   2023年11月22日   25   0   0 Memory字段sed
  onyKAAZLmqqe   2023年11月02日   49   0   0 System操作符子类
  cnCTZTo8tgOC   2023年11月02日   32   0   0 父类字段子类
  cnCTZTo8tgOC   2023年11月02日   52   0   0 ide子类覆写
  LxKByvFwtHdi   2023年11月02日   40   0   0 主键字段外键
cnCTZTo8tgOC
作者其他文章 更多

2023-11-02

2023-11-02

2023-11-02

2023-11-02

2023-11-02

2023-11-02

2023-11-02